@skyhook-io/k8s-ui 1.13.4 → 1.14.1
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 +4 -4
- package/src/components/gitops/GitOpsTableView.tsx +168 -11
- package/src/components/gitops/applicationset-row.test.ts +214 -0
- package/src/components/gitops/detail-helpers.test.ts +54 -0
- package/src/components/gitops/detail-helpers.ts +13 -3
- package/src/components/gitops/index.ts +1 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +7 -0
- package/src/components/resources/ResourcesView.tsx +259 -80
- package/src/components/resources/column-header-width.test.ts +75 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +100 -1
- package/src/components/resources/kyverno-modern-posture.test.ts +36 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +29 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +3 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +69 -3
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +6 -2
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +12 -7
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +92 -1
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +64 -8
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +47 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +16 -1
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +44 -2
- package/src/components/resources/renderers/cnpg-cells.tsx +12 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +8 -4
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +13 -5
- package/src/components/resources/resource-utils-cnpg.test.ts +142 -2
- package/src/components/resources/resource-utils-cnpg.ts +132 -7
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +14 -0
- package/src/components/resources/resource-utils-kueue.ts +4 -2
- package/src/components/resources/resource-utils-kyverno-modern.ts +3 -1
- package/src/components/resources/resource-utils-kyverno.ts +51 -2
- package/src/components/resources/resource-utils-velero.ts +32 -0
- package/src/components/resources/rules-column-width.test.ts +42 -0
- package/src/components/shared/ResourceRendererDispatch.test.tsx +75 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +24 -6
- package/src/components/topology/GroupNode.tsx +6 -2
- package/src/components/topology/topology.css +4 -1
- package/src/components/ui/Collapse.tsx +9 -1
- package/src/components/ui/RowActionMenu.tsx +4 -0
- package/src/components/ui/SelectMenu.tsx +10 -1
- package/src/components/ui/drawer-components.tsx +4 -2
- package/src/components/workload/WorkloadView.tsx +9 -2
- package/src/types/gitops.ts +51 -0
|
@@ -503,6 +503,20 @@ const KNOWN_KINDS = new Set([
|
|
|
503
503
|
'functions', 'configurations',
|
|
504
504
|
])
|
|
505
505
|
|
|
506
|
+
// Cluster topology owns resources across the core, bootstrap, control-plane,
|
|
507
|
+
// and infrastructure contracts. Keep this explicit: a suffix/substring match
|
|
508
|
+
// would also accept unrelated groups such as extension.cluster.x-k8s.io.
|
|
509
|
+
const CAPI_TOPOLOGY_GROUPS = [
|
|
510
|
+
'cluster.x-k8s.io',
|
|
511
|
+
'bootstrap.cluster.x-k8s.io',
|
|
512
|
+
'controlplane.cluster.x-k8s.io',
|
|
513
|
+
'infrastructure.cluster.x-k8s.io',
|
|
514
|
+
] as const
|
|
515
|
+
|
|
516
|
+
function isCAPITopologyGroup(apiVersion?: string): boolean {
|
|
517
|
+
return CAPI_TOPOLOGY_GROUPS.some(group => isApiGroup(apiVersion, group))
|
|
518
|
+
}
|
|
519
|
+
|
|
506
520
|
// ============================================================================
|
|
507
521
|
// RESOURCE CONTENT - Delegates to specific renderers
|
|
508
522
|
// ============================================================================
|
|
@@ -658,11 +672,13 @@ export function ResourceRendererDispatch({
|
|
|
658
672
|
// them, and `subscriptions` is Knative's as well. Adding a kind to KNOWN_KINDS
|
|
659
673
|
// with a positively-gated render line and forgetting this list is what makes a
|
|
660
674
|
// foreign CRD render a BLANK drawer rather than the generic one.
|
|
675
|
+
// CAPI's `machines` and `machinesets` need the same protection because another
|
|
676
|
+
// API group can publish either plural without inheriting CAPI presentation.
|
|
661
677
|
const isGroupGatedKind =
|
|
662
678
|
kind === 'clusters' || kind === 'backups' || kind === 'scheduledbackups' || kind === 'poolers'
|
|
663
679
|
|| kind === 'objectstores' || kind === 'databases' || kind === 'publications'
|
|
664
680
|
|| kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs'
|
|
665
|
-
|| kind === 'policies' || kind === 'rollouts'
|
|
681
|
+
|| kind === 'policies' || kind === 'rollouts' || kind === 'machines' || kind === 'machinesets'
|
|
666
682
|
const isCNPGApiVersion = isApiGroup(data?.apiVersion, CNPG_GROUP)
|
|
667
683
|
const groupGatedMatched =
|
|
668
684
|
(kind === 'clusters' && (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'cluster.x-k8s.io')))
|
|
@@ -675,6 +691,8 @@ export function ResourceRendererDispatch({
|
|
|
675
691
|
&& (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'messaging.knative.dev')))
|
|
676
692
|
|| (kind === 'policies' && isApiGroup(data?.apiVersion, 'kyverno.io'))
|
|
677
693
|
|| (kind === 'rollouts' && isArgoRolloutResource(data))
|
|
694
|
+
|| ((kind === 'machines' || kind === 'machinesets')
|
|
695
|
+
&& isApiGroup(data?.apiVersion, 'cluster.x-k8s.io'))
|
|
678
696
|
const groupGatedFallthrough = isGroupGatedKind && !groupGatedMatched
|
|
679
697
|
|
|
680
698
|
const calicoApiVersionMatched = isCalicoApiVersion(data?.apiVersion)
|
|
@@ -862,15 +880,15 @@ export function ResourceRendererDispatch({
|
|
|
862
880
|
{kind === 'scheduledbackups' && isApiGroup(data?.apiVersion, CNPG_GROUP) && <CNPGScheduledBackupRenderer data={data} onNavigate={onNavigate} />}
|
|
863
881
|
{kind === 'poolers' && isApiGroup(data?.apiVersion, CNPG_GROUP) && <CNPGPoolerRenderer data={data} onNavigate={onNavigate} />}
|
|
864
882
|
{/* Cluster API (CAPI) */}
|
|
865
|
-
{'topology.cluster.x-k8s.io/owned' in (data?.metadata?.labels ?? {}) && data?.apiVersion
|
|
883
|
+
{'topology.cluster.x-k8s.io/owned' in (data?.metadata?.labels ?? {}) && isCAPITopologyGroup(data?.apiVersion) && (
|
|
866
884
|
<AlertBanner
|
|
867
885
|
variant="warning"
|
|
868
886
|
title="Topology-controlled — this resource is managed by ClusterClass. Manual changes will be reconciled back."
|
|
869
887
|
/>
|
|
870
888
|
)}
|
|
871
|
-
{kind === 'machines' && data?.apiVersion
|
|
889
|
+
{kind === 'machines' && isApiGroup(data?.apiVersion, 'cluster.x-k8s.io') && <CAPIMachineRenderer data={data} onNavigate={onNavigate} />}
|
|
872
890
|
{kind === 'machinedeployments' && <CAPIMachineDeploymentRenderer data={data} onNavigate={onNavigate} />}
|
|
873
|
-
{kind === 'machinesets' && data?.apiVersion
|
|
891
|
+
{kind === 'machinesets' && isApiGroup(data?.apiVersion, 'cluster.x-k8s.io') && <CAPIMachineSetRenderer data={data} onNavigate={onNavigate} />}
|
|
874
892
|
{kind === 'machinepools' && <CAPIMachinePoolRenderer data={data} onNavigate={onNavigate} />}
|
|
875
893
|
{kind === 'kubeadmcontrolplanes' && <CAPIKubeadmControlPlaneRenderer data={data} onNavigate={onNavigate} />}
|
|
876
894
|
{kind === 'clusterclasses' && <CAPIClusterClassRenderer data={data} />}
|
|
@@ -1187,9 +1205,9 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
1187
1205
|
// Third-party `clusters` CRDs (KubeBlocks, Redis/Valkey) fall through to the
|
|
1188
1206
|
// generic handling below instead of getting a fabricated PostgreSQL status.
|
|
1189
1207
|
}
|
|
1190
|
-
if (k === 'machines' && data.apiVersion
|
|
1208
|
+
if (k === 'machines' && isApiGroup(data.apiVersion, 'cluster.x-k8s.io')) return getMachineStatus(data)
|
|
1191
1209
|
if (k === 'machinedeployments') return getMachineDeploymentStatus(data)
|
|
1192
|
-
if (k === 'machinesets') return getMachineSetStatus(data)
|
|
1210
|
+
if (k === 'machinesets' && isApiGroup(data.apiVersion, 'cluster.x-k8s.io')) return getMachineSetStatus(data)
|
|
1193
1211
|
if (k === 'machinepools') return getMachinePoolStatus(data)
|
|
1194
1212
|
if (k === 'kubeadmcontrolplanes') return getKCPStatus(data)
|
|
1195
1213
|
if (k === 'clusterclasses') return getClusterClassStatus(data)
|
|
@@ -159,6 +159,10 @@ export const GroupNode = memo(function GroupNode({
|
|
|
159
159
|
</div>
|
|
160
160
|
)
|
|
161
161
|
|
|
162
|
+
// Chip and cardGrid roots must NOT counter-scale with zoom (group-header-scaled):
|
|
163
|
+
// the transform shrinks the visuals but the React Flow wrapper — the hitbox,
|
|
164
|
+
// drag target, and edge-attachment box — keeps the full ELK-computed size, so
|
|
165
|
+
// past zoom 0.7 the interactive area extends well outside the drawn box.
|
|
162
166
|
// ── Level 1: Chip (compact collapsed card) ──
|
|
163
167
|
if (displayLevel === 'chip') {
|
|
164
168
|
// Size tier: 0-9 → 0, 10-99 → 1, 100-999 → 2, 1000+ → 3
|
|
@@ -178,7 +182,7 @@ export const GroupNode = memo(function GroupNode({
|
|
|
178
182
|
<>
|
|
179
183
|
{handles}
|
|
180
184
|
<div
|
|
181
|
-
className="rounded-xl
|
|
185
|
+
className="rounded-xl overflow-hidden"
|
|
182
186
|
style={{ ...getBorderStyle(), ...getHeaderBgStyle(), padding: chipPadding, width: width || '100%', height: height || '100%' }}
|
|
183
187
|
>
|
|
184
188
|
{/* Header row: icon + name + count + status + controls */}
|
|
@@ -220,7 +224,7 @@ export const GroupNode = memo(function GroupNode({
|
|
|
220
224
|
<>
|
|
221
225
|
{handles}
|
|
222
226
|
<div
|
|
223
|
-
className="rounded-xl overflow-hidden
|
|
227
|
+
className="rounded-xl overflow-hidden"
|
|
224
228
|
style={{ ...getBorderStyle(), width: width || '100%', height: height || '100%' }}
|
|
225
229
|
>
|
|
226
230
|
{/* Header bar with level controls */}
|
|
@@ -65,7 +65,10 @@
|
|
|
65
65
|
box-shadow: none !important;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/* Group header scaling using CSS variable set by ViewportController
|
|
68
|
+
/* Group header scaling using CSS variable set by ViewportController.
|
|
69
|
+
Only safe on children INSIDE a group container (the expanded-group header row):
|
|
70
|
+
on a node's root content it desyncs the visible box from the React Flow
|
|
71
|
+
wrapper, whose full-size hitbox and edge anchors don't follow the transform. */
|
|
69
72
|
.group-header-scaled {
|
|
70
73
|
transform: scale(var(--group-header-scale, 1));
|
|
71
74
|
transform-origin: top left;
|
|
@@ -40,7 +40,15 @@ export function Collapse({
|
|
|
40
40
|
className={clsx('grid transition-[grid-template-rows] duration-200 ease-out', className)}
|
|
41
41
|
style={{ gridTemplateRows: open ? '1fr' : '0fr' }}
|
|
42
42
|
>
|
|
43
|
-
|
|
43
|
+
{/* `relative` is load-bearing. Collapsed content is still laid out at full
|
|
44
|
+
height and only clipped by `overflow-hidden` — but an absolutely-positioned
|
|
45
|
+
descendant (Tailwind's `sr-only` is position:absolute) resolves its
|
|
46
|
+
containing block ABOVE this wrapper, so the clip doesn't apply to it. It
|
|
47
|
+
then sits at its static position, hundreds of px below a collapsed section,
|
|
48
|
+
and extends the enclosing scroll container's scrollable overflow: the drawer
|
|
49
|
+
scrolls past its own content into blank space. Anchoring here puts those
|
|
50
|
+
boxes back inside the clip, so a collapsed section contributes nothing. */}
|
|
51
|
+
<div className="relative overflow-hidden" inert={!open || undefined}>{render ? children : null}</div>
|
|
44
52
|
</div>
|
|
45
53
|
)
|
|
46
54
|
}
|
|
@@ -67,6 +67,10 @@ export function RowActionMenu({ items, ariaLabel = 'Row actions', compact = true
|
|
|
67
67
|
const triggerSize = compact ? 'p-1' : 'p-1.5'
|
|
68
68
|
const iconSize = compact ? 'h-4 w-4' : 'h-5 w-5'
|
|
69
69
|
|
|
70
|
+
// A trigger that opens an empty menu is a dead end - render nothing and let
|
|
71
|
+
// the cell collapse instead.
|
|
72
|
+
if (items.length === 0) return null
|
|
73
|
+
|
|
70
74
|
return (
|
|
71
75
|
<div ref={ref} className="relative inline-block">
|
|
72
76
|
<button
|
|
@@ -12,15 +12,21 @@ export function SelectMenu({
|
|
|
12
12
|
options,
|
|
13
13
|
onChange,
|
|
14
14
|
ariaLabel,
|
|
15
|
+
ariaDescribedBy,
|
|
15
16
|
className,
|
|
16
17
|
searchPlaceholder,
|
|
18
|
+
disabled = false,
|
|
19
|
+
id,
|
|
17
20
|
}: {
|
|
18
21
|
value: string
|
|
19
22
|
options: SelectMenuOption[]
|
|
20
23
|
onChange: (value: string) => void
|
|
21
24
|
ariaLabel: string
|
|
25
|
+
ariaDescribedBy?: string
|
|
22
26
|
className?: string
|
|
23
27
|
searchPlaceholder?: string
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
id?: string
|
|
24
30
|
}) {
|
|
25
31
|
const [open, setOpen] = useState(false)
|
|
26
32
|
const [query, setQuery] = useState('')
|
|
@@ -109,8 +115,10 @@ export function SelectMenu({
|
|
|
109
115
|
>
|
|
110
116
|
<button
|
|
111
117
|
ref={triggerRef}
|
|
118
|
+
id={id}
|
|
112
119
|
type="button"
|
|
113
120
|
aria-label={ariaLabel}
|
|
121
|
+
aria-describedby={ariaDescribedBy}
|
|
114
122
|
aria-haspopup="listbox"
|
|
115
123
|
aria-expanded={open}
|
|
116
124
|
aria-controls={open ? listboxId : undefined}
|
|
@@ -125,7 +133,8 @@ export function SelectMenu({
|
|
|
125
133
|
}
|
|
126
134
|
setOpen(!open)
|
|
127
135
|
}}
|
|
128
|
-
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
className="flex h-8 w-full items-center justify-between gap-2 rounded-md border border-theme-border bg-theme-elevated px-2.5 text-xs text-theme-text-primary transition-colors hover:bg-theme-hover disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-theme-elevated"
|
|
129
138
|
>
|
|
130
139
|
<span className="truncate">{selected?.label}</span>
|
|
131
140
|
<ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', open && 'rotate-180')} />
|
|
@@ -101,7 +101,8 @@ export function Section({ title, icon: Icon, children, defaultExpanded = true, c
|
|
|
101
101
|
className="grid transition-[grid-template-rows] duration-200 ease-out"
|
|
102
102
|
style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
|
|
103
103
|
>
|
|
104
|
-
|
|
104
|
+
{/* `relative` is load-bearing — see the note in ui/Collapse.tsx. */}
|
|
105
|
+
<div className="relative overflow-hidden">
|
|
105
106
|
<div className={contentClassName ?? 'pl-6'}>{children}</div>
|
|
106
107
|
</div>
|
|
107
108
|
</div>
|
|
@@ -131,7 +132,8 @@ export function ExpandableSection({ title, children, defaultExpanded = true }: E
|
|
|
131
132
|
className="grid transition-[grid-template-rows] duration-200 ease-out"
|
|
132
133
|
style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
|
|
133
134
|
>
|
|
134
|
-
|
|
135
|
+
{/* `relative` is load-bearing — see the note in ui/Collapse.tsx. */}
|
|
136
|
+
<div className="relative overflow-hidden">
|
|
135
137
|
<div className="ml-5">{children}</div>
|
|
136
138
|
</div>
|
|
137
139
|
</div>
|
|
@@ -880,8 +880,15 @@ export function WorkloadView({
|
|
|
880
880
|
{/* Success animation overlay */}
|
|
881
881
|
{saveSuccess && <SaveSuccessAnimation />}
|
|
882
882
|
|
|
883
|
-
{/* Content — viewTransitionName scopes View Transitions API cross-fade to this element
|
|
884
|
-
|
|
883
|
+
{/* Content — viewTransitionName scopes View Transitions API cross-fade to this element.
|
|
884
|
+
`relative` is the backstop for absolutely-positioned descendants (Tailwind's
|
|
885
|
+
`sr-only` is position:absolute). The drawer frame keeps overflow visible at idle so
|
|
886
|
+
popovers aren't clipped, and the app's content column clips only horizontally, so one
|
|
887
|
+
that resolves its containing block above this scroller escapes into the DOCUMENT's
|
|
888
|
+
scrollable overflow — a second, page-level scrollbar. Anchoring here confines the
|
|
889
|
+
damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
|
|
890
|
+
place. */}
|
|
891
|
+
<div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
|
|
885
892
|
{!resource ? (
|
|
886
893
|
// Fill the drawer body so the loading logo centers in it, not in a
|
|
887
894
|
// 128px box pinned to the top (matches the splash/PaneLoader centering).
|
package/src/types/gitops.ts
CHANGED
|
@@ -359,6 +359,57 @@ export function argoStatusToGitOpsStatus(status: ArgoAppStatus): GitOpsStatus {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Maps an Argo CD ApplicationSet's conditions to a GitOpsStatus.
|
|
364
|
+
*
|
|
365
|
+
* An ApplicationSet generates Applications; it does not apply declared state
|
|
366
|
+
* to a cluster itself. Sync answers "is the declared state applied", so there
|
|
367
|
+
* is no honest sync value here and it stays Unknown - the generated
|
|
368
|
+
* Applications each carry their own.
|
|
369
|
+
*
|
|
370
|
+
* Health answers whether the generator is doing its job, which its conditions
|
|
371
|
+
* do report, in a vocabulary of their own (ErrorOccurred / ParametersGenerated
|
|
372
|
+
* / ResourcesUpToDate - never Ready):
|
|
373
|
+
* - ErrorOccurred=True → Degraded, carrying the controller's message
|
|
374
|
+
* - ResourcesUpToDate=True → Healthy
|
|
375
|
+
* - neither → Unknown (status not populated yet)
|
|
376
|
+
*
|
|
377
|
+
* The Resources view renders the same three conditions through
|
|
378
|
+
* getArgoApplicationSetStatus for its own badge vocabulary. The two read the
|
|
379
|
+
* same signals and must agree on which condition wins.
|
|
380
|
+
*/
|
|
381
|
+
export function argoApplicationSetConditionsToGitOpsStatus(
|
|
382
|
+
conditions: FluxCondition[]
|
|
383
|
+
): GitOpsStatus {
|
|
384
|
+
const error = conditions.find(c => c.type === 'ErrorOccurred' && c.status === 'True')
|
|
385
|
+
if (error) {
|
|
386
|
+
return {
|
|
387
|
+
sync: 'Unknown',
|
|
388
|
+
health: 'Degraded',
|
|
389
|
+
message: error.message || 'Generation failed',
|
|
390
|
+
lastSyncTime: error.lastTransitionTime,
|
|
391
|
+
suspended: false,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const upToDate = conditions.find(c => c.type === 'ResourcesUpToDate')
|
|
396
|
+
if (upToDate?.status === 'True') {
|
|
397
|
+
return {
|
|
398
|
+
sync: 'Unknown',
|
|
399
|
+
health: 'Healthy',
|
|
400
|
+
lastSyncTime: upToDate.lastTransitionTime,
|
|
401
|
+
suspended: false,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
sync: 'Unknown',
|
|
407
|
+
health: 'Unknown',
|
|
408
|
+
message: 'Status cannot be determined',
|
|
409
|
+
suspended: false,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
362
413
|
// ============================================================================
|
|
363
414
|
// INVENTORY PARSERS
|
|
364
415
|
// ============================================================================
|