From e4859dee29c4314f773da9b530e93a5a6d6a1d60 Mon Sep 17 00:00:00 2001 From: hemahg Date: Tue, 13 Jan 2026 18:20:18 +0530 Subject: [PATCH 1/3] feat(ui): disable metrics and display info message for Virtual Kafka Clusters Signed-off-by: hemahg --- ui/api/kafka/schema.ts | 6 +++ .../overview/ConnectedClusterChartsCard.tsx | 47 +++++++++++++++---- .../overview/ConnectedTopicChartsCard.tsx | 20 ++++++-- .../kafka/[kafkaId]/overview/page.tsx | 21 +++++++-- .../ClusterOverview/TopicChartsCard.tsx | 23 +++++++-- .../components/ChartIncomingOutgoing.tsx | 12 +++-- 6 files changed, 102 insertions(+), 27 deletions(-) diff --git a/ui/api/kafka/schema.ts b/ui/api/kafka/schema.ts index 64ddb0a12..778ae8ad0 100644 --- a/ui/api/kafka/schema.ts +++ b/ui/api/kafka/schema.ts @@ -1,6 +1,11 @@ import { z } from "zod"; import { NodesListMetaSummary } from "../nodes/schema"; +export const KafkaClusterKindSchema = z + .enum(["kafkas.kafka.strimzi.io", "virtualkafkaclusters.kroxylicious.io"]) + .nullable() + .optional(); + export const ClusterListSchema = z.object({ id: z.string(), type: z.literal("kafkas"), @@ -55,6 +60,7 @@ const ClusterDetailSchema = z.object({ meta: z .object({ reconciliationPaused: z.boolean().optional(), + kind: KafkaClusterKindSchema, managed: z.boolean(), privileges: z.array(z.string()).optional(), }) diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedClusterChartsCard.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedClusterChartsCard.tsx index 5fb0c677f..7e8c1df54 100644 --- a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedClusterChartsCard.tsx +++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedClusterChartsCard.tsx @@ -14,14 +14,17 @@ import { ClusterDetail } from "@/api/kafka/schema"; import { ClusterChartsCard } from "@/components/ClusterOverview/ClusterChartsCard"; function timeSeriesMetrics( - ranges: Record | undefined, + ranges: Record | undefined, rangeName: string, ): Record { const series: Record = {}; if (ranges) { Object.values(ranges[rangeName] ?? {}).forEach((r) => { - series[r.nodeId!] = r.range.reduce((a, v) => ({ ...a, [v[0]]: parseFloat(v[1]) }), {} as TimeSeriesMetrics); + series[r.nodeId!] = r.range.reduce( + (a, v) => ({ ...a, [v[0]]: parseFloat(v[1]) }), + {} as TimeSeriesMetrics, + ); }); } @@ -36,11 +39,23 @@ export async function ConnectedClusterChartsCard({ const t = useTranslations(); const res = await cluster; - if (res?.attributes.metrics === null) { + const isVirtualKafkaCluster = + res?.meta?.kind === "virtualkafkaclusters.kroxylicious.io"; + + const metricsUnavailable = res?.attributes.metrics === null; + + console.log("is virtual kafka cluster", isVirtualKafkaCluster); + + if (metricsUnavailable || isVirtualKafkaCluster) { /* * metrics being null (rather than undefined or empty) is how the server * indicates that metrics are not configured for this cluster. */ + + const alertTitle = isVirtualKafkaCluster + ? t("ClusterChartsCard.virtual_cluster_metrics_unavailable") + : t("ClusterChartsCard.data_unavailable"); + return ( @@ -52,10 +67,10 @@ export async function ConnectedClusterChartsCard({ @@ -64,11 +79,23 @@ export async function ConnectedClusterChartsCard({ return ( ); } diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedTopicChartsCard.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedTopicChartsCard.tsx index 323c253df..188d2f870 100644 --- a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedTopicChartsCard.tsx +++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/ConnectedTopicChartsCard.tsx @@ -2,14 +2,17 @@ import { ClusterDetail } from "@/api/kafka/schema"; import { TopicChartsCard } from "@/components/ClusterOverview/TopicChartsCard"; function timeSeriesMetrics( - ranges: Record | undefined, + ranges: Record | undefined, rangeName: string, ): TimeSeriesMetrics { let series: TimeSeriesMetrics = {}; if (ranges) { Object.values(ranges[rangeName] ?? {}).forEach((r) => { - series = r.range.reduce((a, v) => ({ ...a, [v[0]]: parseFloat(v[1]) }), series); + series = r.range.reduce( + (a, v) => ({ ...a, [v[0]]: parseFloat(v[1]) }), + series, + ); }); } @@ -26,8 +29,17 @@ export async function ConnectedTopicChartsCard({ return ( ); } diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/page.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/page.tsx index f27f7e267..2adcd28c3 100644 --- a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/page.tsx +++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/page.tsx @@ -18,21 +18,32 @@ export async function generateMetadata() { }; } -export default async function OverviewPage({ params }: { params: KafkaParams }) { +export default async function OverviewPage({ + params, +}: { + params: KafkaParams; +}) { const kafkaCluster = getKafkaCluster(params.kafkaId, { - fields: 'name,namespace,creationTimestamp,status,kafkaVersion,nodes,listeners,conditions,metrics' - }).then(r => r.payload ?? null); + fields: + "name,namespace,creationTimestamp,status,kafkaVersion,nodes,listeners,conditions,metrics", + }).then((r) => r.payload ?? null); const topics = getTopics(params.kafkaId, { fields: "status", pageSize: 1 }); - const consumerGroups = getConsumerGroups(params.kafkaId, { fields: "groupId,state" }); + const consumerGroups = getConsumerGroups(params.kafkaId, { + fields: "groupId,state", + }); const viewedTopics = getViewedTopics().then((topics) => topics.filter((t) => t.kafkaId === params.kafkaId), ); + console.log("kafkaCLuster", kafkaCluster); return ( + } topicsPartitions={} clusterCharts={} diff --git a/ui/components/ClusterOverview/TopicChartsCard.tsx b/ui/components/ClusterOverview/TopicChartsCard.tsx index a6d1c2ca7..23e1daa91 100644 --- a/ui/components/ClusterOverview/TopicChartsCard.tsx +++ b/ui/components/ClusterOverview/TopicChartsCard.tsx @@ -22,11 +22,16 @@ export function TopicChartsCard({ isLoading, incoming, outgoing, + isVirtualKafkaCluster, }: - | ({ isLoading: false } & TopicChartsCardProps) | ({ - isLoading: true; - } & Partial<{ [key in keyof TopicChartsCardProps]?: undefined }>)) { + isLoading: false; + isVirtualKafkaCluster: boolean; + } & TopicChartsCardProps) + | ({ + isLoading: true; + isVirtualKafkaCluster?: boolean; + } & Partial<{ [key in keyof TopicChartsCardProps]?: undefined }>)) { const t = useTranslations(); return ( @@ -42,14 +47,22 @@ export function TopicChartsCard({ {t("topicMetricsCard.topics_bytes_incoming_and_outgoing")}{" "} - + {isLoading ? ( ) : ( - + )} diff --git a/ui/components/ClusterOverview/components/ChartIncomingOutgoing.tsx b/ui/components/ClusterOverview/components/ChartIncomingOutgoing.tsx index 0f93739a2..bdc420eb8 100644 --- a/ui/components/ClusterOverview/components/ChartIncomingOutgoing.tsx +++ b/ui/components/ClusterOverview/components/ChartIncomingOutgoing.tsx @@ -19,6 +19,7 @@ import { formatDateTime } from "@/utils/dateTime"; type ChartIncomingOutgoingProps = { incoming: TimeSeriesMetrics; outgoing: TimeSeriesMetrics; + isVirtualKafkaCluster: boolean; }; type Datum = { @@ -31,6 +32,7 @@ type Datum = { export function ChartIncomingOutgoing({ incoming, outgoing, + isVirtualKafkaCluster, }: ChartIncomingOutgoingProps) { const t = useTranslations(); const formatBytes = useFormatBytes(); @@ -41,13 +43,17 @@ export function ChartIncomingOutgoing({ const hasMetrics = Object.keys(incoming).length > 0 && Object.keys(outgoing).length > 0; - if (!hasMetrics) { + if (!hasMetrics || isVirtualKafkaCluster) { return ( ); } From c3049ce873779a082d6b9d2482a511c35196a9c9 Mon Sep 17 00:00:00 2001 From: hemahg Date: Wed, 14 Jan 2026 17:15:40 +0530 Subject: [PATCH 2/3] Add label if the cluster is kroxylicious virtua cluster Signed-off-by: hemahg --- .../kafka/[kafkaId]/ClusterLinks.tsx | 28 ++++++++++++++++--- .../[kafkaId]/KroxyliciousClusterLabel.tsx | 21 ++++++++++++++ ui/messages/en.json | 5 ++++ 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 ui/app/[locale]/(authorized)/kafka/[kafkaId]/KroxyliciousClusterLabel.tsx diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/ClusterLinks.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/ClusterLinks.tsx index 44fc84ffd..26b0d243a 100644 --- a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/ClusterLinks.tsx +++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/ClusterLinks.tsx @@ -1,8 +1,12 @@ import { ClusterDetail } from "@/api/kafka/schema"; import { NavItemLink } from "@/components/Navigation/NavItemLink"; -import { NavGroup, NavList } from "@/libs/patternfly/react-core"; +import { + NavGroup, + NavList, +} from "@/libs/patternfly/react-core"; import { useTranslations } from "next-intl"; import { Suspense } from "react"; +import { KroxyliciousClusterLabel } from "./KroxyliciousClusterLabel"; export function ClusterLinks({ kafkaDetail }: { kafkaDetail: ClusterDetail }) { const t = useTranslations(); @@ -14,7 +18,10 @@ export function ClusterLinks({ kafkaDetail }: { kafkaDetail: ClusterDetail }) { title={ ( - + ) as unknown as string } @@ -48,6 +55,19 @@ export function ClusterLinks({ kafkaDetail }: { kafkaDetail: ClusterDetail }) { ); } -function ClusterName({ kafkaName }: { kafkaName: string }) { - return `Cluster ${kafkaName}`; +function ClusterName({ + kafkaName, + kind, +}: { + kafkaName: string; + kind?: string | null; +}) { + const isKroxy = kind === "virtualkafkaclusters.kroxylicious.io"; + + return ( + <> + {`Cluster ${kafkaName}`} + {isKroxy && } + + ); } diff --git a/ui/app/[locale]/(authorized)/kafka/[kafkaId]/KroxyliciousClusterLabel.tsx b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/KroxyliciousClusterLabel.tsx new file mode 100644 index 000000000..6da93f2e2 --- /dev/null +++ b/ui/app/[locale]/(authorized)/kafka/[kafkaId]/KroxyliciousClusterLabel.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Label, Tooltip } from "@/libs/patternfly/react-core"; +import { InfoCircleIcon } from "@/libs/patternfly/react-icons"; +import { useTranslations } from "next-intl"; + +export function KroxyliciousClusterLabel() { + const t = useTranslations(); + return ( + + + + ); +} diff --git a/ui/messages/en.json b/ui/messages/en.json index 0d4f87c23..780e45e38 100644 --- a/ui/messages/en.json +++ b/ui/messages/en.json @@ -371,6 +371,7 @@ "Kafka_cluster_details": "Kafka cluster details" }, "ClusterChartsCard": { + "virtual_cluster_metrics_unavailable": "Metrics unavailable for Virtual Kafka Clusters", "data_unavailable": "Cluster metrics are not available for this cluster. Please check your configuration.", "cluster_metrics": "Cluster metrics", "used_disk_space": "Used disk space", @@ -773,5 +774,9 @@ "permissionType": "Permission" } + }, + "KroxyliciousCluster": { + "label": "Virtual", + "tooltip": "A virtual cluster is a logical Kafka endpoint that clients connect to. The proxy forwards requests to a physical Kafka cluster through its filters." } } From ed0fe86497257366b9b993b4b4da09ddb6f9f56b Mon Sep 17 00:00:00 2001 From: hemahg Date: Thu, 15 Jan 2026 12:31:19 +0530 Subject: [PATCH 3/3] Add label to the main kafka cluster table page Signed-off-by: hemahg --- ui/api/kafka/schema.ts | 1 + ui/components/ClustersTable.tsx | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ui/api/kafka/schema.ts b/ui/api/kafka/schema.ts index 778ae8ad0..29e71251b 100644 --- a/ui/api/kafka/schema.ts +++ b/ui/api/kafka/schema.ts @@ -11,6 +11,7 @@ export const ClusterListSchema = z.object({ type: z.literal("kafkas"), meta: z.object({ configured: z.boolean(), + kind: KafkaClusterKindSchema, authentication: z .union([ z.object({ diff --git a/ui/components/ClustersTable.tsx b/ui/components/ClustersTable.tsx index e611efa38..e341084da 100644 --- a/ui/components/ClustersTable.tsx +++ b/ui/components/ClustersTable.tsx @@ -6,6 +6,7 @@ import { ButtonLink } from "./Navigation/ButtonLink"; import { Link } from "@/i18n/routing"; import { Truncate } from "@/libs/patternfly/react-core"; import { EmptyStateNoMatchFound } from "./Table/EmptyStateNoMatchFound"; +import { KroxyliciousClusterLabel } from "@/app/[locale]/(authorized)/kafka/[kafkaId]/KroxyliciousClusterLabel"; export const ClusterColumns = [ "name", @@ -91,18 +92,28 @@ export function ClustersTable({ }} renderCell={({ key, column, row, Td }) => { switch (column) { - case "name": + case "name": { + const isVirtualCluster = + row.meta?.kind === "virtualkafkaclusters.kroxylicious.io"; + return ( {authenticated ? ( - - - + <> + + + + {isVirtualCluster && } + ) : ( - + <> + {row.attributes.name} + {isVirtualCluster && } + )} ); + } case "version": return (