@skyhook-io/k8s-ui 1.5.13 → 1.6.1

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 (57) hide show
  1. package/package.json +4 -4
  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/GitOpsDetailLayout.tsx +621 -0
  6. package/src/components/gitops/GitOpsGraphFilterRail.tsx +216 -0
  7. package/src/components/gitops/GitOpsTableView.tsx +1441 -0
  8. package/src/components/gitops/RollbackDialog.tsx +117 -0
  9. package/src/components/gitops/SyncOptionsDialog.tsx +160 -0
  10. package/src/components/gitops/detail-helpers.test.ts +97 -0
  11. package/src/components/gitops/detail-helpers.ts +112 -0
  12. package/src/components/gitops/index.ts +55 -0
  13. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1456 -0
  14. package/src/components/gitops/insights/index.ts +6 -0
  15. package/src/components/gitops/insights/insights-helpers.test.ts +98 -0
  16. package/src/components/gitops/insights/insights-helpers.ts +99 -0
  17. package/src/components/gitops/short-cluster-name.test.ts +36 -0
  18. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +799 -0
  19. package/src/components/gitops/tree/index.ts +6 -0
  20. package/src/components/gitops/tree/merge.test.ts +240 -0
  21. package/src/components/gitops/tree/merge.ts +160 -0
  22. package/src/components/gitops/tree/tree-helpers.ts +42 -0
  23. package/src/components/resources/ResourcesSidebar.tsx +42 -15
  24. package/src/components/resources/ResourcesView.tsx +136 -30
  25. package/src/components/resources/index.ts +1 -1
  26. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -1
  27. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -1
  28. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -1
  29. package/src/components/resources/renderers/PodRenderer.tsx +4 -3
  30. package/src/components/resources/renderers/SecretRenderer.tsx +4 -10
  31. package/src/components/shared/EditableYamlView.tsx +28 -17
  32. package/src/components/shared/ManagedByChip.tsx +45 -0
  33. package/src/components/shared/index.ts +1 -0
  34. package/src/components/timeline/TimelineList.tsx +3 -3
  35. package/src/components/topology/TopologyGraph.tsx +3 -2
  36. package/src/components/ui/Tooltip.tsx +10 -1
  37. package/src/components/ui/drawer-components.tsx +9 -21
  38. package/src/components/workload/ResourceDetailDrawer.tsx +5 -3
  39. package/src/components/workload/WorkloadView.tsx +66 -0
  40. package/src/hooks/useKeyboardShortcuts.tsx +3 -2
  41. package/src/index.ts +3 -0
  42. package/src/types/core.ts +48 -6
  43. package/src/types/gitops-insights.ts +193 -0
  44. package/src/types/gitops-tree.ts +57 -0
  45. package/src/types/index.ts +2 -0
  46. package/src/utils/badge-colors.ts +31 -1
  47. package/src/utils/format.ts +28 -0
  48. package/src/utils/gitops-owner.test.ts +95 -0
  49. package/src/utils/gitops-owner.ts +55 -0
  50. package/src/utils/gitops-route.test.ts +78 -0
  51. package/src/utils/gitops-route.ts +104 -0
  52. package/src/utils/helm-status.test.ts +50 -0
  53. package/src/utils/index.ts +2 -0
  54. package/src/utils/navigation.ts +14 -0
  55. package/src/utils/resource-hierarchy.ts +47 -3
  56. package/src/utils/yaml.test.ts +101 -0
  57. package/src/utils/yaml.ts +26 -0
@@ -2,6 +2,7 @@ import { useState, useCallback, useEffect, useRef, type ReactNode } from 'react'
2
2
  import { TRANSITION_DRAWER } from '../../utils/animation'
3
3
  import { clsx } from 'clsx'
4
4
  import type { SelectedResource } from '../../types'
5
+ import { useDockReservedHeight } from '../dock/DockContext'
5
6
 
6
7
  interface ResourceDetailDrawerProps {
7
8
  resource: SelectedResource
@@ -111,11 +112,12 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
111
112
  }, [expanded, onNavigateToResource, onNavigate])
112
113
 
113
114
  const headerHeight = headerHeightProp ?? 49
115
+ const dockInset = useDockReservedHeight()
114
116
 
