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}
/>
)
}