1 parent
4106a8ce22
commit
3acf3a66b9
11 files changed
+310
-21
No files matched your search
@@ -65,8 +65,10 @@ const formData = ref({
|
||||
});
|
||||
|
||||
const resetFormData = ref({
|
||||
position: "latest" as "earliest" | "latest" | "timestamp",
|
||||
position: "latest" as "earliest" | "latest" | "timestamp" | "partitionOffset",
|
||||
timestampMs: Date.now(),
|
||||
partition: 0,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
const skipFormData = ref({
|
||||
@@ -261,6 +263,8 @@ function openResetDialog(sub: SubscriptionInfo) {
|
||||
resetFormData.value = {
|
||||
position: "latest",
|
||||
timestampMs: Date.now(),
|
||||
partition: 0,
|
||||
offset: 0,
|
||||
};
|
||||
showResetDialog.value = true;
|
||||
}
|
||||
@@ -365,6 +369,13 @@ async function handleResetCursor() {
|
||||
let pos: ResetPosition;
|
||||
if (resetFormData.value.position === "timestamp") {
|
||||
pos = { kind: "timestamp", timestampMs: resetFormData.value.timestampMs };
|
||||
} else if (resetFormData.value.position === "partitionOffset") {
|
||||
const { partition, offset } = resetFormData.value;
|
||||
if (!Number.isSafeInteger(partition) || partition < 0 || !Number.isSafeInteger(offset) || offset < 0) {
|
||||
error.value = t("mqSubscriptions.nonNegativeIntegerRequired");
|
||||
return;
|
||||
}
|
||||
pos = { kind: "partitionOffset", partition, offset };
|
||||
} else {
|
||||
pos = { kind: resetFormData.value.position };
|
||||
}
|
||||
@@ -650,6 +661,10 @@ watch(
|
||||
<input type="radio" v-model="resetFormData.position" value="timestamp" :disabled="readOnly" />
|
||||
{{ t("mqSubscriptions.timestamp") }}
|
||||
</label>
|
||||
<label v-if="mqSystemKind === 'kafka'" class="radio-label">
|
||||
<input type="radio" v-model="resetFormData.position" value="partitionOffset" :disabled="readOnly" />
|
||||
{{ t("mqSubscriptions.partitionOffset") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="resetFormData.position === 'timestamp'" class="form-group">
|
||||
@@ -657,6 +672,12 @@ watch(
|
||||
<input v-model.number="resetFormData.timestampMs" type="number" :disabled="readOnly" />
|
||||
<div class="form-hint">{{ t("mqSubscriptions.currentTime", { time: new Date(resetFormData.timestampMs).toLocaleString() }) }}</div>
|
||||
</div>
|
||||
<div v-if="mqSystemKind === 'kafka' && resetFormData.position === 'partitionOffset'" class="form-group">
|
||||
<label>{{ t("mqSubscriptions.partition") }}</label>
|
||||
<input v-model.number="resetFormData.partition" data-testid="reset-partition" type="number" min="0" step="1" :disabled="readOnly" />
|
||||
<label>{{ t("mqSubscriptions.offset") }}</label>
|
||||
<input v-model.number="resetFormData.offset" data-testid="reset-offset" type="number" min="0" step="1" :disabled="readOnly" />
|
||||
</div>
|
||||
<div v-if="error" class="form-error">{{ error }}</div>
|
||||
</div>
|
||||
<div class="dialog-footer">
|
||||
|
||||
@@ -58,6 +58,12 @@ function buttonByText(container: ParentNode, text: string): HTMLButtonElement {
|
||||
return button;
|
||||
}
|
||||
|
||||
function buttonWithExactText(container: ParentNode, text: string): HTMLButtonElement {
|
||||
const button = [...container.querySelectorAll<HTMLButtonElement>("button")].find((item) => item.textContent?.trim() === text);
|
||||
if (!button) throw new Error(`Button not found: ${text}`);
|
||||
return button;
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
@@ -75,6 +81,7 @@ async function mountPanel(overrides: Record<string, unknown> = {}) {
|
||||
tenant: "_kafka",
|
||||
namespace: "default",
|
||||
mqSystemKind: "kafka",
|
||||
supportsResetCursor: true,
|
||||
supportsPeekMessages: true,
|
||||
...overrides,
|
||||
});
|
||||
@@ -198,6 +205,77 @@ describe("SubscriptionsPanel message peek", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("SubscriptionsPanel Kafka absolute offset reset", () => {
|
||||
async function openAbsoluteReset(panel: HTMLDivElement) {
|
||||
await vi.waitFor(() => {
|
||||
expect([...panel.querySelectorAll("button")].some((button) => button.textContent?.includes("mqSubscriptions.resetCursor"))).toBe(true);
|
||||
});
|
||||
buttonByText(panel, "mqSubscriptions.resetCursor").click();
|
||||
await flushUi();
|
||||
const absolute = panel.querySelector<HTMLInputElement>('input[value="partitionOffset"]');
|
||||
expect(absolute).toBeTruthy();
|
||||
absolute!.checked = true;
|
||||
absolute!.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.querySelector('[data-testid="reset-partition"]')).toBeTruthy();
|
||||
});
|
||||
}
|
||||
|
||||
it("shows Kafka-only partition fields and dispatches one exact absolute offset", async () => {
|
||||
backend.mqResetCursor.mockResolvedValue(undefined);
|
||||
const panel = await mountPanel();
|
||||
await openAbsoluteReset(panel);
|
||||
|
||||
const partition = panel.querySelector<HTMLInputElement>('[data-testid="reset-partition"]');
|
||||
const offset = panel.querySelector<HTMLInputElement>('[data-testid="reset-offset"]');
|
||||
expect(partition).toBeTruthy();
|
||||
expect(offset).toBeTruthy();
|
||||
partition!.value = "1";
|
||||
partition!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
offset!.value = "42";
|
||||
offset!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
buttonWithExactText(panel, "mqSubscriptions.reset").click();
|
||||
await flushUi();
|
||||
|
||||
expect(backend.mqResetCursor).toHaveBeenCalledWith(
|
||||
"mq-1",
|
||||
expect.objectContaining({ topic: "events" }),
|
||||
"orders-consumer",
|
||||
{ kind: "partitionOffset", partition: 1, offset: 42 },
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["partition", "-1"],
|
||||
["partition", "1.5"],
|
||||
["offset", "-1"],
|
||||
["offset", "42.5"],
|
||||
["offset", "9007199254740992"],
|
||||
])("rejects invalid %s value %s", async (field, value) => {
|
||||
const panel = await mountPanel();
|
||||
await openAbsoluteReset(panel);
|
||||
const input = panel.querySelector<HTMLInputElement>(`[data-testid="reset-${field}"]`)!;
|
||||
input.value = value;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
buttonWithExactText(panel, "mqSubscriptions.reset").click();
|
||||
await vi.waitFor(() => {
|
||||
expect(panel.textContent).toContain("mqSubscriptions.nonNegativeIntegerRequired");
|
||||
});
|
||||
expect(backend.mqResetCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hides the absolute offset option outside Kafka", async () => {
|
||||
const pulsarPanel = await mountPanel({ mqSystemKind: "pulsar", tenant: "public" });
|
||||
await vi.waitFor(() => {
|
||||
expect([...pulsarPanel.querySelectorAll("button")].some((button) => button.textContent?.includes("mqSubscriptions.resetCursor"))).toBe(true);
|
||||
});
|
||||
buttonByText(pulsarPanel, "mqSubscriptions.resetCursor").click();
|
||||
await flushUi();
|
||||
expect(pulsarPanel.querySelector('input[value="partitionOffset"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("SubscriptionsPanel RocketMQ online members", () => {
|
||||
it("shows '-' when onlineMembers is unknown before enrich completes", async () => {
|
||||
const enrich = deferred<ReturnType<typeof subscription>[]>();
|
||||
|
||||
Reference in new issue
Block a user