115
117
  return (
116
118
  <div
117
119
  className={clsx(
118
- 'fixed right-0 bg-theme-surface border-l border-theme-border flex flex-col shadow-drawer z-40',
120
+ 'absolute right-0 bg-theme-surface border-l border-theme-border flex flex-col shadow-drawer z-40',
119
121
  TRANSITION_DRAWER,
120
122
  isOpen
121
123
  ? 'translate-x-0 opacity-100'
@@ -123,9 +125,9 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
123
125
  expanded && '!border-l-0',
124
126
  )}
125
127
  style={{
126
- width: expanded ? `calc(100vw - ${leftOffset}px)` : drawerWidth,
128
+ width: expanded ? `calc(100% - ${leftOffset}px)` : drawerWidth,
127
129
  top: headerHeight,
128
- height: `calc(100vh - ${headerHeight}px)`,
130
+ height: `calc(100% - ${headerHeight}px - ${dockInset}px)`,
129
131
  // Collapse is instant — no animation, content and width snap together.
130
132
  // Expand + slide-in/out animate via TRANSITION_DRAWER class.
131
133
  ...(isCollapsing && { transition: 'none' }),
@@ -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 { gitOpsOwnerFromRelationships, 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(() => gitOpsOwnerFromRelationships(relationships), [relationships])
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
@@ -1,6 +1,8 @@
1
1
  // Topology types matching the Go backend
2
2
 
3
- // Per-resource-type RBAC permissions (matches backend k8s.ResourcePermissions)
3
+ // Per-resource-type RBAC permissions. Field names must match the JSON keys
4
+ // produced by ResourcePermissions in internal/k8s/capabilities.go — there
5
+ // is no automated check across the Go/TS boundary.
4
6
  export interface ResourcePermissions {
5
7
  pods: boolean
6
8
  services: boolean
@@ -12,16 +14,39 @@ export interface ResourcePermissions {
12
14
  configMaps: boolean
13
15
  secrets: boolean
14
16
  events: boolean
15
- pvcs: boolean
17
+ persistentVolumeClaims: boolean
16
18
  nodes: boolean
17
19
  namespaces: boolean
18
20
  jobs: boolean
19
21
  cronJobs: boolean
20
- hpas: boolean
22
+ horizontalPodAutoscalers: boolean
23
+ persistentVolumes: boolean
24
+ storageClasses: boolean
25
+ podDisruptionBudgets: boolean
26
+ networkPolicies: boolean
27
+ serviceAccounts: boolean
28
+ roles: boolean
29
+ clusterRoles: boolean
30
+ roleBindings: boolean
31
+ clusterRoleBindings: boolean
32
+ limitRanges: boolean
21
33
  gateways: boolean
22
34
  httpRoutes: boolean
35
+ verticalPodAutoscalers: boolean
23
36
  }
24
37
 
38
+ // Keys in ResourcePermissions that represent optional CRDs Radar can monitor
39
+ // when they're installed in the cluster. A `false` here means "CRD not
40
+ // installed (or RBAC denied)" — NOT "Radar is missing data the user expects",
41
+ // so banners about RBAC restrictions should ignore these keys.
42
+ //
43
+ // Keep in sync with dynamicCapabilityKinds in internal/k8s/capabilities_alignment_test.go.
44
+ export const OPTIONAL_RESOURCE_KINDS: ReadonlyArray<keyof ResourcePermissions> = [
45
+ 'gateways',
46
+ 'httpRoutes',
47
+ 'verticalPodAutoscalers',
48
+ ]
49
+
25
50
  // Feature capabilities based on RBAC permissions
26
51
  export interface Capabilities {
27
52
  exec: boolean // Terminal feature (pods/exec)
@@ -233,6 +258,7 @@ export interface TimelineEvent {
233
258
 
234
259
  // Resource identity
235
260
  kind: string
261
+ apiVersion?: string // e.g. "apps/v1", "cluster.x-k8s.io/v1beta1"
236
262
  namespace: string
237
263
  name: string
238
264
  uid?: string
@@ -389,6 +415,7 @@ export interface ResourceRef {
389
415
  export interface Relationships {
390
416
  owner?: ResourceRef
391
417
  deployment?: ResourceRef // Grandparent Deployment (for Pods owned by ReplicaSets)
418
+ managedBy?: ResourceRef[] // Topmost meaningful manager(s): GitOps controller (ArgoCD Application / Flux Kustomization / Flux HelmRelease), Helm release, or the topmost K8s owner. Synthesized server-side; replaces client-side detectGitOpsOwner.
392
419
  children?: ResourceRef[]
393
420
  services?: ResourceRef[]
394
421
  ingresses?: ResourceRef[]
@@ -398,8 +425,11 @@ export interface Relationships {
398
425
  consumers?: ResourceRef[]
399
426
  scalers?: ResourceRef[]
400
427
  scaleTarget?: ResourceRef
401
- policies?: ResourceRef[]
428
+ pdbs?: ResourceRef[] // PodDisruptionBudgets protecting this workload
429
+ networkPolicies?: ResourceRef[] // NetworkPolicy / CiliumNetworkPolicy / ClusterNetworkPolicy variants selecting this workload
402
430
  pods?: ResourceRef[]
431
+ serviceAccount?: ResourceRef // For Pods: derived from pod.spec.serviceAccountName
432
+ node?: ResourceRef // For scheduled Pods: derived from pod.spec.nodeName
403
433
  }
404
434
 
405
435
  // Parsed X.509 certificate metadata (from backend cert parsing)
@@ -454,6 +484,11 @@ export interface HelmRelease {
454
484
  resourceHealth?: 'healthy' | 'degraded' | 'unhealthy' | 'unknown'
455
485
  healthIssue?: string // Primary issue if unhealthy (e.g., "OOMKilled")
456
486
  healthSummary?: string // Brief summary like "2/3 pods ready"
487
+ // When set, this release was installed by Flux's helm-controller — the
488
+ // user should manage it via the named HelmRelease CR (GitOps tab) rather
489
+ // than helm CLI / Radar's Helm view, since changes here would get
490
+ // reverted at the next reconcile. Format: "namespace/name".
491
+ managedByFluxHelmRelease?: string
457
492
  }
458
493
 
459
494
  export interface HelmRevision {
@@ -483,6 +518,9 @@ export interface HelmReleaseDetail {
483
518
  hooks?: HelmHook[]
484
519
  readme?: string
485
520
  dependencies?: ChartDependency[]
521
+ // When set, this release was installed by Flux's helm-controller — see
522
+ // HelmRelease.managedByFluxHelmRelease for context. Format: "namespace/name".
523
+ managedByFluxHelmRelease?: string
486
524
  }
487
525
 
488
526
  export interface HelmHook {
@@ -503,6 +541,7 @@ export interface ChartDependency {
503
541
 
504
542
  export interface HelmOwnedResource {
505
543
  kind: string
544
+ apiVersion?: string // e.g. "apps/v1", "cluster.x-k8s.io/v1beta1"
506
545
  name: string
507
546
  namespace: string
508
547
  status?: string // Running, Pending, Failed, Active, etc.
@@ -886,8 +925,11 @@ export interface TrafficFilters {
886
925
  timeRange: string
887
926
  }
888
927
 
889
- // Main view type now includes 'traffic' and 'cost'
890
- export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit'
928
+ // Main view type now includes 'traffic', 'cost', 'audit', 'gitops'.
929
+ // Library consumers (Radar Hub) get all GitOps surfaces the package
930
+ // IS the public surface, so adding new top-level views must extend
931
+ // this type rather than rely on app-local extensions.
932
+ export type ExtendedMainView = MainView | 'traffic' | 'cost' | 'audit' | 'gitops'
891
933
 
892
934
  // ============================================================================
893
935
  // 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':
@@ -281,6 +286,31 @@ export function getHelmStatusColor(status: string): string {
281
286
  return HELM_STATUS_COLORS[statusLower] || 'bg-theme-hover/50 text-theme-text-secondary'
282
287
  }
283
288
 
289
+ /**
290
+ * Helm release statuses where the row UI should signpost the user
291
+ * toward the drawer (history / rollback / logs).
292
+ *
293
+ * Currently `failed` only. The `pending-*` statuses (install /
294
+ * upgrade / rollback) are excluded deliberately: they're Helm's
295
+ * normal in-flight states during every routine operation. Treating
296
+ * them as "actionable" would briefly attach an alarming chevron +
297
+ * tooltip to every install while it ran — indistinguishable from
298
+ * the genuinely-stuck case (controller crashed mid-flight). Until
299
+ * we have release age available client-side to disambiguate
300
+ * "in-flight" from "stuck > N min", we give up the stuck-detect
301
+ * signpost rather than wrongly alarm the common case.
302
+ *
303
+ * @see https://github.com/helm/helm/blob/dev-v3/pkg/release/status.go
304
+ */
305
+ const ACTIONABLE_HELM_STATUSES: ReadonlySet<string> = new Set([
306
+ 'failed',
307
+ ])
308
+
309
+ export function isHelmReleaseActionable(status: string | null | undefined): boolean {
310
+ if (!status) return false
311
+ return ACTIONABLE_HELM_STATUSES.has(status.toLowerCase())
312
+ }
313
+
284
314
  // =============================================================================
285
315
  // VULNERABILITY SEVERITY COLORS - for Trivy and other security scanners
286
316
  // =============================================================================