fix(mq): show clear-backlog errors inside the confirm dialog

This commit is contained in:
jearhe authored and GitHub committed 2026-09-12 21:43:31 +08:00
1 parent 4bac6b56de
commit d36c094c4f
2 files changed
+108 -6

No files matched your search

@@ -60,6 +60,7 @@ const showDeleteDialog = ref(false);
const deleting = ref(false);
const clearBacklogTarget = ref<SubscriptionInfo>();
const showClearBacklogDialog = ref(false);
const clearBacklogError = ref<string>();
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"
/>
>
<template #options>
<div v-if="clearBacklogError" class="form-error" role="alert">{{ clearBacklogError }}</div>
</template>
</DangerConfirmDialog>
</div>
</template>
@@ -27,7 +27,17 @@ vi.mock("@/composables/useMqMutationGuard", () => ({
}));
vi.mock("@/components/editor/DangerConfirmDialog.vue", () => ({
default: { template: "<div />" },
default: {
props: ["open", "title", "confirmLabel", "loading", "closeOnConfirm"],
emits: ["update:open", "confirm"],
template: `
<div v-if="open" role="dialog" :aria-label="title">
<slot name="options" />
<button :disabled="loading" @click="$emit('update:open', false)">dangerDialog.cancel</button>
<button :disabled="loading" @click="closeOnConfirm !== false && $emit('update:open', false); $emit('confirm')">{{ confirmLabel }}</button>
</div>
`,
},
}));
vi.mock("@/components/mq/rocketmq/RocketMqConsumerGroupDialogs.vue", () => ({
@@ -66,10 +76,12 @@ function buttonWithExactText(container: ParentNode, text: string): HTMLButtonEle
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((resolvePromise) => {
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve };
return { promise, resolve, reject };
}
async function mountPanel(overrides: Record<string, unknown> = {}) {
@@ -82,6 +94,7 @@ async function mountPanel(overrides: Record<string, unknown> = {}) {
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<void>();
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(() => {