@skyhook-io/k8s-ui 1.14.0 → 1.14.2
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/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/issues/IssuesView.tsx +1 -1
- package/src/components/issues/types.ts +2 -0
- package/src/components/resources/ResourcesView.tsx +382 -141
- package/src/components/resources/column-header-width.test.ts +75 -0
- package/src/components/resources/column-settings-merge.test.ts +104 -0
- package/src/components/resources/istio-gateway-filter.test.ts +5 -3
- package/src/components/resources/istio-policy-accessors.test.ts +193 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +11 -24
- package/src/components/resources/renderers/eso-cells.tsx +0 -5
- package/src/components/resources/renderers/istio-cells.tsx +36 -26
- package/src/components/resources/renderers/kueue-cells.tsx +0 -9
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +3 -3
- package/src/components/resources/renderers/ray-cells.tsx +12 -4
- package/src/components/resources/renderers/volcano-cells.tsx +0 -9
- package/src/components/resources/resource-utils-cnpg.ts +5 -0
- package/src/components/resources/resource-utils-eso.test.ts +56 -0
- package/src/components/resources/resource-utils-eso.ts +37 -5
- package/src/components/resources/resource-utils-istio.ts +152 -44
- package/src/components/resources/resource-utils-kyverno.ts +3 -6
- package/src/components/resources/resource-utils-prometheus.ts +51 -23
- package/src/components/resources/resource-utils-ray.test.ts +36 -0
- package/src/components/resources/resource-utils-ray.ts +6 -0
- package/src/components/resources/resource-utils.ts +166 -18
- package/src/components/resources/rules-column-width.test.ts +42 -0
- package/src/components/resources/status-accessors.test.ts +363 -0
- package/src/components/resources/status-overclaim.test.ts +158 -0
- package/src/components/shared/ResourceRendererDispatch.test.tsx +75 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +24 -6
- package/src/components/timeline/DiffViewer.test.tsx +26 -0
- package/src/components/timeline/DiffViewer.tsx +16 -12
- package/src/components/topology/GroupNode.tsx +6 -2
- package/src/components/topology/TopologyGraph.tsx +7 -2
- package/src/components/topology/topology.css +4 -1
- package/src/components/ui/Collapse.tsx +18 -3
- package/src/components/ui/ConfirmDialog.test.tsx +37 -0
- package/src/components/ui/ConfirmDialog.tsx +3 -1
- package/src/components/ui/RowActionMenu.tsx +4 -0
- package/src/components/ui/drawer-components.tsx +5 -3
- package/src/components/workload/WorkloadView.tsx +11 -3
- 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)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { DiffViewer } from "./DiffViewer";
|
|
5
|
+
|
|
6
|
+
describe("DiffViewer", () => {
|
|
7
|
+
it("labels added, removed, and changed values without relying on color", () => {
|
|
8
|
+
const html = renderToStaticMarkup(
|
|
9
|
+
<DiffViewer
|
|
10
|
+
diff={{
|
|
11
|
+
summary: "Three fields changed",
|
|
12
|
+
fields: [
|
|
13
|
+
{ path: "spec.added", oldValue: null, newValue: "on" },
|
|
14
|
+
{ path: "spec.removed", oldValue: "on", newValue: null },
|
|
15
|
+
{ path: "spec.changed", oldValue: "v1", newValue: "v2" },
|
|
16
|
+
],
|
|
17
|
+
}}
|
|
18
|
+
/>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(html).toContain("Added value:");
|
|
22
|
+
expect(html).toContain("Removed value:");
|
|
23
|
+
expect(html).toContain("Old value:");
|
|
24
|
+
expect(html).toContain("New value:");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -16,7 +16,7 @@ export const DiffViewer = memo(function DiffViewer({ diff, compact = false }: Di
|
|
|
16
16
|
if (compact) {
|
|
17
17
|
return (
|
|
18
18
|
<div className="text-xs text-theme-text-secondary flex items-center gap-1">
|
|
19
|
-
<RefreshCw className="w-3 h-3" />
|
|
19
|
+
<RefreshCw className="w-3 h-3" aria-hidden />
|
|
20
20
|
<span>{diff.summary || `${diff.fields.length} field(s) changed`}</span>
|
|
21
21
|
</div>
|
|
22
22
|
)
|
|
@@ -27,7 +27,7 @@ export const DiffViewer = memo(function DiffViewer({ diff, compact = false }: Di
|
|
|
27
27
|
{/* Summary */}
|
|
28
28
|
{diff.summary && (
|
|
29
29
|
<div className="text-sm font-medium text-theme-text-secondary flex items-center gap-2">
|
|
30
|
-
<RefreshCw className="w-4 h-4 text-blue-400" />
|
|
30
|
+
<RefreshCw className="w-4 h-4 text-blue-400" aria-hidden />
|
|
31
31
|
{diff.summary}
|
|
32
32
|
</div>
|
|
33
33
|
)}
|
|
@@ -54,31 +54,35 @@ function FieldChangeRow({ field }: FieldChangeRowProps) {
|
|
|
54
54
|
return (
|
|
55
55
|
<div className="rounded bg-theme-surface/50 border border-theme-border px-3 py-2">
|
|
56
56
|
{/* Field path */}
|
|
57
|
-
<div className="text-xs font-mono text-theme-text-tertiary mb-1">{field.path}</div>
|
|
57
|
+
<div className="text-xs font-mono text-theme-text-tertiary mb-1 break-words [overflow-wrap:anywhere]">{field.path}</div>
|
|
58
58
|
|
|
59
59
|
{/* Values */}
|
|
60
|
-
<div className="flex items-center gap-2 text-sm">
|
|
60
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2 text-sm">
|
|
61
61
|
{isAdded ? (
|
|
62
62
|
<>
|
|
63
|
-
<Plus className="w-3.5 h-3.5 text-green-400 shrink-0" />
|
|
64
|
-
<span className="
|
|
63
|
+
<Plus className="w-3.5 h-3.5 text-green-400 shrink-0" aria-hidden />
|
|
64
|
+
<span className="sr-only">Added value: </span>
|
|
65
|
+
<span className="min-w-0 break-words text-green-400 [overflow-wrap:anywhere]">
|
|
65
66
|
{formatValue(field.newValue)}
|
|
66
67
|
</span>
|
|
67
68
|
</>
|
|
68
69
|
) : isRemoved ? (
|
|
69
70
|
<>
|
|
70
|
-
<Minus className="w-3.5 h-3.5 text-red-400 shrink-0" />
|
|
71
|
-
<span className="
|
|
71
|
+
<Minus className="w-3.5 h-3.5 text-red-400 shrink-0" aria-hidden />
|
|
72
|
+
<span className="sr-only">Removed value: </span>
|
|
73
|
+
<span className="min-w-0 break-words text-red-400 line-through [overflow-wrap:anywhere]">
|
|
72
74
|
{formatValue(field.oldValue)}
|
|
73
75
|
</span>
|
|
74
76
|
</>
|
|
75
77
|
) : isModified ? (
|
|
76
78
|
<>
|
|
77
|
-
<span className="
|
|
79
|
+
<span className="sr-only">Old value: </span>
|
|
80
|
+
<span className="min-w-0 break-words text-red-400 line-through [overflow-wrap:anywhere]">
|
|
78
81
|
{formatValue(field.oldValue)}
|
|
79
82
|
</span>
|
|
80
|
-
<ArrowRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
81
|
-
<span className="
|
|
83
|
+
<ArrowRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" aria-hidden />
|
|
84
|
+
<span className="sr-only">New value: </span>
|
|
85
|
+
<span className="min-w-0 break-words text-green-400 [overflow-wrap:anywhere]">
|
|
82
86
|
{formatValue(field.newValue)}
|
|
83
87
|
</span>
|
|
84
88
|
</>
|
|
@@ -124,7 +128,7 @@ export const DiffBadge = memo(function DiffBadge({ diff }: DiffBadgeProps) {
|
|
|
124
128
|
'bg-skyhook-500/10 text-skyhook-400 border border-skyhook-500/20'
|
|
125
129
|
)}
|
|
126
130
|
>
|
|
127
|
-
<RefreshCw className="w-3 h-3" />
|
|
131
|
+
<RefreshCw className="w-3 h-3" aria-hidden />
|
|
128
132
|
{diff.summary}
|
|
129
133
|
</span>
|
|
130
134
|
)
|
|
@@ -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 */}
|
|
@@ -34,7 +34,7 @@ import { GroupNode } from './GroupNode'
|
|
|
34
34
|
import { NEUTRAL_OWNER, type WorkloadFocus } from '../../utils/workload-colors'
|
|
35
35
|
import { ownershipOf } from '../../utils/topology-neighborhood'
|
|
36
36
|
import { buildHierarchicalElkGraph, applyHierarchicalLayout, getGroupKey, isGroupEffectivelyCollapsed, type GroupDisplayLevel } from './layout'
|
|
37
|
-
import type { Topology, TopologyNode, TopologyEdge, ViewMode, GroupingMode } from '../../types'
|
|
37
|
+
import type { Topology, TopologyNode, TopologyEdge, ViewMode, GroupingMode, HealthStatus } from '../../types'
|
|
38
38
|
import { pluralize } from '../../utils/pluralize'
|
|
39
39
|
import { foldHash } from '../../utils/structure-hash'
|
|
40
40
|
import { recordLayoutDuration, recordLayoutSkipped, recordStructureKeyDuration } from '../../perf'
|
|
@@ -408,6 +408,7 @@ export function TopologyGraph({
|
|
|
408
408
|
phase: string
|
|
409
409
|
restarts: number
|
|
410
410
|
containers: number
|
|
411
|
+
status?: HealthStatus
|
|
411
412
|
}>
|
|
412
413
|
|
|
413
414
|
// Find edges pointing to this pod group
|
|
@@ -425,7 +426,11 @@ export function TopologyGraph({
|
|
|
425
426
|
id: podId,
|
|
426
427
|
kind: 'Pod',
|
|
427
428
|
name: pod.name,
|
|
428
|
-
|
|
429
|
+
// The server computes pod health; pkg/health tracks a crash loop across
|
|
430
|
+
// the kubelet's Waiting->Running oscillation, which the phase alone
|
|
431
|
+
// hides — a crash-looping pod sits at Phase=Running. Deriving it here
|
|
432
|
+
// again would rebuild that logic in a second place and get it wrong.
|
|
433
|
+
status: pod.status ?? 'unknown',
|
|
429
434
|
data: {
|
|
430
435
|
...podGroupNode.data,
|
|
431
436
|
namespace: pod.namespace,
|
|
@@ -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;
|
|
@@ -37,10 +37,21 @@ export function Collapse({
|
|
|
37
37
|
const render = !mountLazily || hasOpened
|
|
38
38
|
return (
|
|
39
39
|
<div
|
|
40
|
-
className={clsx(
|
|
40
|
+
className={clsx(
|
|
41
|
+
'grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
41
44
|
style={{ gridTemplateRows: open ? '1fr' : '0fr' }}
|
|
42
45
|
>
|
|
43
|
-
|
|
46
|
+
{/* `relative` is load-bearing. Collapsed content is still laid out at full
|
|
47
|
+
height and only clipped by `overflow-hidden` — but an absolutely-positioned
|
|
48
|
+
descendant (Tailwind's `sr-only` is position:absolute) resolves its
|
|
49
|
+
containing block ABOVE this wrapper, so the clip doesn't apply to it. It
|
|
50
|
+
then sits at its static position, hundreds of px below a collapsed section,
|
|
51
|
+
and extends the enclosing scroll container's scrollable overflow: the drawer
|
|
52
|
+
scrolls past its own content into blank space. Anchoring here puts those
|
|
53
|
+
boxes back inside the clip, so a collapsed section contributes nothing. */}
|
|
54
|
+
<div className="relative overflow-hidden" inert={!open || undefined}>{render ? children : null}</div>
|
|
44
55
|
</div>
|
|
45
56
|
)
|
|
46
57
|
}
|
|
@@ -53,7 +64,11 @@ export function CollapseChevron({ open, className }: { open: boolean; className?
|
|
|
53
64
|
return (
|
|
54
65
|
<ChevronRight
|
|
55
66
|
aria-hidden="true"
|
|
56
|
-
className={clsx(
|
|
67
|
+
className={clsx(
|
|
68
|
+
'shrink-0 text-theme-text-tertiary transition-transform duration-200 motion-reduce:transition-none',
|
|
69
|
+
open && 'rotate-90',
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
57
72
|
/>
|
|
58
73
|
)
|
|
59
74
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { ConfirmDialog } from './ConfirmDialog'
|
|
5
|
+
|
|
6
|
+
vi.mock('./DialogPortal', () => ({
|
|
7
|
+
DialogPortal: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
const props = {
|
|
11
|
+
open: true,
|
|
12
|
+
onClose: () => {},
|
|
13
|
+
onConfirm: () => {},
|
|
14
|
+
title: 'Share this investigation?',
|
|
15
|
+
message: 'Organization members can read and continue this investigation.',
|
|
16
|
+
confirmLabel: 'Share with organization',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ConfirmDialog warning disclosure', () => {
|
|
20
|
+
it('can omit redundant boilerplate without removing the actual disclosure or actions', () => {
|
|
21
|
+
const html = renderToStaticMarkup(<ConfirmDialog {...props} variant="warning" showWarning={false} />)
|
|
22
|
+
expect(html).toContain(props.message)
|
|
23
|
+
expect(html).toContain(props.confirmLabel)
|
|
24
|
+
expect(html).toContain('Cancel')
|
|
25
|
+
expect(html).not.toContain('Please confirm you want to proceed')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('preserves warning boilerplate by default for existing callers', () => {
|
|
29
|
+
expect(renderToStaticMarkup(<ConfirmDialog {...props} variant="warning" />))
|
|
30
|
+
.toContain('Please confirm you want to proceed with this action.')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('preserves the irreversible-action warning for destructive callers', () => {
|
|
34
|
+
expect(renderToStaticMarkup(<ConfirmDialog {...props} />))
|
|
35
|
+
.toContain('This action cannot be undone.')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -18,6 +18,7 @@ interface ConfirmDialogProps {
|
|
|
18
18
|
isLoading?: boolean
|
|
19
19
|
isClosable?: boolean // Allow closing even when isLoading (e.g., for long-running ops the user can dismiss)
|
|
20
20
|
confirmDisabled?: boolean // Block the confirm action while custom content is invalid (e.g., bad YAML)
|
|
21
|
+
showWarning?: boolean
|
|
21
22
|
className?: string
|
|
22
23
|
children?: ReactNode // Optional custom content (e.g., checkboxes)
|
|
23
24
|
}
|
|
@@ -35,6 +36,7 @@ export function ConfirmDialog({
|
|
|
35
36
|
isLoading = false,
|
|
36
37
|
isClosable = false,
|
|
37
38
|
confirmDisabled = false,
|
|
39
|
+
showWarning = true,
|
|
38
40
|
className,
|
|
39
41
|
children,
|
|
40
42
|
}: ConfirmDialogProps) {
|
|
@@ -84,7 +86,7 @@ export function ConfirmDialog({
|
|
|
84
86
|
)}
|
|
85
87
|
|
|
86
88
|
{/* Warning message — hidden once the action is in progress */}
|
|
87
|
-
{!isLoading && !children && (
|
|
89
|
+
{showWarning && !isLoading && !children && (
|
|
88
90
|
<div className="p-4">
|
|
89
91
|
<div
|
|
90
92
|
className={clsx(
|
|
@@ -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
|
|
@@ -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>
|
|
@@ -289,7 +291,7 @@ function isConditionHealthy(cond: { type?: string; status?: string }): boolean {
|
|
|
289
291
|
|
|
290
292
|
export type ConditionTone = 'ok' | 'warning' | 'fail' | 'unknown'
|
|
291
293
|
|
|
292
|
-
function defaultConditionTone(cond: { type?: string; status?: string }): ConditionTone {
|
|
294
|
+
export function defaultConditionTone(cond: { type?: string; status?: string }): ConditionTone {
|
|
293
295
|
if (cond.status !== 'True' && cond.status !== 'False') return 'unknown'
|
|
294
296
|
return isConditionHealthy(cond) ? 'ok' : 'fail'
|
|
295
297
|
}
|
|
@@ -728,10 +728,11 @@ export function WorkloadView({
|
|
|
728
728
|
// prominent "Diagnose" on a problem, a quiet icon when fine. Rendered via the host
|
|
729
729
|
// slot (DiagnoseCustomization); standalone Radar injects it, Hub overrides it.
|
|
730
730
|
const renderDiagnose = actionsBarProps?.renderDiagnose as
|
|
731
|
-
| ((ctx: { kind: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
|
|
731
|
+
| ((ctx: { kind: string; group?: string; namespace: string; name: string; health?: DiagnoseHealthHint }) => ReactNode)
|
|
732
732
|
| undefined
|
|
733
733
|
const diagnoseAction = renderDiagnose?.({
|
|
734
734
|
kind: apiKind,
|
|
735
|
+
group,
|
|
735
736
|
namespace,
|
|
736
737
|
name,
|
|
737
738
|
health: diagnoseHealthHint(apiKind, resource),
|
|
@@ -880,8 +881,15 @@ export function WorkloadView({
|
|
|
880
881
|
{/* Success animation overlay */}
|
|
881
882
|
{saveSuccess && <SaveSuccessAnimation />}
|
|
882
883
|
|
|
883
|
-
{/* Content — viewTransitionName scopes View Transitions API cross-fade to this element
|
|
884
|
-
|
|
884
|
+
{/* Content — viewTransitionName scopes View Transitions API cross-fade to this element.
|
|
885
|
+
`relative` is the backstop for absolutely-positioned descendants (Tailwind's
|
|
886
|
+
`sr-only` is position:absolute). The drawer frame keeps overflow visible at idle so
|
|
887
|
+
popovers aren't clipped, and the app's content column clips only horizontally, so one
|
|
888
|
+
that resolves its containing block above this scroller escapes into the DOCUMENT's
|
|
889
|
+
scrollable overflow — a second, page-level scrollbar. Anchoring here confines the
|
|
890
|
+
damage to this scroll body; ui/Collapse.tsx keeps them from escaping in the first
|
|
891
|
+
place. */}
|
|
892
|
+
<div className="relative flex-1 overflow-y-auto" style={{ viewTransitionName: 'drawer-content' }}>
|
|
885
893
|
{!resource ? (
|
|
886
894
|
// Fill the drawer body so the loading logo centers in it, not in a
|
|
887
895
|
// 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
|
// ============================================================================
|