diff --git a/libs/domains/observability/feature/src/lib/database/database-rds-dashboard/util-chart/process-metrics-data.ts b/libs/domains/observability/feature/src/lib/database/database-rds-dashboard/util-chart/process-metrics-data.ts index 86a76be3626..6154d865b7c 100644 --- a/libs/domains/observability/feature/src/lib/database/database-rds-dashboard/util-chart/process-metrics-data.ts +++ b/libs/domains/observability/feature/src/lib/database/database-rds-dashboard/util-chart/process-metrics-data.ts @@ -1,6 +1,10 @@ -import { type RdsMetricData } from '../hooks/use-rds-metrics/use-rds-metrics' import { formatTimestamp } from './format-timestamp' +interface RdsMetricData { + metric: Record + values: [number, string][] +} + // Generic helper function to process metrics data export function processMetricsData( metricsData: { data?: { result: RdsMetricData[] } } | undefined, diff --git a/libs/domains/observability/feature/src/lib/lazy-chart/lazy-chart.tsx b/libs/domains/observability/feature/src/lib/lazy-chart/lazy-chart.tsx new file mode 100644 index 00000000000..d64620ddbb6 --- /dev/null +++ b/libs/domains/observability/feature/src/lib/lazy-chart/lazy-chart.tsx @@ -0,0 +1,30 @@ +import { type PropsWithChildren, useEffect, useRef, useState } from 'react' + +export function LazyChart({ children, className }: PropsWithChildren<{ className?: string }>) { + const ref = useRef(null) + const [inView, setInView] = useState(false) + + useEffect(() => { + const el = ref.current + if (!el) return + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setInView(true) + observer.disconnect() + } + }, + { rootMargin: '300px' } + ) + + observer.observe(el) + return () => observer.disconnect() + }, []) + + return ( +
+ {inView ? children :
} +
+ ) +} diff --git a/libs/domains/observability/feature/src/lib/local-chart/event-sidebar.tsx b/libs/domains/observability/feature/src/lib/local-chart/event-sidebar.tsx index 3dc5ff43f5b..ca09f2127bf 100644 --- a/libs/domains/observability/feature/src/lib/local-chart/event-sidebar.tsx +++ b/libs/domains/observability/feature/src/lib/local-chart/event-sidebar.tsx @@ -73,15 +73,15 @@ export function EventSidebar({ events, service, isLoading = false }: EventSideba {event.reason} {timestamp.fullTimeString}
- {event.type === 'event' && ( - <> - - {service?.serviceType === 'CONTAINER' ? 'Image name' : 'Repository'}: {event.repository} - - - {service?.serviceType === 'CONTAINER' ? 'Tag' : 'Version'}: {event.version?.slice(0, 8)} - - + {event.type === 'event' && event.repository && ( + + {service?.serviceType === 'CONTAINER' ? 'Image name' : 'Repository'}: {event.repository} + + )} + {event.type === 'event' && event.version && ( + + {service?.serviceType === 'CONTAINER' ? 'Tag' : 'Version'}: {event.version.slice(0, 8)} + )} {event.description && {event.description}} {event.type === 'exit-code' && ( diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.spec.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.spec.tsx index 31c6a97b113..50f3a9b6918 100644 --- a/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.spec.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.spec.tsx @@ -178,7 +178,7 @@ describe('CardInstanceStatus', () => { expect(useInstantMetrics).toHaveBeenCalledTimes(2) expect(useInstantMetrics).toHaveBeenCalledWith({ clusterId: 'test-cluster-id', - query: expect.stringContaining('kube_pod_container_status_restarts_total'), + query: expect.stringContaining('k8s_event_logger_q_k8s_events_total'), startTimestamp: expect.any(String), endTimestamp: expect.any(String), boardShortName: 'service_overview', @@ -186,8 +186,8 @@ describe('CardInstanceStatus', () => { }) const call = useInstantMetrics.mock.calls[0][0].query - expect(call).toContain('test-container-name') - expect(call).toContain('kube_pod_container_status_waiting_reason') + expect(call).toContain('test-service-id') + expect(call).toContain('k8s_event_logger_q_k8s_events_total') }) it('should always show modal link', () => { diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.tsx index 9e8af8d5752..278aed9e1ae 100644 --- a/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-instance-status/card-instance-status.tsx @@ -10,19 +10,34 @@ import { useDashboardContext } from '../../../util-filter/dashboard-context' import { CardMetricButton } from '../card-metric/card-metric' import { InstanceStatusChart } from '../instance-status-chart/instance-status-chart' -const query = (timeRange: string, selector: string) => ` - sum(increase(kube_pod_container_status_restarts_total{${selector}}[${timeRange}])) - + - count( - max by (pod) ( - max_over_time( - kube_pod_container_status_waiting_reason{ - ${selector}, - reason!~"ContainerCreating|PodInitializing|Completed" - }[${timeRange}] - ) - ) > 0 - ) +const query = (timeRange: string, subQueryTimeRange: string, selector: string, serviceId: string) => ` + (sum by () ( + sum_over_time( + ( + sum by (pod, reason) ( + clamp_max( + clamp_min( + ( + k8s_event_logger_q_k8s_events_total{ + qovery_com_service_id="${serviceId}", + reason=~"Failed|OOMKilled|BackOff|Evicted|FailedScheduling|FailedMount|FailedAttachVolume|Preempted|NodeNotReady|Killing", + type="Warning" + } + - + k8s_event_logger_q_k8s_events_total{ + qovery_com_service_id="${serviceId}", + reason=~"Failed|OOMKilled|BackOff|Evicted|FailedScheduling|FailedMount|FailedAttachVolume|Preempted|NodeNotReady|Killing", + type="Warning" + } offset ${subQueryTimeRange} + ), + 0 + ), + 1 + ) + ) + )[${timeRange}:${subQueryTimeRange}] + ) + ) or vector(0)) ` // TODO PG think to use recorder rule @@ -75,7 +90,7 @@ export function CardInstanceStatus({ const { data: service } = useService({ serviceId }) const { data: metricsInstanceErrors, isLoading: isLoadingMetricsInstanceErrors } = useInstantMetrics({ clusterId, - query: query(queryTimeRange, selector), + query: query(queryTimeRange, subQueryTimeRange, selector, serviceId), startTimestamp, endTimestamp, boardShortName: 'service_overview', @@ -100,8 +115,11 @@ export function CardInstanceStatus({ const isLoading = isLoadingMetricsInstanceErrors || isLoadingMetricsAutoscalingReached const isAutoscalingEnabled = match(service) - .with({ serviceType: 'APPLICATION' }, (s) => s.max_running_instances !== s.min_running_instances) - .with({ serviceType: 'CONTAINER' }, (s) => s.max_running_instances !== s.min_running_instances) + .with( + { serviceType: 'APPLICATION' }, + { serviceType: 'CONTAINER' }, + (s) => s.max_running_instances !== s.min_running_instances + ) .otherwise(() => false) return ( diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/card-node-events/card-node-events.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-node-events/card-node-events.tsx new file mode 100644 index 00000000000..5164203aaf0 --- /dev/null +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-node-events/card-node-events.tsx @@ -0,0 +1,139 @@ +import { useMemo } from 'react' +import { Heading, Icon, Section, Skeleton, Tooltip } from '@qovery/shared/ui' +import { useMetrics } from '../../../hooks/use-metrics/use-metrics' +import { formatTimestamp } from '../../../util-chart/format-timestamp' +import { useDashboardContext } from '../../../util-filter/dashboard-context' + +const REASON_COLOR: Record = { + // Errors + DiskPressure: 'text-red-500', + MemoryPressure: 'text-red-500', + PIDPressure: 'text-red-500', + NodeNotReady: 'text-red-500', + FailedDraining: 'text-red-500', + DeletingNodeFailed: 'text-red-500', + InvalidDiskCapacity: 'text-red-500', + TerminationGracePeriodExpiring: 'text-red-500', + // Karpenter lifecycle + Launched: 'text-green-500', + DisruptionLaunching: 'text-green-500', + Finalized: 'text-orange-500', + DisruptionTerminating: 'text-orange-500', + InstanceTerminating: 'text-orange-500', + DeletingNode: 'text-orange-500', +} + +interface NodeEvent { + key: string + timestamp: number + fullTime: string + objectKind: string + reason: string +} + +export function CardNodeEvents({ clusterId }: { clusterId: string }) { + const { startTimestamp, endTimestamp, useLocalTime, timeRange, subQueryTimeRange } = useDashboardContext() + + const { data: metrics, isLoading } = useMetrics({ + clusterId, + startTimestamp, + endTimestamp, + timeRange, + query: `sum by (reason, object_kind) ( + clamp_max(clamp_min( + k8s_event_logger_q_k8s_events_total{ + object_kind=~"Node|NodeClaim", + reason=~"DiskPressure|MemoryPressure|PIDPressure|NodeNotReady|FailedDraining|DeletingNodeFailed|InvalidDiskCapacity|TerminationGracePeriodExpiring|Launched|DisruptionLaunching|Finalized|DisruptionTerminating|InstanceTerminating|DeletingNode" + } + - k8s_event_logger_q_k8s_events_total{ + object_kind=~"Node|NodeClaim", + reason=~"DiskPressure|MemoryPressure|PIDPressure|NodeNotReady|FailedDraining|DeletingNodeFailed|InvalidDiskCapacity|TerminationGracePeriodExpiring|Launched|DisruptionLaunching|Finalized|DisruptionTerminating|InstanceTerminating|DeletingNode" + } offset ${subQueryTimeRange}, + 0), 1) + )`, + boardShortName: 'service_overview', + metricShortName: 'node_events', + }) + + const events = useMemo((): NodeEvent[] => { + if (!metrics?.data?.result) return [] + + const result: NodeEvent[] = [] + + for (const series of metrics.data.result) { + const reason = series.metric?.reason as string + const objectKind = (series.metric?.object_kind as string) ?? '' + const values = series.values as [number, string][] + + for (let i = 0; i < values.length; i++) { + if (parseFloat(values[i][1]) > 0) { + const timestamp = values[i][0] * 1000 + const { fullTimeString } = formatTimestamp(timestamp, useLocalTime) + result.push({ + key: `${reason}-${objectKind}-${timestamp}`, + timestamp, + fullTime: fullTimeString, + objectKind, + reason, + }) + } + } + } + + return result.sort((a, b) => b.timestamp - a.timestamp).slice(0, 50) + }, [metrics, useLocalTime]) + + if (isLoading) { + return ( +
+
+ +
+
+ + + +
+
+ ) + } + + if (events.length === 0) return null + + return ( +
+
+ Node infrastructure events + + + + + +
+
+ + + + + + + + + + {events.map((event) => ( + + + + + + ))} + +
TimeKindReason
{event.fullTime}{event.objectKind} + {event.reason} +
+
+
+ ) +} + +export default CardNodeEvents diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/card-node-status/card-node-status.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-node-status/card-node-status.tsx new file mode 100644 index 00000000000..da27cb15468 --- /dev/null +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/card-node-status/card-node-status.tsx @@ -0,0 +1,42 @@ +import { useState } from 'react' +import { Heading, Icon, Section, Tooltip } from '@qovery/shared/ui' +import { ModalChart } from '../../../modal-chart/modal-chart' +import { CardMetricButton } from '../card-metric/card-metric' +import { NodeStatusChart } from '../node-status-chart/node-status-chart' + +const title = 'Node count' +const description = 'Number of ready and not-ready nodes in the cluster over time, with scale up/down events.' + +export function CardNodeStatus({ clusterId, serviceId }: { clusterId: string; serviceId: string }) { + const [isModalOpen, setIsModalOpen] = useState(false) + + return ( + <> +
+
+
+ {title} + + + + + +
+ setIsModalOpen(true)} hasModalLink /> +
+
+ +
+
+ {isModalOpen && ( + +
+ +
+
+ )} + + ) +} + +export default CardNodeStatus diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/cpu-throttling-chart/cpu-throttling-chart.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/cpu-throttling-chart/cpu-throttling-chart.tsx new file mode 100644 index 00000000000..714d5f7971e --- /dev/null +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/cpu-throttling-chart/cpu-throttling-chart.tsx @@ -0,0 +1,108 @@ +import { useMemo } from 'react' +import { Line } from 'recharts' +import { usePodColor } from '@qovery/shared/util-hooks' +import { calculateRateInterval, useMetrics } from '../../../hooks/use-metrics/use-metrics' +import { LocalChart } from '../../../local-chart/local-chart' +import { addTimeRangePadding } from '../../../util-chart/add-time-range-padding' +import { buildPromSelector } from '../../../util-chart/build-selector' +import { convertPodName } from '../../../util-chart/convert-pod-name' +import { processMetricsData } from '../../../util-chart/process-metrics-data' +import { useDashboardContext } from '../../../util-filter/dashboard-context' + +const queryThrottleByPod = (rateInterval: string, selector: string) => ` + ( + sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{${selector}}[${rateInterval}])) + / + (sum by (pod) (rate(container_cpu_cfs_periods_total{${selector}}[${rateInterval}])) > 0) + ) * 100 +` + +export function CpuThrottlingChart({ + clusterId, + serviceId, + containerName, + podNames, +}: { + clusterId: string + serviceId: string + containerName: string + podNames?: string[] +}) { + const { startTimestamp, endTimestamp, useLocalTime, timeRange } = useDashboardContext() + const getColorByPod = usePodColor() + + const rateInterval = useMemo( + () => calculateRateInterval(startTimestamp, endTimestamp), + [startTimestamp, endTimestamp] + ) + + const selector = useMemo(() => buildPromSelector(containerName, podNames), [containerName, podNames]) + + const { data: throttleMetrics, isLoading } = useMetrics({ + clusterId, + startTimestamp, + endTimestamp, + timeRange, + query: queryThrottleByPod(rateInterval, selector), + boardShortName: 'service_overview', + metricShortName: 'cpu_throttle_by_pod', + }) + + const { chartData, seriesNames } = useMemo(() => { + const timeSeriesMap = new Map< + number, + { timestamp: number; time: string; fullTime: string; [key: string]: string | number | null } + >() + + processMetricsData( + throttleMetrics, + timeSeriesMap, + (_, index) => convertPodName(throttleMetrics?.data?.result?.[index]?.metric?.pod || ''), + (value) => { + const v = parseFloat(value) + return isFinite(v) ? Math.min(v, 100) : 0 + }, + useLocalTime + ) + + const baseChartData = Array.from(timeSeriesMap.values()).sort((a, b) => a.timestamp - b.timestamp) + const names = (throttleMetrics?.data?.result ?? []).map((_: unknown, index: number) => + convertPodName(throttleMetrics!.data.result[index].metric.pod) + ) as string[] + + return { + chartData: addTimeRangePadding(baseChartData, startTimestamp, endTimestamp, useLocalTime), + seriesNames: names, + } + }, [throttleMetrics, useLocalTime, startTimestamp, endTimestamp]) + + return ( + + {seriesNames.map((name) => ( + + ))} + + ) +} + +export default CpuThrottlingChart diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/instance-status-chart/instance-status-chart.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/instance-status-chart/instance-status-chart.tsx index 8323d800f8f..2a1c46d9db8 100644 --- a/libs/domains/observability/feature/src/lib/service/service-dashboard/instance-status-chart/instance-status-chart.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/instance-status-chart/instance-status-chart.tsx @@ -36,13 +36,13 @@ const queryK8sEvent = (serviceId: string, dynamicRange: string) => ` ( k8s_event_logger_q_k8s_events_total{ qovery_com_service_id="${serviceId}", - reason=~"Failed|OOMKilled|BackOff|Evicted|FailedScheduling|FailedMount|FailedAttachVolume|Preempted|NodeNotReady", + reason=~"Failed|OOMKilled|BackOff|Evicted|FailedScheduling|FailedMount|FailedAttachVolume|Preempted|NodeNotReady|Killing", type="Warning" } - k8s_event_logger_q_k8s_events_total{ qovery_com_service_id="${serviceId}", - reason=~"Failed|OOMKilled|BackOff|Evicted|FailedScheduling|FailedMount|FailedAttachVolume|Preempted|NodeNotReady", + reason=~"Failed|OOMKilled|BackOff|Evicted|FailedScheduling|FailedMount|FailedAttachVolume|Preempted|NodeNotReady|Killing", type="Warning" } offset ${dynamicRange} ) > 0 @@ -108,7 +108,7 @@ const getDescriptionFromK8sEvent = (reason: string): string => { case 'BackOff': return 'Container restart is being delayed due to repeated failures (CrashLoopBackOff or image-pull back-off).' case 'Evicted': - return 'Pod was evicted from the node due to resource pressure or eviction policy.' + return 'Pod was evicted from the node due to resource pressure (CPU, memory, or disk) or Karpenter node consolidation.' case 'FailedScheduling': return 'Scheduler could not place the pod on any node (resource constraints or node selectors).' case 'FailedMount': @@ -119,6 +119,8 @@ const getDescriptionFromK8sEvent = (reason: string): string => { return 'Pod was pre-empted by another higher-priority pod.' case 'NodeNotReady': return 'The node hosting the pod became NotReady.' + case 'Killing': + return 'Container is being terminated by the Kubernetes node (kubelet). This can occur during node consolidation, resource pressure, or graceful shutdown.' default: return 'Unknown' } diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/modal-chart/modal-chart.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/modal-chart/modal-chart.tsx index 7c3a5d0b43d..6d91a258d56 100644 --- a/libs/domains/observability/feature/src/lib/service/service-dashboard/modal-chart/modal-chart.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/modal-chart/modal-chart.tsx @@ -1,8 +1,8 @@ import * as Dialog from '@radix-ui/react-dialog' import { type PropsWithChildren } from 'react' import { Button, Heading, Icon, InputSelectSmall, Section } from '@qovery/shared/ui' -import { SelectTimeRange } from '../service/service-dashboard/select-time-range/select-time-range' -import { useDashboardContext } from '../util-filter/dashboard-context' +import { useDashboardContext } from '../../../util-filter/dashboard-context' +import { SelectTimeRange } from '../select-time-range/select-time-range' interface ModalChartProps extends PropsWithChildren { title: string diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/node-status-chart/node-status-chart.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/node-status-chart/node-status-chart.tsx new file mode 100644 index 00000000000..22037f54185 --- /dev/null +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/node-status-chart/node-status-chart.tsx @@ -0,0 +1,153 @@ +import { useMemo } from 'react' +import { Area } from 'recharts' +import { useMetrics } from '../../../hooks/use-metrics/use-metrics' +import { LocalChart, type ReferenceLineEvent } from '../../../local-chart/local-chart' +import { addTimeRangePadding } from '../../../util-chart/add-time-range-padding' +import { processMetricsData } from '../../../util-chart/process-metrics-data' +import { useDashboardContext } from '../../../util-filter/dashboard-context' + +const queryTotalNodes = () => `count(kube_node_info)` + +const queryReadyNodes = () => `count(kube_node_status_condition{condition="Ready",status="true"} == 1)` + +export function NodeStatusChart({ + clusterId, + serviceId, + isFullscreen, +}: { + clusterId: string + serviceId: string + isFullscreen?: boolean +}) { + const { startTimestamp, endTimestamp, useLocalTime, timeRange } = useDashboardContext() + const showReferenceLines = + isFullscreen || timeRange === '5m' || timeRange === '15m' || timeRange === '30m' || timeRange === '1h' + + const { data: totalMetrics, isLoading: isLoadingTotal } = useMetrics({ + clusterId, + startTimestamp, + endTimestamp, + timeRange, + query: queryTotalNodes(), + boardShortName: 'service_overview', + metricShortName: 'node_status_total', + }) + + const { data: readyMetrics, isLoading: isLoadingReady } = useMetrics({ + clusterId, + startTimestamp, + endTimestamp, + timeRange, + query: queryReadyNodes(), + boardShortName: 'service_overview', + metricShortName: 'node_status_ready', + }) + + const chartData = useMemo(() => { + const timeSeriesMap = new Map< + number, + { timestamp: number; time: string; fullTime: string; [key: string]: string | number | null } + >() + + processMetricsData( + totalMetrics, + timeSeriesMap, + () => 'Total', + (value) => parseFloat(value), + useLocalTime + ) + processMetricsData( + readyMetrics, + timeSeriesMap, + () => 'Node ready', + (value) => parseFloat(value), + useLocalTime + ) + + for (const point of timeSeriesMap.values()) { + const total = point['Total'] as number | null + const ready = point['Node ready'] as number | null + if (total != null && ready != null) { + point['Node not ready'] = Math.max(0, total - ready) + } + } + + const baseChartData = Array.from(timeSeriesMap.values()).sort((a, b) => a.timestamp - b.timestamp) + return addTimeRangePadding(baseChartData, startTimestamp, endTimestamp, useLocalTime) + }, [totalMetrics, readyMetrics, useLocalTime, startTimestamp, endTimestamp]) + + const referenceLineData = useMemo((): ReferenceLineEvent[] => { + if (!totalMetrics?.data?.result?.[0]?.values) return [] + + const lines: ReferenceLineEvent[] = [] + const values = totalMetrics.data.result[0].values as [number, string][] + + for (let i = 1; i < values.length; i++) { + const prev = parseFloat(values[i - 1][1]) + const curr = parseFloat(values[i][1]) + if (!isFinite(prev) || !isFinite(curr) || curr === prev) continue + + const delta = curr - prev + const timestamp = values[i][0] * 1000 + + if (delta > 0) { + lines.push({ + type: 'event', + timestamp, + reason: `+${delta} node${delta > 1 ? 's' : ''}`, + icon: 'arrow-up', + color: 'var(--color-green-500)', + key: `node-up-${timestamp}`, + }) + } else { + lines.push({ + type: 'event', + timestamp, + reason: `${delta} node${Math.abs(delta) > 1 ? 's' : ''}`, + icon: 'arrow-down', + color: 'var(--color-red-500)', + key: `node-down-${timestamp}`, + }) + } + } + + return lines + }, [totalMetrics]) + + return ( + + + + + ) +} + +export default NodeStatusChart diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx index 451c7322725..a36a1ea837f 100644 --- a/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx +++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx @@ -13,15 +13,19 @@ import { useIngressName } from '../../hooks/use-ingress-name/use-ingress-name' import { useNamespace } from '../../hooks/use-namespace/use-namespace' import { usePodCount } from '../../hooks/use-pod-count/use-pod-count' import { usePodNames } from '../../hooks/use-pod-names/use-pod-names' +import { LazyChart } from '../../lazy-chart/lazy-chart' import { DashboardProvider, useDashboardContext } from '../../util-filter/dashboard-context' import { CardHTTPErrors } from './card-http-errors/card-http-errors' import { CardInstanceStatus } from './card-instance-status/card-instance-status' import { CardLogErrors } from './card-log-errors/card-log-errors' +import { CardNodeEvents } from './card-node-events/card-node-events' +import { CardNodeStatus } from './card-node-status/card-node-status' import { CardPercentile99 } from './card-percentile-99/card-percentile-99' import { CardPrivateHTTPErrors } from './card-private-http-errors/card-private-http-errors' import { CardPrivatePercentile99 } from './card-private-percentile-99/card-private-percentile-99' import { CardStorage } from './card-storage/card-storage' import { CpuChart } from './cpu-chart/cpu-chart' +import { CpuThrottlingChart } from './cpu-throttling-chart/cpu-throttling-chart' import { DiskChart } from './disk-chart/disk-chart' import { MemoryChart } from './memory-chart/memory-chart' import { NetworkRequestDurationChart } from './network-request-duration-chart/network-request-duration-chart' @@ -292,121 +296,144 @@ function ServiceDashboardContent() { -
-
- Resources - {!resourcesModeLoading && resourcesMode && ( - - - - - {resourcesMode === 'aggregate' ? 'Aggregated view' : 'Pod-level view'} - - - - )} -
-
-
- -
-
- -
- {hasStorage && ( -
- -
- )} -
-
- {hasPublicPort && ( +
- Network + Node infrastructure
-
- -
-
- -
-
- -
+ +
- )} - {hasOnlyPrivatePorts && ( +
+
- Network +
+ Resources + {!resourcesModeLoading && resourcesMode && ( + + + + + {resourcesMode === 'aggregate' ? 'Aggregated view' : 'Pod-level view'} + + + + )} +
-
-
+ {hasStorage && ( +
+ +
+ )}
-
+
+ {hasPublicPort && ( + +
+ Network +
+
+ +
+
+ +
+
+ +
+
+
+
+ )} + {hasOnlyPrivatePorts && ( + +
+ Network +
+
+ +
+
+ +
+
+ +
+
+
+
)}