@skyhook-io/k8s-ui 1.7.10 → 1.7.12
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.
- package/package.json +1 -1
- package/src/components/dock/TerminalTab.tsx +4 -2
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
- package/src/components/issues/IssuesView.tsx +169 -17
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +4 -4
- package/src/components/issues/severity.ts +5 -0
- package/src/components/issues/types.ts +74 -0
- package/src/components/logs/LogCore.tsx +13 -2
- package/src/components/logs/LogToolbarSelects.tsx +6 -2
- package/src/components/logs/LogsViewer.tsx +66 -10
- package/src/components/logs/WorkloadLogsViewer.tsx +60 -8
- package/src/components/logs/useLogStream.ts +41 -3
- package/src/components/resources/ResourcesView.tsx +550 -52
- package/src/components/resources/column-filter-serialization.test.ts +26 -0
- package/src/components/resources/get-default-container-name.test.ts +31 -0
- package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
- package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
- package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
- package/src/components/resources/renderers/PodRenderer.tsx +2 -2
- package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
- package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
- package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
- package/src/components/resources/renderers/dra-cells.tsx +80 -0
- package/src/components/resources/renderers/index.ts +8 -0
- package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
- package/src/components/resources/resource-utils-dra.ts +90 -0
- package/src/components/resources/resource-utils-nvidia.ts +63 -0
- package/src/components/resources/resource-utils.ts +62 -4
- package/src/components/shared/CreateResourceDialog.tsx +32 -3
- package/src/components/shared/EditableYamlView.tsx +31 -2
- package/src/components/shared/ResourceActionsBar.tsx +5 -4
- package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
- package/src/components/ui/ConfirmDialog.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +23 -1
- package/src/types/core.ts +1 -0
- package/src/utils/api-resources.ts +21 -0
- package/src/utils/custom-columns.test.ts +111 -0
- package/src/utils/custom-columns.ts +49 -0
- package/src/utils/extended-resources.test.ts +152 -0
- package/src/utils/extended-resources.ts +121 -0
- package/src/utils/index.ts +1 -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 ||
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|