diff --git a/ui/src/components/metrics-cell.tsx b/ui/src/components/metrics-cell.tsx index 8dbacc2d..f7d83229 100644 --- a/ui/src/components/metrics-cell.tsx +++ b/ui/src/components/metrics-cell.tsx @@ -10,11 +10,13 @@ export function MetricCell({ type, limitLabel = 'Limit', showPercentage = false, + mode = 'usage', }: { metrics?: MetricsData type: 'cpu' | 'memory' limitLabel?: string // e.g., "Limit" or "Capacity" showPercentage?: boolean // Whether to show percentage in the display + mode?: 'usage' | 'request' | 'limit' }) { const metricValue = type === 'cpu' ? metrics?.cpuUsage || 0 : metrics?.memoryUsage || 0 @@ -26,25 +28,50 @@ export function MetricCell({ const formatValue = useCallback( (val?: number) => { - if (val === undefined || val === null) return '-' - return type === 'cpu' ? `${val}m` : formatMemory(val) + if (val === undefined || val === null || val === 0) { + if (mode !== 'usage' && (val === undefined || val === null)) return '-' + if (val === 0 && mode === 'usage') return '0' + if (val === 0) return '-' + } + return type === 'cpu' ? `${val}m` : formatMemory(val as number) }, - [type] + [type, mode] ) + const displayValue = useMemo(() => { + if (mode === 'usage') return metricValue + if (mode === 'request') return metricRequest + return metricLimit + }, [mode, metricValue, metricRequest, metricLimit]) + return useMemo(() => { - const percentage = metricLimit - ? Math.min((metricValue / metricLimit) * 100, 100) - : 0 + const barPercentage = (() => { + const limit = metricLimit || 0 + if (limit === 0) return 0 + if (mode === 'request') + return Math.min(((metricRequest || 0) / limit) * 100, 100) + if (mode === 'limit') return 100 + return Math.min((metricValue / limit) * 100, 100) + })() - const requestPercentage = - metricRequest && metricLimit - ? Math.min((metricRequest / metricLimit) * 100, 100) - : 0 + const markerPercentage = (() => { + const limit = metricLimit || 0 + if (limit === 0) return null + // In usage mode, marker is request + if (mode === 'usage') + return metricRequest ? (metricRequest / limit) * 100 : null + // In request mode, marker is usage + if (mode === 'request') return (metricValue / limit) * 100 + // In limit mode, marker is usage + if (mode === 'limit') return (metricValue / limit) * 100 + return null + })() const getProgressColor = () => { - if (percentage > 90) return 'bg-red-500' - if (percentage > 60) return 'bg-yellow-500' + if (mode === 'limit') return 'bg-muted-foreground opacity-30' + if (mode === 'request') return 'bg-green-500/80' + if (barPercentage > 90) return 'bg-red-500' + if (barPercentage > 60) return 'bg-yellow-500' return 'bg-blue-500' } @@ -56,18 +83,18 @@ export function MetricCell({
- {metricRequest && metricLimit && ( + {markerPercentage !== null && (
-
+
)}
@@ -86,10 +113,10 @@ export function MetricCell({ - {formatValue(metricValue)} - {showPercentage && metricLimit && metricValue > 0 && ( + {formatValue(displayValue)} + {mode === 'usage' && showPercentage && metricLimit && metricValue > 0 && ( - ({percentage.toFixed(0)}%) + ({barPercentage.toFixed(0)}%) )} @@ -100,6 +127,8 @@ export function MetricCell({ metricValue, metricRequest, formatValue, + displayValue, + mode, limitLabel, type, showPercentage, diff --git a/ui/src/components/resource-table.tsx b/ui/src/components/resource-table.tsx index 4fe60cf5..7a98d64f 100644 --- a/ui/src/components/resource-table.tsx +++ b/ui/src/components/resource-table.tsx @@ -74,6 +74,7 @@ export interface ResourceTableProps { onCreateClick?: () => void // Callback for create button click extraToolbars?: React.ReactNode[] // Additional toolbar components defaultHiddenColumns?: string[] // Columns to hide by default + reduce?: boolean // If true, fetch reduced data for performance } export function ResourceTable({ @@ -86,6 +87,7 @@ export function ResourceTable({ onCreateClick, extraToolbars = [], defaultHiddenColumns = [], + reduce = true, }: ResourceTableProps) { const { t } = useTranslation() const [sorting, setSorting] = useState([]) @@ -156,7 +158,7 @@ export function ResourceTable({ effectiveNamespace, { refreshInterval: useSSE ? 0 : refreshInterval, // disable polling when SSE - reduce: true, // Fetch reduced data for performance + reduce: reduce, // Fetch reduced data for performance disable: useSSE, // do not query when using SSE } ) @@ -173,7 +175,7 @@ export function ResourceTable({ (resourceType ?? (resourceName.toLowerCase() as ResourceType)) as ResourceType, effectiveNamespace, - { reduce: true, enabled: useSSE } + { reduce: reduce, enabled: useSSE } ) useEffect(() => { diff --git a/ui/src/lib/k8s.ts b/ui/src/lib/k8s.ts index 25b18ba1..905208d3 100644 --- a/ui/src/lib/k8s.ts +++ b/ui/src/lib/k8s.ts @@ -5,7 +5,7 @@ import { ObjectMeta } from 'kubernetes-types/meta/v1' import { clusterScopeResources, ResourceType } from '@/types/api' import { DeploymentStatusType, PodStatus, SimpleContainer } from '@/types/k8s' -import { getAge } from './utils' +import { parseBytes, parseCPU, getAge } from './utils' // This function retrieves the status of a Pod in Kubernetes. // @see https://github.com/kubernetes/kubernetes/blob/master/pkg/printers/internalversion/printers.go#L881 @@ -407,3 +407,30 @@ export function toSimpleContainer( })), ] } + +export function getPodResources(pod: Pod) { + let cpuRequest = 0 + let cpuLimit = 0 + let memoryRequest = 0 + let memoryLimit = 0 + + pod.spec?.containers?.forEach((container) => { + const requests = container.resources?.requests + const limits = container.resources?.limits + + if (requests?.cpu) { + cpuRequest += parseCPU(requests.cpu) + } + if (limits?.cpu) { + cpuLimit += parseCPU(limits.cpu) + } + if (requests?.memory) { + memoryRequest += parseBytes(requests.memory) + } + if (limits?.memory) { + memoryLimit += parseBytes(limits.memory) + } + }) + + return { cpuRequest, cpuLimit, memoryRequest, memoryLimit } +} diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index eb14cc71..ad56e271 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -91,6 +91,7 @@ export function formatBytes(bytes: number): string { } export function parseBytes(capacity: string): number { + if (!capacity) return 0 const units: { [key: string]: number } = { Ki: 1024, Mi: 1024 ** 2, @@ -98,15 +99,29 @@ export function parseBytes(capacity: string): number { Ti: 1024 ** 4, Pi: 1024 ** 5, Ei: 1024 ** 6, + K: 1000, + M: 1000 ** 2, + G: 1000 ** 3, + T: 1000 ** 4, + P: 1000 ** 5, + E: 1000 ** 6, } - const match = capacity.match(/^(\d+)([KMGTP]i)?$/) + const match = capacity.match(/^(\d+(?:\.\d+)?)([KMGTP]i|[KMGTP])?$/) if (match) { - const value = parseInt(match[1], 10) + const value = parseFloat(match[1]) const unit = match[2] - return unit ? value * units[unit] : value + return unit ? Math.floor(value * units[unit]) : Math.floor(value) + } + return parseInt(capacity, 10) || 0 +} + +export function parseCPU(cpu: string): number { + if (!cpu) return 0 + if (cpu.endsWith('m')) { + return parseInt(cpu.slice(0, -1), 10) } - return parseInt(capacity, 10) + return Math.floor(parseFloat(cpu) * 1000) } // Format CPU cores diff --git a/ui/src/pages/pod-list-page.tsx b/ui/src/pages/pod-list-page.tsx index dd122697..e2ba35da 100644 --- a/ui/src/pages/pod-list-page.tsx +++ b/ui/src/pages/pod-list-page.tsx @@ -1,11 +1,11 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import { createColumnHelper } from '@tanstack/react-table' import { Pod } from 'kubernetes-types/core/v1' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { PodWithMetrics } from '@/types/api' -import { getPodStatus } from '@/lib/k8s' +import { getPodStatus, getPodResources } from '@/lib/k8s' import { formatDate, getAge } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { @@ -16,12 +16,43 @@ import { import { MetricCell } from '@/components/metrics-cell' import { PodStatusIcon } from '@/components/pod-status-icon' import { ResourceTable } from '@/components/resource-table' +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' export function PodListPage() { const { t } = useTranslation() + const [metricMode, setMetricMode] = useState<'usage' | 'request' | 'limit'>( + 'usage' + ) + // Define column helper outside of any hooks const columnHelper = createColumnHelper() + const MetricModeSelector = useMemo( + () => ( + { + if (value) setMetricMode(value as 'usage' | 'request' | 'limit') + }} + variant="outline" + size="sm" + className="bg-background" + > + + Usage + + + Request + + + Limit + + + ), + [metricMode] + ) + // Define columns for the pod table - moved outside render cycle for better performance const columns = useMemo( () => [ @@ -79,16 +110,30 @@ export function PodListPage() { columnHelper.accessor((row) => row.metrics?.cpuUsage || 0, { id: 'cpu', header: 'CPU', - cell: ({ row }) => ( - - ), + cell: ({ row }) => { + const metrics = { ...row.original.metrics } + const resources = getPodResources(row.original) + metrics.cpuRequest = metrics.cpuRequest || resources.cpuRequest + metrics.cpuLimit = metrics.cpuLimit || resources.cpuLimit + + return ( + + ) + }, }), columnHelper.accessor((row) => row.metrics?.memoryUsage || 0, { id: 'memory', header: 'Memory', - cell: ({ row }) => ( - - ), + cell: ({ row }) => { + const metrics = { ...row.original.metrics } + const resources = getPodResources(row.original) + metrics.memoryRequest = metrics.memoryRequest || resources.memoryRequest + metrics.memoryLimit = metrics.memoryLimit || resources.memoryLimit + + return ( + + ) + }, }), columnHelper.accessor((row) => row.status?.podIP, { id: 'podIP', @@ -137,7 +182,7 @@ export function PodListPage() { }, }), ], - [columnHelper, t] + [columnHelper, t, metricMode] ) // Custom filter for pod search @@ -155,6 +200,8 @@ export function PodListPage() { columns={columns} clusterScope={false} searchQueryFilter={podSearchFilter} + extraToolbars={[MetricModeSelector]} + reduce={false} /> ) }