From a5d74ae76ab02e49925ca5c419a328bd10c6fed5 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 18:55:39 +0000 Subject: [PATCH 01/23] feat: Add Kubernetes menu item to the Dashboard navbar --- .../src/Components/NavBar/NavBar.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx b/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx index 0079edc4ed..666a1b07d4 100644 --- a/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx +++ b/App/FeatureSet/Dashboard/src/Components/NavBar/NavBar.tsx @@ -144,17 +144,17 @@ const DashboardNavbar: FunctionComponent = ( iconColor: "indigo", category: "Observability", }, - // { - // title: "Kubernetes", - // description: "Monitor Kubernetes clusters.", - // route: RouteUtil.populateRouteParams( - // RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route, - // ), - // activeRoute: RouteMap[PageMap.KUBERNETES_CLUSTERS], - // icon: IconProp.Kubernetes, - // iconColor: "blue", - // category: "Observability", - // }, + { + title: "Kubernetes", + description: "Monitor Kubernetes clusters.", + route: RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route, + ), + activeRoute: RouteMap[PageMap.KUBERNETES_CLUSTERS], + icon: IconProp.Kubernetes, + iconColor: "blue", + category: "Observability", + }, // Automation & Analytics { title: "Dashboards", From 5b5b0e8d5424b6d03e70cf3b22af88fd772d1965 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 19:03:56 +0000 Subject: [PATCH 02/23] fix: Update PostgreSQL hbaConfiguration to allow local connections for database initialization --- HelmChart/Public/oneuptime/values.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HelmChart/Public/oneuptime/values.yaml b/HelmChart/Public/oneuptime/values.yaml index 1e8b88fea1..6e49003bf5 100644 --- a/HelmChart/Public/oneuptime/values.yaml +++ b/HelmChart/Public/oneuptime/values.yaml @@ -128,6 +128,8 @@ postgresql: # pg_hba.conf rules. These enable password auth (md5) from any host/IP. # Tighten these for production to your pod/service/network CIDRs. hbaConfiguration: |- + # Local connections (needed for initdb/entrypoint to create databases) + local all all trust # Allow all IPv4 and IPv6 clients with md5 password auth host all all 0.0.0.0/0 md5 host all all ::/0 md5 From 5a8733327505f032d3feed6c81780817060ec022 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 19:28:29 +0000 Subject: [PATCH 03/23] Add Kubernetes resource views for DaemonSets, Deployments, Jobs, and StatefulSets - Implemented DaemonSets view with resource fetching and metrics display. - Created DeploymentDetail page to show metrics for specific deployments. - Added Deployments view to list all deployments in a Kubernetes cluster. - Developed JobDetail page for displaying metrics related to jobs. - Introduced Jobs view to list all jobs in the cluster. - Created NamespaceDetail page to show metrics for specific namespaces. - Added Namespaces view to list all namespaces in the cluster. - Implemented StatefulSetDetail page for metrics related to stateful sets. - Created StatefulSets view to list all stateful sets in the cluster. --- .../Kubernetes/KubernetesResourceTable.tsx | 147 ++++++++++ .../Utils/KubernetesResourceUtils.ts | 215 +++++++++++++++ .../Pages/Kubernetes/View/ContainerDetail.tsx | 173 ++++++++++++ .../src/Pages/Kubernetes/View/Containers.tsx | 107 ++++++++ .../Pages/Kubernetes/View/CronJobDetail.tsx | 173 ++++++++++++ .../src/Pages/Kubernetes/View/CronJobs.tsx | 100 +++++++ .../Pages/Kubernetes/View/DaemonSetDetail.tsx | 173 ++++++++++++ .../src/Pages/Kubernetes/View/DaemonSets.tsx | 100 +++++++ .../Kubernetes/View/DeploymentDetail.tsx | 173 ++++++++++++ .../src/Pages/Kubernetes/View/Deployments.tsx | 100 +++++++ .../src/Pages/Kubernetes/View/Index.tsx | 121 +++++++++ .../src/Pages/Kubernetes/View/JobDetail.tsx | 173 ++++++++++++ .../src/Pages/Kubernetes/View/Jobs.tsx | 100 +++++++ .../Pages/Kubernetes/View/NamespaceDetail.tsx | 173 ++++++++++++ .../src/Pages/Kubernetes/View/Namespaces.tsx | 102 +++++++ .../src/Pages/Kubernetes/View/Nodes.tsx | 200 +++----------- .../src/Pages/Kubernetes/View/Pods.tsx | 211 ++++----------- .../src/Pages/Kubernetes/View/SideMenu.tsx | 85 +++++- .../Kubernetes/View/StatefulSetDetail.tsx | 173 ++++++++++++ .../Pages/Kubernetes/View/StatefulSets.tsx | 100 +++++++ .../Dashboard/src/Routes/KubernetesRoutes.tsx | 251 ++++++++++++++++++ .../Breadcrumbs/KubernetesBreadcrumbs.ts | 118 ++++++++ App/FeatureSet/Dashboard/src/Utils/PageMap.ts | 14 + .../Dashboard/src/Utils/RouteMap.ts | 98 +++++++ 24 files changed, 3058 insertions(+), 322 deletions(-) create mode 100644 App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx new file mode 100644 index 0000000000..6fe41ae31a --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx @@ -0,0 +1,147 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../../Pages/Kubernetes/Utils/KubernetesResourceUtils"; +import Card from "Common/UI/Components/Card/Card"; +import Route from "Common/Types/API/Route"; + +export interface Column { + title: string; + key: string; + getValue?: (resource: KubernetesResource) => string; +} + +export interface ComponentProps { + resources: Array; + title: string; + description: string; + columns?: Array; + showNamespace?: boolean; + getViewRoute?: (resource: KubernetesResource) => Route; + emptyMessage?: string; +} + +const KubernetesResourceTable: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const showNamespace: boolean = props.showNamespace !== false; + + return ( + + {props.resources.length === 0 ? ( +

+ {props.emptyMessage || + "No resources found. Resources will appear here once the kubernetes-agent is sending data."} +

