@skyhook-io/k8s-ui 1.7.11 → 1.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/AppChips.tsx +109 -0
  3. package/src/components/applications/AppTooltips.tsx +199 -0
  4. package/src/components/applications/ApplicationDetail.tsx +671 -0
  5. package/src/components/applications/ApplicationsList.tsx +569 -0
  6. package/src/components/applications/ReadyBar.tsx +22 -0
  7. package/src/components/applications/index.ts +8 -0
  8. package/src/components/audit/AuditFindingsTable.tsx +3 -25
  9. package/src/components/dock/TerminalTab.tsx +4 -2
  10. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
  11. package/src/components/issues/IssuesView.tsx +64 -17
  12. package/src/components/issues/index.ts +1 -1
  13. package/src/components/issues/issues.test.ts +4 -4
  14. package/src/components/issues/severity.ts +5 -0
  15. package/src/components/issues/types.ts +43 -0
  16. package/src/components/logs/LogCore.tsx +13 -2
  17. package/src/components/logs/LogToolbarSelects.tsx +6 -2
  18. package/src/components/logs/LogsViewer.tsx +66 -10
  19. package/src/components/logs/WorkloadLogsViewer.tsx +68 -13
  20. package/src/components/logs/useLogStream.ts +41 -3
  21. package/src/components/resources/ResourcesView.tsx +550 -52
  22. package/src/components/resources/column-filter-serialization.test.ts +26 -0
  23. package/src/components/resources/get-default-container-name.test.ts +31 -0
  24. package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
  25. package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
  26. package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
  27. package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
  28. package/src/components/resources/renderers/PodRenderer.tsx +2 -2
  29. package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
  30. package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
  31. package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
  32. package/src/components/resources/renderers/WorkloadRenderer.tsx +5 -4
  33. package/src/components/resources/renderers/dra-cells.tsx +80 -0
  34. package/src/components/resources/renderers/index.ts +8 -0
  35. package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
  36. package/src/components/resources/resource-utils-dra.ts +90 -0
  37. package/src/components/resources/resource-utils-nvidia.ts +63 -0
  38. package/src/components/resources/resource-utils.ts +62 -4
  39. package/src/components/shared/DetailShell.tsx +14 -7
  40. package/src/components/shared/EditableYamlView.tsx +37 -17
  41. package/src/components/shared/ResourceActionsBar.tsx +5 -4
  42. package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
  43. package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
  44. package/src/components/timeline/TimelineList.tsx +3 -32
  45. package/src/components/timeline/TimelineSwimlanes.tsx +3 -31
  46. package/src/components/topology/K8sResourceNode.tsx +26 -5
  47. package/src/components/topology/TopologyGraph.tsx +102 -3
  48. package/src/components/topology/layout.ts +36 -11
  49. package/src/components/ui/CenteredEmpty.tsx +27 -0
  50. package/src/components/ui/ConfirmDialog.tsx +1 -1
  51. package/src/components/ui/SearchBox.tsx +85 -0
  52. package/src/components/ui/drawer-components.tsx +23 -1
  53. package/src/components/ui/index.ts +1 -0
  54. package/src/components/workload/WorkloadView.tsx +167 -33
  55. package/src/components/workload/index.ts +1 -1
  56. package/src/hooks/useKeyboardShortcuts.tsx +3 -1
  57. package/src/index.ts +4 -0
  58. package/src/types/core.ts +1 -0
  59. package/src/utils/api-resources.ts +21 -0
  60. package/src/utils/applications.test.ts +207 -0
  61. package/src/utils/applications.ts +674 -0
  62. package/src/utils/custom-columns.test.ts +111 -0
  63. package/src/utils/custom-columns.ts +49 -0
  64. package/src/utils/extended-resources.test.ts +152 -0
  65. package/src/utils/extended-resources.ts +121 -0
  66. package/src/utils/format.ts +11 -0
  67. package/src/utils/index.ts +3 -0
  68. package/src/utils/topology-neighborhood.test.ts +185 -0
  69. package/src/utils/topology-neighborhood.ts +262 -0
  70. package/src/utils/workload-colors.ts +36 -0
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { parseColumnFilters, serializeColumnFilters } from './resource-utils'
3
+
4
+ describe('column filter serialization round-trip', () => {
5
+ it('round-trips built-in keys', () => {
6
+ const filters = { status: ['Running'], namespace: ['kube-system', 'default'] }
7
+ expect(parseColumnFilters(serializeColumnFilters(filters))).toEqual(filters)
8
+ })
9
+
10
+ it('round-trips custom-column keys whose own colon collides with the delimiter', () => {
11
+ const filters = { 'label:tier': ['control-plane'], 'annotation:foo/bar': ['x'] }
12
+ const serialized = serializeColumnFilters(filters)
13
+ // The key colon must be encoded so the first literal ':' is the delimiter.
14
+ expect(serialized).toBe('label%3Atier:control-plane|annotation%3Afoo%2Fbar:x')
15
+ expect(parseColumnFilters(serialized)).toEqual(filters)
16
+ })
17
+
18
+ it('preserves commas inside values', () => {
19
+ const filters = { conditions: ['Ready,SchedulingDisabled'] }
20
+ expect(parseColumnFilters(serializeColumnFilters(filters))).toEqual(filters)
21
+ })
22
+
23
+ it('parses legacy unencoded built-in keys', () => {
24
+ expect(parseColumnFilters('status:Running')).toEqual({ status: ['Running'] })
25
+ })
26
+ })
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getDefaultContainerName } from './resource-utils'
3
+
4
+ const podWith = (annotation: string | undefined, ...containers: string[]) => ({
5
+ metadata: annotation
6
+ ? { annotations: { 'kubectl.kubernetes.io/default-container': annotation } }
7
+ : {},
8
+ spec: { containers: containers.map((name) => ({ name })) },
9
+ })
10
+
11
+ describe('getDefaultContainerName', () => {
12
+ it('honors the default-container annotation over the first container', () => {
13
+ expect(getDefaultContainerName(podWith('app', 'istio-proxy', 'app'))).toBe('app')
14
+ })
15
+
16
+ it('falls back to the first container when no annotation is present', () => {
17
+ expect(getDefaultContainerName(podWith(undefined, 'istio-proxy', 'app'))).toBe('istio-proxy')
18
+ })
19
+
20
+ it('ignores an annotation naming a container that does not exist', () => {
21
+ expect(getDefaultContainerName(podWith('ghost', 'istio-proxy', 'app'))).toBe('istio-proxy')
22
+ })
23
+
24
+ it('returns the only container for a single-container pod', () => {
25
+ expect(getDefaultContainerName(podWith(undefined, 'app'))).toBe('app')
26
+ })
27
+
28
+ it('returns undefined for a pod with no containers', () => {
29
+ expect(getDefaultContainerName(podWith(undefined))).toBeUndefined()
30
+ })
31
+ })
@@ -0,0 +1,49 @@
1
+ import { Filter, Settings } from 'lucide-react'
2
+ import { Section } from '../../ui/drawer-components'
3
+
4
+ interface DeviceClassRendererProps {
5
+ data: any
6
+ }
7
+
8
+ export function DeviceClassRenderer({ data }: DeviceClassRendererProps) {
9
+ const selectors = data.spec?.selectors || []
10
+ const config = data.spec?.config || []
11
+
12
+ return (
13
+ <>
14
+ <Section title={`Selectors (${selectors.length})`} icon={Filter} defaultExpanded>
15
+ {selectors.length > 0 ? (
16
+ <div className="space-y-2">
17
+ {selectors.map((sel: any, i: number) => (
18
+ <div key={i} className="card-inner">
19
+ {sel?.cel?.expression ? (
20
+ <pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">{sel.cel.expression}</pre>
21
+ ) : (
22
+ <span className="text-sm text-theme-text-tertiary">-</span>
23
+ )}
24
+ </div>
25
+ ))}
26
+ </div>
27
+ ) : (
28
+ <div className="text-sm text-theme-text-tertiary">No selectors — matches all devices</div>
29
+ )}
30
+ </Section>
31
+
32
+ {config.length > 0 && (
33
+ <Section title={`Configuration (${config.length})`} icon={Settings}>
34
+ <div className="space-y-2">
35
+ {config.map((c: any, i: number) => (
36
+ <div key={i} className="card-inner text-sm">
37
+ {c?.opaque?.driver ? (
38
+ <span className="text-theme-text-secondary">opaque config for driver <span className="text-theme-text-primary font-medium">{c.opaque.driver}</span></span>
39
+ ) : (
40
+ <span className="text-theme-text-tertiary">config entry {i + 1}</span>
41
+ )}
42
+ </div>
43
+ ))}
44
+ </div>
45
+ </Section>
46
+ )}
47
+ </>
48
+ )
49
+ }
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
3
3
  import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
