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
commit 81e3ab0b17bf094b62fc8473e92daddc868da325 Author: yyqdbngt <[email protected]> AuthorDate: Fri Aug 7 16:26:01 2026 +0800 fix(ui): batch of small UI fixes (#1169, #1170, #1171, #1172, #1174) * fix(ui): use the latest data source when switching metrics source * fix(ui): surface config save errors in cluster dialog * fix(ui): guard address search and normalize case * fix(ui): wire up the live refresh toggle with polling * fix(ui): tolerate partial failures in batch consumer group delete --------- --- web/src/components/MetricsExplorer.tsx | 10 ++++-- web/src/pages/cluster/clients.tsx | 2 +- web/src/pages/cluster/index.tsx | 56 +++++++++++++++++--------------- web/src/pages/instance/consumer.tsx | 30 ++++++++++++----- web/src/pages/studio/BrokerCluster.tsx | 11 ++++++- web/src/pages/studio/GroupManagement.tsx | 9 +++++ web/src/services/consumerService.ts | 19 +++++++++-- 7 files changed, 94 insertions(+), 43 deletions(-) diff --git a/web/src/components/MetricsExplorer.tsx b/web/src/components/MetricsExplorer.tsx index 6acc99f0..1c71a791 100644 --- a/web/src/components/MetricsExplorer.tsx +++ b/web/src/components/MetricsExplorer.tsx @@ -252,6 +252,9 @@ const MetricsExplorer = () => { const [dataSourceKey, setDataSourceKey] = useState(''); const [dataSourcesLoading, setDataSourcesLoading] = useState(true); const requestId = useRef(0); + // Keeps the latest data source readable from the stable loadMetrics callback so switching + // the source uses the new key instead of a stale closure value. + const dataSourceKeyRef = useRef(dataSourceKey); const selectedProfile = useMemo( () => profiles.find((profile) => profile.id === profileId), @@ -277,8 +280,8 @@ const MetricsExplorer = () => { setQueryLoading(true); setQueryError(false); try { - const result = dataSourceKey - ? await queryByDataSource({ key: dataSourceKey, query }) + const result = dataSourceKeyRef.current + ? await queryByDataSource({ key: dataSourceKeyRef.current, query }) : await queryMetrics(query); if (currentRequest === requestId.current) setData(result); } catch { @@ -290,7 +293,7 @@ const MetricsExplorer = () => { if (currentRequest === requestId.current) setQueryLoading(false); } }, - [dataSourceKey], + [], ); useEffect(() => { @@ -343,6 +346,7 @@ const MetricsExplorer = () => { }; const handleDataSourceChange = (nextKey: string) => { + dataSourceKeyRef.current = nextKey; setDataSourceKey(nextKey); setData(null); void loadMetrics(selectedMetric, selectedRange); diff --git a/web/src/pages/cluster/clients.tsx b/web/src/pages/cluster/clients.tsx index 8dab3d21..f4febdfb 100644 --- a/web/src/pages/cluster/clients.tsx +++ b/web/src/pages/cluster/clients.tsx @@ -201,7 +201,7 @@ const ClientsPage = () => { return clusterConnections.filter( (connection) => connection.clientId.toLowerCase().includes(normalizedSearch) || - connection.address.includes(search), + connection.address?.toLowerCase().includes(normalizedSearch), ); }, [clusterConnections, search]); diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx index f8558027..9468de9e 100644 --- a/web/src/pages/cluster/index.tsx +++ b/web/src/pages/cluster/index.tsx @@ -521,34 +521,38 @@ const ClusterPage = () => { onOk={() => { configForm.validateFields().then(async (values) => { if (!selectedCluster) return; - const { maxMessageSizeMB, ...configValues } = values; - const nextConfig: ClusterConfig = { - ...(selectedCluster.config ?? {}), - ...configValues, - maxMessageSize: maxMessageSizeMB * 1048576, - }; - const result = await updateClusterConfig({ - id: selectedCluster.id, - ...nextConfig, - }); - if (result.status === 'SUCCESS') { - await requestRefresh('operation'); - message.success(t('cluster.configUpdated')); - setConfigModalOpen(false); - return; - } + try { + const { maxMessageSizeMB, ...configValues } = values; + const nextConfig: ClusterConfig = { + ...(selectedCluster.config ?? {}), + ...configValues, + maxMessageSize: maxMessageSizeMB * 1048576, + }; + const result = await updateClusterConfig({ + id: selectedCluster.id, + ...nextConfig, + }); + if (result.status === 'SUCCESS') { + await requestRefresh('operation'); + message.success(t('cluster.configUpdated')); + setConfigModalOpen(false); + return; + } - const failedAddresses = result.failedBrokers - .map((failure) => failure.address) - .join(', '); - if (result.status === 'PARTIAL') { - await requestRefresh('operation'); - message.warning( - t('cluster.configPartiallyUpdated', { brokers: failedAddresses }), - ); - return; + const failedAddresses = result.failedBrokers + .map((failure) => failure.address) + .join(', '); + if (result.status === 'PARTIAL') { + await requestRefresh('operation'); + message.warning( + t('cluster.configPartiallyUpdated', { brokers: failedAddresses }), + ); + return; + } + message.error(t('cluster.configUpdateFailed', { brokers: failedAddresses })); + } catch { + message.error(t('cluster.configUpdateFailed', { brokers: '' })); } - message.error(t('cluster.configUpdateFailed', { brokers: failedAddresses })); }); }} width={560} diff --git a/web/src/pages/instance/consumer.tsx b/web/src/pages/instance/consumer.tsx index dce8a33e..b6c2b117 100644 --- a/web/src/pages/instance/consumer.tsx +++ b/web/src/pages/instance/consumer.tsx @@ -806,10 +806,22 @@ const ConsumerPage = () => { cancelText: '取消', onOk: async () => { const names = selectedRowKeys.map(String); - await batchDeleteConsumerGroups(names, selectedInstanceId || undefined); - setGroups((prev) => prev.filter((g) => !names.includes(g.name))); - message.success(`已删除 ${selectedRowKeys.length} 个 Group`); - setSelectedRowKeys([]); + const { deleted, failed } = await batchDeleteConsumerGroups( + names, + selectedInstanceId || undefined, + ); + setGroups((prev) => prev.filter((g) => !deleted.includes(g.name))); + if (failed.length > 0) { + message.warning( + `已删除 ${deleted.length} 个,失败 ${failed.length} 个:${failed.join(', ')}`, + ); + setSelectedRowKeys((prev) => + prev.filter((key) => !deleted.includes(String(key))), + ); + } else { + message.success(`已删除 ${deleted.length} 个 Group`); + setSelectedRowKeys([]); + } }, }); }} @@ -1419,11 +1431,11 @@ const ConsumerPage = () => { if (resetGroup) { setResetSubmitting(true); try { - await resetConsumerOffset({ - name: resetGroup.name, - instanceId: selectedInstanceId || undefined, - timestamp: resetTime.valueOf(), - }); + await resetConsumerOffset({ + name: resetGroup.name, + instanceId: selectedInstanceId || undefined, + timestamp: resetTime.valueOf(), + }); message.success( `${resetGroup.name} 消费位点已重置到 ${resetTime.format('YYYY-MM-DD HH:mm:ss')}`, ); diff --git a/web/src/pages/studio/BrokerCluster.tsx b/web/src/pages/studio/BrokerCluster.tsx index 3063def5..6f551bb0 100644 --- a/web/src/pages/studio/BrokerCluster.tsx +++ b/web/src/pages/studio/BrokerCluster.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { Table, Button, Tag, Tabs, Card, Space, Switch, Progress, Tooltip, Spin, App } from 'antd'; import { Plus, @@ -158,6 +158,15 @@ const BrokerClusterPage = () => { } }, [message, t]); + // Live refresh: poll while the auto-refresh switch is on. + useEffect(() => { + if (!autoRefresh) return; + const timer = setInterval(() => { + void loadData(); + }, 5000); + return () => clearInterval(timer); + }, [autoRefresh, loadData]); + const initialized = useRef<boolean | null>(null); if (initialized.current == null) { initialized.current = true; diff --git a/web/src/pages/studio/GroupManagement.tsx b/web/src/pages/studio/GroupManagement.tsx index 7727b231..d4bc74fe 100644 --- a/web/src/pages/studio/GroupManagement.tsx +++ b/web/src/pages/studio/GroupManagement.tsx @@ -99,6 +99,15 @@ const GroupManagementPage = () => { } }, [t]); + // Live refresh: poll while the auto-refresh switch is on. + useEffect(() => { + if (!autoRefresh) return; + const timer = setInterval(() => { + void handleRefresh(); + }, 5000); + return () => clearInterval(timer); + }, [autoRefresh, handleRefresh]); + const handleViewDetail = useCallback( async (group: ConsumerGroup) => { setSelectedGroup(group); diff --git a/web/src/services/consumerService.ts b/web/src/services/consumerService.ts index 3f4b05e9..b48f209a 100644 --- a/web/src/services/consumerService.ts +++ b/web/src/services/consumerService.ts @@ -121,12 +121,25 @@ export async function resetConsumerOffset(data: ResetConsumerOffsetRequest): Pro return metadataApi.resetConsumerOffset(data); } -// Batch delete: loop through single delete calls +export interface BatchDeleteConsumerGroupsResult { + deleted: string[]; + failed: string[]; +} + +// Batch delete: attempt every selected group and report partial failures so a single +// failing group cannot silently abort the whole batch. export async function batchDeleteConsumerGroups( names: string[], instanceId?: string, -): Promise<void> { +): Promise<BatchDeleteConsumerGroupsResult> { + const result: BatchDeleteConsumerGroupsResult = { deleted: [], failed: [] }; for (const name of names) { - await deleteConsumerGroup(name, instanceId); + try { + await deleteConsumerGroup(name, instanceId); + result.deleted.push(name); + } catch { + result.failed.push(name); + } } + return result; }
