@skyhook-io/k8s-ui 1.8.10 → 1.8.11
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 +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +3 -3
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +51 -38
- package/src/components/issues/ResourceIssuesSection.test.tsx +6 -5
- package/src/components/issues/ResourceIssuesSection.tsx +28 -146
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +2 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +57 -4
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { compareVersions, appGroupingExplainer, APP_IDENTITY_ANNOTATION, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, worstHealth, type AppGroupFoldEntry } from './applications'
|
|
2
|
+
import { compareVersions, appGroupingExplainer, APP_IDENTITY_ANNOTATION, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, worstHealth, buildAppMembershipIndex, batchActivityForApp, batchRuntimeForApp, servingReadiness, type AppGroupFoldEntry, type AppRow, type AppWorkload } from './applications'
|
|
3
|
+
|
|
4
|
+
describe('batch application runtime', () => {
|
|
5
|
+
it('uses the latest retained outcome instead of historical failure count', () => {
|
|
6
|
+
const app: AppRow = {
|
|
7
|
+
key: 'batch', name: 'batch', health: 'unhealthy', workload_class: 'job',
|
|
8
|
+
workloads: [{
|
|
9
|
+
kind: 'CronJob', namespace: 'demo', name: 'nightly', workload_class: 'job', health: 'unhealthy', ready: 0, desired: 0, restarts: 0,
|
|
10
|
+
batch: { retainedRuns: 4, failedRuns: 1, succeededRuns: 3, latestRunName: 'nightly-new', latestRunPhase: 'Succeeded', latestFinishedAt: '2026-07-10T00:00:00Z' },
|
|
11
|
+
}],
|
|
12
|
+
}
|
|
13
|
+
expect(batchRuntimeForApp(app)).toMatchObject({ label: 'Succeeded', health: 'healthy' })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('does not let one workload success mask another workload latest failure', () => {
|
|
17
|
+
const app: AppRow = {
|
|
18
|
+
key: 'batch', name: 'batch', health: 'healthy', workload_class: 'job',
|
|
19
|
+
workloads: [
|
|
20
|
+
{
|
|
21
|
+
kind: 'CronJob', namespace: 'demo', name: 'nightly', workload_class: 'job', health: 'healthy', ready: 0, desired: 0, restarts: 0,
|
|
22
|
+
batch: { retainedRuns: 1, succeededRuns: 1, latestRunName: 'nightly-new', latestRunPhase: 'Succeeded', latestFinishedAt: '2026-07-10T01:00:00Z' },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
kind: 'CronWorkflow', namespace: 'demo', name: 'sync', workload_class: 'job', health: 'unhealthy', ready: 0, desired: 0, restarts: 0,
|
|
26
|
+
batch: { retainedRuns: 1, failedRuns: 1, latestRunName: 'sync-old', latestRunPhase: 'Failed', latestFinishedAt: '2026-07-10T00:00:00Z' },
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
expect(batchRuntimeForApp(app)).toEqual({
|
|
32
|
+
label: 'Failed',
|
|
33
|
+
health: 'unhealthy',
|
|
34
|
+
detail: 'CronWorkflow/sync latest retained run sync-old failed.',
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('excludes batch workloads from serving readiness', () => {
|
|
39
|
+
expect(servingReadiness([
|
|
40
|
+
{ kind: 'Deployment', namespace: 'demo', name: 'api', workload_class: 'service', health: 'healthy', ready: 2, desired: 3, restarts: 0 },
|
|
41
|
+
{ kind: 'Job', namespace: 'demo', name: 'run', workload_class: 'job', health: 'healthy', ready: 1, desired: 1, restarts: 0, batch: { retainedRuns: 1 } },
|
|
42
|
+
])).toEqual({ ready: 2, desired: 3 })
|
|
43
|
+
})
|
|
44
|
+
})
|
|
3
45
|
|
|
4
46
|
describe('compareVersions', () => {
|
|
5
47
|
it('orders semver', () => {
|
|
@@ -87,6 +129,46 @@ describe('identityEnvInferred', () => {
|
|
|
87
129
|
})
|
|
88
130
|
})
|
|
89
131
|
|
|
132
|
+
describe('batchActivityForApp', () => {
|
|
133
|
+
const workload = (kind: string, name: string, batch?: AppWorkload['batch']): AppWorkload => ({
|
|
134
|
+
kind,
|
|
135
|
+
namespace: 'prod',
|
|
136
|
+
name,
|
|
137
|
+
health: 'neutral',
|
|
138
|
+
ready: 0,
|
|
139
|
+
desired: 0,
|
|
140
|
+
restarts: 0,
|
|
141
|
+
workload_class: kind === 'Deployment' ? 'service' : 'job',
|
|
142
|
+
batch,
|
|
143
|
+
})
|
|
144
|
+
const app = (workloads: AppWorkload[]): AppRow => ({
|
|
145
|
+
key: 'billing',
|
|
146
|
+
name: 'billing',
|
|
147
|
+
health: 'neutral',
|
|
148
|
+
workloads,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('ranks failed, active, suspended, and quiet batch workloads without inventing retained history', () => {
|
|
152
|
+
const activity = batchActivityForApp(app([
|
|
153
|
+
workload('Deployment', 'api'),
|
|
154
|
+
workload('CronJob', 'nightly-success', { retainedRuns: 2, latestRunPhase: 'Succeeded', latestRunName: 'nightly-success-1' }),
|
|
155
|
+
workload('CronWorkflow', 'hourly-suspended', { suspended: true, retainedRuns: 0, schedule: '0 * * * *' }),
|
|
156
|
+
workload('Job', 'active-reindex', { activeRuns: 1, retainedRuns: 1, latestRunPhase: 'Running' }),
|
|
157
|
+
workload('Workflow', 'failed-migration', { failedRuns: 1, retainedRuns: 1, latestRunPhase: 'Failed', latestRunName: 'failed-migration', message: 'pod crashed' }),
|
|
158
|
+
]))
|
|
159
|
+
|
|
160
|
+
expect(activity.map((item) => item.workload.name)).toEqual([
|
|
161
|
+
'failed-migration',
|
|
162
|
+
'active-reindex',
|
|
163
|
+
'hourly-suspended',
|
|
164
|
+
'nightly-success',
|
|
165
|
+
])
|
|
166
|
+
expect(activity[0]).toMatchObject({ tone: 'rose', label: 'Latest run failed', failedRuns: 1, detail: 'pod crashed' })
|
|
167
|
+
expect(activity[1]).toMatchObject({ tone: 'sky', label: '1 active run', activeRuns: 1 })
|
|
168
|
+
expect(activity[3]).toMatchObject({ tone: 'muted', label: 'Latest run succeeded', retainedRuns: 2 })
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
90
172
|
// Position-preserving env switch: exact match, stem fallback (suffix, prefix,
|
|
91
173
|
// discovered tokens), and the explicit no-counterpart null.
|
|
92
174
|
describe('matchWorkloadAcrossInstances', () => {
|
|
@@ -296,3 +378,87 @@ describe('foldAppGroups pathKey disambiguation', () => {
|
|
|
296
378
|
expect(rows.filter((r) => r.kind === 'instance').length).toBe(2)
|
|
297
379
|
})
|
|
298
380
|
})
|
|
381
|
+
|
|
382
|
+
describe('buildAppMembershipIndex', () => {
|
|
383
|
+
// Minimal AppRow builder — only the fields the index reads.
|
|
384
|
+
const row = (over: Partial<AppRow>): AppRow => ({
|
|
385
|
+
key: 'k', name: 'app', health: 'healthy', workloads: [], ...over,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('indexes workloads by Kind/ns/name and satellites by the app namespace', () => {
|
|
389
|
+
const idx = buildAppMembershipIndex([
|
|
390
|
+
row({
|
|
391
|
+
key: 'team-a/app/billing', name: 'billing', namespace: 'team-a',
|
|
392
|
+
workloads: [{ kind: 'Deployment', namespace: 'team-a', name: 'billing-api', health: 'healthy', ready: 1, desired: 1, restarts: 0 }],
|
|
393
|
+
relationships: { services: ['billing-svc'], ingresses: ['billing-ing'] },
|
|
394
|
+
}),
|
|
395
|
+
])
|
|
396
|
+
expect(idx.byResource.get('Deployment/team-a/billing-api')?.appName).toBe('billing')
|
|
397
|
+
expect(idx.byResource.get('Service/team-a/billing-svc')?.appName).toBe('billing')
|
|
398
|
+
expect(idx.byResource.get('Ingress/team-a/billing-ing')?.appName).toBe('billing')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('indexes routes under their concrete kind, not a generic "Route"', () => {
|
|
402
|
+
// The server ships "Kind/name" for polymorphic routes; the index must key on
|
|
403
|
+
// the concrete kind so it matches the route lane id (HTTPRoute/…/name).
|
|
404
|
+
const idx = buildAppMembershipIndex([
|
|
405
|
+
row({
|
|
406
|
+
key: 'team-a/app/web', name: 'web', namespace: 'team-a',
|
|
407
|
+
relationships: { routes: ['HTTPRoute/web-http', 'GRPCRoute/web-grpc'] },
|
|
408
|
+
}),
|
|
409
|
+
])
|
|
410
|
+
expect(idx.byResource.get('HTTPRoute/team-a/web-http')?.appName).toBe('web')
|
|
411
|
+
expect(idx.byResource.get('GRPCRoute/team-a/web-grpc')?.appName).toBe('web')
|
|
412
|
+
expect(idx.byResource.has('Route/team-a/HTTPRoute/web-http')).toBe(false)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('skips satellites for a multi-namespace app (can not place them unambiguously)', () => {
|
|
416
|
+
const idx = buildAppMembershipIndex([
|
|
417
|
+
row({
|
|
418
|
+
key: 'a', name: 'span', namespaces: ['team-a', 'team-b'],
|
|
419
|
+
workloads: [{ kind: 'Deployment', namespace: 'team-a', name: 'api', health: 'healthy', ready: 1, desired: 1, restarts: 0 }],
|
|
420
|
+
relationships: { services: ['svc'] },
|
|
421
|
+
}),
|
|
422
|
+
])
|
|
423
|
+
expect(idx.byResource.has('Deployment/team-a/api')).toBe(true)
|
|
424
|
+
expect([...idx.byResource.keys()].some((k) => k.startsWith('Service/'))).toBe(false)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('indexes matchKeys by evidence but excludes name-stem (exact kinds only in v1)', () => {
|
|
428
|
+
// matchKeys are namespace-scoped (kind:namespace:value); name-stem stays unscoped.
|
|
429
|
+
const idx = buildAppMembershipIndex([
|
|
430
|
+
row({ key: 'a', name: 'billing', identity: { key: 'billing', env: 'prod', confidence: 'high', evidence: 'x' }, matchKeys: ['instance:team-a:billing-prod', 'helm:team-a:billing-v2', 'name-stem:billing'] }),
|
|
431
|
+
])
|
|
432
|
+
expect(idx.byEvidence.get('instance:team-a:billing-prod')?.appName).toBe('billing')
|
|
433
|
+
expect(idx.byEvidence.get('helm:team-a:billing-v2')?.appName).toBe('billing')
|
|
434
|
+
expect(idx.byEvidence.has('name-stem:billing')).toBe(false)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('carries env + evidence onto the membership', () => {
|
|
438
|
+
const idx = buildAppMembershipIndex([
|
|
439
|
+
row({ key: 'a', name: 'billing', identity: { key: 'billing', env: 'prod', confidence: 'high', evidence: 'name stem billing' }, matchKeys: ['name:team-a:billing'] }),
|
|
440
|
+
])
|
|
441
|
+
const m = idx.byEvidence.get('name:team-a:billing')
|
|
442
|
+
expect(m?.env).toBe('prod')
|
|
443
|
+
expect(m?.evidence).toBe('name stem billing')
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('collision resolves first-wins (server-sorted row order)', () => {
|
|
447
|
+
const idx = buildAppMembershipIndex([
|
|
448
|
+
row({ key: 'first', name: 'first', matchKeys: ['name:team-a:shared'] }),
|
|
449
|
+
row({ key: 'second', name: 'second', matchKeys: ['name:team-a:shared'] }),
|
|
450
|
+
])
|
|
451
|
+
expect(idx.byEvidence.get('name:team-a:shared')?.appName).toBe('first')
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('same label value in two namespaces indexes as distinct evidence keys', () => {
|
|
455
|
+
// The whole point of namespace scoping: "shared" in team-a and team-b must
|
|
456
|
+
// NOT collapse to one key — each namespace maps to its own app.
|
|
457
|
+
const idx = buildAppMembershipIndex([
|
|
458
|
+
row({ key: 'a', name: 'app-a', matchKeys: ['instance:team-a:shared'] }),
|
|
459
|
+
row({ key: 'b', name: 'app-b', matchKeys: ['instance:team-b:shared'] }),
|
|
460
|
+
])
|
|
461
|
+
expect(idx.byEvidence.get('instance:team-a:shared')?.appName).toBe('app-a')
|
|
462
|
+
expect(idx.byEvidence.get('instance:team-b:shared')?.appName).toBe('app-b')
|
|
463
|
+
})
|
|
464
|
+
})
|