@skyhook-io/k8s-ui 1.7.11 → 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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/components/dock/TerminalTab.tsx +4 -2
  3. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
  4. package/src/components/issues/IssuesView.tsx +64 -17
  5. package/src/components/issues/index.ts +1 -1
  6. package/src/components/issues/issues.test.ts +4 -4
  7. package/src/components/issues/severity.ts +5 -0
  8. package/src/components/issues/types.ts +43 -0
  9. package/src/components/logs/LogCore.tsx +13 -2
  10. package/src/components/logs/LogToolbarSelects.tsx +6 -2
  11. package/src/components/logs/LogsViewer.tsx +66 -10
  12. package/src/components/logs/WorkloadLogsViewer.tsx +60 -8
  13. package/src/components/logs/useLogStream.ts +41 -3
  14. package/src/components/resources/ResourcesView.tsx +550 -52
  15. package/src/components/resources/column-filter-serialization.test.ts +26 -0
  16. package/src/components/resources/get-default-container-name.test.ts +31 -0
  17. package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
  18. package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
  19. package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
  20. package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
  21. package/src/components/resources/renderers/PodRenderer.tsx +2 -2
  22. package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
  23. package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
  24. package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
  25. package/src/components/resources/renderers/dra-cells.tsx +80 -0
  26. package/src/components/resources/renderers/index.ts +8 -0
  27. package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
  28. package/src/components/resources/resource-utils-dra.ts +90 -0
  29. package/src/components/resources/resource-utils-nvidia.ts +63 -0
  30. package/src/components/resources/resource-utils.ts +62 -4
  31. package/src/components/shared/ResourceActionsBar.tsx +5 -4
  32. package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
  33. package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
  34. package/src/components/ui/ConfirmDialog.tsx +1 -1
  35. package/src/components/ui/drawer-components.tsx +23 -1
  36. package/src/types/core.ts +1 -0
  37. package/src/utils/api-resources.ts +21 -0
  38. package/src/utils/custom-columns.test.ts +111 -0
  39. package/src/utils/custom-columns.ts +49 -0
  40. package/src/utils/extended-resources.test.ts +152 -0
  41. package/src/utils/extended-resources.ts +121 -0
  42. package/src/utils/index.ts +1 -0
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from './custom-columns'
3
+
4
+ describe('customColumnKey', () => {
5
+ it('encodes source and path', () => {
6
+ expect(customColumnKey({ source: 'label', path: 'topology.kubernetes.io/zone' })).toBe('label:topology.kubernetes.io/zone')
7
+ expect(customColumnKey({ source: 'annotation', path: 'foo/bar' })).toBe('annotation:foo/bar')
8
+ })
9
+
10
+ it('produces equal keys for same source+path (dedupe contract)', () => {
11
+ const a = customColumnKey({ source: 'label', path: 'x' })
12
+ const b = customColumnKey({ source: 'label', path: 'x' })
13
+ expect(a).toBe(b)
14
+ })
15
+
16
+ it('distinguishes source for the same path', () => {
17
+ expect(customColumnKey({ source: 'label', path: 'x' }))
18
+ .not.toBe(customColumnKey({ source: 'annotation', path: 'x' }))
19
+ })
20
+ })
21
+
22
+ describe('readCustomColumnValue', () => {
23
+ const res = (meta: any) => ({ metadata: meta })
24
+
25
+ it('reads a label value', () => {
26
+ expect(readCustomColumnValue(res({ labels: { zone: 'us-east-1a' } }), { source: 'label', path: 'zone' })).toBe('us-east-1a')
27
+ })
28
+
29
+ it('reads an annotation value', () => {
30
+ expect(readCustomColumnValue(res({ annotations: { team: 'infra' } }), { source: 'annotation', path: 'team' })).toBe('infra')
31
+ })
32
+
33
+ it('does not cross labels and annotations', () => {
34
+ const r = res({ labels: { k: 'fromLabel' }, annotations: { k: 'fromAnnotation' } })
35
+ expect(readCustomColumnValue(r, { source: 'label', path: 'k' })).toBe('fromLabel')
36
+ expect(readCustomColumnValue(r, { source: 'annotation', path: 'k' })).toBe('fromAnnotation')
37
+ })
38
+
39
+ it('returns empty string for a missing key', () => {
40
+ expect(readCustomColumnValue(res({ labels: { a: '1' } }), { source: 'label', path: 'b' })).toBe('')
41
+ })
42
+
43
+ it('returns empty string when metadata/bag is absent', () => {
44
+ expect(readCustomColumnValue({}, { source: 'label', path: 'b' })).toBe('')
45
+ expect(readCustomColumnValue(res({}), { source: 'annotation', path: 'b' })).toBe('')
46
+ expect(readCustomColumnValue(undefined, { source: 'label', path: 'b' })).toBe('')
47
+ })
48
+
49
+ it('coerces non-string values, but maps null/undefined to empty (not the literal "null")', () => {
50
+ expect(readCustomColumnValue(res({ labels: { n: 5 as any } }), { source: 'label', path: 'n' })).toBe('5')
51
+ expect(readCustomColumnValue(res({ labels: { n: null as any } }), { source: 'label', path: 'n' })).toBe('')
52
+ })
53
+ })
54
+
55
+ describe('sanitizeCustomColumnDefs', () => {
56
+ it('returns [] for non-array input', () => {
57
+ expect(sanitizeCustomColumnDefs(undefined)).toEqual([])
58
+ expect(sanitizeCustomColumnDefs(null)).toEqual([])
59
+ expect(sanitizeCustomColumnDefs({ source: 'label', path: 'x' })).toEqual([])
60
+ expect(sanitizeCustomColumnDefs('label:x')).toEqual([])
61
+ })
62
+
63
+ it('keeps well-formed defs', () => {
64
+ const defs = [
65
+ { source: 'label', path: 'zone' },
66
+ { source: 'annotation', path: 'team' },
67
+ ]
68
+ expect(sanitizeCustomColumnDefs(defs)).toEqual(defs)
69
+ })
70
+
71
+ it('drops entries with invalid source, missing or blank path', () => {
72
+ const raw = [
73
+ { source: 'label', path: 'good' },
74
+ { source: 'lable', path: 'typo-source' },
75
+ { source: 'label', path: '' },
76
+ { source: 'label', path: ' ' },
77
+ { source: 'annotation' },
78
+ null,
79
+ 'label:x',
80
+ ]
81
+ expect(sanitizeCustomColumnDefs(raw)).toEqual([{ source: 'label', path: 'good' }])
82
+ })
83
+
84
+ it('drops a non-string path without throwing (predicate short-circuits before trim)', () => {
85
+ const raw = [
86
+ { source: 'label', path: 42 },
87
+ { source: 'label', path: { nested: true } },
88
+ { source: 'annotation', path: 'ok' },
89
+ ]
90
+ expect(() => sanitizeCustomColumnDefs(raw)).not.toThrow()
91
+ expect(sanitizeCustomColumnDefs(raw)).toEqual([{ source: 'annotation', path: 'ok' }])
92
+ })
93
+
94
+ it('trims paths so the load path matches the add path', () => {
95
+ expect(sanitizeCustomColumnDefs([{ source: 'label', path: ' zone ' }]))
96
+ .toEqual([{ source: 'label', path: 'zone' }])
97
+ })
98
+
99
+ it('dedupes by key, keeping the first occurrence', () => {
100
+ const raw = [
101
+ { source: 'label', path: 'zone' },
102
+ { source: 'label', path: 'zone' },
103
+ { source: 'label', path: ' zone ' },
104
+ { source: 'annotation', path: 'zone' },
105
+ ]
106
+ expect(sanitizeCustomColumnDefs(raw)).toEqual([
107
+ { source: 'label', path: 'zone' },
108
+ { source: 'annotation', path: 'zone' },
109
+ ])
110
+ })
111
+ })
@@ -0,0 +1,49 @@
1
+ // User-defined table columns sourcing a metadata.labels[path] or
2
+ // metadata.annotations[path] value. Persisted per-kind in localStorage and
3
+ // materialized (in ResourcesView) into a self-contained ExtraColumn so they
4
+ // ride the existing render/sort/filter override rails.
5
+
6
+ export type CustomColumnSource = 'label' | 'annotation'
7
+
8
+ export interface CustomColumnDef {
9
+ source: CustomColumnSource
10
+ path: string
11
+ }
12
+
13
+ // Stable identity used as the React key, ExtraColumn.key, visibility-set
14
+ // membership, and dedupe key. Built-in column keys never use a `:` prefix, so
15
+ // `label:`/`annotation:` can't collide with them. Build-only — never parsed
16
+ // back into source+path, so a colon inside `path` is harmless.
17
+ export function customColumnKey(d: CustomColumnDef): string {
18
+ return `${d.source}:${d.path}`
19
+ }
20
+
21
+ export function readCustomColumnValue(resource: any, d: CustomColumnDef): string {
22
+ const bag = d.source === 'label' ? resource?.metadata?.labels : resource?.metadata?.annotations
23
+ const v = bag?.[d.path]
24
+ return typeof v === 'string' ? v : v == null ? '' : String(v)
25
+ }
26
+
27
+ // Drop anything that isn't a well-formed def — guards the localStorage load
28
+ // boundary where a corrupted/hand-edited blob could otherwise be a non-array
29
+ // (crashing a later .map) or carry empty/invalid entries (dead columns).
30
+ // TypeScript's guarantees stop at JSON.parse, so this is the one place the
31
+ // (source, non-empty path) invariant must be enforced at runtime. Paths are
32
+ // trimmed and deduped by key so the load path produces the same defs the add
33
+ // path would — a hand-edited blob can't yield a padded key or two columns
34
+ // sharing one key.
35
+ export function sanitizeCustomColumnDefs(raw: unknown): CustomColumnDef[] {
36
+ if (!Array.isArray(raw)) return []
37
+ const seen = new Set<string>()
38
+ const out: CustomColumnDef[] = []
39
+ for (const c of raw) {
40
+ if (!c || (c.source !== 'label' && c.source !== 'annotation') || typeof c.path !== 'string') continue
41
+ const def: CustomColumnDef = { source: c.source, path: c.path.trim() }
42
+ if (def.path === '') continue
43
+ const key = customColumnKey(def)
44
+ if (seen.has(key)) continue
45
+ seen.add(key)
46
+ out.push(def)
47
+ }
48
+ return out
49
+ }
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ isGpuResourceKey,
4
+ isGpuCountKey,
5
+ getEffectiveResourceValue,
6
+ getEffectiveResources,
7
+ getPodGpuCount,
8
+ getNodeGpuCount,
9
+ getExtendedCapacityRows,
10
+ } from './extended-resources'
11
+
12
+ describe('isGpuResourceKey', () => {
13
+ it('matches vendor whole-GPU keys', () => {
14
+ expect(isGpuResourceKey('nvidia.com/gpu')).toBe(true)
15
+ expect(isGpuResourceKey('amd.com/gpu')).toBe(true)
16
+ })
17
+ it('matches NVIDIA MIG slices', () => {
18
+ expect(isGpuResourceKey('nvidia.com/mig-1g.5gb')).toBe(true)
19
+ })
20
+ it('matches the Intel gpu.intel.com family', () => {
21
+ expect(isGpuResourceKey('gpu.intel.com/i915')).toBe(true)
22
+ expect(isGpuResourceKey('gpu.intel.com/millicores')).toBe(true)
23
+ })
24
+ it('rejects non-GPU keys', () => {
25
+ expect(isGpuResourceKey('cpu')).toBe(false)
26
+ expect(isGpuResourceKey('hugepages-2Mi')).toBe(false)
27
+ expect(isGpuResourceKey('example.com/fpga')).toBe(false)
28
+ })
29
+ })
30
+
31
+ describe('isGpuCountKey', () => {
32
+ it('excludes fractional Intel dimensions', () => {
33
+ expect(isGpuCountKey('gpu.intel.com/millicores')).toBe(false)
34
+ expect(isGpuCountKey('gpu.intel.com/memory.max')).toBe(false)
35
+ expect(isGpuCountKey('gpu.intel.com/tiles')).toBe(false)
36
+ })
37
+ it('includes count-like keys', () => {
38
+ expect(isGpuCountKey('nvidia.com/gpu')).toBe(true)
39
+ expect(isGpuCountKey('nvidia.com/mig-3g.20gb')).toBe(true)
40
+ expect(isGpuCountKey('gpu.intel.com/i915')).toBe(true)
41
+ })
42
+ })
43
+
44
+ describe('getEffectiveResourceValue', () => {
45
+ it('falls back to limits when requests omit the key (canonical GPU form)', () => {
46
+ const resources = { limits: { 'nvidia.com/gpu': '2' } }
47
+ expect(getEffectiveResourceValue(resources, 'nvidia.com/gpu')).toBe('2')
48
+ })
49
+ it('prefers an explicit request', () => {
50
+ const resources = { requests: { 'nvidia.com/gpu': '1' }, limits: { 'nvidia.com/gpu': '1' } }
51
+ expect(getEffectiveResourceValue(resources, 'nvidia.com/gpu')).toBe('1')
52
+ })
53
+ it('returns undefined when absent', () => {
54
+ expect(getEffectiveResourceValue({}, 'nvidia.com/gpu')).toBeUndefined()
55
+ expect(getEffectiveResourceValue(undefined, 'nvidia.com/gpu')).toBeUndefined()
56
+ })
57
+ })
58
+
59
+ describe('getPodGpuCount', () => {
60
+ it('sums limits-only GPU containers', () => {
61
+ const pod = {
62
+ spec: {
63
+ containers: [
64
+ { resources: { limits: { 'nvidia.com/gpu': '2' } } },
65
+ { resources: { limits: { 'nvidia.com/gpu': '1' } } },
66
+ ],
67
+ },
68
+ }
69
+ expect(getPodGpuCount(pod)).toBe(3)
70
+ })
71
+ it('applies max(sum(containers), max(initContainers))', () => {
72
+ const pod = {
73
+ spec: {
74
+ containers: [{ resources: { limits: { 'nvidia.com/gpu': '1' } } }],
75
+ initContainers: [{ resources: { limits: { 'nvidia.com/gpu': '4' } } }],
76
+ },
77
+ }
78
+ expect(getPodGpuCount(pod)).toBe(4)
79
+ })
80
+ it('counts MIG slices but not fractional Intel dimensions', () => {
81
+ const pod = {
82
+ spec: {
83
+ containers: [
84
+ { resources: { limits: { 'nvidia.com/mig-1g.5gb': '2', 'gpu.intel.com/millicores': '500' } } },
85
+ ],
86
+ },
87
+ }
88
+ expect(getPodGpuCount(pod)).toBe(2)
89
+ })
90
+ it('returns 0 for GPU-free pods and malformed values', () => {
91
+ expect(getPodGpuCount({ spec: { containers: [{ resources: { limits: { cpu: '1' } } }] } })).toBe(0)
92
+ expect(getPodGpuCount({ spec: { containers: [{ resources: { limits: { 'nvidia.com/gpu': 'x' } } }] } })).toBe(0)
93
+ expect(getPodGpuCount({})).toBe(0)
94
+ })
95
+ it('parses K8s quantity suffixes (3000m = 3, 3k = 3000)', () => {
96
+ expect(getPodGpuCount({ spec: { containers: [{ resources: { limits: { 'nvidia.com/gpu': '3000m' } } }] } })).toBe(3)
97
+ expect(getPodGpuCount({ spec: { containers: [{ resources: { limits: { 'nvidia.com/gpu': '3k' } } }] } })).toBe(3000)
98
+ })
99
+ })
100
+
101
+ describe('getEffectiveResources', () => {
102
+ it('merges limits-only keys with explicit requests', () => {
103
+ const resources = {
104
+ requests: { cpu: '100m' },
105
+ limits: { cpu: '200m', 'nvidia.com/gpu': '1' },
106
+ }
107
+ expect(getEffectiveResources(resources)).toEqual({ cpu: '100m', 'nvidia.com/gpu': '1' })
108
+ })
109
+ it('returns empty for missing blocks', () => {
110
+ expect(getEffectiveResources(undefined)).toEqual({})
111
+ expect(getEffectiveResources({})).toEqual({})
112
+ })
113
+ })
114
+
115
+ describe('getNodeGpuCount', () => {
116
+ it('sums allocatable count-like GPU keys', () => {
117
+ const node = { status: { allocatable: { 'nvidia.com/gpu': '8', cpu: '64' } } }
118
+ expect(getNodeGpuCount(node)).toBe(8)
119
+ })
120
+ it('returns 0 without GPU keys', () => {
121
+ expect(getNodeGpuCount({ status: { allocatable: { cpu: '64' } } })).toBe(0)
122
+ expect(getNodeGpuCount({})).toBe(0)
123
+ })
124
+ })
125
+
126
+ describe('getExtendedCapacityRows', () => {
127
+ it('excludes the curated standard keys and sorts GPU rows first', () => {
128
+ const capacity = {
129
+ cpu: '64',
130
+ memory: '256Gi',
131
+ pods: '110',
132
+ 'ephemeral-storage': '1Ti',
133
+ 'hugepages-1Gi': '2Gi',
134
+ 'nvidia.com/gpu': '8',
135
+ }
136
+ const allocatable = { ...capacity, 'nvidia.com/gpu': '7' }
137
+ const rows = getExtendedCapacityRows(capacity, allocatable)
138
+ expect(rows.map(r => r.key)).toEqual(['nvidia.com/gpu', 'hugepages-1Gi'])
139
+ expect(rows[0]).toMatchObject({ capacity: '8', allocatable: '7', isGpu: true })
140
+ })
141
+ it('drops all-zero rows (every node advertises hugepages-* as 0)', () => {
142
+ const capacity = { cpu: '4', 'hugepages-2Mi': '0', 'hugepages-1Gi': '0' }
143
+ expect(getExtendedCapacityRows(capacity, capacity)).toEqual([])
144
+ })
145
+ it('handles keys present on only one side', () => {
146
+ const rows = getExtendedCapacityRows({ 'amd.com/gpu': '4' }, {})
147
+ expect(rows).toEqual([{ key: 'amd.com/gpu', capacity: '4', allocatable: undefined, isGpu: true }])
148
+ })
149
+ it('returns empty for nodes with only standard resources', () => {
150
+ expect(getExtendedCapacityRows({ cpu: '4', memory: '8Gi' }, { cpu: '4' })).toEqual([])
151
+ })
152
+ })
@@ -0,0 +1,121 @@
1
+ // Extended-resource (GPU etc.) helpers shared by table cells, renderers, and
2
+ // the GPU views. Keep all effective-request math here — K8s semantics for
3
+ // extended resources are subtle and must not be reimplemented per call site.
4
+
5
+ import { parseMemoryToBytes } from './format'
6
+
7
+ const STANDARD_NODE_RESOURCES = new Set(['cpu', 'memory', 'pods', 'ephemeral-storage'])
8
+
9
+ // Broad GPU detection for badging/labeling: whole-GPU keys (nvidia.com/gpu,
10
+ // amd.com/gpu), NVIDIA MIG slices, and Intel's gpu.intel.com family
11
+ // (i915/xe/millicores/memory.max/tiles).
12
+ export function isGpuResourceKey(key: string): boolean {
13
+ return key.endsWith('/gpu') || key.startsWith('nvidia.com/mig-') || key.startsWith('gpu.intel.com/')
14
+ }
15
+
16
+ // Count-like GPU keys only — values that mean "N devices/slices". Excludes
17
+ // fractional dimensions (gpu.intel.com/millicores, memory.max, tiles) whose
18
+ // numbers would distort a device count.
19
+ export function isGpuCountKey(key: string): boolean {
20
+ return (
21
+ key.endsWith('/gpu') ||
22
+ key.startsWith('nvidia.com/mig-') ||
23
+ key === 'gpu.intel.com/i915' ||
24
+ key === 'gpu.intel.com/xe'
25
+ )
26
+ }
27
+
28
+ // GPUs are canonically specified in limits only: the request defaults to the
29
+ // limit, and the two must be equal when both are set (no overcommit).
30
+ export function getEffectiveResourceValue(resources: any, key: string): string | undefined {
31
+ return resources?.requests?.[key] ?? resources?.limits?.[key]
32
+ }
33
+
34
+ // Merged per-key effective view of a container resources block. Needed for pod
35
+ // TEMPLATES (workload detail): apiserver defaulting copies limits→requests only
36
+ // on live Pods, never on templates, so limits-only GPU templates have no requests.
37
+ export function getEffectiveResources(resources: any): Record<string, string> {
38
+ const keys = new Set([
39
+ ...Object.keys(resources?.requests || {}),
40
+ ...Object.keys(resources?.limits || {}),
41
+ ])
42
+ const out: Record<string, string> = {}
43
+ for (const key of keys) {
44
+ const value = getEffectiveResourceValue(resources, key)
45
+ if (value !== undefined) out[key] = value
46
+ }
47
+ return out
48
+ }
49
+
50
+ // K8s quantity → number. Counts are usually plain integers, but quantities may
51
+ // legally carry suffixes: "3000m" is 3, "3k" is 3000.
52
+ function asCount(value: unknown): number {
53
+ if (typeof value === 'number') return Number.isFinite(value) ? value : 0
54
+ const s = String(value ?? '').trim()
55
+ if (!s) return 0
56
+ if (/^\d+(\.\d+)?m$/.test(s)) return Number(s.slice(0, -1)) / 1000
57
+ const n = parseMemoryToBytes(s)
58
+ return Number.isFinite(n) ? n : 0
59
+ }
60
+
61
+ function containerGpuCount(container: any): number {
62
+ const resources = container?.resources
63
+ const keys = new Set([
64
+ ...Object.keys(resources?.requests || {}),
65
+ ...Object.keys(resources?.limits || {}),
66
+ ])
67
+ let total = 0
68
+ for (const key of keys) {
69
+ if (isGpuCountKey(key)) total += asCount(getEffectiveResourceValue(resources, key))
70
+ }
71
+ return total
72
+ }
73
+
74
+ // Effective pod GPU count per scheduling semantics:
75
+ // max(sum of regular containers, largest init container).
76
+ export function getPodGpuCount(pod: any): number {
77
+ const containers: any[] = pod?.spec?.containers || []
78
+ const initContainers: any[] = pod?.spec?.initContainers || []
79
+ const regular = containers.reduce((sum, c) => sum + containerGpuCount(c), 0)
80
+ const init = initContainers.reduce((max, c) => Math.max(max, containerGpuCount(c)), 0)
81
+ return Math.max(regular, init)
82
+ }
83
+
84
+ export function getNodeGpuCount(node: any): number {
85
+ const allocatable = node?.status?.allocatable || {}
86
+ let total = 0
87
+ for (const [key, value] of Object.entries(allocatable)) {
88
+ if (isGpuCountKey(key)) total += asCount(value)
89
+ }
90
+ return total
91
+ }
92
+
93
+ export interface ExtendedCapacityRow {
94
+ key: string
95
+ capacity?: string
96
+ allocatable?: string
97
+ isGpu: boolean
98
+ }
99
+
100
+ // Node capacity/allocatable keys beyond the curated cpu/memory/pods/ephemeral-storage
101
+ // rows (hugepages, attachable-volumes, vendor GPU keys, ...). GPU keys sort first.
102
+ // All-zero rows are dropped — every node advertises hugepages-* as "0".
103
+ export function getExtendedCapacityRows(capacity: any, allocatable: any): ExtendedCapacityRow[] {
104
+ const keys = new Set([...Object.keys(capacity || {}), ...Object.keys(allocatable || {})])
105
+ const rows: ExtendedCapacityRow[] = []
106
+ for (const key of keys) {
107
+ if (STANDARD_NODE_RESOURCES.has(key)) continue
108
+ if ((capacity?.[key] ?? '0') === '0' && (allocatable?.[key] ?? '0') === '0') continue
109
+ rows.push({
110
+ key,
111
+ capacity: capacity?.[key],
112
+ allocatable: allocatable?.[key],
113
+ isGpu: isGpuResourceKey(key),
114
+ })
115
+ }
116
+ rows.sort((a, b) => {
117
+ if (a.isGpu !== b.isGpu) return a.isGpu ? -1 : 1
118
+ return a.key.localeCompare(b.key)
119
+ })
120
+ return rows
121
+ }
@@ -9,6 +9,7 @@ export * from './resource-hierarchy'
9
9
  export * from './log-format'
10
10
  export * from './download'
11
11
  export * from './env-from'
12
+ export * from './extended-resources'
12
13
  export * from './api-resources'
13
14
  export * from './skeleton-yaml'
14
15
  export * from './k8s-errors'