fix(kafka): improve message browsing and topic navigation
This commit is contained in:
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) : [];
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user