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 71ffb696a fix(deliveries): offer a retry when the instance filter
cannot load (#4700)
71ffb696a is described below
commit 71ffb696a273c500d89c091e8ebd96f374751519
Author: Apulupie <[email protected]>
AuthorDate: Mon Sep 21 21:10:02 2026 +0800
fix(deliveries): offer a retry when the instance filter cannot load (#4700)
`notificationDeliveries.tsx` loaded its instance filter with `void
listInstances().then(setInstances).catch(() => undefined)`, so a failed `GET
/api/instances` left `instances` at its empty initial value: the Select
rendered no options, with no message and no retry, and the page read as "this
deployment has no instances" — a conclusion a failed request cannot support. It
was the only silent degrade on the page, since the delivery-list load below it
reports `deliveries.loadFailed` and [...]
A failure is now a state. `instanceLoadFailed` puts the Select into its
error status and renders a retry button under a tooltip explaining why, backed
by a new `deliveries.instancesLoadFailed` key in both languages and the
existing `common.retry`; a reload nonce in the effect's deps re-runs the load,
and the request picked up the `cancelled` guard it did not have.
Open follow-up: `home/dashboard.tsx` and `settings/DataSourceTab.tsx` still
clear their instance lists silently, while `useInstanceFilter` documents silent
degradation as deliberate. Whether instance-filter load failures should get one
shared affordance repo-wide is a product decision this change does not settle.
Fixes #4701
---
web/src/i18n/translations.ts | 4 +++
.../__tests__/NotificationDeliveriesPage.test.tsx | 26 ++++++++++++++++++
web/src/pages/ops/notificationDeliveries.tsx | 32 ++++++++++++++++++++--
3 files changed, 59 insertions(+), 3 deletions(-)
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 0a200d625..bb1bceae1 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1168,6 +1168,10 @@ const translations: Record<string, Record<Lang, string>>
= {
zh: '告警投递记录加载失败,请稍后重试',
en: 'Failed to load alert deliveries. Please try again later.',
},
+ 'deliveries.instancesLoadFailed': {
+ zh: '实例列表加载失败,实例筛选暂时不可用',
+ en: 'Failed to load the instance list; the instance filter is unavailable',
+ },
'deliveries.retryQueued': { zh: '已加入重新投递队列', en: 'Added to the redelivery
queue.' },
'deliveries.retryFailed': {
zh: '重新投递失败,请稍后重试',
diff --git a/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
b/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
index e5c590c11..99ab398df 100644
--- a/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
+++ b/web/src/pages/ops/__tests__/NotificationDeliveriesPage.test.tsx
@@ -169,4 +169,30 @@ describe('NotificationDeliveriesPage', () => {
expect.objectContaining({ status: 'DELIVERED' }),
);
});
+
+ it('surfaces a failed instance-list load with a retry instead of an empty
filter', async () => {
+ const user = userEvent.setup();
+ vi.mocked(listInstances)
+ .mockRejectedValueOnce(new Error('the instance service is down'))
+ .mockResolvedValueOnce([]);
+ render(
+ <App>
+ <LangProvider>
+ <NotificationDeliveriesPage />
+ </LangProvider>
+ </App>,
+ );
+
+ await screen.findByText('Broker disk usage');
+ // An empty instance filter reads as "this deployment has no instances",
which is not something a
+ // failed request can establish. The retry is the affordance the silent
catch never offered.
+ const retry = await screen.findByRole('button', { name: /^重\s*试$/ });
+
+ await user.click(retry);
+
+ await waitFor(() => expect(listInstances).toHaveBeenCalledTimes(2));
+ await waitFor(() =>
+ expect(screen.queryByRole('button', { name: /^重\s*试$/
})).not.toBeInTheDocument(),
+ );
+ });
});
diff --git a/web/src/pages/ops/notificationDeliveries.tsx
b/web/src/pages/ops/notificationDeliveries.tsx
index 4e335ec49..31150cd40 100644
--- a/web/src/pages/ops/notificationDeliveries.tsx
+++ b/web/src/pages/ops/notificationDeliveries.tsx
@@ -52,6 +52,8 @@ const NotificationDeliveriesPage = () => {
const [channel, setChannel] = useState<string>();
const [status, setStatus] = useState<NotificationDeliveryRecord['status']>();
const [instanceId, setInstanceId] = useState<string>();
+ const [instanceLoadFailed, setInstanceLoadFailed] = useState(false);
+ const [instanceReloadNonce, setInstanceReloadNonce] = useState(0);
const [selectedDelivery, setSelectedDelivery] =
useState<NotificationDeliveryRecord>();
const [retryingIds, setRetryingIds] = useState<Set<number>>(() => new Set());
const [retryingVisible, setRetryingVisible] = useState(false);
@@ -115,10 +117,22 @@ const NotificationDeliveriesPage = () => {
};
useEffect(() => {
+ let cancelled = false;
void listInstances()
- .then(setInstances)
- .catch(() => undefined);
- }, []);
+ .then((loaded) => {
+ if (cancelled) return;
+ setInstances(loaded);
+ setInstanceLoadFailed(false);
+ })
+ .catch(() => {
+ // Swallowing this leaves an empty filter, which reads as "this
deployment has no instances"
+ // — a claim a failed request cannot make, and one the user has no way
to retry.
+ if (!cancelled) setInstanceLoadFailed(true);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [instanceReloadNonce]);
useEffect(() => {
let cancelled = false;
@@ -263,6 +277,7 @@ const NotificationDeliveriesPage = () => {
optionFilterProp="label"
placeholder={t('deliveries.allInstances')}
value={instanceId}
+ status={instanceLoadFailed ? 'error' : undefined}
style={{ width: 280, flex: '1 1 280px' }}
options={instances.map((instance) => ({
value: instance.name,
@@ -270,6 +285,17 @@ const NotificationDeliveriesPage = () => {
}))}
onChange={(value) => resetPage(() => setInstanceId(value))}
/>
+ {instanceLoadFailed && (
+ <Tooltip title={t('deliveries.instancesLoadFailed')}>
+ <Button
+ size="small"
+ icon={<ArrowClockwise size={16} />}
+ onClick={() => setInstanceReloadNonce((nonce) => nonce + 1)}
+ >
+ {t('common.retry')}
+ </Button>
+ </Tooltip>
+ )}
</Flex>
<Table
rowKey="id"