@skyhook-io/k8s-ui 1.14.0 → 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 +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/resources/ResourcesView.tsx +257 -78
- package/src/components/resources/column-header-width.test.ts +75 -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/drawer-components.tsx +4 -2
- package/src/components/workload/WorkloadView.tsx +9 -2
- package/src/types/gitops.ts +51 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getColumnMinWidth, type Column } from './ResourcesView'
|
|
3
|
+
|
|
4
|
+
// The header row is px-4 with a `truncate` label, under table-layout:fixed with
|
|
5
|
+
// explicit <col> widths. A column narrower than its own heading clips to
|
|
6
|
+
// "ADDRESS T…" and — because only the Name column flexes — stays clipped at
|
|
7
|
+
// 1280, 1920 and 2560 alike. These pin the floor that stops that.
|
|
8
|
+
//
|
|
9
|
+
// Reference measurements taken in the browser against the real header font
|
|
10
|
+
// (DM Sans 500, 12px, uppercase, tracking-wide), on kind-radar-kyverno-demo:
|
|
11
|
+
// RECLAIM 52.7px ADDRESSES 69.6px
|
|
12
|
+
// EXPANSION 67.5px ADDRESS TYPE 87.7px
|
|
13
|
+
const MEASURED = {
|
|
14
|
+
Reclaim: 52.7,
|
|
15
|
+
Expansion: 67.5,
|
|
16
|
+
Addresses: 69.6,
|
|
17
|
+
'Address Type': 87.7,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const col = (over: Partial<Column> & Pick<Column, 'key' | 'label'>): Column => over
|
|
21
|
+
|
|
22
|
+
describe('getColumnMinWidth header floor', () => {
|
|
23
|
+
it.each(Object.entries(MEASURED))(
|
|
24
|
+
'reserves at least the measured width of %s plus padding',
|
|
25
|
+
(label, measuredPx) => {
|
|
26
|
+
const got = getColumnMinWidth(col({ key: 'x', label, width: 'w-16' }))
|
|
27
|
+
// 32px is the px-4 on both sides; the estimate must never land under the
|
|
28
|
+
// width the browser actually renders, or the label clips again.
|
|
29
|
+
expect(got).toBeGreaterThanOrEqual(Math.ceil(measuredPx + 32))
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
it('leaves a column that already fits its label untouched', () => {
|
|
34
|
+
// "Age" needs ~25px + 32 padding + 20 for its sort icon = well under w-24.
|
|
35
|
+
expect(getColumnMinWidth(col({ key: 'age', label: 'Age', width: 'w-24' }))).toBe(96)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('raises a column that is narrower than its label', () => {
|
|
39
|
+
// w-20 is 80px; "Address Type" cannot fit in 80 - 32 = 48px.
|
|
40
|
+
expect(getColumnMinWidth(col({ key: 'addressType', label: 'Address Type', width: 'w-20' })))
|
|
41
|
+
.toBeGreaterThan(80)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('reserves extra room for the sort icon on sortable columns', () => {
|
|
45
|
+
const sortable = getColumnMinWidth(col({ key: 'status', label: 'Wide Label Here', width: 'w-16' }))
|
|
46
|
+
const plain = getColumnMinWidth(col({ key: 'notSortable', label: 'Wide Label Here', width: 'w-16' }))
|
|
47
|
+
expect(sortable).toBeGreaterThan(plain)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('still honours an explicit minWidth override', () => {
|
|
51
|
+
expect(getColumnMinWidth(col({ key: 'x', label: 'Address Type', width: 'w-20', minWidth: 300 }))).toBe(300)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('keeps the flexible Name column at its wider default', () => {
|
|
55
|
+
expect(getColumnMinWidth(col({ key: 'name', label: 'Name' }))).toBe(200)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Printer columns (uncurated CRDs) and host-injected custom columns are
|
|
59
|
+
// sortable via their own getSortValue, not by being in the built-in key list —
|
|
60
|
+
// the header renders a sort icon for them, so the floor has to reserve it.
|
|
61
|
+
it('reserves the sort icon for a printer/custom column that carries getSortValue', () => {
|
|
62
|
+
const c = col({ key: 'printer:Phase', label: 'Reconcile Phase', width: 'w-24' })
|
|
63
|
+
const extras = new Map([[c.key, { ...c, render: () => null, getSortValue: () => '' }]]) as any
|
|
64
|
+
expect(getColumnMinWidth(c, extras)).toBeGreaterThan(getColumnMinWidth(c))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('caps the floor so one long vendor label cannot size a column off screen', () => {
|
|
68
|
+
const monstrous = 'A'.repeat(200)
|
|
69
|
+
expect(getColumnMinWidth(col({ key: 'printer:x', label: monstrous, width: 'w-24' }))).toBe(320)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('never returns less than the declared width, even past the cap', () => {
|
|
73
|
+
expect(getColumnMinWidth(col({ key: 'printer:x', label: 'A'.repeat(200), width: 'w-96' }))).toBe(384)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
|
|
5
|
+
// Parsed from the source rather than exported, matching
|
|
6
|
+
// curated-column-ownership.test.ts: this asserts a property of the data as it
|
|
7
|
+
// is written, and an export would let the two drift.
|
|
8
|
+
const SRC = readFileSync(join(__dirname, 'ResourcesView.tsx'), 'utf8')
|
|
9
|
+
|
|
10
|
+
function columnSetBody(key: string): string {
|
|
11
|
+
const start = SRC.match(new RegExp(`^ ${key}: \\[`, 'm'))
|
|
12
|
+
if (!start || start.index === undefined) throw new Error(`${key} not found in KNOWN_COLUMNS`)
|
|
13
|
+
const open = SRC.indexOf('[', start.index)
|
|
14
|
+
let depth = 1
|
|
15
|
+
let i = open + 1
|
|
16
|
+
while (depth > 0) {
|
|
17
|
+
if (SRC[i] === '[') depth++
|
|
18
|
+
else if (SRC[i] === ']') depth--
|
|
19
|
+
i++
|
|
20
|
+
}
|
|
21
|
+
return SRC.slice(open + 1, i)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The header cell is px-4, so w-16 (64px) leaves 31px for the label while
|
|
25
|
+
// uppercase "RULES" needs ~37px — it clips to "RUL…" unconditionally, with no
|
|
26
|
+
// sort or filter button involved: 'rules' is in SKIP_FILTER_COLUMNS and is not
|
|
27
|
+
// in the sortable-key list, so neither affordance ever renders on it. w-20
|
|
28
|
+
// (80px) leaves 47px.
|
|
29
|
+
describe('Rules column width', () => {
|
|
30
|
+
const kinds = ['kyvernopolicies', 'clusterpolicies', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes']
|
|
31
|
+
|
|
32
|
+
it.each(kinds)('%s Rules column is at least w-20', (kind) => {
|
|
33
|
+
const rules = columnSetBody(kind).match(/\{ key: 'rules'.*\}/)
|
|
34
|
+
expect(rules, `${kind} has no rules column`).not.toBeNull()
|
|
35
|
+
const width = rules![0].match(/width: 'w-(\d+)'/)
|
|
36
|
+
expect(width, `${kind} rules column has no fixed w-* width`).not.toBeNull()
|
|
37
|
+
// Compared numerically, not pinned to w-20, so the wider table-width pass
|
|
38
|
+
// tracked in #1583 can widen these without tripping a test that only
|
|
39
|
+
// claims a floor.
|
|
40
|
+
expect(Number(width![1])).toBeGreaterThanOrEqual(20)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -651,6 +651,81 @@ describe('colliding plurals — near-match API groups', () => {
|
|
|
651
651
|
})
|
|
652
652
|
})
|
|
653
653
|
|
|
654
|
+
describe('CAPI Machine kind collisions', () => {
|
|
655
|
+
const foreignApiVersion = 'extension.cluster.x-k8s.io/v1'
|
|
656
|
+
|
|
657
|
+
it.each([
|
|
658
|
+
['machines', 'Machine', 'Role'],
|
|
659
|
+
['machinesets', 'MachineSet', 'Delete Policy'],
|
|
660
|
+
])('routes a foreign %s CRD through the generic renderer only', (plural, kind, capiMarker) => {
|
|
661
|
+
const html = renderKind(plural, {
|
|
662
|
+
apiVersion: foreignApiVersion,
|
|
663
|
+
kind,
|
|
664
|
+
metadata: {
|
|
665
|
+
name: 'foreign',
|
|
666
|
+
namespace: 'default',
|
|
667
|
+
labels: { 'topology.cluster.x-k8s.io/owned': '' },
|
|
668
|
+
},
|
|
669
|
+
spec: { collisionProbe: COLLISION_PROBE },
|
|
670
|
+
status: { phase: 'provisioned' },
|
|
671
|
+
}, 'default')
|
|
672
|
+
|
|
673
|
+
expect(html).toContain(COLLISION_PROBE)
|
|
674
|
+
expect(html).not.toContain(capiMarker)
|
|
675
|
+
expect(html).not.toContain('Topology-controlled')
|
|
676
|
+
expect(getResourceStatus(plural, {
|
|
677
|
+
apiVersion: foreignApiVersion,
|
|
678
|
+
status: { phase: 'provisioned' },
|
|
679
|
+
})?.text).toBe('provisioned')
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
it.each([
|
|
683
|
+
['machines', 'Machine', 'Role'],
|
|
684
|
+
['machinesets', 'MachineSet', 'Delete Policy'],
|
|
685
|
+
])('keeps exact-group CAPI %s resources on the dedicated path', (plural, kind, capiMarker) => {
|
|
686
|
+
const html = renderKind(plural, {
|
|
687
|
+
apiVersion: 'cluster.x-k8s.io/v1beta1',
|
|
688
|
+
kind,
|
|
689
|
+
metadata: {
|
|
690
|
+
name: 'capi',
|
|
691
|
+
namespace: 'default',
|
|
692
|
+
labels: { 'topology.cluster.x-k8s.io/owned': '' },
|
|
693
|
+
},
|
|
694
|
+
spec: { collisionProbe: COLLISION_PROBE },
|
|
695
|
+
status: { phase: 'provisioned' },
|
|
696
|
+
}, 'default')
|
|
697
|
+
|
|
698
|
+
expect(html).toContain(capiMarker)
|
|
699
|
+
expect(html).toContain('Topology-controlled')
|
|
700
|
+
expect(html).not.toContain(COLLISION_PROBE)
|
|
701
|
+
expect(getResourceStatus(plural, {
|
|
702
|
+
apiVersion: 'cluster.x-k8s.io/v1beta1',
|
|
703
|
+
status: { phase: 'provisioned' },
|
|
704
|
+
})?.text).toBe('Provisioned')
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
it.each([
|
|
708
|
+
['kubeadmcontrolplanes', 'KubeadmControlPlane', 'controlplane.cluster.x-k8s.io/v1beta1'],
|
|
709
|
+
['awsmanagedcontrolplanes', 'AWSManagedControlPlane', 'controlplane.cluster.x-k8s.io/v1beta2'],
|
|
710
|
+
['awsmanagedmachinepools', 'AWSManagedMachinePool', 'infrastructure.cluster.x-k8s.io/v1beta2'],
|
|
711
|
+
['awsmachines', 'AWSMachine', 'infrastructure.cluster.x-k8s.io/v1beta2'],
|
|
712
|
+
])('keeps the topology-owned warning for %s', (plural, kind, apiVersion) => {
|
|
713
|
+
const html = renderKind(plural, {
|
|
714
|
+
apiVersion,
|
|
715
|
+
kind,
|
|
716
|
+
metadata: {
|
|
717
|
+
name: 'topology-owned',
|
|
718
|
+
namespace: 'default',
|
|
719
|
+
labels: { 'topology.cluster.x-k8s.io/owned': '' },
|
|
720
|
+
},
|
|
721
|
+
spec: {},
|
|
722
|
+
status: {},
|
|
723
|
+
}, 'default')
|
|
724
|
+
|
|
725
|
+
expect(html).toContain('Topology-controlled')
|
|
726
|
+
})
|
|
727
|
+
})
|
|
728
|
+
|
|
654
729
|
describe('shared plurals — the status path collides too', () => {
|
|
655
730
|
// The renderer and the status badge are two separate switches over the same
|
|
656
731
|
// plural, and fixing one leaves the other reading a foreign CRD's conditions
|
|
@@ -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
|
|
@@ -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
|
// ============================================================================
|