diff --git a/ui/.env b/ui/.env index 77120b430..3930d4a02 100644 --- a/ui/.env +++ b/ui/.env @@ -1,4 +1,5 @@ -NEXT_PUBLIC_KEYCLOAK_URL="https://console-keycloak.mycluster-us-east-107719-da779aef12eee96bd4161f4e402b70ec-0000.us-east.containers.appdomain.cloud/realms/demo" +#NEXT_PUBLIC_KEYCLOAK_URL="https://console-keycloak.mycluster-us-east-107719-da779aef12eee96bd4161f4e402b70ec-0000.us-east.containers.appdomain.cloud/realms/demo" BACKEND_URL=https://console-api-eyefloaters-dev.mycluster-us-east-107719-da779aef12eee96bd4161f4e402b70ec-0000.us-east.containers.appdomain.cloud LOG_LEVEL="info" -CONSOLE_MODE="read-write" \ No newline at end of file +CONSOLE_MODE="read-write" +CONSOLE_METRICS_PROMETHEUS_URL="REQUIRED" diff --git a/ui/.storybook/preview-head.html b/ui/.storybook/preview-head.html new file mode 100644 index 000000000..895e26d54 --- /dev/null +++ b/ui/.storybook/preview-head.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/ui/.storybook/preview.ts b/ui/.storybook/preview.ts deleted file mode 100644 index 4e36fb7e3..000000000 --- a/ui/.storybook/preview.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Preview } from "@storybook/react"; -import "../app/globals.css"; - -const preview: Preview = { - parameters: { - nextjs: { - appDirectory: true, - }, - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -}; - -export default preview; diff --git a/ui/.storybook/preview.tsx b/ui/.storybook/preview.tsx new file mode 100644 index 000000000..ca46879be --- /dev/null +++ b/ui/.storybook/preview.tsx @@ -0,0 +1,32 @@ +import NextIntlProvider from "@/app/[locale]/NextIntlProvider"; +import { Page } from "@patternfly/react-core"; +import type { Preview } from "@storybook/react"; +import "../app/globals.css"; +import messages from "../messages/en.json"; + +const preview: Preview = { + parameters: { + nextjs: { + appDirectory: true, + }, + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: "fullscreen", + }, + decorators: [ + (Story) => ( + + + + + + ), + ], +}; + +export default preview; diff --git a/ui/api/kafka/actions.ts b/ui/api/kafka/actions.ts index 9db5621ed..3fa955d65 100644 --- a/ui/api/kafka/actions.ts +++ b/ui/api/kafka/actions.ts @@ -2,11 +2,25 @@ import { getHeaders } from "@/api/api"; import { ClusterDetail, + ClusterKpis, + ClusterKpisSchema, ClusterList, + ClusterMetricRange, + ClusterMetricRangeSchema, ClusterResponse, ClustersResponseSchema, } from "@/api/kafka/schema"; import { logger } from "@/utils/logger"; +import groupBy from "lodash.groupby"; +import { PrometheusDriver } from "prometheus-query"; +import * as ranges from "./ranges.promql"; +import { values } from "./values.promql"; + +export type Range = keyof typeof ranges; + +const prom = new PrometheusDriver({ + endpoint: process.env.CONSOLE_METRICS_PROMETHEUS_URL, +}); const log = logger.child({ module: "kafka-api" }); @@ -27,15 +41,191 @@ export async function getKafkaClusters(): Promise { export async function getKafkaCluster( clusterId: string, ): Promise { - const url = `${process.env.BACKEND_URL}/api/kafkas/${clusterId}/?fields%5Bkafkas%5D=name,namespace,creationTimestamp,nodes,controller,authorizedOperations,bootstrapServers,authType,metrics`; + const url = `${process.env.BACKEND_URL}/api/kafkas/${clusterId}/?fields%5Bkafkas%5D=name,namespace,creationTimestamp,status,kafkaVersion,nodes,controller,authorizedOperations,bootstrapServers,authType`; try { const res = await fetch(url, { headers: await getHeaders(), + cache: "force-cache", }); const rawData = await res.json(); + log.debug(rawData, "getKafkaCluster response"); return ClusterResponse.parse(rawData).data; } catch (err) { - log.error(err, "getKafkaCluster"); + log.error({ err, clusterId }, "getKafkaCluster"); + return null; + } +} + +export async function getKafkaClusterKpis( + clusterId: string, +): Promise<{ cluster: ClusterDetail; kpis: ClusterKpis } | null> { + try { + const cluster = await getKafkaCluster(clusterId); + if (!cluster) { + return null; + } + + const valuesRes = await prom.instantQuery( + values( + cluster.attributes.namespace, + cluster.attributes.name, + cluster.attributes.controller.id, + ), + ); + + /* + Prometheus returns the data unaggregated. Eg. + + [ + { + "metric": { + "labels": { + "__console_metric_name__": "broker_state", + "nodeId": "2" + } + }, + "value": { + "time": "2023-12-12T16:00:53.381Z", + "value": 3 + } + }, + ... + ] + + We start by flattening the labels, and then group by metric name + */ + const groupedMetrics = groupBy( + valuesRes.result.map((serie) => ({ + metric: serie.metric.labels.__console_metric_name__, + nodeId: serie.metric.labels.nodeId, + time: serie.value.time, + value: serie.value.value, + })), + (v) => v.metric, + ); + + /* + Now we want to transform the data in something easier to work with in the UI. + + Some are totals, in an array form with a single entry; we just need the number. These will look like a metric:value + mapping. + + Some KPIs are provided split by broker id. Of these, some are counts (identified by the string `_count` in the + metric name), and some are other infos. Both will be grouped by nodeId. + The `_count` metrics will have a value with two properties, `byNode` and `total`. `byNode` will hold the grouping. `total` will + have the sum of all the counts. + Other metrics will look like a metric:[node:value] mapping. + + Expected result: + { + "broker_state": { + "0": 3, + "1": 3, + "2": 3 + }, + "total_topics": 5, + "total_partitions": 55, + "underreplicated_topics": 0, + "replica_count": { + "byNode": { + "0": 57, + "1": 54, + "2": 54 + }, + "total": 165 + }, + "leader_count": { + "byNode": { + "0": 19, + "1": 18, + "2": 18 + }, + "total": 55 + } + } + */ + const kpis = Object.fromEntries( + Object.entries(groupedMetrics).map(([metric, value]) => { + const total = value.reduce((acc, v) => acc + v.value, 0); + if (value.find((v) => v.nodeId)) { + const byNode = Object.fromEntries( + value.map(({ nodeId, value }) => + nodeId ? [nodeId, value] : ["value", value], + ), + ); + return metric.includes("_count") + ? [ + metric, + { + byNode, + total, + }, + ] + : [metric, byNode]; + } else { + return [metric, total]; + } + }), + ); + log.debug({ kpis, clusterId }, "getKafkaClusterKpis"); + return { + cluster, + kpis: ClusterKpisSchema.parse(kpis), + }; + } catch (err) { + log.error({ err, clusterId }, "getKafkaClusterKpis"); + return null; + } +} + +export async function getKafkaClusterMetrics( + clusterId: string, + metrics: Array, +): Promise<{ + cluster: ClusterDetail; + ranges: Record; +} | null> { + async function getRange(namespace: string, name: string, metric: Range) { + const start = new Date().getTime() - 1 * 60 * 60 * 1000; + const end = new Date(); + const step = 60 * 10; + const seriesRes = await prom.rangeQuery( + ranges[metric](namespace, name), + start, + end, + step, + ); + const range = Object.fromEntries( + seriesRes.result.flatMap((serie) => + serie.values.map((v: any) => [new Date(v.time).getTime(), v.value]), + ), + ); + return [metric, ClusterMetricRangeSchema.parse(range)]; + } + + try { + const cluster = await getKafkaCluster(clusterId); + if (!cluster) { + return null; + } + + const rangesRes = Object.fromEntries( + await Promise.all( + metrics.map((m) => + getRange(cluster.attributes.namespace, cluster.attributes.name, m), + ), + ), + ); + log.debug( + { ranges: rangesRes, clusterId, metric: metrics }, + "getKafkaClusterMetric", + ); + return { + cluster, + ranges: rangesRes, + }; + } catch (err) { + log.error({ err, clusterId, metric: metrics }, "getKafkaClusterMetric"); return null; } } diff --git a/ui/api/kafka/ranges.promql.ts b/ui/api/kafka/ranges.promql.ts new file mode 100644 index 000000000..34691fdee --- /dev/null +++ b/ui/api/kafka/ranges.promql.ts @@ -0,0 +1,95 @@ +export const cpu = (namespace: string, cluster: string) => ` + sum by (nodeId, __console_metric_name__) ( + label_replace( + label_replace( + rate(container_cpu_usage_seconds_total{namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+"}[1m]), + "nodeId", + "$1", + "pod", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "cpu_usage_seconds", + "", + "" + ) + ) +`; + +export const memory = (namespace: string, cluster: string) => ` + sum by (nodeId, __console_metric_name__) ( + label_replace( + label_replace( + container_memory_usage_bytes{namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+"}, + "nodeId", + "$1", + "pod", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "memory_usage_bytes", + "", + "" + ) + ) +`; + +export const incomingByteRate = (namespace: string, cluster: string) => ` + sum by (__console_metric_name__) ( + label_replace( + irate(kafka_server_brokertopicmetrics_bytesin_total{topic!="",namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+",strimzi_io_kind="Kafka"}[5m]), + "__console_metric_name__", + "incoming_byte_rate", + "", + "" + ) + ) +`; + +export const outgoingByteRate = (namespace: string, cluster: string) => ` + sum by (__console_metric_name__) ( + label_replace( + irate(kafka_server_brokertopicmetrics_bytesout_total{topic!="",namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+",strimzi_io_kind="Kafka"}[5m]), + "__console_metric_name__", + "outgoing_byte_rate", + "", + "" + ) + ) +`; + +export const volumeCapacity = (namespace: string, cluster: string) => ` + sum by (nodeId, __console_metric_name__) ( + label_replace( + label_replace( + kubelet_volume_stats_capacity_bytes{namespace="${namespace}",persistentvolumeclaim=~"data(?:-\\\\d+)?-${cluster}-.+-\\\\d+"}, + "nodeId", + "$1", + "persistentvolumeclaim", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "volume_stats_capacity_bytes", + "", + "" + ) + ) +`; + +export const volumeUsed = (namespace: string, cluster: string) => ` + sum by (nodeId, __console_metric_name__) ( + label_replace( + label_replace( + kubelet_volume_stats_used_bytes{namespace="${namespace}",persistentvolumeclaim=~"data(?:-\\\\d+)?-${cluster}-.+-\\\\d+"}, + "nodeId", + "$1", + "persistentvolumeclaim", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "volume_stats_used_bytes", + "", + "" + ) + ) +`; diff --git a/ui/api/kafka/schema.ts b/ui/api/kafka/schema.ts index 9394dbbdd..d202bd316 100644 --- a/ui/api/kafka/schema.ts +++ b/ui/api/kafka/schema.ts @@ -9,7 +9,7 @@ export const NodeSchema = z.object({ export type KafkaNode = z.infer; export const ClusterListSchema = z.object({ id: z.string(), - type: z.string(), + type: z.literal("kafkas"), attributes: z.object({ name: z.string(), namespace: z.string(), @@ -22,44 +22,49 @@ export const ClustersResponseSchema = z.object({ export type ClusterList = z.infer; const ClusterDetailSchema = z.object({ id: z.string(), - type: z.string(), + type: z.literal("kafkas"), attributes: z.object({ name: z.string(), namespace: z.string(), creationTimestamp: z.string(), + status: z.string(), + kafkaVersion: z.string().optional(), nodes: z.array(NodeSchema), controller: NodeSchema, authorizedOperations: z.array(z.string()), bootstrapServers: z.string(), authType: z.string().optional().nullable(), - listeners: z.array( - z.object({ - type: z.string(), - bootstrapServers: z.string().nullable(), - authType: z.string().nullable(), - }) - ).optional() /* remove .optional() when `listeners` is added to the fetched fields */, - metrics: z.object({ - values: z.record( - z.array( - z.object({ - value: z.string(), - nodeId: z.string().optional(), - }) - ) - ), - ranges: z.record( - z.array( - z.object({ - range: z.array(z.array(z.string())), - nodeId: z.string().optional(), - }) - ) - ), - }).optional(), + listeners: z + .array( + z.object({ + type: z.string(), + bootstrapServers: z.string().nullable(), + authType: z.string().nullable(), + }), + ) + .optional() /* remove .optional() when `listeners` is added to the fetched fields */, }), }); export const ClusterResponse = z.object({ data: ClusterDetailSchema, }); export type ClusterDetail = z.infer; + +export const ClusterKpisSchema = z.object({ + broker_state: z.record(z.number()), + total_topics: z.number(), + total_partitions: z.number(), + underreplicated_topics: z.number(), + replica_count: z.object({ + byNode: z.record(z.number()), + total: z.number(), + }), + leader_count: z.object({ + byNode: z.record(z.number()), + total: z.number(), + }), +}); +export type ClusterKpis = z.infer; + +export const ClusterMetricRangeSchema = z.record(z.number()); +export type ClusterMetricRange = z.infer; diff --git a/ui/api/kafka/values.promql.ts b/ui/api/kafka/values.promql.ts new file mode 100644 index 000000000..2353d5a62 --- /dev/null +++ b/ui/api/kafka/values.promql.ts @@ -0,0 +1,98 @@ +export const values = ( + namespace: string, + cluster: string, + controller: number, +) => ` +sum by (__console_metric_name__, nodeId) ( + label_replace( + label_replace( + kafka_server_kafkaserver_brokerstate{namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+",strimzi_io_kind="Kafka"} > 0, + "nodeId", + "$1", + "pod", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "broker_state", + "", + "" + ) +) + +or + +sum by (__console_metric_name__) ( + label_replace( + kafka_controller_kafkacontroller_globaltopiccount{namespace="${namespace}",pod=~"${cluster}-.+-${controller}",strimzi_io_kind="Kafka"} > 0, + "__console_metric_name__", + "total_topics", + "", + "" + ) +) + +or + +sum by (__console_metric_name__) ( + label_replace( + kafka_controller_kafkacontroller_globalpartitioncount{namespace="${namespace}",pod=~"${cluster}-.+-${controller}",strimzi_io_kind="Kafka"} > 0, + "__console_metric_name__", + "total_partitions", + "", + "" + ) +) + +or + +label_replace( + ( + count( + sum by (topic) ( + kafka_cluster_partition_underreplicated{namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+",strimzi_io_kind="Kafka"} > 0 + ) + ) + OR on() vector(0) + ), + "__console_metric_name__", + "underreplicated_topics", + "", + "" +) + +or + +sum by (__console_metric_name__, nodeId) ( + label_replace( + label_replace( + kafka_cluster_partition_replicascount{namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+",strimzi_io_kind="Kafka"} > 0, + "nodeId", + "$1", + "pod", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "replica_count", + "", + "" + ) +) + +or + +sum by (__console_metric_name__, nodeId) ( + label_replace( + label_replace( + kafka_server_replicamanager_leadercount{namespace="${namespace}",pod=~"${cluster}-.+-\\\\d+",strimzi_io_kind="Kafka"} > 0, + "nodeId", + "$1", + "pod", + ".+-(\\\\d+)" + ), + "__console_metric_name__", + "leader_count", + "", + "" + ) +) +`; diff --git a/ui/app/[locale]/kafka/[kafkaId]/@activeBreadcrumb/overview/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/@activeBreadcrumb/overview/page.tsx new file mode 100644 index 000000000..76205ed62 --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/@activeBreadcrumb/overview/page.tsx @@ -0,0 +1,5 @@ +import { BreadcrumbItem } from "@/libs/patternfly/react-core"; + +export default function OverviewBreadcrumb() { + return Overview; +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/@header/overview/ConnectButton.tsx b/ui/app/[locale]/kafka/[kafkaId]/@header/overview/ConnectButton.tsx new file mode 100644 index 000000000..a6156bb1b --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/@header/overview/ConnectButton.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useHelp } from "@/components/Quickstarts/HelpContainer"; +import { Button } from "@/libs/patternfly/react-core"; +import { HelpIcon } from "@/libs/patternfly/react-icons"; + +export function ConnectButton() { + const openHelp = useHelp(); + return ( + + ); +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/@header/overview/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/@header/overview/page.tsx index 673c0f49e..5645d08ed 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/@header/overview/page.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/@header/overview/page.tsx @@ -1,4 +1,12 @@ -import { KafkaHeader } from "@/app/[locale]/kafka/[kafkaId]/@header/KafkaHeader"; +import { ConnectButton } from "@/app/[locale]/kafka/[kafkaId]/@header/overview/ConnectButton"; +import { AppHeader } from "@/components/AppHeader"; -export { fetchCache } from "@/app/[locale]/kafka/[kafkaId]/@header/KafkaHeader"; -export default KafkaHeader; +export default function OverviewHeader() { + return ( + ]} + /> + ); +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx b/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx index 96ac13e99..830de9ad0 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/nodes/NodesTable.tsx @@ -2,7 +2,7 @@ import { KafkaNode } from "@/api/kafka/schema"; import { ResponsiveTable } from "@/components/table"; -import { ClipboardCopy, Label } from "@patternfly/react-core"; +import { Label } from "@patternfly/react-core"; import { ServerIcon } from "@patternfly/react-icons"; import Link from "next/link"; @@ -11,11 +11,11 @@ const columns = ["id", "replicas", "rack"] as const; export function NodesTable({ nodes, controller, - metrics, + //metrics, }: { nodes: KafkaNode[]; controller: KafkaNode; - metrics: Record; + //metrics: Record; }) { return ( + {/* { 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 + +
+
+
+
+ + + + + {isLoading ? ( + new Array(5).fill(0).map((_, i) => ( + + + + + , + + + , + + + , + ]} + /> + + + )) + ) : ( + <> + {messages.length === 0 && ( + + + + No messages + , + ]} + /> + + + )} + {messages + .sort((a, b) => a.date.localeCompare(b.date)) + .reverse() + .map((m, i) => ( + + + + + {m.variant === "danger" && ( + + )} + {m.variant === "warning" && ( + + )} + +   + + + + , + +
+ +
+
+ {m.message} +
+
, + + + , + ]} + /> +
+
+ ))} + + )} +
+
+
+
+
+
+ ); +} 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  + <LabelGroup> + <Label color={"red"} isCompact={true}> + <ExclamationCircleIcon /> {dangers} + </Label> + <Label color={"gold"} isCompact={true}> + <ExclamationTriangleIcon /> {warnings} + </Label> + </LabelGroup> + + } + > + {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'}