Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ui/api/kafka/schema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
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"),
meta: z.object({
configured: z.boolean(),
kind: KafkaClusterKindSchema,
authentication: z
.union([
z.object({
Expand Down Expand Up @@ -55,6 +61,7 @@ const ClusterDetailSchema = z.object({
meta: z
.object({
reconciliationPaused: z.boolean().optional(),
kind: KafkaClusterKindSchema,
managed: z.boolean(),
privileges: z.array(z.string()).optional(),
})
Expand Down
28 changes: 24 additions & 4 deletions ui/app/[locale]/(authorized)/kafka/[kafkaId]/ClusterLinks.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -14,7 +18,10 @@ export function ClusterLinks({ kafkaDetail }: { kafkaDetail: ClusterDetail }) {
title={
(
<Suspense>
<ClusterName kafkaName={kafkaDetail.attributes.name} />
<ClusterName
kafkaName={kafkaDetail.attributes.name}
kind={kafkaDetail.meta?.kind}
/>
</Suspense>
) as unknown as string
}
Expand Down Expand Up @@ -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 && <KroxyliciousClusterLabel />}
</>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip content={t("KroxyliciousCluster.tooltip")}>
<Label
isCompact
color="blue"
icon={<InfoCircleIcon />}
className="pf-v6-u-ml-sm"
>
{t("KroxyliciousCluster.label")}
</Label>
</Tooltip>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import { ClusterDetail } from "@/api/kafka/schema";
import { ClusterChartsCard } from "@/components/ClusterOverview/ClusterChartsCard";

function timeSeriesMetrics(
ranges: Record<string, { range: string[][]; nodeId?: string; }[]> | undefined,
ranges: Record<string, { range: string[][]; nodeId?: string }[]> | undefined,
rangeName: string,
): Record<string, TimeSeriesMetrics> {
const series: Record<string, TimeSeriesMetrics> = {};

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,
);
});
}

Expand All @@ -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 (
<Card>
<CardHeader>
Expand All @@ -52,10 +67,10 @@ export async function ConnectedClusterChartsCard({
</CardHeader>
<CardBody>
<Alert
variant="warning"
variant={isVirtualKafkaCluster ? "info" : "warning"}
isInline
isPlain
title={t("ClusterChartsCard.data_unavailable")}
title={alertTitle}
/>
</CardBody>
</Card>
Expand All @@ -64,11 +79,23 @@ export async function ConnectedClusterChartsCard({

return (
<ClusterChartsCard
isLoading={ false }
usedDiskSpace={ timeSeriesMetrics(res?.attributes.metrics?.ranges, "volume_stats_used_bytes") }
availableDiskSpace={ timeSeriesMetrics(res?.attributes.metrics?.ranges, "volume_stats_capacity_bytes") }
memoryUsage={ timeSeriesMetrics(res?.attributes.metrics?.ranges, "memory_usage_bytes") }
cpuUsage={ timeSeriesMetrics(res?.attributes.metrics?.ranges, "cpu_usage_seconds") }
isLoading={false}
usedDiskSpace={timeSeriesMetrics(
res?.attributes.metrics?.ranges,
"volume_stats_used_bytes",
)}
availableDiskSpace={timeSeriesMetrics(
res?.attributes.metrics?.ranges,
"volume_stats_capacity_bytes",
)}
memoryUsage={timeSeriesMetrics(
res?.attributes.metrics?.ranges,
"memory_usage_bytes",
)}
cpuUsage={timeSeriesMetrics(
res?.attributes.metrics?.ranges,
"cpu_usage_seconds",
)}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { ClusterDetail } from "@/api/kafka/schema";
import { TopicChartsCard } from "@/components/ClusterOverview/TopicChartsCard";

function timeSeriesMetrics(
ranges: Record<string, { range: string[][]; nodeId?: string; }[]> | undefined,
ranges: Record<string, { range: string[][]; nodeId?: string }[]> | 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,
);
});
}

Expand All @@ -26,8 +29,17 @@ export async function ConnectedTopicChartsCard({
return (
<TopicChartsCard
isLoading={false}
incoming={timeSeriesMetrics(res?.attributes.metrics?.ranges, "incoming_byte_rate")}
outgoing={timeSeriesMetrics(res?.attributes.metrics?.ranges, "outgoing_byte_rate")}
isVirtualKafkaCluster={
res?.meta?.kind === "virtualkafkaclusters.kroxylicious.io"
}
incoming={timeSeriesMetrics(
res?.attributes.metrics?.ranges,
"incoming_byte_rate",
)}
outgoing={timeSeriesMetrics(
res?.attributes.metrics?.ranges,
"outgoing_byte_rate",
)}
/>
);
}
21 changes: 16 additions & 5 deletions ui/app/[locale]/(authorized)/kafka/[kafkaId]/overview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<PageLayout
clusterOverview={
<ConnectedClusterCard cluster={kafkaCluster} consumerGroups={consumerGroups} />
<ConnectedClusterCard
cluster={kafkaCluster}
consumerGroups={consumerGroups}
/>
}
topicsPartitions={<ConnectedTopicsPartitionsCard data={topics} />}
clusterCharts={<ConnectedClusterChartsCard cluster={kafkaCluster} />}
Expand Down
23 changes: 18 additions & 5 deletions ui/components/ClusterOverview/TopicChartsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -42,14 +47,22 @@ export function TopicChartsCard({
<Flex direction={{ default: "column" }} gap={{ default: "gapLg" }}>
<b>
{t("topicMetricsCard.topics_bytes_incoming_and_outgoing")}{" "}
<Tooltip content={t("topicMetricsCard.topics_bytes_incoming_and_outgoing_tooltip")}>
<Tooltip
content={t(
"topicMetricsCard.topics_bytes_incoming_and_outgoing_tooltip",
)}
>
<HelpIcon />
</Tooltip>
</b>
{isLoading ? (
<ChartSkeletonLoader />
) : (
<ChartIncomingOutgoing incoming={incoming} outgoing={outgoing} />
<ChartIncomingOutgoing
incoming={incoming}
outgoing={outgoing}
isVirtualKafkaCluster={isVirtualKafkaCluster}
/>
)}
</Flex>
</CardBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { formatDateTime } from "@/utils/dateTime";
type ChartIncomingOutgoingProps = {
incoming: TimeSeriesMetrics;
outgoing: TimeSeriesMetrics;
isVirtualKafkaCluster: boolean;
};

type Datum = {
Expand All @@ -31,6 +32,7 @@ type Datum = {
export function ChartIncomingOutgoing({
incoming,
outgoing,
isVirtualKafkaCluster,
}: ChartIncomingOutgoingProps) {
const t = useTranslations();
const formatBytes = useFormatBytes();
Expand All @@ -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 (
<Alert
variant="warning"
variant={isVirtualKafkaCluster ? "info" : "warning"}
isInline
isPlain
title={t("ChartIncomingOutgoing.data_unavailable")}
title={
isVirtualKafkaCluster
? t("ClusterChartsCard.virtual_cluster_metrics_unavailable")
: t("ChartIncomingOutgoing.data_unavailable")
}
/>
);
}
Expand Down
21 changes: 16 additions & 5 deletions ui/components/ClustersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 (
<Td key={key}>
{authenticated ? (
<Link href={`/kafka/${row.id}`}>
<Truncate content={row.attributes.name} />
</Link>
<>
<Link href={`/kafka/${row.id}`}>
<Truncate content={row.attributes.name} />
</Link>
{isVirtualCluster && <KroxyliciousClusterLabel />}
</>
) : (
<Truncate content={row.attributes.name} />
<>
{row.attributes.name}
{isVirtualCluster && <KroxyliciousClusterLabel />}
</>
)}
</Td>
);
}
case "version":
return (
<Td key={key}>
Expand Down
Loading
Loading