@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.
Files changed (45) hide show
  1. package/package.json +3 -3
  2. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -3
  3. package/src/components/dock/BottomDock.tsx +24 -17
  4. package/src/components/dock/DockContext.tsx +39 -0
  5. package/src/components/gitops/index.ts +4 -0
  6. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1456 -0
  7. package/src/components/gitops/insights/index.ts +6 -0
  8. package/src/components/gitops/insights/insights-helpers.test.ts +98 -0
  9. package/src/components/gitops/insights/insights-helpers.ts +99 -0
  10. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +799 -0
  11. package/src/components/gitops/tree/index.ts +4 -0
  12. package/src/components/gitops/tree/tree-helpers.ts +42 -0
  13. package/src/components/resources/ResourcesView.tsx +71 -18
  14. package/src/components/resources/index.ts +1 -1
  15. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -1
  16. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -1
  17. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -1
  18. package/src/components/resources/renderers/PodRenderer.tsx +4 -3
  19. package/src/components/resources/renderers/SecretRenderer.tsx +4 -10
  20. package/src/components/shared/EditableYamlView.tsx +28 -17
  21. package/src/components/shared/ManagedByChip.tsx +45 -0
  22. package/src/components/shared/index.ts +1 -0
  23. package/src/components/timeline/TimelineList.tsx +3 -3
  24. package/src/components/topology/TopologyGraph.tsx +3 -2
  25. package/src/components/ui/Tooltip.tsx +10 -1
  26. package/src/components/ui/drawer-components.tsx +1 -1
  27. package/src/components/workload/ResourceDetailDrawer.tsx +5 -3
  28. package/src/components/workload/WorkloadView.tsx +66 -0
  29. package/src/hooks/useKeyboardShortcuts.tsx +3 -2
  30. package/src/index.ts +3 -0
  31. package/src/types/core.ts +19 -2
  32. package/src/types/gitops-insights.ts +193 -0
  33. package/src/types/gitops-tree.ts +57 -0
  34. package/src/types/index.ts +2 -0
  35. package/src/utils/badge-colors.ts +6 -1
  36. package/src/utils/format.ts +28 -0
  37. package/src/utils/gitops-owner.test.ts +136 -0
  38. package/src/utils/gitops-owner.ts +92 -0
  39. package/src/utils/gitops-route.test.ts +78 -0
  40. package/src/utils/gitops-route.ts +104 -0
  41. package/src/utils/index.ts +2 -0
  42. package/src/utils/navigation.ts +14 -0
  43. package/src/utils/resource-hierarchy.ts +47 -3
  44. package/src/utils/yaml.test.ts +101 -0
  45. package/src/utils/yaml.ts +26 -0
@@ -0,0 +1,6 @@
1
+ export {
2
+ GitOpsActivityInsightView,
3
+ GitOpsChangesView,
4
+ GitOpsIssuesBand,
5
+ GitOpsStatusStrip,
6
+ } from './GitOpsInsightViews'
@@ -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
+ }