This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 3a443e9a1 fix(web): Segmented group type in create modal and
equal-height audit cards (#7367)
3a443e9a1 is described below
commit 3a443e9a15015b5290cf6533568c3de1606d9e07
Author: lizhimins <[email protected]>
AuthorDate: Thu Oct 29 15:20:41 2026 +0800
fix(web): Segmented group type in create modal and equal-height audit cards
(#7367)
Create Group modal: the message-type Select offered delay/transaction types
that do not apply to consumer groups, plus a delivery-order Select with a
global order the backends do not support. Use the same Segmented control as
the create-topic modal with only NORMAL/FIFO, and derive deliveryOrderType
from the chosen type so ordered groups still reach the broker as orderly.
Audit page: the summary statistic cards sized to their content, leaving the
success-rate card (one extra detail line) 44px taller than its row siblings.
Stretch every card in the summary and risk-insight rows (Row align="stretch"
plus Card height 100%) so each visual row is even.
---
.../pages/instance/__tests__/ConsumerPage.test.tsx | 24 +++++-----
web/src/pages/instance/consumer.tsx | 52 ++++++++--------------
web/src/pages/ops/AuditRiskInsights.tsx | 6 +--
web/src/pages/ops/AuditSummaryCards.tsx | 14 +++---
4 files changed, 39 insertions(+), 57 deletions(-)
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index c7fd83a45..bd8a2f406 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -643,7 +643,7 @@ describe('Consumer page', () => {
expect(await screen.findByText('资源命名与归属规则')).toBeInTheDocument();
});
- it('submits the canonical global delivery order type', async () => {
+ it('submits the partition-ordered delivery type for an ordered group', async
() => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const confirmSpy = vi.spyOn(Modal, 'confirm').mockImplementation((config)
=> {
void config.onOk?.();
@@ -656,27 +656,23 @@ describe('Consumer page', () => {
await waitFor(() => expect(createButton).toBeEnabled());
await user.click(createButton);
const dialog = await screen.findByRole('dialog');
- await user.type(within(dialog).getByLabelText('Group 名称'),
'cg-global-orders');
+ await user.type(within(dialog).getByLabelText('Group 名称'), 'cg-orderly');
- const dataTypeSelect = within(dialog).getByRole('combobox', { name:
'订阅组类型' });
- fireEvent.mouseDown(dataTypeSelect.parentElement!);
- await user.click(
- await screen.findByText('顺序消息', { selector:
'.ant-select-item-option-content' }),
- );
+ // 订阅组类型与创建 Topic 共用 Segmented,只提供普通/顺序两档,顺序组固定分区顺序
+ expect(within(dialog).queryByText('延迟消息')).not.toBeInTheDocument();
+ expect(within(dialog).queryByText('事务消息')).not.toBeInTheDocument();
+ await user.click(within(dialog).getByRole('radio', { name: '顺序消息' }));
+ expect(within(dialog).queryByRole('combobox', { name: '顺序类型'
})).not.toBeInTheDocument();
+ expect(within(dialog).queryByText('全局顺序')).not.toBeInTheDocument();
- const orderTypeSelect = within(dialog).getByRole('combobox', { name:
'顺序类型' });
- fireEvent.mouseDown(orderTypeSelect.parentElement!);
- await user.click(
- await screen.findByText('全局顺序', { selector:
'.ant-select-item-option-content' }),
- );
await user.click(within(dialog).getByRole('button', { name: /创\s*建/ }));
await waitFor(() =>
expect(consumerService.createConsumerGroup).toHaveBeenCalledWith(
expect.objectContaining({
- name: 'cg-global-orders',
+ name: 'cg-orderly',
subscriptionDataType: 'FIFO',
- deliveryOrderType: 'MESSAGES_ORDER',
+ deliveryOrderType: 'PARTITON_ORDER',
}),
),
);
diff --git a/web/src/pages/instance/consumer.tsx
b/web/src/pages/instance/consumer.tsx
index 35925203d..41b426e92 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -34,6 +34,7 @@ import {
Descriptions,
Statistic,
Radio,
+ Segmented,
InputNumber,
Typography,
Row,
@@ -122,6 +123,13 @@ const { Text } = Typography;
const UNKNOWN_LAG_COLOR = '#8c8c8c';
const UNAVAILABLE_LAG_LABEL = '不可用';
+// 消费组只区分普通/顺序:延迟与事务是 Topic 的消息类型;顺序组固定分区顺序(云 OpenAPI 只有 Orderly)
+const GROUP_TYPE_CARDS = [
+ { value: 'NORMAL', label: '普通消息', desc: '并发消费,吞吐优先,适用于绝大多数场景。' },
+ { value: 'FIFO', label: '顺序消息', desc: '按分区顺序消费,需搭配顺序 Topic 使用。' },
+];
+const ORDERED_DELIVERY_TYPE = 'PARTITON_ORDER'; // 拼写与后端
normalizeDeliveryOrderType、CSV 导入词表一致
+
/**
* The provider leaves delaySeconds at zero when the broker consume stats
carried no
* consumed-message timestamp (RocketMQMetadataProvider: "keep zeros") and
says so through
@@ -304,7 +312,7 @@ const ConsumerPageContent = ({
}>();
const [createModalOpen, setCreateModalOpen] = useState(false);
const [form] = Form.useForm();
- const [dataTypeValue, setDataTypeValue] = useState<string |
undefined>(undefined);
+ const createDataType = Form.useWatch('dataType', form);
const [resetModalOpen, setResetModalOpen] = useState(false);
const [resetGroup, setResetGroup] = useState<ConsumerGroup | null>(null);
const [resetTopic, setResetTopic] = useState<string>();
@@ -2502,7 +2510,6 @@ const ConsumerPageContent = ({
onCancel={() => {
setCreateModalOpen(false);
form.resetFields();
- setDataTypeValue(undefined);
}}
onOk={() => {
form
@@ -2525,8 +2532,9 @@ const ConsumerPageContent = ({
subscriptionMode: values.subscriptionMode,
consumeType: values.consumeType,
retryMaxTimes: values.retryMaxTimes,
- subscriptionDataType: values.dataType || 'NORMAL',
- deliveryOrderType: values.deliveryOrderType,
+ subscriptionDataType: values.dataType,
+ deliveryOrderType:
+ values.dataType === 'FIFO' ? ORDERED_DELIVERY_TYPE :
undefined,
subscribedTopics: [],
instanceId: selectedInstanceId,
});
@@ -2537,7 +2545,6 @@ const ConsumerPageContent = ({
message.success(`消费组 ${values.name} 创建成功`);
setCreateModalOpen(false);
form.resetFields();
- setDataTypeValue(undefined);
} catch {
message.error(t('consumer.createFailed'));
throw new Error(t('consumer.createFailed'));
@@ -2563,6 +2570,7 @@ const ConsumerPageContent = ({
subscriptionMode: 'Push',
consumeType: 'CLUSTERING',
retryMaxTimes: 16,
+ dataType: 'NORMAL',
}}
>
<Form.Item
@@ -2610,35 +2618,13 @@ const ConsumerPageContent = ({
<InputNumber min={0} max={128} style={{ width: '100%' }} />
</Form.Item>
- <Form.Item label="订阅组类型" name="dataType">
- <Select
- placeholder="选择消息类型"
- options={[
- { value: 'NORMAL', label: '普通消息' },
- { value: 'FIFO', label: '顺序消息' },
- { value: 'DELAY', label: '延迟消息' },
- { value: 'TRANSACTION', label: '事务消息' },
- ]}
- onChange={(val) => setDataTypeValue(val)}
- />
+ <Form.Item
+ label="订阅组类型"
+ name="dataType"
+ extra={GROUP_TYPE_CARDS.find((c) => c.value ===
createDataType)?.desc}
+ >
+ <Segmented options={GROUP_TYPE_CARDS.map(({ value, label }) => ({
value, label }))} />
</Form.Item>
-
- {dataTypeValue === 'FIFO' && (
- <Form.Item label="顺序类型" name="deliveryOrderType"
initialValue="PARTITON_ORDER">
- <Select
- options={[
- {
- value: 'PARTITON_ORDER',
- label: '分区顺序',
- },
- {
- value: 'MESSAGES_ORDER',
- label: '全局顺序',
- },
- ]}
- />
- </Form.Item>
- )}
</Form>
</Modal>
diff --git a/web/src/pages/ops/AuditRiskInsights.tsx
b/web/src/pages/ops/AuditRiskInsights.tsx
index c9afa7340..bfca512e7 100644
--- a/web/src/pages/ops/AuditRiskInsights.tsx
+++ b/web/src/pages/ops/AuditRiskInsights.tsx
@@ -235,9 +235,9 @@ const AuditRiskInsights = ({ summary, records, loading }:
Props) => {
}
style={{ marginBottom: 16 }}
>
- <Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
+ <Row gutter={[12, 12]} align="stretch" style={{ marginBottom: 16 }}>
<Col xs={12} lg={12}>
- <Card size="small">
+ <Card size="small" style={{ height: '100%' }}>
<Statistic
title={t('auditInsights.controlPlaneFailures')}
value={insights.controlPlaneFailureCount}
@@ -251,7 +251,7 @@ const AuditRiskInsights = ({ summary, records, loading }:
Props) => {
</Card>
</Col>
<Col xs={12} lg={12}>
- <Card size="small">
+ <Card size="small" style={{ height: '100%' }}>
<Statistic
title={t('auditInsights.topOperator')}
value={insights.topOperator?.name ?? '-'}
diff --git a/web/src/pages/ops/AuditSummaryCards.tsx
b/web/src/pages/ops/AuditSummaryCards.tsx
index 6299d60a3..7fba554fb 100644
--- a/web/src/pages/ops/AuditSummaryCards.tsx
+++ b/web/src/pages/ops/AuditSummaryCards.tsx
@@ -79,14 +79,14 @@ const AuditSummaryCards = ({ summary, loading }: Props) => {
// 成功率配色按阈值走,不再写死绿色(用户约定 2026-10-16):47% 曾照样显示绿色,读起来像健康。
const successColor = successRate >= 95 ? '#52c41a' : successRate >= 80 ?
'#faad14' : '#cf1322';
return (
- <Row gutter={[12, 12]} style={{ marginBottom: 16 }}>
+ <Row gutter={[12, 12]} align="stretch" style={{ marginBottom: 16 }}>
<Col xs={12} lg={6}>
- <Card size="small">
+ <Card size="small" style={{ height: '100%' }}>
<Statistic title={t('audit.summaryMatched')} value={data.total} />
</Card>
</Col>
<Col xs={12} lg={6}>
- <Card size="small">
+ <Card size="small" style={{ height: '100%' }}>
<Statistic
title={t('audit.summarySuccessRate')}
value={successRate}
@@ -104,7 +104,7 @@ const AuditSummaryCards = ({ summary, loading }: Props) => {
</Card>
</Col>
<Col xs={12} lg={6}>
- <Card size="small">
+ <Card size="small" style={{ height: '100%' }}>
<Statistic
title={t('auditInsights.failPartial')}
value={`${data.failed} / ${data.partial}`}
@@ -113,17 +113,17 @@ const AuditSummaryCards = ({ summary, loading }: Props)
=> {
</Card>
</Col>
<Col xs={12} lg={6}>
- <Card size="small">
+ <Card size="small" style={{ height: '100%' }}>
<Statistic title={t('audit.summaryOperators')}
value={data.uniqueOperators} />
</Card>
</Col>
<Col xs={24} lg={12}>
- <Card size="small" title={t('audit.summaryTopOperations')}>
+ <Card size="small" title={t('audit.summaryTopOperations')} style={{
height: '100%' }}>
<BucketList items={data.byOperation} total={data.total}
kind="operation" />
</Card>
</Col>
<Col xs={24} lg={12}>
- <Card size="small" title={t('audit.summaryResourceTypes')}>
+ <Card size="small" title={t('audit.summaryResourceTypes')} style={{
height: '100%' }}>
<BucketList items={data.byResourceType} total={data.total}
kind="resource" />
</Card>
</Col>