@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
|
@@ -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'
|
|
890
|
-
|
|
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
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -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':
|
package/src/utils/format.ts
CHANGED
|
@@ -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.
|