fix(kafka): improve message browsing and topic navigation

This commit is contained in:
miracle authored and GitHub committed 2026-08-14 00:19:09 +08:00
1 parent f6518ff3a0
commit e77b547cea
24 files changed
+751 -114

No files matched your search

@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { extractKafkaPartitionRows, isKafkaStatsPayload } from "@/lib/mq/kafkaTopicStats";
describe("Kafka topic stats", () => {
it("normalizes and sorts partition rows", () => {
expect(
extractKafkaPartitionRows({
partitionStats: [{ partition: 2, beginOffset: 8, endOffset: 20, messageCount: 12, leader: 3, replicas: [3, 4], isr: [3] }, { partition: 0, beginOffset: 1, endOffset: 4, messageCount: 3 }, null],
}),
).toEqual([
{ partition: 0, beginOffset: 1, endOffset: 4, messageCount: 3, leader: -1, replicas: [], isr: [] },
{ partition: 2, beginOffset: 8, endOffset: 20, messageCount: 12, leader: 3, replicas: [3, 4], isr: [3] },
]);
});
it("recognizes Kafka summaries and rejects unrelated payloads", () => {
expect(isKafkaStatsPayload({ partitionStats: [] })).toBe(true);
expect(isKafkaStatsPayload({ partitions: 2, replicationFactor: 1, totalMessages: 4 })).toBe(true);
expect(isKafkaStatsPayload({ partitions: 2 })).toBe(false);
expect(extractKafkaPartitionRows(undefined)).toEqual([]);
});
});
@@ -77,6 +77,13 @@ describe("mqConsoleDefaults", () => {
expect(defaultMqCapabilitiesForSystemKind("pulsar").supportsClientConnections).toBeFalsy();
});
it("exposes Kafka message browsing through peek capability without query capability", () => {
const caps = { ...defaultMqCapabilitiesForSystemKind("kafka"), supportsSendMessage: false };
expect(caps.supportsPeekMessages).toBe(true);
expect(caps.supportsMessageQuery).toBe(false);
expect(resolveAvailableMqTabs({ systemKind: "kafka", capabilities: caps })).toContain("messages");
});
it("exposes the namespaces tab for RabbitMQ vhost management", () => {
const caps = defaultMqCapabilitiesForSystemKind("rabbitmq");
expect(resolveAvailableMqTabs({ systemKind: "rabbitmq", capabilities: caps })).toEqual(["namespaces", "topics", "subscriptions", "monitoring", "clients", "messages", "broker", "policies", "permissions"]);
@@ -0,0 +1,49 @@
export interface KafkaPartitionStatsRow {
partition: number;
beginOffset: number;
endOffset: number;
messageCount: number;
leader: number;
replicas: number[];
isr: number[];
}
export function extractKafkaPartitionRows(raw: unknown): KafkaPartitionStatsRow[] {
return arrayObjects(objectRecord(raw).partitionStats)
.map((body) => ({
partition: numberField(body.partition) ?? 0,
beginOffset: numberField(body.beginOffset) ?? 0,
endOffset: numberField(body.endOffset) ?? 0,
messageCount: numberField(body.messageCount) ?? 0,
leader: numberField(body.leader) ?? -1,
replicas: numberArrayField(body.replicas),
isr: numberArrayField(body.isr),
}))
.sort((left, right) => left.partition - right.partition);
}
export function isKafkaStatsPayload(raw: unknown): boolean {
const root = objectRecord(raw);
return Array.isArray(root.partitionStats) || (numberField(root.partitions) !== undefined && numberField(root.replicationFactor) !== undefined && numberField(root.totalMessages) !== undefined);
}
function objectRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}
function arrayObjects(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => !!item && typeof item === "object" && !Array.isArray(item)) : [];
}
function numberField(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}
return undefined;
}
function numberArrayField(value: unknown): number[] {
return Array.isArray(value) ? value.map(numberField).filter((item): item is number => item !== undefined) : [];
}
+1 -1
View File
@@ -177,7 +177,7 @@ export function resolveAvailableMqTabs(options: { systemKind?: MqSystemKind; cap
if (capabilities.supportsSubscriptions) tabs.push("subscriptions");
tabs.push("monitoring");
tabs.push("clients");
if (capabilities.supportsSendMessage) tabs.push("messages");
if (capabilities.supportsSendMessage || (systemKind === "kafka" && capabilities.supportsPeekMessages)) tabs.push("messages");
tabs.push("broker");
// RabbitMQ lights this tab via virtual-host policies instead of Pulsar rates/quotas.
if (capabilities.supportsRateLimits || capabilities.supportsBacklogQuota || capabilities.supportsRetention || capabilities.supportsPolicies) {