4
4
  import { MetricsChart } from '../../ui/MetricsChart'
5
5
  import { formatMemoryString } from '../../../utils/format'
6
+ import { getExtendedCapacityRows } from '../../../utils/extended-resources'
6
7
  import type { MetricsDataPoint } from '../../../types/core'
7
8
 
8
9
  interface NodeRendererProps {
@@ -132,6 +133,12 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
132
133
  allocatable: formatStorage(allocatable['ephemeral-storage']),
133
134
  inUse: undefined,
134
135
  },
136
+ ...getExtendedCapacityRows(capacity, allocatable).map((row) => ({
137
+ label: row.key,
138
+ capacity: row.capacity,
139
+ allocatable: row.allocatable,
140
+ inUse: undefined,
141
+ })),
135
142
  ].map((row) => (
136
143
  <div key={row.label} className="card-inner">
137
144
  <div className="text-xs font-medium text-theme-text-secondary mb-1">{row.label}</div>
@@ -0,0 +1,57 @@
1
+ import { Cpu, Boxes } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
4
+ import {
5
+ getNvidiaClusterPolicyStatus,
6
+ getNvidiaClusterPolicyEnabledComponents,
7
+ getNvidiaClusterPolicyMigStrategy,
8
+ } from '../resource-utils-nvidia'
9
+ import { healthColors } from '../resource-utils'
10
+
11
+ interface NvidiaClusterPolicyRendererProps {
12
+ data: any
13
+ }
14
+
15
+ export function NvidiaClusterPolicyRenderer({ data }: NvidiaClusterPolicyRendererProps) {
16
+ const status = getNvidiaClusterPolicyStatus(data)
17
+ const components = getNvidiaClusterPolicyEnabledComponents(data)
18
+ const mig = getNvidiaClusterPolicyMigStrategy(data)
19
+
20
+ return (
21
+ <>
22
+ {status.level === 'alert' && (
23
+ <AlertBanner
24
+ variant="warning"
25
+ title="GPU Operator not ready"
26
+ items={['One or more operator components have not reached ready state.']}
27
+ />
28
+ )}
29
+
30
+ <Section title="Operator Status" icon={Cpu} defaultExpanded>
31
+ <PropertyList>
32
+ <Property
33
+ label="State"
34
+ value={<span className={clsx('badge', status.color)}>{status.text}</span>}
35
+ />
36
+ {data.status?.namespace && <Property label="Operator Namespace" value={data.status.namespace} />}
37
+ {mig !== '-' && <Property label="MIG Strategy" value={mig} />}
38
+ </PropertyList>
39
+ </Section>
40
+
41
+ {components.length > 0 && (
42
+ <Section title={`Components (${components.length})`} icon={Boxes} defaultExpanded>
43
+ <div className="flex flex-wrap gap-1.5">
44
+ {components.map((c) => (
45
+ <span
46
+ key={c.label}
47
+ className={clsx('badge', c.enabled ? healthColors.healthy : healthColors.neutral)}
48
+ >
49
+ {c.label}{c.enabled ? '' : ' (off)'}
50
+ </span>
51
+ ))}
52
+ </div>
53
+ </Section>
54
+ )}
55
+ </>
56
+ )
57
+ }
@@ -0,0 +1,47 @@
1
+ import { Cpu } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, AlertBanner, LabelSelectorDisplay } from '../../ui/drawer-components'
4
+ import { getNvidiaDriverStatus } from '../resource-utils-nvidia'
5
+
6
+ interface NvidiaDriverRendererProps {
7
+ data: any
8
+ }
9
+
10
+ export function NvidiaDriverRenderer({ data }: NvidiaDriverRendererProps) {
11
+ const status = getNvidiaDriverStatus(data)
12
+ const spec = data.spec || {}
13
+
14
+ return (
15
+ <>
16
+ {status.level === 'alert' && (
17
+ <AlertBanner
18
+ variant="warning"
19
+ title="Driver rollout not ready"
20
+ items={['The driver DaemonSet has not reached ready state on all selected nodes.']}
21
+ />
22
+ )}
23
+
24
+ <Section title="Driver" icon={Cpu} defaultExpanded>
25
+ <PropertyList>
26
+ <Property
27
+ label="State"
28
+ value={<span className={clsx('badge', status.color)}>{status.text}</span>}
29
+ />
30
+ <Property label="Type" value={spec.driverType} />
31
+ <Property label="Version" value={spec.version} />
32
+ {spec.image && <Property label="Image" value={spec.image} />}
33
+ {spec.repository && <Property label="Repository" value={spec.repository} />}
34
+ {spec.usePrecompiled !== undefined && (
35
+ <Property label="Precompiled" value={spec.usePrecompiled ? 'Yes' : 'No'} />
36
+ )}
37
+ </PropertyList>
38
+ </Section>
39
+
40
+ {spec.nodeSelector && Object.keys(spec.nodeSelector).length > 0 && (
41
+ <Section title="Node Selector">
42
+ <LabelSelectorDisplay selector={{ matchLabels: spec.nodeSelector }} />
43
+ </Section>
44
+ )}
45
+ </>
46
+ )
47
+ }
@@ -2,7 +2,7 @@ import { useState, type ReactNode, type JSX } from 'react'
2
2
  import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen, List, Eye, EyeOff, Shield } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink } from '../../ui/drawer-components'