+ ) : ( +
+ + + + + {showNamespace && ( + + )} + {props.columns?.map((column: Column) => { + return ( + + ); + })} + + + {props.getViewRoute && ( + + )} + + + + {props.resources.map( + (resource: KubernetesResource, index: number) => { + return ( + + + {showNamespace && ( + + )} + {props.columns?.map((column: Column) => { + return ( + + ); + })} + + + {props.getViewRoute && ( + + )} + + ); + }, + )} + +
+ Name + + Namespace + + {column.title} + + CPU + + Memory + + Actions +
+ {resource.name} + + + {resource.namespace || "default"} + + + {column.getValue + ? column.getValue(resource) + : resource.additionalAttributes[column.key] || + ""} + + 80 + ? "bg-red-50 text-red-700" + : resource.cpuUtilization !== null && + resource.cpuUtilization > 60 + ? "bg-yellow-50 text-yellow-700" + : "bg-green-50 text-green-700" + }`} + > + {KubernetesResourceUtils.formatCpuValue( + resource.cpuUtilization, + )} + + + {KubernetesResourceUtils.formatMemoryValue( + resource.memoryUsageBytes, + )} + + + View + +
+
+ )} +
+ ); +}; + +export default KubernetesResourceTable; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts new file mode 100644 index 0000000000..2b500e5117 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -0,0 +1,215 @@ +import Metric from "Common/Models/AnalyticsModels/Metric"; +import AnalyticsModelAPI from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI"; +import ProjectUtil from "Common/UI/Utils/Project"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType"; +import AggregatedResult from "Common/Types/BaseDatabase/AggregatedResult"; +import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax"; +import Dictionary from "Common/Types/Dictionary"; + +export interface KubernetesResource { + name: string; + namespace: string; + cpuUtilization: number | null; + memoryUsageBytes: number | null; + additionalAttributes: Record; +} + +export interface FetchResourceListOptions { + clusterIdentifier: string; + metricName: string; + resourceNameAttribute: string; + namespaceAttribute?: string; + additionalAttributes?: Array; + filterAttributes?: Dictionary; + hoursBack?: number; +} + +export default class KubernetesResourceUtils { + public static async fetchResourceList( + options: FetchResourceListOptions, + ): Promise> { + const { + clusterIdentifier, + metricName, + resourceNameAttribute, + namespaceAttribute = "resource.k8s.namespace.name", + additionalAttributes = [], + filterAttributes = {}, + hoursBack = 1, + } = options; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -hoursBack); + + const cpuResult: AggregatedResult = + await AnalyticsModelAPI.aggregate({ + modelType: Metric, + aggregateBy: { + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + time: new InBetween(startDate, endDate), + name: metricName, + attributes: { + "resource.k8s.cluster.name": clusterIdentifier, + ...filterAttributes, + } as Dictionary, + }, + aggregationType: MetricsAggregationType.Avg, + aggregateColumnName: "value", + aggregationTimestampColumnName: "time", + startTimestamp: startDate, + endTimestamp: endDate, + limit: LIMIT_PER_PROJECT, + skip: 0, + groupBy: { + attributes: true, + }, + }, + }); + + const resourceMap: Map = new Map(); + + for (const dataPoint of cpuResult.data) { + const attributes: Record = + (dataPoint["attributes"] as Record) || {}; + + const resourceName: string = + (attributes[resourceNameAttribute] as string) || ""; + + if (!resourceName) { + continue; + } + + const namespace: string = + (attributes[namespaceAttribute] as string) || ""; + + const key: string = `${namespace}/${resourceName}`; + + if (!resourceMap.has(key)) { + const additionalAttrs: Record = {}; + + for (const attr of additionalAttributes) { + additionalAttrs[attr] = (attributes[attr] as string) || ""; + } + + resourceMap.set(key, { + name: resourceName, + namespace: namespace, + cpuUtilization: dataPoint.value ?? null, + memoryUsageBytes: null, + additionalAttributes: additionalAttrs, + }); + } + } + + return Array.from(resourceMap.values()).sort( + (a: KubernetesResource, b: KubernetesResource) => { + const nsCompare: number = a.namespace.localeCompare(b.namespace); + if (nsCompare !== 0) { + return nsCompare; + } + return a.name.localeCompare(b.name); + }, + ); + } + + public static async fetchResourceListWithMemory( + options: FetchResourceListOptions & { memoryMetricName: string }, + ): Promise> { + const resources: Array = + await KubernetesResourceUtils.fetchResourceList(options); + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours( + endDate, + -(options.hoursBack || 1), + ); + + try { + const memoryResult: AggregatedResult = + await AnalyticsModelAPI.aggregate({ + modelType: Metric, + aggregateBy: { + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + time: new InBetween(startDate, endDate), + name: options.memoryMetricName, + attributes: { + "resource.k8s.cluster.name": options.clusterIdentifier, + ...(options.filterAttributes || {}), + } as Dictionary, + }, + aggregationType: MetricsAggregationType.Avg, + aggregateColumnName: "value", + aggregationTimestampColumnName: "time", + startTimestamp: startDate, + endTimestamp: endDate, + limit: LIMIT_PER_PROJECT, + skip: 0, + groupBy: { + attributes: true, + }, + }, + }); + + const memoryMap: Map = new Map(); + + for (const dataPoint of memoryResult.data) { + const attributes: Record = + (dataPoint["attributes"] as Record) || {}; + const resourceName: string = + (attributes[options.resourceNameAttribute] as string) || ""; + const namespace: string = + (attributes[ + options.namespaceAttribute || "resource.k8s.namespace.name" + ] as string) || ""; + const key: string = `${namespace}/${resourceName}`; + + if (resourceName && !memoryMap.has(key)) { + memoryMap.set(key, dataPoint.value ?? 0); + } + } + + for (const resource of resources) { + const key: string = `${resource.namespace}/${resource.name}`; + const memValue: number | undefined = memoryMap.get(key); + if (memValue !== undefined) { + resource.memoryUsageBytes = memValue; + } + } + } catch (_err) { + // Memory data is optional, don't fail if not available + } + + return resources; + } + + public static formatCpuValue(value: number | null): string { + if (value === null || value === undefined) { + return "N/A"; + } + return `${value.toFixed(1)}%`; + } + + public static formatMemoryValue(bytes: number | null): string { + if (bytes === null || bytes === undefined) { + return "N/A"; + } + + if (bytes < 1024) { + return `${bytes} B`; + } + + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } + + if (bytes < 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + } + + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + } +} diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx new file mode 100644 index 0000000000..7e7d66eba8 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx @@ -0,0 +1,173 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import MetricView from "../../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData, { + ChartSeries, +} from "Common/Types/Metrics/MetricQueryConfigData"; +import AggregationType from "Common/Types/BaseDatabase/AggregationType"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; + +const KubernetesClusterContainerDetail: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const containerName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const name: string = + (attributes["k8s.container.name"] as string) || "Unknown Container"; + return { title: name }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "container_cpu", + title: "Container CPU Utilization", + description: `CPU utilization for container ${containerName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "container.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "k8s.container.name": containerName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "container_memory", + title: "Container Memory Usage", + description: `Memory usage for container ${containerName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "container.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "k8s.container.name": containerName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterContainerDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx new file mode 100644 index 0000000000..9f7dec1aae --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx @@ -0,0 +1,107 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterContainers: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const containerList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "container.cpu.utilization", + memoryMetricName: "container.memory.usage", + resourceNameAttribute: "resource.k8s.container.name", + additionalAttributes: ["resource.k8s.pod.name"], + }); + + setResources(containerList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterContainers; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx new file mode 100644 index 0000000000..127baa2074 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -0,0 +1,173 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import MetricView from "../../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData, { + ChartSeries, +} from "Common/Types/Metrics/MetricQueryConfigData"; +import AggregationType from "Common/Types/BaseDatabase/AggregationType"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; + +const KubernetesClusterCronJobDetail: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const cronJobName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const podName: string = + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + return { title: podName }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "cronjob_cpu", + title: "Pod CPU Utilization", + description: `CPU utilization for pods in cronjob ${cronJobName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cronjob.name": cronJobName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "cronjob_memory", + title: "Pod Memory Usage", + description: `Memory usage for pods in cronjob ${cronJobName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cronjob.name": cronJobName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterCronJobDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx new file mode 100644 index 0000000000..6aecdf59a5 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx @@ -0,0 +1,100 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterCronJobs: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const cronjobList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.cronjob.name", + }); + + setResources(cronjobList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOB_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterCronJobs; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx new file mode 100644 index 0000000000..c29130f38c --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -0,0 +1,173 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import MetricView from "../../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData, { + ChartSeries, +} from "Common/Types/Metrics/MetricQueryConfigData"; +import AggregationType from "Common/Types/BaseDatabase/AggregationType"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; + +const KubernetesClusterDaemonSetDetail: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const daemonSetName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const podName: string = + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + return { title: podName }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "daemonset_cpu", + title: "Pod CPU Utilization", + description: `CPU utilization for pods in daemonset ${daemonSetName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.daemonset.name": daemonSetName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "daemonset_memory", + title: "Pod Memory Usage", + description: `Memory usage for pods in daemonset ${daemonSetName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.daemonset.name": daemonSetName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterDaemonSetDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx new file mode 100644 index 0000000000..2bbc94677f --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx @@ -0,0 +1,100 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterDaemonSets: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const daemonsetList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.daemonset.name", + }); + + setResources(daemonsetList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSET_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterDaemonSets; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx new file mode 100644 index 0000000000..f4f57ba3ba --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -0,0 +1,173 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import MetricView from "../../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData, { + ChartSeries, +} from "Common/Types/Metrics/MetricQueryConfigData"; +import AggregationType from "Common/Types/BaseDatabase/AggregationType"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; + +const KubernetesClusterDeploymentDetail: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const deploymentName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const podName: string = + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + return { title: podName }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "deployment_cpu", + title: "Pod CPU Utilization", + description: `CPU utilization for pods in deployment ${deploymentName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.deployment.name": deploymentName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "deployment_memory", + title: "Pod Memory Usage", + description: `Memory usage for pods in deployment ${deploymentName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.deployment.name": deploymentName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterDeploymentDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx new file mode 100644 index 0000000000..3dacfb9327 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx @@ -0,0 +1,100 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterDeployments: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const deploymentList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.deployment.name", + }); + + setResources(deploymentList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterDeployments; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx index fb10436918..ad81b8f750 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx @@ -6,6 +6,10 @@ import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail"; import FieldType from "Common/UI/Components/Types/FieldType"; import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import Card from "Common/UI/Components/Card/Card"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; import React, { Fragment, FunctionComponent, @@ -19,6 +23,12 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +interface ResourceLink { + title: string; + description: string; + pageMap: PageMap; +} + const KubernetesClusterOverview: FunctionComponent< PageComponentProps > = (): ReactElement => { @@ -75,6 +85,57 @@ const KubernetesClusterOverview: FunctionComponent< ? "text-green-600" : "text-red-600"; + const workloadLinks: Array = [ + { + title: "Namespaces", + description: "View all namespaces", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACES, + }, + { + title: "Pods", + description: "View all pods", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_PODS, + }, + { + title: "Deployments", + description: "View all deployments", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENTS, + }, + { + title: "StatefulSets", + description: "View all statefulsets", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSETS, + }, + { + title: "DaemonSets", + description: "View all daemonsets", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSETS, + }, + { + title: "Jobs", + description: "View all jobs", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_JOBS, + }, + { + title: "CronJobs", + description: "View all cron jobs", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOBS, + }, + ]; + + const infraLinks: Array = [ + { + title: "Nodes", + description: "View all nodes", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_NODES, + }, + { + title: "Containers", + description: "View all containers", + pageMap: PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINERS, + }, + ]; + return ( {/* Summary Cards */} @@ -115,6 +176,66 @@ const KubernetesClusterOverview: FunctionComponent< /> + {/* Quick Navigation - Workloads */} + +
+ {workloadLinks.map((link: ResourceLink) => { + return ( + +
+
+ {link.title} +
+
+ {link.description} +
+
+
+ ); + })} +
+
+ + {/* Quick Navigation - Infrastructure */} + +
+ {infraLinks.map((link: ResourceLink) => { + return ( + +
+
+ {link.title} +
+
+ {link.description} +
+
+
+ ); + })} +
+
+ {/* Cluster Details */} name="Cluster Details" diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx new file mode 100644 index 0000000000..5056378511 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -0,0 +1,173 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import MetricView from "../../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData, { + ChartSeries, +} from "Common/Types/Metrics/MetricQueryConfigData"; +import AggregationType from "Common/Types/BaseDatabase/AggregationType"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; + +const KubernetesClusterJobDetail: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const jobName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const podName: string = + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + return { title: podName }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "job_cpu", + title: "Pod CPU Utilization", + description: `CPU utilization for pods in job ${jobName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.job.name": jobName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "job_memory", + title: "Pod Memory Usage", + description: `Memory usage for pods in job ${jobName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.job.name": jobName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterJobDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx new file mode 100644 index 0000000000..4b35e99940 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx @@ -0,0 +1,100 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterJobs: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const jobList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.job.name", + }); + + setResources(jobList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_JOB_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterJobs; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx new file mode 100644 index 0000000000..91996587e4 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -0,0 +1,173 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import MetricView from "../../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData, { + ChartSeries, +} from "Common/Types/Metrics/MetricQueryConfigData"; +import AggregationType from "Common/Types/BaseDatabase/AggregationType"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; + +const KubernetesClusterNamespaceDetail: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const namespaceName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const podName: string = + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + return { title: podName }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "namespace_cpu", + title: "Pod CPU Utilization", + description: `CPU utilization for pods in namespace ${namespaceName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.namespace.name": namespaceName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "namespace_memory", + title: "Pod Memory Usage", + description: `Memory usage for pods in namespace ${namespaceName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.namespace.name": namespaceName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterNamespaceDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx new file mode 100644 index 0000000000..652d0168d0 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx @@ -0,0 +1,102 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterNamespaces: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const namespaceList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.namespace.name", + namespaceAttribute: "resource.k8s.namespace.name", + }); + + setResources(namespaceList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACE_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterNamespaces; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx index ae95b70e2e..92ef97b42a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx @@ -2,14 +2,10 @@ import PageComponentProps from "../../PageComponentProps"; import ObjectID from "Common/Types/ObjectID"; import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; -import MetricQueryConfigData, { - ChartSeries, -} from "Common/Types/Metrics/MetricQueryConfigData"; -import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; import React, { Fragment, FunctionComponent, @@ -22,31 +18,46 @@ import API from "Common/UI/Utils/API/API"; import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; -import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; const KubernetesClusterNodes: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); - const [cluster, setCluster] = useState(null); + const [resources, setResources] = useState>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - const [metricViewData, setMetricViewData] = useState( - null, - ); - const fetchCluster: PromiseVoidFunction = async (): Promise => { + const fetchData: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { - const item: KubernetesCluster | null = await ModelAPI.getItem({ + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ modelType: KubernetesCluster, id: modelId, select: { clusterIdentifier: true, }, }); - setCluster(item); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const nodeList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.node.cpu.utilization", + memoryMetricName: "k8s.node.memory.usage", + resourceNameAttribute: "resource.k8s.node.name", + namespaceAttribute: "resource.k8s.node.name", + }); + + setResources(nodeList); } catch (err) { setError(API.getFriendlyMessage(err)); } @@ -54,140 +65,11 @@ const KubernetesClusterNodes: FunctionComponent< }; useEffect(() => { - fetchCluster().catch((err: Error) => { + fetchData().catch((err: Error) => { setError(API.getFriendlyMessage(err)); }); }, []); - useEffect(() => { - if (!cluster) { - return; - } - - const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const getNodeSeries: (data: AggregateModel) => ChartSeries = ( - data: AggregateModel, - ): ChartSeries => { - const attributes: Record = - (data["attributes"] as Record) || {}; - const nodeName: string = - (attributes["resource.k8s.node.name"] as string) || "Unknown Node"; - return { title: nodeName }; - }; - - const nodeCpuQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "node_cpu", - title: "Node CPU Utilization", - description: "CPU utilization by node", - legend: "CPU", - legendUnit: "%", - }, - metricQueryData: { - filterData: { - metricName: "k8s.node.cpu.utilization", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getNodeSeries, - }; - - const nodeMemoryQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "node_memory", - title: "Node Memory Usage", - description: "Memory usage by node", - legend: "Memory", - legendUnit: "bytes", - }, - metricQueryData: { - filterData: { - metricName: "k8s.node.memory.usage", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getNodeSeries, - }; - - const nodeFilesystemQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "node_filesystem", - title: "Node Filesystem Usage", - description: "Filesystem usage by node", - legend: "Filesystem", - legendUnit: "bytes", - }, - metricQueryData: { - filterData: { - metricName: "k8s.node.filesystem.usage", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getNodeSeries, - }; - - const nodeNetworkRxQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "node_network_rx", - title: "Node Network Receive", - description: "Network bytes received by node", - legend: "Network RX", - legendUnit: "bytes/s", - }, - metricQueryData: { - filterData: { - metricName: "k8s.node.network.io", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - "metricAttributes.direction": "receive", - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getNodeSeries, - }; - - setMetricViewData({ - startAndEndDate: startAndEndDate, - queryConfigs: [ - nodeCpuQuery, - nodeMemoryQuery, - nodeFilesystemQuery, - nodeNetworkRxQuery, - ], - formulaConfigs: [], - }); - }, [cluster]); - if (isLoading) { return ; } @@ -196,21 +78,21 @@ const KubernetesClusterNodes: FunctionComponent< return ; } - if (!cluster || !metricViewData) { - return ; - } - return ( - { - setMetricViewData({ - ...data, - queryConfigs: metricViewData.queryConfigs, - formulaConfigs: [], - }); + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); }} /> diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx index f8191c2541..cd90eacda8 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx @@ -2,14 +2,10 @@ import PageComponentProps from "../../PageComponentProps"; import ObjectID from "Common/Types/ObjectID"; import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; -import MetricQueryConfigData, { - ChartSeries, -} from "Common/Types/Metrics/MetricQueryConfigData"; -import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; import React, { Fragment, FunctionComponent, @@ -22,31 +18,49 @@ import API from "Common/UI/Utils/API/API"; import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; -import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; const KubernetesClusterPods: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); - const [cluster, setCluster] = useState(null); + const [resources, setResources] = useState>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - const [metricViewData, setMetricViewData] = useState( - null, - ); - const fetchCluster: PromiseVoidFunction = async (): Promise => { + const fetchData: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { - const item: KubernetesCluster | null = await ModelAPI.getItem({ + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ modelType: KubernetesCluster, id: modelId, select: { clusterIdentifier: true, }, }); - setCluster(item); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const podList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.pod.name", + additionalAttributes: [ + "resource.k8s.node.name", + "resource.k8s.deployment.name", + ], + }); + + setResources(podList); } catch (err) { setError(API.getFriendlyMessage(err)); } @@ -54,143 +68,11 @@ const KubernetesClusterPods: FunctionComponent< }; useEffect(() => { - fetchCluster().catch((err: Error) => { + fetchData().catch((err: Error) => { setError(API.getFriendlyMessage(err)); }); }, []); - useEffect(() => { - if (!cluster) { - return; - } - - const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const getPodSeries: (data: AggregateModel) => ChartSeries = ( - data: AggregateModel, - ): ChartSeries => { - const attributes: Record = - (data["attributes"] as Record) || {}; - const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; - const namespace: string = - (attributes["resource.k8s.namespace.name"] as string) || ""; - return { title: namespace ? `${namespace}/${podName}` : podName }; - }; - - const podCpuQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "pod_cpu", - title: "Pod CPU Utilization", - description: "CPU utilization by pod", - legend: "CPU", - legendUnit: "%", - }, - metricQueryData: { - filterData: { - metricName: "k8s.pod.cpu.utilization", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getPodSeries, - }; - - const podMemoryQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "pod_memory", - title: "Pod Memory Usage", - description: "Memory usage by pod", - legend: "Memory", - legendUnit: "bytes", - }, - metricQueryData: { - filterData: { - metricName: "k8s.pod.memory.usage", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getPodSeries, - }; - - const podNetworkRxQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "pod_network_rx", - title: "Pod Network Receive", - description: "Network bytes received by pod", - legend: "Network RX", - legendUnit: "bytes/s", - }, - metricQueryData: { - filterData: { - metricName: "k8s.pod.network.io", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - "metricAttributes.direction": "receive", - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getPodSeries, - }; - - const podNetworkTxQuery: MetricQueryConfigData = { - metricAliasData: { - metricVariable: "pod_network_tx", - title: "Pod Network Transmit", - description: "Network bytes transmitted by pod", - legend: "Network TX", - legendUnit: "bytes/s", - }, - metricQueryData: { - filterData: { - metricName: "k8s.pod.network.io", - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - "metricAttributes.direction": "transmit", - }, - aggegationType: AggregationType.Avg, - aggregateBy: {}, - }, - groupBy: { - attributes: true, - }, - }, - getSeries: getPodSeries, - }; - - setMetricViewData({ - startAndEndDate: startAndEndDate, - queryConfigs: [ - podCpuQuery, - podMemoryQuery, - podNetworkRxQuery, - podNetworkTxQuery, - ], - formulaConfigs: [], - }); - }, [cluster]); - if (isLoading) { return ; } @@ -199,21 +81,26 @@ const KubernetesClusterPods: FunctionComponent< return ; } - if (!cluster || !metricViewData) { - return ; - } - return ( - { - setMetricViewData({ - ...data, - queryConfigs: metricViewData.queryConfigs, - formulaConfigs: [], - }); + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); }} /> diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx index ff62834736..67031555a9 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx @@ -40,7 +40,17 @@ const KubernetesClusterSideMenu: FunctionComponent = ( /> - + + = ( }} icon={IconProp.Circle} /> + + + + + + + + = ( }} icon={IconProp.Server} /> + @@ -87,6 +160,16 @@ const KubernetesClusterSideMenu: FunctionComponent = ( + = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); + const statefulSetName: string = Navigation.getLastParam()?.toString() || ""; + + const [cluster, setCluster] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchCluster: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const item: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + setCluster(item); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchCluster().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!cluster) { + return ; + } + + const clusterIdentifier: string = cluster.clusterIdentifier || ""; + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const getSeries: (data: AggregateModel) => ChartSeries = ( + data: AggregateModel, + ): ChartSeries => { + const attributes: Record = + (data["attributes"] as Record) || {}; + const podName: string = + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + return { title: podName }; + }; + + const cpuQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "statefulset_cpu", + title: "Pod CPU Utilization", + description: `CPU utilization for pods in statefulset ${statefulSetName}`, + legend: "CPU", + legendUnit: "%", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.cpu.utilization", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.statefulset.name": statefulSetName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const memoryQuery: MetricQueryConfigData = { + metricAliasData: { + metricVariable: "statefulset_memory", + title: "Pod Memory Usage", + description: `Memory usage for pods in statefulset ${statefulSetName}`, + legend: "Memory", + legendUnit: "bytes", + }, + metricQueryData: { + filterData: { + metricName: "k8s.pod.memory.usage", + attributes: { + "k8s.cluster.name": clusterIdentifier, + "resource.k8s.statefulset.name": statefulSetName, + }, + aggegationType: AggregationType.Avg, + aggregateBy: {}, + }, + groupBy: { + attributes: true, + }, + }, + getSeries: getSeries, + }; + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + + return ( + +
+ + +
+ + + { + setMetricViewData({ + ...data, + queryConfigs: [cpuQuery, memoryQuery], + formulaConfigs: [], + }); + }} + /> + +
+ ); +}; + +export default KubernetesClusterStatefulSetDetail; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx new file mode 100644 index 0000000000..352a60f552 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx @@ -0,0 +1,100 @@ +import PageComponentProps from "../../PageComponentProps"; +import ObjectID from "Common/Types/ObjectID"; +import Navigation from "Common/UI/Utils/Navigation"; +import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; +import KubernetesResourceTable from "../../../Components/Kubernetes/KubernetesResourceTable"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; +import React, { + Fragment, + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import API from "Common/UI/Utils/API/API"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import PageMap from "../../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; + +const KubernetesClusterStatefulSets: FunctionComponent< + PageComponentProps +> = (): ReactElement => { + const modelId: ObjectID = Navigation.getLastParamAsObjectID(1); + + const [resources, setResources] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + const fetchData: PromiseVoidFunction = async (): Promise => { + setIsLoading(true); + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { + clusterIdentifier: true, + }, + }); + + if (!cluster?.clusterIdentifier) { + setError("Cluster not found."); + setIsLoading(false); + return; + } + + const statefulsetList: Array = + await KubernetesResourceUtils.fetchResourceListWithMemory({ + clusterIdentifier: cluster.clusterIdentifier, + metricName: "k8s.pod.cpu.utilization", + memoryMetricName: "k8s.pod.memory.usage", + resourceNameAttribute: "resource.k8s.statefulset.name", + }); + + setResources(statefulsetList); + } catch (err) { + setError(API.getFriendlyMessage(err)); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchData().catch((err: Error) => { + setError(API.getFriendlyMessage(err)); + }); + }, []); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + return ( + + { + return RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL] as Route, + { + modelId: modelId, + subModelId: new ObjectID(resource.name), + }, + ); + }} + /> + + ); +}; + +export default KubernetesClusterStatefulSets; diff --git a/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx b/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx index ae8920621d..515dc8aab1 100644 --- a/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx +++ b/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx @@ -10,13 +10,28 @@ import { Route as PageRoute, Routes } from "react-router-dom"; // Pages import KubernetesClusters from "../Pages/Kubernetes/Clusters"; import KubernetesClusterView from "../Pages/Kubernetes/View/Index"; +import KubernetesClusterViewNamespaces from "../Pages/Kubernetes/View/Namespaces"; +import KubernetesClusterViewNamespaceDetail from "../Pages/Kubernetes/View/NamespaceDetail"; import KubernetesClusterViewPods from "../Pages/Kubernetes/View/Pods"; import KubernetesClusterViewPodDetail from "../Pages/Kubernetes/View/PodDetail"; +import KubernetesClusterViewDeployments from "../Pages/Kubernetes/View/Deployments"; +import KubernetesClusterViewDeploymentDetail from "../Pages/Kubernetes/View/DeploymentDetail"; +import KubernetesClusterViewStatefulSets from "../Pages/Kubernetes/View/StatefulSets"; +import KubernetesClusterViewStatefulSetDetail from "../Pages/Kubernetes/View/StatefulSetDetail"; +import KubernetesClusterViewDaemonSets from "../Pages/Kubernetes/View/DaemonSets"; +import KubernetesClusterViewDaemonSetDetail from "../Pages/Kubernetes/View/DaemonSetDetail"; +import KubernetesClusterViewJobs from "../Pages/Kubernetes/View/Jobs"; +import KubernetesClusterViewJobDetail from "../Pages/Kubernetes/View/JobDetail"; +import KubernetesClusterViewCronJobs from "../Pages/Kubernetes/View/CronJobs"; +import KubernetesClusterViewCronJobDetail from "../Pages/Kubernetes/View/CronJobDetail"; import KubernetesClusterViewNodes from "../Pages/Kubernetes/View/Nodes"; import KubernetesClusterViewNodeDetail from "../Pages/Kubernetes/View/NodeDetail"; +import KubernetesClusterViewContainers from "../Pages/Kubernetes/View/Containers"; +import KubernetesClusterViewContainerDetail from "../Pages/Kubernetes/View/ContainerDetail"; import KubernetesClusterViewEvents from "../Pages/Kubernetes/View/Events"; import KubernetesClusterViewControlPlane from "../Pages/Kubernetes/View/ControlPlane"; import KubernetesClusterViewDelete from "../Pages/Kubernetes/View/Delete"; +import KubernetesClusterViewSettings from "../Pages/Kubernetes/View/Settings"; import KubernetesClusterViewDocumentation from "../Pages/Kubernetes/View/Documentation"; import KubernetesDocumentation from "../Pages/Kubernetes/Documentation"; @@ -60,6 +75,38 @@ const KubernetesRoutes: FunctionComponent = ( } /> + {/* Namespaces */} + + } + /> + + + } + /> + + {/* Pods */} = ( } /> + {/* Deployments */} + + } + /> + + + } + /> + + {/* StatefulSets */} + + } + /> + + + } + /> + + {/* DaemonSets */} + + } + /> + + + } + /> + + {/* Jobs */} + + } + /> + + + } + /> + + {/* CronJobs */} + + } + /> + + + } + /> + + {/* Nodes */} = ( } /> + {/* Containers */} + + } + /> + + + } + /> + + {/* Events */} = ( } /> + {/* Control Plane */} = ( } /> + {/* Settings */} + + } + /> + + {/* Delete */} = ( } /> + {/* Documentation */} = { export const KubernetesRoutePath: Dictionary = { [PageMap.KUBERNETES_CLUSTER_VIEW]: `${RouteParams.ModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACES]: `${RouteParams.ModelID}/namespaces`, + [PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACE_DETAIL]: `${RouteParams.ModelID}/namespaces/${RouteParams.SubModelID}`, [PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: `${RouteParams.ModelID}/pods`, [PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: `${RouteParams.ModelID}/pods/${RouteParams.SubModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENTS]: `${RouteParams.ModelID}/deployments`, + [PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL]: `${RouteParams.ModelID}/deployments/${RouteParams.SubModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSETS]: `${RouteParams.ModelID}/statefulsets`, + [PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL]: `${RouteParams.ModelID}/statefulsets/${RouteParams.SubModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSETS]: `${RouteParams.ModelID}/daemonsets`, + [PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSET_DETAIL]: `${RouteParams.ModelID}/daemonsets/${RouteParams.SubModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_JOBS]: `${RouteParams.ModelID}/jobs`, + [PageMap.KUBERNETES_CLUSTER_VIEW_JOB_DETAIL]: `${RouteParams.ModelID}/jobs/${RouteParams.SubModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOBS]: `${RouteParams.ModelID}/cronjobs`, + [PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOB_DETAIL]: `${RouteParams.ModelID}/cronjobs/${RouteParams.SubModelID}`, [PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: `${RouteParams.ModelID}/nodes`, [PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: `${RouteParams.ModelID}/nodes/${RouteParams.SubModelID}`, + [PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINERS]: `${RouteParams.ModelID}/containers`, + [PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL]: `${RouteParams.ModelID}/containers/${RouteParams.SubModelID}`, [PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: `${RouteParams.ModelID}/events`, [PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: `${RouteParams.ModelID}/control-plane`, [PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: `${RouteParams.ModelID}/delete`, @@ -1499,6 +1513,18 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACES]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACES] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACE_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NAMESPACE_DETAIL] + }`, + ), + [PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: new Route( `/dashboard/${RouteParams.ProjectID}/kubernetes/${ KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] @@ -1511,6 +1537,66 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENTS]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENTS] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSETS]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSETS] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSETS]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSETS] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSET_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DAEMONSET_DETAIL] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_JOBS]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_JOBS] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_JOB_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_JOB_DETAIL] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOBS]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOBS] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOB_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CRONJOB_DETAIL] + }`, + ), + [PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: new Route( `/dashboard/${RouteParams.ProjectID}/kubernetes/${ KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] @@ -1523,6 +1609,18 @@ const RouteMap: Dictionary = { }`, ), + [PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINERS]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINERS] + }`, + ), + + [PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL]: new Route( + `/dashboard/${RouteParams.ProjectID}/kubernetes/${ + KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CONTAINER_DETAIL] + }`, + ), + [PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: new Route( `/dashboard/${RouteParams.ProjectID}/kubernetes/${ KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] From 3ddd5658a1d2ffeb442128763cf66a4e2dbc3c65 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 19:41:48 +0000 Subject: [PATCH 04/23] refactor: Update Kubernetes resource attribute names for consistency --- .../Kubernetes/Utils/KubernetesResourceUtils.ts | 8 ++++---- .../src/Pages/Kubernetes/View/Containers.tsx | 6 +++--- .../src/Pages/Kubernetes/View/ControlPlane.tsx | 12 ++++++------ .../src/Pages/Kubernetes/View/CronJobDetail.tsx | 6 +++--- .../Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx | 2 +- .../src/Pages/Kubernetes/View/DaemonSetDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/DaemonSets.tsx | 2 +- .../src/Pages/Kubernetes/View/DeploymentDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/Deployments.tsx | 2 +- .../src/Pages/Kubernetes/View/JobDetail.tsx | 6 +++--- .../Dashboard/src/Pages/Kubernetes/View/Jobs.tsx | 2 +- .../src/Pages/Kubernetes/View/NamespaceDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/Namespaces.tsx | 4 ++-- .../Dashboard/src/Pages/Kubernetes/View/Nodes.tsx | 4 ++-- .../Dashboard/src/Pages/Kubernetes/View/Pods.tsx | 8 ++++---- .../src/Pages/Kubernetes/View/StatefulSetDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/StatefulSets.tsx | 2 +- 17 files changed, 44 insertions(+), 44 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts index 2b500e5117..5c4d5a801a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -34,7 +34,7 @@ export default class KubernetesResourceUtils { clusterIdentifier, metricName, resourceNameAttribute, - namespaceAttribute = "resource.k8s.namespace.name", + namespaceAttribute = "k8s.namespace.name", additionalAttributes = [], filterAttributes = {}, hoursBack = 1, @@ -52,7 +52,7 @@ export default class KubernetesResourceUtils { time: new InBetween(startDate, endDate), name: metricName, attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, ...filterAttributes, } as Dictionary, }, @@ -137,7 +137,7 @@ export default class KubernetesResourceUtils { time: new InBetween(startDate, endDate), name: options.memoryMetricName, attributes: { - "resource.k8s.cluster.name": options.clusterIdentifier, + "k8s.cluster.name": options.clusterIdentifier, ...(options.filterAttributes || {}), } as Dictionary, }, @@ -163,7 +163,7 @@ export default class KubernetesResourceUtils { (attributes[options.resourceNameAttribute] as string) || ""; const namespace: string = (attributes[ - options.namespaceAttribute || "resource.k8s.namespace.name" + options.namespaceAttribute || "k8s.namespace.name" ] as string) || ""; const key: string = `${namespace}/${resourceName}`; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx index 9f7dec1aae..1b8c51169f 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx @@ -53,8 +53,8 @@ const KubernetesClusterContainers: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "container.cpu.utilization", memoryMetricName: "container.memory.usage", - resourceNameAttribute: "resource.k8s.container.name", - additionalAttributes: ["resource.k8s.pod.name"], + resourceNameAttribute: "k8s.container.name", + additionalAttributes: ["k8s.pod.name"], }); setResources(containerList); @@ -87,7 +87,7 @@ const KubernetesClusterContainers: FunctionComponent< columns={[ { title: "Pod", - key: "resource.k8s.pod.name", + key: "k8s.pod.name", }, ]} getViewRoute={(resource: KubernetesResource) => { diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx index 5fb856a253..ebbfbf8094 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx @@ -84,7 +84,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "etcd_mvcc_db_total_size_in_bytes", attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -107,7 +107,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "apiserver_request_total", attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Sum, aggregateBy: {}, @@ -130,7 +130,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "apiserver_request_duration_seconds", attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -153,7 +153,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "scheduler_pending_pods", attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -176,7 +176,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "scheduler_e2e_scheduling_duration_seconds", attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -199,7 +199,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "workqueue_depth", attributes: { - "resource.k8s.cluster.name": clusterIdentifier, + "k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index 127baa2074..c3af61d157 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.cronjob.name": cronJobName, + "k8s.cronjob.name": cronJobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.cronjob.name": cronJobName, + "k8s.cronjob.name": cronJobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx index 6aecdf59a5..d3f387d0a5 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx @@ -53,7 +53,7 @@ const KubernetesClusterCronJobs: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.cronjob.name", + resourceNameAttribute: "k8s.cronjob.name", }); setResources(cronjobList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index c29130f38c..34f77b6247 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.daemonset.name": daemonSetName, + "k8s.daemonset.name": daemonSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.daemonset.name": daemonSetName, + "k8s.daemonset.name": daemonSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx index 2bbc94677f..36c88336a8 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx @@ -53,7 +53,7 @@ const KubernetesClusterDaemonSets: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.daemonset.name", + resourceNameAttribute: "k8s.daemonset.name", }); setResources(daemonsetList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index f4f57ba3ba..1317266107 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.deployment.name": deploymentName, + "k8s.deployment.name": deploymentName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.deployment.name": deploymentName, + "k8s.deployment.name": deploymentName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx index 3dacfb9327..c4c9ee5746 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx @@ -53,7 +53,7 @@ const KubernetesClusterDeployments: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.deployment.name", + resourceNameAttribute: "k8s.deployment.name", }); setResources(deploymentList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index 5056378511..adf0f1cd3d 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterJobDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterJobDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.job.name": jobName, + "k8s.job.name": jobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterJobDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.job.name": jobName, + "k8s.job.name": jobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx index 4b35e99940..7ec9b888cd 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx @@ -53,7 +53,7 @@ const KubernetesClusterJobs: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.job.name", + resourceNameAttribute: "k8s.job.name", }); setResources(jobList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index 91996587e4..456ab77d89 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.namespace.name": namespaceName, + "k8s.namespace.name": namespaceName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.namespace.name": namespaceName, + "k8s.namespace.name": namespaceName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx index 652d0168d0..67f1114b4d 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx @@ -53,8 +53,8 @@ const KubernetesClusterNamespaces: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.namespace.name", - namespaceAttribute: "resource.k8s.namespace.name", + resourceNameAttribute: "k8s.namespace.name", + namespaceAttribute: "k8s.namespace.name", }); setResources(namespaceList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx index 92ef97b42a..f0576df74d 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx @@ -53,8 +53,8 @@ const KubernetesClusterNodes: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.node.cpu.utilization", memoryMetricName: "k8s.node.memory.usage", - resourceNameAttribute: "resource.k8s.node.name", - namespaceAttribute: "resource.k8s.node.name", + resourceNameAttribute: "k8s.node.name", + namespaceAttribute: "k8s.node.name", }); setResources(nodeList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx index cd90eacda8..91215e90eb 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx @@ -53,10 +53,10 @@ const KubernetesClusterPods: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.pod.name", + resourceNameAttribute: "k8s.pod.name", additionalAttributes: [ - "resource.k8s.node.name", - "resource.k8s.deployment.name", + "k8s.node.name", + "k8s.deployment.name", ], }); @@ -90,7 +90,7 @@ const KubernetesClusterPods: FunctionComponent< columns={[ { title: "Node", - key: "resource.k8s.node.name", + key: "k8s.node.name", }, ]} getViewRoute={(resource: KubernetesResource) => { diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index 8ec7ed562e..dab2b64559 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.statefulset.name": statefulSetName, + "k8s.statefulset.name": statefulSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "resource.k8s.statefulset.name": statefulSetName, + "k8s.statefulset.name": statefulSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx index 352a60f552..384e35c95b 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx @@ -53,7 +53,7 @@ const KubernetesClusterStatefulSets: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "resource.k8s.statefulset.name", + resourceNameAttribute: "k8s.statefulset.name", }); setResources(statefulsetList); From 6af7f24d1bdd87691c117f3c7f54f8c673bd0e19 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 19:52:38 +0000 Subject: [PATCH 05/23] Revert "refactor: Update Kubernetes resource attribute names for consistency" This reverts commit 3ddd5658a1d2ffeb442128763cf66a4e2dbc3c65. --- .../Kubernetes/Utils/KubernetesResourceUtils.ts | 8 ++++---- .../src/Pages/Kubernetes/View/Containers.tsx | 6 +++--- .../src/Pages/Kubernetes/View/ControlPlane.tsx | 12 ++++++------ .../src/Pages/Kubernetes/View/CronJobDetail.tsx | 6 +++--- .../Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx | 2 +- .../src/Pages/Kubernetes/View/DaemonSetDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/DaemonSets.tsx | 2 +- .../src/Pages/Kubernetes/View/DeploymentDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/Deployments.tsx | 2 +- .../src/Pages/Kubernetes/View/JobDetail.tsx | 6 +++--- .../Dashboard/src/Pages/Kubernetes/View/Jobs.tsx | 2 +- .../src/Pages/Kubernetes/View/NamespaceDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/Namespaces.tsx | 4 ++-- .../Dashboard/src/Pages/Kubernetes/View/Nodes.tsx | 4 ++-- .../Dashboard/src/Pages/Kubernetes/View/Pods.tsx | 8 ++++---- .../src/Pages/Kubernetes/View/StatefulSetDetail.tsx | 6 +++--- .../src/Pages/Kubernetes/View/StatefulSets.tsx | 2 +- 17 files changed, 44 insertions(+), 44 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts index 5c4d5a801a..2b500e5117 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -34,7 +34,7 @@ export default class KubernetesResourceUtils { clusterIdentifier, metricName, resourceNameAttribute, - namespaceAttribute = "k8s.namespace.name", + namespaceAttribute = "resource.k8s.namespace.name", additionalAttributes = [], filterAttributes = {}, hoursBack = 1, @@ -52,7 +52,7 @@ export default class KubernetesResourceUtils { time: new InBetween(startDate, endDate), name: metricName, attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, ...filterAttributes, } as Dictionary, }, @@ -137,7 +137,7 @@ export default class KubernetesResourceUtils { time: new InBetween(startDate, endDate), name: options.memoryMetricName, attributes: { - "k8s.cluster.name": options.clusterIdentifier, + "resource.k8s.cluster.name": options.clusterIdentifier, ...(options.filterAttributes || {}), } as Dictionary, }, @@ -163,7 +163,7 @@ export default class KubernetesResourceUtils { (attributes[options.resourceNameAttribute] as string) || ""; const namespace: string = (attributes[ - options.namespaceAttribute || "k8s.namespace.name" + options.namespaceAttribute || "resource.k8s.namespace.name" ] as string) || ""; const key: string = `${namespace}/${resourceName}`; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx index 1b8c51169f..9f7dec1aae 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Containers.tsx @@ -53,8 +53,8 @@ const KubernetesClusterContainers: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "container.cpu.utilization", memoryMetricName: "container.memory.usage", - resourceNameAttribute: "k8s.container.name", - additionalAttributes: ["k8s.pod.name"], + resourceNameAttribute: "resource.k8s.container.name", + additionalAttributes: ["resource.k8s.pod.name"], }); setResources(containerList); @@ -87,7 +87,7 @@ const KubernetesClusterContainers: FunctionComponent< columns={[ { title: "Pod", - key: "k8s.pod.name", + key: "resource.k8s.pod.name", }, ]} getViewRoute={(resource: KubernetesResource) => { diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx index ebbfbf8094..5fb856a253 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ControlPlane.tsx @@ -84,7 +84,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "etcd_mvcc_db_total_size_in_bytes", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -107,7 +107,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "apiserver_request_total", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Sum, aggregateBy: {}, @@ -130,7 +130,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "apiserver_request_duration_seconds", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -153,7 +153,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "scheduler_pending_pods", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -176,7 +176,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "scheduler_e2e_scheduling_duration_seconds", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -199,7 +199,7 @@ const KubernetesClusterControlPlane: FunctionComponent< filterData: { metricName: "workqueue_depth", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index c3af61d157..127baa2074 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.cronjob.name": cronJobName, + "resource.k8s.cronjob.name": cronJobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.cronjob.name": cronJobName, + "resource.k8s.cronjob.name": cronJobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx index d3f387d0a5..6aecdf59a5 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobs.tsx @@ -53,7 +53,7 @@ const KubernetesClusterCronJobs: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.cronjob.name", + resourceNameAttribute: "resource.k8s.cronjob.name", }); setResources(cronjobList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index 34f77b6247..c29130f38c 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.daemonset.name": daemonSetName, + "resource.k8s.daemonset.name": daemonSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.daemonset.name": daemonSetName, + "resource.k8s.daemonset.name": daemonSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx index 36c88336a8..2bbc94677f 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSets.tsx @@ -53,7 +53,7 @@ const KubernetesClusterDaemonSets: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.daemonset.name", + resourceNameAttribute: "resource.k8s.daemonset.name", }); setResources(daemonsetList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index 1317266107..f4f57ba3ba 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.deployment.name": deploymentName, + "resource.k8s.deployment.name": deploymentName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.deployment.name": deploymentName, + "resource.k8s.deployment.name": deploymentName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx index c4c9ee5746..3dacfb9327 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx @@ -53,7 +53,7 @@ const KubernetesClusterDeployments: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.deployment.name", + resourceNameAttribute: "resource.k8s.deployment.name", }); setResources(deploymentList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index adf0f1cd3d..5056378511 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterJobDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterJobDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.job.name": jobName, + "resource.k8s.job.name": jobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterJobDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.job.name": jobName, + "resource.k8s.job.name": jobName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx index 7ec9b888cd..4b35e99940 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Jobs.tsx @@ -53,7 +53,7 @@ const KubernetesClusterJobs: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.job.name", + resourceNameAttribute: "resource.k8s.job.name", }); setResources(jobList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index 456ab77d89..91996587e4 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.namespace.name": namespaceName, + "resource.k8s.namespace.name": namespaceName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.namespace.name": namespaceName, + "resource.k8s.namespace.name": namespaceName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx index 67f1114b4d..652d0168d0 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Namespaces.tsx @@ -53,8 +53,8 @@ const KubernetesClusterNamespaces: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.namespace.name", - namespaceAttribute: "k8s.namespace.name", + resourceNameAttribute: "resource.k8s.namespace.name", + namespaceAttribute: "resource.k8s.namespace.name", }); setResources(namespaceList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx index f0576df74d..92ef97b42a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx @@ -53,8 +53,8 @@ const KubernetesClusterNodes: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.node.cpu.utilization", memoryMetricName: "k8s.node.memory.usage", - resourceNameAttribute: "k8s.node.name", - namespaceAttribute: "k8s.node.name", + resourceNameAttribute: "resource.k8s.node.name", + namespaceAttribute: "resource.k8s.node.name", }); setResources(nodeList); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx index 91215e90eb..cd90eacda8 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx @@ -53,10 +53,10 @@ const KubernetesClusterPods: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.pod.name", + resourceNameAttribute: "resource.k8s.pod.name", additionalAttributes: [ - "k8s.node.name", - "k8s.deployment.name", + "resource.k8s.node.name", + "resource.k8s.deployment.name", ], }); @@ -90,7 +90,7 @@ const KubernetesClusterPods: FunctionComponent< columns={[ { title: "Node", - key: "k8s.node.name", + key: "resource.k8s.node.name", }, ]} getViewRoute={(resource: KubernetesResource) => { diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index dab2b64559..8ec7ed562e 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const podName: string = - (attributes["k8s.pod.name"] as string) || "Unknown Pod"; + (attributes["resource.k8s.pod.name"] as string) || "Unknown Pod"; return { title: podName }; }; @@ -100,7 +100,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< metricName: "k8s.pod.cpu.utilization", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.statefulset.name": statefulSetName, + "resource.k8s.statefulset.name": statefulSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -125,7 +125,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< metricName: "k8s.pod.memory.usage", attributes: { "k8s.cluster.name": clusterIdentifier, - "k8s.statefulset.name": statefulSetName, + "resource.k8s.statefulset.name": statefulSetName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx index 384e35c95b..352a60f552 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx @@ -53,7 +53,7 @@ const KubernetesClusterStatefulSets: FunctionComponent< clusterIdentifier: cluster.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", memoryMetricName: "k8s.pod.memory.usage", - resourceNameAttribute: "k8s.statefulset.name", + resourceNameAttribute: "resource.k8s.statefulset.name", }); setResources(statefulsetList); From 758aab5f17149feddf6ba67099ec90380771cec1 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 19:56:00 +0000 Subject: [PATCH 06/23] fix: Update default hoursBack value to 24 in fetchResourceList options --- .../src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts index 2b500e5117..13cb238408 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -37,7 +37,7 @@ export default class KubernetesResourceUtils { namespaceAttribute = "resource.k8s.namespace.name", additionalAttributes = [], filterAttributes = {}, - hoursBack = 1, + hoursBack = 24, } = options; const endDate: Date = OneUptimeDate.getCurrentDate(); From 7cc6e81fe62034772be2b43800b452042fe84bf4 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 19:56:30 +0000 Subject: [PATCH 07/23] fix: Update hoursBack default value to 1 in fetchResourceList options --- .../src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts index 13cb238408..2b500e5117 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -37,7 +37,7 @@ export default class KubernetesResourceUtils { namespaceAttribute = "resource.k8s.namespace.name", additionalAttributes = [], filterAttributes = {}, - hoursBack = 24, + hoursBack = 1, } = options; const endDate: Date = OneUptimeDate.getCurrentDate(); From a2c8022442e9b4e957863db30c1a9d0abed9f600 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 20:53:31 +0000 Subject: [PATCH 08/23] fix: Update default hoursBack value to 24 in fetchResourceList options --- .../src/Components/Logs/LogsViewer.tsx | 20 +++++++++++++++++-- .../Utils/KubernetesResourceUtils.ts | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx b/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx index 7a1b7a1438..0446688a38 100644 --- a/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx @@ -413,10 +413,26 @@ const DashboardLogsViewer: FunctionComponent = ( } try { + // When live polling, recompute the time range so the query window + // slides forward to "now" and new logs become visible. + let query: Query = filterOptions; + + if (skipLoadingState && isLiveEnabled && timeRange.range !== TimeRange.CUSTOM) { + const freshRange: InBetween = + RangeStartAndEndDateTimeUtil.getStartAndEndDate(timeRange); + query = { + ...filterOptions, + time: new InBetween( + freshRange.startValue, + freshRange.endValue, + ), + }; + } + const listResult: ListResult = await AnalyticsModelAPI.getList({ modelType: Log, - query: filterOptions, + query: query, limit: pageSize, skip: (page - 1) * pageSize, select: select, @@ -452,7 +468,7 @@ const DashboardLogsViewer: FunctionComponent = ( } } }, - [filterOptions, page, pageSize, select, sortField, sortOrder], + [filterOptions, isLiveEnabled, page, pageSize, select, sortField, sortOrder, timeRange], ); // --- Fetch histogram --- diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts index 2b500e5117..13cb238408 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -37,7 +37,7 @@ export default class KubernetesResourceUtils { namespaceAttribute = "resource.k8s.namespace.name", additionalAttributes = [], filterAttributes = {}, - hoursBack = 1, + hoursBack = 24, } = options; const endDate: Date = OneUptimeDate.getCurrentDate(); From c4903e5d1cab820ef6c1639ff70652410eeb0355 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 21:00:37 +0000 Subject: [PATCH 09/23] feat: Add IP whitelist API and configuration support --- Common/Server/API/IPWhitelistAPI.ts | 34 +++++++++++++++++++ Common/Server/API/Index.ts | 2 ++ Common/Server/EnvironmentConfig.ts | 2 ++ .../Public/oneuptime/templates/_helpers.tpl | 2 ++ HelmChart/Public/oneuptime/values.schema.json | 4 +++ HelmChart/Public/oneuptime/values.yaml | 6 ++++ 6 files changed, 50 insertions(+) create mode 100644 Common/Server/API/IPWhitelistAPI.ts diff --git a/Common/Server/API/IPWhitelistAPI.ts b/Common/Server/API/IPWhitelistAPI.ts new file mode 100644 index 0000000000..07bfb23a28 --- /dev/null +++ b/Common/Server/API/IPWhitelistAPI.ts @@ -0,0 +1,34 @@ +import Express, { + ExpressRequest, + ExpressResponse, + ExpressRouter, +} from "../Utils/Express"; +import Response from "../Utils/Response"; +import { IpWhitelist } from "../EnvironmentConfig"; + +export default class IPWhitelistAPI { + public static init(): ExpressRouter { + const router: ExpressRouter = Express.getRouter(); + + router.get( + "/ip-whitelist", + (req: ExpressRequest, res: ExpressResponse) => { + const ipList: Array = IpWhitelist + ? IpWhitelist.split(",") + .map((ip: string) => { + return ip.trim(); + }) + .filter((ip: string) => { + return ip.length > 0; + }) + : []; + + Response.sendJsonObjectResponse(req, res, { + ipWhitelist: ipList, + }); + }, + ); + + return router; + } +} diff --git a/Common/Server/API/Index.ts b/Common/Server/API/Index.ts index a4aba7cf9c..c135fd7fb9 100644 --- a/Common/Server/API/Index.ts +++ b/Common/Server/API/Index.ts @@ -1,5 +1,6 @@ import Express, { ExpressApplication } from "../Utils/Express"; import StatusAPI, { StatusAPIOptions } from "./StatusAPI"; +import IPWhitelistAPI from "./IPWhitelistAPI"; import version from "./VersionAPI"; const app: ExpressApplication = Express.getExpressApp(); @@ -14,6 +15,7 @@ type InitFunction = (data: InitOptions) => void; const init: InitFunction = (data: InitOptions): void => { app.use([`/${data.appName}`, "/"], version); app.use([`/${data.appName}`, "/"], StatusAPI.init(data.statusOptions)); + app.use([`/${data.appName}`, "/"], IPWhitelistAPI.init()); }; export default init; diff --git a/Common/Server/EnvironmentConfig.ts b/Common/Server/EnvironmentConfig.ts index c095ad9073..76c4947eb2 100644 --- a/Common/Server/EnvironmentConfig.ts +++ b/Common/Server/EnvironmentConfig.ts @@ -397,6 +397,8 @@ export const DocsClientUrl: URL = new URL( new Route(DocsRoute.toString()), ); +export const IpWhitelist: string = process.env["IP_WHITELIST"] || ""; + export const DisableTelemetry: boolean = process.env["DISABLE_TELEMETRY"] === "true"; diff --git a/HelmChart/Public/oneuptime/templates/_helpers.tpl b/HelmChart/Public/oneuptime/templates/_helpers.tpl index c7520000c9..53c146f1cc 100644 --- a/HelmChart/Public/oneuptime/templates/_helpers.tpl +++ b/HelmChart/Public/oneuptime/templates/_helpers.tpl @@ -121,6 +121,8 @@ Usage: value: {{ $.Values.home.ports.http | squote }} - name: WORKER_PORT value: {{ $.Values.worker.ports.http | squote }} +- name: IP_WHITELIST + value: {{ default "" $.Values.ipWhitelist | quote }} {{- end }} diff --git a/HelmChart/Public/oneuptime/values.schema.json b/HelmChart/Public/oneuptime/values.schema.json index 2951b58791..bd4a1ccbbc 100644 --- a/HelmChart/Public/oneuptime/values.schema.json +++ b/HelmChart/Public/oneuptime/values.schema.json @@ -41,6 +41,10 @@ "encryptionSecret": { "type": ["string", "null"] }, + "ipWhitelist": { + "type": ["string", "null"], + "description": "Comma-separated list of probe egress IP addresses for firewall whitelisting. Returned via the /ip-whitelist API endpoint." + }, "externalSecrets": { "type": "object", "properties": { diff --git a/HelmChart/Public/oneuptime/values.yaml b/HelmChart/Public/oneuptime/values.yaml index 6e49003bf5..3c635ddbac 100644 --- a/HelmChart/Public/oneuptime/values.yaml +++ b/HelmChart/Public/oneuptime/values.yaml @@ -35,6 +35,12 @@ oneuptimeSecret: registerProbeKey: encryptionSecret: +# Comma-separated list of egress IP addresses that probes use for monitoring checks. +# Customers can use this to whitelist probe traffic in their firewalls. +# This is returned as a JSON array via the /ip-whitelist API endpoint. +# Example: "203.0.113.1,203.0.113.2,198.51.100.10" +ipWhitelist: + # External Secrets # You need to leave blank oneuptimeSecret and encryptionSecret to use this section externalSecrets: From 27b94fdbaf0425e67db0a9655816275bbe99fd3e Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 21:10:16 +0000 Subject: [PATCH 10/23] docs: Update IP whitelist documentation with programmatic fetching instructions --- .../Content/configuration/ip-addresses.md | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/App/FeatureSet/Docs/Content/configuration/ip-addresses.md b/App/FeatureSet/Docs/Content/configuration/ip-addresses.md index 779a3d4f5e..ff60078107 100644 --- a/App/FeatureSet/Docs/Content/configuration/ip-addresses.md +++ b/App/FeatureSet/Docs/Content/configuration/ip-addresses.md @@ -5,6 +5,24 @@ If you are using OneUptime.com and want to whitelist our IP's for security reaso Please whitelist the following IP's in your firewall to allow oneuptime.com to reach your resources. - 172.174.206.132 -- 57.151.99.117 +- 57.151.99.117 -These IP's can change, we will let you know in advance if this happens. +These IP's can change, we will let you know in advance if this happens. + +## Fetch IP Addresses Programmatically + +You can also fetch the list of probe egress IP addresses programmatically via the following API endpoint: + +``` +GET https://oneuptime.com/ip-whitelist +``` + +This returns a JSON response: + +```json +{ + "ipWhitelist": ["172.174.206.132", "57.151.99.117"] +} +``` + +You can use this endpoint to keep your firewall whitelist updated automatically. From 68600335869a64f84f310dfb99e334dc073552c1 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 21:14:56 +0000 Subject: [PATCH 11/23] feat: Enhance KubernetesResourceTable with dynamic column rendering and integrate Table component --- .../Kubernetes/KubernetesResourceTable.tsx | 248 ++++++++++-------- .../Content/configuration/ip-addresses.md | 5 +- App/FeatureSet/Docs/Index.ts | 17 +- 3 files changed, 152 insertions(+), 118 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx index 6fe41ae31a..06f13926fd 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesResourceTable.tsx @@ -3,9 +3,14 @@ import KubernetesResourceUtils, { KubernetesResource, } from "../../Pages/Kubernetes/Utils/KubernetesResourceUtils"; import Card from "Common/UI/Components/Card/Card"; +import Table from "Common/UI/Components/Table/Table"; +import FieldType from "Common/UI/Components/Types/FieldType"; +import Link from "Common/UI/Components/Link/Link"; +import SortOrder from "Common/Types/BaseDatabase/SortOrder"; import Route from "Common/Types/API/Route"; +import Column from "Common/UI/Components/Table/Types/Column"; -export interface Column { +export interface ResourceColumn { title: string; key: string; getValue?: (resource: KubernetesResource) => string; @@ -15,7 +20,7 @@ export interface ComponentProps { resources: Array; title: string; description: string; - columns?: Array; + columns?: Array; showNamespace?: boolean; getViewRoute?: (resource: KubernetesResource) => Route; emptyMessage?: string; @@ -26,120 +31,135 @@ const KubernetesResourceTable: FunctionComponent = ( ): ReactElement => { const showNamespace: boolean = props.showNamespace !== false; + const tableColumns: Array> = [ + { + title: "Name", + type: FieldType.Element, + key: "name", + disableSort: true, + getElement: (resource: KubernetesResource): ReactElement => { + return ( + {resource.name} + ); + }, + }, + ]; + + if (showNamespace) { + tableColumns.push({ + title: "Namespace", + type: FieldType.Element, + key: "namespace", + disableSort: true, + getElement: (resource: KubernetesResource): ReactElement => { + return ( + + {resource.namespace || "default"} + + ); + }, + }); + } + + if (props.columns) { + for (const col of props.columns) { + tableColumns.push({ + title: col.title, + type: FieldType.Element, + key: col.key as keyof KubernetesResource, + disableSort: true, + getElement: (resource: KubernetesResource): ReactElement => { + const value: string = col.getValue + ? col.getValue(resource) + : resource.additionalAttributes[col.key] || ""; + return {value}; + }, + }); + } + } + + tableColumns.push( + { + title: "CPU", + type: FieldType.Element, + key: "cpuUtilization", + disableSort: true, + getElement: (resource: KubernetesResource): ReactElement => { + return ( + 80 + ? "bg-red-50 text-red-700" + : resource.cpuUtilization !== null && + resource.cpuUtilization > 60 + ? "bg-yellow-50 text-yellow-700" + : "bg-green-50 text-green-700" + }`} + > + {KubernetesResourceUtils.formatCpuValue(resource.cpuUtilization)} + + ); + }, + }, + { + title: "Memory", + type: FieldType.Element, + key: "memoryUsageBytes", + disableSort: true, + getElement: (resource: KubernetesResource): ReactElement => { + return ( + + {KubernetesResourceUtils.formatMemoryValue( + resource.memoryUsageBytes, + )} + + ); + }, + }, + ); + + if (props.getViewRoute) { + tableColumns.push({ + title: "Actions", + type: FieldType.Element, + key: "name", + disableSort: true, + getElement: (resource: KubernetesResource): ReactElement => { + return ( + + View + + ); + }, + }); + } + return ( - {props.resources.length === 0 ? ( -

- {props.emptyMessage || - "No resources found. Resources will appear here once the kubernetes-agent is sending data."} -

- ) : ( -
- - - - - {showNamespace && ( - - )} - {props.columns?.map((column: Column) => { - return ( - - ); - })} - - - {props.getViewRoute && ( - - )} - - - - {props.resources.map( - (resource: KubernetesResource, index: number) => { - return ( - - - {showNamespace && ( - - )} - {props.columns?.map((column: Column) => { - return ( - - ); - })} - - - {props.getViewRoute && ( - - )} - - ); - }, - )} - -
- Name - - Namespace - - {column.title} - - CPU - - Memory - - Actions -
- {resource.name} - - - {resource.namespace || "default"} - - - {column.getValue - ? column.getValue(resource) - : resource.additionalAttributes[column.key] || - ""} - - 80 - ? "bg-red-50 text-red-700" - : resource.cpuUtilization !== null && - resource.cpuUtilization > 60 - ? "bg-yellow-50 text-yellow-700" - : "bg-green-50 text-green-700" - }`} - > - {KubernetesResourceUtils.formatCpuValue( - resource.cpuUtilization, - )} - - - {KubernetesResourceUtils.formatMemoryValue( - resource.memoryUsageBytes, - )} - - - View - -
-
- )} + + id={`kubernetes-${props.title.toLowerCase().replace(/\s+/g, "-")}-table`} + columns={tableColumns} + data={props.resources} + singularLabel={props.title} + pluralLabel={props.title} + isLoading={false} + error="" + disablePagination={true} + currentPageNumber={1} + totalItemsCount={props.resources.length} + itemsOnPage={props.resources.length} + onNavigateToPage={() => {}} + sortBy={null} + sortOrder={SortOrder.Ascending} + onSortChanged={() => {}} + noItemsMessage={ + props.emptyMessage || + "No resources found. Resources will appear here once the kubernetes-agent is sending data." + } + />
); }; diff --git a/App/FeatureSet/Docs/Content/configuration/ip-addresses.md b/App/FeatureSet/Docs/Content/configuration/ip-addresses.md index ff60078107..d5dc83b6db 100644 --- a/App/FeatureSet/Docs/Content/configuration/ip-addresses.md +++ b/App/FeatureSet/Docs/Content/configuration/ip-addresses.md @@ -4,8 +4,7 @@ If you are using OneUptime.com and want to whitelist our IP's for security reaso Please whitelist the following IP's in your firewall to allow oneuptime.com to reach your resources. -- 172.174.206.132 -- 57.151.99.117 +{{IP_WHITELIST}} These IP's can change, we will let you know in advance if this happens. @@ -21,7 +20,7 @@ This returns a JSON response: ```json { - "ipWhitelist": ["172.174.206.132", "57.151.99.117"] + "ipWhitelist": [""] } ``` diff --git a/App/FeatureSet/Docs/Index.ts b/App/FeatureSet/Docs/Index.ts index ed559783bf..f95531a257 100755 --- a/App/FeatureSet/Docs/Index.ts +++ b/App/FeatureSet/Docs/Index.ts @@ -13,7 +13,7 @@ import Response from "Common/Server/Utils/Response"; import LocalFile from "Common/Server/Utils/LocalFile"; import logger from "Common/Server/Utils/Logger"; import "ejs"; -import { IsBillingEnabled } from "Common/Server/EnvironmentConfig"; +import { IsBillingEnabled, IpWhitelist } from "Common/Server/EnvironmentConfig"; const DocsFeatureSet: FeatureSet = { init: async (): Promise => { @@ -78,6 +78,21 @@ const DocsFeatureSet: FeatureSet = { // Remove first line (title) from content as it is already present in the navigation contentInMarkdown = contentInMarkdown.split("\n").slice(1).join("\n"); + // Replace dynamic placeholders in markdown content + if (contentInMarkdown.includes("{{IP_WHITELIST}}")) { + const ipList: string = IpWhitelist + ? IpWhitelist.split(",") + .map((ip: string) => { + return `- ${ip.trim()}`; + }) + .filter((line: string) => { + return line.length > 2; + }) + .join("\n") + : "- No IP addresses configured."; + contentInMarkdown = contentInMarkdown.replace("{{IP_WHITELIST}}", ipList); + } + // Render Markdown content to HTML const renderedContent: string = await DocsRender.render(contentInMarkdown); From 6eb7b980022de43f27d61d8711c4604fadacb621 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 21:21:41 +0000 Subject: [PATCH 12/23] feat: Implement session revocation on password reset and change --- App/FeatureSet/Identity/API/Authentication.ts | 5 ++++ Common/Server/Services/UserService.ts | 6 +++++ Common/Server/Services/UserSessionService.ts | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/App/FeatureSet/Identity/API/Authentication.ts b/App/FeatureSet/Identity/API/Authentication.ts index c098739032..6c61a49249 100644 --- a/App/FeatureSet/Identity/API/Authentication.ts +++ b/App/FeatureSet/Identity/API/Authentication.ts @@ -575,6 +575,11 @@ router.post( }, }); + // Revoke all active sessions for this user on password reset + await UserSessionService.revokeAllSessionsByUserId(alreadySavedUser.id!, { + reason: "Password reset", + }); + const host: Hostname = await DatabaseConfig.getHost(); const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); diff --git a/Common/Server/Services/UserService.ts b/Common/Server/Services/UserService.ts index d62afad5d4..8b744f0540 100755 --- a/Common/Server/Services/UserService.ts +++ b/Common/Server/Services/UserService.ts @@ -13,6 +13,7 @@ import MailService from "./MailService"; import TeamMemberService from "./TeamMemberService"; import UserNotificationRuleService from "./UserNotificationRuleService"; import UserNotificationSettingService from "./UserNotificationSettingService"; +import UserSessionService from "./UserSessionService"; import { AccountsRoute } from "../../ServiceRoute"; import Hostname from "../../Types/API/Hostname"; import Protocol from "../../Types/API/Protocol"; @@ -252,6 +253,11 @@ export class Service extends DatabaseService { const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol(); for (const user of onUpdate.carryForward) { + // Revoke all active sessions for this user on password change + await UserSessionService.revokeAllSessionsByUserId(user.id!, { + reason: "Password changed", + }); + // password changed, send password changed mail MailService.sendMail({ toEmail: user.email!, diff --git a/Common/Server/Services/UserSessionService.ts b/Common/Server/Services/UserSessionService.ts index d670bd37f4..8e4b473554 100644 --- a/Common/Server/Services/UserSessionService.ts +++ b/Common/Server/Services/UserSessionService.ts @@ -1,6 +1,7 @@ import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/UserSession"; import ObjectID from "../../Types/ObjectID"; +import LIMIT_MAX from "../../Types/Database/LimitMax"; import { JSONObject } from "../../Types/JSON"; import HashedString from "../../Types/HashedString"; import { EncryptionSecret } from "../EnvironmentConfig"; @@ -275,6 +276,28 @@ export class Service extends DatabaseService { await this.revokeSessionById(session.id, options); } + public async revokeAllSessionsByUserId( + userId: ObjectID, + options?: RevokeSessionOptions, + ): Promise { + await this.updateBy({ + query: { + userId: userId, + isRevoked: false, + }, + data: { + isRevoked: true, + revokedAt: OneUptimeDate.getCurrentDate(), + revokedReason: options?.reason ?? null, + }, + limit: LIMIT_MAX, + skip: 0, + props: { + isRoot: true, + }, + }); + } + private buildSessionModel( options: CreateSessionOptions, tokenMeta: { refreshToken: string; refreshTokenExpiresAt: Date }, From 7708d791b1bb168a18e5ec0cc6bde38f6e4bb767 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 21:27:46 +0000 Subject: [PATCH 13/23] fix: Update resource attribute keys for Kubernetes metrics --- .../Pages/Kubernetes/View/ContainerDetail.tsx | 10 +++++----- .../Pages/Kubernetes/View/CronJobDetail.tsx | 4 ++-- .../Pages/Kubernetes/View/DaemonSetDetail.tsx | 4 ++-- .../Kubernetes/View/DeploymentDetail.tsx | 4 ++-- .../src/Pages/Kubernetes/View/JobDetail.tsx | 4 ++-- .../Pages/Kubernetes/View/NamespaceDetail.tsx | 4 ++-- .../src/Pages/Kubernetes/View/NodeDetail.tsx | 20 +++++++++---------- .../src/Pages/Kubernetes/View/PodDetail.tsx | 18 ++++++++--------- .../Kubernetes/View/StatefulSetDetail.tsx | 4 ++-- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx index 7e7d66eba8..138880d51a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterContainerDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const name: string = - (attributes["k8s.container.name"] as string) || "Unknown Container"; + (attributes["resource.k8s.container.name"] as string) || "Unknown Container"; return { title: name }; }; @@ -99,8 +99,8 @@ const KubernetesClusterContainerDetail: FunctionComponent< filterData: { metricName: "container.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.container.name": containerName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.container.name": containerName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -124,8 +124,8 @@ const KubernetesClusterContainerDetail: FunctionComponent< filterData: { metricName: "container.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.container.name": containerName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.container.name": containerName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index 127baa2074..6b495e96a2 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -99,7 +99,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.cronjob.name": cronJobName, }, aggegationType: AggregationType.Avg, @@ -124,7 +124,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.cronjob.name": cronJobName, }, aggegationType: AggregationType.Avg, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index c29130f38c..f6463f76b8 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -99,7 +99,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.daemonset.name": daemonSetName, }, aggegationType: AggregationType.Avg, @@ -124,7 +124,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.daemonset.name": daemonSetName, }, aggegationType: AggregationType.Avg, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index f4f57ba3ba..ebf64ab18a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -99,7 +99,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.deployment.name": deploymentName, }, aggegationType: AggregationType.Avg, @@ -124,7 +124,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.deployment.name": deploymentName, }, aggegationType: AggregationType.Avg, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index 5056378511..fd410a5ca9 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -99,7 +99,7 @@ const KubernetesClusterJobDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.job.name": jobName, }, aggegationType: AggregationType.Avg, @@ -124,7 +124,7 @@ const KubernetesClusterJobDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.job.name": jobName, }, aggegationType: AggregationType.Avg, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index 91996587e4..5f51fe70fb 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -99,7 +99,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.namespace.name": namespaceName, }, aggegationType: AggregationType.Avg, @@ -124,7 +124,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.namespace.name": namespaceName, }, aggegationType: AggregationType.Avg, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx index 1618746cc8..8d9a9a86b7 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx @@ -86,8 +86,8 @@ const KubernetesClusterNodeDetail: FunctionComponent< filterData: { metricName: "k8s.node.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.node.name": nodeName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.node.name": nodeName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -110,8 +110,8 @@ const KubernetesClusterNodeDetail: FunctionComponent< filterData: { metricName: "k8s.node.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.node.name": nodeName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.node.name": nodeName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -134,8 +134,8 @@ const KubernetesClusterNodeDetail: FunctionComponent< filterData: { metricName: "k8s.node.filesystem.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.node.name": nodeName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.node.name": nodeName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -158,8 +158,8 @@ const KubernetesClusterNodeDetail: FunctionComponent< filterData: { metricName: "k8s.node.network.io.receive", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.node.name": nodeName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.node.name": nodeName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -182,8 +182,8 @@ const KubernetesClusterNodeDetail: FunctionComponent< filterData: { metricName: "k8s.node.network.io.transmit", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.node.name": nodeName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.node.name": nodeName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx index 21711434f2..6b314bde93 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx @@ -83,7 +83,7 @@ const KubernetesClusterPodDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const containerName: string = - (attributes["k8s.container.name"] as string) || "Unknown Container"; + (attributes["resource.k8s.container.name"] as string) || "Unknown Container"; return { title: containerName }; }; @@ -99,8 +99,8 @@ const KubernetesClusterPodDetail: FunctionComponent< filterData: { metricName: "container.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.pod.name": podName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.pod.name": podName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -124,8 +124,8 @@ const KubernetesClusterPodDetail: FunctionComponent< filterData: { metricName: "container.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.pod.name": podName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.pod.name": podName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -149,8 +149,8 @@ const KubernetesClusterPodDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.pod.name": podName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.pod.name": podName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, @@ -173,8 +173,8 @@ const KubernetesClusterPodDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, - "k8s.pod.name": podName, + "resource.k8s.cluster.name": clusterIdentifier, + "resource.k8s.pod.name": podName, }, aggegationType: AggregationType.Avg, aggregateBy: {}, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index 8ec7ed562e..bdac9884eb 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -99,7 +99,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< filterData: { metricName: "k8s.pod.cpu.utilization", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.statefulset.name": statefulSetName, }, aggegationType: AggregationType.Avg, @@ -124,7 +124,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< filterData: { metricName: "k8s.pod.memory.usage", attributes: { - "k8s.cluster.name": clusterIdentifier, + "resource.k8s.cluster.name": clusterIdentifier, "resource.k8s.statefulset.name": statefulSetName, }, aggegationType: AggregationType.Avg, From 10d0237747a52a9f24a0f4572029ff8f2b1b21a8 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 21:48:10 +0000 Subject: [PATCH 14/23] feat: Add metric view data initialization and update query configurations for Kubernetes detail views --- .../Pages/Kubernetes/View/ContainerDetail.tsx | 25 +++++++------ .../Pages/Kubernetes/View/CronJobDetail.tsx | 25 +++++++------ .../Pages/Kubernetes/View/DaemonSetDetail.tsx | 25 +++++++------ .../Kubernetes/View/DeploymentDetail.tsx | 25 +++++++------ .../src/Pages/Kubernetes/View/JobDetail.tsx | 25 +++++++------ .../Pages/Kubernetes/View/NamespaceDetail.tsx | 25 +++++++------ .../src/Pages/Kubernetes/View/NodeDetail.tsx | 37 ++++++++++--------- .../src/Pages/Kubernetes/View/PodDetail.tsx | 30 +++++++++------ .../Kubernetes/View/StatefulSetDetail.tsx | 25 +++++++------ .../Dashboard/src/Routes/KubernetesRoutes.tsx | 9 +++++ 10 files changed, 146 insertions(+), 105 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx index 138880d51a..865a91f496 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterContainerDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterContainerDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterContainerDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterContainerDetail: FunctionComponent< description="CPU and memory usage for this container over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index 6b495e96a2..623a3ba5d9 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterCronJobDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterCronJobDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterCronJobDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterCronJobDetail: FunctionComponent< description="CPU and memory usage for pods in this cronjob over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index f6463f76b8..7c2a344d70 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< description="CPU and memory usage for pods in this daemonset over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index ebf64ab18a..54c5c6d6ab 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< description="CPU and memory usage for pods in this deployment over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index fd410a5ca9..2ce5d73fe5 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterJobDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterJobDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterJobDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterJobDetail: FunctionComponent< description="CPU and memory usage for pods in this job over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index 5f51fe70fb..d6f4043bd0 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< description="CPU and memory usage for pods in this namespace over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx index 8d9a9a86b7..0234597b0f 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx @@ -33,6 +33,16 @@ const KubernetesClusterNodeDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -70,10 +80,6 @@ const KubernetesClusterNodeDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const cpuQuery: MetricQueryConfigData = { metricAliasData: { metricVariable: "node_cpu", @@ -194,18 +200,6 @@ const KubernetesClusterNodeDetail: FunctionComponent< }, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [ - cpuQuery, - memoryQuery, - filesystemQuery, - networkRxQuery, - networkTxQuery, - ], - formulaConfigs: [], - }); - return (
@@ -218,7 +212,16 @@ const KubernetesClusterNodeDetail: FunctionComponent< description="CPU, memory, filesystem, and network usage for this node over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx index 6b314bde93..a1b32ef459 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterPodDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterPodDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getContainerSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -185,12 +191,6 @@ const KubernetesClusterPodDetail: FunctionComponent< }, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -203,7 +203,15 @@ const KubernetesClusterPodDetail: FunctionComponent< description="CPU, memory, and container-level resource usage for this pod over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index bdac9884eb..39a780ca68 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -36,6 +36,16 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -73,10 +83,6 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< const clusterIdentifier: string = cluster.clusterIdentifier || ""; - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - const getSeries: (data: AggregateModel) => ChartSeries = ( data: AggregateModel, ): ChartSeries => { @@ -137,12 +143,6 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< getSeries: getSeries, }; - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - return (
@@ -155,7 +155,10 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< description="CPU and memory usage for pods in this statefulset over the last 6 hours." > { setMetricViewData({ diff --git a/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx b/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx index 515dc8aab1..f2cb6ac6a9 100644 --- a/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx +++ b/App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx @@ -93,6 +93,7 @@ const KubernetesRoutes: FunctionComponent = ( = ( = ( = ( = ( = ( = ( = ( = ( Date: Wed, 18 Mar 2026 21:54:14 +0000 Subject: [PATCH 15/23] feat: Enhance Kubernetes configuration with kubeletstats receiver and update daemonset tolerations --- .../templates/configmap-daemonset.yaml | 43 ++++++++++++++++++- .../templates/configmap-deployment.yaml | 16 ------- .../kubernetes-agent/templates/daemonset.yaml | 2 + 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml index d00626b001..a87cff9018 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml @@ -13,6 +13,21 @@ data: endpoint: "0.0.0.0:13133" receivers: + # Collect node, pod, and container resource metrics from kubelet + kubeletstats: + collection_interval: {{ .Values.collectionInterval }} + auth_type: serviceAccount + endpoint: "https://${env:NODE_NAME}:10250" + insecure_skip_verify: true + metric_groups: + - node + - pod + - container + extra_metadata_labels: + - container.id + k8s_api_config: + auth_type: serviceAccount + # Collect pod logs from /var/log/pods filelog: include: @@ -95,11 +110,25 @@ data: - k8s.replicaset.name - k8s.statefulset.name - k8s.daemonset.name + - k8s.job.name + - k8s.cronjob.name - k8s.container.name + labels: + - tag_name: k8s.pod.label.app + key: app + from: pod + - tag_name: k8s.pod.label.app.kubernetes.io/name + key: app.kubernetes.io/name + from: pod pod_association: + - sources: + - from: resource_attribute + name: k8s.pod.ip - sources: - from: resource_attribute name: k8s.pod.uid + - sources: + - from: connection # Stamp with cluster name resource: @@ -114,8 +143,8 @@ data: memory_limiter: check_interval: 5s - limit_mib: 200 - spike_limit_mib: 50 + limit_mib: 400 + spike_limit_mib: 100 exporters: otlphttp: @@ -127,6 +156,16 @@ data: extensions: - health_check pipelines: + metrics: + receivers: + - kubeletstats + processors: + - memory_limiter + - k8sattributes + - resource + - batch + exporters: + - otlphttp logs: receivers: - filelog diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml index a28e092cb4..54beb0f2e7 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml @@ -12,21 +12,6 @@ data: endpoint: "0.0.0.0:13133" receivers: - # Collect node, pod, and container resource metrics from kubelet - kubeletstats: - collection_interval: {{ .Values.collectionInterval }} - auth_type: serviceAccount - endpoint: "https://${env:NODE_NAME}:10250" - insecure_skip_verify: true - metric_groups: - - node - - pod - - container - extra_metadata_labels: - - container.id - k8s_api_config: - auth_type: serviceAccount - # Collect cluster-level metrics from the Kubernetes API k8s_cluster: collection_interval: {{ .Values.collectionInterval }} @@ -159,7 +144,6 @@ data: pipelines: metrics: receivers: - - kubeletstats - k8s_cluster {{- if .Values.controlPlane.enabled }} - prometheus diff --git a/HelmChart/Public/kubernetes-agent/templates/daemonset.yaml b/HelmChart/Public/kubernetes-agent/templates/daemonset.yaml index c2cfcb8036..e6f6d8bcb6 100644 --- a/HelmChart/Public/kubernetes-agent/templates/daemonset.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/daemonset.yaml @@ -21,6 +21,8 @@ spec: checksum/config: {{ include (print $.Template.BasePath "/configmap-daemonset.yaml") . | sha256sum }} spec: serviceAccountName: {{ include "kubernetes-agent.serviceAccountName" . }} + tolerations: + - operator: Exists containers: - name: otel-collector image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" From 50d5514fea35a4368ea041e830fcc147fa744122 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 22:09:07 +0000 Subject: [PATCH 16/23] chore: clean up empty code change sections in the changes log --- .claude/launch.json | 18 - Home/Routes.ts | 14 + Home/Utils/PageSEO.ts | 35 + Home/Views/Partials/icons/kubernetes.ejs | 5 + Home/Views/kubernetes.ejs | 846 +++++++++++++++++++++++ Home/Views/nav.ejs | 19 + 6 files changed, 919 insertions(+), 18 deletions(-) delete mode 100644 .claude/launch.json create mode 100644 Home/Views/Partials/icons/kubernetes.ejs create mode 100644 Home/Views/kubernetes.ejs diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index e49adee269..0000000000 --- a/.claude/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "mobile-app", - "runtimeExecutable": "bash", - "runtimeArgs": ["-c", "cd MobileApp && npx expo start --port 8081"], - "port": 8081 - }, - { - "name": "dashboard", - "runtimeExecutable": "bash", - "runtimeArgs": ["-c", "cd App/FeatureSet/Dashboard && npm run dev"], - "port": 3002, - "autoPort": false - } - ] -} diff --git a/Home/Routes.ts b/Home/Routes.ts index cff91f0543..7b62a4df69 100755 --- a/Home/Routes.ts +++ b/Home/Routes.ts @@ -1208,6 +1208,20 @@ const HomeFeatureSet: FeatureSet = { }, ); + app.get( + "/product/kubernetes", + (_req: ExpressRequest, res: ExpressResponse) => { + const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath( + "/product/kubernetes", + res.locals["homeUrl"] as string, + ); + res.render(`${ViewsPath}/kubernetes`, { + enableGoogleTagManager: IsBillingEnabled, + seo, + }); + }, + ); + app.get("/product/traces", (_req: ExpressRequest, res: ExpressResponse) => { const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath( "/product/traces", diff --git a/Home/Utils/PageSEO.ts b/Home/Utils/PageSEO.ts index 53766621a6..d303f864f7 100644 --- a/Home/Utils/PageSEO.ts +++ b/Home/Utils/PageSEO.ts @@ -403,6 +403,41 @@ export const PageSEOConfig: Record = { }, }, + "/product/kubernetes": { + title: + "Kubernetes Observability | Monitor Clusters, Pods & Nodes | OneUptime", + description: + "Complete Kubernetes observability with real-time cluster monitoring, pod health tracking, node metrics, and automated alerting. OpenTelemetry native. Open source.", + canonicalPath: "/product/kubernetes", + twitterCard: "summary_large_image", + pageType: "product", + breadcrumbs: [ + { name: "Home", url: "/" }, + { name: "Products", url: "/#products" }, + { name: "Kubernetes", url: "/product/kubernetes" }, + ], + softwareApplication: { + name: "OneUptime Kubernetes Observability", + applicationCategory: "DeveloperApplication", + operatingSystem: "Web, Cloud", + description: + "Monitor Kubernetes clusters, nodes, pods, and containers with real-time metrics, intelligent alerting, and pre-built dashboards.", + features: [ + "Multi-cluster monitoring", + "Node health and metrics", + "Pod and container monitoring", + "CrashLoopBackOff detection", + "OOMKill alerting", + "Resource utilization tracking", + "Namespace-level breakdowns", + "OpenTelemetry native", + "DaemonSet deployment", + "Kubelet stats receiver", + "Logs and traces correlation", + ], + }, + }, + "/product/traces": { title: "Distributed Tracing | End-to-End Request Tracing | OneUptime", description: diff --git a/Home/Views/Partials/icons/kubernetes.ejs b/Home/Views/Partials/icons/kubernetes.ejs new file mode 100644 index 0000000000..ea51114870 --- /dev/null +++ b/Home/Views/Partials/icons/kubernetes.ejs @@ -0,0 +1,5 @@ + + + + + diff --git a/Home/Views/kubernetes.ejs b/Home/Views/kubernetes.ejs new file mode 100644 index 0000000000..6e87322c3d --- /dev/null +++ b/Home/Views/kubernetes.ejs @@ -0,0 +1,846 @@ + + + + + + + OneUptime | Kubernetes Observability - Monitor Clusters, Pods & Nodes + + <%- include('head', { + enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false +}) -%> + + + + <%- include('nav') -%> + +
+ +
+ +
+ + +
+ +
+
+
+ +

OpenTelemetry Native Kubernetes Monitoring

+ +

+ Full visibility into your Kubernetes clusters +

+

+ Monitor clusters, nodes, pods, and containers in real-time. Detect issues before they impact your workloads with intelligent alerting and dashboards. +

+ + + + +
+ Cluster health + + Pod monitoring + + Node metrics + + Container insights +
+
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+ Kubernetes Cluster Overview +
+
+ production-cluster + + + Healthy + +
+
+ + +
+ +
+
+
Nodes
+
12
+
All healthy
+
+
+
Pods
+
147
+
143 running
+
+
+
Deployments
+
23
+
All available
+
+
+
CPU Usage
+
62%
+
avg across nodes
+
+
+ + +
+ +
+
+ production + 48 pods +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ staging + 32 pods +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ monitoring + 18 pods +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + <%- include('logo-roll') -%> + + +
+
+
+

How It Works

+

+ Start monitoring Kubernetes in minutes +

+

+ Deploy our lightweight collector and get full visibility into your clusters instantly. +

+
+ +
+ + + +
+ +
+
+ + + + 1 +
+

Deploy Collector

+

Install the OpenTelemetry Collector as a DaemonSet in your cluster with a single Helm command.

+
+ + +
+
+ + + + + 2 +
+

Auto-discover

+

Automatically discover all nodes, pods, containers, and services in your cluster.

+
+ + +
+
+ + + + 3 +
+

View Dashboards

+

Get pre-built dashboards for cluster, node, pod, and container-level metrics.

+
+ + +
+
+ + + + 4 +
+

Set Alerts

+

Configure alerts for pod crashes, node pressure, resource limits, and more.

+
+
+
+
+
+ + +
+ +
+ +
+
+ + + + Why OneUptime for Kubernetes +
+

+ Kubernetes observability built for production +

+

+ From cluster health to individual container metrics, OneUptime provides deep visibility into every layer of your Kubernetes infrastructure. +

+
+ + +
+ + +
+
+
+
+ + + +
+ Cluster Health +
+

Complete cluster visibility at a glance

+

See the health of every cluster, namespace, and workload in a single pane of glass. Instantly know when nodes are under pressure or pods are failing.

+
    +
  • + + + + Multi-cluster monitoring +
  • +
  • + + + + Namespace-level breakdowns +
  • +
  • + + + + Real-time resource utilization +
  • +
+ + Get started + + + + +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ Cluster Health +
+
+
+ +
+
+
+ + + +
+
+
prod-us-east
+
12 nodes, 147 pods
+
+
+ Healthy +
+ +
+
+
+ + + +
+
+
prod-eu-west
+
8 nodes, 89 pods
+
+
+ Warning +
+ +
+
+
+ + + +
+
+
staging-us
+
4 nodes, 42 pods
+
+
+ Healthy +
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ Pod Details +
+
+
+ +
+
+
api-server-7d4f8b6c5-x2k9p
+
namespace: production
+
+ + + Running + +
+ + +
+
+
+ CPU Usage + 245m / 500m +
+
+
+
+
+
+
+ Memory Usage + 312Mi / 512Mi +
+
+
+
+
+
+
+ Network I/O + 2.4 MB/s +
+
+ RX: 1.8 MB/s + TX: 0.6 MB/s +
+
+
+ + +
+ Restarts + 0 +
+
+
+
+
+
+
+
+ + + +
+ Pod & Container Monitoring +
+

Deep visibility into every pod and container

+

Track CPU, memory, network, and disk usage for every container. See resource requests vs limits, restart counts, and OOMKill events in real-time.

+
    +
  • + + + + Container-level CPU & memory metrics +
  • +
  • + + + + OOMKill and CrashLoopBackOff detection +
  • +
  • + + + + Resource requests vs limits tracking +
  • +
+ + Get started + + + + +
+
+ + +
+
+
+
+ + + +
+ Node Monitoring +
+

Monitor every node in your cluster

+

Track node-level CPU, memory, disk, and network metrics. Get alerted on node pressure conditions, disk space issues, and scheduling problems.

+
    +
  • + + + + CPU, memory, disk & network metrics +
  • +
  • + + + + Node condition alerts (MemoryPressure, DiskPressure) +
  • +
  • + + + + Kubelet and system component health +
  • +
+ + Get started + + + + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ Node Metrics +
+
+
+ +
+
+ + node-01 +
+
+ CPU 45% + MEM 62% + DISK 38% +
+
+
+
+ + node-02 +
+
+ CPU 67% + MEM 71% + DISK 42% +
+
+
+
+ + node-03 +
+
+ CPU 89% + MEM 78% + DISK 55% +
+
+
+
+ + node-04 +
+
+ CPU 32% + MEM 48% + DISK 29% +
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+
+ Active Alerts +
+
+
+
+
+ CRITICAL + 2 min ago +
+
Pod CrashLoopBackOff
+
payment-service-6d8f9 in production
+
+
+
+ WARNING + 15 min ago +
+
Node Memory Pressure
+
node-03 memory at 89%
+
+
+
+ INFO + 1 hr ago +
+
Deployment Scaled Up
+
api-server scaled from 3 to 5 replicas
+
+
+
+
+
+
+
+
+ + + +
+ Smart Alerting +
+

Kubernetes-aware alerting

+

Get alerts that understand Kubernetes semantics. Detect CrashLoopBackOff, OOMKill, pending pods, failed deployments, and node pressure conditions automatically.

+
    +
  • + + + + Pod crash and restart alerts +
  • +
  • + + + + Resource quota and limit alerts +
  • +
  • + + + + Multi-channel notifications (Slack, PagerDuty, Email) +
  • +
+ + Get started + + + + +
+
+ +
+
+
+ + +
+ +
+ +
+
+ + + + Advanced capabilities +
+

+ Everything you need for Kubernetes observability +

+

+ Built for production Kubernetes environments with enterprise-grade features. +

+
+ + +
+
+ + +
+
+
+ + + +
+

OpenTelemetry Native

+

Built on OpenTelemetry from the ground up. Use the standard K8s receivers and the kubeletstats receiver for comprehensive metrics.

+
+
+ + +
+
+
+ + + +
+

Multi-Cluster Support

+

Monitor all your clusters from a single dashboard. Compare resource usage across environments and regions.

+
+
+ + +
+
+
+ + + +
+

Logs & Traces Correlation

+

Jump from a pod metric spike to its container logs and distributed traces. Full correlation across all telemetry signals.

+
+
+ + +
+
+
+ + + +
+

DaemonSet Deployment

+

Deploy as a DaemonSet with tolerations for all nodes including control plane. One Helm command to get started.

+
+
+ + +
+
+
+ + + +
+

Resource Right-sizing

+

See actual resource usage vs requests and limits. Identify over-provisioned and under-provisioned workloads to optimize costs.

+
+
+ + +
+
+
+ + + +
+

Enterprise Compliance

+

SOC 2/3, ISO, GDPR, HIPAA compliant. Data residency options. Self-hosted or cloud deployment.

+
+
+ +
+
+
+
+ + <%- include('./Partials/enterprise-ready') -%> + <%- include('features-table') -%> + <%- include('cta') -%> +
+ + <%- include('footer') -%> + <%- include('./Partials/video-script') -%> + + + + + + diff --git a/Home/Views/nav.ejs b/Home/Views/nav.ejs index 39a660ad49..0572b88a9a 100644 --- a/Home/Views/nav.ejs +++ b/Home/Views/nav.ejs @@ -161,6 +161,17 @@

Error tracking & debugging

+ + + +
+ <%- include('./Partials/icons/kubernetes', {iconClass: 'h-4 w-4'}) %> +
+
+

Kubernetes

+

Cluster & pod observability

+
+
@@ -796,6 +807,14 @@ Exceptions + + +
+ <%- include('./Partials/icons/kubernetes', {iconClass: 'h-4 w-4'}) %> +
+ Kubernetes +
+
From 139aa83fe40ffcc28092d8497378b48144ee3884 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 22:21:10 +0000 Subject: [PATCH 17/23] feat: Revamp Kubernetes overview page with enhanced UI components and updated resource metrics display --- Home/Views/kubernetes.ejs | 413 ++++++++++++++++++++++++++++++-------- 1 file changed, 332 insertions(+), 81 deletions(-) diff --git a/Home/Views/kubernetes.ejs b/Home/Views/kubernetes.ejs index 6e87322c3d..d2616e4899 100644 --- a/Home/Views/kubernetes.ejs +++ b/Home/Views/kubernetes.ejs @@ -73,106 +73,357 @@
- Kubernetes Cluster Overview +
+ + + + Kubernetes Cluster Overview +
-
- production-cluster - - - Healthy +
+
+ + + + Last 1 hour +
+ + + prod-us-east-1
-
- -
-
-
Nodes
-
12
-
All healthy
+
+ +
+
+
+
Nodes
+
+ + + +
+
+
12
+
+ + All healthy +
-
-
Pods
-
147
-
143 running
+
+
+
Pods
+
+ + + +
+
+
147
+
+ 143 running + 3 pending + 1 failed +
-
-
Deployments
-
23
-
All available
+
+
+
Deployments
+
+ + + +
+
+
23
+
+ + All available +
-
-
CPU Usage
-
62%
-
avg across nodes
+
+
+
CPU
+
+ + + +
+
+
62%
+
+
+
+
+
+
+
Memory
+
+ + + +
+
+
71%
+
+
+
- -
- -
-
- production - 48 pods + +
+ +
+
+
+
Cluster Resource Usage
+
CPU and Memory over time
+
+
+
+ + CPU +
+
+ + Memory +
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + 75% + 50% + 25% + +
+ + +
+
Pod Status
+
Across all namespaces
+ +
+
+ + + + + + +
+ 97% + healthy +
+
+
+
+
+
+ + Running +
+ 143 +
+
+
+ + Pending +
+ 3 +
+
+
+ + Failed +
+ 1 +
- -
-
- staging - 32 pods +
+ + +
+ +
+
+ Namespaces + 6 total
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ + production +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 48 pods +
+
+
+
+ + staging +
+
+
+
+
+
+
+
+
+
+
+
+ 32 pods +
+
+
+
+ + monitoring +
+
+
+
+
+
+
+
+
+ 18 pods +
+
+
+
+ + kube-system +
+
+
+
+
+
+
+
+ 14 pods +
+
- -
-
- monitoring - 18 pods + + +
+
+ Node Health + 12 nodes
-
-
-
-
-
-
-
-
-
+
+
+
+ + node-01 +
+
+
+ CPU +
+
+
+ MEM +
+
+
+
+
+
+ + node-02 +
+
+
+ CPU +
+
+
+ MEM +
+
+
+
+
+
+ + node-03 +
+
+
+ CPU +
+
+
+ MEM +
+
+
+
+
+
+ + node-04 +
+
+
+ CPU +
+
+
+ MEM +
+
+
+
From dc3db1ec4721518879e6ded4987a657db2cf119b Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 19 Mar 2026 08:15:35 +0000 Subject: [PATCH 18/23] Refactor code structure for improved readability and maintainability --- .../Kubernetes/KubernetesContainersTab.tsx | 257 ++++ .../Kubernetes/KubernetesEventsTab.tsx | 127 ++ .../Kubernetes/KubernetesLogsTab.tsx | 118 ++ .../Kubernetes/KubernetesMetricsTab.tsx | 47 + .../Kubernetes/KubernetesOverviewTab.tsx | 168 +++ .../Utils/KubernetesObjectFetcher.ts | 410 +++++ .../Utils/KubernetesObjectParser.ts | 1329 +++++++++++++++++ .../Pages/Kubernetes/View/ContainerDetail.tsx | 88 +- .../Pages/Kubernetes/View/CronJobDetail.tsx | 166 +- .../Pages/Kubernetes/View/DaemonSetDetail.tsx | 160 +- .../Kubernetes/View/DeploymentDetail.tsx | 157 +- .../src/Pages/Kubernetes/View/Events.tsx | 164 +- .../src/Pages/Kubernetes/View/JobDetail.tsx | 169 ++- .../Pages/Kubernetes/View/NamespaceDetail.tsx | 137 +- .../src/Pages/Kubernetes/View/NodeDetail.tsx | 214 ++- .../src/Pages/Kubernetes/View/PodDetail.tsx | 208 ++- .../Kubernetes/View/StatefulSetDetail.tsx | 163 +- .../templates/configmap-deployment.yaml | 32 + HelmChart/Public/kubernetes-agent/values.yaml | 7 + Home/Routes.ts | 21 + Home/Utils/PageSEO.ts | 37 + Home/Utils/Sitemap.ts | 1 + .../hero-cards/scheduled-maintenance.ejs | 10 + Home/Views/Partials/icons/kubernetes.ejs | 7 +- .../Partials/icons/scheduled-maintenance.ejs | 4 + Home/Views/kubernetes.ejs | 200 +++ Home/Views/nav.ejs | 19 + Home/Views/scheduled-maintenance.ejs | 704 +++++++++ 28 files changed, 4671 insertions(+), 453 deletions(-) create mode 100644 App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx create mode 100644 App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx create mode 100644 App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx create mode 100644 App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx create mode 100644 App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts create mode 100644 App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts create mode 100644 Home/Views/Partials/hero-cards/scheduled-maintenance.ejs create mode 100644 Home/Views/Partials/icons/scheduled-maintenance.ejs create mode 100644 Home/Views/scheduled-maintenance.ejs diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx new file mode 100644 index 0000000000..222da2021f --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx @@ -0,0 +1,257 @@ +import React, { FunctionComponent, ReactElement, useState } from "react"; +import Card from "Common/UI/Components/Card/Card"; +import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer"; +import { + KubernetesContainerSpec, + KubernetesContainerStatus, +} from "../../Pages/Kubernetes/Utils/KubernetesObjectParser"; + +export interface ComponentProps { + containers: Array; + initContainers: Array; + containerStatuses?: Array | undefined; + initContainerStatuses?: Array | undefined; +} + +interface ContainerCardProps { + container: KubernetesContainerSpec; + status?: KubernetesContainerStatus | undefined; + isInit: boolean; +} + +const ContainerCard: FunctionComponent = ( + props: ContainerCardProps, +): ReactElement => { + const [showEnv, setShowEnv] = useState(false); + const [showMounts, setShowMounts] = useState(false); + + const envRecord: Record = {}; + for (const env of props.container.env) { + envRecord[env.name] = env.value; + } + + const hasResources: boolean = + Object.keys(props.container.resources.requests).length > 0 || + Object.keys(props.container.resources.limits).length > 0; + + return ( + +
+ {/* Status */} + {props.status && ( +
+
+ State:{" "} + + {props.status.state} + +
+
+ Ready:{" "} + + {props.status.ready ? "Yes" : "No"} + +
+
+ Restarts:{" "} + 0 + ? "text-yellow-700" + : "text-gray-700" + } + > + {props.status.restartCount} + +
+
+ )} + + {/* Command & Args */} + {props.container.command.length > 0 && ( +
+ Command:{" "} + + {props.container.command.join(" ")} + +
+ )} + {props.container.args.length > 0 && ( +
+ Args:{" "} + + {props.container.args.join(" ")} + +
+ )} + + {/* Ports */} + {props.container.ports.length > 0 && ( +
+ Ports:{" "} + {props.container.ports.map((port, idx) => ( + + {port.name ? `${port.name}: ` : ""} + {port.containerPort}/{port.protocol} + + ))} +
+ )} + + {/* Resources */} + {hasResources && ( +
+ {Object.keys(props.container.resources.requests).length > 0 && ( +
+ + Requests: + + +
+ )} + {Object.keys(props.container.resources.limits).length > 0 && ( +
+ + Limits: + + +
+ )} +
+ )} + + {/* Environment Variables (expandable) */} + {props.container.env.length > 0 && ( +
+ + {showEnv && ( +
+ +
+ )} +
+ )} + + {/* Volume Mounts (expandable) */} + {props.container.volumeMounts.length > 0 && ( +
+ + {showMounts && ( +
+ {props.container.volumeMounts.map((mount, idx) => ( +
+ + {mount.name} + + + + {mount.mountPath} + + {mount.readOnly && ( + (read-only) + )} +
+ ))} +
+ )} +
+ )} +
+
+ ); +}; + +const KubernetesContainersTab: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + if ( + props.containers.length === 0 && + props.initContainers.length === 0 + ) { + return ( +
+ No container information available. +
+ ); + } + + const getStatus: ( + name: string, + isInit: boolean, + ) => KubernetesContainerStatus | undefined = ( + name: string, + isInit: boolean, + ): KubernetesContainerStatus | undefined => { + const statuses: Array | undefined = isInit + ? props.initContainerStatuses + : props.containerStatuses; + return statuses?.find( + (s: KubernetesContainerStatus) => s.name === name, + ); + }; + + return ( +
+ {props.initContainers.map( + (container: KubernetesContainerSpec, index: number) => ( + + ), + )} + {props.containers.map( + (container: KubernetesContainerSpec, index: number) => ( + + ), + )} +
+ ); +}; + +export default KubernetesContainersTab; diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx new file mode 100644 index 0000000000..22d6c3f139 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx @@ -0,0 +1,127 @@ +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import { + fetchK8sEventsForResource, + KubernetesEvent, +} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; + +export interface ComponentProps { + clusterIdentifier: string; + resourceKind: string; // "Pod", "Node", "Deployment", etc. + resourceName: string; + namespace?: string | undefined; +} + +const KubernetesEventsTab: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const [events, setEvents] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchEvents: () => Promise = async (): Promise => { + setIsLoading(true); + try { + const result: Array = + await fetchK8sEventsForResource({ + clusterIdentifier: props.clusterIdentifier, + resourceKind: props.resourceKind, + resourceName: props.resourceName, + namespace: props.namespace, + }); + setEvents(result); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to fetch events", + ); + } + setIsLoading(false); + }; + + fetchEvents().catch(() => {}); + }, [ + props.clusterIdentifier, + props.resourceKind, + props.resourceName, + props.namespace, + ]); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (events.length === 0) { + return ( +
+ No events found for this {props.resourceKind.toLowerCase()} in the last + 24 hours. +
+ ); + } + + return ( +
+ + + + + + + + + + + {events.map((event: KubernetesEvent, index: number) => { + const isWarning: boolean = + event.type.toLowerCase() === "warning"; + return ( + + + + + + + ); + })} + +
+ Time + + Type + + Reason + + Message +
+ {event.timestamp} + + + {event.type} + + + {event.reason} + + {event.message} +
+
+ ); +}; + +export default KubernetesEventsTab; diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx new file mode 100644 index 0000000000..965790884e --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx @@ -0,0 +1,118 @@ +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import { + fetchPodLogs, + KubernetesLogEntry, +} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; + +export interface ComponentProps { + clusterIdentifier: string; + podName: string; + containerName?: string | undefined; + namespace?: string | undefined; +} + +const KubernetesLogsTab: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const [logs, setLogs] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + const fetchLogs: () => Promise = async (): Promise => { + setIsLoading(true); + try { + const result: Array = await fetchPodLogs({ + clusterIdentifier: props.clusterIdentifier, + podName: props.podName, + containerName: props.containerName, + namespace: props.namespace, + limit: 500, + }); + setLogs(result); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to fetch logs", + ); + } + setIsLoading(false); + }; + + fetchLogs().catch(() => {}); + }, [ + props.clusterIdentifier, + props.podName, + props.containerName, + props.namespace, + ]); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (logs.length === 0) { + return ( +
+ No application logs found for this pod in the last 6 hours. Logs will + appear here once the kubernetes-agent's filelog receiver is collecting + data. +
+ ); + } + + const getSeverityColor: (severity: string) => string = ( + severity: string, + ): string => { + const s: string = severity.toUpperCase(); + if (s === "ERROR" || s === "FATAL" || s === "CRITICAL") { + return "text-red-600"; + } + if (s === "WARN" || s === "WARNING") { + return "text-yellow-600"; + } + if (s === "DEBUG" || s === "TRACE") { + return "text-gray-400"; + } + return "text-gray-700"; + }; + + return ( +
+ {logs.map((log: KubernetesLogEntry, index: number) => { + return ( +
+ + {log.timestamp} + + {log.containerName && ( + + [{log.containerName}] + + )} + + {log.severity} + + + {log.body} + +
+ ); + })} +
+ ); +}; + +export default KubernetesLogsTab; diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx new file mode 100644 index 0000000000..5e84c12272 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx @@ -0,0 +1,47 @@ +import React, { + FunctionComponent, + ReactElement, + useState, +} from "react"; +import MetricView from "../../Components/Metrics/MetricView"; +import MetricViewData from "Common/Types/Metrics/MetricViewData"; +import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData"; +import OneUptimeDate from "Common/Types/Date"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; + +export interface ComponentProps { + queryConfigs: Array; +} + +const KubernetesMetricsTab: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startAndEndDate: InBetween = new InBetween(startDate, endDate); + + const [metricViewData, setMetricViewData] = useState({ + startAndEndDate: startAndEndDate, + queryConfigs: [], + formulaConfigs: [], + }); + + return ( + { + setMetricViewData({ + ...data, + queryConfigs: props.queryConfigs, + formulaConfigs: [], + }); + }} + /> + ); +}; + +export default KubernetesMetricsTab; diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx new file mode 100644 index 0000000000..a0f310b20c --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx @@ -0,0 +1,168 @@ +import React, { FunctionComponent, ReactElement } from "react"; +import Card from "Common/UI/Components/Card/Card"; +import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; +import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer"; +import { KubernetesCondition } from "../../Pages/Kubernetes/Utils/KubernetesObjectParser"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; + +export interface SummaryField { + title: string; + value: string | ReactElement; +} + +export interface ComponentProps { + summaryFields: Array; + labels: Record; + annotations: Record; + conditions?: Array | undefined; + ownerReferences?: Array<{ kind: string; name: string }> | undefined; + isLoading: boolean; + emptyMessage?: string | undefined; +} + +const KubernetesOverviewTab: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + if (props.isLoading) { + return ; + } + + if ( + props.summaryFields.length === 0 && + Object.keys(props.labels).length === 0 + ) { + return ( +
+ {props.emptyMessage || + "Resource details not yet available. Ensure the kubernetes-agent Helm chart has resourceSpecs.enabled set to true and wait for the next data pull (up to 5 minutes)."} +
+ ); + } + + return ( +
+ {/* Summary Info Cards */} + {props.summaryFields.length > 0 && ( +
+ {props.summaryFields.map( + (field: SummaryField, index: number) => { + return ( + + ); + }, + )} +
+ )} + + {/* Owner References */} + {props.ownerReferences && props.ownerReferences.length > 0 && ( + +
+ {props.ownerReferences.map( + (ref: { kind: string; name: string }, index: number) => { + return ( +
+ + {ref.kind}: + {" "} + {ref.name} +
+ ); + }, + )} +
+
+ )} + + {/* Conditions */} + {props.conditions && props.conditions.length > 0 && ( + +
+ + + + + + + + + + + + {props.conditions.map( + (condition: KubernetesCondition, index: number) => { + return ( + + + + + + + + ); + }, + )} + +
+ Type + + Status + + Reason + + Message + + Last Transition +
+ {condition.type} + + + {condition.status} + + + {condition.reason || "-"} + + {condition.message || "-"} + + {condition.lastTransitionTime || "-"} +
+
+
+ )} + + {/* Labels */} + {Object.keys(props.labels).length > 0 && ( + + + + )} + + {/* Annotations */} + {Object.keys(props.annotations).length > 0 && ( + + + + )} +
+ ); +}; + +export default KubernetesOverviewTab; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts new file mode 100644 index 0000000000..e76d0494cc --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts @@ -0,0 +1,410 @@ +import Log from "Common/Models/AnalyticsModels/Log"; +import AnalyticsModelAPI, { + ListResult, +} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI"; +import ProjectUtil from "Common/UI/Utils/Project"; +import OneUptimeDate from "Common/Types/Date"; +import SortOrder from "Common/Types/BaseDatabase/SortOrder"; +import InBetween from "Common/Types/BaseDatabase/InBetween"; +import { JSONObject } from "Common/Types/JSON"; +import { + extractObjectFromLogBody, + getKvStringValue, + getKvValue, + KubernetesPodObject, + KubernetesNodeObject, + KubernetesDeploymentObject, + KubernetesStatefulSetObject, + KubernetesDaemonSetObject, + KubernetesJobObject, + KubernetesCronJobObject, + KubernetesNamespaceObject, + parsePodObject, + parseNodeObject, + parseDeploymentObject, + parseStatefulSetObject, + parseDaemonSetObject, + parseJobObject, + parseCronJobObject, + parseNamespaceObject, +} from "./KubernetesObjectParser"; + +export type KubernetesObjectType = + | KubernetesPodObject + | KubernetesNodeObject + | KubernetesDeploymentObject + | KubernetesStatefulSetObject + | KubernetesDaemonSetObject + | KubernetesJobObject + | KubernetesCronJobObject + | KubernetesNamespaceObject; + +export interface FetchK8sObjectOptions { + clusterIdentifier: string; + resourceType: string; // "pods", "nodes", "deployments", etc. + resourceName: string; + namespace?: string | undefined; // Not needed for cluster-scoped resources (nodes, namespaces) +} + +type ParserFunction = (kvList: JSONObject) => KubernetesObjectType | null; + +function getParser(resourceType: string): ParserFunction | null { + const parsers: Record = { + pods: parsePodObject, + nodes: parseNodeObject, + deployments: parseDeploymentObject, + statefulsets: parseStatefulSetObject, + daemonsets: parseDaemonSetObject, + jobs: parseJobObject, + cronjobs: parseCronJobObject, + namespaces: parseNamespaceObject, + }; + return parsers[resourceType] || null; +} + +/** + * Fetch the latest K8s resource object from the Log table. + * The k8sobjects pull mode stores full K8s API objects as log entries. + */ +export async function fetchLatestK8sObject( + options: FetchK8sObjectOptions, +): Promise { + const parser: ParserFunction | null = getParser(options.resourceType); + if (!parser) { + return null; + } + + const projectId: string | undefined = + ProjectUtil.getCurrentProjectId()?.toString(); + if (!projectId) { + return null; + } + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24); + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const queryOptions: any = { + modelType: Log, + query: { + projectId: projectId, + time: new InBetween(startDate, endDate), + attributes: { + "logAttributes.event.domain": "k8s", + "logAttributes.k8s.resource.name": options.resourceType, + }, + }, + limit: 500, // Get enough logs to find the resource + skip: 0, + select: { + time: true, + body: true, + attributes: true, + }, + sort: { + time: SortOrder.Descending, + }, + requestOptions: {}, + }; + const listResult: ListResult = + await AnalyticsModelAPI.getList(queryOptions); + + // Parse each log body and find the matching resource + for (const log of listResult.data) { + const attrs: JSONObject = log.attributes || {}; + + // Filter to this cluster + if ( + attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier && + attrs["k8s.cluster.name"] !== options.clusterIdentifier + ) { + continue; + } + + if (typeof log.body !== "string") { + continue; + } + + const objectKvList: JSONObject | null = extractObjectFromLogBody( + log.body, + ); + if (!objectKvList) { + continue; + } + + // Check if this is the resource we're looking for + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + continue; + } + + const name: string = getKvStringValue(metadataKv, "name"); + const namespace: string = getKvStringValue(metadataKv, "namespace"); + + if (name !== options.resourceName) { + continue; + } + + // For namespaced resources, also match namespace + if (options.namespace && namespace && namespace !== options.namespace) { + continue; + } + + const parsed: KubernetesObjectType | null = parser(objectKvList); + if (parsed) { + return parsed as T; + } + } + + return null; + } catch { + return null; + } +} + +/** + * Fetch K8s events related to a specific resource. + */ +export interface KubernetesEvent { + timestamp: string; + type: string; + reason: string; + objectKind: string; + objectName: string; + namespace: string; + message: string; +} + +export async function fetchK8sEventsForResource(options: { + clusterIdentifier: string; + resourceKind: string; // "Pod", "Node", "Deployment", etc. + resourceName: string; + namespace?: string | undefined; +}): Promise> { + const projectId: string | undefined = + ProjectUtil.getCurrentProjectId()?.toString(); + if (!projectId) { + return []; + } + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24); + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const eventsQueryOptions: any = { + modelType: Log, + query: { + projectId: projectId, + time: new InBetween(startDate, endDate), + attributes: { + "logAttributes.event.domain": "k8s", + "logAttributes.k8s.resource.name": "events", + }, + }, + limit: 500, + skip: 0, + select: { + time: true, + body: true, + attributes: true, + }, + sort: { + time: SortOrder.Descending, + }, + requestOptions: {}, + }; + const listResult: ListResult = + await AnalyticsModelAPI.getList(eventsQueryOptions); + + const events: Array = []; + + for (const log of listResult.data) { + const attrs: JSONObject = log.attributes || {}; + + // Filter to this cluster + if ( + attrs["resource.k8s.cluster.name"] !== options.clusterIdentifier && + attrs["k8s.cluster.name"] !== options.clusterIdentifier + ) { + continue; + } + + if (typeof log.body !== "string") { + continue; + } + + let bodyObj: JSONObject | null = null; + try { + bodyObj = JSON.parse(log.body) as JSONObject; + } catch { + continue; + } + + const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as + | JSONObject + | undefined; + if (!topKvList) { + continue; + } + + // Get the "object" which is the actual k8s Event + const objectVal: string | JSONObject | null = getKvValue( + topKvList, + "object", + ); + if (!objectVal || typeof objectVal === "string") { + continue; + } + const objectKvList: JSONObject = objectVal; + + // Get event details + const eventType: string = + getKvStringValue(objectKvList, "type") || ""; + const reason: string = getKvStringValue(objectKvList, "reason") || ""; + const note: string = getKvStringValue(objectKvList, "note") || ""; + + // Get regarding object + const regardingKind: string = + getKvStringValue( + getKvValue(objectKvList, "regarding") as JSONObject | undefined, + "kind", + ) || ""; + const regardingName: string = + getKvStringValue( + getKvValue(objectKvList, "regarding") as JSONObject | undefined, + "name", + ) || ""; + const regardingNamespace: string = + getKvStringValue( + getKvValue(objectKvList, "regarding") as JSONObject | undefined, + "namespace", + ) || ""; + + // Filter to events for this specific resource + if ( + regardingKind.toLowerCase() !== options.resourceKind.toLowerCase() || + regardingName !== options.resourceName + ) { + continue; + } + + if ( + options.namespace && + regardingNamespace && + regardingNamespace !== options.namespace + ) { + continue; + } + + events.push({ + timestamp: log.time + ? OneUptimeDate.getDateAsLocalFormattedString(log.time) + : "", + type: eventType || "Unknown", + reason: reason || "Unknown", + objectKind: regardingKind || "Unknown", + objectName: regardingName || "Unknown", + namespace: regardingNamespace || "default", + message: note || "", + }); + } + + return events; + } catch { + return []; + } +} + +/** + * Fetch application logs for a pod/container from the Log table. + * These come from the filelog receiver (not k8sobjects). + */ +export interface KubernetesLogEntry { + timestamp: string; + body: string; + severity: string; + containerName: string; +} + +export async function fetchPodLogs(options: { + clusterIdentifier: string; + podName: string; + containerName?: string | undefined; + namespace?: string | undefined; + limit?: number | undefined; +}): Promise> { + const projectId: string | undefined = + ProjectUtil.getCurrentProjectId()?.toString(); + if (!projectId) { + return []; + } + + const endDate: Date = OneUptimeDate.getCurrentDate(); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + + // Build attribute filters for filelog data + const attributeFilters: Record = { + "resource.k8s.cluster.name": options.clusterIdentifier, + "resource.k8s.pod.name": options.podName, + }; + + if (options.containerName) { + attributeFilters["resource.k8s.container.name"] = options.containerName; + } + + if (options.namespace) { + attributeFilters["resource.k8s.namespace.name"] = options.namespace; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const logsQueryOptions: any = { + modelType: Log, + query: { + projectId: projectId, + time: new InBetween(startDate, endDate), + attributes: attributeFilters, + }, + limit: options.limit || 200, + skip: 0, + select: { + time: true, + body: true, + severityText: true, + attributes: true, + }, + sort: { + time: SortOrder.Descending, + }, + requestOptions: {}, + }; + const listResult: ListResult = + await AnalyticsModelAPI.getList(logsQueryOptions); + + return listResult.data + .filter((log: Log) => { + // Exclude k8s event logs — only application logs + const attrs: JSONObject = log.attributes || {}; + return attrs["logAttributes.event.domain"] !== "k8s"; + }) + .map((log: Log) => { + const attrs: JSONObject = log.attributes || {}; + return { + timestamp: log.time + ? OneUptimeDate.getDateAsLocalFormattedString(log.time) + : "", + body: typeof log.body === "string" ? log.body : "", + severity: log.severityText || "INFO", + containerName: + (attrs["resource.k8s.container.name"] as string) || "", + }; + }); + } catch { + return []; + } +} diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts new file mode 100644 index 0000000000..30e885237e --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts @@ -0,0 +1,1329 @@ +import { JSONObject } from "Common/Types/JSON"; + +// ============================================================ +// OTLP kvlistValue parsing helpers +// ============================================================ + +/** + * Extract a value from an OTLP kvlistValue by key. + * Returns the string/int value or nested kvlistValue as JSONObject. + */ +export function getKvValue( + kvList: JSONObject | undefined, + key: string, +): string | JSONObject | null { + if (!kvList) { + return null; + } + const values: Array | undefined = kvList["values"] as + | Array + | undefined; + if (!values) { + return null; + } + for (const entry of values) { + if (entry["key"] === key) { + const val: JSONObject | undefined = entry["value"] as + | JSONObject + | undefined; + if (!val) { + return null; + } + if (val["stringValue"] !== undefined) { + return val["stringValue"] as string; + } + if (val["intValue"] !== undefined) { + return String(val["intValue"]); + } + if (val["boolValue"] !== undefined) { + return String(val["boolValue"]); + } + if (val["kvlistValue"]) { + return val["kvlistValue"] as JSONObject; + } + if (val["arrayValue"]) { + return val["arrayValue"] as JSONObject; + } + return null; + } + } + return null; +} + +/** + * Extract a string value from an OTLP kvlistValue by key. + */ +export function getKvStringValue( + kvList: JSONObject | undefined, + key: string, +): string { + const val: string | JSONObject | null = getKvValue(kvList, key); + if (typeof val === "string") { + return val; + } + return ""; +} + +/** + * Extract a nested kvlist value (parent → child). + */ +export function getNestedKvValue( + kvList: JSONObject | undefined, + parentKey: string, + childKey: string, +): string { + const parent: string | JSONObject | null = getKvValue(kvList, parentKey); + if (!parent || typeof parent === "string") { + return ""; + } + return getKvStringValue(parent, childKey); +} + +/** + * Convert a kvlistValue to a flat Record (for labels, annotations, env). + */ +export function getKvListAsRecord( + kvList: JSONObject | undefined, +): Record { + const result: Record = {}; + if (!kvList) { + return result; + } + const values: Array | undefined = kvList["values"] as + | Array + | undefined; + if (!values) { + return result; + } + for (const entry of values) { + const key: string = (entry["key"] as string) || ""; + const val: JSONObject | undefined = entry["value"] as + | JSONObject + | undefined; + if (key && val) { + if (val["stringValue"] !== undefined) { + result[key] = val["stringValue"] as string; + } else if (val["intValue"] !== undefined) { + result[key] = String(val["intValue"]); + } else if (val["boolValue"] !== undefined) { + result[key] = String(val["boolValue"]); + } + } + } + return result; +} + +/** + * Convert an OTLP arrayValue to an array of JSONObjects (kvlistValues). + */ +export function getArrayValues( + arrayValue: JSONObject | undefined, +): Array { + if (!arrayValue) { + return []; + } + const values: Array | undefined = arrayValue["values"] as + | Array + | undefined; + if (!values) { + return []; + } + return values + .map((item: JSONObject) => { + if (item["kvlistValue"]) { + return item["kvlistValue"] as JSONObject; + } + if (item["stringValue"]) { + return item as JSONObject; + } + return null; + }) + .filter(Boolean) as Array; +} + +// ============================================================ +// TypeScript interfaces for parsed K8s objects +// ============================================================ + +export interface KubernetesObjectMetadata { + name: string; + namespace: string; + uid: string; + creationTimestamp: string; + labels: Record; + annotations: Record; + ownerReferences: Array<{ + kind: string; + name: string; + uid: string; + }>; +} + +export interface KubernetesContainerEnvVar { + name: string; + value: string; // Direct value, or description like "" +} + +export interface KubernetesContainerPort { + name: string; + containerPort: number; + protocol: string; +} + +export interface KubernetesContainerSpec { + name: string; + image: string; + command: Array; + args: Array; + env: Array; + ports: Array; + resources: { + requests: Record; + limits: Record; + }; + volumeMounts: Array<{ + name: string; + mountPath: string; + readOnly: boolean; + }>; +} + +export interface KubernetesCondition { + type: string; + status: string; + reason: string; + message: string; + lastTransitionTime: string; +} + +export interface KubernetesContainerStatus { + name: string; + ready: boolean; + restartCount: number; + state: string; + image: string; +} + +export interface KubernetesPodObject { + metadata: KubernetesObjectMetadata; + spec: { + containers: Array; + initContainers: Array; + serviceAccountName: string; + nodeName: string; + nodeSelector: Record; + tolerations: Array<{ + key: string; + operator: string; + value: string; + effect: string; + }>; + volumes: Array<{ + name: string; + type: string; + source: string; + }>; + }; + status: { + phase: string; + podIP: string; + hostIP: string; + conditions: Array; + containerStatuses: Array; + initContainerStatuses: Array; + }; +} + +export interface KubernetesNodeObject { + metadata: KubernetesObjectMetadata; + status: { + conditions: Array; + nodeInfo: { + osImage: string; + kernelVersion: string; + containerRuntimeVersion: string; + kubeletVersion: string; + architecture: string; + operatingSystem: string; + }; + allocatable: Record; + capacity: Record; + addresses: Array<{ type: string; address: string }>; + }; +} + +export interface KubernetesDeploymentObject { + metadata: KubernetesObjectMetadata; + spec: { + replicas: number; + strategy: string; + selector: Record; + }; + status: { + replicas: number; + readyReplicas: number; + availableReplicas: number; + unavailableReplicas: number; + conditions: Array; + }; +} + +export interface KubernetesStatefulSetObject { + metadata: KubernetesObjectMetadata; + spec: { + replicas: number; + serviceName: string; + podManagementPolicy: string; + updateStrategy: string; + }; + status: { + replicas: number; + readyReplicas: number; + currentReplicas: number; + }; +} + +export interface KubernetesDaemonSetObject { + metadata: KubernetesObjectMetadata; + spec: { + updateStrategy: string; + }; + status: { + desiredNumberScheduled: number; + currentNumberScheduled: number; + numberReady: number; + numberMisscheduled: number; + numberAvailable: number; + }; +} + +export interface KubernetesJobObject { + metadata: KubernetesObjectMetadata; + spec: { + completions: number; + parallelism: number; + backoffLimit: number; + }; + status: { + active: number; + succeeded: number; + failed: number; + startTime: string; + completionTime: string; + conditions: Array; + }; +} + +export interface KubernetesCronJobObject { + metadata: KubernetesObjectMetadata; + spec: { + schedule: string; + suspend: boolean; + concurrencyPolicy: string; + successfulJobsHistoryLimit: number; + failedJobsHistoryLimit: number; + }; + status: { + lastScheduleTime: string; + activeCount: number; + }; +} + +export interface KubernetesNamespaceObject { + metadata: KubernetesObjectMetadata; + status: { + phase: string; + }; +} + +// ============================================================ +// Parsers +// ============================================================ + +function parseMetadata(kvList: JSONObject): KubernetesObjectMetadata { + const labelsKvList: string | JSONObject | null = getKvValue( + kvList, + "labels", + ); + const annotationsKvList: string | JSONObject | null = getKvValue( + kvList, + "annotations", + ); + const ownerRefsArrayValue: string | JSONObject | null = getKvValue( + kvList, + "ownerReferences", + ); + + const ownerReferences: Array<{ + kind: string; + name: string; + uid: string; + }> = []; + if (ownerRefsArrayValue && typeof ownerRefsArrayValue !== "string") { + const refs: Array = getArrayValues(ownerRefsArrayValue); + for (const ref of refs) { + ownerReferences.push({ + kind: getKvStringValue(ref, "kind"), + name: getKvStringValue(ref, "name"), + uid: getKvStringValue(ref, "uid"), + }); + } + } + + return { + name: getKvStringValue(kvList, "name"), + namespace: getKvStringValue(kvList, "namespace"), + uid: getKvStringValue(kvList, "uid"), + creationTimestamp: getKvStringValue(kvList, "creationTimestamp"), + labels: + labelsKvList && typeof labelsKvList !== "string" + ? getKvListAsRecord(labelsKvList) + : {}, + annotations: + annotationsKvList && typeof annotationsKvList !== "string" + ? getKvListAsRecord(annotationsKvList) + : {}, + ownerReferences, + }; +} + +function parseContainerEnv( + envArrayValue: JSONObject | null, +): Array { + if (!envArrayValue || typeof envArrayValue === "string") { + return []; + } + const envItems: Array = getArrayValues(envArrayValue); + const result: Array = []; + + for (const envKvList of envItems) { + const name: string = getKvStringValue(envKvList, "name"); + const directValue: string = getKvStringValue(envKvList, "value"); + + if (directValue) { + result.push({ name, value: directValue }); + } else { + // Check for valueFrom (secretKeyRef, configMapKeyRef, fieldRef) + const valueFrom: string | JSONObject | null = getKvValue( + envKvList, + "valueFrom", + ); + if (valueFrom && typeof valueFrom !== "string") { + const secretRef: string | JSONObject | null = getKvValue( + valueFrom, + "secretKeyRef", + ); + const configMapRef: string | JSONObject | null = getKvValue( + valueFrom, + "configMapKeyRef", + ); + const fieldRef: string | JSONObject | null = getKvValue( + valueFrom, + "fieldRef", + ); + + if (secretRef && typeof secretRef !== "string") { + const secretName: string = getKvStringValue(secretRef, "name"); + const secretKey: string = getKvStringValue(secretRef, "key"); + result.push({ + name, + value: ``, + }); + } else if (configMapRef && typeof configMapRef !== "string") { + const cmName: string = getKvStringValue(configMapRef, "name"); + const cmKey: string = getKvStringValue(configMapRef, "key"); + result.push({ + name, + value: ``, + }); + } else if (fieldRef && typeof fieldRef !== "string") { + const fieldPath: string = getKvStringValue(fieldRef, "fieldPath"); + result.push({ name, value: `` }); + } else { + result.push({ name, value: "" }); + } + } else { + result.push({ name, value: directValue || "" }); + } + } + } + return result; +} + +function parseContainerSpec(kvList: JSONObject): KubernetesContainerSpec { + const portsArrayValue: string | JSONObject | null = getKvValue( + kvList, + "ports", + ); + const ports: Array = []; + if (portsArrayValue && typeof portsArrayValue !== "string") { + const portItems: Array = getArrayValues(portsArrayValue); + for (const portKv of portItems) { + ports.push({ + name: getKvStringValue(portKv, "name"), + containerPort: parseInt(getKvStringValue(portKv, "containerPort")) || 0, + protocol: getKvStringValue(portKv, "protocol") || "TCP", + }); + } + } + + const envArrayValue: string | JSONObject | null = getKvValue(kvList, "env"); + const env: Array = parseContainerEnv( + envArrayValue as JSONObject | null, + ); + + const volumeMountsArray: string | JSONObject | null = getKvValue( + kvList, + "volumeMounts", + ); + const volumeMounts: Array<{ + name: string; + mountPath: string; + readOnly: boolean; + }> = []; + if (volumeMountsArray && typeof volumeMountsArray !== "string") { + const mountItems: Array = getArrayValues(volumeMountsArray); + for (const mountKv of mountItems) { + volumeMounts.push({ + name: getKvStringValue(mountKv, "name"), + mountPath: getKvStringValue(mountKv, "mountPath"), + readOnly: getKvStringValue(mountKv, "readOnly") === "true", + }); + } + } + + const resourcesKv: string | JSONObject | null = getKvValue( + kvList, + "resources", + ); + let requests: Record = {}; + let limits: Record = {}; + if (resourcesKv && typeof resourcesKv !== "string") { + const reqKv: string | JSONObject | null = getKvValue( + resourcesKv, + "requests", + ); + const limKv: string | JSONObject | null = getKvValue( + resourcesKv, + "limits", + ); + if (reqKv && typeof reqKv !== "string") { + requests = getKvListAsRecord(reqKv); + } + if (limKv && typeof limKv !== "string") { + limits = getKvListAsRecord(limKv); + } + } + + const commandArray: string | JSONObject | null = getKvValue( + kvList, + "command", + ); + const command: Array = []; + if (commandArray && typeof commandArray !== "string") { + const cmdValues: Array = (commandArray["values"] as Array) || []; + for (const v of cmdValues) { + if (v["stringValue"]) { + command.push(v["stringValue"] as string); + } + } + } + + const argsArray: string | JSONObject | null = getKvValue(kvList, "args"); + const args: Array = []; + if (argsArray && typeof argsArray !== "string") { + const argValues: Array = (argsArray["values"] as Array) || []; + for (const v of argValues) { + if (v["stringValue"]) { + args.push(v["stringValue"] as string); + } + } + } + + return { + name: getKvStringValue(kvList, "name"), + image: getKvStringValue(kvList, "image"), + command, + args, + env, + ports, + resources: { requests, limits }, + volumeMounts, + }; +} + +function parseConditions( + conditionsArrayValue: JSONObject | null, +): Array { + if (!conditionsArrayValue) { + return []; + } + const items: Array = getArrayValues(conditionsArrayValue); + return items.map((kvList: JSONObject) => { + return { + type: getKvStringValue(kvList, "type"), + status: getKvStringValue(kvList, "status"), + reason: getKvStringValue(kvList, "reason"), + message: getKvStringValue(kvList, "message"), + lastTransitionTime: getKvStringValue(kvList, "lastTransitionTime"), + }; + }); +} + +function parseContainerStatuses( + statusesArrayValue: JSONObject | null, +): Array { + if (!statusesArrayValue) { + return []; + } + const items: Array = getArrayValues(statusesArrayValue); + return items.map((kvList: JSONObject) => { + // state is a kvlist with one key (running/waiting/terminated) + const stateKv: string | JSONObject | null = getKvValue(kvList, "state"); + let state: string = "Unknown"; + if (stateKv && typeof stateKv !== "string") { + const stateValues: Array | undefined = stateKv["values"] as + | Array + | undefined; + if (stateValues && stateValues.length > 0 && stateValues[0]) { + state = (stateValues[0]["key"] as string) || "Unknown"; + } + } + + return { + name: getKvStringValue(kvList, "name"), + ready: getKvStringValue(kvList, "ready") === "true", + restartCount: + parseInt(getKvStringValue(kvList, "restartCount")) || 0, + state, + image: getKvStringValue(kvList, "image"), + }; + }); +} + +/** + * Parse a raw OTLP log body into a Pod object. + * The body format: { kvlistValue: { values: [{ key: "type", value: ... }, { key: "object", value: { kvlistValue: ... } }] } } + */ +export function parsePodObject( + objectKvList: JSONObject, +): KubernetesPodObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + const metadata: KubernetesObjectMetadata = parseMetadata(metadataKv); + + const specKv: string | JSONObject | null = getKvValue( + objectKvList, + "spec", + ); + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + // Parse containers + const containers: Array = []; + const initContainers: Array = []; + + if (specKv && typeof specKv !== "string") { + const containersArray: string | JSONObject | null = getKvValue( + specKv, + "containers", + ); + if (containersArray && typeof containersArray !== "string") { + const containerItems: Array = + getArrayValues(containersArray); + for (const cKv of containerItems) { + containers.push(parseContainerSpec(cKv)); + } + } + + const initContainersArray: string | JSONObject | null = getKvValue( + specKv, + "initContainers", + ); + if (initContainersArray && typeof initContainersArray !== "string") { + const initItems: Array = + getArrayValues(initContainersArray); + for (const cKv of initItems) { + initContainers.push(parseContainerSpec(cKv)); + } + } + } + + // Parse volumes + const volumes: Array<{ name: string; type: string; source: string }> = []; + if (specKv && typeof specKv !== "string") { + const volumesArray: string | JSONObject | null = getKvValue( + specKv, + "volumes", + ); + if (volumesArray && typeof volumesArray !== "string") { + const volItems: Array = getArrayValues(volumesArray); + for (const volKv of volItems) { + const name: string = getKvStringValue(volKv, "name"); + // Volume type is one of: configMap, secret, emptyDir, hostPath, persistentVolumeClaim, etc. + const volValues: Array | undefined = volKv["values"] as + | Array + | undefined; + let volType: string = "unknown"; + let volSource: string = ""; + if (volValues) { + for (const v of volValues) { + const k: string = (v["key"] as string) || ""; + if (k !== "name") { + volType = k; + const innerVal: JSONObject | undefined = v["value"] as + | JSONObject + | undefined; + if (innerVal && innerVal["kvlistValue"]) { + const innerKv: JSONObject = + innerVal["kvlistValue"] as JSONObject; + volSource = + getKvStringValue(innerKv, "name") || + getKvStringValue(innerKv, "path") || + getKvStringValue(innerKv, "claimName") || + volType; + } + break; + } + } + } + volumes.push({ name, type: volType, source: volSource }); + } + } + } + + // Parse tolerations + const tolerations: Array<{ + key: string; + operator: string; + value: string; + effect: string; + }> = []; + if (specKv && typeof specKv !== "string") { + const tolArray: string | JSONObject | null = getKvValue( + specKv, + "tolerations", + ); + if (tolArray && typeof tolArray !== "string") { + const tolItems: Array = getArrayValues(tolArray); + for (const tolKv of tolItems) { + tolerations.push({ + key: getKvStringValue(tolKv, "key"), + operator: getKvStringValue(tolKv, "operator"), + value: getKvStringValue(tolKv, "value"), + effect: getKvStringValue(tolKv, "effect"), + }); + } + } + } + + // Parse nodeSelector + let nodeSelector: Record = {}; + if (specKv && typeof specKv !== "string") { + const nsKv: string | JSONObject | null = getKvValue( + specKv, + "nodeSelector", + ); + if (nsKv && typeof nsKv !== "string") { + nodeSelector = getKvListAsRecord(nsKv); + } + } + + // Parse status + let phase: string = ""; + let podIP: string = ""; + let hostIP: string = ""; + let conditions: Array = []; + let containerStatuses: Array = []; + let initContainerStatuses: Array = []; + + if (statusKv && typeof statusKv !== "string") { + phase = getKvStringValue(statusKv, "phase"); + podIP = getKvStringValue(statusKv, "podIP"); + hostIP = getKvStringValue(statusKv, "hostIP"); + + const condArray: string | JSONObject | null = getKvValue( + statusKv, + "conditions", + ); + conditions = parseConditions(condArray as JSONObject | null); + + const csArray: string | JSONObject | null = getKvValue( + statusKv, + "containerStatuses", + ); + containerStatuses = parseContainerStatuses(csArray as JSONObject | null); + + const icsArray: string | JSONObject | null = getKvValue( + statusKv, + "initContainerStatuses", + ); + initContainerStatuses = parseContainerStatuses( + icsArray as JSONObject | null, + ); + } + + return { + metadata, + spec: { + containers, + initContainers, + serviceAccountName: specKv + ? getKvStringValue(specKv as JSONObject, "serviceAccountName") + : "", + nodeName: specKv + ? getKvStringValue(specKv as JSONObject, "nodeName") + : "", + nodeSelector, + tolerations, + volumes, + }, + status: { + phase, + podIP, + hostIP, + conditions, + containerStatuses, + initContainerStatuses, + }, + }; + } catch { + return null; + } +} + +export function parseNodeObject( + objectKvList: JSONObject, +): KubernetesNodeObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + let conditions: Array = []; + let nodeInfo: KubernetesNodeObject["status"]["nodeInfo"] = { + osImage: "", + kernelVersion: "", + containerRuntimeVersion: "", + kubeletVersion: "", + architecture: "", + operatingSystem: "", + }; + let allocatable: Record = {}; + let capacity: Record = {}; + let addresses: Array<{ type: string; address: string }> = []; + + if (statusKv && typeof statusKv !== "string") { + const condArray: string | JSONObject | null = getKvValue( + statusKv, + "conditions", + ); + conditions = parseConditions(condArray as JSONObject | null); + + const nodeInfoKv: string | JSONObject | null = getKvValue( + statusKv, + "nodeInfo", + ); + if (nodeInfoKv && typeof nodeInfoKv !== "string") { + nodeInfo = { + osImage: getKvStringValue(nodeInfoKv, "osImage"), + kernelVersion: getKvStringValue(nodeInfoKv, "kernelVersion"), + containerRuntimeVersion: getKvStringValue( + nodeInfoKv, + "containerRuntimeVersion", + ), + kubeletVersion: getKvStringValue(nodeInfoKv, "kubeletVersion"), + architecture: getKvStringValue(nodeInfoKv, "architecture"), + operatingSystem: getKvStringValue(nodeInfoKv, "operatingSystem"), + }; + } + + const allocKv: string | JSONObject | null = getKvValue( + statusKv, + "allocatable", + ); + if (allocKv && typeof allocKv !== "string") { + allocatable = getKvListAsRecord(allocKv); + } + + const capKv: string | JSONObject | null = getKvValue( + statusKv, + "capacity", + ); + if (capKv && typeof capKv !== "string") { + capacity = getKvListAsRecord(capKv); + } + + const addrArray: string | JSONObject | null = getKvValue( + statusKv, + "addresses", + ); + if (addrArray && typeof addrArray !== "string") { + const addrItems: Array = getArrayValues(addrArray); + addresses = addrItems.map((a: JSONObject) => ({ + type: getKvStringValue(a, "type"), + address: getKvStringValue(a, "address"), + })); + } + } + + return { + metadata: parseMetadata(metadataKv), + status: { + conditions, + nodeInfo, + allocatable, + capacity, + addresses, + }, + }; + } catch { + return null; + } +} + +export function parseDeploymentObject( + objectKvList: JSONObject, +): KubernetesDeploymentObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const specKv: string | JSONObject | null = getKvValue( + objectKvList, + "spec", + ); + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + let replicas: number = 0; + let strategy: string = ""; + let selector: Record = {}; + if (specKv && typeof specKv !== "string") { + replicas = parseInt(getKvStringValue(specKv, "replicas")) || 0; + const strategyKv: string | JSONObject | null = getKvValue( + specKv, + "strategy", + ); + if (strategyKv && typeof strategyKv !== "string") { + strategy = getKvStringValue(strategyKv, "type"); + } + const selectorKv: string | JSONObject | null = getKvValue( + specKv, + "selector", + ); + if (selectorKv && typeof selectorKv !== "string") { + const matchLabelsKv: string | JSONObject | null = getKvValue( + selectorKv, + "matchLabels", + ); + if (matchLabelsKv && typeof matchLabelsKv !== "string") { + selector = getKvListAsRecord(matchLabelsKv); + } + } + } + + let statusReplicas: number = 0; + let readyReplicas: number = 0; + let availableReplicas: number = 0; + let unavailableReplicas: number = 0; + let conditions: Array = []; + if (statusKv && typeof statusKv !== "string") { + statusReplicas = + parseInt(getKvStringValue(statusKv, "replicas")) || 0; + readyReplicas = + parseInt(getKvStringValue(statusKv, "readyReplicas")) || 0; + availableReplicas = + parseInt(getKvStringValue(statusKv, "availableReplicas")) || 0; + unavailableReplicas = + parseInt(getKvStringValue(statusKv, "unavailableReplicas")) || 0; + const condArray: string | JSONObject | null = getKvValue( + statusKv, + "conditions", + ); + conditions = parseConditions(condArray as JSONObject | null); + } + + return { + metadata: parseMetadata(metadataKv), + spec: { replicas, strategy, selector }, + status: { + replicas: statusReplicas, + readyReplicas, + availableReplicas, + unavailableReplicas, + conditions, + }, + }; + } catch { + return null; + } +} + +export function parseStatefulSetObject( + objectKvList: JSONObject, +): KubernetesStatefulSetObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const specKv: string | JSONObject | null = getKvValue( + objectKvList, + "spec", + ); + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + let replicas: number = 0; + let serviceName: string = ""; + let podManagementPolicy: string = ""; + let updateStrategy: string = ""; + if (specKv && typeof specKv !== "string") { + replicas = parseInt(getKvStringValue(specKv, "replicas")) || 0; + serviceName = getKvStringValue(specKv, "serviceName"); + podManagementPolicy = getKvStringValue(specKv, "podManagementPolicy"); + const usKv: string | JSONObject | null = getKvValue( + specKv, + "updateStrategy", + ); + if (usKv && typeof usKv !== "string") { + updateStrategy = getKvStringValue(usKv, "type"); + } + } + + return { + metadata: parseMetadata(metadataKv), + spec: { replicas, serviceName, podManagementPolicy, updateStrategy }, + status: { + replicas: statusKv + ? parseInt(getKvStringValue(statusKv as JSONObject, "replicas")) || 0 + : 0, + readyReplicas: statusKv + ? parseInt( + getKvStringValue(statusKv as JSONObject, "readyReplicas"), + ) || 0 + : 0, + currentReplicas: statusKv + ? parseInt( + getKvStringValue(statusKv as JSONObject, "currentReplicas"), + ) || 0 + : 0, + }, + }; + } catch { + return null; + } +} + +export function parseDaemonSetObject( + objectKvList: JSONObject, +): KubernetesDaemonSetObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const specKv: string | JSONObject | null = getKvValue( + objectKvList, + "spec", + ); + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + let updateStrategy: string = ""; + if (specKv && typeof specKv !== "string") { + const usKv: string | JSONObject | null = getKvValue( + specKv, + "updateStrategy", + ); + if (usKv && typeof usKv !== "string") { + updateStrategy = getKvStringValue(usKv, "type"); + } + } + + return { + metadata: parseMetadata(metadataKv), + spec: { updateStrategy }, + status: { + desiredNumberScheduled: statusKv + ? parseInt( + getKvStringValue( + statusKv as JSONObject, + "desiredNumberScheduled", + ), + ) || 0 + : 0, + currentNumberScheduled: statusKv + ? parseInt( + getKvStringValue( + statusKv as JSONObject, + "currentNumberScheduled", + ), + ) || 0 + : 0, + numberReady: statusKv + ? parseInt( + getKvStringValue(statusKv as JSONObject, "numberReady"), + ) || 0 + : 0, + numberMisscheduled: statusKv + ? parseInt( + getKvStringValue(statusKv as JSONObject, "numberMisscheduled"), + ) || 0 + : 0, + numberAvailable: statusKv + ? parseInt( + getKvStringValue(statusKv as JSONObject, "numberAvailable"), + ) || 0 + : 0, + }, + }; + } catch { + return null; + } +} + +export function parseJobObject( + objectKvList: JSONObject, +): KubernetesJobObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const specKv: string | JSONObject | null = getKvValue( + objectKvList, + "spec", + ); + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + return { + metadata: parseMetadata(metadataKv), + spec: { + completions: specKv + ? parseInt( + getKvStringValue(specKv as JSONObject, "completions"), + ) || 0 + : 0, + parallelism: specKv + ? parseInt( + getKvStringValue(specKv as JSONObject, "parallelism"), + ) || 0 + : 0, + backoffLimit: specKv + ? parseInt( + getKvStringValue(specKv as JSONObject, "backoffLimit"), + ) || 0 + : 0, + }, + status: { + active: statusKv + ? parseInt(getKvStringValue(statusKv as JSONObject, "active")) || 0 + : 0, + succeeded: statusKv + ? parseInt( + getKvStringValue(statusKv as JSONObject, "succeeded"), + ) || 0 + : 0, + failed: statusKv + ? parseInt(getKvStringValue(statusKv as JSONObject, "failed")) || 0 + : 0, + startTime: statusKv + ? getKvStringValue(statusKv as JSONObject, "startTime") + : "", + completionTime: statusKv + ? getKvStringValue(statusKv as JSONObject, "completionTime") + : "", + conditions: statusKv + ? parseConditions( + getKvValue( + statusKv as JSONObject, + "conditions", + ) as JSONObject | null, + ) + : [], + }, + }; + } catch { + return null; + } +} + +export function parseCronJobObject( + objectKvList: JSONObject, +): KubernetesCronJobObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const specKv: string | JSONObject | null = getKvValue( + objectKvList, + "spec", + ); + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + return { + metadata: parseMetadata(metadataKv), + spec: { + schedule: specKv + ? getKvStringValue(specKv as JSONObject, "schedule") + : "", + suspend: + specKv + ? getKvStringValue(specKv as JSONObject, "suspend") === "true" + : false, + concurrencyPolicy: specKv + ? getKvStringValue(specKv as JSONObject, "concurrencyPolicy") + : "", + successfulJobsHistoryLimit: specKv + ? parseInt( + getKvStringValue( + specKv as JSONObject, + "successfulJobsHistoryLimit", + ), + ) || 0 + : 0, + failedJobsHistoryLimit: specKv + ? parseInt( + getKvStringValue( + specKv as JSONObject, + "failedJobsHistoryLimit", + ), + ) || 0 + : 0, + }, + status: { + lastScheduleTime: statusKv + ? getKvStringValue(statusKv as JSONObject, "lastScheduleTime") + : "", + activeCount: statusKv + ? parseInt(getKvStringValue(statusKv as JSONObject, "active")) || 0 + : 0, + }, + }; + } catch { + return null; + } +} + +export function parseNamespaceObject( + objectKvList: JSONObject, +): KubernetesNamespaceObject | null { + try { + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + if (!metadataKv || typeof metadataKv === "string") { + return null; + } + + const statusKv: string | JSONObject | null = getKvValue( + objectKvList, + "status", + ); + + return { + metadata: parseMetadata(metadataKv), + status: { + phase: statusKv + ? getKvStringValue(statusKv as JSONObject, "phase") + : "", + }, + }; + } catch { + return null; + } +} + +/** + * Extract the K8s object from a raw OTLP log body string. + * For k8sobjects pull mode, the body is: + * { kvlistValue: { values: [{ key: "type", value: ... }, { key: "object", value: { kvlistValue: ... } }] } } + * OR for some modes, the object may be at the top level. + */ +export function extractObjectFromLogBody( + bodyString: string, +): JSONObject | null { + try { + const bodyObj: JSONObject = JSON.parse(bodyString) as JSONObject; + const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as + | JSONObject + | undefined; + if (!topKvList) { + return null; + } + + // Try to get the "object" key (used in watch mode) + const objectVal: string | JSONObject | null = getKvValue( + topKvList, + "object", + ); + if (objectVal && typeof objectVal !== "string") { + return objectVal; + } + + // If no "object" key, the kvlist might BE the object (pull mode) + // Check if it has typical K8s fields + const kind: string | JSONObject | null = getKvValue(topKvList, "kind"); + if (kind) { + return topKvList; + } + + return null; + } catch { + return null; + } +} diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx index 865a91f496..79357d0b5e 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,10 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab"; const KubernetesClusterContainerDetail: FunctionComponent< PageComponentProps @@ -36,16 +36,6 @@ const KubernetesClusterContainerDetail: FunctionComponent< const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); - const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { @@ -89,7 +79,8 @@ const KubernetesClusterContainerDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const name: string = - (attributes["resource.k8s.container.name"] as string) || "Unknown Container"; + (attributes["resource.k8s.container.name"] as string) || + "Unknown Container"; return { title: name }; }; @@ -143,32 +134,55 @@ const KubernetesClusterContainerDetail: FunctionComponent< getSeries: getSeries, }; + const tabs: Array = [ + { + name: "Overview", + children: ( +
+
+ + +
+
+ ), + }, + { + name: "Logs", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index 623a3ba5d9..93d48f4fa4 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesCronJobObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterCronJobDetail: FunctionComponent< PageComponentProps @@ -35,16 +38,9 @@ const KubernetesClusterCronJobDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [cronJobObject, setCronJobObject] = + useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +65,32 @@ const KubernetesClusterCronJobDetail: FunctionComponent< }); }, []); + // Fetch the K8s cronjob object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchCronJobObject: () => Promise = + async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesCronJobObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "cronjobs", + resourceName: cronJobName, + }); + setCronJobObject(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchCronJobObject().catch(() => {}); + }, [cluster?.clusterIdentifier, cronJobName]); + if (isLoading) { return ; } @@ -143,32 +165,104 @@ const KubernetesClusterCronJobDetail: FunctionComponent< getSeries: getSeries, }; + // Build overview summary fields from cronjob object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "CronJob Name", value: cronJobName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (cronJobObject) { + summaryFields.push( + { + title: "Namespace", + value: cronJobObject.metadata.namespace || "default", + }, + { + title: "Schedule", + value: cronJobObject.spec.schedule || "N/A", + }, + { + title: "Suspend", + value: cronJobObject.spec.suspend ? "Yes" : "No", + }, + { + title: "Concurrency Policy", + value: cronJobObject.spec.concurrencyPolicy || "N/A", + }, + { + title: "Successful Jobs History Limit", + value: String(cronJobObject.spec.successfulJobsHistoryLimit ?? "N/A"), + }, + { + title: "Failed Jobs History Limit", + value: String(cronJobObject.spec.failedJobsHistoryLimit ?? "N/A"), + }, + { + title: "Last Schedule Time", + value: cronJobObject.status.lastScheduleTime || "N/A", + }, + { + title: "Active Jobs", + value: String(cronJobObject.status.activeCount ?? 0), + }, + { + title: "Created", + value: cronJobObject.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index 7c2a344d70..acfcd83f24 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesDaemonSetObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterDaemonSetDetail: FunctionComponent< PageComponentProps @@ -35,16 +38,9 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [objectData, setObjectData] = + useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +65,31 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< }); }, []); + // Fetch the K8s daemonset object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesDaemonSetObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "daemonsets", + resourceName: daemonSetName, + }); + setObjectData(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchObject().catch(() => {}); + }, [cluster?.clusterIdentifier, daemonSetName]); + if (isLoading) { return ; } @@ -143,32 +164,99 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< getSeries: getSeries, }; + // Build overview summary fields from daemonset object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "Name", value: daemonSetName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (objectData) { + summaryFields.push( + { + title: "Namespace", + value: objectData.metadata.namespace || "default", + }, + { + title: "Desired Scheduled", + value: String(objectData.status.desiredNumberScheduled ?? "N/A"), + }, + { + title: "Current Scheduled", + value: String(objectData.status.currentNumberScheduled ?? "N/A"), + }, + { + title: "Number Ready", + value: String(objectData.status.numberReady ?? "N/A"), + }, + { + title: "Number Available", + value: String(objectData.status.numberAvailable ?? "N/A"), + }, + { + title: "Update Strategy", + value: objectData.spec.updateStrategy || "N/A", + }, + { + title: "Created", + value: objectData.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index 54c5c6d6ab..ba877cafd1 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesDeploymentObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterDeploymentDetail: FunctionComponent< PageComponentProps @@ -35,16 +38,9 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [objectData, setObjectData] = + useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +65,31 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< }); }, []); + // Fetch the K8s deployment object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesDeploymentObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "deployments", + resourceName: deploymentName, + }); + setObjectData(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchObject().catch(() => {}); + }, [cluster?.clusterIdentifier, deploymentName]); + if (isLoading) { return ; } @@ -143,32 +164,96 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< getSeries: getSeries, }; + // Build overview summary fields from deployment object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "Name", value: deploymentName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (objectData) { + summaryFields.push( + { + title: "Namespace", + value: objectData.metadata.namespace || "default", + }, + { + title: "Replicas", + value: String(objectData.spec.replicas ?? "N/A"), + }, + { + title: "Ready Replicas", + value: String(objectData.status.readyReplicas ?? "N/A"), + }, + { + title: "Available Replicas", + value: String(objectData.status.availableReplicas ?? "N/A"), + }, + { + title: "Strategy", + value: objectData.spec.strategy || "N/A", + }, + { + title: "Created", + value: objectData.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx index ba6e56a86b..0302bc902a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx @@ -24,16 +24,11 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import { JSONObject } from "Common/Types/JSON"; import InBetween from "Common/Types/BaseDatabase/InBetween"; - -interface KubernetesEvent { - timestamp: string; - type: string; - reason: string; - objectKind: string; - objectName: string; - namespace: string; - message: string; -} +import { + getKvValue, + getKvStringValue, +} from "../Utils/KubernetesObjectParser"; +import { KubernetesEvent } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterEvents: FunctionComponent< PageComponentProps @@ -65,13 +60,18 @@ const KubernetesClusterEvents: FunctionComponent< const endDate: Date = OneUptimeDate.getCurrentDate(); const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24); - const listResult: ListResult = await AnalyticsModelAPI.getList({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const eventsQueryOptions: any = { modelType: Log, query: { projectId: ProjectUtil.getCurrentProjectId()!.toString(), time: new InBetween(startDate, endDate), + attributes: { + "logAttributes.event.domain": "k8s", + "logAttributes.k8s.resource.name": "events", + }, }, - limit: 200, + limit: 500, skip: 0, select: { time: true, @@ -83,76 +83,9 @@ const KubernetesClusterEvents: FunctionComponent< time: SortOrder.Descending, }, requestOptions: {}, - }); - - // Helper to extract a string value from OTLP kvlistValue - const getKvValue: ( - kvList: JSONObject | undefined, - key: string, - ) => string = (kvList: JSONObject | undefined, key: string): string => { - if (!kvList) { - return ""; - } - const values: Array | undefined = (kvList as JSONObject)[ - "values" - ] as Array | undefined; - if (!values) { - return ""; - } - for (const entry of values) { - if (entry["key"] === key) { - const val: JSONObject | undefined = entry["value"] as - | JSONObject - | undefined; - if (!val) { - return ""; - } - if (val["stringValue"]) { - return val["stringValue"] as string; - } - if (val["intValue"]) { - return String(val["intValue"]); - } - // Nested kvlist (e.g., regarding, metadata) - if (val["kvlistValue"]) { - return val["kvlistValue"] as unknown as string; - } - } - } - return ""; - }; - - // Helper to get nested kvlist value - const getNestedKvValue: ( - kvList: JSONObject | undefined, - parentKey: string, - childKey: string, - ) => string = ( - kvList: JSONObject | undefined, - parentKey: string, - childKey: string, - ): string => { - if (!kvList) { - return ""; - } - const values: Array | undefined = (kvList as JSONObject)[ - "values" - ] as Array | undefined; - if (!values) { - return ""; - } - for (const entry of values) { - if (entry["key"] === parentKey) { - const val: JSONObject | undefined = entry["value"] as - | JSONObject - | undefined; - if (val && val["kvlistValue"]) { - return getKvValue(val["kvlistValue"] as JSONObject, childKey); - } - } - } - return ""; }; + const listResult: ListResult = + await AnalyticsModelAPI.getList(eventsQueryOptions); const k8sEvents: Array = []; @@ -167,11 +100,6 @@ const KubernetesClusterEvents: FunctionComponent< continue; } - // Only process k8s event logs (from k8sobjects receiver) - if (attrs["logAttributes.event.domain"] !== "k8s") { - continue; - } - // Parse the body which is OTLP kvlistValue JSON let bodyObj: JSONObject | null = null; try { @@ -186,7 +114,6 @@ const KubernetesClusterEvents: FunctionComponent< continue; } - // The body has a top-level kvlistValue with "type" (ADDED/MODIFIED) and "object" keys const topKvList: JSONObject | undefined = bodyObj["kvlistValue"] as | JSONObject | undefined; @@ -195,25 +122,55 @@ const KubernetesClusterEvents: FunctionComponent< } // Get the "object" which is the actual k8s Event - const objectKvListRaw: string = getKvValue(topKvList, "object"); - if (!objectKvListRaw || typeof objectKvListRaw === "string") { + const objectVal: string | JSONObject | null = getKvValue( + topKvList, + "object", + ); + if (!objectVal || typeof objectVal === "string") { continue; } - const objectKvList: JSONObject = - objectKvListRaw as unknown as JSONObject; + const objectKvList: JSONObject = objectVal; - const eventType: string = getKvValue(objectKvList, "type") || ""; - const reason: string = getKvValue(objectKvList, "reason") || ""; - const note: string = getKvValue(objectKvList, "note") || ""; + const eventType: string = + getKvStringValue(objectKvList, "type") || ""; + const reason: string = + getKvStringValue(objectKvList, "reason") || ""; + const note: string = + getKvStringValue(objectKvList, "note") || ""; + + // Get regarding object details using shared parser + const regardingKv: string | JSONObject | null = getKvValue( + objectKvList, + "regarding", + ); + const regardingObj: JSONObject | undefined = + regardingKv && typeof regardingKv !== "string" + ? regardingKv + : undefined; + + const objectKind: string = regardingObj + ? getKvStringValue(regardingObj, "kind") + : ""; + const objectName: string = regardingObj + ? getKvStringValue(regardingObj, "name") + : ""; + + const metadataKv: string | JSONObject | null = getKvValue( + objectKvList, + "metadata", + ); + const metadataObj: JSONObject | undefined = + metadataKv && typeof metadataKv !== "string" + ? metadataKv + : undefined; - // Get object details from "regarding" sub-object - const objectKind: string = - getNestedKvValue(objectKvList, "regarding", "kind") || ""; - const objectName: string = - getNestedKvValue(objectKvList, "regarding", "name") || ""; const namespace: string = - getNestedKvValue(objectKvList, "regarding", "namespace") || - getNestedKvValue(objectKvList, "metadata", "namespace") || + (regardingObj + ? getKvStringValue(regardingObj, "namespace") + : "") || + (metadataObj + ? getKvStringValue(metadataObj, "namespace") + : "") || ""; if (eventType || reason) { @@ -297,7 +254,10 @@ const KubernetesClusterEvents: FunctionComponent< const isWarning: boolean = event.type.toLowerCase() === "warning"; return ( - + {event.timestamp} diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index 2ce5d73fe5..8b32227a89 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesJobObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterJobDetail: FunctionComponent< PageComponentProps @@ -35,16 +38,8 @@ const KubernetesClusterJobDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [jobObject, setJobObject] = useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +64,31 @@ const KubernetesClusterJobDetail: FunctionComponent< }); }, []); + // Fetch the K8s job object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchJobObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesJobObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "jobs", + resourceName: jobName, + }); + setJobObject(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchJobObject().catch(() => {}); + }, [cluster?.clusterIdentifier, jobName]); + if (isLoading) { return ; } @@ -143,32 +163,109 @@ const KubernetesClusterJobDetail: FunctionComponent< getSeries: getSeries, }; + // Build overview summary fields from job object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "Job Name", value: jobName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (jobObject) { + summaryFields.push( + { + title: "Namespace", + value: jobObject.metadata.namespace || "default", + }, + { + title: "Completions", + value: String(jobObject.spec.completions ?? "N/A"), + }, + { + title: "Parallelism", + value: String(jobObject.spec.parallelism ?? "N/A"), + }, + { + title: "Backoff Limit", + value: String(jobObject.spec.backoffLimit ?? "N/A"), + }, + { + title: "Active", + value: String(jobObject.status.active ?? 0), + }, + { + title: "Succeeded", + value: String(jobObject.status.succeeded ?? 0), + }, + { + title: "Failed", + value: String(jobObject.status.failed ?? 0), + }, + { + title: "Start Time", + value: jobObject.status.startTime || "N/A", + }, + { + title: "Completion Time", + value: jobObject.status.completionTime || "N/A", + }, + { + title: "Created", + value: jobObject.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index d6f4043bd0..fd37e12f59 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesNamespaceObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterNamespaceDetail: FunctionComponent< PageComponentProps @@ -35,16 +38,9 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [namespaceObject, setNamespaceObject] = + useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +65,32 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< }); }, []); + // Fetch the K8s namespace object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchNamespaceObject: () => Promise = + async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesNamespaceObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "namespaces", + resourceName: namespaceName, + }); + setNamespaceObject(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchNamespaceObject().catch(() => {}); + }, [cluster?.clusterIdentifier, namespaceName]); + if (isLoading) { return ; } @@ -143,32 +165,75 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< getSeries: getSeries, }; + // Build overview summary fields from namespace object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "Namespace Name", value: namespaceName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (namespaceObject) { + summaryFields.push( + { + title: "Status Phase", + value: namespaceObject.status.phase || "N/A", + }, + { + title: "Created", + value: namespaceObject.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx index 0234597b0f..3382ce8f30 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx @@ -4,12 +4,8 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -22,6 +18,13 @@ import API from "Common/UI/Utils/API/API"; import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesNodeObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterNodeDetail: FunctionComponent< PageComponentProps @@ -32,16 +35,10 @@ const KubernetesClusterNodeDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [nodeObject, setNodeObject] = useState( + null, + ); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -66,6 +63,31 @@ const KubernetesClusterNodeDetail: FunctionComponent< }); }, []); + // Fetch the K8s node object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchNodeObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesNodeObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "nodes", + resourceName: nodeName, + }); + setNodeObject(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchNodeObject().catch(() => {}); + }, [cluster?.clusterIdentifier, nodeName]); + if (isLoading) { return ; } @@ -200,44 +222,144 @@ const KubernetesClusterNodeDetail: FunctionComponent< }, }; - return ( - -
- - -
+ // Determine node status from conditions + const getNodeStatus: () => { label: string; isReady: boolean } = (): { + label: string; + isReady: boolean; + } => { + if (!nodeObject) { + return { label: "Unknown", isReady: false }; + } + const readyCondition = nodeObject.status.conditions.find( + (c) => { + return c.type === "Ready"; + }, + ); + if (readyCondition && readyCondition.status === "True") { + return { label: "Ready", isReady: true }; + } + return { label: "NotReady", isReady: false }; + }; - - = + [ + { title: "Node Name", value: nodeName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (nodeObject) { + const nodeStatus = getNodeStatus(); + + summaryFields.push( + { + title: "Status", + value: ( + + {nodeStatus.label} + + ), + }, + { + title: "OS Image", + value: nodeObject.status.nodeInfo.osImage || "N/A", + }, + { + title: "Kernel", + value: nodeObject.status.nodeInfo.kernelVersion || "N/A", + }, + { + title: "Container Runtime", + value: nodeObject.status.nodeInfo.containerRuntimeVersion || "N/A", + }, + { + title: "Kubelet Version", + value: nodeObject.status.nodeInfo.kubeletVersion || "N/A", + }, + { + title: "Architecture", + value: nodeObject.status.nodeInfo.architecture || "N/A", + }, + { + title: "CPU Allocatable", + value: nodeObject.status.allocatable["cpu"] || "N/A", + }, + { + title: "Memory Allocatable", + value: nodeObject.status.allocatable["memory"] || "N/A", + }, + { + title: "Created", + value: nodeObject.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + { - setMetricViewData({ - ...data, - queryConfigs: [ - cpuQuery, - memoryQuery, - filesystemQuery, - networkRxQuery, - networkTxQuery, - ], - formulaConfigs: [], - }); - }} - /> - + ]} + /> + + ), + }, + ]; + + return ( + +
+
+ + +
+
+ + {}} />
); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx index a1b32ef459..9ecd356348 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,15 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesContainersTab from "../../../Components/Kubernetes/KubernetesContainersTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesLogsTab from "../../../Components/Kubernetes/KubernetesLogsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesPodObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterPodDetail: FunctionComponent< PageComponentProps @@ -35,16 +40,8 @@ const KubernetesClusterPodDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [podObject, setPodObject] = useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +66,31 @@ const KubernetesClusterPodDetail: FunctionComponent< }); }, []); + // Fetch the K8s pod object for overview/containers tabs + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchPodObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesPodObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "pods", + resourceName: podName, + }); + setPodObject(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchPodObject().catch(() => {}); + }, [cluster?.clusterIdentifier, podName]); + if (isLoading) { return ; } @@ -89,7 +111,8 @@ const KubernetesClusterPodDetail: FunctionComponent< const attributes: Record = (data["attributes"] as Record) || {}; const containerName: string = - (attributes["resource.k8s.container.name"] as string) || "Unknown Container"; + (attributes["resource.k8s.container.name"] as string) || + "Unknown Container"; return { title: containerName }; }; @@ -191,42 +214,133 @@ const KubernetesClusterPodDetail: FunctionComponent< }, }; + // Build overview summary fields from pod object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "Pod Name", value: podName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (podObject) { + summaryFields.push( + { + title: "Namespace", + value: podObject.metadata.namespace || "default", + }, + { + title: "Status", + value: ( + + {podObject.status.phase || "Unknown"} + + ), + }, + { title: "Node", value: podObject.spec.nodeName || "N/A" }, + { title: "Pod IP", value: podObject.status.podIP || "N/A" }, + { title: "Host IP", value: podObject.status.hostIP || "N/A" }, + { + title: "Service Account", + value: podObject.spec.serviceAccountName || "default", + }, + { + title: "Created", + value: podObject.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Containers", + children: podObject ? ( + + ) : isLoadingObject ? ( + + ) : ( +
+ Container details not yet available. Ensure the kubernetes-agent Helm + chart has resourceSpecs.enabled set to true. +
+ ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Logs", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [ - podCpuQuery, - podMemoryQuery, - cpuQuery, - memoryQuery, - ], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index 39a780ca68..1e55e3643f 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -4,14 +4,10 @@ import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; import Card from "Common/UI/Components/Card/Card"; import InfoCard from "Common/UI/Components/InfoCard/InfoCard"; -import MetricView from "../../../Components/Metrics/MetricView"; -import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData, { ChartSeries, } from "Common/Types/Metrics/MetricQueryConfigData"; import AggregationType from "Common/Types/BaseDatabase/AggregationType"; -import OneUptimeDate from "Common/Types/Date"; -import InBetween from "Common/Types/BaseDatabase/InBetween"; import React, { Fragment, FunctionComponent, @@ -25,6 +21,13 @@ import PageLoader from "Common/UI/Components/Loader/PageLoader"; import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel"; +import Tabs from "Common/UI/Components/Tabs/Tabs"; +import { Tab } from "Common/UI/Components/Tabs/Tab"; +import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; +import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; +import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; +import { KubernetesStatefulSetObject } from "../Utils/KubernetesObjectParser"; +import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterStatefulSetDetail: FunctionComponent< PageComponentProps @@ -35,16 +38,9 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); - - const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); - const startAndEndDate: InBetween = new InBetween(startDate, endDate); - - const [metricViewData, setMetricViewData] = useState({ - startAndEndDate: startAndEndDate, - queryConfigs: [], - formulaConfigs: [], - }); + const [objectData, setObjectData] = + useState(null); + const [isLoadingObject, setIsLoadingObject] = useState(true); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -69,6 +65,31 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< }); }, []); + // Fetch the K8s statefulset object for overview tab + useEffect(() => { + if (!cluster?.clusterIdentifier) { + return; + } + + const fetchObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesStatefulSetObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "statefulsets", + resourceName: statefulSetName, + }); + setObjectData(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; + + fetchObject().catch(() => {}); + }, [cluster?.clusterIdentifier, statefulSetName]); + if (isLoading) { return ; } @@ -143,32 +164,102 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< getSeries: getSeries, }; + // Build overview summary fields from statefulset object + const summaryFields: Array<{ title: string; value: string | ReactElement }> = + [ + { title: "Name", value: statefulSetName }, + { title: "Cluster", value: clusterIdentifier }, + ]; + + if (objectData) { + summaryFields.push( + { + title: "Namespace", + value: objectData.metadata.namespace || "default", + }, + { + title: "Replicas", + value: String(objectData.spec.replicas ?? "N/A"), + }, + { + title: "Ready Replicas", + value: String(objectData.status.readyReplicas ?? "N/A"), + }, + { + title: "Service Name", + value: objectData.spec.serviceName || "N/A", + }, + { + title: "Pod Management Policy", + value: objectData.spec.podManagementPolicy || "N/A", + }, + { + title: "Update Strategy", + value: objectData.spec.updateStrategy || "N/A", + }, + { + title: "Created", + value: objectData.metadata.creationTimestamp || "N/A", + }, + ); + } + + const tabs: Array = [ + { + name: "Overview", + children: ( + + ), + }, + { + name: "Events", + children: ( + + + + ), + }, + { + name: "Metrics", + children: ( + + + + ), + }, + ]; + return ( -
- - +
+
+ + +
- - { - setMetricViewData({ - ...data, - queryConfigs: [cpuQuery, memoryQuery], - formulaConfigs: [], - }); - }} - /> - + {}} /> ); }; diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml index 54beb0f2e7..95bd5937d7 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml @@ -32,6 +32,38 @@ data: - name: events mode: watch group: events.k8s.io + {{- if .Values.resourceSpecs.enabled }} + # Pull full resource specs for dashboard detail views + - name: pods + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + - name: nodes + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + - name: namespaces + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + - name: deployments + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + group: apps + - name: statefulsets + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + group: apps + - name: daemonsets + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + group: apps + - name: jobs + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + group: batch + - name: cronjobs + mode: pull + interval: {{ .Values.resourceSpecs.interval }} + group: batch + {{- end }} {{- if .Values.controlPlane.enabled }} # Scrape control plane metrics via Prometheus endpoints diff --git a/HelmChart/Public/kubernetes-agent/values.yaml b/HelmChart/Public/kubernetes-agent/values.yaml index 1dff9d71b2..ec8a5e6159 100644 --- a/HelmChart/Public/kubernetes-agent/values.yaml +++ b/HelmChart/Public/kubernetes-agent/values.yaml @@ -70,6 +70,13 @@ logs: cpu: 200m memory: 256Mi +# Resource spec collection — pulls full K8s resource objects (pods, nodes, +# deployments, etc.) for displaying labels, annotations, env vars, status, +# and other details in the dashboard. +resourceSpecs: + enabled: true + interval: 300s # How often to pull full resource specs (default: 5 minutes) + # Collection intervals collectionInterval: 30s diff --git a/Home/Routes.ts b/Home/Routes.ts index 7b62a4df69..5d3f845059 100755 --- a/Home/Routes.ts +++ b/Home/Routes.ts @@ -1222,6 +1222,27 @@ const HomeFeatureSet: FeatureSet = { }, ); + app.get( + "/product/scheduled-maintenance", + (_req: ExpressRequest, res: ExpressResponse) => { + const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath( + "/product/scheduled-maintenance", + res.locals["homeUrl"] as string, + ); + res.render(`${ViewsPath}/scheduled-maintenance`, { + enableGoogleTagManager: IsBillingEnabled, + seo, + }); + }, + ); + + app.get( + "/scheduled-maintenance", + (_req: ExpressRequest, res: ExpressResponse) => { + res.redirect("/product/scheduled-maintenance"); + }, + ); + app.get("/product/traces", (_req: ExpressRequest, res: ExpressResponse) => { const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath( "/product/traces", diff --git a/Home/Utils/PageSEO.ts b/Home/Utils/PageSEO.ts index d303f864f7..f42e417373 100644 --- a/Home/Utils/PageSEO.ts +++ b/Home/Utils/PageSEO.ts @@ -438,6 +438,43 @@ export const PageSEOConfig: Record = { }, }, + "/product/scheduled-maintenance": { + title: + "Scheduled Maintenance | Plan & Communicate Downtime | OneUptime", + description: + "Plan, schedule, and communicate maintenance windows to your users. Notify subscribers automatically, update status pages in real-time. Open source maintenance management.", + canonicalPath: "/product/scheduled-maintenance", + twitterCard: "summary_large_image", + pageType: "product", + breadcrumbs: [ + { name: "Home", url: "/" }, + { name: "Products", url: "/#products" }, + { + name: "Scheduled Maintenance", + url: "/product/scheduled-maintenance", + }, + ], + softwareApplication: { + name: "OneUptime Scheduled Maintenance", + applicationCategory: "DeveloperApplication", + operatingSystem: "Web, Cloud", + description: + "Schedule maintenance windows, notify subscribers via email, SMS, and webhooks, and keep your status page updated in real-time.", + features: [ + "Maintenance scheduling", + "Subscriber notifications", + "Status page integration", + "Custom maintenance states", + "Email and SMS alerts", + "Webhook integrations", + "Slack and Teams notifications", + "Maintenance timeline", + "Affected monitors tracking", + "Automatic state transitions", + ], + }, + }, + "/product/traces": { title: "Distributed Tracing | End-to-End Request Tracing | OneUptime", description: diff --git a/Home/Utils/Sitemap.ts b/Home/Utils/Sitemap.ts index 0044b97e42..561ec7ad95 100644 --- a/Home/Utils/Sitemap.ts +++ b/Home/Utils/Sitemap.ts @@ -37,6 +37,7 @@ const PAGE_CONFIG: Record = { "/product/workflows": { priority: 0.9, changefreq: "weekly" }, "/product/dashboards": { priority: 0.9, changefreq: "weekly" }, "/product/ai-agent": { priority: 0.9, changefreq: "weekly" }, + "/product/scheduled-maintenance": { priority: 0.9, changefreq: "weekly" }, // Teams (Solutions) pages "/solutions/devops": { priority: 0.8, changefreq: "weekly" }, diff --git a/Home/Views/Partials/hero-cards/scheduled-maintenance.ejs b/Home/Views/Partials/hero-cards/scheduled-maintenance.ejs new file mode 100644 index 0000000000..4730898014 --- /dev/null +++ b/Home/Views/Partials/hero-cards/scheduled-maintenance.ejs @@ -0,0 +1,10 @@ + + diff --git a/Home/Views/Partials/icons/kubernetes.ejs b/Home/Views/Partials/icons/kubernetes.ejs index ea51114870..e1c9f1b4ff 100644 --- a/Home/Views/Partials/icons/kubernetes.ejs +++ b/Home/Views/Partials/icons/kubernetes.ejs @@ -1,5 +1,2 @@ - - - - - + +Kubernetes diff --git a/Home/Views/Partials/icons/scheduled-maintenance.ejs b/Home/Views/Partials/icons/scheduled-maintenance.ejs new file mode 100644 index 0000000000..580475a444 --- /dev/null +++ b/Home/Views/Partials/icons/scheduled-maintenance.ejs @@ -0,0 +1,4 @@ + + + + diff --git a/Home/Views/kubernetes.ejs b/Home/Views/kubernetes.ejs index d2616e4899..11ef99a596 100644 --- a/Home/Views/kubernetes.ejs +++ b/Home/Views/kubernetes.ejs @@ -1062,6 +1062,193 @@
+ +
+
+
+ +
+

Team Notifications

+

+ Kubernetes alerts where your team already works +

+

+ Get instant notifications for pod crashes, node issues, and deployment failures in Slack and Microsoft Teams. Acknowledge and investigate without leaving your chat app. +

+ + +
+ +
+
+ + + + + + +
+
+

Slack

+

Interactive Kubernetes alert actions

+
+
+ + +
+
+ + + + + + + +
+
+

Microsoft Teams

+

Native adaptive cards integration

+
+
+
+ + +
+
+ + + + Real-time delivery +
+
+ + + + Pod crash alerts +
+
+ + + + Rich formatting +
+
+ + + + Action buttons +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ + + + #kubernetes-alerts +
+
+
+ + +
+ +
+
+ + + +
+
+
+ OneUptime Kubernetes + 2:47 PM +
+ +
+
+
+ CRITICAL + Pod CrashLoopBackOff +
+

payment-service-6d8f9 restarting

+
+ + + + + ns: production + + + + + + Restarts: 5 + +
+ +
+ + + +
+
+
+ +
+ + 👀 + 3 + + + 🚨 + 2 + +
+
+
+ + + +
+ + +
+
+ + + + Message #kubernetes-alerts +
+
+
+
+
+
+
+ <%- include('./Partials/enterprise-ready') -%> <%- include('features-table') -%> <%- include('cta') -%> @@ -1070,6 +1257,19 @@ <%- include('footer') -%> <%- include('./Partials/video-script') -%> + + + + From 0161bac9949b3e7bdeabb31e4d9fe819ef05e954 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 19 Mar 2026 08:41:25 +0000 Subject: [PATCH 19/23] feat: Update Kubernetes detail views to use Navigation.getLastParamAsString for improved parameter handling feat: Increase memory limits in Helm chart configuration for better resource management feat: Add resourceSpecs configuration to values schema for enhanced dashboard detail views --- .../src/Pages/Kubernetes/View/ContainerDetail.tsx | 2 +- .../src/Pages/Kubernetes/View/CronJobDetail.tsx | 2 +- .../src/Pages/Kubernetes/View/DaemonSetDetail.tsx | 2 +- .../Pages/Kubernetes/View/DeploymentDetail.tsx | 2 +- .../src/Pages/Kubernetes/View/JobDetail.tsx | 2 +- .../src/Pages/Kubernetes/View/NamespaceDetail.tsx | 2 +- .../src/Pages/Kubernetes/View/NodeDetail.tsx | 2 +- .../src/Pages/Kubernetes/View/PodDetail.tsx | 2 +- .../Pages/Kubernetes/View/StatefulSetDetail.tsx | 2 +- .../templates/configmap-deployment.yaml | 4 ++-- .../Public/kubernetes-agent/values.schema.json | 15 +++++++++++++++ HelmChart/Public/kubernetes-agent/values.yaml | 4 ++-- 12 files changed, 28 insertions(+), 13 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx index 79357d0b5e..d111e8211e 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx @@ -30,7 +30,7 @@ const KubernetesClusterContainerDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const containerName: string = Navigation.getLastParam()?.toString() || ""; + const containerName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index 93d48f4fa4..20403c1e4a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -33,7 +33,7 @@ const KubernetesClusterCronJobDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const cronJobName: string = Navigation.getLastParam()?.toString() || ""; + const cronJobName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index acfcd83f24..a1837e0ce0 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -33,7 +33,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const daemonSetName: string = Navigation.getLastParam()?.toString() || ""; + const daemonSetName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index ba877cafd1..79498ff38f 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -33,7 +33,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const deploymentName: string = Navigation.getLastParam()?.toString() || ""; + const deploymentName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index 8b32227a89..2eb4239b35 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -33,7 +33,7 @@ const KubernetesClusterJobDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const jobName: string = Navigation.getLastParam()?.toString() || ""; + const jobName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index fd37e12f59..a576f9e460 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -33,7 +33,7 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const namespaceName: string = Navigation.getLastParam()?.toString() || ""; + const namespaceName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx index 3382ce8f30..b4e3d6b4ed 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx @@ -30,7 +30,7 @@ const KubernetesClusterNodeDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const nodeName: string = Navigation.getLastParam()?.toString() || ""; + const nodeName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx index 9ecd356348..dfed41d26a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx @@ -35,7 +35,7 @@ const KubernetesClusterPodDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const podName: string = Navigation.getLastParam()?.toString() || ""; + const podName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index 1e55e3643f..cb3f664943 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -33,7 +33,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< PageComponentProps > = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(2); - const statefulSetName: string = Navigation.getLastParam()?.toString() || ""; + const statefulSetName: string = Navigation.getLastParamAsString(); const [cluster, setCluster] = useState(null); const [isLoading, setIsLoading] = useState(true); diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml index 95bd5937d7..3ab5556cd9 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml @@ -161,8 +161,8 @@ data: # Limit memory usage memory_limiter: check_interval: 5s - limit_mib: 1500 - spike_limit_mib: 300 + limit_mib: 3000 + spike_limit_mib: 600 exporters: otlphttp: diff --git a/HelmChart/Public/kubernetes-agent/values.schema.json b/HelmChart/Public/kubernetes-agent/values.schema.json index c602921306..d334964c72 100644 --- a/HelmChart/Public/kubernetes-agent/values.schema.json +++ b/HelmChart/Public/kubernetes-agent/values.schema.json @@ -184,6 +184,21 @@ }, "additionalProperties": false }, + "resourceSpecs": { + "type": "object", + "description": "Pull full K8s resource specs for dashboard detail views (labels, annotations, env vars, status, etc.)", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable pulling full resource specs via k8sobjects receiver" + }, + "interval": { + "type": "string", + "description": "How often to pull resource specs (e.g., 300s, 5m)" + } + }, + "additionalProperties": false + }, "collectionInterval": { "type": "string", "description": "Collection interval for metrics (e.g., 30s, 1m)" diff --git a/HelmChart/Public/kubernetes-agent/values.yaml b/HelmChart/Public/kubernetes-agent/values.yaml index ec8a5e6159..691915d72c 100644 --- a/HelmChart/Public/kubernetes-agent/values.yaml +++ b/HelmChart/Public/kubernetes-agent/values.yaml @@ -30,10 +30,10 @@ deployment: resources: requests: cpu: 200m - memory: 512Mi + memory: 1Gi limits: cpu: 1000m - memory: 2Gi + memory: 4Gi # Control plane monitoring (etcd, API server, scheduler, controller manager) # Disabled by default — enable for self-managed clusters. From e814027048514bdf602b7d22cad3ad4e6b013eb6 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 19 Mar 2026 09:06:35 +0000 Subject: [PATCH 20/23] feat: Update Icon component stroke properties and enhance configmap deployment settings for telemetry --- Common/UI/Components/Icon/Icon.tsx | 15 +++++++++------ .../templates/configmap-deployment.yaml | 13 +++++++++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Common/UI/Components/Icon/Icon.tsx b/Common/UI/Components/Icon/Icon.tsx index fa1978556f..712c99d883 100644 --- a/Common/UI/Components/Icon/Icon.tsx +++ b/Common/UI/Components/Icon/Icon.tsx @@ -2764,9 +2764,8 @@ const Icon: FunctionComponent = ({ y1={sy.toFixed(2)} x2={ex.toFixed(2)} y2={ey.toFixed(2)} - stroke="currentColor" - strokeWidth="1.5" strokeLinecap="round" + strokeLinejoin="round" />, ); } @@ -2775,12 +2774,16 @@ const Icon: FunctionComponent = ({ <> + - {spokes} , ); diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml index 3ab5556cd9..6615a62248 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml @@ -154,8 +154,8 @@ data: # Batch telemetry for efficient export batch: - send_batch_size: 200 - send_batch_max_size: 500 + send_batch_size: 100 + send_batch_max_size: 200 timeout: 10s # Limit memory usage @@ -169,6 +169,15 @@ data: endpoint: "{{ .Values.oneuptime.url }}/otlp" headers: x-oneuptime-token: "${env:ONEUPTIME_API_KEY}" + sending_queue: + enabled: true + num_consumers: 10 + queue_size: 5000 + retry_on_failure: + enabled: true + initial_interval: 5s + max_interval: 60s + max_elapsed_time: 300s service: extensions: From 36041cef6aef10c24034b95e9c286fc0b37b33c1 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 19 Mar 2026 09:09:37 +0000 Subject: [PATCH 21/23] feat: Add JSON encoding to OTLP HTTP exporter configuration in configmap deployment --- .../Public/kubernetes-agent/templates/configmap-deployment.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml index 6615a62248..a4ab08a5a1 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml @@ -167,6 +167,7 @@ data: exporters: otlphttp: endpoint: "{{ .Values.oneuptime.url }}/otlp" + encoding: json headers: x-oneuptime-token: "${env:ONEUPTIME_API_KEY}" sending_queue: From f84df206106f36850a60d60575c4a58f623ed2a9 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 19 Mar 2026 09:17:18 +0000 Subject: [PATCH 22/23] feat: Update Icon component and Kubernetes icon for improved styling and consistency --- Common/UI/Components/Icon/Icon.tsx | 15 ++++++--------- Home/Views/Partials/icons/kubernetes.ejs | 14 ++++++++++++-- Home/Views/scheduled-maintenance.ejs | 14 +++++++++----- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Common/UI/Components/Icon/Icon.tsx b/Common/UI/Components/Icon/Icon.tsx index 712c99d883..fa1978556f 100644 --- a/Common/UI/Components/Icon/Icon.tsx +++ b/Common/UI/Components/Icon/Icon.tsx @@ -2764,8 +2764,9 @@ const Icon: FunctionComponent = ({ y1={sy.toFixed(2)} x2={ex.toFixed(2)} y2={ey.toFixed(2)} + stroke="currentColor" + strokeWidth="1.5" strokeLinecap="round" - strokeLinejoin="round" />, ); } @@ -2774,16 +2775,12 @@ const Icon: FunctionComponent = ({ <> - + {spokes} , ); diff --git a/Home/Views/Partials/icons/kubernetes.ejs b/Home/Views/Partials/icons/kubernetes.ejs index e1c9f1b4ff..f16d9ef555 100644 --- a/Home/Views/Partials/icons/kubernetes.ejs +++ b/Home/Views/Partials/icons/kubernetes.ejs @@ -1,2 +1,12 @@ - -Kubernetes + + + + + + + + + + + + diff --git a/Home/Views/scheduled-maintenance.ejs b/Home/Views/scheduled-maintenance.ejs index 2ad184d5e0..c228e6dd0d 100644 --- a/Home/Views/scheduled-maintenance.ejs +++ b/Home/Views/scheduled-maintenance.ejs @@ -636,7 +636,10 @@
- + + + +

Slack

@@ -647,10 +650,11 @@
- - - - + + + + +

Microsoft Teams

From 3efacce00266b061ca60e3917fe733c3cbe24f2d Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Thu, 19 Mar 2026 09:25:52 +0000 Subject: [PATCH 23/23] Refactor and clean up code across multiple components - Simplified error handling in KubernetesEventsTab and KubernetesLogsTab by removing unnecessary line breaks. - Consolidated import statements in KubernetesMetricsTab for better readability. - Improved JSX formatting in KubernetesOverviewTab, KubernetesClusterContainerDetail, and other components for consistency. - Enhanced code clarity in KubernetesObjectFetcher and KubernetesObjectParser by removing redundant line breaks and comments. - Streamlined API response handling in IPWhitelistAPI for better readability. - Updated PageSEO configuration for improved formatting. --- .../Kubernetes/KubernetesContainersTab.tsx | 111 +++++++++------ .../Kubernetes/KubernetesEventsTab.tsx | 20 ++- .../Kubernetes/KubernetesLogsTab.tsx | 8 +- .../Kubernetes/KubernetesMetricsTab.tsx | 6 +- .../Kubernetes/KubernetesOverviewTab.tsx | 26 ++-- .../src/Components/Logs/LogsViewer.tsx | 23 ++- .../Utils/KubernetesObjectFetcher.ts | 6 +- .../Utils/KubernetesObjectParser.ts | 131 +++++++----------- .../Utils/KubernetesResourceUtils.ts | 96 +++++++------ .../Pages/Kubernetes/View/ContainerDetail.tsx | 14 +- .../Pages/Kubernetes/View/CronJobDetail.tsx | 40 +++--- .../Pages/Kubernetes/View/DaemonSetDetail.tsx | 4 +- .../Kubernetes/View/DeploymentDetail.tsx | 4 +- .../src/Pages/Kubernetes/View/Deployments.tsx | 4 +- .../src/Pages/Kubernetes/View/Events.tsx | 31 ++--- .../src/Pages/Kubernetes/View/JobDetail.tsx | 9 +- .../Pages/Kubernetes/View/NamespaceDetail.tsx | 9 +- .../src/Pages/Kubernetes/View/NodeDetail.tsx | 14 +- .../src/Pages/Kubernetes/View/PodDetail.tsx | 10 +- .../Kubernetes/View/StatefulSetDetail.tsx | 9 +- .../Pages/Kubernetes/View/StatefulSets.tsx | 4 +- App/FeatureSet/Docs/Index.ts | 5 +- Common/Server/API/IPWhitelistAPI.ts | 31 ++--- Home/Utils/PageSEO.ts | 3 +- 24 files changed, 303 insertions(+), 315 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx index 222da2021f..cea19f3ea8 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesContainersTab.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent, ReactElement, useState } from "react"; import Card from "Common/UI/Components/Card/Card"; import DictionaryOfStringsViewer from "Common/UI/Components/Dictionary/DictionaryOfStingsViewer"; import { + KubernetesContainerPort, KubernetesContainerSpec, KubernetesContainerStatus, } from "../../Pages/Kubernetes/Utils/KubernetesObjectParser"; @@ -104,15 +105,19 @@ const ContainerCard: FunctionComponent = ( {props.container.ports.length > 0 && (
Ports:{" "} - {props.container.ports.map((port, idx) => ( - - {port.name ? `${port.name}: ` : ""} - {port.containerPort}/{port.protocol} - - ))} + {props.container.ports.map( + (port: KubernetesContainerPort, idx: number) => { + return ( + + {port.name ? `${port.name}: ` : ""} + {port.containerPort}/{port.protocol} + + ); + }, + )}
)} @@ -176,20 +181,33 @@ const ContainerCard: FunctionComponent = ( {showMounts && (
- {props.container.volumeMounts.map((mount, idx) => ( -
- - {mount.name} - - - - {mount.mountPath} - - {mount.readOnly && ( - (read-only) - )} -
- ))} + {props.container.volumeMounts.map( + ( + mount: { + name: string; + mountPath: string; + readOnly: boolean; + }, + idx: number, + ) => { + return ( +
+ + {mount.name} + + + + {mount.mountPath} + + {mount.readOnly && ( + + (read-only) + + )} +
+ ); + }, + )}
)}
@@ -202,10 +220,7 @@ const ContainerCard: FunctionComponent = ( const KubernetesContainersTab: FunctionComponent = ( props: ComponentProps, ): ReactElement => { - if ( - props.containers.length === 0 && - props.initContainers.length === 0 - ) { + if (props.containers.length === 0 && props.initContainers.length === 0) { return (
No container information available. @@ -223,32 +238,36 @@ const KubernetesContainersTab: FunctionComponent = ( const statuses: Array | undefined = isInit ? props.initContainerStatuses : props.containerStatuses; - return statuses?.find( - (s: KubernetesContainerStatus) => s.name === name, - ); + return statuses?.find((s: KubernetesContainerStatus) => { + return s.name === name; + }); }; return (
{props.initContainers.map( - (container: KubernetesContainerSpec, index: number) => ( - - ), + (container: KubernetesContainerSpec, index: number) => { + return ( + + ); + }, )} {props.containers.map( - (container: KubernetesContainerSpec, index: number) => ( - - ), + (container: KubernetesContainerSpec, index: number) => { + return ( + + ); + }, )}
); diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx index 22d6c3f139..680220ed7d 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx @@ -29,18 +29,15 @@ const KubernetesEventsTab: FunctionComponent = ( const fetchEvents: () => Promise = async (): Promise => { setIsLoading(true); try { - const result: Array = - await fetchK8sEventsForResource({ - clusterIdentifier: props.clusterIdentifier, - resourceKind: props.resourceKind, - resourceName: props.resourceName, - namespace: props.namespace, - }); + const result: Array = await fetchK8sEventsForResource({ + clusterIdentifier: props.clusterIdentifier, + resourceKind: props.resourceKind, + resourceName: props.resourceName, + namespace: props.namespace, + }); setEvents(result); } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to fetch events", - ); + setError(err instanceof Error ? err.message : "Failed to fetch events"); } setIsLoading(false); }; @@ -91,8 +88,7 @@ const KubernetesEventsTab: FunctionComponent = ( {events.map((event: KubernetesEvent, index: number) => { - const isWarning: boolean = - event.type.toLowerCase() === "warning"; + const isWarning: boolean = event.type.toLowerCase() === "warning"; return ( diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx index 965790884e..c677540c98 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesLogsTab.tsx @@ -38,9 +38,7 @@ const KubernetesLogsTab: FunctionComponent = ( }); setLogs(result); } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to fetch logs", - ); + setError(err instanceof Error ? err.message : "Failed to fetch logs"); } setIsLoading(false); }; @@ -65,8 +63,8 @@ const KubernetesLogsTab: FunctionComponent = ( return (
No application logs found for this pod in the last 6 hours. Logs will - appear here once the kubernetes-agent's filelog receiver is collecting - data. + appear here once the kubernetes-agent's filelog receiver is + collecting data.
); } diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx index 5e84c12272..d1ef5073d4 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx @@ -1,8 +1,4 @@ -import React, { - FunctionComponent, - ReactElement, - useState, -} from "react"; +import React, { FunctionComponent, ReactElement, useState } from "react"; import MetricView from "../../Components/Metrics/MetricView"; import MetricViewData from "Common/Types/Metrics/MetricViewData"; import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData"; diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx index a0f310b20c..3dc3eff1ce 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx @@ -44,23 +44,20 @@ const KubernetesOverviewTab: FunctionComponent = ( {/* Summary Info Cards */} {props.summaryFields.length > 0 && (
- {props.summaryFields.map( - (field: SummaryField, index: number) => { - return ( - - ); - }, - )} + {props.summaryFields.map((field: SummaryField, index: number) => { + return ( + + ); + })}
)} {/* Owner References */} {props.ownerReferences && props.ownerReferences.length > 0 && ( - +
{props.ownerReferences.map( (ref: { kind: string; name: string }, index: number) => { @@ -147,7 +144,10 @@ const KubernetesOverviewTab: FunctionComponent = ( {/* Labels */} {Object.keys(props.labels).length > 0 && ( - + )} diff --git a/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx b/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx index 0446688a38..a5ae442ff4 100644 --- a/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Logs/LogsViewer.tsx @@ -413,11 +413,17 @@ const DashboardLogsViewer: FunctionComponent = ( } try { - // When live polling, recompute the time range so the query window - // slides forward to "now" and new logs become visible. + /* + * When live polling, recompute the time range so the query window + * slides forward to "now" and new logs become visible. + */ let query: Query = filterOptions; - if (skipLoadingState && isLiveEnabled && timeRange.range !== TimeRange.CUSTOM) { + if ( + skipLoadingState && + isLiveEnabled && + timeRange.range !== TimeRange.CUSTOM + ) { const freshRange: InBetween = RangeStartAndEndDateTimeUtil.getStartAndEndDate(timeRange); query = { @@ -468,7 +474,16 @@ const DashboardLogsViewer: FunctionComponent = ( } } }, - [filterOptions, isLiveEnabled, page, pageSize, select, sortField, sortOrder, timeRange], + [ + filterOptions, + isLiveEnabled, + page, + pageSize, + select, + sortField, + sortOrder, + timeRange, + ], ); // --- Fetch histogram --- diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts index e76d0494cc..d4665cb566 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts @@ -263,8 +263,7 @@ export async function fetchK8sEventsForResource(options: { const objectKvList: JSONObject = objectVal; // Get event details - const eventType: string = - getKvStringValue(objectKvList, "type") || ""; + const eventType: string = getKvStringValue(objectKvList, "type") || ""; const reason: string = getKvStringValue(objectKvList, "reason") || ""; const note: string = getKvStringValue(objectKvList, "note") || ""; @@ -400,8 +399,7 @@ export async function fetchPodLogs(options: { : "", body: typeof log.body === "string" ? log.body : "", severity: log.severityText || "INFO", - containerName: - (attrs["resource.k8s.container.name"] as string) || "", + containerName: (attrs["resource.k8s.container.name"] as string) || "", }; }); } catch { diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts index 30e885237e..42d8e52b17 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts @@ -1,8 +1,10 @@ import { JSONObject } from "Common/Types/JSON"; -// ============================================================ -// OTLP kvlistValue parsing helpers -// ============================================================ +/* + * ============================================================ + * OTLP kvlistValue parsing helpers + * ============================================================ + */ /** * Extract a value from an OTLP kvlistValue by key. @@ -141,9 +143,11 @@ export function getArrayValues( .filter(Boolean) as Array; } -// ============================================================ -// TypeScript interfaces for parsed K8s objects -// ============================================================ +/* + * ============================================================ + * TypeScript interfaces for parsed K8s objects + * ============================================================ + */ export interface KubernetesObjectMetadata { name: string; @@ -336,15 +340,14 @@ export interface KubernetesNamespaceObject { }; } -// ============================================================ -// Parsers -// ============================================================ +/* + * ============================================================ + * Parsers + * ============================================================ + */ function parseMetadata(kvList: JSONObject): KubernetesObjectMetadata { - const labelsKvList: string | JSONObject | null = getKvValue( - kvList, - "labels", - ); + const labelsKvList: string | JSONObject | null = getKvValue(kvList, "labels"); const annotationsKvList: string | JSONObject | null = getKvValue( kvList, "annotations", @@ -503,10 +506,7 @@ function parseContainerSpec(kvList: JSONObject): KubernetesContainerSpec { resourcesKv, "requests", ); - const limKv: string | JSONObject | null = getKvValue( - resourcesKv, - "limits", - ); + const limKv: string | JSONObject | null = getKvValue(resourcesKv, "limits"); if (reqKv && typeof reqKv !== "string") { requests = getKvListAsRecord(reqKv); } @@ -521,7 +521,8 @@ function parseContainerSpec(kvList: JSONObject): KubernetesContainerSpec { ); const command: Array = []; if (commandArray && typeof commandArray !== "string") { - const cmdValues: Array = (commandArray["values"] as Array) || []; + const cmdValues: Array = + (commandArray["values"] as Array) || []; for (const v of cmdValues) { if (v["stringValue"]) { command.push(v["stringValue"] as string); @@ -532,7 +533,8 @@ function parseContainerSpec(kvList: JSONObject): KubernetesContainerSpec { const argsArray: string | JSONObject | null = getKvValue(kvList, "args"); const args: Array = []; if (argsArray && typeof argsArray !== "string") { - const argValues: Array = (argsArray["values"] as Array) || []; + const argValues: Array = + (argsArray["values"] as Array) || []; for (const v of argValues) { if (v["stringValue"]) { args.push(v["stringValue"] as string); @@ -593,8 +595,7 @@ function parseContainerStatuses( return { name: getKvStringValue(kvList, "name"), ready: getKvStringValue(kvList, "ready") === "true", - restartCount: - parseInt(getKvStringValue(kvList, "restartCount")) || 0, + restartCount: parseInt(getKvStringValue(kvList, "restartCount")) || 0, state, image: getKvStringValue(kvList, "image"), }; @@ -618,10 +619,7 @@ export function parsePodObject( } const metadata: KubernetesObjectMetadata = parseMetadata(metadataKv); - const specKv: string | JSONObject | null = getKvValue( - objectKvList, - "spec", - ); + const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec"); const statusKv: string | JSONObject | null = getKvValue( objectKvList, "status", @@ -683,8 +681,9 @@ export function parsePodObject( | JSONObject | undefined; if (innerVal && innerVal["kvlistValue"]) { - const innerKv: JSONObject = - innerVal["kvlistValue"] as JSONObject; + const innerKv: JSONObject = innerVal[ + "kvlistValue" + ] as JSONObject; volSource = getKvStringValue(innerKv, "name") || getKvStringValue(innerKv, "path") || @@ -877,10 +876,12 @@ export function parseNodeObject( ); if (addrArray && typeof addrArray !== "string") { const addrItems: Array = getArrayValues(addrArray); - addresses = addrItems.map((a: JSONObject) => ({ - type: getKvStringValue(a, "type"), - address: getKvStringValue(a, "address"), - })); + addresses = addrItems.map((a: JSONObject) => { + return { + type: getKvStringValue(a, "type"), + address: getKvStringValue(a, "address"), + }; + }); } } @@ -911,10 +912,7 @@ export function parseDeploymentObject( return null; } - const specKv: string | JSONObject | null = getKvValue( - objectKvList, - "spec", - ); + const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec"); const statusKv: string | JSONObject | null = getKvValue( objectKvList, "status", @@ -953,8 +951,7 @@ export function parseDeploymentObject( let unavailableReplicas: number = 0; let conditions: Array = []; if (statusKv && typeof statusKv !== "string") { - statusReplicas = - parseInt(getKvStringValue(statusKv, "replicas")) || 0; + statusReplicas = parseInt(getKvStringValue(statusKv, "replicas")) || 0; readyReplicas = parseInt(getKvStringValue(statusKv, "readyReplicas")) || 0; availableReplicas = @@ -996,10 +993,7 @@ export function parseStatefulSetObject( return null; } - const specKv: string | JSONObject | null = getKvValue( - objectKvList, - "spec", - ); + const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec"); const statusKv: string | JSONObject | null = getKvValue( objectKvList, "status", @@ -1058,10 +1052,7 @@ export function parseDaemonSetObject( return null; } - const specKv: string | JSONObject | null = getKvValue( - objectKvList, - "spec", - ); + const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec"); const statusKv: string | JSONObject | null = getKvValue( objectKvList, "status", @@ -1099,9 +1090,8 @@ export function parseDaemonSetObject( ) || 0 : 0, numberReady: statusKv - ? parseInt( - getKvStringValue(statusKv as JSONObject, "numberReady"), - ) || 0 + ? parseInt(getKvStringValue(statusKv as JSONObject, "numberReady")) || + 0 : 0, numberMisscheduled: statusKv ? parseInt( @@ -1132,10 +1122,7 @@ export function parseJobObject( return null; } - const specKv: string | JSONObject | null = getKvValue( - objectKvList, - "spec", - ); + const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec"); const statusKv: string | JSONObject | null = getKvValue( objectKvList, "status", @@ -1145,19 +1132,14 @@ export function parseJobObject( metadata: parseMetadata(metadataKv), spec: { completions: specKv - ? parseInt( - getKvStringValue(specKv as JSONObject, "completions"), - ) || 0 + ? parseInt(getKvStringValue(specKv as JSONObject, "completions")) || 0 : 0, parallelism: specKv - ? parseInt( - getKvStringValue(specKv as JSONObject, "parallelism"), - ) || 0 + ? parseInt(getKvStringValue(specKv as JSONObject, "parallelism")) || 0 : 0, backoffLimit: specKv - ? parseInt( - getKvStringValue(specKv as JSONObject, "backoffLimit"), - ) || 0 + ? parseInt(getKvStringValue(specKv as JSONObject, "backoffLimit")) || + 0 : 0, }, status: { @@ -1165,9 +1147,7 @@ export function parseJobObject( ? parseInt(getKvStringValue(statusKv as JSONObject, "active")) || 0 : 0, succeeded: statusKv - ? parseInt( - getKvStringValue(statusKv as JSONObject, "succeeded"), - ) || 0 + ? parseInt(getKvStringValue(statusKv as JSONObject, "succeeded")) || 0 : 0, failed: statusKv ? parseInt(getKvStringValue(statusKv as JSONObject, "failed")) || 0 @@ -1205,10 +1185,7 @@ export function parseCronJobObject( return null; } - const specKv: string | JSONObject | null = getKvValue( - objectKvList, - "spec", - ); + const specKv: string | JSONObject | null = getKvValue(objectKvList, "spec"); const statusKv: string | JSONObject | null = getKvValue( objectKvList, "status", @@ -1220,10 +1197,9 @@ export function parseCronJobObject( schedule: specKv ? getKvStringValue(specKv as JSONObject, "schedule") : "", - suspend: - specKv - ? getKvStringValue(specKv as JSONObject, "suspend") === "true" - : false, + suspend: specKv + ? getKvStringValue(specKv as JSONObject, "suspend") === "true" + : false, concurrencyPolicy: specKv ? getKvStringValue(specKv as JSONObject, "concurrencyPolicy") : "", @@ -1237,10 +1213,7 @@ export function parseCronJobObject( : 0, failedJobsHistoryLimit: specKv ? parseInt( - getKvStringValue( - specKv as JSONObject, - "failedJobsHistoryLimit", - ), + getKvStringValue(specKv as JSONObject, "failedJobsHistoryLimit"), ) || 0 : 0, }, @@ -1315,8 +1288,10 @@ export function extractObjectFromLogBody( return objectVal; } - // If no "object" key, the kvlist might BE the object (pull mode) - // Check if it has typical K8s fields + /* + * If no "object" key, the kvlist might BE the object (pull mode) + * Check if it has typical K8s fields + */ const kind: string | JSONObject | null = getKvValue(topKvList, "kind"); if (kind) { return topKvList; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts index 13cb238408..fa6f626742 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesResourceUtils.ts @@ -43,31 +43,30 @@ export default class KubernetesResourceUtils { const endDate: Date = OneUptimeDate.getCurrentDate(); const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -hoursBack); - const cpuResult: AggregatedResult = - await AnalyticsModelAPI.aggregate({ - modelType: Metric, - aggregateBy: { - query: { - projectId: ProjectUtil.getCurrentProjectId()!, - time: new InBetween(startDate, endDate), - name: metricName, - attributes: { - "resource.k8s.cluster.name": clusterIdentifier, - ...filterAttributes, - } as Dictionary, - }, - aggregationType: MetricsAggregationType.Avg, - aggregateColumnName: "value", - aggregationTimestampColumnName: "time", - startTimestamp: startDate, - endTimestamp: endDate, - limit: LIMIT_PER_PROJECT, - skip: 0, - groupBy: { - attributes: true, - }, + const cpuResult: AggregatedResult = await AnalyticsModelAPI.aggregate({ + modelType: Metric, + aggregateBy: { + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + time: new InBetween(startDate, endDate), + name: metricName, + attributes: { + "resource.k8s.cluster.name": clusterIdentifier, + ...filterAttributes, + } as Dictionary, }, - }); + aggregationType: MetricsAggregationType.Avg, + aggregateColumnName: "value", + aggregationTimestampColumnName: "time", + startTimestamp: startDate, + endTimestamp: endDate, + limit: LIMIT_PER_PROJECT, + skip: 0, + groupBy: { + attributes: true, + }, + }, + }); const resourceMap: Map = new Map(); @@ -128,31 +127,30 @@ export default class KubernetesResourceUtils { ); try { - const memoryResult: AggregatedResult = - await AnalyticsModelAPI.aggregate({ - modelType: Metric, - aggregateBy: { - query: { - projectId: ProjectUtil.getCurrentProjectId()!, - time: new InBetween(startDate, endDate), - name: options.memoryMetricName, - attributes: { - "resource.k8s.cluster.name": options.clusterIdentifier, - ...(options.filterAttributes || {}), - } as Dictionary, - }, - aggregationType: MetricsAggregationType.Avg, - aggregateColumnName: "value", - aggregationTimestampColumnName: "time", - startTimestamp: startDate, - endTimestamp: endDate, - limit: LIMIT_PER_PROJECT, - skip: 0, - groupBy: { - attributes: true, - }, + const memoryResult: AggregatedResult = await AnalyticsModelAPI.aggregate({ + modelType: Metric, + aggregateBy: { + query: { + projectId: ProjectUtil.getCurrentProjectId()!, + time: new InBetween(startDate, endDate), + name: options.memoryMetricName, + attributes: { + "resource.k8s.cluster.name": options.clusterIdentifier, + ...(options.filterAttributes || {}), + } as Dictionary, }, - }); + aggregationType: MetricsAggregationType.Avg, + aggregateColumnName: "value", + aggregationTimestampColumnName: "time", + startTimestamp: startDate, + endTimestamp: endDate, + limit: LIMIT_PER_PROJECT, + skip: 0, + groupBy: { + attributes: true, + }, + }, + }); const memoryMap: Map = new Map(); @@ -179,7 +177,7 @@ export default class KubernetesResourceUtils { resource.memoryUsageBytes = memValue; } } - } catch (_err) { + } catch { // Memory data is optional, don't fail if not available } diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx index d111e8211e..e510c4f3bf 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/ContainerDetail.tsx @@ -140,7 +140,10 @@ const KubernetesClusterContainerDetail: FunctionComponent< children: (
- +
@@ -149,7 +152,10 @@ const KubernetesClusterContainerDetail: FunctionComponent< { name: "Logs", children: ( - + - + ), }, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index 20403c1e4a..31c458a1f4 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -71,22 +71,21 @@ const KubernetesClusterCronJobDetail: FunctionComponent< return; } - const fetchCronJobObject: () => Promise = - async (): Promise => { - setIsLoadingObject(true); - try { - const obj: KubernetesCronJobObject | null = - await fetchLatestK8sObject({ - clusterIdentifier: cluster.clusterIdentifier || "", - resourceType: "cronjobs", - resourceName: cronJobName, - }); - setCronJobObject(obj); - } catch { - // Graceful degradation — overview tab shows empty state - } - setIsLoadingObject(false); - }; + const fetchCronJobObject: () => Promise = async (): Promise => { + setIsLoadingObject(true); + try { + const obj: KubernetesCronJobObject | null = + await fetchLatestK8sObject({ + clusterIdentifier: cluster.clusterIdentifier || "", + resourceType: "cronjobs", + resourceName: cronJobName, + }); + setCronJobObject(obj); + } catch { + // Graceful degradation — overview tab shows empty state + } + setIsLoadingObject(false); + }; fetchCronJobObject().catch(() => {}); }, [cluster?.clusterIdentifier, cronJobName]); @@ -228,7 +227,10 @@ const KubernetesClusterCronJobDetail: FunctionComponent< { name: "Events", children: ( - + - + ), }, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index a1837e0ce0..3121929fc5 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -239,9 +239,7 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< title={`DaemonSet Metrics: ${daemonSetName}`} description="CPU and memory usage for pods in this daemonset over the last 6 hours." > - + ), }, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index 79498ff38f..847f9c881c 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -236,9 +236,7 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< title={`Deployment Metrics: ${deploymentName}`} description="CPU and memory usage for pods in this deployment over the last 6 hours." > - + ), }, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx index 3dacfb9327..235adc8dae 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Deployments.tsx @@ -85,7 +85,9 @@ const KubernetesClusterDeployments: FunctionComponent< resources={resources} getViewRoute={(resource: KubernetesResource) => { return RouteUtil.populateRouteParams( - RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL] as Route, + RouteMap[ + PageMap.KUBERNETES_CLUSTER_VIEW_DEPLOYMENT_DETAIL + ] as Route, { modelId: modelId, subModelId: new ObjectID(resource.name), diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx index 0302bc902a..27807636e7 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx @@ -24,10 +24,7 @@ import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; import { JSONObject } from "Common/Types/JSON"; import InBetween from "Common/Types/BaseDatabase/InBetween"; -import { - getKvValue, - getKvStringValue, -} from "../Utils/KubernetesObjectParser"; +import { getKvValue, getKvStringValue } from "../Utils/KubernetesObjectParser"; import { KubernetesEvent } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterEvents: FunctionComponent< @@ -131,12 +128,9 @@ const KubernetesClusterEvents: FunctionComponent< } const objectKvList: JSONObject = objectVal; - const eventType: string = - getKvStringValue(objectKvList, "type") || ""; - const reason: string = - getKvStringValue(objectKvList, "reason") || ""; - const note: string = - getKvStringValue(objectKvList, "note") || ""; + const eventType: string = getKvStringValue(objectKvList, "type") || ""; + const reason: string = getKvStringValue(objectKvList, "reason") || ""; + const note: string = getKvStringValue(objectKvList, "note") || ""; // Get regarding object details using shared parser const regardingKv: string | JSONObject | null = getKvValue( @@ -160,17 +154,11 @@ const KubernetesClusterEvents: FunctionComponent< "metadata", ); const metadataObj: JSONObject | undefined = - metadataKv && typeof metadataKv !== "string" - ? metadataKv - : undefined; + metadataKv && typeof metadataKv !== "string" ? metadataKv : undefined; const namespace: string = - (regardingObj - ? getKvStringValue(regardingObj, "namespace") - : "") || - (metadataObj - ? getKvStringValue(metadataObj, "namespace") - : "") || + (regardingObj ? getKvStringValue(regardingObj, "namespace") : "") || + (metadataObj ? getKvStringValue(metadataObj, "namespace") : "") || ""; if (eventType || reason) { @@ -254,10 +242,7 @@ const KubernetesClusterEvents: FunctionComponent< const isWarning: boolean = event.type.toLowerCase() === "warning"; return ( - + {event.timestamp} diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx index 2eb4239b35..8ed7927bf6 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -231,7 +231,10 @@ const KubernetesClusterJobDetail: FunctionComponent< { name: "Events", children: ( - + - + ), }, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index a576f9e460..e41802b9df 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -200,7 +200,10 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< { name: "Events", children: ( - + - + ), }, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx index b4e3d6b4ed..0bc447245c 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx @@ -23,7 +23,10 @@ import { Tab } from "Common/UI/Components/Tabs/Tab"; import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOverviewTab"; import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetricsTab"; -import { KubernetesNodeObject } from "../Utils/KubernetesObjectParser"; +import { + KubernetesCondition, + KubernetesNodeObject, +} from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; const KubernetesClusterNodeDetail: FunctionComponent< @@ -230,11 +233,10 @@ const KubernetesClusterNodeDetail: FunctionComponent< if (!nodeObject) { return { label: "Unknown", isReady: false }; } - const readyCondition = nodeObject.status.conditions.find( - (c) => { + const readyCondition: KubernetesCondition | undefined = + nodeObject.status.conditions.find((c: KubernetesCondition) => { return c.type === "Ready"; - }, - ); + }); if (readyCondition && readyCondition.status === "True") { return { label: "Ready", isReady: true }; } @@ -249,7 +251,7 @@ const KubernetesClusterNodeDetail: FunctionComponent< ]; if (nodeObject) { - const nodeStatus = getNodeStatus(); + const nodeStatus: { label: string; isReady: boolean } = getNodeStatus(); summaryFields.push( { diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx index dfed41d26a..79492d4309 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx @@ -294,7 +294,10 @@ const KubernetesClusterPodDetail: FunctionComponent< { name: "Events", children: ( - + + - + ), }, @@ -251,10 +249,7 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent<
- +
diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx index 352a60f552..54cd58b1fd 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSets.tsx @@ -85,7 +85,9 @@ const KubernetesClusterStatefulSets: FunctionComponent< resources={resources} getViewRoute={(resource: KubernetesResource) => { return RouteUtil.populateRouteParams( - RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL] as Route, + RouteMap[ + PageMap.KUBERNETES_CLUSTER_VIEW_STATEFULSET_DETAIL + ] as Route, { modelId: modelId, subModelId: new ObjectID(resource.name), diff --git a/App/FeatureSet/Docs/Index.ts b/App/FeatureSet/Docs/Index.ts index f95531a257..7c97deaccc 100755 --- a/App/FeatureSet/Docs/Index.ts +++ b/App/FeatureSet/Docs/Index.ts @@ -90,7 +90,10 @@ const DocsFeatureSet: FeatureSet = { }) .join("\n") : "- No IP addresses configured."; - contentInMarkdown = contentInMarkdown.replace("{{IP_WHITELIST}}", ipList); + contentInMarkdown = contentInMarkdown.replace( + "{{IP_WHITELIST}}", + ipList, + ); } // Render Markdown content to HTML diff --git a/Common/Server/API/IPWhitelistAPI.ts b/Common/Server/API/IPWhitelistAPI.ts index 07bfb23a28..4e9c150fee 100644 --- a/Common/Server/API/IPWhitelistAPI.ts +++ b/Common/Server/API/IPWhitelistAPI.ts @@ -10,24 +10,21 @@ export default class IPWhitelistAPI { public static init(): ExpressRouter { const router: ExpressRouter = Express.getRouter(); - router.get( - "/ip-whitelist", - (req: ExpressRequest, res: ExpressResponse) => { - const ipList: Array = IpWhitelist - ? IpWhitelist.split(",") - .map((ip: string) => { - return ip.trim(); - }) - .filter((ip: string) => { - return ip.length > 0; - }) - : []; + router.get("/ip-whitelist", (req: ExpressRequest, res: ExpressResponse) => { + const ipList: Array = IpWhitelist + ? IpWhitelist.split(",") + .map((ip: string) => { + return ip.trim(); + }) + .filter((ip: string) => { + return ip.length > 0; + }) + : []; - Response.sendJsonObjectResponse(req, res, { - ipWhitelist: ipList, - }); - }, - ); + Response.sendJsonObjectResponse(req, res, { + ipWhitelist: ipList, + }); + }); return router; } diff --git a/Home/Utils/PageSEO.ts b/Home/Utils/PageSEO.ts index f42e417373..8ea5e37d28 100644 --- a/Home/Utils/PageSEO.ts +++ b/Home/Utils/PageSEO.ts @@ -439,8 +439,7 @@ export const PageSEOConfig: Record = { }, "/product/scheduled-maintenance": { - title: - "Scheduled Maintenance | Plan & Communicate Downtime | OneUptime", + title: "Scheduled Maintenance | Plan & Communicate Downtime | OneUptime", description: "Plan, schedule, and communicate maintenance windows to your users. Notify subscribers automatically, update status pages in real-time. Open source maintenance management.", canonicalPath: "/product/scheduled-maintenance",