diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx index 680220ed7d..de5e83bd7c 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesEventsTab.tsx @@ -24,20 +24,26 @@ const KubernetesEventsTab: FunctionComponent = ( const [events, setEvents] = useState>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const [typeFilter, setTypeFilter] = useState<"all" | "warning" | "normal">( + "all", + ); 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, - }); + 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); }; @@ -67,55 +73,128 @@ const KubernetesEventsTab: FunctionComponent = ( ); } + const warningCount: number = events.filter( + (e: KubernetesEvent) => e.type.toLowerCase() === "warning", + ).length; + const normalCount: number = events.length - warningCount; + + const filteredEvents: Array = events.filter( + (e: KubernetesEvent) => { + if (typeFilter === "warning") { + return e.type.toLowerCase() === "warning"; + } + if (typeFilter === "normal") { + return e.type.toLowerCase() !== "warning"; + } + return true; + }, + ); + return ( -
- - - - - - - - - - - {events.map((event: KubernetesEvent, index: number) => { - const isWarning: boolean = event.type.toLowerCase() === "warning"; - return ( - - - + + + + + ); + }, + )} + +
- Time - - Type - - Reason - - Message -
- {event.timestamp} - - + {/* Summary and Filters */} +
+
+ {events.length} events + {warningCount > 0 && ( + + {" "} + ( + {warningCount} + {" "} + warning{warningCount !== 1 ? "s" : ""},{" "} + {normalCount}{" "} + normal) + + )} +
+
+ {(["all", "warning", "normal"] as const).map( + (filter: "all" | "warning" | "normal") => { + return ( + + ); + }, + )} +
+
+ +
+ + + + + + + + + + + {filteredEvents.map( + (event: KubernetesEvent, index: number) => { + const isWarning: boolean = + event.type.toLowerCase() === "warning"; + return ( + - {event.type} - - - - - - ); - })} - -
+ Time + + Type + + Reason + + Message +
- {event.reason} - - {event.message} -
+
+ {event.timestamp} + + + {event.type} + + + {event.reason} + + {event.message} +
+
+ {filteredEvents.length === 0 && ( +
+ No {typeFilter} events found. +
+ )} ); }; diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx index d1ef5073d4..e9e577e54d 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesMetricsTab.tsx @@ -13,7 +13,7 @@ const KubernetesMetricsTab: FunctionComponent = ( props: ComponentProps, ): ReactElement => { const endDate: Date = OneUptimeDate.getCurrentDate(); - const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6); + const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -1); const startAndEndDate: InBetween = new InBetween(startDate, endDate); const [metricViewData, setMetricViewData] = useState({ diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx index 3dc3eff1ce..b422a33d8d 100644 --- a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesOverviewTab.tsx @@ -1,10 +1,72 @@ -import React, { FunctionComponent, ReactElement } from "react"; +import React, { FunctionComponent, ReactElement, useState } 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"; +// Conditions where "True" means something is wrong +const negativeConditionTypes: Array = [ + "MemoryPressure", + "DiskPressure", + "PIDPressure", + "NetworkUnavailable", +]; + +function isConditionBad(condition: KubernetesCondition): boolean { + const isNegativeType: boolean = negativeConditionTypes.includes( + condition.type, + ); + if (isNegativeType) { + return condition.status === "True"; + } + // For positive conditions (Ready, Initialized, etc.), False is bad + return condition.status === "False"; +} + +function getConditionStatusColor(condition: KubernetesCondition): string { + const isNegativeType: boolean = negativeConditionTypes.includes( + condition.type, + ); + if (condition.status === "True") { + return isNegativeType + ? "bg-red-100 text-red-800" + : "bg-green-100 text-green-800"; + } + if (condition.status === "False") { + return isNegativeType + ? "bg-green-100 text-green-800" + : "bg-red-100 text-red-800"; + } + return "bg-yellow-100 text-yellow-800"; +} + +function formatRelativeTime(timestamp: string): string { + if (!timestamp) { + return "-"; + } + const date: Date = new Date(timestamp); + const now: Date = new Date(); + const diffMs: number = now.getTime() - date.getTime(); + if (diffMs < 0) { + return timestamp; + } + const diffSec: number = Math.floor(diffMs / 1000); + if (diffSec < 60) { + return `${diffSec}s ago`; + } + const diffMin: number = Math.floor(diffSec / 60); + if (diffMin < 60) { + return `${diffMin}m ago`; + } + const diffHrs: number = Math.floor(diffMin / 60); + if (diffHrs < 24) { + return `${diffHrs}h ago`; + } + const diffDays: number = Math.floor(diffHrs / 24); + return `${diffDays}d ago`; +} + export interface SummaryField { title: string; value: string | ReactElement; @@ -20,6 +82,37 @@ export interface ComponentProps { emptyMessage?: string | undefined; } +const ExpandableMessage: FunctionComponent<{ message: string }> = ( + msgProps: { message: string }, +): ReactElement => { + const [expanded, setExpanded] = useState(false); + const isLong: boolean = msgProps.message.length > 80; + + if (!msgProps.message || msgProps.message === "-") { + return -; + } + + if (!isLong) { + return {msgProps.message}; + } + + return ( +
+ + {expanded ? msgProps.message : msgProps.message.substring(0, 80) + "..."} + + +
+ ); +}; + const KubernetesOverviewTab: FunctionComponent = ( props: ComponentProps, ): ReactElement => { @@ -105,20 +198,18 @@ const KubernetesOverviewTab: FunctionComponent = ( {props.conditions.map( (condition: KubernetesCondition, index: number) => { + const isBad: boolean = isConditionBad(condition); return ( - + {condition.type} {condition.status} @@ -126,11 +217,19 @@ const KubernetesOverviewTab: FunctionComponent = ( {condition.reason || "-"} - - {condition.message || "-"} + + - {condition.lastTransitionTime || "-"} + + {formatRelativeTime( + condition.lastTransitionTime, + )} + ); diff --git a/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesYamlTab.tsx b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesYamlTab.tsx new file mode 100644 index 0000000000..5b7cfd8501 --- /dev/null +++ b/App/FeatureSet/Dashboard/src/Components/Kubernetes/KubernetesYamlTab.tsx @@ -0,0 +1,206 @@ +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; +import Card from "Common/UI/Components/Card/Card"; +import CodeEditor from "Common/UI/Components/CodeEditor/CodeEditor"; +import CodeType from "Common/Types/Code/CodeType"; +import PageLoader from "Common/UI/Components/Loader/PageLoader"; +import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage"; +import { + fetchLatestK8sObject, + KubernetesObjectType, +} from "../../Pages/Kubernetes/Utils/KubernetesObjectFetcher"; +import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button"; +import IconProp from "Common/Types/Icon/IconProp"; + +export interface ComponentProps { + clusterIdentifier: string; + resourceType: string; + resourceName: string; + namespace?: string | undefined; +} + +/** + * Convert a JavaScript object to YAML string. + */ +function toYaml(obj: unknown, indent: number = 0): string { + const prefix: string = " ".repeat(indent); + + if (obj === null || obj === undefined) { + return "null"; + } + + if (typeof obj === "string") { + // Quote strings that contain special chars or look like numbers + if ( + obj.includes(":") || + obj.includes("#") || + obj.includes("\n") || + obj.includes("'") || + obj.includes('"') || + obj === "" || + obj === "true" || + obj === "false" || + obj === "null" || + /^\d/.test(obj) + ) { + return `"${obj.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; + } + return obj; + } + + if (typeof obj === "number" || typeof obj === "boolean") { + return String(obj); + } + + if (Array.isArray(obj)) { + if (obj.length === 0) { + return "[]"; + } + const lines: Array = []; + for (const item of obj) { + if (typeof item === "object" && item !== null && !Array.isArray(item)) { + const entries: Array<[string, unknown]> = Object.entries( + item as Record, + ); + if (entries.length > 0) { + const [firstKey, firstVal] = entries[0]!; + lines.push( + `${prefix}- ${firstKey}: ${toYaml(firstVal, indent + 2)}`, + ); + for (let i: number = 1; i < entries.length; i++) { + const [key, val] = entries[i]!; + lines.push( + `${prefix} ${key}: ${toYaml(val, indent + 2)}`, + ); + } + } else { + lines.push(`${prefix}- {}`); + } + } else { + lines.push(`${prefix}- ${toYaml(item, indent + 1)}`); + } + } + return "\n" + lines.join("\n"); + } + + if (typeof obj === "object") { + const record: Record = obj as Record; + const keys: Array = Object.keys(record); + if (keys.length === 0) { + return "{}"; + } + const lines: Array = []; + for (const key of keys) { + const val: unknown = record[key]; + if ( + val !== null && + val !== undefined && + typeof val === "object" && + !Array.isArray(val) && + Object.keys(val as Record).length > 0 + ) { + lines.push(`${prefix}${key}:`); + lines.push(toYaml(val, indent + 1)); + } else if (Array.isArray(val) && val.length > 0) { + lines.push(`${prefix}${key}:${toYaml(val, indent + 1)}`); + } else { + lines.push(`${prefix}${key}: ${toYaml(val, indent + 1)}`); + } + } + return lines.join("\n"); + } + + return String(obj); +} + +const KubernetesYamlTab: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const [yamlContent, setYamlContent] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + const [copied, setCopied] = useState(false); + + useEffect(() => { + const fetchData: () => Promise = async (): Promise => { + setIsLoading(true); + setError(""); + try { + const result: KubernetesObjectType | null = + await fetchLatestK8sObject({ + clusterIdentifier: props.clusterIdentifier, + resourceType: props.resourceType, + resourceName: props.resourceName, + namespace: props.namespace, + }); + + if (result) { + const yaml: string = toYaml(result); + setYamlContent(yaml); + } else { + setYamlContent(""); + } + } catch { + setError("Failed to fetch resource data."); + } finally { + setIsLoading(false); + } + }; + fetchData(); + }, [ + props.clusterIdentifier, + props.resourceType, + props.resourceName, + props.namespace, + ]); + + if (isLoading) { + return ; + } + + if (error) { + return ; + } + + if (!yamlContent) { + return ( + + ); + } + + return ( + { + navigator.clipboard.writeText(yamlContent); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 2000); + }, + }, + ]} + > +
+ +
+
+ ); +}; + +export default KubernetesYamlTab; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts index b9adc3749c..eeabbd423a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectFetcher.ts @@ -426,6 +426,147 @@ export async function fetchK8sEventsForResource(options: { } } +/** + * Fetch recent warning events for an entire cluster. + */ +export async function fetchClusterWarningEvents(options: { + clusterIdentifier: string; + 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, -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 = []; + const maxEvents: number = options.limit || 10; + + for (const log of listResult.data) { + if (events.length >= maxEvents) { + break; + } + + const attrs: JSONObject = log.attributes || {}; + + 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; + } + + const objectVal: string | JSONObject | null = getKvValue( + topKvList, + "object", + ); + if (!objectVal || typeof objectVal === "string") { + continue; + } + const objectKvList: JSONObject = objectVal; + + const eventType: string = + getKvStringValue(objectKvList, "type") || ""; + + // Only include Warning events + if (eventType !== "Warning") { + continue; + } + + const reason: string = + getKvStringValue(objectKvList, "reason") || ""; + const note: string = + getKvStringValue(objectKvList, "note") || ""; + + 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", + ) || ""; + + events.push({ + timestamp: log.time + ? OneUptimeDate.getDateAsLocalFormattedString(log.time) + : "", + type: eventType, + 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). diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts index 99962bf156..07949d9270 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesObjectParser.ts @@ -232,6 +232,7 @@ export interface KubernetesPodObject { phase: string; podIP: string; hostIP: string; + qosClass: string; conditions: Array; containerStatuses: Array; initContainerStatuses: Array; @@ -779,6 +780,7 @@ export function parsePodObject( let phase: string = ""; let podIP: string = ""; let hostIP: string = ""; + let qosClass: string = ""; let conditions: Array = []; let containerStatuses: Array = []; let initContainerStatuses: Array = []; @@ -787,6 +789,7 @@ export function parsePodObject( phase = getKvStringValue(statusKv, "phase"); podIP = getKvStringValue(statusKv, "podIP"); hostIP = getKvStringValue(statusKv, "hostIP"); + qosClass = getKvStringValue(statusKv, "qosClass"); const condArray: string | JSONObject | null = getKvValue( statusKv, @@ -828,6 +831,7 @@ export function parsePodObject( phase, podIP, hostIP, + qosClass, conditions, containerStatuses, initContainerStatuses, diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx index a1bf7c2364..4e0c193f56 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/CronJobDetail.tsx @@ -28,6 +28,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesCronJobObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterCronJobDetail: FunctionComponent< PageComponentProps @@ -252,6 +253,17 @@ const KubernetesClusterCronJobDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx index 3303c18149..5f996864fa 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DaemonSetDetail.tsx @@ -28,6 +28,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesDaemonSetObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterDaemonSetDetail: FunctionComponent< PageComponentProps @@ -244,6 +245,17 @@ const KubernetesClusterDaemonSetDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx index 886fcb1a39..c4243c03ae 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/DeploymentDetail.tsx @@ -28,6 +28,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesDeploymentObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterDeploymentDetail: FunctionComponent< PageComponentProps @@ -173,30 +174,84 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< ]; if (objectData) { + const desired: number = objectData.spec.replicas; + const ready: number = objectData.status.readyReplicas ?? 0; + const available: number = objectData.status.availableReplicas ?? 0; + const unavailable: number = objectData.status.unavailableReplicas ?? 0; + const isFullyRolledOut: boolean = + ready === desired && unavailable === 0; + summaryFields.push( { title: "Namespace", value: objectData.metadata.namespace || "default", }, { - title: "Replicas", - value: String(objectData.spec.replicas ?? "N/A"), + title: "Rollout Status", + value: ( +
+
+ + {isFullyRolledOut ? "Complete" : "In Progress"} + + + {ready}/{desired} ready + +
+
+
0 ? (ready / desired) * 100 : 0}%`, + }} + /> +
+
+ ), + }, + { + title: "Desired Replicas", + value: String(desired), }, { title: "Ready Replicas", - value: String(objectData.status.readyReplicas ?? "N/A"), + value: String(ready), }, { - title: "Available Replicas", - value: String(objectData.status.availableReplicas ?? "N/A"), + title: "Available", + value: String(available), }, + ); + + if (unavailable > 0) { + summaryFields.push({ + title: "Unavailable", + value: ( + + {String(unavailable)} + + ), + }); + } + + summaryFields.push( { title: "Strategy", value: objectData.spec.strategy || "N/A", }, { title: "Created", - value: objectData.metadata.creationTimestamp || "N/A", + value: objectData.metadata.creationTimestamp + ? KubernetesResourceUtils.formatAge( + objectData.metadata.creationTimestamp, + ) + : "N/A", }, ); } @@ -241,6 +296,17 @@ const KubernetesClusterDeploymentDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx index 27807636e7..5c8b7c947b 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx @@ -36,6 +36,11 @@ const KubernetesClusterEvents: FunctionComponent< const [events, setEvents] = useState>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(""); + const [typeFilter, setTypeFilter] = useState<"all" | "warning" | "normal">( + "all", + ); + const [namespaceFilter, setNamespaceFilter] = useState("all"); + const [searchText, setSearchText] = useState(""); const fetchData: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -201,17 +206,144 @@ const KubernetesClusterEvents: FunctionComponent< return ; } + // Compute filter options + const namespaces: Array = Array.from( + new Set(events.map((e: KubernetesEvent) => e.namespace)), + ).sort(); + + const warningCount: number = events.filter( + (e: KubernetesEvent) => e.type.toLowerCase() === "warning", + ).length; + const normalCount: number = events.length - warningCount; + + // Apply filters + const filteredEvents: Array = events.filter( + (e: KubernetesEvent) => { + if ( + typeFilter === "warning" && + e.type.toLowerCase() !== "warning" + ) { + return false; + } + if ( + typeFilter === "normal" && + e.type.toLowerCase() === "warning" + ) { + return false; + } + if (namespaceFilter !== "all" && e.namespace !== namespaceFilter) { + return false; + } + if (searchText.trim()) { + const search: string = searchText.toLowerCase(); + return ( + e.message.toLowerCase().includes(search) || + e.reason.toLowerCase().includes(search) || + e.objectName.toLowerCase().includes(search) || + e.objectKind.toLowerCase().includes(search) + ); + } + return true; + }, + ); + return ( + {/* Event Summary Banner */} +
+
+ + {events.length} + {" "} + total events +
+ {warningCount > 0 && ( + + {warningCount} Warning{warningCount !== 1 ? "s" : ""} + + )} + + {normalCount} Normal + +
+ + {/* Filters Row */} +
+ {/* Type Filter Buttons */} +
+ {(["all", "warning", "normal"] as const).map( + (filter: "all" | "warning" | "normal") => { + return ( + + ); + }, + )} +
+ + {/* Namespace Filter */} + + + {/* Text Search */} + ) => { + setSearchText(e.target.value); + }} + className="px-3 py-1.5 text-xs rounded-md border border-gray-200 bg-white text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-indigo-500 w-64" + /> + + {/* Results Count */} + + Showing {filteredEvents.length} of {events.length} + +
+ {events.length === 0 ? ( -

+

No Kubernetes events found in the last 24 hours. Events will appear here once the kubernetes-agent is sending data.

+ ) : filteredEvents.length === 0 ? ( +

+ No events match the current filters. +

) : (
@@ -238,40 +370,47 @@ const KubernetesClusterEvents: FunctionComponent< - {events.map((event: KubernetesEvent, index: number) => { - const isWarning: boolean = - event.type.toLowerCase() === "warning"; - return ( - - - - - - - - - ); - })} + {filteredEvents.map( + (event: KubernetesEvent, index: number) => { + const isWarning: boolean = + event.type.toLowerCase() === "warning"; + return ( + + + + + + + + + ); + }, + )}
- {event.timestamp} - - - {event.type} - - - {event.reason} - - {event.objectKind}/{event.objectName} - - {event.namespace} - - {event.message} -
+ {event.timestamp} + + + {event.type} + + + {event.reason} + + {event.objectKind}/{event.objectName} + + + {event.namespace} + + + {event.message} +
diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx index fcca850efe..babe0e07d2 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx @@ -27,12 +27,15 @@ import KubernetesResourceUtils, { } from "../Utils/KubernetesResourceUtils"; import { fetchK8sObjectsBatch, + fetchClusterWarningEvents, KubernetesObjectType, + KubernetesEvent, } from "../Utils/KubernetesObjectFetcher"; import { KubernetesPodObject, KubernetesNodeObject, } from "../Utils/KubernetesObjectParser"; +import OneUptimeDate from "Common/Types/Date"; interface ResourceLink { title: string; @@ -64,6 +67,18 @@ const KubernetesClusterOverview: FunctionComponent< const [clusterHealth, setClusterHealth] = useState< "Healthy" | "Degraded" | "Unhealthy" >("Healthy"); + const [topCpuPods, setTopCpuPods] = useState>([]); + const [topMemoryPods, setTopMemoryPods] = useState< + Array + >([]); + const [recentWarnings, setRecentWarnings] = useState< + Array + >([]); + const [nodePressure, setNodePressure] = useState<{ + memoryPressure: number; + diskPressure: number; + pidPressure: number; + }>({ memoryPressure: 0, diskPressure: 0, pidPressure: 0 }); const fetchCluster: PromiseVoidFunction = async (): Promise => { setIsLoading(true); @@ -94,10 +109,11 @@ const KubernetesClusterOverview: FunctionComponent< resourceNameAttribute: "resource.k8s.node.name", namespaceAttribute: "resource.k8s.node.name", }), - KubernetesResourceUtils.fetchResourceList({ + KubernetesResourceUtils.fetchResourceListWithMemory({ clusterIdentifier: item.clusterIdentifier, metricName: "k8s.pod.cpu.utilization", resourceNameAttribute: "resource.k8s.pod.name", + memoryMetricName: "k8s.pod.memory.usage", }), KubernetesResourceUtils.fetchResourceList({ clusterIdentifier: item.clusterIdentifier, @@ -111,6 +127,32 @@ const KubernetesClusterOverview: FunctionComponent< setPodCount(pods.length); setNamespaceCount(namespaces.length); + // Top resource consumers + const sortedByCpu: Array = [...pods] + .filter( + (p: KubernetesResource) => + p.cpuUtilization !== null && p.cpuUtilization !== undefined, + ) + .sort( + (a: KubernetesResource, b: KubernetesResource) => + (b.cpuUtilization ?? 0) - (a.cpuUtilization ?? 0), + ) + .slice(0, 5); + setTopCpuPods(sortedByCpu); + + const sortedByMemory: Array = [...pods] + .filter( + (p: KubernetesResource) => + p.memoryUsageBytes !== null && + p.memoryUsageBytes !== undefined, + ) + .sort( + (a: KubernetesResource, b: KubernetesResource) => + (b.memoryUsageBytes ?? 0) - (a.memoryUsageBytes ?? 0), + ) + .slice(0, 5); + setTopMemoryPods(sortedByMemory); + // Fetch pod and node objects for health status try { const [podObjects, nodeObjects]: [ @@ -149,9 +191,12 @@ const KubernetesClusterOverview: FunctionComponent< } setPodHealthSummary({ running, pending, failed, succeeded }); - // Calculate node health + // Calculate node health and pressure let ready: number = 0; let notReady: number = 0; + let memPressure: number = 0; + let diskPressure: number = 0; + let pidPressure: number = 0; for (const nodeObj of nodeObjects.values()) { const node: KubernetesNodeObject = @@ -165,8 +210,34 @@ const KubernetesClusterOverview: FunctionComponent< } else { notReady++; } + // Check pressure conditions + for (const cond of node.status.conditions) { + if ( + cond.type === "MemoryPressure" && + cond.status === "True" + ) { + memPressure++; + } + if ( + cond.type === "DiskPressure" && + cond.status === "True" + ) { + diskPressure++; + } + if ( + cond.type === "PIDPressure" && + cond.status === "True" + ) { + pidPressure++; + } + } } setNodeHealthSummary({ ready, notReady }); + setNodePressure({ + memoryPressure: memPressure, + diskPressure: diskPressure, + pidPressure: pidPressure, + }); // Determine overall health if (failed > 0 || notReady > 0) { @@ -179,6 +250,18 @@ const KubernetesClusterOverview: FunctionComponent< } catch { // Health data is supplementary, don't fail } + + // Fetch recent warning events + try { + const warnings: Array = + await fetchClusterWarningEvents({ + clusterIdentifier: item.clusterIdentifier, + limit: 5, + }); + setRecentWarnings(warnings); + } catch { + // Warnings are supplementary + } } } catch (err) { setError(API.getFriendlyMessage(err)); @@ -467,6 +550,249 @@ const KubernetesClusterOverview: FunctionComponent<
+ {/* Node Pressure Indicators */} + {(nodePressure.memoryPressure > 0 || + nodePressure.diskPressure > 0 || + nodePressure.pidPressure > 0) && ( +
+
+ + Node Pressure Detected + +
+
+ {nodePressure.memoryPressure > 0 && ( + + {nodePressure.memoryPressure} node + {nodePressure.memoryPressure > 1 ? "s" : ""}: Memory + Pressure + + )} + {nodePressure.diskPressure > 0 && ( + + {nodePressure.diskPressure} node + {nodePressure.diskPressure > 1 ? "s" : ""}: Disk Pressure + + )} + {nodePressure.pidPressure > 0 && ( + + {nodePressure.pidPressure} node + {nodePressure.pidPressure > 1 ? "s" : ""}: PID Pressure + + )} +
+
+ )} + + {/* Pod Health Visual Breakdown */} + {podCount > 0 && ( + +
+
+ {podHealthSummary.running > 0 && ( +
+ )} + {podHealthSummary.succeeded > 0 && ( +
+ )} + {podHealthSummary.pending > 0 && ( +
+ )} + {podHealthSummary.failed > 0 && ( +
+ )} +
+
+
+ + + Running ({podHealthSummary.running}) + +
+ {podHealthSummary.succeeded > 0 && ( +
+ + + Succeeded ({podHealthSummary.succeeded}) + +
+ )} + {podHealthSummary.pending > 0 && ( +
+ + + Pending ({podHealthSummary.pending}) + +
+ )} + {podHealthSummary.failed > 0 && ( +
+ + + Failed ({podHealthSummary.failed}) + +
+ )} +
+
+ + )} + + {/* Top Resource Consumers */} + {(topCpuPods.length > 0 || topMemoryPods.length > 0) && ( + +
+ {/* Top CPU */} +
+

+ Top CPU Usage +

+
+ {topCpuPods.map( + (pod: KubernetesResource, index: number) => { + const pct: number = Math.min( + pod.cpuUtilization ?? 0, + 100, + ); + return ( +
+
+ {pod.name} +
+ + {pod.namespace} + +
+
80 ? "bg-red-500" : pct > 60 ? "bg-yellow-500" : "bg-green-500"}`} + style={{ width: `${pct}%` }} + /> +
+ + {KubernetesResourceUtils.formatCpuValue( + pod.cpuUtilization, + )} + +
+ ); + }, + )} +
+
+ {/* Top Memory */} +
+

+ Top Memory Usage +

+
+ {topMemoryPods.map( + (pod: KubernetesResource, index: number) => { + return ( +
+
+ {pod.name} +
+ + {pod.namespace} + +
+ + {KubernetesResourceUtils.formatMemoryValue( + pod.memoryUsageBytes, + )} + +
+
+ ); + }, + )} +
+
+
+ + )} + + {/* Recent Warning Events */} + {recentWarnings.length > 0 && ( + +
+
+ {recentWarnings.map( + (event: KubernetesEvent, index: number) => { + return ( +
+ + {event.reason} + +
+
+ {event.message} +
+
+ {event.objectKind}/{event.objectName} in{" "} + {event.namespace} · {event.timestamp} +
+
+
+ ); + }, + )} +
+
+ { + Navigation.navigate( + RouteUtil.populateRouteParams( + RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route, + { modelId: modelId }, + ), + ); + }} + className="text-sm text-indigo-600 hover:text-indigo-800 cursor-pointer font-medium" + > + View All Events → + +
+
+
+ )} + {/* 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 index 8829997b26..51d1db91a8 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/JobDetail.tsx @@ -28,6 +28,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesJobObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterJobDetail: FunctionComponent< PageComponentProps @@ -256,6 +257,17 @@ const KubernetesClusterJobDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx index cb6ee5e0c6..377518631a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Layout.tsx @@ -2,12 +2,22 @@ import { getKubernetesBreadcrumbs } from "../../../Utils/Breadcrumbs"; import { RouteUtil } from "../../../Utils/RouteMap"; import PageComponentProps from "../../PageComponentProps"; import SideMenu from "./SideMenu"; +import { ResourceCounts } from "./SideMenu"; import ObjectID from "Common/Types/ObjectID"; import ModelPage from "Common/UI/Components/Page/ModelPage"; import Navigation from "Common/UI/Utils/Navigation"; import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster"; -import React, { FunctionComponent, ReactElement } from "react"; +import React, { + FunctionComponent, + ReactElement, + useEffect, + useState, +} from "react"; import { Outlet, useParams } from "react-router-dom"; +import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI"; +import KubernetesResourceUtils, { + KubernetesResource, +} from "../Utils/KubernetesResourceUtils"; const KubernetesClusterViewLayout: FunctionComponent< PageComponentProps @@ -15,6 +25,105 @@ const KubernetesClusterViewLayout: FunctionComponent< const { id } = useParams(); const modelId: ObjectID = new ObjectID(id || ""); const path: string = Navigation.getRoutePath(RouteUtil.getRoutes()); + const [resourceCounts, setResourceCounts] = useState< + ResourceCounts | undefined + >(undefined); + + useEffect(() => { + const fetchCounts: () => Promise = async (): Promise => { + try { + const cluster: KubernetesCluster | null = await ModelAPI.getItem({ + modelType: KubernetesCluster, + id: modelId, + select: { clusterIdentifier: true }, + }); + + if (!cluster?.clusterIdentifier) { + return; + } + + const ci: string = cluster.clusterIdentifier; + + // Fetch counts for key resources in parallel + const [ + nodes, + pods, + namespaces, + deployments, + statefulSets, + daemonSets, + jobs, + cronJobs, + containers, + ]: Array> = await Promise.all([ + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.node.cpu.utilization", + resourceNameAttribute: "resource.k8s.node.name", + namespaceAttribute: "resource.k8s.node.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.pod.cpu.utilization", + resourceNameAttribute: "resource.k8s.pod.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.pod.cpu.utilization", + resourceNameAttribute: "resource.k8s.namespace.name", + namespaceAttribute: "resource.k8s.namespace.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.deployment.desired", + resourceNameAttribute: "resource.k8s.deployment.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.statefulset.desired_pods", + resourceNameAttribute: "resource.k8s.statefulset.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.daemonset.desired_scheduled_nodes", + resourceNameAttribute: "resource.k8s.daemonset.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.job.active_pods", + resourceNameAttribute: "resource.k8s.job.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "k8s.cronjob.active_jobs", + resourceNameAttribute: "resource.k8s.cronjob.name", + }), + KubernetesResourceUtils.fetchResourceList({ + clusterIdentifier: ci, + metricName: "container.cpu.utilization", + resourceNameAttribute: "resource.k8s.container.name", + }), + ]); + + setResourceCounts({ + nodes: nodes.length, + pods: pods.length, + namespaces: namespaces.length, + deployments: deployments.length, + statefulSets: statefulSets.length, + daemonSets: daemonSets.length, + jobs: jobs.length, + cronJobs: cronJobs.length, + containers: containers.length, + }); + } catch { + // Counts are supplementary, don't fail the layout + } + }; + + fetchCounts().catch(() => {}); + }, []); + return ( } + sideMenu={ + + } > diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx index 2b04608be5..c2fca50612 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NamespaceDetail.tsx @@ -28,6 +28,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesNamespaceObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterNamespaceDetail: FunctionComponent< PageComponentProps @@ -224,6 +225,16 @@ const KubernetesClusterNamespaceDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx index 8d52b25481..29fb5bc6ae 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/NodeDetail.tsx @@ -28,6 +28,7 @@ import { } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterNodeDetail: FunctionComponent< PageComponentProps @@ -257,6 +258,39 @@ const KubernetesClusterNodeDetail: FunctionComponent< if (nodeObject) { const nodeStatus: { label: string; isReady: boolean } = getNodeStatus(); + // Extract node roles from labels + const roles: Array = Object.keys( + nodeObject.metadata.labels, + ) + .filter((key: string) => { + return key.startsWith("node-role.kubernetes.io/"); + }) + .map((key: string) => { + return key.replace("node-role.kubernetes.io/", ""); + }); + + // Extract internal IP + const internalIP: string = + nodeObject.status.addresses.find( + (a: { type: string; address: string }) => { + return a.type === "InternalIP"; + }, + )?.address || "N/A"; + + // Check pressure conditions + const pressureConditions: Array = nodeObject.status.conditions + .filter((c: KubernetesCondition) => { + return ( + c.status === "True" && + (c.type === "MemoryPressure" || + c.type === "DiskPressure" || + c.type === "PIDPressure") + ); + }) + .map((c: KubernetesCondition) => { + return c.type; + }); + summaryFields.push( { title: "Status", @@ -272,14 +306,69 @@ const KubernetesClusterNodeDetail: FunctionComponent< ), }, + ); + + if (roles.length > 0) { + summaryFields.push({ + title: "Roles", + value: ( +
+ {roles.map((role: string) => { + return ( + + {role} + + ); + })} +
+ ), + }); + } + + summaryFields.push( + { title: "Internal IP", value: internalIP }, + ); + + if (pressureConditions.length > 0) { + summaryFields.push({ + title: "Pressure", + value: ( +
+ {pressureConditions.map((p: string) => { + return ( + + {p} + + ); + })} +
+ ), + }); + } + + summaryFields.push( + { + title: "CPU (Capacity / Allocatable)", + value: `${nodeObject.status.capacity["cpu"] || "N/A"} / ${nodeObject.status.allocatable["cpu"] || "N/A"}`, + }, + { + title: "Memory (Capacity / Allocatable)", + value: `${nodeObject.status.capacity["memory"] || "N/A"} / ${nodeObject.status.allocatable["memory"] || "N/A"}`, + }, + { + title: "Pods (Capacity)", + value: nodeObject.status.capacity["pods"] || "N/A", + }, { 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", @@ -290,19 +379,19 @@ const KubernetesClusterNodeDetail: FunctionComponent< }, { title: "Architecture", - value: nodeObject.status.nodeInfo.architecture || "N/A", + value: `${nodeObject.status.nodeInfo.operatingSystem || "N/A"}/${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: "Kernel", + value: nodeObject.status.nodeInfo.kernelVersion || "N/A", }, { title: "Created", - value: nodeObject.metadata.creationTimestamp || "N/A", + value: nodeObject.metadata.creationTimestamp + ? KubernetesResourceUtils.formatAge( + nodeObject.metadata.creationTimestamp, + ) + : "N/A", }, ); } @@ -354,6 +443,16 @@ const KubernetesClusterNodeDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail.tsx index a399e30f45..5a12c8c844 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVCDetail.tsx @@ -20,6 +20,7 @@ import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOver import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; import { KubernetesPVCObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterPVCDetail: FunctionComponent< PageComponentProps @@ -179,6 +180,17 @@ const KubernetesClusterPVCDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail.tsx index 15530f3350..c66df469ec 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PVDetail.tsx @@ -20,6 +20,7 @@ import KubernetesOverviewTab from "../../../Components/Kubernetes/KubernetesOver import KubernetesEventsTab from "../../../Components/Kubernetes/KubernetesEventsTab"; import { KubernetesPVObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterPVDetail: FunctionComponent< PageComponentProps @@ -177,6 +178,16 @@ const KubernetesClusterPVDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx index e4fd3f83bb..7fae62dc99 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx @@ -29,6 +29,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesPodObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterPodDetail: FunctionComponent< PageComponentProps @@ -223,6 +224,21 @@ const KubernetesClusterPodDetail: FunctionComponent< ]; if (podObject) { + // Compute restart count + const restartCount: number = podObject.status.containerStatuses.reduce( + (sum: number, cs: { restartCount: number }) => { + return sum + cs.restartCount; + }, + 0, + ); + + // Compute container images + const containerImages: Array = podObject.spec.containers.map( + (c: { image: string }) => { + return c.image; + }, + ); + summaryFields.push( { title: "Namespace", @@ -246,6 +262,24 @@ const KubernetesClusterPodDetail: FunctionComponent< ), }, + { + title: "QoS Class", + value: podObject.status.qosClass || "N/A", + }, + { + title: "Restarts", + value: ( + 0 + ? "text-yellow-700 font-medium" + : "text-gray-700" + } + > + {restartCount.toString()} + + ), + }, { 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" }, @@ -253,9 +287,30 @@ const KubernetesClusterPodDetail: FunctionComponent< title: "Service Account", value: podObject.spec.serviceAccountName || "default", }, + { + title: "Images", + value: ( +
+ {containerImages.map((img: string, idx: number) => { + return ( +
+ {img} +
+ ); + })} +
+ ), + }, { title: "Created", - value: podObject.metadata.creationTimestamp || "N/A", + value: podObject.metadata.creationTimestamp + ? KubernetesResourceUtils.formatAge( + podObject.metadata.creationTimestamp, + ) + : "N/A", }, ); } @@ -331,6 +386,17 @@ const KubernetesClusterPodDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx index 9c1c940277..360f74daee 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx @@ -8,13 +8,30 @@ import SideMenuItem from "Common/UI/Components/SideMenu/SideMenuItem"; import SideMenuSection from "Common/UI/Components/SideMenu/SideMenuSection"; import React, { FunctionComponent, ReactElement } from "react"; +export interface ResourceCounts { + namespaces?: number | undefined; + pods?: number | undefined; + deployments?: number | undefined; + statefulSets?: number | undefined; + daemonSets?: number | undefined; + jobs?: number | undefined; + cronJobs?: number | undefined; + nodes?: number | undefined; + containers?: number | undefined; + pvcs?: number | undefined; + pvs?: number | undefined; +} + export interface ComponentProps { modelId: ObjectID; + resourceCounts?: ResourceCounts | undefined; } const KubernetesClusterSideMenu: FunctionComponent = ( props: ComponentProps, ): ReactElement => { + const counts: ResourceCounts = props.resourceCounts || {}; + return ( @@ -50,6 +67,7 @@ const KubernetesClusterSideMenu: FunctionComponent = ( ), }} icon={IconProp.Folder} + badge={counts.namespaces} /> = ( ), }} icon={IconProp.Circle} + badge={counts.pods} /> = ( ), }} icon={IconProp.Layers} + badge={counts.deployments} /> = ( ), }} icon={IconProp.Database} + badge={counts.statefulSets} /> = ( ), }} icon={IconProp.Settings} + badge={counts.daemonSets} /> = ( ), }} icon={IconProp.Play} + badge={counts.jobs} /> = ( ), }} icon={IconProp.Clock} + badge={counts.cronJobs} /> @@ -123,6 +147,7 @@ const KubernetesClusterSideMenu: FunctionComponent = ( ), }} icon={IconProp.Server} + badge={counts.nodes} /> = ( ), }} icon={IconProp.Cube} + badge={counts.containers} /> = ( ), }} icon={IconProp.Disc} + badge={counts.pvcs} /> = ( ), }} icon={IconProp.Disc} + badge={counts.pvs} /> diff --git a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx index 77fd8dc33f..0f4a1ddb12 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/StatefulSetDetail.tsx @@ -28,6 +28,7 @@ import KubernetesMetricsTab from "../../../Components/Kubernetes/KubernetesMetri import { KubernetesStatefulSetObject } from "../Utils/KubernetesObjectParser"; import { fetchLatestK8sObject } from "../Utils/KubernetesObjectFetcher"; import KubernetesResourceUtils from "../Utils/KubernetesResourceUtils"; +import KubernetesYamlTab from "../../../Components/Kubernetes/KubernetesYamlTab"; const KubernetesClusterStatefulSetDetail: FunctionComponent< PageComponentProps @@ -244,6 +245,17 @@ const KubernetesClusterStatefulSetDetail: FunctionComponent< ), }, + { + name: "YAML", + children: ( + + ), + }, ]; return {}} />; diff --git a/Common/Types/Code/CodeType.ts b/Common/Types/Code/CodeType.ts index 8dfaa2fad3..b146855448 100644 --- a/Common/Types/Code/CodeType.ts +++ b/Common/Types/Code/CodeType.ts @@ -6,7 +6,7 @@ enum CodeType { Markdown = "markdown", SQL = "sql", Text = "text", - // TODO add more mime types. + YAML = "yaml", } export default CodeType;