+ {/*
{ metrics.values.replica_count
.find((e: any) => parseInt(e.nodeId) == row.id)?.value ?? "-" }
+*/}
+ TODO
);
case "rack":
diff --git a/ui/app/[locale]/kafka/[kafkaId]/nodes/[nodeId]/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/nodes/[nodeId]/page.tsx
index 0aaac9845..26d4c1fef 100644
--- a/ui/app/[locale]/kafka/[kafkaId]/nodes/[nodeId]/page.tsx
+++ b/ui/app/[locale]/kafka/[kafkaId]/nodes/[nodeId]/page.tsx
@@ -2,5 +2,5 @@ import { KafkaNodeParams } from "@/app/[locale]/kafka/[kafkaId]/nodes/kafkaNode.
import { redirect } from "@/navigation";
export default function NodePage({ params }: { params: KafkaNodeParams }) {
- redirect(`${params.nodeId}/configuration`);
+ redirect(`/kafka/${params.kafkaId}/nodes/${params.nodeId}/configuration`);
}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx
index d9e8560ef..c357c7266 100644
--- a/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx
+++ b/ui/app/[locale]/kafka/[kafkaId]/nodes/page.tsx
@@ -15,7 +15,7 @@ export default async function NodesPage({ params }: { params: KafkaParams }) {
);
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ChartDiskUsage.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ChartDiskUsage.tsx
new file mode 100644
index 000000000..9de7d08fc
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ChartDiskUsage.tsx
@@ -0,0 +1,135 @@
+"use client";
+import {
+ Chart,
+ ChartArea,
+ ChartAxis,
+ ChartLegend,
+ ChartThemeColor,
+ ChartThreshold,
+ ChartVoronoiContainer,
+} from "@/libs/patternfly/react-charts";
+import { useFormatBytes } from "@/utils/format";
+import { chart_color_orange_300 } from "@patternfly/react-tokens";
+import { useFormatter } from "next-intl";
+import { useChartWidth } from "./useChartWidth";
+
+export type TimeSeriesMetrics = { [timestamp: string]: number };
+
+type ChartData = {
+ areaColor: string;
+ softLimitColor: string;
+ area: BrokerChartData[];
+ softLimit: BrokerChartData[];
+};
+
+type BrokerChartData = {
+ name: string;
+ x: number | string;
+ y: number;
+};
+
+type LegendData = {
+ name: string;
+ symbol?: {
+ fill?: string;
+ type?: string;
+ };
+};
+
+type ChartLinearWithOptionalLimitProps = {
+ usage: TimeSeriesMetrics;
+ available: TimeSeriesMetrics;
+};
+
+export function ChartDiskUsage({
+ usage,
+ available,
+}: ChartLinearWithOptionalLimitProps) {
+ const format = useFormatter();
+ const formatBytes = useFormatBytes();
+ const [containerRef, width] = useChartWidth();
+
+ const itemsPerRow = width && width > 650 ? 6 : 3;
+
+ const hasMetrics = Object.keys(usage).length > 0;
+ if (!hasMetrics) {
+ return TODO
;
+ }
+ // const showDate = shouldShowDate(duration);
+ const usageArray = Object.entries(usage);
+ const maxUsage = Math.max(...Object.values(usage));
+
+ return (
+
+ `${datum.name}: ${formatBytes(datum.y)}`}
+ constrainToVisibleArea
+ />
+ }
+ legendPosition="bottom-left"
+ legendComponent={
+
+ }
+ height={350}
+ padding={{ bottom: 70, top: 0, left: 90, right: 20 }}
+ themeColor={ChartThemeColor.multiUnordered}
+ width={width}
+ legendAllowWrap={true}
+ >
+ {
+ const [_, time] = format
+ .dateTime(d, {
+ dateStyle: "short",
+ timeStyle: "short",
+ timeZone: "UTC",
+ })
+ .split(" ");
+ return time;
+ }}
+ />
+ {
+ return formatBytes(d);
+ }}
+ />
+ ({
+ name: "Used storage",
+ x,
+ y,
+ }))}
+ />
+ ({
+ name: "Available storage threshold",
+ x: usageArray[idx][0],
+ y,
+ }))}
+ style={{
+ data: {
+ stroke: chart_color_orange_300.var,
+ },
+ }}
+ />
+
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader.stories.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader.stories.tsx
new file mode 100644
index 000000000..e0838ae28
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader.stories.tsx
@@ -0,0 +1,20 @@
+import type { ComponentStory, ComponentMeta } from "@storybook/react";
+
+import { ChartSkeletonLoader } from "./ChartSkeletonLoader";
+
+export default {
+ component: ChartSkeletonLoader,
+ args: {},
+ parameters: {
+ backgrounds: {
+ default: "Background color 100",
+ },
+ },
+} as ComponentMeta;
+
+const Template: ComponentStory = (args) => (
+
+);
+
+export const Story = Template.bind({});
+Story.args = {};
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader.tsx
new file mode 100644
index 000000000..fa56de6d3
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader.tsx
@@ -0,0 +1,26 @@
+import { Flex, FlexItem, Skeleton } from "@/libs/patternfly/react-core";
+
+export function ChartSkeletonLoader({
+ height,
+ padding,
+}: {
+ height: number;
+ padding: number;
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterCard.stories.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterCard.stories.tsx
new file mode 100644
index 000000000..a085c0b71
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterCard.stories.tsx
@@ -0,0 +1,81 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { ClusterCard } from "./ClusterCard";
+
+const meta: Meta = {
+ component: ClusterCard,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const WithData: Story = {
+ args: {
+ isLoading: false,
+ name: "my-kafka-cluster",
+ status: "ready",
+ brokersTotal: 9999,
+ brokersOnline: 9999,
+ consumerGroups: 9999,
+ kafkaVersion: "3.5.6",
+ messages: [
+ {
+ variant: "danger",
+ subject: { type: "broker", name: "Broker 1", id: "1" },
+ message:
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, accusantium amet consequuntur delectus dolor excepturi hic illum iure labore magnam nemo nobis non obcaecati odit officia qui quo saepe sapiente",
+ date: "2023-12-09T13:54:17.687Z",
+ },
+ {
+ variant: "warning",
+ subject: { type: "topic", name: "Pet-sales", id: "1" },
+ message:
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, accusantium amet consequuntur delectus dolor excepturi hic illum iure labore magnam nemo nobis non obcaecati odit officia qui quo saepe sapiente",
+ date: "2023-12-10T13:54:17.687Z",
+ },
+ {
+ variant: "warning",
+ subject: { type: "topic", name: "night-orders", id: "1" },
+ message:
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, accusantium amet consequuntur delectus dolor excepturi hic illum iure labore magnam nemo nobis non obcaecati odit officia qui quo saepe sapiente",
+ date: "2023-12-11T13:54:17.687Z",
+ },
+ {
+ variant: "danger",
+ subject: { type: "topic", name: "night-orders", id: "1" },
+ message:
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, accusantium amet consequuntur delectus dolor excepturi hic illum iure labore magnam nemo nobis non obcaecati odit officia qui quo saepe sapiente",
+ date: "2023-12-12T13:54:17.687Z",
+ },
+ {
+ variant: "danger",
+ subject: {
+ type: "topic",
+ name: "very-very-very-long-name-that-will-cause-problems",
+ id: "1",
+ },
+ message:
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab, accusantium amet consequuntur delectus dolor excepturi hic illum iure labore magnam nemo nobis non obcaecati odit officia qui quo saepe sapiente",
+ date: "2023-12-13T13:54:17.687Z",
+ },
+ ],
+ },
+};
+
+export const NoMessages: Story = {
+ args: {
+ isLoading: false,
+ name: "my-kafka-cluster",
+ status: "ready",
+ brokersTotal: 9999,
+ brokersOnline: 9999,
+ consumerGroups: 9999,
+ kafkaVersion: "3.5.6",
+ messages: [],
+ },
+};
+export const Loading: Story = {
+ args: {
+ isLoading: true,
+ },
+};
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterCard.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterCard.tsx
new file mode 100644
index 000000000..c08502bd7
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterCard.tsx
@@ -0,0 +1,276 @@
+"use client";
+import { ExpandableMessages } from "@/app/[locale]/kafka/[kafkaId]/overview/ExpandableMessages";
+import { DateTime } from "@/components/DateTime";
+import { Number } from "@/components/Number";
+import {
+ Card,
+ CardBody,
+ DataList,
+ DataListCell,
+ DataListItem,
+ DataListItemCells,
+ DataListItemRow,
+ Divider,
+ Flex,
+ FlexItem,
+ Grid,
+ GridItem,
+ Icon,
+ Skeleton,
+ Text,
+ TextContent,
+ Title,
+ Truncate,
+} from "@/libs/patternfly/react-core";
+import {
+ CheckCircleIcon,
+ ExclamationCircleIcon,
+ ExclamationTriangleIcon,
+} from "@/libs/patternfly/react-icons";
+import { Link } from "@/navigation";
+
+type ClusterCardProps = {
+ name: string;
+ status: string;
+ brokersOnline: number;
+ brokersTotal: number;
+ consumerGroups: number;
+ kafkaVersion: string;
+ messages: Array<{
+ variant: "danger" | "warning";
+ subject: { type: "cluster" | "broker" | "topic"; name: string; id: string };
+ message: string;
+ date: string;
+ }>;
+};
+
+export function ClusterCard({
+ isLoading,
+ name,
+ status,
+ brokersOnline,
+ brokersTotal,
+ consumerGroups,
+ kafkaVersion,
+ messages,
+}:
+ | ({
+ isLoading: false;
+ } & ClusterCardProps)
+ | ({ isLoading: true } & { [K in keyof ClusterCardProps]?: undefined })) {
+ const warnings = messages?.filter((m) => m.variant === "warning").length || 0;
+ const dangers = messages?.filter((m) => m.variant === "danger").length || 0;
+ return (
+
+
+
+
+
+
+ {isLoading ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+ {status ? (
+
+ ) : (
+
+ )}
+
+
+ {name}
+
+
+ {status}
+
+ >
+ )}
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ <>
+
+ /
+
+ >
+ )}
+
+
+ Online brokers
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ Consumer groups
+
+
+
+
+ {isLoading ? : kafkaVersion}
+
+
+ Kafka version
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterChartsCard.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterChartsCard.tsx
new file mode 100644
index 000000000..6fbfadce2
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ClusterChartsCard.tsx
@@ -0,0 +1,46 @@
+"use client";
+import {
+ ChartDiskUsage,
+ TimeSeriesMetrics,
+} from "@/app/[locale]/kafka/[kafkaId]/overview/ChartDiskUsage";
+import { ChartSkeletonLoader } from "@/app/[locale]/kafka/[kafkaId]/overview/ChartSkeletonLoader";
+import {
+ Card,
+ CardBody,
+ CardHeader,
+ CardTitle,
+} from "@/libs/patternfly/react-core";
+import { useFormatBytes } from "@/utils/format";
+
+type ClusterChartsCardProps = {
+ usedDiskSpace: TimeSeriesMetrics;
+ availableDiskSpace: TimeSeriesMetrics;
+};
+
+export function ClusterChartsCard({
+ isLoading,
+ usedDiskSpace,
+ availableDiskSpace,
+}:
+ | ({ isLoading: false } & ClusterChartsCardProps)
+ | ({
+ isLoading: true;
+ } & Partial<{ [key in keyof ClusterChartsCardProps]?: undefined }>)) {
+ const formatter = useFormatBytes();
+ return (
+
+
+ Available disk space
+
+
+ {isLoading && }
+ {!isLoading && (
+
+ )}
+
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/ExpandableMessages.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/ExpandableMessages.tsx
new file mode 100644
index 000000000..076fc085a
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/ExpandableMessages.tsx
@@ -0,0 +1,40 @@
+"use client";
+import { Label, LabelGroup, Title } from "@/libs/patternfly/react-core";
+import {
+ ExclamationCircleIcon,
+ ExclamationTriangleIcon,
+} from "@/libs/patternfly/react-icons";
+import { ExpandableSection } from "@patternfly/react-core";
+import { PropsWithChildren, useState } from "react";
+
+export function ExpandableMessages({
+ warnings,
+ dangers,
+ children,
+}: PropsWithChildren<{
+ warnings: number;
+ dangers: number;
+}>) {
+ const [showMessages, setShowMessages] = useState(warnings + dangers > 0);
+ return (
+ setShowMessages(isOpen)}
+ toggleContent={
+
+ Cluster errors and warnings
+
+
+
+
+
+ }
+ >
+ {children}
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/PageLayout.stories.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/PageLayout.stories.tsx
new file mode 100644
index 000000000..734e1306f
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/PageLayout.stories.tsx
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { composeStories } from "@storybook/react";
+import * as ClusterCardStories from "./ClusterCard.stories";
+import { PageLayout } from "./PageLayout";
+import * as TopicsPartitionsCardStories from "./TopicsPartitionsCard.stories";
+
+const { WithData: ClusterWithData } = composeStories(ClusterCardStories);
+const { WithData: TopicsPartitionsWithData } = composeStories(
+ TopicsPartitionsCardStories,
+);
+
+const meta: Meta = {
+ component: PageLayout,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const WithData: Story = {
+ render: (args) => (
+ }
+ clusterCharts={todo
}
+ topicsPartitions={
+
+ }
+ />
+ ),
+};
+export const Loading: Story = {
+ render: (args) => (
+ }
+ clusterCharts={}
+ topicsPartitions={}
+ />
+ ),
+};
+
+async function Everloading() {
+ await new Promise(() => {});
+ return hello
;
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/PageLayout.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/PageLayout.tsx
new file mode 100644
index 000000000..1e11b5fbf
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/PageLayout.tsx
@@ -0,0 +1,47 @@
+import { ClusterCard } from "@/app/[locale]/kafka/[kafkaId]/overview/ClusterCard";
+import { ClusterChartsCard } from "@/app/[locale]/kafka/[kafkaId]/overview/ClusterChartsCard";
+import { TopicsPartitionsCard } from "@/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard";
+import {
+ Flex,
+ FlexItem,
+ Grid,
+ GridItem,
+ PageSection,
+} from "@/libs/patternfly/react-core";
+import { ReactNode, Suspense } from "react";
+
+export function PageLayout({
+ clusterOverview,
+ clusterCharts,
+ topicsPartitions,
+}: {
+ clusterOverview: ReactNode;
+ clusterCharts: ReactNode;
+ topicsPartitions: ReactNode;
+}) {
+ return (
+
+
+
+
+
+ }>
+ {clusterOverview}
+
+
+
+ }>
+ {clusterCharts}
+
+
+
+
+
+ }>
+ {topicsPartitions}
+
+
+
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard.stories.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard.stories.tsx
new file mode 100644
index 000000000..bbf2150c6
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard.stories.tsx
@@ -0,0 +1,25 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { TopicsPartitionsCard } from "./TopicsPartitionsCard";
+
+const meta: Meta = {
+ component: TopicsPartitionsCard,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const WithData: Story = {
+ args: {
+ isLoading: false,
+ topicsTotal: 999999,
+ topicsReplicated: 400000,
+ topicsUnderReplicated: 400000,
+ partitions: 999999,
+ },
+};
+export const Loading: Story = {
+ args: {
+ isLoading: true,
+ },
+};
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard.tsx
new file mode 100644
index 000000000..1a1fe019f
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard.tsx
@@ -0,0 +1,163 @@
+import { Number } from "@/components/Number";
+import {
+ Card,
+ CardBody,
+ CardHeader,
+ CardTitle,
+ Divider,
+ Flex,
+ FlexItem,
+ Icon,
+ Skeleton,
+ Text,
+ TextContent,
+} from "@/libs/patternfly/react-core";
+import {
+ CheckCircleIcon,
+ ExclamationCircleIcon,
+ ExclamationTriangleIcon,
+} from "@/libs/patternfly/react-icons";
+import { Link } from "@/navigation";
+
+type TopicsPartitionsCardProps = {
+ topicsTotal: number;
+ topicsReplicated: number;
+ topicsUnderReplicated: number;
+ partitions: number;
+};
+
+export function TopicsPartitionsCard({
+ isLoading,
+ topicsTotal,
+ topicsReplicated,
+ topicsUnderReplicated,
+ partitions,
+}:
+ | ({ isLoading: false } & TopicsPartitionsCardProps)
+ | ({
+ isLoading: true;
+ } & Partial<{ [key in keyof TopicsPartitionsCardProps]?: undefined }>)) {
+ return (
+
+ View all,
+ }}
+ >
+ Topics
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+
+ total topics
+
+
+
+
+
+
+
+
+
+ partitions
+
+
+
+
+ )}
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Fully replicated
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Under replicated
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ Unavailable
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx
index a9346a603..abf1420d4 100644
--- a/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/page.tsx
@@ -1,449 +1,91 @@
-"use client";
import {
- ChartArea,
- ChartGroup,
- ChartVoronoiContainer,
-} from "@/libs/patternfly/react-charts";
+ getKafkaClusterKpis,
+ getKafkaClusterMetrics,
+ Range,
+} from "@/api/kafka/actions";
import {
- Card,
- CardBody,
- CardHeader,
- CardTitle,
- Flex,
- FlexItem,
- Gallery,
- GalleryItem,
- PageSection,
- Title,
-} from "@/libs/patternfly/react-core";
-import {
- CheckCircleIcon,
- TimesCircleIcon,
-} from "@/libs/patternfly/react-icons";
+ ClusterDetail,
+ ClusterKpis,
+ ClusterMetricRange,
+} from "@/api/kafka/schema";
+import { KafkaParams } from "@/app/[locale]/kafka/[kafkaId]/kafka.params";
+import { ClusterCard } from "@/app/[locale]/kafka/[kafkaId]/overview/ClusterCard";
+import { ClusterChartsCard } from "@/app/[locale]/kafka/[kafkaId]/overview/ClusterChartsCard";
+import { PageLayout } from "@/app/[locale]/kafka/[kafkaId]/overview/PageLayout";
+import { TopicsPartitionsCard } from "@/app/[locale]/kafka/[kafkaId]/overview/TopicsPartitionsCard";
-export default function OverviewPage() {
+export default function OverviewPage({ params }: { params: KafkaParams }) {
+ const kpi = getKafkaClusterKpis(params.kafkaId);
+ const range = getKafkaClusterMetrics(params.kafkaId, [
+ "volumeUsed",
+ "volumeCapacity",
+ ]);
return (
- <>
-
-
-
- Nodes
-
-
-
-
-
-
- 3
-
-
-
-
-
- 1
-
-
-
-
-
-
- Topics
-
-
-
-
-
- 123
-
-
-
- Total
-
-
-
+ }
+ topicsPartitions={}
+ clusterCharts={}
+ />
+ );
+}
-
-
-
-
- 842
-
-
-
- Partitions
-
-
-
+async function ConnectedClusterCard({
+ data,
+}: {
+ data: Promise<{ cluster: ClusterDetail; kpis: ClusterKpis } | null>;
+}) {
+ const res = await data;
+ const brokersTotal = Object.keys(res?.kpis.broker_state || {}).length;
+ const brokersOnline =
+ Object.values(res?.kpis.broker_state || {}).filter((s) => s === 3).length ||
+ 0;
+ return (
+
+ );
+}
-
-
-
-
- 3,45 GB
-
-
-
- Storage
-
-
-
-
-
-
-
-
- Traffic
-
-
-
-
-
- 894,8 KB/s
-
-
-
- Egress
-
-
-
+async function ConnectedClusterChartsCard({
+ data,
+}: {
+ data: Promise<{
+ cluster: ClusterDetail;
+ ranges: Record;
+ } | null>;
+}) {
+ const res = await data;
+ return (
+
+ );
+}
-
-
-
-
- 456,3 KB/s
-
-
-
- Ingress
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1,050,765 IOPS
-
-
-
-
- Workload
-
-
-
-
- `${datum.name}: ${datum.y}`}
- constrainToVisibleArea
- />
- }
- height={100}
- maxDomain={{ y: 9 }}
- padding={0}
- width={400}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- 1,050,765 IOPS
-
-
-
-
- Workload
-
-
-
-
- `${datum.name}: ${datum.y}`}
- constrainToVisibleArea
- />
- }
- height={100}
- maxDomain={{ y: 9 }}
- padding={0}
- width={400}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- 1,050,765 IOPS
-
-
-
-
- Workload
-
-
-
-
- `${datum.name}: ${datum.y}`}
- constrainToVisibleArea
- />
- }
- height={100}
- maxDomain={{ y: 9 }}
- padding={0}
- width={400}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- 1,050,765 IOPS
-
-
-
-
- Workload
-
-
-
-
- `${datum.name}: ${datum.y}`}
- constrainToVisibleArea
- />
- }
- height={100}
- maxDomain={{ y: 9 }}
- padding={0}
- width={400}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- 1,050,765 IOPS
-
-
-
-
- Workload
-
-
-
-
- `${datum.name}: ${datum.y}`}
- constrainToVisibleArea
- />
- }
- height={100}
- maxDomain={{ y: 9 }}
- padding={0}
- width={400}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- 1,050,765 IOPS
-
-
-
-
- Workload
-
-
-
-
- `${datum.name}: ${datum.y}`}
- constrainToVisibleArea
- />
- }
- height={100}
- maxDomain={{ y: 9 }}
- padding={0}
- width={400}
- >
-
-
-
-
-
-
-
- >
+async function ConnectedTopicsPartitionsCard({
+ data,
+}: {
+ data: Promise<{ cluster: ClusterDetail; kpis: ClusterKpis } | null>;
+}) {
+ const res = await data;
+ const topicsTotal = res?.kpis.total_topics || 0;
+ const topicsUnderreplicated = res?.kpis.underreplicated_topics || 0;
+ return (
+
);
}
diff --git a/ui/app/[locale]/kafka/[kafkaId]/overview/useChartWidth.tsx b/ui/app/[locale]/kafka/[kafkaId]/overview/useChartWidth.tsx
new file mode 100644
index 000000000..d432a2f74
--- /dev/null
+++ b/ui/app/[locale]/kafka/[kafkaId]/overview/useChartWidth.tsx
@@ -0,0 +1,20 @@
+import type { RefObject } from "react";
+import { useEffect, useLayoutEffect, useRef, useState } from "react";
+
+export function useChartWidth(): [RefObject, number] {
+ const containerRef = useRef(null);
+ const [width, setWidth] = useState(0);
+
+ const handleResize = () =>
+ containerRef.current && setWidth(containerRef.current.clientWidth);
+
+ useLayoutEffect(() => {
+ handleResize();
+ }, []);
+
+ useEffect(() => {
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+ return [containerRef, width];
+}
diff --git a/ui/components/AppHeader.tsx b/ui/components/AppHeader.tsx
index e5c0c5d4a..76504eeb2 100644
--- a/ui/components/AppHeader.tsx
+++ b/ui/components/AppHeader.tsx
@@ -1,18 +1,20 @@
import {
Divider,
+ Flex,
+ FlexItem,
PageSection,
- Split,
- SplitItem,
Title,
} from "@/libs/patternfly/react-core";
import { ReactNode } from "react";
export function AppHeader({
title,
+ subTitle,
actions,
navigation,
}: {
title: ReactNode;
+ subTitle?: ReactNode;
actions?: ReactNode[];
navigation?: ReactNode;
}) {
@@ -24,13 +26,27 @@ export function AppHeader({
className={navigation ? "pf-v5-u-px-lg pf-v5-u-pt-sm" : undefined}
hasShadowBottom={!navigation}
>
-
-
- {title}
-
- {actions &&
- actions.map((a, idx) => {a})}
-
+
+
+
+ {title}
+
+ {subTitle && {subTitle}}
+
+ {actions && (
+
+
+ {actions.map((a, idx) => (
+ {a}
+ ))}
+
+
+ )}
+
{navigation}
{navigation && }
diff --git a/ui/environment.d.ts b/ui/environment.d.ts
index bbaffd677..86824cecd 100644
--- a/ui/environment.d.ts
+++ b/ui/environment.d.ts
@@ -7,6 +7,7 @@ namespace NodeJS {
KEYCLOAK_CLIENTID: string;
KEYCLOAK_CLIENTSECRET: string;
NEXT_PUBLIC_KEYCLOAK_URL: string;
+ CONSOLE_METRICS_PROMETHEUS_URL: string;
LOG_LEVEL: "fatal" | "error" | "warn" | "info" | "debug" | "trace";
CONSOLE_MODE: "read-only" | "read-write";
}
diff --git a/ui/libs/patternfly/react-tokens.ts b/ui/libs/patternfly/react-tokens.ts
new file mode 100644
index 000000000..3772e924c
--- /dev/null
+++ b/ui/libs/patternfly/react-tokens.ts
@@ -0,0 +1,2 @@
+"use client";
+export * from "@patternfly/react-tokens";
diff --git a/ui/package.json b/ui/package.json
index 26ac0220d..9c9bf9818 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -17,6 +17,7 @@
"@patternfly/react-core": "^5.1.2",
"@patternfly/react-icons": "^5.1.2",
"@patternfly/react-table": "^5.1.2",
+ "@patternfly/react-tokens": "^5.1.2",
"@types/lodash.groupby": "^4.6.9",
"@types/node": "20.10.4",
"@types/react": "18.2.43",
@@ -34,6 +35,7 @@
"next-intl": "^3.2.2",
"next-logger": "^3.0.2",
"pino": "^8.16.2",
+ "prometheus-query": "^3.3.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.3.2",
@@ -44,7 +46,6 @@
"@storybook/addon-essentials": "^7.6.4",
"@storybook/addon-interactions": "^7.6.3",
"@storybook/addon-links": "^7.6.4",
- "@storybook/addon-onboarding": "^1.0.9",
"@storybook/blocks": "^7.6.4",
"@storybook/nextjs": "^7.6.3",
"@storybook/react": "^7.6.3",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index f1c1cb5be..778cc8443 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -23,6 +23,9 @@ dependencies:
'@patternfly/react-table':
specifier: ^5.1.2
version: 5.1.2(react-dom@18.2.0)(react@18.2.0)
+ '@patternfly/react-tokens':
+ specifier: ^5.1.2
+ version: 5.1.2
'@types/lodash.groupby':
specifier: ^4.6.9
version: 4.6.9
@@ -74,6 +77,9 @@ dependencies:
pino:
specifier: ^8.16.2
version: 8.16.2
+ prometheus-query:
+ specifier: ^3.3.3
+ version: 3.3.3
react:
specifier: 18.2.0
version: 18.2.0
@@ -100,9 +106,6 @@ devDependencies:
'@storybook/addon-links':
specifier: ^7.6.4
version: 7.6.4(react@18.2.0)
- '@storybook/addon-onboarding':
- specifier: ^1.0.9
- version: 1.0.9(react-dom@18.2.0)(react@18.2.0)
'@storybook/blocks':
specifier: ^7.6.4
version: 7.6.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0)
@@ -2100,7 +2103,7 @@ packages:
react-dom: ^17 || ^18
dependencies:
'@patternfly/react-styles': 5.1.1
- '@patternfly/react-tokens': 5.1.1
+ '@patternfly/react-tokens': 5.1.2
hoist-non-react-statics: 3.3.2
lodash: 4.17.21
react: 18.2.0
@@ -2175,10 +2178,6 @@ packages:
tslib: 2.6.1
dev: false
- /@patternfly/react-tokens@5.1.1:
- resolution: {integrity: sha512-cHuNkzNA9IY9aDwfjSEkitQoVEvRhOJRKhH0yIRlRByEkbdoV9jJZ9xj20hNShE+bxmNuom+MCTQSkpkN1bV8A==}
- dev: false
-
/@patternfly/react-tokens@5.1.2:
resolution: {integrity: sha512-hu/6kEEMnyDc4GiMiaEau3kYq0BZoB3X1tZLcNfg9zQZnOydUgaLcUgR8+IlMF/nVVIqNjZF2RA/5lmKAVz2cQ==}
dev: false
@@ -2954,21 +2953,6 @@ packages:
tiny-invariant: 1.3.1
dev: true
- /@storybook/addon-onboarding@1.0.9(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-HlHm05Py18XOf4g7abiWkvb2WteoHcRNk1PY3Wtsmjuu5aAAjBmp4mVEg59xEeA2HAMICZ2fb72NIpFlBvDN+g==}
- peerDependencies:
- react: ^16.8.0 || ^17.0.0 || ^18.0.0
- react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- dependencies:
- '@storybook/telemetry': 7.6.3
- react: 18.2.0
- react-confetti: 6.1.0(react@18.2.0)
- react-dom: 18.2.0(react@18.2.0)
- transitivePeerDependencies:
- - encoding
- - supports-color
- dev: true
-
/@storybook/addon-outline@7.6.4:
resolution: {integrity: sha512-CFxGASRse/qeFocetDKFNeWZ3Aa2wapVtRciDNa4Zx7k1wCnTjEsPIm54waOuCaNVcrvO+nJUAZG5WyiorQvcg==}
dependencies:
@@ -3387,22 +3371,6 @@ packages:
- supports-color
dev: true
- /@storybook/csf-tools@7.6.3:
- resolution: {integrity: sha512-Zi3pg2pg88/mvBKewkfWhFUR1J4uYpHI5fSjOE+J/FeZObX/DIE7r+wJxZ0UBGyrk0Wy7Jajlb2uSP56Y0i19w==}
- dependencies:
- '@babel/generator': 7.23.0
- '@babel/parser': 7.23.0
- '@babel/traverse': 7.23.2
- '@babel/types': 7.23.0
- '@storybook/csf': 0.1.2
- '@storybook/types': 7.6.3
- fs-extra: 11.1.1
- recast: 0.23.4
- ts-dedent: 2.2.0
- transitivePeerDependencies:
- - supports-color
- dev: true
-
/@storybook/csf-tools@7.6.4:
resolution: {integrity: sha512-6sLayuhgReIK3/QauNj5BW4o4ZfEMJmKf+EWANPEM/xEOXXqrog6Un8sjtBuJS9N1DwyhHY6xfkEiPAwdttwqw==}
dependencies:
@@ -3775,22 +3743,6 @@ packages:
qs: 6.11.2
dev: true
- /@storybook/telemetry@7.6.3:
- resolution: {integrity: sha512-NDCZWhVIUI3M6Lq4M/HPOvZqDXqANDNbI3kyHr4pFGoVaCUXuDPokL9wR+CZcMvATkJ1gHrfLPBdcRq6Biw3Iw==}
- dependencies:
- '@storybook/client-logger': 7.6.3
- '@storybook/core-common': 7.6.3
- '@storybook/csf-tools': 7.6.3
- chalk: 4.1.2
- detect-package-manager: 2.0.1
- fetch-retry: 5.0.6
- fs-extra: 11.1.1
- read-pkg-up: 7.0.1
- transitivePeerDependencies:
- - encoding
- - supports-color
- dev: true
-
/@storybook/telemetry@7.6.4:
resolution: {integrity: sha512-Q4QpvcgloHUEqC9PGo7tgqkUH91/PjX+74/0Hi9orLo8QmLMgdYS5fweFwgSKoTwDGNg2PaHp/jqvhhw7UmnJA==}
dependencies:
@@ -4928,7 +4880,6 @@ packages:
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
- dev: true
/atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
@@ -4948,6 +4899,16 @@ packages:
engines: {node: '>=4'}
dev: false
+ /axios@1.6.2:
+ resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==}
+ dependencies:
+ follow-redirects: 1.15.3
+ form-data: 4.0.0
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/axobject-query@3.2.1:
resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==}
dependencies:
@@ -5441,7 +5402,6 @@ packages:
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
- dev: true
/commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -5942,7 +5902,6 @@ packages:
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
- dev: true
/depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
@@ -6975,6 +6934,16 @@ packages:
tabbable: 6.2.0
dev: false
+ /follow-redirects@1.15.3:
+ resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+ dev: false
+
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
@@ -7018,7 +6987,6 @@ packages:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
- dev: true
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
@@ -8337,14 +8305,12 @@ packages:
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
- dev: true
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
- dev: true
/mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@@ -9335,6 +9301,14 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
+ /prometheus-query@3.3.3:
+ resolution: {integrity: sha512-BL76MllxFdgaXUw1tOtg1osvgaDpHdq0PjUVvWTMXRr/txRxdY4TvQ4k6il9xxUL+L52qOWr4XZKXGWF3n7GZw==}
+ dependencies:
+ axios: 1.6.2
+ transitivePeerDependencies:
+ - debug
+ dev: false
+
/prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -9360,7 +9334,6 @@ packages:
/proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
- dev: true
/public-encrypt@4.0.3:
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
@@ -9511,16 +9484,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: true
- /react-confetti@6.1.0(react@18.2.0):
- resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==}
- engines: {node: '>=10.18'}
- peerDependencies:
- react: ^16.3.0 || ^17.0.1 || ^18.0.0
- dependencies:
- react: 18.2.0
- tween-functions: 1.2.0
- dev: true
-
/react-docgen-typescript@2.2.2(typescript@5.3.2):
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies:
@@ -10761,10 +10724,6 @@ packages:
safe-buffer: 5.2.1
dev: true
- /tween-functions@1.2.0:
- resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==}
- dev: true
-
/type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}