@skyhook-io/k8s-ui 1.5.13 → 1.6.0
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 +3 -3
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -3
- package/src/components/dock/BottomDock.tsx +24 -17
- package/src/components/dock/DockContext.tsx +39 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +1456 -0
- package/src/components/gitops/insights/index.ts +6 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +98 -0
- package/src/components/gitops/insights/insights-helpers.ts +99 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +799 -0
- package/src/components/gitops/tree/index.ts +4 -0
- package/src/components/gitops/tree/tree-helpers.ts +42 -0
- package/src/components/resources/ResourcesView.tsx +71 -18
- package/src/components/resources/index.ts +1 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -3
- package/src/components/resources/renderers/SecretRenderer.tsx +4 -10
- package/src/components/shared/EditableYamlView.tsx +28 -17
- package/src/components/shared/ManagedByChip.tsx +45 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +3 -3
- package/src/components/topology/TopologyGraph.tsx +3 -2
- package/src/components/ui/Tooltip.tsx +10 -1
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/workload/ResourceDetailDrawer.tsx +5 -3
- package/src/components/workload/WorkloadView.tsx +66 -0
- package/src/hooks/useKeyboardShortcuts.tsx +3 -2
- package/src/index.ts +3 -0
- package/src/types/core.ts +19 -2
- package/src/types/gitops-insights.ts +193 -0
- package/src/types/gitops-tree.ts +57 -0
- package/src/types/index.ts +2 -0
- package/src/utils/badge-colors.ts +6 -1
- package/src/utils/format.ts +28 -0
- package/src/utils/gitops-owner.test.ts +136 -0
- package/src/utils/gitops-owner.ts +92 -0
- package/src/utils/gitops-route.test.ts +78 -0
- package/src/utils/gitops-route.ts +104 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.ts +14 -0
- package/src/utils/resource-hierarchy.ts +47 -3
- package/src/utils/yaml.test.ts +101 -0
- package/src/utils/yaml.ts +26 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { SEVERITY_DOT } from '../../../utils/badge-colors'
|
|
3
|
+
import { compactSource, entryTone, gitopsToSeverity, messageToPhase, phaseToTone } from './insights-helpers'
|
|
4
|
+
|
|
5
|
+
describe('gitopsToSeverity', () => {
|
|
6
|
+
it.each([
|
|
7
|
+
['critical', 'error'],
|
|
8
|
+
['Failed', 'error'],
|
|
9
|
+
['UpgradeFailed', 'error'],
|
|
10
|
+
['alert', 'alert'],
|
|
11
|
+
['warning', 'warning'],
|
|
12
|
+
['Terminating', 'warning'],
|
|
13
|
+
['Pending', 'warning'],
|
|
14
|
+
['info', 'info'],
|
|
15
|
+
['Progressing', 'info'],
|
|
16
|
+
['Reconciling', 'info'],
|
|
17
|
+
['Succeeded', 'success'],
|
|
18
|
+
['Healthy', 'success'],
|
|
19
|
+
['', 'neutral'],
|
|
20
|
+
[undefined, 'neutral'],
|
|
21
|
+
['mystery-phase', 'neutral'],
|
|
22
|
+
] as const)('%s → %s', (input, expected) => {
|
|
23
|
+
expect(gitopsToSeverity(input)).toBe(expected)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('phaseToTone', () => {
|
|
28
|
+
it('returns SEVERITY_DOT class for known phases', () => {
|
|
29
|
+
expect(phaseToTone('Succeeded')).toBe(SEVERITY_DOT.success)
|
|
30
|
+
expect(phaseToTone('Failed')).toBe(SEVERITY_DOT.error)
|
|
31
|
+
expect(phaseToTone('Progressing')).toBe(SEVERITY_DOT.info)
|
|
32
|
+
expect(phaseToTone('Pending')).toBe(SEVERITY_DOT.warning)
|
|
33
|
+
})
|
|
34
|
+
it('returns null when phase has no meaningful signal', () => {
|
|
35
|
+
expect(phaseToTone(undefined)).toBeNull()
|
|
36
|
+
expect(phaseToTone('')).toBeNull()
|
|
37
|
+
expect(phaseToTone('mystery')).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('messageToPhase', () => {
|
|
42
|
+
it('detects success language', () => {
|
|
43
|
+
expect(messageToPhase('Application was synced successfully')).toBe('succeeded')
|
|
44
|
+
expect(messageToPhase('reconcile succeeded')).toBe('succeeded')
|
|
45
|
+
})
|
|
46
|
+
it('detects failure language', () => {
|
|
47
|
+
expect(messageToPhase('reconciliation failed: context deadline')).toBe('failed')
|
|
48
|
+
expect(messageToPhase('Helm upgrade error')).toBe('failed')
|
|
49
|
+
})
|
|
50
|
+
it('detects in-flight language', () => {
|
|
51
|
+
expect(messageToPhase('progressing toward target state')).toBe('progressing')
|
|
52
|
+
expect(messageToPhase('still reconciling')).toBe('progressing')
|
|
53
|
+
})
|
|
54
|
+
it('returns undefined when nothing matches', () => {
|
|
55
|
+
expect(messageToPhase(undefined)).toBeUndefined()
|
|
56
|
+
expect(messageToPhase('')).toBeUndefined()
|
|
57
|
+
expect(messageToPhase('plain note')).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('entryTone', () => {
|
|
62
|
+
it('uses explicit phase when present', () => {
|
|
63
|
+
const tone = entryTone({ phase: 'Succeeded' })
|
|
64
|
+
expect(tone.dot).toBe(SEVERITY_DOT.success)
|
|
65
|
+
expect(tone.inferredFrom).toBeUndefined()
|
|
66
|
+
})
|
|
67
|
+
it('falls back to message inference when phase is missing', () => {
|
|
68
|
+
const tone = entryTone({ message: 'reconciliation failed' })
|
|
69
|
+
expect(tone.dot).toBe(SEVERITY_DOT.error)
|
|
70
|
+
expect(tone.inferredFrom).toBe('inferred from message')
|
|
71
|
+
})
|
|
72
|
+
it('returns neutral when neither phase nor message carries signal', () => {
|
|
73
|
+
const tone = entryTone({})
|
|
74
|
+
expect(tone.dot).toBe(SEVERITY_DOT.neutral)
|
|
75
|
+
expect(tone.inferredFrom).toBe('no phase information')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('compactSource', () => {
|
|
80
|
+
it('strips https://github.com/ prefix and collapses deep paths', () => {
|
|
81
|
+
const got = compactSource('https://github.com/KoalaOps/deployment · argocd/addons/karpenter/default-nodepool/overlays/nonprod-cluster-us-east1')
|
|
82
|
+
expect(got).toBe('KoalaOps/deployment · argocd/…/nonprod-cluster-us-east1')
|
|
83
|
+
})
|
|
84
|
+
it('keeps short paths intact', () => {
|
|
85
|
+
expect(compactSource('https://github.com/org/repo · charts/foo')).toBe('org/repo · charts/foo')
|
|
86
|
+
})
|
|
87
|
+
it('handles trailing slash and no path', () => {
|
|
88
|
+
expect(compactSource('https://github.com/org/repo/')).toBe('org/repo')
|
|
89
|
+
expect(compactSource('https://github.com/org/repo')).toBe('org/repo')
|
|
90
|
+
})
|
|
91
|
+
it('strips http and www prefixes', () => {
|
|
92
|
+
expect(compactSource('http://www.github.com/org/repo')).toBe('org/repo')
|
|
93
|
+
})
|
|
94
|
+
it('returns empty string for missing input', () => {
|
|
95
|
+
expect(compactSource(undefined)).toBe('')
|
|
96
|
+
expect(compactSource('')).toBe('')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Pure helpers for GitOps insight rendering. Extracted from
|
|
2
|
+
// GitOpsInsightViews.tsx so they can be unit-tested without importing JSX.
|
|
3
|
+
|
|
4
|
+
import { SEVERITY_DOT, type Severity } from '../../../utils/badge-colors'
|
|
5
|
+
import type { GitOpsHistoryItem } from '../../../types'
|
|
6
|
+
import type { SyncStatus, GitOpsHealthStatus } from '../../../types/gitops'
|
|
7
|
+
|
|
8
|
+
const SYNC_STATUS_SET = new Set<SyncStatus>(['Synced', 'OutOfSync', 'Reconciling', 'Unknown'])
|
|
9
|
+
const HEALTH_STATUS_SET = new Set<GitOpsHealthStatus>(['Healthy', 'Progressing', 'Degraded', 'Suspended', 'Missing', 'Unknown'])
|
|
10
|
+
|
|
11
|
+
// normalizeSyncStatus narrows an arbitrary string (e.g. a GitOpsChange.category
|
|
12
|
+
// or .sync from the backend) onto the SyncStatusBadge's expected union. Unknown
|
|
13
|
+
// values fall back to "Unknown" rather than rendering whatever the badge's
|
|
14
|
+
// default-case branch happens to do — silently rendering wrong was the failure
|
|
15
|
+
// mode of the `as any` casts this replaces.
|
|
16
|
+
export function normalizeSyncStatus(value: string | undefined | null): SyncStatus {
|
|
17
|
+
if (value && (SYNC_STATUS_SET as Set<string>).has(value)) return value as SyncStatus
|
|
18
|
+
return 'Unknown'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeHealthStatus(value: string | undefined | null): GitOpsHealthStatus {
|
|
22
|
+
if (value && (HEALTH_STATUS_SET as Set<string>).has(value)) return value as GitOpsHealthStatus
|
|
23
|
+
return 'Unknown'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Map GitOps-flavored vocabulary (Argo/Flux phase strings, insight Issue
|
|
27
|
+
// severities) onto the canonical Severity tokens used by SEVERITY_BADGE /
|
|
28
|
+
// SEVERITY_TEXT / SEVERITY_DOT. Centralizing this keeps call sites from
|
|
29
|
+
// hand-rolling Tailwind color literals (which bypass theme overrides and
|
|
30
|
+
// drift from the rest of the OSS surface).
|
|
31
|
+
export function gitopsToSeverity(value: string | undefined): Severity {
|
|
32
|
+
const v = (value || '').toLowerCase()
|
|
33
|
+
if (!v) return 'neutral'
|
|
34
|
+
if (v === 'critical' || v.includes('fail') || v.includes('error')) return 'error'
|
|
35
|
+
if (v === 'alert') return 'alert'
|
|
36
|
+
if (v === 'warning' || v.includes('terminat') || v.includes('pending') || v.includes('wait')) return 'warning'
|
|
37
|
+
if (v === 'info' || v.includes('progress') || v.includes('running') || v.includes('reconcil')) return 'info'
|
|
38
|
+
if (v.includes('succeed') || v === 'healthy' || v === 'ok') return 'success'
|
|
39
|
+
return 'neutral'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Map a phase string to its dot color, or null if the phase carries no
|
|
43
|
+
// meaningful signal (caller decides whether to fall back to inference).
|
|
44
|
+
export function phaseToTone(phase?: string): string | null {
|
|
45
|
+
const sev = gitopsToSeverity(phase)
|
|
46
|
+
return sev === 'neutral' ? null : SEVERITY_DOT[sev]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Best-effort phase recovery from message text. Argo only populates the
|
|
50
|
+
// phase field on the most recent revision; older entries lose their
|
|
51
|
+
// outcome signal unless we read it from the human-readable message.
|
|
52
|
+
export function messageToPhase(message?: string): string | undefined {
|
|
53
|
+
if (!message) return undefined
|
|
54
|
+
const m = message.toLowerCase()
|
|
55
|
+
if (m.includes('successfully') || m.includes('succeeded')) return 'succeeded'
|
|
56
|
+
if (m.includes('failed') || m.includes('error')) return 'failed'
|
|
57
|
+
if (m.includes('progressing') || m.includes('reconciling')) return 'progressing'
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface EntryTone {
|
|
62
|
+
dot: string
|
|
63
|
+
inferredFrom?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pick a dot color via the canonical SEVERITY_DOT palette. Argo only
|
|
67
|
+
// populates phase on the most recent revision; older entries fall back to
|
|
68
|
+
// inference from the message string so the timeline still encodes outcome
|
|
69
|
+
// at a glance instead of degenerating into a column of neutral dots.
|
|
70
|
+
export function entryTone(item: GitOpsHistoryItem): EntryTone {
|
|
71
|
+
const explicit = phaseToTone(item.phase)
|
|
72
|
+
if (explicit) return { dot: explicit }
|
|
73
|
+
const inferred = phaseToTone(messageToPhase(item.message))
|
|
74
|
+
if (inferred) return { dot: inferred, inferredFrom: 'inferred from message' }
|
|
75
|
+
// No signal at all — keep the dot visible but neutral. Coloring it green
|
|
76
|
+
// would be a guess (a failed revision can sit at history's head with no
|
|
77
|
+
// successor), and a wrong-color dot is worse than no information.
|
|
78
|
+
return { dot: SEVERITY_DOT.neutral, inferredFrom: 'no phase information' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Compact a source string for inline display. Argo emits the full GitHub URL
|
|
82
|
+
// followed by " · path/within/repo", which dominates the timeline row when
|
|
83
|
+
// rendered raw. Strip the protocol+host (full string still shown on hover via
|
|
84
|
+
// title), and shorten deep paths to "head/…/leaf" form.
|
|
85
|
+
export function compactSource(source?: string): string {
|
|
86
|
+
if (!source) return ''
|
|
87
|
+
const [repoPart, ...pathParts] = source.split(' · ')
|
|
88
|
+
const repo = repoPart
|
|
89
|
+
.replace(/^https?:\/\/(www\.)?github\.com\//, '')
|
|
90
|
+
.replace(/^https?:\/\//, '')
|
|
91
|
+
.replace(/\/$/, '')
|
|
92
|
+
const path = pathParts.join(' · ').trim()
|
|
93
|
+
if (!path) return repo
|
|
94
|
+
const segments = path.split('/').filter(Boolean)
|
|
95
|
+
const shortPath = segments.length > 3
|
|
96
|
+
? `${segments[0]}/…/${segments[segments.length - 1]}`
|
|
97
|
+
: path
|
|
98
|
+
return `${repo} · ${shortPath}`
|
|
99
|
+
}
|