From d2385a83cf4bb125722c72ebb8df486866573f5a Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Tue, 24 Mar 2026 21:46:18 +0000 Subject: [PATCH] feat: enhance Kubernetes cluster overview with fallback counts for resources and improve stateful set handling --- .../src/Pages/Kubernetes/Clusters.tsx | 14 + .../src/Pages/Kubernetes/View/Events.tsx | 11 +- .../src/Pages/Kubernetes/View/Index.tsx | 350 +++++++++++------- .../src/Pages/Kubernetes/View/Layout.tsx | 119 +++++- .../src/Pages/Kubernetes/View/SideMenu.tsx | 8 +- .../Pages/Kubernetes/View/StatefulSets.tsx | 33 ++ Clickhouse/config.xml | 4 +- 7 files changed, 386 insertions(+), 153 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx index 801ef0198e..75c4402fe3 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx @@ -158,6 +158,20 @@ const KubernetesClusters: FunctionComponent< ); }, }, + { + field: { + nodeCount: true, + }, + title: "Nodes", + type: FieldType.Number, + }, + { + field: { + podCount: true, + }, + title: "Pods", + type: FieldType.Number, + }, ]} onViewPage={(item: KubernetesCluster): Promise => { return Promise.resolve( diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx index 1b211f1400..e23d2d4f51 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx @@ -438,6 +438,15 @@ const KubernetesClusterEvents: FunctionComponent< const hasActiveFilters: boolean = Object.keys(filterData).length > 0; const cardButtons: Array = [ + { + title: "", + buttonStyle: ButtonStyleType.ICON, + className: "py-0 pr-0 pl-1 mt-1", + onClick: () => { + fetchData().catch(() => {}); + }, + icon: IconProp.Refresh, + }, { title: "", buttonStyle: ButtonStyleType.ICON, @@ -452,7 +461,7 @@ const KubernetesClusterEvents: FunctionComponent< return ( diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx index 42e94c2cb6..622d3c26d9 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx @@ -44,7 +44,6 @@ import StackedProgressBar, { import StatusBadge, { StatusBadgeType, } from "Common/UI/Components/StatusBadge/StatusBadge"; -import ResourceUsageBar from "Common/UI/Components/ResourceUsageBar/ResourceUsageBar"; import Icon from "Common/UI/Components/Icon/Icon"; import IconProp from "Common/Types/Icon/IconProp"; @@ -181,7 +180,7 @@ const KubernetesClusterOverview: FunctionComponent< jobs, cronJobs, containers, - ]: Array> = await Promise.all([ + ] = await Promise.all([ KubernetesResourceUtils.fetchResourceList({ clusterIdentifier: item.clusterIdentifier, metricName: "k8s.deployment.desired", @@ -244,14 +243,9 @@ const KubernetesClusterOverview: FunctionComponent< .slice(0, 5); setTopMemoryPods(sortedByMemory); - // Fetch pod and node objects for health status + // Fetch k8s objects for health status and fallback counts try { - const [podObjects, nodeObjects, pvcObjects, pvObjects]: [ - Map, - Map, - Map, - Map, - ] = await Promise.all([ + const objectResults = await Promise.all([ fetchK8sObjectsBatch({ clusterIdentifier: item.clusterIdentifier, resourceType: "pods", @@ -268,11 +262,61 @@ const KubernetesClusterOverview: FunctionComponent< clusterIdentifier: item.clusterIdentifier, resourceType: "persistentvolumes", }), + fetchK8sObjectsBatch({ + clusterIdentifier: item.clusterIdentifier, + resourceType: "deployments", + }), + fetchK8sObjectsBatch({ + clusterIdentifier: item.clusterIdentifier, + resourceType: "statefulsets", + }), + fetchK8sObjectsBatch({ + clusterIdentifier: item.clusterIdentifier, + resourceType: "daemonsets", + }), + fetchK8sObjectsBatch({ + clusterIdentifier: item.clusterIdentifier, + resourceType: "jobs", + }), + fetchK8sObjectsBatch({ + clusterIdentifier: item.clusterIdentifier, + resourceType: "cronjobs", + }), ]); + const podObjects: Map = objectResults[0]!; + const nodeObjects: Map = objectResults[1]!; + const pvcObjects: Map = objectResults[2]!; + const pvObjects: Map = objectResults[3]!; + const deploymentObjects: Map = objectResults[4]!; + const statefulSetObjects: Map = objectResults[5]!; + const daemonSetObjects: Map = objectResults[6]!; + const jobObjects: Map = objectResults[7]!; + const cronJobObjects: Map = objectResults[8]!; + setPvcCount(pvcObjects.size); setPvCount(pvObjects.size); + // Use k8s object counts as fallback when metric-based counts are 0 + if (deploymentCount === 0 && deploymentObjects.size > 0) { + setDeploymentCount(deploymentObjects.size); + } + if (statefulSetCount === 0 && statefulSetObjects.size > 0) { + setStatefulSetCount(statefulSetObjects.size); + } + if (daemonSetCount === 0 && daemonSetObjects.size > 0) { + setDaemonSetCount(daemonSetObjects.size); + } + if (jobCount === 0 && jobObjects.size > 0) { + setJobCount(jobObjects.size); + } + if (cronJobCount === 0 && cronJobObjects.size > 0) { + setCronJobCount(cronJobObjects.size); + } + if (containerCount === 0 && podObjects.size > 0) { + setContainerCount(podObjects.size); + } + // Calculate pod health let running: number = 0; let pending: number = 0; @@ -771,157 +815,189 @@ const KubernetesClusterOverview: FunctionComponent< )} {/* Top Resource Consumers */} -
- - {topCpuPods.length === 0 ? ( -

- No CPU usage data available. -

- ) : ( -
- {topCpuPods.map((pod: KubernetesResource, index: number) => { - const pct: number = Math.min(pod.cpuUtilization ?? 0, 100); - return ( -
{ - Navigation.navigate( - RouteUtil.populateRouteParams( - RouteMap[ - PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL - ] as Route, - { - modelId: modelId, - subModelId: new ObjectID(pod.name), - }, - ), - ); - }} - className="flex items-center gap-4 px-5 py-3.5 cursor-pointer hover:bg-gray-50 transition-colors" - > - - {index + 1} - -
-
- - {pod.name} - - {pod.namespace && ( - - {pod.namespace} + +
+ {/* CPU Usage */} +
+
+
+ +
+
+

+ CPU Usage +

+

Top 5 pods by CPU

+
+
+ {topCpuPods.length === 0 ? ( +

+ No CPU usage data available. +

+ ) : ( +
+ {topCpuPods.map((pod: KubernetesResource, index: number) => { + const pct: number = Math.min(pod.cpuUtilization ?? 0, 100); + return ( +
{ + Navigation.navigate( + RouteUtil.populateRouteParams( + RouteMap[ + PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL + ] as Route, + { + modelId: modelId, + subModelId: new ObjectID(pod.name), + }, + ), + ); + }} + className="group cursor-pointer rounded-lg p-3 hover:bg-gray-50 transition-colors" + > +
+
+ + {index + 1}. + + + {pod.name} - )} -
-
-
-
80 - ? "bg-red-500" - : pct > 60 - ? "bg-amber-500" - : "bg-emerald-500" - }`} - style={{ - width: `${Math.max(pct, 1)}%`, - }} - />
- + {KubernetesResourceUtils.formatCpuValue( pod.cpuUtilization, )}
-
-
- ); - })} -
- )} - - - {topMemoryPods.length === 0 ? ( -

- No memory usage data available. -

- ) : ( -
- {topMemoryPods.map((pod: KubernetesResource, index: number) => { - const maxMemory: number = - topMemoryPods[0]?.memoryUsageBytes ?? 1; - const memPercent: number = - maxMemory > 0 - ? ((pod.memoryUsageBytes ?? 0) / maxMemory) * 100 - : 0; - return ( -
{ - Navigation.navigate( - RouteUtil.populateRouteParams( - RouteMap[ - PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL - ] as Route, - { - modelId: modelId, - subModelId: new ObjectID(pod.name), - }, - ), - ); - }} - className="flex items-center gap-4 px-5 py-3.5 cursor-pointer hover:bg-gray-50 transition-colors" - > - - {index + 1} - -
-
- - {pod.name} - +
{pod.namespace && ( - + {pod.namespace} )} -
-
-
+
85 + className={`h-1.5 rounded-full transition-all duration-300 ${ + pct > 80 ? "bg-red-500" - : memPercent > 70 + : pct > 60 ? "bg-amber-500" : "bg-blue-500" }`} style={{ - width: `${Math.max(memPercent, 1)}%`, + width: `${Math.max(pct, 2)}%`, }} />
- - {KubernetesResourceUtils.formatMemoryValue( - pod.memoryUsageBytes, - )} -
-
- ); - })} + ); + })} +
+ )} +
+ + {/* Memory Usage */} +
+
+
+ +
+
+

+ Memory Usage +

+

Top 5 pods by memory

+
- )} - -
+ {topMemoryPods.length === 0 ? ( +

+ No memory usage data available. +

+ ) : ( +
+ {topMemoryPods.map( + (pod: KubernetesResource, index: number) => { + const maxMemory: number = + topMemoryPods[0]?.memoryUsageBytes ?? 1; + const memPercent: number = + maxMemory > 0 + ? ((pod.memoryUsageBytes ?? 0) / maxMemory) * 100 + : 0; + return ( +
{ + Navigation.navigate( + RouteUtil.populateRouteParams( + RouteMap[ + PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL + ] as Route, + { + modelId: modelId, + subModelId: new ObjectID(pod.name), + }, + ), + ); + }} + className="group cursor-pointer rounded-lg p-3 hover:bg-gray-50 transition-colors" + > +
+
+ + {index + 1}. + + + {pod.name} + +
+ + {KubernetesResourceUtils.formatMemoryValue( + pod.memoryUsageBytes, + )} + +
+
+ {pod.namespace && ( + + {pod.namespace} + + )} +
+
85 + ? "bg-red-500" + : memPercent > 70 + ? "bg-amber-500" + : "bg-purple-500" + }`} + style={{ + width: `${Math.max(memPercent, 2)}%`, + }} + /> +
+
+
+ ); + }, + )} +
+ )} +
+
+ {/* Recent Warning Events */} {recentWarnings.length > 0 && ( diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx index d85b507645..ce959afc46 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx @@ -129,21 +129,124 @@ const KubernetesClusterViewLayout: FunctionComponent< }), ]); + // Use k8s objects as fallback for counts when metrics return 0 + const deploymentObjectCount: number = + deployments?.length ?? 0; + const statefulSetObjectCount: number = + statefulSets?.length ?? 0; + const daemonSetObjectCount: number = + daemonSets?.length ?? 0; + const jobObjectCount: number = jobs?.length ?? 0; + const cronJobObjectCount: number = cronJobs?.length ?? 0; + + // Fetch k8s object-based fallback counts for resources whose metrics may be empty + let deploymentFallback: number = deploymentObjectCount; + let statefulSetFallback: number = statefulSetObjectCount; + let daemonSetFallback: number = daemonSetObjectCount; + let jobFallback: number = jobObjectCount; + let cronJobFallback: number = cronJobObjectCount; + + if ( + deploymentObjectCount === 0 || + statefulSetObjectCount === 0 || + daemonSetObjectCount === 0 || + jobObjectCount === 0 || + cronJobObjectCount === 0 + ) { + try { + const fallbackResults = await Promise.all([ + deploymentObjectCount === 0 + ? fetchK8sObjectsBatch({ + clusterIdentifier: ci, + resourceType: "deployments", + }) + : Promise.resolve(new Map()), + statefulSetObjectCount === 0 + ? fetchK8sObjectsBatch({ + clusterIdentifier: ci, + resourceType: "statefulsets", + }) + : Promise.resolve(new Map()), + daemonSetObjectCount === 0 + ? fetchK8sObjectsBatch({ + clusterIdentifier: ci, + resourceType: "daemonsets", + }) + : Promise.resolve(new Map()), + jobObjectCount === 0 + ? fetchK8sObjectsBatch({ + clusterIdentifier: ci, + resourceType: "jobs", + }) + : Promise.resolve(new Map()), + cronJobObjectCount === 0 + ? fetchK8sObjectsBatch({ + clusterIdentifier: ci, + resourceType: "cronjobs", + }) + : Promise.resolve(new Map()), + ]); + + const deploymentObjs = fallbackResults[0]!; + const statefulSetObjs = fallbackResults[1]!; + const daemonSetObjs = fallbackResults[2]!; + const jobObjs = fallbackResults[3]!; + const cronJobObjs = fallbackResults[4]!; + + if (deploymentObjectCount === 0 && deploymentObjs.size > 0) { + deploymentFallback = deploymentObjs.size; + } + if (statefulSetObjectCount === 0 && statefulSetObjs.size > 0) { + statefulSetFallback = statefulSetObjs.size; + } + if (daemonSetObjectCount === 0 && daemonSetObjs.size > 0) { + daemonSetFallback = daemonSetObjs.size; + } + if (jobObjectCount === 0 && jobObjs.size > 0) { + jobFallback = jobObjs.size; + } + if (cronJobObjectCount === 0 && cronJobObjs.size > 0) { + cronJobFallback = cronJobObjs.size; + } + } catch { + // Fallback counts are supplementary + } + } + + const computedNodeCount: number = nodes?.length ?? 0; + const computedPodCount: number = pods?.length ?? 0; + const computedNamespaceCount: number = namespaces?.length ?? 0; + setResourceCounts({ - nodes: nodes?.length ?? 0, - pods: pods?.length ?? 0, - namespaces: namespaces?.length ?? 0, - deployments: deployments?.length ?? 0, - statefulSets: statefulSets?.length ?? 0, - daemonSets: daemonSets?.length ?? 0, - jobs: jobs?.length ?? 0, - cronJobs: cronJobs?.length ?? 0, + nodes: computedNodeCount, + pods: computedPodCount, + namespaces: computedNamespaceCount, + deployments: deploymentFallback, + statefulSets: statefulSetFallback, + daemonSets: daemonSetFallback, + jobs: jobFallback, + cronJobs: cronJobFallback, containers: containers?.length ?? 0, pvcs: pvcs?.size ?? 0, pvs: pvs?.size ?? 0, hpas: hpas?.size ?? 0, vpas: vpas?.size ?? 0, }); + + // Update cached counts on the cluster model for the clusters list table + try { + await ModelAPI.updateById({ + modelType: KubernetesCluster, + id: modelId, + data: { + nodeCount: computedNodeCount, + podCount: computedPodCount, + namespaceCount: computedNamespaceCount, + }, + }); + } catch { + // Updating cached counts is best-effort + } } catch { // Counts are supplementary, don't fail the layout } diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx index 5024e3fad2..daeab99a3a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx @@ -195,7 +195,7 @@ const KubernetesClusterSideMenu: FunctionComponent = ( { modelId: props.modelId }, ), }} - icon={IconProp.ArrowUpDown} + icon={IconProp.ChartBar} badge={counts.hpas} /> = ( { modelId: props.modelId }, ), }} - icon={IconProp.Scale} + icon={IconProp.AdjustmentVertical} badge={counts.vpas} /> @@ -230,7 +230,7 @@ const KubernetesClusterSideMenu: FunctionComponent = ( { modelId: props.modelId }, ), }} - icon={IconProp.Activity} + icon={IconProp.CPUChip} /> = ( { modelId: props.modelId }, ), }} - icon={IconProp.Globe} + icon={IconProp.FlowDiagram} /> diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx index c84add65a2..1d8ce34bcc 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx @@ -68,8 +68,12 @@ const KubernetesClusterStatefulSets: FunctionComponent< }), ]); + // Build a set of resource keys we already have from metrics + const existingKeys: Set = new Set(); + for (const resource of statefulsetList) { const key: string = `${resource.namespace}/${resource.name}`; + existingKeys.add(key); const stsObj: KubernetesObjectType | undefined = statefulsetObjects.get(key); if (stsObj) { @@ -93,6 +97,35 @@ const KubernetesClusterStatefulSets: FunctionComponent< } } + // Add statefulsets from k8s objects that were not found via metrics + for (const [key, stsObj] of statefulsetObjects.entries()) { + if (existingKeys.has(key)) { + continue; + } + const sts: KubernetesStatefulSetObject = + stsObj as KubernetesStatefulSetObject; + const readyReplicas: number = sts.status.readyReplicas ?? 0; + const replicas: number = sts.spec.replicas ?? 0; + + statefulsetList.push({ + name: sts.metadata.name, + namespace: sts.metadata.namespace, + cpuUtilization: null, + memoryUsageBytes: null, + memoryLimitBytes: null, + status: + readyReplicas === replicas && replicas > 0 + ? "Ready" + : "Progressing", + age: KubernetesResourceUtils.formatAge( + sts.metadata.creationTimestamp, + ), + additionalAttributes: { + ready: `${readyReplicas}/${replicas}`, + }, + }); + } + setResources(statefulsetList); } catch (err) { setError(API.getFriendlyMessage(err)); diff --git a/Clickhouse/config.xml b/Clickhouse/config.xml index 38ecb94601..40c41c97e2 100644 --- a/Clickhouse/config.xml +++ b/Clickhouse/config.xml @@ -1520,11 +1520,9 @@ --> -