feat(kafka): add message display ordering

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-08-31 23:47:51 +08:00
1 parent 42c0dc7f89
commit 2fe26052a7
12 files changed
+171 -26

No files matched your search

@@ -7,6 +7,7 @@ import { mqPeekMessages } from "@/lib/backend/api";
import { formatError } from "@/lib/backend/errorUtils";
import { copyToClipboard } from "@/lib/common/clipboard";
import { buildKafkaMessageSearchText, kafkaMessageSearchTextMatches, normalizeKafkaMessageSearchQuery } from "@/lib/mq/kafkaMessageSearch";
import { sortKafkaMessagesByPublishTime, type KafkaMessageDisplayOrder } from "@/lib/mq/kafkaMessageSort";
import { parseNonNegativeSafeInteger } from "@/lib/mq/mqPeekFilters";
import { useToast } from "@/composables/useToast";
import { Button } from "@/components/ui/button";
@@ -40,6 +41,7 @@ const advancedExpanded = ref(false);
const messageSearchQuery = ref("");
type KafkaPeekStartPosition = NonNullable<PeekMessagesOptions["startPosition"]>;
const kafkaStartPosition = ref<KafkaPeekStartPosition>("latest");
const kafkaMessageDisplayOrder = ref<KafkaMessageDisplayOrder>("newest");
let messageRequestVersion = 0;
const isKafka = computed(() => props.mqSystemKind === "kafka");
@@ -57,6 +59,7 @@ const filteredMessages = computed(() => {
if (!query) return messages.value;
return searchableMessages.value.filter(({ searchText }) => kafkaMessageSearchTextMatches(searchText, query)).map(({ message }) => message);
});
const displayedMessages = computed(() => (isKafka.value ? sortKafkaMessagesByPublishTime(filteredMessages.value, kafkaMessageDisplayOrder.value) : filteredMessages.value));
function peekGroupName(): string {
if (props.mqSystemKind === "rocketmq") return "__dbx_rocketmq_viewer__";
@@ -223,6 +226,18 @@ watch(kafkaStartPosition, () => {
</SelectContent>
</Select>
</label>
<label v-if="isKafka">
<span>{{ t("mqMessages.timeOrder") }}</span>
<Select v-model="kafkaMessageDisplayOrder">
<SelectTrigger data-testid="kafka-message-display-order" class="message-browser-start-position">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper" class="message-browser-start-position-content">
<SelectItem value="newest">{{ t("mqMessages.newestFirst") }}</SelectItem>
<SelectItem value="oldest">{{ t("mqMessages.oldestFirst") }}</SelectItem>
</SelectContent>
</Select>
</label>
<label v-if="isKafka">
<span>{{ t("mqMessages.partition") }}</span>
<input v-model="partition" data-testid="kafka-peek-partition" type="number" min="0" :placeholder="t('mqMessages.partitionPlaceholderAll')" :disabled="loading" />
@@ -263,7 +278,7 @@ watch(kafkaStartPosition, () => {
<div v-else-if="!messages.length" class="message-empty">{{ t("mqMessages.noMessages") }}</div>
<div v-else-if="!filteredMessages.length" class="message-empty" data-testid="kafka-message-filter-empty">{{ t("mqMessages.noMatchingMessages") }}</div>
<div v-else class="message-list">
<article v-for="message in filteredMessages" :key="`${message.properties?.partition ?? 'p'}-${message.messageId || message.position}`" class="message-row">
<article v-for="message in displayedMessages" :key="`${message.properties?.partition ?? 'p'}-${message.messageId || message.position}`" class="message-row">
<div class="message-meta">
<span>#{{ message.position }}</span>
<span v-if="message.properties?.partition != null">{{ t("mqMessages.metaPartition", { partition: message.properties.partition }) }}</span>
@@ -74,13 +74,14 @@ async function setSelectValue(trigger: HTMLElement, value: string) {
await flushUi();
}
async function mountBrowser(mqSystemKind: "kafka" | "rabbitmq" = "kafka") {
async function mountBrowser(mqSystemKind: "kafka" | "rabbitmq" = "kafka", appearance: "form" | "monitoring" = "form") {
root = document.createElement("div");
document.body.appendChild(root);
app = createApp(MessageBrowser, {
connectionId: "mq-1",
topic: { ...TOPIC, tenant: mqSystemKind === "rabbitmq" ? "_rabbitmq" : "_kafka" },
mqSystemKind,
appearance,
});
app.mount(root);
await flushUi();
@@ -121,6 +122,10 @@ async function loadMessages(container: ParentNode) {
await flushUi();
}
function displayedPayloads(container: ParentNode): string[] {
return [...container.querySelectorAll<HTMLElement>(".message-payload")].map((element) => element.textContent || "");
}
beforeEach(() => {
backend.mqPeekMessages.mockReset();
clipboard.copyToClipboard.mockReset();
@@ -229,6 +234,51 @@ describe("MessageBrowser", () => {
expect(backend.mqPeekMessages).toHaveBeenCalledWith("mq-1", expect.objectContaining({ topic: "events" }), "__dbx_kafka_viewer__", 20, { startPosition: "latest" });
});
it("orders loaded Kafka messages newest first by default and changes presentation without reloading", async () => {
backend.mqPeekMessages.mockResolvedValueOnce([
{ position: 1, messageId: "1", publishTime: "1000", payloadBase64: "", payloadText: "oldest", properties: {}, headers: {} },
{ position: 2, messageId: "2", publishTime: "3000", payloadBase64: "", payloadText: "newest", properties: {}, headers: {} },
{ position: 3, messageId: "3", publishTime: "2000", payloadBase64: "", payloadText: "middle", properties: {}, headers: {} },
]);
const browser = await mountBrowser();
await loadMessages(browser);
expect(displayedPayloads(browser)).toEqual(["newest", "middle", "oldest"]);
const displayOrder = browser.querySelector<HTMLElement>('[data-testid="kafka-message-display-order"]');
if (!displayOrder) throw new Error("Kafka message display order select not found");
await setSelectValue(displayOrder, "oldest");
expect(displayedPayloads(browser)).toEqual(["oldest", "middle", "newest"]);
expect(backend.mqPeekMessages).toHaveBeenCalledTimes(1);
});
it("keeps the selected Kafka display order when filtering loaded messages", async () => {
backend.mqPeekMessages.mockResolvedValueOnce([
{ position: 1, messageId: "1", publishTime: "1000", payloadBase64: "", payloadText: "matching oldest", properties: {}, headers: {} },
{ position: 2, messageId: "2", publishTime: "3000", payloadBase64: "", payloadText: "matching newest", properties: {}, headers: {} },
{ position: 3, messageId: "3", publishTime: "2000", payloadBase64: "", payloadText: "excluded", properties: {}, headers: {} },
]);
const browser = await mountBrowser();
await loadMessages(browser);
const displayOrder = browser.querySelector<HTMLElement>('[data-testid="kafka-message-display-order"]');
const filter = browser.querySelector<HTMLInputElement>('[data-testid="kafka-message-filter-input"]');
if (!displayOrder || !filter) throw new Error("Kafka message presentation controls not found");
await setSelectValue(displayOrder, "oldest");
await setInputValue(filter, "matching");
expect(displayedPayloads(browser)).toEqual(["matching oldest", "matching newest"]);
expect(backend.mqPeekMessages).toHaveBeenCalledTimes(1);
});
it("shows Kafka display ordering in the monitoring appearance", async () => {
const browser = await mountBrowser("kafka", "monitoring");
expect(browser.querySelector('[data-testid="kafka-message-display-order"]')).not.toBeNull();
expect(browser.querySelector('[data-testid="kafka-peek-start-position"]')).toBeNull();
});
it("normalizes a decimal count before sending the request", async () => {
const browser = await mountBrowser();
const countInput = browser.querySelector<HTMLInputElement>('[data-testid="peek-count"]');
@@ -383,6 +433,7 @@ describe("MessageBrowser", () => {
await loadMessages(browser);
expect(browser.querySelector('[data-testid="kafka-message-filter"]')).toBeNull();
expect(browser.querySelector('[data-testid="kafka-message-display-order"]')).toBeNull();
});
it("sends explicit earliest and offset read positions", async () => {