@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,207 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { compareVersions, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, type AppGroupFoldEntry } from './applications'
|
|
3
|
+
|
|
4
|
+
describe('compareVersions', () => {
|
|
5
|
+
it('orders semver', () => {
|
|
6
|
+
expect(compareVersions('1.2.0', '1.10.0')).toBe(-1)
|
|
7
|
+
expect(compareVersions('v2.0.0', 'v2.0.0')).toBe(0)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// Date-stamped CI tags are the dominant shape on real clusters
|
|
11
|
+
// (main_2026-03-26_05) — semver-only made promotion lag inert on them.
|
|
12
|
+
it('orders same-prefix date-stamped tags by date then sequence', () => {
|
|
13
|
+
expect(compareVersions('main_2026-03-26_05', 'main_2026-06-02_03')).toBe(-1)
|
|
14
|
+
expect(compareVersions('main_2026-06-02_03', 'main_2026-06-02_01')).toBe(1)
|
|
15
|
+
expect(compareVersions('main_2026-06-02_03', 'main_2026-06-02_03')).toBe(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('refuses date tags with different prefixes', () => {
|
|
19
|
+
expect(compareVersions('main_2026-06-02_03', 'hotfix_2026-06-02_03')).toBeNull()
|
|
20
|
+
expect(compareVersions('billing_main_2026-05-18_00', 'project-infra_main_2026-06-05_01')).toBeNull()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('refuses mixed date-tag vs non-date and unparseable input', () => {
|
|
24
|
+
expect(compareVersions('main_2026-06-02_03', '1.2.0')).toBeNull()
|
|
25
|
+
expect(compareVersions('latest', 'abc123')).toBeNull()
|
|
26
|
+
expect(compareVersions(undefined, '1.0.0')).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('handles long compound prefixes as one prefix', () => {
|
|
30
|
+
expect(compareVersions('billing_main_2026-05-18_00', 'billing_main_2026-06-05_01')).toBe(-1)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// The lag arrow is the trust-fatal output: direction, unranked exclusion, and
|
|
35
|
+
// same-env refusal each have a distinct silent-inversion failure mode.
|
|
36
|
+
describe('appGroupLagMessage', () => {
|
|
37
|
+
it('fires when a strictly-lower env runs a strictly-newer version, with correct direction', () => {
|
|
38
|
+
expect(appGroupLagMessage([
|
|
39
|
+
{ env: 'dev', version: '2.0.0' },
|
|
40
|
+
{ env: 'staging', version: '1.0.0' },
|
|
41
|
+
])).toBe('staging is behind dev')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('does not fire when the higher env is newer (healthy promotion)', () => {
|
|
45
|
+
expect(appGroupLagMessage([
|
|
46
|
+
{ env: 'dev', version: '1.0.0' },
|
|
47
|
+
{ env: 'staging', version: '2.0.0' },
|
|
48
|
+
])).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('never draws arrows through unranked (discovered) envs', () => {
|
|
52
|
+
expect(appGroupLagMessage([
|
|
53
|
+
{ env: 'qa', version: '9.0.0' },
|
|
54
|
+
{ env: 'prod', version: '1.0.0' },
|
|
55
|
+
])).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('never compares two instances of the same env', () => {
|
|
59
|
+
expect(appGroupLagMessage([
|
|
60
|
+
{ env: 'prod', version: '2.0.0' },
|
|
61
|
+
{ env: 'prod', version: '1.0.0' },
|
|
62
|
+
])).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('treats missing or incomparable versions as no signal', () => {
|
|
66
|
+
expect(appGroupLagMessage([{ env: 'dev' }, { env: 'prod', version: '1.0.0' }])).toBeNull()
|
|
67
|
+
expect(appGroupLagMessage([
|
|
68
|
+
{ env: 'dev', version: 'latest' },
|
|
69
|
+
{ env: 'prod', version: '1.0.0' },
|
|
70
|
+
])).toBeNull()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('orders date-stamped CI tags through the ladder', () => {
|
|
74
|
+
expect(appGroupLagMessage([
|
|
75
|
+
{ env: 'dev', version: 'main_2026-06-07_02' },
|
|
76
|
+
{ env: 'staging', version: 'main_2026-03-26_05' },
|
|
77
|
+
])).toBe('staging is behind dev')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('identityEnvInferred', () => {
|
|
82
|
+
it('marks only namespace-derived identity envs as inferred', () => {
|
|
83
|
+
expect(identityEnvInferred({ key: 'billing', env: 'staging', confidence: 'medium', evidence: 'namespace stem "billing" + shared image repo repo/app' })).toBe(true)
|
|
84
|
+
expect(identityEnvInferred({ key: 'billing', env: 'staging', confidence: 'medium', evidence: 'name stem "billing" + shared image repo repo/app' })).toBe(false)
|
|
85
|
+
expect(identityEnvInferred({ key: 'billing', env: 'staging', confidence: 'medium', evidence: 'environment label "staging" + name/repo evidence' })).toBe(false)
|
|
86
|
+
expect(identityEnvInferred({ key: 'billing', env: 'staging', confidence: 'high', evidence: 'Argo CD source path billing (env overlay staging)' })).toBe(false)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Position-preserving env switch: exact match, stem fallback (suffix, prefix,
|
|
91
|
+
// discovered tokens), and the explicit no-counterpart null.
|
|
92
|
+
describe('matchWorkloadAcrossInstances', () => {
|
|
93
|
+
const dep = (name: string, namespace = 'staging') => ({ kind: 'Deployment', namespace, name })
|
|
94
|
+
|
|
95
|
+
it('prefers the exact kind+name match', () => {
|
|
96
|
+
expect(matchWorkloadAcrossInstances('Deployment/dev/billing', [dep('billing')])).toEqual(dep('billing'))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('prefers namespace-specific exact matches and refuses ambiguous same-name matches', () => {
|
|
100
|
+
expect(matchWorkloadAcrossInstances('Deployment/dev/billing', [dep('billing', 'prod'), dep('billing', 'dev')])).toEqual(dep('billing', 'dev'))
|
|
101
|
+
expect(matchWorkloadAcrossInstances('Deployment/dev/billing', [dep('billing', 'prod'), dep('billing', 'staging')])).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('falls back to the env-affix-stripped stem (suffix and prefix)', () => {
|
|
105
|
+
expect(matchWorkloadAcrossInstances('Deployment/dev/billing-dev', [dep('billing-staging')])).toEqual(dep('billing-staging'))
|
|
106
|
+
expect(matchWorkloadAcrossInstances('Deployment/qa/qa-koala', [dep('staging-koala')])).toEqual(dep('staging-koala'))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('strips discovered env tokens passed via extraTokens', () => {
|
|
110
|
+
const tokens = new Set(['loadtest'])
|
|
111
|
+
expect(matchWorkloadAcrossInstances('Deployment/team/api-loadtest', [dep('api', 'dev')], tokens)).toEqual(dep('api', 'dev'))
|
|
112
|
+
expect(matchWorkloadAcrossInstances('Deployment/team/api-loadtest', [dep('api', 'dev')])).toBeNull()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns null when no counterpart exists', () => {
|
|
116
|
+
expect(matchWorkloadAcrossInstances('Deployment/dev/billing', [dep('finops')])).toBeNull()
|
|
117
|
+
expect(matchWorkloadAcrossInstances('garbage', [dep('billing')])).toBeNull()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// foldAppGroups pins the collapse experiment's safety rails — each fails
|
|
122
|
+
// silently in a component-embedded loop.
|
|
123
|
+
describe('foldAppGroups', () => {
|
|
124
|
+
const entry = (key: string, name: string, famEnv?: string, over: Partial<AppGroupFoldEntry> = {}): AppGroupFoldEntry => ({
|
|
125
|
+
row: { key, name, identity: famEnv ? { key: 'billing', env: famEnv, confidence: 'medium', evidence: 'e' } : undefined },
|
|
126
|
+
health: 'healthy',
|
|
127
|
+
versions: [],
|
|
128
|
+
ready: 1,
|
|
129
|
+
desired: 1,
|
|
130
|
+
kinds: { Deployment: 1 },
|
|
131
|
+
classComposition: [{ cls: 'service', count: 1 }],
|
|
132
|
+
...over,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('folds app group members into one ladder row with instances hidden by default', () => {
|
|
136
|
+
const rows = foldAppGroups([entry('a', 'billing', 'dev'), entry('b', 'billing-staging', 'staging'), entry('c', 'lonely')], new Set(), false)
|
|
137
|
+
expect(rows.map((r) => r.kind)).toEqual(['group', 'instance'])
|
|
138
|
+
const group = rows[0] as Extract<(typeof rows)[0], { kind: 'group' }>
|
|
139
|
+
expect(group.label).toBe('billing')
|
|
140
|
+
expect(group.cells.map((c) => c.env)).toEqual(['dev', 'staging'])
|
|
141
|
+
expect(group.ready).toBe(2)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('renders a filter-orphaned member as the plain instance it is', () => {
|
|
145
|
+
const rows = foldAppGroups([entry('a', 'billing', 'dev')], new Set(), false)
|
|
146
|
+
expect(rows).toEqual([{ kind: 'instance', entry: entry('a', 'billing', 'dev') }])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('search auto-expansion emits the member rows', () => {
|
|
150
|
+
const rows = foldAppGroups([entry('a', 'billing', 'dev'), entry('b', 'billing-staging', 'staging')], new Set(), true)
|
|
151
|
+
expect(rows.map((r) => r.kind)).toEqual(['group', 'instance', 'instance'])
|
|
152
|
+
expect(rows.filter((r) => r.kind === 'instance').every((r) => (r as { child?: boolean }).child)).toBe(true)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('aggregates same-env instances into one cell: count, worst health, newest version', () => {
|
|
156
|
+
const rows = foldAppGroups(
|
|
157
|
+
[
|
|
158
|
+
entry('a', 'billing', 'staging', { versions: ['1.0.0'], health: 'healthy' }),
|
|
159
|
+
entry('b', 'billing-2', 'staging', { versions: ['2.0.0'], health: 'unhealthy' }),
|
|
160
|
+
entry('c', 'billing-dev', 'dev'),
|
|
161
|
+
],
|
|
162
|
+
new Set(),
|
|
163
|
+
false,
|
|
164
|
+
)
|
|
165
|
+
const group = rows[0] as Extract<(typeof rows)[0], { kind: 'group' }>
|
|
166
|
+
const staging = group.cells.find((c) => c.env === 'staging')!
|
|
167
|
+
expect(staging.count).toBe(2)
|
|
168
|
+
expect(staging.health).toBe('unhealthy')
|
|
169
|
+
expect(staging.version).toBe('2.0.0')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('derives the group workload class like the server: service+worker collapses, jobs make mixed', () => {
|
|
173
|
+
const mk = (cls: 'service' | 'worker' | 'job', key: string, env: string) =>
|
|
174
|
+
entry(key, key, env, { classComposition: [{ cls, count: 1 }] })
|
|
175
|
+
const sw = foldAppGroups([mk('service', 'a', 'dev'), mk('worker', 'b', 'staging')], new Set(), false)
|
|
176
|
+
expect((sw[0] as { workloadClass?: string }).workloadClass).toBe('service')
|
|
177
|
+
const sj = foldAppGroups([mk('service', 'a', 'dev'), mk('job', 'b', 'staging')], new Set(), false)
|
|
178
|
+
expect((sj[0] as { workloadClass?: string }).workloadClass).toBe('mixed')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('scopes non-portable identities but folds portable identities across scopes', () => {
|
|
182
|
+
const portable = (key: string, name: string, env: string): AppGroupFoldEntry => ({
|
|
183
|
+
...entry(key, name, env),
|
|
184
|
+
row: { key, name, identity: { key: 'billing', env, confidence: 'high', evidence: 'e', portable: true } },
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const oss = foldAppGroups(
|
|
188
|
+
[portable('a', 'billing', 'dev'), entry('b', 'billing-staging', 'staging')],
|
|
189
|
+
new Set(),
|
|
190
|
+
false,
|
|
191
|
+
)
|
|
192
|
+
expect(oss.map((r) => r.kind)).toEqual(['group'])
|
|
193
|
+
|
|
194
|
+
const local = foldAppGroups(
|
|
195
|
+
[entry('a', 'billing', 'dev'), entry('b', 'billing-staging', 'staging')],
|
|
196
|
+
new Set(),
|
|
197
|
+
false,
|
|
198
|
+
{ localScope: (e) => e.row.key },
|
|
199
|
+
)
|
|
200
|
+
expect(local.map((r) => r.kind)).toEqual(['instance', 'instance'])
|
|
201
|
+
|
|
202
|
+
const grouped = foldAppGroups([portable('a', 'billing', 'dev'), portable('b', 'billing-staging', 'staging')], new Set(), false, {
|
|
203
|
+
localScope: (e) => e.row.key,
|
|
204
|
+
})
|
|
205
|
+
expect(grouped.map((r) => r.kind)).toEqual(['group'])
|
|
206
|
+
})
|
|
207
|
+
})
|