From d36c094c4fd09693e2c677851a15577db27aa0ae Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Sat, 12 Sep 2026 21:43:31 +0800 Subject: [PATCH] fix(mq): show clear-backlog errors inside the confirm dialog --- .../src/components/mq/SubscriptionsPanel.vue | 12 ++- .../mq/__tests__/SubscriptionsPanel.spec.ts | 102 +++++++++++++++++- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/mq/SubscriptionsPanel.vue b/apps/desktop/src/components/mq/SubscriptionsPanel.vue index c34a0b5684..728d40650b 100644 --- a/apps/desktop/src/components/mq/SubscriptionsPanel.vue +++ b/apps/desktop/src/components/mq/SubscriptionsPanel.vue @@ -60,6 +60,7 @@ const showDeleteDialog = ref(false); const deleting = ref(false); const clearBacklogTarget = ref(); const showClearBacklogDialog = ref(false); +const clearBacklogError = ref(); const clearingBacklog = ref(false); const formData = ref({ @@ -440,6 +441,7 @@ function handleClearBacklog(sub: SubscriptionInfo) { return; } clearBacklogTarget.value = sub; + clearBacklogError.value = undefined; showClearBacklogDialog.value = true; } @@ -450,13 +452,13 @@ async function confirmClearBacklog() { const topicRef = getPulsarTopicRef(); if (!topicRef) return; clearingBacklog.value = true; - error.value = undefined; + clearBacklogError.value = undefined; try { await mqClearBacklog(props.connectionId, topicRef, sub.name); showClearBacklogDialog.value = false; await loadSubscriptions(); } catch (e: unknown) { - error.value = formatError(e); + clearBacklogError.value = formatError(e); } finally { clearingBacklog.value = false; } @@ -829,7 +831,11 @@ onBeforeUnmount(() => { :loading="clearingBacklog" :close-on-confirm="false" @confirm="confirmClearBacklog" - /> + > + + diff --git a/apps/desktop/src/components/mq/__tests__/SubscriptionsPanel.spec.ts b/apps/desktop/src/components/mq/__tests__/SubscriptionsPanel.spec.ts index 728f7517ca..f793e3e252 100644 --- a/apps/desktop/src/components/mq/__tests__/SubscriptionsPanel.spec.ts +++ b/apps/desktop/src/components/mq/__tests__/SubscriptionsPanel.spec.ts @@ -27,7 +27,17 @@ vi.mock("@/composables/useMqMutationGuard", () => ({ })); vi.mock("@/components/editor/DangerConfirmDialog.vue", () => ({ - default: { template: "
" }, + default: { + props: ["open", "title", "confirmLabel", "loading", "closeOnConfirm"], + emits: ["update:open", "confirm"], + template: ` +
+ + + +
+ `, + }, })); vi.mock("@/components/mq/rocketmq/RocketMqConsumerGroupDialogs.vue", () => ({ @@ -66,10 +76,12 @@ function buttonWithExactText(container: ParentNode, text: string): HTMLButtonEle function deferred() { let resolve!: (value: T) => void; - const promise = new Promise((resolvePromise) => { + let reject!: (reason: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; + reject = rejectPromise; }); - return { promise, resolve }; + return { promise, resolve, reject }; } async function mountPanel(overrides: Record = {}) { @@ -82,6 +94,7 @@ async function mountPanel(overrides: Record = {}) { namespace: "default", mqSystemKind: "kafka", supportsResetCursor: true, + supportsClearBacklog: true, supportsPeekMessages: true, ...overrides, }); @@ -205,6 +218,89 @@ describe("SubscriptionsPanel message peek", () => { }); }); +describe("SubscriptionsPanel clear backlog", () => { + async function openClearBacklog(panel: HTMLDivElement) { + buttonWithExactText(panel, "mqSubscriptions.clearBacklog").click(); + await flushUi(); + const dialog = panel.querySelector('[role="dialog"][aria-label="mqSubscriptions.clearBacklog"]'); + expect(dialog).not.toBeNull(); + return dialog!; + } + + it("keeps a failed clear backlog open with the error in the dialog, not the panel", async () => { + backend.mqClearBacklog.mockRejectedValueOnce(new Error("Cannot clear an active consumer group")); + const panel = await mountPanel(); + const dialog = await openClearBacklog(panel); + + buttonWithExactText(dialog, "mqSubscriptions.clearBacklog").click(); + await vi.waitFor(() => expect(dialog.querySelector(".form-error")?.textContent).toContain("Cannot clear an active consumer group")); + + expect(backend.mqClearBacklog).toHaveBeenCalledExactlyOnceWith("mq-1", { tenant: "_kafka", namespace: "default", topic: "events", persistent: true, partitioned: false }, "orders-consumer"); + expect(dialog.isConnected).toBe(true); + expect(buttonWithExactText(dialog, "mqSubscriptions.clearBacklog").disabled).toBe(false); + expect(panel.querySelector(".panel-error")).toBeNull(); + expect(panel.querySelector(".subscriptions-table")?.textContent).toContain("orders-consumer"); + expect(backend.mqListSubscriptions).toHaveBeenCalledTimes(1); + }); + + it("clears the previous error when the dialog is closed and reopened", async () => { + backend.mqClearBacklog.mockRejectedValueOnce(new Error("Previous clear backlog failed")); + const panel = await mountPanel(); + const dialog = await openClearBacklog(panel); + buttonWithExactText(dialog, "mqSubscriptions.clearBacklog").click(); + await vi.waitFor(() => expect(dialog.textContent).toContain("Previous clear backlog failed")); + + buttonWithExactText(dialog, "dangerDialog.cancel").click(); + await flushUi(); + expect(dialog.isConnected).toBe(false); + + const reopenedDialog = await openClearBacklog(panel); + expect(reopenedDialog.querySelector(".form-error")).toBeNull(); + expect(panel.textContent).not.toContain("Previous clear backlog failed"); + expect(panel.querySelector(".panel-error")).toBeNull(); + }); + + it.each(["success", "failure"])("clears the previous error while retrying and handles retry %s", async (outcome) => { + const retry = deferred(); + backend.mqClearBacklog.mockRejectedValueOnce(new Error("First attempt failed")).mockReturnValueOnce(retry.promise); + const panel = await mountPanel(); + const dialog = await openClearBacklog(panel); + buttonWithExactText(dialog, "mqSubscriptions.clearBacklog").click(); + await vi.waitFor(() => expect(dialog.textContent).toContain("First attempt failed")); + + buttonWithExactText(dialog, "mqSubscriptions.clearBacklog").click(); + await vi.waitFor(() => expect(backend.mqClearBacklog).toHaveBeenCalledTimes(2)); + expect(dialog.querySelector(".form-error")).toBeNull(); + expect(dialog.isConnected).toBe(true); + expect(buttonWithExactText(dialog, "mqSubscriptions.clearBacklog").disabled).toBe(true); + + if (outcome === "success") { + backend.mqListSubscriptions.mockResolvedValue([{ ...subscription("orders-consumer", "shared"), msgBacklog: 123 }]); + retry.resolve(); + await vi.waitFor(() => { + expect(dialog.isConnected).toBe(false); + expect(backend.mqListSubscriptions).toHaveBeenCalledTimes(2); + expect(panel.querySelector(".subscriptions-table")?.textContent).toContain("123"); + }); + } else { + retry.reject(new Error("Retry failed")); + await vi.waitFor(() => expect(dialog.querySelector(".form-error")?.textContent).toContain("Retry failed")); + expect(dialog.isConnected).toBe(true); + expect(dialog.textContent).not.toContain("First attempt failed"); + expect(backend.mqListSubscriptions).toHaveBeenCalledTimes(1); + } + expect(panel.querySelector(".panel-error")).toBeNull(); + }); + + it("keeps subscription list loading failures in the panel", async () => { + backend.mqListSubscriptions.mockRejectedValueOnce(new Error("Subscription list failed")); + const panel = await mountPanel(); + + await vi.waitFor(() => expect(panel.querySelector(".panel-error")?.textContent).toContain("Subscription list failed")); + expect(panel.querySelector('[role="dialog"]')).toBeNull(); + }); +}); + describe("SubscriptionsPanel Kafka absolute offset reset", () => { async function openAbsoluteReset(panel: HTMLDivElement) { await vi.waitFor(() => {