@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
@@ -7,6 +7,7 @@ import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
7
7
  import { clsx } from 'clsx'
8
8
  import {
9
9
  ArrowLeft,
10
+ ArrowRight,
10
11
  RefreshCw,
11
12
  Activity,
12
13
  Terminal,
@@ -22,6 +23,8 @@ import {
22
23
  import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom } from '../../types'
23
24
  import type { NavigateToResource } from '../../utils/navigation'
24
25
  import { refToSelectedResource, pluralToKind } from '../../utils/navigation'
26
+ import { detectGitOpsOwner, type GitOpsOwnerRef } from '../../utils/gitops-owner'
27
+ import { gitOpsRouteForResource } from '../../utils/gitops-route'
25
28
  import { isChangeEvent, isHistoricalEvent } from '../../types'
26
29
  import { getKindBadgeColor, getHealthBadgeColor } from '../../utils/badge-colors'
27
30
  import { buildResourceHierarchy, getAllEventsFromHierarchy, isProblematicEvent, type ResourceLane } from '../../utils/resource-hierarchy'
@@ -41,6 +44,7 @@ import {
41
44
  import { ResourceActionsBar } from '../shared/ResourceActionsBar'
42
45
  import { EditableYamlView, SaveSuccessAnimation } from '../shared/EditableYamlView'
43
46
  import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from '../shared/ResourceRendererDispatch'
47
+ import { ManagedByChip } from '../shared/ManagedByChip'
44
48
  import { getKindColorOutline, formatKindName } from '../ui/drawer-components'
45
49
 
46
50
  type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
@@ -110,6 +114,24 @@ interface WorkloadViewProps {
110
114
  /** Called when tab changes (for URL sync etc.) */
111
115
  onTabChange?: (tab: TabType) => void
112
116
 
117
+ // ── GitOps navigation ─────────────────────────────────────────────────────
118
+ /**
119
+ * Open the GitOps detail page for a controller (Argo Application,
120
+ * Flux Kustomization, Flux HelmRelease). The drawer's "Managed by" chip
121
+ * invokes this when the user clicks through; if not provided, the chip
122
+ * is rendered as a non-interactive label so the relationship is still
123
+ * visible (useful for hosts that haven't routed the GitOps tab yet).
124
+ */
125
+ onOpenGitOpsResource?: (ref: GitOpsOwnerRef) => void
126
+ /**
127
+ * Open the GitOps detail page for the resource itself, when the resource
128
+ * is a portal-classified GitOps CR (Argo Application/ApplicationSet/
129
+ * AppProject, Flux Kustomization/HelmRelease). Wired in addition to
130
+ * `onOpenGitOpsResource` because the URL is derived here from the live
131
+ * resource rather than from owner labels on a managed object.
132
+ */
133
+ onNavigateGitOpsPath?: (path: string) => void
134
+
113
135
  // ── Render props for platform-specific content ───────────────────────────
114
136
  /** Render the logs tab content */
115
137
  renderLogsTab?: (props: {
@@ -135,6 +157,10 @@ interface WorkloadViewProps {
135
157
  /** Duplicate handler — opens create dialog with this resource's YAML */
136
158
  onDuplicate?: (params: { kind: string; namespace: string; name: string; yaml: string }) => void
137
159
 
160
+ // ── Download ─────────────────────────────────────────────────────────────
161
+ /** Forwarded to EditableYamlView; see there. */
162
+ onDownload?: (content: string, mime: string, filename: string) => void
163
+
138
164
  // ── ResourceActionsBar props (passed through) ────────────────────────────
139
165
  /** All props for the actions bar (forwarded as-is) */
140
166
  actionsBarProps?: Record<string, any>
@@ -186,6 +212,7 @@ export function WorkloadView({
186
212
  isMetricsAvailable,
187
213
  // Duplicate
188
214
  onDuplicate,
215
+ onDownload,
189
216
  renderOverviewExtra,
190
217
  // Actions bar
191
218
  actionsBarProps,
@@ -193,6 +220,9 @@ export function WorkloadView({
193
220
  rendererOverrides,
194
221
  // Pod env expansion
195
222
  resolvedEnvFrom,
223
+ // GitOps
224
+ onOpenGitOpsResource,
225
+ onNavigateGitOpsPath,
196
226
  }: WorkloadViewProps) {
197
227
  // Normalize kind: URL has plural lowercase, internal logic uses singular PascalCase
198
228
  const kind = pluralToKind(kindProp)
@@ -281,6 +311,12 @@ export function WorkloadView({
281
311
 
282
312
  // Metadata
283
313
  const metadata = useMemo(() => extractMetadata(kind, resource), [kind, resource])
314
+ const gitopsOwner = useMemo(() => detectGitOpsOwner(resource), [resource])
315
+ // When the resource itself is a portal GitOps CR (Application, Kustomization,
316
+ // HelmRelease, etc.), surface a link to its dedicated GitOps detail page —
317
+ // the drawer's renderer is thorough but the tab has the tree + insights +
318
+ // operations the drawer can't reproduce inline.
319
+ const gitOpsResourcePath = useMemo(() => gitOpsRouteForResource(resource), [resource])
284
320
 
285
321
  // Copy to clipboard
286
322
  const copyToClipboard = useCallback((text: string, key: string) => {
@@ -425,6 +461,14 @@ export function WorkloadView({
425
461
  </button>
426
462
  </div>
427
463
  <p className="text-sm text-theme-text-tertiary">{namespace}</p>
464
+ {(gitopsOwner || (gitOpsResourcePath && onNavigateGitOpsPath)) && (
465
+ <div className="mt-1 flex flex-wrap items-center gap-1.5">
466
+ {gitopsOwner && <ManagedByChip owner={gitopsOwner} onOpen={onOpenGitOpsResource} />}
467
+ {gitOpsResourcePath && onNavigateGitOpsPath && (
468
+ <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
469
+ )}
470
+ </div>
471
+ )}
428
472
  </div>
429
473
 
430
474
  {/* Actions bar */}
@@ -451,6 +495,7 @@ export function WorkloadView({
451
495
  isSaving={isUpdatingResource}
452
496
  saveError={updateResourceError}
453
497
  onDuplicate={onDuplicate}
498
+ onDownload={onDownload}
454
499
  />
455
500
  ) : (
456
501
  <>
@@ -527,6 +572,12 @@ export function WorkloadView({
527
572
  {metadata.find(m => m.label === 'Image') && (
528
573
  <span className="truncate max-w-md font-mono text-xs">{metadata.find(m => m.label === 'Image')?.value}</span>
529
574
  )}
575
+ {gitopsOwner && (
576
+ <ManagedByChip owner={gitopsOwner} onOpen={onOpenGitOpsResource} variant="block" />
577
+ )}
578
+ {gitOpsResourcePath && onNavigateGitOpsPath && (
579
+ <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
580
+ )}
530
581
  {relationships?.owner && (
531
582
  <span>Owner: <button onClick={() => onNavigateToResource?.(refToSelectedResource(relationships.owner!))} className="text-blue-500 hover:underline">{relationships.owner.name}</button></span>
532
583
  )}
@@ -675,6 +726,7 @@ export function WorkloadView({
675
726
  isSaving={isUpdatingResource}
676
727
  saveError={updateResourceError}
677
728
  onDuplicate={onDuplicate}
729
+ onDownload={onDownload}
678
730
  />
679
731
  )}
680
732
  </div>
@@ -725,6 +777,20 @@ function extractMetadata(kind: string, resource: any): { label: string; value: s
725
777
  // SUB-COMPONENTS
726
778
  // ============================================================================
727
779
 
780
+ function OpenInGitOpsChip({ onClick }: { onClick: () => void }) {
781
+ return (
782
+ <button
783
+ type="button"
784
+ onClick={onClick}
785
+ title="Open this resource in the GitOps tab (tree + insights + ops)"
786
+ className="inline-flex items-center gap-1 rounded border border-skyhook-500/40 bg-skyhook-500/10 px-1.5 py-0.5 text-[11px] font-medium text-skyhook-500 hover:bg-skyhook-500/20 transition-colors"
787
+ >
788
+ Open in GitOps
789
+ <ArrowRight className="h-3 w-3 shrink-0" />
790
+ </button>
791
+ )
792
+ }
793
+
728
794
  function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
729
795
  return (
730
796
  <button
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
2
2
 
3
- export type ShortcutScope = 'global' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'drawer'
3
+ export type ShortcutScope = 'global' | 'topology' | 'resources' | 'timeline' | 'helm' | 'gitops' | 'traffic' | 'drawer'
4
4
 
5
5
  // Scope priority: higher number = higher priority (wins when multiple scopes active)
6
6
  const SCOPE_PRIORITY: Record<ShortcutScope, number> = {
@@ -9,11 +9,12 @@ const SCOPE_PRIORITY: Record<ShortcutScope, number> = {
9
9
  resources: 1,
10
10
  timeline: 1,
11
11
  helm: 1,
12
+ gitops: 1,
12
13
  traffic: 1,
13
14
  drawer: 2,
14
15
  }
15
16
 
16
- export type ShortcutCategory = 'Navigation' | 'Search' | 'Resource Actions' | 'Table' | 'General' | 'Topology' | 'Timeline' | 'Helm' | 'Drawer' | 'Dock'
17
+ export type ShortcutCategory = 'Navigation' | 'Search' | 'Resource Actions' | 'Table' | 'General' | 'Topology' | 'Timeline' | 'Helm' | 'GitOps' | 'Drawer' | 'Dock'
17
18
 
18
19
  export interface KeyboardShortcut {
19
20
  /** Unique ID for this shortcut */
package/src/index.ts CHANGED
@@ -22,6 +22,9 @@ export * from './components/logs'
22
22
  // Timeline
23
23
  export * from './components/timeline'
24
24
 
25
+ // GitOps
26
+ export * from './components/gitops'
27
+
25
28
  // Shared components (ResourceRendererDispatch, EditableYamlView, ResourceActionsBar)
26
29
  export * from './components/shared'
27
30
 
package/src/types/core.ts CHANGED
@@ -20,6 +20,10 @@ export interface ResourcePermissions {
20
20
  hpas: boolean
21
21
  gateways: boolean
22
22
  httpRoutes: boolean
23
+ roles: boolean
24
+ clusterRoles: boolean
25
+ roleBindings: boolean
26
+ clusterRoleBindings: boolean
23
27
  }
24
28
 
25
29
  // Feature capabilities based on RBAC permissions
@@ -233,6 +237,7 @@ export interface TimelineEvent {
233
237
 
234
238
  // Resource identity
235
239
  kind: string
240
+ apiVersion?: string // e.g. "apps/v1", "cluster.x-k8s.io/v1beta1"
236
241
  namespace: string
237
242
  name: string
238
243
  uid?: string
@@ -454,6 +459,11 @@ export interface HelmRelease {
454
459
  resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
455
460
  healthIssue?: string // Primary issue if unhealthy (e.g., "OOMKilled")
456
461
  healthSummary?: string // Brief summary like "2/3 pods ready"
462
+ // When set, this release was installed by Flux's helm-controller — the
463
+ // user should manage it via the named HelmRelease CR (GitOps tab) rather
464
+ // than helm CLI / Radar's Helm view, since changes here would get
465
+ // reverted at the next reconcile. Format: "namespace/name".
466
+ managedByFluxHelmRelease?: string
457
467
  }
458
468
 
459
469
  export interface HelmRevision {
@@ -483,6 +493,9 @@ export interface HelmReleaseDetail {
483
493
  hooks?: HelmHook[]
484
494
  readme?: string
485
495
  dependencies?: ChartDependency[]
496
+ // When set, this release was installed by Flux's helm-controller — see
497
+ // HelmRelease.managedByFluxHelmRelease for context. Format: "namespace/name".
498
+ managedByFluxHelmRelease?: string
486
499
  }
487
500
 
488
501
  export interface HelmHook {
@@ -503,6 +516,7 @@ export interface ChartDependency {
503
516
 
504
517
  export interface HelmOwnedResource {
505
518
  kind: string
519
+ apiVersion?: string // e.g. "apps/v1", "cluster.x-k8s.io/v1beta1"
506
520
  name: string
507
521
  namespace: string
508
522
  status?: string // Running, Pending, Failed, Active, etc.
@@ -886,8 +900,11 @@ export interface TrafficFilters {
886
900
  timeRange: string
887
901
  }
888
902
 
889
- // Main view type now includes 'traffic' and 'cost'
890
- export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit'
903
+ // Main view type now includes 'traffic', 'cost', 'audit', 'gitops'.
904
+ // Library consumers (Radar Hub) get all GitOps surfaces the package
905
+ // IS the public surface, so adding new top-level views must extend
906
+ // this type rather than rely on app-local extensions.
907
+ export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit' | 'gitops'
891
908
 
892
909
  // ============================================================================
893
910
  // Image Filesystem Types
@@ -0,0 +1,193 @@
1
+ export interface GitOpsInsight {
2
+ summary: GitOpsInsightSummary
3
+ issues?: GitOpsIssue[]
4
+ changes?: GitOpsChange[]
5
+ plan?: GitOpsPlanItem[]
6
+ history?: GitOpsHistoryItem[]
7
+ capabilities?: GitOpsCapabilities
8
+ // Non-fatal reasons the response is incomplete (RBAC short-circuit,
9
+ // controller unreachable, etc.). UI surfaces these so users can tell
10
+ // "no data" from "we couldn't fetch it".
11
+ warnings?: string[]
12
+ partial?: boolean
13
+ }
14
+
15
+ import type { GitOpsTool } from './gitops'
16
+
17
+ // Closed enums mirroring `pkg/gitops/insights/vocab.go`. Keeping the FE
18
+ // vocabulary in lockstep with the Go side means switches over these fields
19
+ // are exhaustive and wire-contract drift surfaces at compile time instead of
20
+ // at runtime as a missing render branch.
21
+ export type GitOpsScope = 'operation' | 'resource' | 'condition' | 'tree' | 'lifecycle'
22
+
23
+ export type GitOpsCategory =
24
+ | 'Synced'
25
+ | 'OutOfSync'
26
+ | 'Degraded'
27
+ | 'Missing'
28
+ | 'Pruned'
29
+ | 'Hook'
30
+ | 'Progressing'
31
+ | 'Reconciling'
32
+ | 'Suspended'
33
+ | 'Unknown'
34
+
35
+ export type GitOpsDriftSource = 'lastAppliedAnnotation'
36
+
37
+ export interface GitOpsInsightSummary {
38
+ tool: GitOpsTool
39
+ kind: string
40
+ namespace: string
41
+ name: string
42
+ sync?: string
43
+ health?: string
44
+ operationPhase?: string
45
+ // Latest operation status message — surfaced inline in the status strip
46
+ // when an operation is in flight or just failed.
47
+ operationMessage?: string
48
+ source?: string
49
+ targetRevision?: string
50
+ lastRevision?: string
51
+ lastReconcile?: string
52
+ partialReason?: string
53
+ // Human-readable sync mode for the chip in the status strip.
54
+ // Argo: "Manual" | "Auto" | "Auto · prune" | "Auto · self-heal" | "Auto · prune · self-heal"
55
+ // Flux: "Auto" | "Suspended"
56
+ autoSyncMode?: string
57
+ // True when the resource has metadata.deletionTimestamp set. Drives the
58
+ // [Terminating] chip in the title row + disables mutating action buttons.
59
+ // Backend mirrors this guard in pkg/gitops/operations.go so direct API
60
+ // hits also fail with ErrResourceTerminating.
61
+ terminating?: boolean
62
+ // RFC3339 deletion timestamp; used to compute "21d ago" text in the chip
63
+ // tooltip.
64
+ terminationStartedAt?: string
65
+ // Finalizers blocking deletion. When stuck, naming the finalizer points
66
+ // the user at the controller they need to investigate.
67
+ finalizers?: string[]
68
+ }
69
+
70
+ export interface GitOpsInsightRef {
71
+ group?: string
72
+ kind: string
73
+ namespace?: string
74
+ name: string
75
+ }
76
+
77
+ export interface GitOpsIssue {
78
+ severity: 'critical' | 'alert' | 'warning' | 'info'
79
+ scope: GitOpsScope
80
+ reason: string
81
+ message: string
82
+ refs?: GitOpsInsightRef[]
83
+ action?: string
84
+ // Plain-English root cause when the message matched a recognized error
85
+ // pattern. Empty for unrecognized messages — UI falls back to the raw message.
86
+ cause?: string
87
+ // Argo retry count parsed from "(retried N times)". 0 = no retry info.
88
+ retryCount?: number
89
+ // True when retry count crossed the "no longer transient" threshold.
90
+ // Drives a stronger visual treatment.
91
+ stuck?: boolean
92
+ // Structured one-click remediation. When present, the failure card renders
93
+ // a contextual action button. Nil when no automated remedy applies — the
94
+ // `action` string still describes the manual path in that case.
95
+ remediation?: GitOpsRemediation
96
+ }
97
+
98
+ export type GitOpsRemediationKind = 'create-namespace'
99
+
100
+ export interface GitOpsRemediation {
101
+ kind: GitOpsRemediationKind
102
+ target?: string
103
+ hint?: string
104
+ }
105
+
106
+ export interface GitOpsChange {
107
+ ref: GitOpsInsightRef
108
+ category: GitOpsCategory
109
+ sync?: string
110
+ health?: string
111
+ message?: string
112
+ // Per-resource sync failure message (Argo's status.resources[].syncResult).
113
+ // Distinct from `message` (live health). Empty when sync succeeded.
114
+ syncError?: string
115
+ // Sync hook phase: PreSync / PostSync / SyncFail / PostDelete. Empty
116
+ // for non-hook resources.
117
+ hookPhase?: string
118
+ hasDesired: boolean
119
+ hasLive: boolean
120
+ // Structured per-field diff between the desired state (parsed from
121
+ // kubectl.kubernetes.io/last-applied-configuration) and the live spec.
122
+ // Undefined when the diff couldn't be computed (no annotation, SSA-applied
123
+ // resource, Helm-managed). Renderer falls back to the textual explainer
124
+ // when undefined.
125
+ drift?: GitOpsDrift
126
+ // Up to ~5 most recent events involving this resource, newest first.
127
+ // Surfaces the underlying "why is this stuck" cause (ImagePullBackOff,
128
+ // FailedScheduling, FailedMount, webhook denial) inline so the operator
129
+ // doesn't have to drill into the standard resource drawer.
130
+ recentEvents?: GitOpsEventSummary[]
131
+ partial: boolean
132
+ partialNote?: string
133
+ }
134
+
135
+ export interface GitOpsDrift {
136
+ entries: GitOpsDriftEntry[]
137
+ source: GitOpsDriftSource
138
+ truncated?: boolean
139
+ }
140
+
141
+ export interface GitOpsDriftEntry {
142
+ path: string // e.g. "spec.disruption.expireAfter"
143
+ op: 'added' | 'removed' | 'changed'
144
+ desired?: string // JSON-encoded
145
+ live?: string // JSON-encoded
146
+ }
147
+
148
+ export interface GitOpsEventSummary {
149
+ type: GitOpsEventType
150
+ reason: string
151
+ message: string
152
+ count?: number
153
+ lastTimestamp: string // RFC3339
154
+ reportingComponent?: string
155
+ }
156
+
157
+ export type GitOpsEventType = 'Normal' | 'Warning'
158
+
159
+ export interface GitOpsPlanItem {
160
+ ref: GitOpsInsightRef
161
+ phase?: string
162
+ wave?: number
163
+ waveSet?: boolean
164
+ order: number
165
+ hook?: string
166
+ relationship?: string
167
+ status?: string
168
+ blockedBy?: GitOpsInsightRef[]
169
+ notes?: string[]
170
+ }
171
+
172
+ export interface GitOpsHistoryItem {
173
+ id?: string
174
+ revision?: string
175
+ deployedAt?: string
176
+ phase?: string
177
+ message?: string
178
+ source?: string
179
+ initiatedBy?: string
180
+ }
181
+
182
+ export interface GitOpsCapabilities {
183
+ sync: boolean
184
+ refresh: boolean
185
+ terminate: boolean
186
+ suspend: boolean
187
+ resume: boolean
188
+ syncWithSource: boolean
189
+ selectiveSync: boolean
190
+ rollback: boolean
191
+ unsupportedReason?: string
192
+ warnings?: string[]
193
+ }
@@ -0,0 +1,57 @@
1
+ import type { HealthStatus } from './core'
2
+ import type { GitOpsHealthStatus, SyncStatus } from './gitops'
3
+
4
+ export type GitOpsTreeTool = 'argocd' | 'fluxcd'
5
+ export type GitOpsTreeNodeRole = 'root' | 'declared' | 'generated' | 'group'
6
+
7
+ export interface GitOpsTreeRef {
8
+ group?: string
9
+ kind: string
10
+ namespace: string
11
+ name: string
12
+ uid?: string
13
+ }
14
+
15
+ export interface GitOpsTreeInfoItem {
16
+ name: string
17
+ value: string
18
+ }
19
+
20
+ export interface GitOpsTreeNode {
21
+ id: string
22
+ ref: GitOpsTreeRef
23
+ role: GitOpsTreeNodeRole
24
+ tool: GitOpsTreeTool
25
+ sync?: SyncStatus
26
+ health?: GitOpsHealthStatus
27
+ topologyStatus?: HealthStatus
28
+ info?: GitOpsTreeInfoItem[]
29
+ resource?: unknown
30
+ groupedNodeIDs?: string[]
31
+ count?: number
32
+ data?: Record<string, unknown>
33
+ }
34
+
35
+ export type GitOpsTreeEdgeType = 'owns' | 'source' | 'dependsOn'
36
+
37
+ export interface GitOpsTreeEdge {
38
+ source: string
39
+ target: string
40
+ type: GitOpsTreeEdgeType
41
+ }
42
+
43
+ export interface GitOpsTreeSummary {
44
+ declared: number
45
+ generated: number
46
+ grouped: number
47
+ degraded: number
48
+ outOfSync: number
49
+ }
50
+
51
+ export interface GitOpsResourceTree {
52
+ root: GitOpsTreeNode
53
+ nodes: GitOpsTreeNode[]
54
+ edges: GitOpsTreeEdge[]
55
+ warnings?: string[]
56
+ summary?: GitOpsTreeSummary
57
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './core'
2
2
  export * from './gitops'
3
+ export * from './gitops-tree'
4
+ export * from './gitops-insights'
@@ -93,6 +93,7 @@ export const SEVERITY_BADGE = BADGE_SEVERITY_COLORS
93
93
  export const SEVERITY_TEXT = {
94
94
  success: 'text-emerald-700 dark:text-emerald-400',
95
95
  warning: 'text-amber-700 dark:text-amber-400',
96
+ alert: 'text-orange-700 dark:text-orange-400',
96
97
  error: 'text-red-700 dark:text-red-400',
97
98
  info: 'text-sky-700 dark:text-sky-400',
98
99
  neutral: 'text-theme-text-secondary',
@@ -102,6 +103,7 @@ export const SEVERITY_TEXT = {
102
103
  export const SEVERITY_DOT = {
103
104
  success: 'bg-emerald-500',
104
105
  warning: 'bg-amber-500',
106
+ alert: 'bg-orange-500',
105
107
  error: 'bg-red-500',
106
108
  info: 'bg-sky-500',
107
109
  neutral: 'bg-theme-hover',
@@ -111,6 +113,7 @@ export const SEVERITY_DOT = {
111
113
  export const SEVERITY_BORDER = {
112
114
  success: 'border-emerald-200 dark:border-emerald-800/40',
113
115
  warning: 'border-amber-200 dark:border-amber-800/40',
116
+ alert: 'border-orange-200 dark:border-orange-800/40',
114
117
  error: 'border-red-200 dark:border-red-800/40',
115
118
  info: 'border-sky-200 dark:border-sky-800/40',
116
119
  neutral: 'border-theme-border',
@@ -123,7 +126,7 @@ export const SEVERITY_BADGE_BORDERED = {
123
126
  } as const
124
127
 
125
128
  // Severity type
126
- export type Severity = 'success' | 'warning' | 'error' | 'info' | 'neutral'
129
+ export type Severity = 'success' | 'warning' | 'alert' | 'error' | 'info' | 'neutral'
127
130
 
128
131
  // =============================================================================
129
132
  // RESOURCE STATUS COLORS - for K8s resource states
@@ -216,6 +219,8 @@ export function healthToSeverity(health: string): Severity {
216
219
  case 'warning':
217
220
  case 'pending':
218
221
  return 'warning'
222
+ case 'alert':
223
+ return 'alert'
219
224
  case 'unhealthy':
220
225
  case 'error':
221
226
  case 'failed':
@@ -175,6 +175,34 @@ export function formatCPUMillicores(millicores: number): string {
175
175
  return formatCoresValue(cores)
176
176
  }
177
177
 
178
+ // =============================================================================
179
+ // Time Formatting
180
+ // =============================================================================
181
+
182
+ export function formatCompactAge(value?: string): string {
183
+ if (!value) return ''
184
+ const time = Date.parse(value)
185
+ if (!Number.isFinite(time)) return ''
186
+ const seconds = Math.max(0, Math.floor((Date.now() - time) / 1000))
187
+ if (seconds < 60) return `${seconds}s`
188
+ const minutes = Math.floor(seconds / 60)
189
+ if (minutes < 60) return `${minutes}m`
190
+ const hours = Math.floor(minutes / 60)
191
+ if (hours < 24) return `${hours}h`
192
+ return `${Math.floor(hours / 24)}d`
193
+ }
194
+
195
+ export function formatRelativeAgeTime(value?: string, fallback = '-'): string {
196
+ if (!value) return fallback
197
+ const time = Date.parse(value)
198
+ if (!Number.isFinite(time)) return value
199
+ const diff = Date.now() - time
200
+ if (diff < 0) return new Date(time).toLocaleString()
201
+ const compact = formatCompactAge(value)
202
+ if (!compact) return fallback
203
+ return compact === '0s' ? 'just now' : `${compact} ago`
204
+ }
205
+
178
206
  /**
179
207
  * Format memory MiB to human-readable string.
180
208
  * Used by dashboard API which returns memory in MiB.