5
- import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR } from '../resource-utils'
5
+ import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
6
6
  import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
7
7
  import {
8
8
  rbacVerbBadgeClass,
@@ -286,7 +286,7 @@ export function PodRenderer({
286
286
  const [podFilesContainer, setPodFilesContainer] = useState<string | null>(null)
287
287
 
288
288
  const handleOpenTerminal = (containerName?: string) => {
289
- const container = containerName || containers[0]?.name
289
+ const container = containerName || getDefaultContainerName(data)
290
290
  if (namespace && podName && container) {
291
291
  onOpenTerminal?.({
292
292
  namespace,
@@ -0,0 +1,118 @@
1
+ import { Cpu, Layers, Users } from 'lucide-react'
2
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import {
4
+ getResourceClaimStatus,
5
+ getResourceClaimAllocation,
6
+ getResourceClaimReservedFor,
7
+ } from '../resource-utils-dra'
8
+
9
+ interface ResourceClaimRendererProps {
10
+ data: any
11
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
12
+ }
13
+
14
+ function requestDeviceClass(req: any): string {
15
+ return req?.exactly?.deviceClassName ||
16
+ req?.deviceClassName ||
17
+ (req?.firstAvailable || []).map((s: any) => s?.deviceClassName).filter(Boolean).join(' | ') ||
18
+ '-'
19
+ }
20
+
21
+ export function ResourceClaimRenderer({ data, onNavigate }: ResourceClaimRendererProps) {
22
+ const status = getResourceClaimStatus(data)
23
+ const requests = data.spec?.devices?.requests || []
24
+ const allocation = getResourceClaimAllocation(data)
25
+ const reservedFor = getResourceClaimReservedFor(data)
26
+ const deviceStatuses = data.status?.devices || []
27
+
28
+ return (
29
+ <>
30
+ {status.level === 'degraded' && (
31
+ <AlertBanner
32
+ variant="warning"
33
+ title="Allocated but unreserved"
34
+ items={['A device is allocated to this claim but no consumer holds it — long-lived, this leaks the device.']}
35
+ />
36
+ )}
37
+
38
+ {/* Device Requests */}
39
+ {requests.length > 0 && (
40
+ <Section title={`Device Requests (${requests.length})`} icon={Cpu} defaultExpanded>
41
+ <div className="space-y-2">
42
+ {requests.map((req: any, i: number) => {
43
+ const detail = req?.exactly || req
44
+ return (
45
+ <div key={req?.name || i} className="card-inner">
46
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
47
+ <span className="text-theme-text-primary font-medium">{req?.name || `request-${i}`}</span>
48
+ <span className="text-theme-text-secondary">{requestDeviceClass(req)}</span>
49
+ {detail?.count != null && (
50
+ <span className="text-theme-text-tertiary">count: {String(detail.count)}</span>
51
+ )}
52
+ {detail?.allocationMode && (
53
+ <span className="text-theme-text-tertiary">{detail.allocationMode}</span>
54
+ )}
55
+ </div>
56
+ </div>
57
+ )
58
+ })}
59
+ </div>
60
+ </Section>
61
+ )}
62
+
63
+ {/* Allocation */}
64
+ <Section title="Allocation" icon={Layers} defaultExpanded>
65
+ {allocation.length > 0 ? (
66
+ <div className="space-y-2">
67
+ {allocation.map((r, i) => (
68
+ <div key={i} className="card-inner">
69
+ <PropertyList>
70
+ <Property label="Driver" value={r.driver} />
71
+ <Property label="Pool" value={r.pool} />
72
+ <Property label="Device" value={r.device} />
73
+ </PropertyList>
74
+ </div>
75
+ ))}
76
+ </div>
77
+ ) : (
78
+ <div className="text-sm text-theme-text-tertiary">Not allocated — waiting for a driver to satisfy this claim</div>
79
+ )}
80
+ </Section>
81
+
82
+ {/* Reserved For */}
83
+ {reservedFor.length > 0 && (
84
+ <Section title={`Reserved For (${reservedFor.length})`} icon={Users} defaultExpanded>
85
+ <div className="space-y-1">
86
+ {reservedFor.map((r, i) => (
87
+ <div key={i} className="card-inner text-sm">
88
+ {r.resource === 'pods' ? (
89
+ <ResourceLink name={r.name} kind="pods" namespace={data.metadata?.namespace || ''} onNavigate={onNavigate} />
90
+ ) : (
91
+ <span className="text-theme-text-secondary">{r.resource}/{r.name}</span>
92
+ )}
93
+ </div>
94
+ ))}
95
+ </div>
96
+ </Section>
97
+ )}
98
+
99
+ {/* Per-device health (beta, populated by drivers that report it) */}
100
+ {deviceStatuses.length > 0 && (
101
+ <Section title={`Device Status (${deviceStatuses.length})`}>
102
+ <div className="space-y-2">
103
+ {deviceStatuses.map((d: any, i: number) => (
104
+ <div key={i} className="card-inner text-sm">
105
+ <div className="text-theme-text-primary font-medium">{d.device || '-'}</div>
106
+ {(d.conditions || []).map((c: any, j: number) => (
107
+ <div key={j} className="text-xs text-theme-text-secondary mt-0.5">
108
+ {c.type}: {c.status}{c.message ? ` — ${c.message}` : ''}
109
+ </div>
110
+ ))}
111
+ </div>
112
+ ))}
113
+ </div>
114
+ </Section>
115
+ )}
116
+ </>
117
+ )
118
+ }
@@ -0,0 +1,39 @@
1
+ import { Cpu } from 'lucide-react'
2
+ import { Section } from '../../ui/drawer-components'
3
+
4
+ interface ResourceClaimTemplateRendererProps {
5
+ data: any
6
+ }
7
+
8
+ export function ResourceClaimTemplateRenderer({ data }: ResourceClaimTemplateRendererProps) {
9
+ const requests = data.spec?.spec?.devices?.requests || []
10
+
11
+ return (
12
+ <>
13
+ <Section title={`Device Requests (${requests.length})`} icon={Cpu} defaultExpanded>
14
+ {requests.length > 0 ? (
15
+ <div className="space-y-2">
16
+ {requests.map((req: any, i: number) => {
17
+ const detail = req?.exactly || req
18
+ const deviceClass = detail?.deviceClassName ||
19
+ (req?.firstAvailable || []).map((s: any) => s?.deviceClassName).filter(Boolean).join(' | ') || '-'
20
+ return (
21
+ <div key={req?.name || i} className="card-inner">
22
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
23
+ <span className="text-theme-text-primary font-medium">{req?.name || `request-${i}`}</span>
24
+ <span className="text-theme-text-secondary">{deviceClass}</span>
25
+ {detail?.count != null && (
26
+ <span className="text-theme-text-tertiary">count: {String(detail.count)}</span>
27
+ )}
28
+ </div>
29
+ </div>
30
+ )
31
+ })}
32
+ </div>
33
+ ) : (
34
+ <div className="text-sm text-theme-text-tertiary">No device requests in template</div>
35
+ )}
36
+ </Section>
37
+ </>
38
+ )
39
+ }
@@ -0,0 +1,72 @@
1
+ import { HardDrive, Cpu } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ResourceLink } from '../../ui/drawer-components'
3
+
4
+ interface ResourceSliceRendererProps {
5
+ data: any
6
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
7
+ }
8
+
9
+ const DEVICE_DISPLAY_LIMIT = 20
10
+
11
+ export function ResourceSliceRenderer({ data, onNavigate }: ResourceSliceRendererProps) {
12
+ const spec = data.spec || {}
13
+ const devices = spec.devices || []
14
+ const visible = devices.slice(0, DEVICE_DISPLAY_LIMIT)
15
+
16
+ return (
17
+ <>
18
+ <Section title="Slice Info" icon={HardDrive} defaultExpanded>
19
+ <PropertyList>
20
+ <Property label="Driver" value={spec.driver} />
21
+ <Property label="Pool" value={spec.pool?.name} />
22
+ {spec.nodeName && (
23
+ <Property
24
+ label="Node"
25
+ value={<ResourceLink name={spec.nodeName} kind="nodes" namespace="" onNavigate={onNavigate} />}
26
+ />
27
+ )}
28
+ {spec.allNodes && <Property label="Scope" value="All nodes" />}
29
+ </PropertyList>
30
+ </Section>
31
+
32
+ <Section title={`Devices (${devices.length})`} icon={Cpu} defaultExpanded>
33
+ {devices.length > 0 ? (
34
+ <div className="space-y-2">
35
+ {visible.map((d: any, i: number) => {
36
+ // v1 puts attributes/capacity at the device level; v1beta1 nested them under "basic"
37
+ const attrs = d?.attributes || d?.basic?.attributes || {}
38
+ const capacity = d?.capacity || d?.basic?.capacity || {}
39
+ const attrEntries = Object.entries(attrs).slice(0, 6)
40
+ return (
41
+ <div key={d?.name || i} className="card-inner">
42
+ <div className="text-sm text-theme-text-primary font-medium">{d?.name || `device-${i}`}</div>
43
+ {attrEntries.length > 0 && (
44
+ <div className="mt-1 space-y-0.5">
45
+ {attrEntries.map(([k, v]: [string, any]) => (
46
+ <div key={k} className="text-xs text-theme-text-secondary">
47
+ {k}: {String(v?.string ?? v?.int ?? v?.bool ?? v?.version ?? '-')}
48
+ </div>
49
+ ))}
50
+ </div>
51
+ )}
52
+ {Object.keys(capacity).length > 0 && (
53
+ <div className="mt-1 text-xs text-theme-text-tertiary">
54
+ capacity: {Object.entries(capacity).map(([k, v]: [string, any]) => `${k}=${v?.value ?? v}`).join(', ')}
55
+ </div>
56
+ )}
57
+ </div>
58
+ )
59
+ })}
60
+ {devices.length > DEVICE_DISPLAY_LIMIT && (
61
+ <div className="text-xs text-theme-text-tertiary">
62
+ +{devices.length - DEVICE_DISPLAY_LIMIT} more devices — see YAML for the full list
63
+ </div>
64
+ )}
65
+ </div>
66
+ ) : (
67
+ <div className="text-sm text-theme-text-tertiary">No devices published in this slice</div>
68
+ )}
69
+ </Section>
70
+ </>
71
+ )
72
+ }
@@ -174,12 +174,13 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
174
174
 
175
175
  return (
176
176
  <>
177
- {/* Scaling in progress banner */}
177
+ {/* Scaling in progress banner — amber: replicas short of desired is an
178
+ attention state, not an info note (it may be a stuck rollout). */}
178
179
  {(scaledTo !== null || progressMessage) && !hasProblems && (
179
- <div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
180
+ <div className="mb-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
180
181
  <div className="flex items-center gap-2">
181
- <Loader2 className="w-4 h-4 text-blue-400 animate-spin shrink-0" />
182
- <div className="text-sm text-blue-300">
182
+ <Loader2 className="w-4 h-4 text-amber-500 animate-spin shrink-0" />
183
+ <div className="text-sm text-amber-700 dark:text-amber-300">
183
184
  {progressMessage || `Scaling to ${scaledTo} replicas...`}
184
185
  </div>
185
186
  </div>
@@ -0,0 +1,80 @@
1
+ // DRA (resource.k8s.io) cell components for ResourcesView table
2
+
3
+ import { clsx } from 'clsx'
4
+ import {
5
+ getResourceClaimStatus,
6
+ getResourceClaimDeviceClasses,
7
+ getResourceClaimAllocation,
8
+ getResourceClaimReservedFor,
9
+ getResourceClaimTemplateDeviceClasses,
10
+ getDeviceClassSelectorCount,
11
+ getResourceSliceDriver,
12
+ getResourceSlicePool,
13
+ getResourceSliceNode,
14
+ getResourceSliceDeviceCount,
15
+ } from '../resource-utils-dra'
16
+
17
+ export function ResourceClaimCell({ resource, column }: { resource: any; column: string }) {
18
+ switch (column) {
19
+ case 'status': {
20
+ const status = getResourceClaimStatus(resource)
21
+ return <span className={clsx('badge', status.color)}>{status.text}</span>
22
+ }
23
+ case 'deviceClass': {
24
+ const classes = getResourceClaimDeviceClasses(resource)
25
+ return <span className="text-sm text-theme-text-secondary truncate block">{classes.join(', ') || '-'}</span>
26
+ }
27
+ case 'allocated': {
28
+ const results = getResourceClaimAllocation(resource)
29
+ if (results.length === 0) return <span className="text-sm text-theme-text-tertiary">-</span>
30
+ return <span className="text-sm text-theme-text-secondary truncate block">{results[0].driver}{results.length > 1 ? ` +${results.length - 1}` : ''}</span>
31
+ }
32
+ case 'reservedFor': {
33
+ const reserved = getResourceClaimReservedFor(resource)
34
+ if (reserved.length === 0) return <span className="text-sm text-theme-text-tertiary">-</span>
35
+ return <span className="text-sm text-theme-text-secondary truncate block">{reserved.map(r => r.name).join(', ')}</span>
36
+ }
37
+ default:
38
+ return <span className="text-sm text-theme-text-tertiary">-</span>
39
+ }
40
+ }
41
+
42
+ export function ResourceClaimTemplateCell({ resource, column }: { resource: any; column: string }) {
43
+ switch (column) {
44
+ case 'deviceClass': {
45
+ const classes = getResourceClaimTemplateDeviceClasses(resource)
46
+ return <span className="text-sm text-theme-text-secondary truncate block">{classes.join(', ') || '-'}</span>
47
+ }
48
+ default:
49
+ return <span className="text-sm text-theme-text-tertiary">-</span>
50
+ }
51
+ }
52
+
53
+ export function DeviceClassCell({ resource, column }: { resource: any; column: string }) {
54
+ switch (column) {
55
+ case 'selectors': {
56
+ // 0 is meaningful — a class with no selectors matches all devices
57
+ const count = getDeviceClassSelectorCount(resource)
58
+ return <span className="text-sm text-theme-text-secondary">{count}</span>
59
+ }
60
+ default:
61
+ return <span className="text-sm text-theme-text-tertiary">-</span>
62
+ }
63
+ }
64
+
65
+ export function ResourceSliceCell({ resource, column }: { resource: any; column: string }) {
66
+ switch (column) {
67
+ case 'driver':
68
+ return <span className="text-sm text-theme-text-secondary truncate block">{getResourceSliceDriver(resource)}</span>
69
+ case 'pool':
70
+ return <span className="text-sm text-theme-text-secondary truncate block">{getResourceSlicePool(resource)}</span>
71
+ case 'node': {
72
+ const node = getResourceSliceNode(resource)
73
+ return <span className="text-sm text-theme-text-secondary truncate block">{node || '-'}</span>
74
+ }
75
+ case 'devices':
76
+ return <span className="text-sm text-theme-text-secondary">{getResourceSliceDeviceCount(resource)}</span>
77
+ default:
78
+ return <span className="text-sm text-theme-text-tertiary">-</span>
79
+ }
80
+ }
@@ -142,3 +142,11 @@ export * from './azure-capi-cells'
142
142
  export * from './AzureManagedControlPlaneRenderer'
143
143
  export * from './AzureManagedMachinePoolRenderer'
144
144
  export * from './AzureMachineRenderer'
145
+ export * from './dra-cells'
146
+ export * from './ResourceClaimRenderer'
147
+ export * from './ResourceClaimTemplateRenderer'
148
+ export * from './DeviceClassRenderer'
149
+ export * from './ResourceSliceRenderer'
150
+ export * from './nvidia-cells'
151
+ export * from './NvidiaClusterPolicyRenderer'
152
+ export * from './NvidiaDriverRenderer'
@@ -0,0 +1,43 @@
1
+ // NVIDIA GPU Operator (nvidia.com) cell components for ResourcesView table
2
+
3
+ import { clsx } from 'clsx'
4
+ import {
5
+ getNvidiaClusterPolicyStatus,
6
+ getNvidiaClusterPolicyEnabledComponents,
7
+ getNvidiaClusterPolicyMigStrategy,
8
+ getNvidiaDriverStatus,
9
+ getNvidiaDriverType,
10
+ getNvidiaDriverVersion,
11
+ } from '../resource-utils-nvidia'
12
+
13
+ export function NvidiaClusterPolicyCell({ resource, column }: { resource: any; column: string }) {
14
+ switch (column) {
15
+ case 'status': {
16
+ const status = getNvidiaClusterPolicyStatus(resource)
17
+ return <span className={clsx('badge', status.color)}>{status.text}</span>
18
+ }
19
+ case 'components': {
20
+ const enabled = getNvidiaClusterPolicyEnabledComponents(resource).filter(c => c.enabled)
21
+ return <span className="text-sm text-theme-text-secondary truncate block">{enabled.length ? enabled.map(c => c.label).join(', ') : '-'}</span>
22
+ }
23
+ case 'mig':
24
+ return <span className="text-sm text-theme-text-secondary">{getNvidiaClusterPolicyMigStrategy(resource)}</span>
25
+ default:
26
+ return <span className="text-sm text-theme-text-tertiary">-</span>
27
+ }
28
+ }
29
+
30
+ export function NvidiaDriverCell({ resource, column }: { resource: any; column: string }) {
31
+ switch (column) {
32
+ case 'status': {
33
+ const status = getNvidiaDriverStatus(resource)
34
+ return <span className={clsx('badge', status.color)}>{status.text}</span>
35
+ }
36
+ case 'driverType':
37
+ return <span className="text-sm text-theme-text-secondary">{getNvidiaDriverType(resource)}</span>
38
+ case 'version':
39
+ return <span className="text-sm text-theme-text-secondary">{getNvidiaDriverVersion(resource)}</span>
40
+ default:
41
+ return <span className="text-sm text-theme-text-tertiary">-</span>
42
+ }
43
+ }