@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.
- package/package.json +1 -1
- package/src/components/applications/AppChips.tsx +109 -0
- package/src/components/applications/AppTooltips.tsx +199 -0
- package/src/components/applications/ApplicationDetail.tsx +671 -0
- package/src/components/applications/ApplicationsList.tsx +569 -0
- package/src/components/applications/ReadyBar.tsx +22 -0
- package/src/components/applications/index.ts +8 -0
- package/src/components/audit/AuditFindingsTable.tsx +3 -25
- package/src/components/dock/TerminalTab.tsx +4 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
- package/src/components/issues/IssuesView.tsx +64 -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 +43 -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 +68 -13
- 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/WorkloadRenderer.tsx +5 -4
- 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/DetailShell.tsx +14 -7
- package/src/components/shared/EditableYamlView.tsx +37 -17
- 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/timeline/TimelineList.tsx +3 -32
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -31
- package/src/components/topology/K8sResourceNode.tsx +26 -5
- package/src/components/topology/TopologyGraph.tsx +102 -3
- package/src/components/topology/layout.ts +36 -11
- package/src/components/ui/CenteredEmpty.tsx +27 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -1
- package/src/components/ui/SearchBox.tsx +85 -0
- package/src/components/ui/drawer-components.tsx +23 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +167 -33
- package/src/components/workload/index.ts +1 -1
- package/src/hooks/useKeyboardShortcuts.tsx +3 -1
- package/src/index.ts +4 -0
- package/src/types/core.ts +1 -0
- package/src/utils/api-resources.ts +21 -0
- package/src/utils/applications.test.ts +207 -0
- package/src/utils/applications.ts +674 -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/format.ts +11 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/topology-neighborhood.test.ts +185 -0
- package/src/utils/topology-neighborhood.ts +262 -0
- 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 ||
|
|
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-
|
|
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-
|
|
182
|
-
<div className="text-sm text-
|
|
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
|
+
}
|