@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,90 @@
|
|
|
1
|
+
// DRA (Dynamic Resource Allocation, resource.k8s.io) utility functions.
|
|
2
|
+
// Accessors tolerate all served shapes: v1/v1beta2 nest device-class refs
|
|
3
|
+
// under requests[].exactly (or firstAvailable subrequests); v1beta1 had
|
|
4
|
+
// deviceClassName directly on the request.
|
|
5
|
+
|
|
6
|
+
import type { StatusBadge } from './resource-utils'
|
|
7
|
+
import { healthColors } from './resource-utils'
|
|
8
|
+
|
|
9
|
+
function requestDeviceClasses(requests: any[]): string[] {
|
|
10
|
+
const seen = new Set<string>()
|
|
11
|
+
for (const req of requests || []) {
|
|
12
|
+
const direct = req?.exactly?.deviceClassName || req?.deviceClassName
|
|
13
|
+
if (direct) seen.add(direct)
|
|
14
|
+
for (const sub of req?.firstAvailable || []) {
|
|
15
|
+
if (sub?.deviceClassName) seen.add(sub.deviceClassName)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...seen]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getResourceClaimDeviceClasses(resource: any): string[] {
|
|
22
|
+
return requestDeviceClasses(resource?.spec?.devices?.requests)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getResourceClaimTemplateDeviceClasses(resource: any): string[] {
|
|
26
|
+
return requestDeviceClasses(resource?.spec?.spec?.devices?.requests)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getResourceClaimAllocation(resource: any): { driver: string; pool: string; device: string }[] {
|
|
30
|
+
const results = resource?.status?.allocation?.devices?.results || []
|
|
31
|
+
return results.map((r: any) => ({
|
|
32
|
+
driver: r?.driver || '',
|
|
33
|
+
pool: r?.pool || '',
|
|
34
|
+
device: r?.device || '',
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getResourceClaimReservedFor(resource: any): { resource: string; name: string }[] {
|
|
39
|
+
return (resource?.status?.reservedFor || []).map((r: any) => ({
|
|
40
|
+
resource: r?.resource || r?.apiGroup || '',
|
|
41
|
+
name: r?.name || '',
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getResourceClaimStatus(resource: any): StatusBadge {
|
|
46
|
+
// Allocated means devices were actually assigned — keyed to devices.results
|
|
47
|
+
// so the badge always agrees with the Allocation section.
|
|
48
|
+
const allocated = (resource?.status?.allocation?.devices?.results || []).length > 0
|
|
49
|
+
const reserved = (resource?.status?.reservedFor || []).length > 0
|
|
50
|
+
if (allocated && reserved) {
|
|
51
|
+
return { text: 'Allocated', color: healthColors.healthy, level: 'healthy' }
|
|
52
|
+
}
|
|
53
|
+
if (allocated) {
|
|
54
|
+
// Allocated-but-unreserved is normal transiently; long-lived it leaks a device
|
|
55
|
+
return { text: 'Unreserved', color: healthColors.degraded, level: 'degraded' }
|
|
56
|
+
}
|
|
57
|
+
return { text: 'Pending', color: healthColors.unknown, level: 'unknown' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getResourceClaimTemplateStatus(_resource: any): StatusBadge {
|
|
61
|
+
return { text: 'Template', color: healthColors.neutral, level: 'neutral' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getDeviceClassStatus(_resource: any): StatusBadge {
|
|
65
|
+
return { text: 'Available', color: healthColors.neutral, level: 'neutral' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getDeviceClassSelectorCount(resource: any): number {
|
|
69
|
+
return (resource?.spec?.selectors || []).length
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getResourceSliceStatus(_resource: any): StatusBadge {
|
|
73
|
+
return { text: 'Published', color: healthColors.neutral, level: 'neutral' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getResourceSliceDriver(resource: any): string {
|
|
77
|
+
return resource?.spec?.driver || '-'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getResourceSlicePool(resource: any): string {
|
|
81
|
+
return resource?.spec?.pool?.name || '-'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getResourceSliceNode(resource: any): string {
|
|
85
|
+
return resource?.spec?.nodeName || ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getResourceSliceDeviceCount(resource: any): number {
|
|
89
|
+
return (resource?.spec?.devices || []).length
|
|
90
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// NVIDIA GPU Operator (nvidia.com) utility functions.
|
|
2
|
+
// ClusterPolicy here is nvidia.com's — distinct from Kyverno's ClusterPolicy;
|
|
3
|
+
// dispatch call sites guard on apiVersion before reaching these.
|
|
4
|
+
|
|
5
|
+
import type { StatusBadge } from './resource-utils'
|
|
6
|
+
import { healthColors } from './resource-utils'
|
|
7
|
+
|
|
8
|
+
function stateBadge(state: string | undefined): StatusBadge {
|
|
9
|
+
switch (state) {
|
|
10
|
+
case 'ready':
|
|
11
|
+
return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
12
|
+
case 'notReady':
|
|
13
|
+
return { text: 'Not Ready', color: healthColors.alert, level: 'alert' }
|
|
14
|
+
case 'disabled':
|
|
15
|
+
return { text: 'Disabled', color: healthColors.neutral, level: 'neutral' }
|
|
16
|
+
default:
|
|
17
|
+
return { text: state || 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getNvidiaClusterPolicyStatus(resource: any): StatusBadge {
|
|
22
|
+
return stateBadge(resource?.status?.state)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const NVIDIA_CLUSTER_POLICY_COMPONENTS = [
|
|
26
|
+
{ key: 'driver', label: 'Driver' },
|
|
27
|
+
{ key: 'toolkit', label: 'Container Toolkit' },
|
|
28
|
+
{ key: 'devicePlugin', label: 'Device Plugin' },
|
|
29
|
+
{ key: 'dcgmExporter', label: 'DCGM Exporter' },
|
|
30
|
+
{ key: 'dcgm', label: 'DCGM' },
|
|
31
|
+
{ key: 'gfd', label: 'GPU Feature Discovery' },
|
|
32
|
+
{ key: 'migManager', label: 'MIG Manager' },
|
|
33
|
+
{ key: 'nodeStatusExporter', label: 'Node Status Exporter' },
|
|
34
|
+
{ key: 'vgpuManager', label: 'vGPU Manager' },
|
|
35
|
+
{ key: 'gds', label: 'GPUDirect Storage' },
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
export function getNvidiaClusterPolicyEnabledComponents(resource: any): { label: string; enabled: boolean }[] {
|
|
39
|
+
const spec = resource?.spec || {}
|
|
40
|
+
return NVIDIA_CLUSTER_POLICY_COMPONENTS
|
|
41
|
+
.filter(({ key }) => spec[key]?.enabled !== undefined)
|
|
42
|
+
.map(({ key, label }) => ({ label, enabled: !!spec[key].enabled }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getNvidiaClusterPolicyMigStrategy(resource: any): string {
|
|
46
|
+
return resource?.spec?.mig?.strategy || '-'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getNvidiaDriverStatus(resource: any): StatusBadge {
|
|
50
|
+
return stateBadge(resource?.status?.state)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getNvidiaDriverType(resource: any): string {
|
|
54
|
+
return resource?.spec?.driverType || '-'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getNvidiaDriverVersion(resource: any): string {
|
|
58
|
+
return resource?.spec?.version || '-'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isNvidiaResource(resource: any): boolean {
|
|
62
|
+
return !!resource?.apiVersion?.startsWith('nvidia.com/')
|
|
63
|
+
}
|
|
@@ -10,6 +10,8 @@ import { getScaledObjectStatus, getScaledJobStatus } from './resource-utils-keda
|
|
|
10
10
|
import { getGitRepositoryStatus, getOCIRepositoryStatus, getHelmRepositoryStatus, getHelmRepositoryType, getKustomizationStatus, getFluxHelmReleaseStatus, getFluxAlertStatus } from './resource-utils-flux'
|
|
11
11
|
import { getArgoApplicationStatus, getArgoApplicationSetStatus, getArgoApplicationSync, getArgoApplicationHealth, getArgoApplicationProject } from './resource-utils-argo'
|
|
12
12
|
import { getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
|
|
13
|
+
import { getResourceClaimStatus as _getResourceClaimStatus, getResourceClaimDeviceClasses as _getResourceClaimDeviceClasses, getResourceClaimTemplateDeviceClasses as _getResourceClaimTemplateDeviceClasses, getResourceClaimAllocation as _getResourceClaimAllocation, getResourceClaimReservedFor as _getResourceClaimReservedFor } from './resource-utils-dra'
|
|
14
|
+
import { getNvidiaClusterPolicyStatus as _getNvidiaClusterPolicyStatus, getNvidiaClusterPolicyEnabledComponents as _getNvidiaClusterPolicyEnabledComponents, getNvidiaDriverStatus as _getNvidiaDriverStatus } from './resource-utils-nvidia'
|
|
13
15
|
import { getBackupStatus as _getBackupStatus, getRestoreStatus as _getRestoreStatus, getScheduleStatus as _getScheduleStatus, getBSLStatus as _getBSLStatus } from './resource-utils-velero'
|
|
14
16
|
import { getExternalSecretStatus as _getExternalSecretStatus, getClusterExternalSecretStatus as _getClusterExternalSecretStatus, getSecretStoreStatus as _getSecretStoreStatus, getClusterSecretStoreStatus as _getClusterSecretStoreStatus, getSecretStoreProviderType as _getSecretStoreProviderType } from './resource-utils-eso'
|
|
15
17
|
|
|
@@ -418,6 +420,23 @@ export interface ContainerSquareState {
|
|
|
418
420
|
}
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
/**
|
|
424
|
+
* The container to default to for exec / logs on a multi-container pod. Honors
|
|
425
|
+
* the kubectl.kubernetes.io/default-container annotation (the convention
|
|
426
|
+
* kubectl, k9s, and Lens follow, and what service meshes like Istio set to
|
|
427
|
+
* point past their injected sidecar), falling back to the first container.
|
|
428
|
+
* Without this, mesh-injected pods default to their distroless sidecar — which
|
|
429
|
+
* has no shell — making the terminal appear broken.
|
|
430
|
+
*/
|
|
431
|
+
export function getDefaultContainerName(pod: any): string | undefined {
|
|
432
|
+
const containers = pod?.spec?.containers || []
|
|
433
|
+
const annotated = pod?.metadata?.annotations?.['kubectl.kubernetes.io/default-container']
|
|
434
|
+
if (annotated && containers.some((c: any) => c.name === annotated)) {
|
|
435
|
+
return annotated
|
|
436
|
+
}
|
|
437
|
+
return containers[0]?.name
|
|
438
|
+
}
|
|
439
|
+
|
|
421
440
|
export function getContainerSquareStates(pod: any): ContainerSquareState[] {
|
|
422
441
|
const result: ContainerSquareState[] = []
|
|
423
442
|
const initStatuses = pod.status?.initContainerStatuses || []
|
|
@@ -1610,6 +1629,10 @@ export function formatResources(resources: any): string {
|
|
|
1610
1629
|
if (resources.memory) {
|
|
1611
1630
|
parts.push(`Mem: ${formatMemoryString(resources.memory)}`)
|
|
1612
1631
|
}
|
|
1632
|
+
for (const [key, value] of Object.entries(resources)) {
|
|
1633
|
+
if (key === 'cpu' || key === 'memory') continue
|
|
1634
|
+
parts.push(`${key}: ${value}`)
|
|
1635
|
+
}
|
|
1613
1636
|
return parts.join(', ') || '-'
|
|
1614
1637
|
}
|
|
1615
1638
|
|
|
@@ -1631,8 +1654,12 @@ export function parseColumnFilters(filtersParam: string | null): Record<string,
|
|
|
1631
1654
|
for (const pair of filtersParam.split('|')) {
|
|
1632
1655
|
const colonIdx = pair.indexOf(':')
|
|
1633
1656
|
if (colonIdx > 0) {
|
|
1634
|
-
const
|
|
1657
|
+
const rawKey = pair.slice(0, colonIdx).trim()
|
|
1635
1658
|
const valStr = pair.slice(colonIdx + 1).trim()
|
|
1659
|
+
// Keys are URI-encoded so a custom-column key's own colon (e.g.
|
|
1660
|
+
// "label:tier") doesn't collide with the key:value delimiter.
|
|
1661
|
+
let key: string
|
|
1662
|
+
try { key = decodeURIComponent(rawKey) } catch { key = rawKey }
|
|
1636
1663
|
if (key && valStr) {
|
|
1637
1664
|
filters[key] = valStr.split(',').map(v => {
|
|
1638
1665
|
try { return decodeURIComponent(v.trim()) } catch { return v.trim() }
|
|
@@ -1643,12 +1670,13 @@ export function parseColumnFilters(filtersParam: string | null): Record<string,
|
|
|
1643
1670
|
return filters
|
|
1644
1671
|
}
|
|
1645
1672
|
|
|
1646
|
-
// Serialize column filters to URL param format
|
|
1647
|
-
//
|
|
1673
|
+
// Serialize column filters to URL param format. Keys and values are both
|
|
1674
|
+
// URI-encoded so a colon inside a custom-column key (e.g. "label:tier") or a
|
|
1675
|
+
// comma inside a value (e.g. "Ready,SchedulingDisabled") survives the round-trip.
|
|
1648
1676
|
export function serializeColumnFilters(filters: Record<string, string[]>): string {
|
|
1649
1677
|
const result = Object.entries(filters)
|
|
1650
1678
|
.filter(([, v]) => v.length > 0)
|
|
1651
|
-
.map(([k, vals]) => `${k}:${vals.map(v => encodeURIComponent(v)).join(',')}`)
|
|
1679
|
+
.map(([k, vals]) => `${encodeURIComponent(k)}:${vals.map(v => encodeURIComponent(v)).join(',')}`)
|
|
1652
1680
|
.join('|')
|
|
1653
1681
|
return result
|
|
1654
1682
|
}
|
|
@@ -1706,6 +1734,9 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
1706
1734
|
if (kindLower === 'scaledjobs') return getScaledJobStatus(resource).text
|
|
1707
1735
|
if (kindLower === 'policyreports' || kindLower === 'clusterpolicyreports') return _getPolicyReportStatus(resource).text
|
|
1708
1736
|
if (kindLower === 'kyvernopolicies' || kindLower === 'clusterpolicies') return _getKyvernoPolicyStatus(resource).text
|
|
1737
|
+
if (kindLower === 'nvidiaclusterpolicies') return _getNvidiaClusterPolicyStatus(resource).text
|
|
1738
|
+
if (kindLower === 'nvidiadrivers') return _getNvidiaDriverStatus(resource).text
|
|
1739
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimStatus(resource).text
|
|
1709
1740
|
if (kindLower === 'backups') return _getBackupStatus(resource).text
|
|
1710
1741
|
if (kindLower === 'restores') return _getRestoreStatus(resource).text
|
|
1711
1742
|
if (kindLower === 'schedules') return _getScheduleStatus(resource).text
|
|
@@ -1766,6 +1797,33 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
1766
1797
|
case 'provider':
|
|
1767
1798
|
if (kindLower === 'secretstores' || kindLower === 'clustersecretstores') return _getSecretStoreProviderType(resource)
|
|
1768
1799
|
return resource.spec?.provider || ''
|
|
1800
|
+
// DRA + NVIDIA columns. Unmatched kinds break to the generic fallback —
|
|
1801
|
+
// these keys are shared (e.g. 'components' is also a Trivy SBOM column).
|
|
1802
|
+
case 'deviceClass':
|
|
1803
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimDeviceClasses(resource).join(', ')
|
|
1804
|
+
if (kindLower === 'resourceclaimtemplates') return _getResourceClaimTemplateDeviceClasses(resource).join(', ')
|
|
1805
|
+
break
|
|
1806
|
+
case 'allocated':
|
|
1807
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimAllocation(resource).map(r => r.driver).join(', ')
|
|
1808
|
+
break
|
|
1809
|
+
case 'reservedFor':
|
|
1810
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimReservedFor(resource).map(r => r.name).join(', ')
|
|
1811
|
+
break
|
|
1812
|
+
case 'pool':
|
|
1813
|
+
if (kindLower === 'resourceslices') return resource.spec?.pool?.name || ''
|
|
1814
|
+
break
|
|
1815
|
+
case 'devices':
|
|
1816
|
+
if (kindLower === 'resourceslices') return String((resource.spec?.devices || []).length)
|
|
1817
|
+
break
|
|
1818
|
+
case 'selectors':
|
|
1819
|
+
if (kindLower === 'deviceclasses') return String((resource.spec?.selectors || []).length)
|
|
1820
|
+
break
|
|
1821
|
+
case 'components':
|
|
1822
|
+
if (kindLower === 'nvidiaclusterpolicies') return _getNvidiaClusterPolicyEnabledComponents(resource).filter(c => c.enabled).map(c => c.label).join(', ')
|
|
1823
|
+
break
|
|
1824
|
+
case 'mig':
|
|
1825
|
+
if (kindLower === 'nvidiaclusterpolicies') return resource.spec?.mig?.strategy || ''
|
|
1826
|
+
break
|
|
1769
1827
|
}
|
|
1770
1828
|
|
|
1771
1829
|
// Fallback: try common paths
|
|
@@ -30,6 +30,8 @@ export interface DetailShellProps<TId extends string = string> {
|
|
|
30
30
|
onTabChange: (id: TId) => void
|
|
31
31
|
tabStripEnd?: ReactNode
|
|
32
32
|
overlay?: ReactNode
|
|
33
|
+
/** Hide breadcrumb/identity/header actions when a host page already owns that chrome. */
|
|
34
|
+
compactHeader?: boolean
|
|
33
35
|
children: ReactNode
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -44,6 +46,7 @@ export function DetailShell<TId extends string = string>({
|
|
|
44
46
|
onTabChange,
|
|
45
47
|
tabStripEnd,
|
|
46
48
|
overlay,
|
|
49
|
+
compactHeader = false,
|
|
47
50
|
children,
|
|
48
51
|
}: DetailShellProps<TId>) {
|
|
49
52
|
const visibleTabs = tabs.filter((t) => !t.hidden)
|
|
@@ -52,15 +55,19 @@ export function DetailShell<TId extends string = string>({
|
|
|
52
55
|
<div className="flex flex-col h-full w-full bg-theme-surface">
|
|
53
56
|
{/* Header */}
|
|
54
57
|
<div className="shrink-0 border-b border-theme-border bg-theme-surface">
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
{!compactHeader && (
|
|
59
|
+
<>
|
|
60
|
+
{breadcrumb && <div className="px-6 pt-2.5">{breadcrumb}</div>}
|
|
61
|
+
<div className={clsx('px-6 flex items-start gap-4', breadcrumb ? 'pb-3 pt-1.5' : 'py-3')}>
|
|
62
|
+
{nav}
|
|
63
|
+
<div className="flex-1 min-w-0">{identity}</div>
|
|
64
|
+
{headerActions}
|
|
65
|
+
</div>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
61
68
|
|
|
62
69
|
{/* Tabs (left) + scope controls / actions (right) */}
|
|
63
|
-
<div className=
|
|
70
|
+
<div className={clsx('flex items-center', compactHeader ? 'px-0' : 'border-t border-theme-border px-6')}>
|
|
64
71
|
<div className="flex gap-1" role="tablist">
|
|
65
72
|
{visibleTabs.map((t) => (
|
|
66
73
|
<DetailShellTabButton key={t.id} active={activeTab === t.id} onClick={() => onTabChange(t.id)}>
|
|
@@ -66,19 +66,27 @@ function formatSaveError(error: string): { summary: string; details?: string } {
|
|
|
66
66
|
const errorPart = parts[1]?.trim() || ''
|
|
67
67
|
|
|
68
68
|
if (errorPart.includes('Forbidden:')) {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
69
|
+
const forbiddenAt = errorPart.indexOf(': Forbidden:')
|
|
70
|
+
if (forbiddenAt > 0) {
|
|
71
|
+
const target = errorPart.slice(0, forbiddenAt).trim()
|
|
72
|
+
const messageStart = forbiddenAt + ': Forbidden:'.length
|
|
73
|
+
const dotAt = errorPart.indexOf('.', messageStart)
|
|
74
|
+
const braceAt = errorPart.indexOf('{', messageStart)
|
|
75
|
+
const endCandidates = [dotAt, braceAt].filter((i) => i >= 0)
|
|
76
|
+
const messageEnd = endCandidates.length ? Math.min(...endCandidates) : errorPart.length
|
|
77
|
+
const message = errorPart.slice(messageStart, messageEnd).trim()
|
|
71
78
|
return {
|
|
72
|
-
summary: `Cannot update ${
|
|
79
|
+
summary: `Cannot update ${target}: ${message}`,
|
|
73
80
|
details: error.length > 200 ? error : undefined
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
const
|
|
79
|
-
|
|
85
|
+
const braceAt = errorPart.indexOf('{')
|
|
86
|
+
const summary = (braceAt >= 0 ? errorPart.slice(0, braceAt) : errorPart).trim()
|
|
87
|
+
if (summary) {
|
|
80
88
|
return {
|
|
81
|
-
summary
|
|
89
|
+
summary,
|
|
82
90
|
details: error.length > 200 ? error : undefined
|
|
83
91
|
}
|
|
84
92
|
}
|
|
@@ -111,6 +119,8 @@ interface EditableYamlViewProps {
|
|
|
111
119
|
data: any
|
|
112
120
|
onCopy: (text: string) => void
|
|
113
121
|
copied: boolean
|
|
122
|
+
/** Hide edit affordances when the host surface is read-only. */
|
|
123
|
+
readOnly?: boolean
|
|
114
124
|
/** Called after a successful save so the parent can refetch */
|
|
115
125
|
onSaved?: () => void
|
|
116
126
|
/** Save handler — injected by the platform wrapper */
|
|
@@ -128,13 +138,13 @@ interface EditableYamlViewProps {
|
|
|
128
138
|
onDownload?: (content: string, mime: string, filename: string) => void
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSave, isSaving, saveError, onDuplicate, onDownload }: EditableYamlViewProps) {
|
|
141
|
+
export function EditableYamlView({ resource, data, onCopy, copied, readOnly = false, onSaved, onSave, isSaving, saveError, onDuplicate, onDownload }: EditableYamlViewProps) {
|
|
132
142
|
const draftKey = `radar_yaml_draft:${resource.kind}/${resource.namespace}/${resource.name}`
|
|
133
143
|
|
|
134
144
|
// Restore draft from sessionStorage (e.g., after session-expiry redirect).
|
|
135
145
|
// All sessionStorage calls are wrapped in try-catch — storage can throw
|
|
136
146
|
// QuotaExceededError or be blocked by browser security policies.
|
|
137
|
-
const savedDraft = useRef(safeSessionGet(draftKey))
|
|
147
|
+
const savedDraft = useRef(readOnly ? null : safeSessionGet(draftKey))
|
|
138
148
|
const [isEditing, setIsEditing] = useState(savedDraft.current !== null)
|
|
139
149
|
const [editedYaml, setEditedYaml] = useState(savedDraft.current ?? '')
|
|
140
150
|
const [yamlErrors, setYamlErrors] = useState<string[]>([])
|
|
@@ -153,12 +163,19 @@ export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSa
|
|
|
153
163
|
|
|
154
164
|
// Autosave draft to sessionStorage while editing (best-effort)
|
|
155
165
|
useEffect(() => {
|
|
166
|
+
if (readOnly) {
|
|
167
|
+
setIsEditing(false)
|
|
168
|
+
setEditedYaml('')
|
|
169
|
+
setYamlErrors([])
|
|
170
|
+
safeSessionRemove(draftKey)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
156
173
|
if (isEditing && editedYaml) {
|
|
157
174
|
safeSessionSet(draftKey, editedYaml)
|
|
158
175
|
} else {
|
|
159
176
|
safeSessionRemove(draftKey)
|
|
160
177
|
}
|
|
161
|
-
}, [isEditing, editedYaml, draftKey])
|
|
178
|
+
}, [isEditing, editedYaml, draftKey, readOnly])
|
|
162
179
|
|
|
163
180
|
const handleDownload = useCallback(() => {
|
|
164
181
|
const yaml = resourceToYaml(data)
|
|
@@ -170,10 +187,11 @@ export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSa
|
|
|
170
187
|
}, [data, resource.kind, resource.name, onDownload])
|
|
171
188
|
|
|
172
189
|
const handleStartEdit = useCallback(() => {
|
|
190
|
+
if (readOnly) return
|
|
173
191
|
setEditedYaml(resourceToYaml(data))
|
|
174
192
|
setYamlErrors([])
|
|
175
193
|
setIsEditing(true)
|
|
176
|
-
}, [data])
|
|
194
|
+
}, [data, readOnly])
|
|
177
195
|
|
|
178
196
|
const handleCancelEdit = useCallback(() => {
|
|
179
197
|
setIsEditing(false)
|
|
@@ -345,13 +363,15 @@ export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSa
|
|
|
345
363
|
<div className="flex items-center justify-between mb-2">
|
|
346
364
|
<span className="text-sm font-medium text-theme-text-secondary">YAML</span>
|
|
347
365
|
<div className="flex items-center gap-2">
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
366
|
+
{!readOnly && (
|
|
367
|
+
<button
|
|
368
|
+
onClick={handleStartEdit}
|
|
369
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:text-blue-300 hover:bg-theme-elevated rounded"
|
|
370
|
+
>
|
|
371
|
+
<Pencil className="w-3.5 h-3.5" />
|
|
372
|
+
Edit
|
|
373
|
+
</button>
|
|
374
|
+
)}
|
|
355
375
|
<button
|
|
356
376
|
onClick={() => onCopy(yamlContent)}
|
|
357
377
|
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
@@ -24,6 +24,7 @@ import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
|
24
24
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
25
25
|
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
26
26
|
import { formatKindName } from '../ui/drawer-components'
|
|
27
|
+
import { getDefaultContainerName } from '../resources/resource-utils'
|
|
27
28
|
|
|
28
29
|
// ============================================================================
|
|
29
30
|
// ACTIONS BAR - Interactive buttons that change based on resource kind
|
|
@@ -64,7 +65,7 @@ interface ResourceActionsBarProps {
|
|
|
64
65
|
renderPortForward?: (props: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => React.ReactNode
|
|
65
66
|
|
|
66
67
|
// Delete
|
|
67
|
-
onDelete?: (params: { kind: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
68
|
+
onDelete?: (params: { kind: string; group?: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
68
69
|
isDeleting?: boolean
|
|
69
70
|
cascadeDependents?: CascadeDependent[]
|
|
70
71
|
cascadeLoading?: boolean
|
|
@@ -165,7 +166,7 @@ export function ResourceActionsBar({
|
|
|
165
166
|
|
|
166
167
|
function handleDeleteConfirm(force: boolean) {
|
|
167
168
|
onDelete?.(
|
|
168
|
-
{ kind: resource.kind, namespace: resource.namespace, name: resource.name, force },
|
|
169
|
+
{ kind: resource.kind, group: resource.group, namespace: resource.namespace, name: resource.name, force },
|
|
169
170
|
{
|
|
170
171
|
onSuccess: () => {
|
|
171
172
|
setShowDeleteConfirm(false)
|
|
@@ -183,7 +184,7 @@ export function ResourceActionsBar({
|
|
|
183
184
|
onOpenTerminal?.({
|
|
184
185
|
namespace: resource.namespace,
|
|
185
186
|
podName: resource.name,
|
|
186
|
-
containerName: containers[0],
|
|
187
|
+
containerName: getDefaultContainerName(data) || containers[0],
|
|
187
188
|
containers,
|
|
188
189
|
})
|
|
189
190
|
}
|
|
@@ -195,7 +196,7 @@ export function ResourceActionsBar({
|
|
|
195
196
|
namespace: resource.namespace,
|
|
196
197
|
podName: resource.name,
|
|
197
198
|
containers,
|
|
198
|
-
containerName,
|
|
199
|
+
containerName: containerName || getDefaultContainerName(data),
|
|
199
200
|
})
|
|
200
201
|
}
|
|
201
202
|
}
|
|
@@ -43,3 +43,106 @@ describe('ResourceRendererDispatch', () => {
|
|
|
43
43
|
expect(html).toContain('none')
|
|
44
44
|
})
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
function renderKind(kind: string, data: any, namespace = ''): string {
|
|
48
|
+
return renderToString(
|
|
49
|
+
<ResourceRendererDispatch
|
|
50
|
+
resource={{ kind, namespace, name: data?.metadata?.name || 'x' }}
|
|
51
|
+
data={data}
|
|
52
|
+
onCopy={() => {}}
|
|
53
|
+
copied={null}
|
|
54
|
+
showCommonSections={false}
|
|
55
|
+
/>,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('ClusterPolicy kind collision (nvidia.com vs kyverno.io)', () => {
|
|
60
|
+
it('routes nvidia.com ClusterPolicy to the GPU Operator renderer', () => {
|
|
61
|
+
const html = renderKind('clusterpolicies', {
|
|
62
|
+
apiVersion: 'nvidia.com/v1',
|
|
63
|
+
kind: 'ClusterPolicy',
|
|
64
|
+
metadata: { name: 'cluster-policy' },
|
|
65
|
+
spec: { driver: { enabled: true }, devicePlugin: { enabled: true } },
|
|
66
|
+
status: { state: 'ready' },
|
|
67
|
+
})
|
|
68
|
+
expect(html).toContain('Operator Status')
|
|
69
|
+
expect(html).not.toContain('Rules')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('routes kyverno.io ClusterPolicy to the Kyverno renderer', () => {
|
|
73
|
+
const html = renderKind('clusterpolicies', {
|
|
74
|
+
apiVersion: 'kyverno.io/v1',
|
|
75
|
+
kind: 'ClusterPolicy',
|
|
76
|
+
metadata: { name: 'require-labels' },
|
|
77
|
+
spec: { rules: [{ name: 'check-labels' }] },
|
|
78
|
+
})
|
|
79
|
+
expect(html).not.toContain('Operator Status')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('DRA renderers dispatch', () => {
|
|
84
|
+
it('renders ResourceClaim with allocation sections (not GenericRenderer)', () => {
|
|
85
|
+
const html = renderKind('resourceclaims', {
|
|
86
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
87
|
+
kind: 'ResourceClaim',
|
|
88
|
+
metadata: { name: 'gpu-claim', namespace: 'ml' },
|
|
89
|
+
spec: { devices: { requests: [{ name: 'gpu', exactly: { deviceClassName: 'gpu.nvidia.com', count: 1 } }] } },
|
|
90
|
+
status: {
|
|
91
|
+
allocation: { devices: { results: [{ request: 'gpu', driver: 'gpu.nvidia.com', pool: 'node-1', device: 'gpu-0' }] } },
|
|
92
|
+
reservedFor: [{ resource: 'pods', name: 'train-1' }],
|
|
93
|
+
},
|
|
94
|
+
}, 'ml')
|
|
95
|
+
expect(html).toContain('Device Requests')
|
|
96
|
+
expect(html).toContain('gpu.nvidia.com')
|
|
97
|
+
expect(html).toContain('Reserved For')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not treat an empty allocation block as allocated', () => {
|
|
101
|
+
const html = renderKind('resourceclaims', {
|
|
102
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
103
|
+
kind: 'ResourceClaim',
|
|
104
|
+
metadata: { name: 'partial-claim', namespace: 'ml' },
|
|
105
|
+
spec: { devices: { requests: [{ name: 'gpu', exactly: { deviceClassName: 'gpu.example.com' } }] } },
|
|
106
|
+
status: { allocation: {} },
|
|
107
|
+
}, 'ml')
|
|
108
|
+
expect(html).toContain('Not allocated')
|
|
109
|
+
expect(html).not.toContain('Allocated but unreserved')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('reads v1beta1 request shape (deviceClassName at request level)', () => {
|
|
113
|
+
const html = renderKind('resourceclaims', {
|
|
114
|
+
apiVersion: 'resource.k8s.io/v1beta1',
|
|
115
|
+
kind: 'ResourceClaim',
|
|
116
|
+
metadata: { name: 'old-claim', namespace: 'ml' },
|
|
117
|
+
spec: { devices: { requests: [{ name: 'gpu', deviceClassName: 'gpu.example.com' }] } },
|
|
118
|
+
}, 'ml')
|
|
119
|
+
expect(html).toContain('gpu.example.com')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders ResourceSlice with device inventory', () => {
|
|
123
|
+
const html = renderKind('resourceslices', {
|
|
124
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
125
|
+
kind: 'ResourceSlice',
|
|
126
|
+
metadata: { name: 'node-1-gpus' },
|
|
127
|
+
spec: {
|
|
128
|
+
driver: 'gpu.nvidia.com',
|
|
129
|
+
pool: { name: 'node-1' },
|
|
130
|
+
nodeName: 'node-1',
|
|
131
|
+
devices: [{ name: 'gpu-0', attributes: { productName: { string: 'H100' } } }],
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
expect(html).toContain('Slice Info')
|
|
135
|
+
expect(html).toContain('H100')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('renders DeviceClass selectors', () => {
|
|
139
|
+
const html = renderKind('deviceclasses', {
|
|
140
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
141
|
+
kind: 'DeviceClass',
|
|
142
|
+
metadata: { name: 'gpu.nvidia.com' },
|
|
143
|
+
spec: { selectors: [{ cel: { expression: "device.driver == 'gpu.nvidia.com'" } }] },
|
|
144
|
+
})
|
|
145
|
+
expect(html).toContain('Selectors (1)')
|
|
146
|
+
expect(html).toContain('device.driver')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -50,6 +50,8 @@ import { getNodePoolStatus, getNodeClaimStatus, getEC2NodeClassStatus } from '..
|
|
|
50
50
|
import { getScaledObjectStatus, getScaledJobStatus } from '../resources/resource-utils-keda'
|
|
51
51
|
import { getServiceMonitorStatus, getPrometheusRuleStatus, getPodMonitorStatus } from '../resources/resource-utils-prometheus'
|
|
52
52
|
import { getPolicyReportStatus, getKyvernoPolicyStatus } from '../resources/resource-utils-kyverno'
|
|
53
|
+
import { getResourceClaimStatus, getResourceClaimTemplateStatus, getDeviceClassStatus, getResourceSliceStatus } from '../resources/resource-utils-dra'
|
|
54
|
+
import { getNvidiaClusterPolicyStatus, getNvidiaDriverStatus } from '../resources/resource-utils-nvidia'
|
|
53
55
|
import { getBackupStatus, getRestoreStatus, getScheduleStatus, getBSLStatus } from '../resources/resource-utils-velero'
|
|
54
56
|
import {
|
|
55
57
|
getVirtualServiceStatus,
|
|
@@ -208,6 +210,12 @@ import {
|
|
|
208
210
|
CompositionRenderer,
|
|
209
211
|
CompositionRevisionRenderer,
|
|
210
212
|
XRDRenderer,
|
|
213
|
+
ResourceClaimRenderer,
|
|
214
|
+
ResourceClaimTemplateRenderer,
|
|
215
|
+
DeviceClassRenderer,
|
|
216
|
+
ResourceSliceRenderer,
|
|
217
|
+
NvidiaClusterPolicyRenderer,
|
|
218
|
+
NvidiaDriverRenderer,
|
|
211
219
|
} from '../resources/renderers'
|
|
212
220
|
import type { ComposedRefStatus } from '../resources/renderers/CompositeRenderer'
|
|
213
221
|
import {
|
|
@@ -318,6 +326,8 @@ const KNOWN_KINDS = new Set([
|
|
|
318
326
|
'triggerauthentications', 'clustertriggerauthentications',
|
|
319
327
|
'servicemonitors', 'prometheusrules', 'podmonitors',
|
|
320
328
|
'policyreports', 'clusterpolicyreports', 'kyvernopolicies', 'clusterpolicies',
|
|
329
|
+
'resourceclaims', 'resourceclaimtemplates', 'deviceclasses', 'resourceslices',
|
|
330
|
+
'nvidiadrivers',
|
|
321
331
|
'vulnerabilityreports', 'configauditreports', 'exposedsecretreports',
|
|
322
332
|
'rbacassessmentreports', 'clusterrbacassessmentreports',
|
|
323
333
|
'clustercompliancereports', 'sbomreports', 'clustersbomreports',
|
|
@@ -557,7 +567,14 @@ export function ResourceRendererDispatch({
|
|
|
557
567
|
{kind === 'prometheusrules' && <PrometheusRuleRenderer data={data} />}
|
|
558
568
|
{kind === 'podmonitors' && <PodMonitorRenderer data={data} />}
|
|
559
569
|
{(kind === 'policyreports' || kind === 'clusterpolicyreports') && <PolicyReportRenderer data={data} />}
|
|
560
|
-
{(kind === 'kyvernopolicies' || kind === 'clusterpolicies') && <KyvernoPolicyRenderer data={data} />}
|
|
570
|
+
{(kind === 'kyvernopolicies' || (kind === 'clusterpolicies' && !data?.apiVersion?.startsWith('nvidia.com/'))) && <KyvernoPolicyRenderer data={data} />}
|
|
571
|
+
{(kind === 'clusterpolicies' && data?.apiVersion?.startsWith('nvidia.com/')) && <NvidiaClusterPolicyRenderer data={data} />}
|
|
572
|
+
{kind === 'nvidiadrivers' && <NvidiaDriverRenderer data={data} />}
|
|
573
|
+
{/* DRA (resource.k8s.io) */}
|
|
574
|
+
{kind === 'resourceclaims' && <ResourceClaimRenderer data={data} onNavigate={onNavigate} />}
|
|
575
|
+
{kind === 'resourceclaimtemplates' && <ResourceClaimTemplateRenderer data={data} />}
|
|
576
|
+
{kind === 'deviceclasses' && <DeviceClassRenderer data={data} />}
|
|
577
|
+
{kind === 'resourceslices' && <ResourceSliceRenderer data={data} onNavigate={onNavigate} />}
|
|
561
578
|
{kind === 'backups' && data.apiVersion?.includes('cnpg.io') && <CNPGBackupRenderer data={data} onNavigate={onNavigate} />}
|
|
562
579
|
{kind === 'backups' && !data.apiVersion?.includes('cnpg.io') && <VeleroBackupRenderer data={data} />}
|
|
563
580
|
{kind === 'restores' && <VeleroRestoreRenderer data={data} />}
|
|
@@ -760,7 +777,15 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
760
777
|
if (k === 'clustercompliancereports') return getClusterComplianceReportStatus(data)
|
|
761
778
|
if (k === 'sbomreports' || k === 'clustersbomreports') return getSbomReportStatus(data)
|
|
762
779
|
if (k === 'policyreports' || k === 'clusterpolicyreports') return getPolicyReportStatus(data)
|
|
763
|
-
if (k === 'kyvernopolicies' || k === 'clusterpolicies')
|
|
780
|
+
if (k === 'kyvernopolicies' || k === 'clusterpolicies') {
|
|
781
|
+
if (data?.apiVersion?.startsWith('nvidia.com/')) return getNvidiaClusterPolicyStatus(data)
|
|
782
|
+
return getKyvernoPolicyStatus(data)
|
|
783
|
+
}
|
|
784
|
+
if (k === 'nvidiadrivers') return getNvidiaDriverStatus(data)
|
|
785
|
+
if (k === 'resourceclaims') return getResourceClaimStatus(data)
|
|
786
|
+
if (k === 'resourceclaimtemplates') return getResourceClaimTemplateStatus(data)
|
|
787
|
+
if (k === 'deviceclasses') return getDeviceClassStatus(data)
|
|
788
|
+
if (k === 'resourceslices') return getResourceSliceStatus(data)
|
|
764
789
|
if (k === 'backups') {
|
|
765
790
|
if (data.apiVersion?.includes('cnpg.io')) return getCNPGBackupStatus(data)
|
|
766
791
|
return getBackupStatus(data)
|