diff --git a/ui/api/schema/action.ts b/ui/api/schema/action.ts
new file mode 100644
index 000000000..9231d28d5
--- /dev/null
+++ b/ui/api/schema/action.ts
@@ -0,0 +1,12 @@
+"use server";
+
+import { getHeaders } from "../api";
+
+export async function getSchema(contentLink: string) {
+ const url = `${process.env.BACKEND_URL}/${contentLink}`;
+ const res = await fetch(url, {
+ headers: await getHeaders(),
+ });
+ const rawData = await res.text();
+ return rawData;
+}
diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/ConnectedMessagesTable.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/ConnectedMessagesTable.tsx
index d81d3a416..d24d47b42 100644
--- a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/ConnectedMessagesTable.tsx
+++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/ConnectedMessagesTable.tsx
@@ -30,12 +30,14 @@ export function ConnectedMessagesTable({
topicName,
selectedMessage: serverSelectedMessage,
partitions,
+ baseurl
}: {
kafkaId: string;
topicId: string;
topicName: string;
selectedMessage: Message | undefined;
partitions: number;
+ baseurl: string;
}) {
const [params, sp] = useParseSearchParams();
const updateUrl = useFilterParams(sp);
@@ -208,6 +210,7 @@ export function ConnectedMessagesTable({
onSelectMessage={setSelected}
onDeselectMessage={deselectMessage}
onReset={onReset}
+ baseurl={baseurl}
>
{limit === "continuously" && (
);
}
diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/schema/ConnectedSchema.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/schema/ConnectedSchema.tsx
new file mode 100644
index 000000000..5e28ba77a
--- /dev/null
+++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/schema/ConnectedSchema.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { SchemaValue } from "@/components/MessagesTable/components/SchemaValue";
+import { PageSection, Title } from "@patternfly/react-core";
+
+export function ConnectedSchema({
+ content,
+ name,
+}: {
+ content: string;
+ name: string;
+}) {
+ return (
+
+ {name}
+
+
+ );
+}
diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/schema/page.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/schema/page.tsx
new file mode 100644
index 000000000..a36a44ed2
--- /dev/null
+++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/topics/[topicId]/messages/schema/page.tsx
@@ -0,0 +1,21 @@
+import { getSchema } from "@/api/schema/action";
+import { ConnectedSchema } from "./ConnectedSchema";
+
+export default async function ConnectedSchemaPage({
+ searchParams,
+}: {
+ searchParams: { content?: string; schemaname?: string };
+}) {
+ const urlSearchParams = new URLSearchParams(searchParams);
+
+ const content = urlSearchParams.get("content");
+ const schemaname = urlSearchParams.get("schemaname");
+
+ if (!content) {
+ throw new Error("Content parameter is missing.");
+ }
+
+ const schemaContent = await getSchema(content);
+
+ return ;
+}
diff --git a/ui/components/MessagesTable/MessagesTable.tsx b/ui/components/MessagesTable/MessagesTable.tsx
index d47ccf101..0b6c3926b 100644
--- a/ui/components/MessagesTable/MessagesTable.tsx
+++ b/ui/components/MessagesTable/MessagesTable.tsx
@@ -12,7 +12,10 @@ import {
TextContent,
Tooltip,
} from "@/libs/patternfly/react-core";
-import { ExclamationTriangleIcon, HelpIcon } from "@/libs/patternfly/react-icons";
+import {
+ ExclamationTriangleIcon,
+ HelpIcon,
+} from "@/libs/patternfly/react-icons";
import {
BaseCellProps,
InnerScrollContainer,
@@ -36,6 +39,7 @@ import { NoData } from "./components/NoData";
import { NoResultsEmptyState } from "./components/NoResultsEmptyState";
import { UnknownValuePreview } from "./components/UnknownValuePreview";
import { beautifyUnknownValue, isSameMessage } from "./components/utils";
+import { ExternalLink } from "../Navigation/ExternalLink";
const columnWidths: Record = {
"offset-partition": 10,
@@ -48,10 +52,12 @@ const columnWidths: Record = {
};
const defaultColumns: Column[] = [
- "timestampUTC",
"offset-partition",
+ "timestampUTC",
"key",
+ "headers",
"value",
+ "size",
];
export type MessagesTableProps = {
@@ -71,6 +77,7 @@ export type MessagesTableProps = {
onSelectMessage: (message: Message) => void;
onDeselectMessage: () => void;
onReset: () => void;
+ baseurl: string;
};
export function MessagesTable({
@@ -91,6 +98,7 @@ export function MessagesTable({
onDeselectMessage,
onReset,
children,
+ baseurl,
}: PropsWithChildren) {
const t = useTranslations("message-browser");
const columnLabels = useColumnLabels();
@@ -181,7 +189,7 @@ export function MessagesTable({
+ |
{children}
|
);
@@ -264,30 +268,49 @@ export function MessagesTable({
{row.attributes.key ? (
<>
- {
- setDefaultTab("key");
- onSelectMessage(row);
- }}
- />
- {row.relationships.keySchema && (
-
-
- {row.relationships.keySchema?.meta?.artifactType && (
- <>
- {row.relationships.keySchema?.meta?.artifactType}
- >
- )}
- {row.relationships.keySchema?.meta?.errors && (
+ {
+ setDefaultTab("key");
+ onSelectMessage(row);
+ }}
+ />
+ {row.relationships.keySchema && (
+
+
+ {row.relationships.keySchema?.meta
+ ?.name &&
+ row.relationships.keySchema?.links
+ ?.content ? (
+
+ {
+ row.relationships.keySchema?.meta
+ ?.name
+ }
+
+ ) : (
+ row.relationships.keySchema?.meta?.name
+ )}
+ {row.relationships.keySchema?.meta
+ ?.errors && (
<>
- { row.relationships.keySchema?.meta?.errors[0].detail }
+ {" "}
+ {
+ row.relationships.keySchema?.meta
+ ?.errors[0].detail
+ }
>
- )}
-
-
- )}
+ )}
+
+
+ )}
>
) : (
empty
@@ -318,30 +341,47 @@ export function MessagesTable({
{row.attributes.value ? (
<>
- {
- setDefaultTab("value");
- onSelectMessage(row);
- }}
- />
- {row.relationships.valueSchema && (
-
-
- {row.relationships.valueSchema?.meta?.artifactType && (
- <>
- {row.relationships.valueSchema?.meta?.artifactType}
- >
- )}
- {row.relationships.valueSchema?.meta?.errors && (
- <>
- { row.relationships.valueSchema?.meta?.errors[0].detail }
- >
- )}
-
-
- )}
+ {
+ setDefaultTab("value");
+ onSelectMessage(row);
+ }}
+ />
+ {row.relationships.valueSchema && (
+
+
+ {row.relationships.valueSchema?.meta
+ ?.name &&
+ row.relationships.valueSchema?.links
+ ?.content ? (
+
+ {
+ row.relationships.valueSchema.meta
+ .name
+ }
+
+ ) : (
+ row.relationships.valueSchema?.meta
+ ?.name
+ )}
+ {row.relationships.valueSchema?.meta
+ ?.errors && (
+ <>
+ {" "}
+ {
+ row.relationships.valueSchema?.meta
+ ?.errors[0].detail
+ }
+ >
+ )}
+
+
+ )}
>
) : (
empty
diff --git a/ui/components/MessagesTable/components/MessageDetails.stories.tsx b/ui/components/MessagesTable/components/MessageDetails.stories.tsx
index 706ec8e0a..f35e8dd01 100644
--- a/ui/components/MessagesTable/components/MessageDetails.stories.tsx
+++ b/ui/components/MessagesTable/components/MessageDetails.stories.tsx
@@ -16,7 +16,9 @@ export default {
partition: 4,
offset: 16,
size: 1234,
- timestamp: new Date(Date.UTC(2024, 11, 31, 23, 59, 59, 999)).toISOString(),
+ timestamp: new Date(
+ Date.UTC(2024, 11, 31, 23, 59, 59, 999),
+ ).toISOString(),
headers: {
"post-office-box": "string",
"extended-address": "string",
@@ -29,6 +31,23 @@ export default {
value:
'{"order":{"address":{"street":"123 any st","city":"Austin","state":"TX","zip":"78626"},"contact":{"firstName":"james","lastName":"smith","phone":"512-123-1234"},"orderId":"123"},"primitives":{"stringPrimitive":"some value","booleanPrimitive":true,"numberPrimitive":24},"addressList":[{"street":"123 any st","city":"Austin","state":"TX","zip":"78626"},{"street":"123 any st","city":"Austin","state":"TX","zip":"78626"},{"street":"123 any st","city":"Austin","state":"TX","zip":"78626"},{"street":"123 any st","city":"Austin","state":"TX","zip":"78626"}]}',
},
+ relationships: {
+ keySchema: null,
+ valueSchema: {
+ meta: {
+ artifactType: "AVRO",
+ name: "com.example.price",
+ },
+ data: {
+ type: "schemas",
+ id: "eyJnbG9iYWxJZCI6MX0=",
+ },
+ links: {
+ content:
+ "/api/registries/my-apicurio-registry/schemas/eyJnbG9iYWxJZCI6MX0=",
+ },
+ },
+ },
},
defaultTab: "value",
onClose: fn(),
diff --git a/ui/components/MessagesTable/components/MessageDetails.tsx b/ui/components/MessagesTable/components/MessageDetails.tsx
index d9f5a7691..68b3d8c67 100644
--- a/ui/components/MessagesTable/components/MessageDetails.tsx
+++ b/ui/components/MessagesTable/components/MessageDetails.tsx
@@ -23,12 +23,20 @@ import {
Tooltip,
} from "@/libs/patternfly/react-core";
import { HelpIcon } from "@/libs/patternfly/react-icons";
-import { ClipboardCopy } from "@patternfly/react-core";
+import {
+ ClipboardCopy,
+ Stack,
+ StackItem,
+ TabContent,
+ Title,
+} from "@patternfly/react-core";
import { useTranslations } from "next-intl";
-import { useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
import { allExpanded, defaultStyles, JsonView } from "react-json-view-lite";
import { NoData } from "./NoData";
import { maybeJson } from "./utils";
+import { getSchema } from "@/api/schema/action";
+import { SchemaValue } from "./SchemaValue";
export type MessageDetailsProps = {
onClose: () => void;
@@ -42,7 +50,6 @@ export function MessageDetails({
message,
}: MessageDetailsProps) {
const t = useTranslations("message-browser");
-
const body = useMemo(() => {
return (
message && (
@@ -83,6 +90,37 @@ export function MessageDetailsBody({
const [key, isKeyJson] = maybeJson(message.attributes.key || "{}");
const [value, isValueJson] = maybeJson(message.attributes.value || "{}");
+ const [keySchemaContent, setKeySchemaContent] = useState();
+ const [valueSchemaContent, setValueSchemaContent] = useState();
+
+ useEffect(() => {
+ async function fetchSchemas() {
+ try {
+ // Fetch Key Schema
+ if (message?.relationships.keySchema?.links?.content) {
+ const keySchemaLink = message.relationships.keySchema.links.content;
+ const keySchema = await getSchema(keySchemaLink);
+ setKeySchemaContent(keySchema);
+ } else {
+ console.log("No URL found for key schema.");
+ }
+
+ // Fetch Value Schema
+ if (message?.relationships.valueSchema?.links?.content) {
+ const valueSchemaLink =
+ message.relationships.valueSchema.links.content;
+ const valueSchema = await getSchema(valueSchemaLink);
+ setValueSchemaContent(valueSchema);
+ } else {
+ console.log("No URL found for value schema.");
+ }
+ } catch (error) {
+ console.error("Error fetching schemas:", error);
+ }
+ }
+ fetchSchemas();
+ }, [message]);
+
return (
@@ -143,6 +181,18 @@ export function MessageDetailsBody({
)}
+
+ {t("field.key-format")}
+
+ {message.relationships.keySchema?.meta?.artifactType ?? "Plain"}
+
+
+
+ {t("field.value-format")}
+
+ {message.relationships.valueSchema?.meta?.artifactType ?? "Plain"}
+
+
@@ -152,45 +202,75 @@ export function MessageDetailsBody({
eventKey={"value"}
title={{t("field.value")}}
>
-
- {message.attributes.value ?? "-"}
-
- {isValueJson && (
-
- )}
+
+
+
+ {message.attributes.value ?? "-"}
+
+ {isValueJson && (
+
+ )}
+
+ {valueSchemaContent && (
+
+
+ {message.relationships.valueSchema?.meta?.name}
+
+
+
+ )}
+
{t("field.key")}}
>
-
- {message.attributes.key ?? "-"}
-
- {isKeyJson && (
-
- )}
+
+
+
+ {message.attributes.key ?? "-"}
+
+ {isKeyJson && (
+
+ )}
+
+ {keySchemaContent && (
+
+
+ {message.relationships.keySchema?.meta?.name}
+
+
+
+ )}
+
;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ name: "SchemaValue",
+ schema: JSON.stringify({
+ type: "record",
+ name: "price",
+ namespace: "com.example",
+ fields: [
+ { name: "symbol", type: "string" },
+ { name: "price", type: "string" },
+ ],
+ }),
+ },
+};
diff --git a/ui/components/MessagesTable/components/SchemaValue.tsx b/ui/components/MessagesTable/components/SchemaValue.tsx
new file mode 100644
index 000000000..2bd2d9e27
--- /dev/null
+++ b/ui/components/MessagesTable/components/SchemaValue.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import {
+ Button,
+ ClipboardCopyButton,
+ CodeBlock,
+ CodeBlockAction,
+ CodeBlockCode,
+} from "@/libs/patternfly/react-core";
+import { DownloadIcon } from "@patternfly/react-icons";
+import { useState } from "react";
+
+export function SchemaValue({
+ schema,
+ name,
+}: {
+ schema: string;
+ name: string;
+}) {
+ const [copyStatus, setCopyStatus] = useState("Copy schema");
+
+ const copyToClipboard = () => {
+ navigator.clipboard
+ .writeText(schema)
+ .then(() => {
+ setCopyStatus("Successfully copied");
+ setTimeout(() => setCopyStatus("Copy schema"), 2000);
+ })
+ .catch((error) => {
+ console.error("Error copying text to clipboard:", error);
+ });
+ };
+
+ const onClickDownload = () => {
+ const blob = new Blob([schema], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${name}.text`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ const actions = (
+
+
+ {copyStatus}
+
+ }
+ />
+
+ );
+ return (
+
+ {schema}
+
+ );
+}
diff --git a/ui/messages/en.json b/ui/messages/en.json
index 0eab68d4b..e87f8e454 100644
--- a/ui/messages/en.json
+++ b/ui/messages/en.json
@@ -187,7 +187,9 @@
"size": "Size",
"timestamp": "Timestamp (local)",
"timestamp--utc": "Timestamp (UTC)",
- "value": "Value"
+ "value": "Value",
+ "key-format": "Key format",
+ "value-format": "Value format"
},
"filter": {
"epoch": "From Unix timestamp",
| |