@skyhook-io/k8s-ui 1.8.4 → 1.8.6
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 +2 -2
- package/src/components/charts/PrometheusChartsView.tsx +3 -8
- package/src/components/gitops/GitOpsDetailLayout.tsx +2 -3
- package/src/components/gitops/GitOpsTableView.tsx +2 -4
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +3 -6
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +3 -7
- package/src/components/logs/LogCore.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +34 -5
- package/src/components/resources/get-pod-phase-display.test.ts +115 -3
- package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
- package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
- package/src/components/resources/renderers/JobRenderer.tsx +6 -5
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
- package/src/components/resources/renderers/ServiceRenderer.tsx +1 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
- package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
- package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
- package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
- package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +1 -1
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
- package/src/components/resources/renderers/contour-cells.tsx +6 -5
- package/src/components/resources/renderers/index.ts +4 -0
- package/src/components/resources/renderers/istio-cells.tsx +16 -25
- package/src/components/resources/resource-utils-cnpg.ts +1 -1
- package/src/components/resources/resource-utils-keda.ts +12 -6
- package/src/components/resources/resource-utils.ts +172 -57
- package/src/components/shared/ResourceActionsBar.tsx +1 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
- package/src/components/topology/TopologyControls.tsx +41 -9
- package/src/components/topology/TopologyGraph.tsx +20 -7
- package/src/components/topology/layout-elk-graph.test.ts +222 -0
- package/src/components/topology/layout.ts +96 -33
- package/src/components/topology/layout.worker.ts +18 -34
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/CodeViewer.tsx +1 -1
- package/src/components/ui/PaneLoader.tsx +6 -1
- package/src/components/ui/RestrictedState.tsx +152 -0
- package/src/components/ui/YamlEditor.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +1 -1
- package/src/types/core.ts +37 -0
- package/src/utils/asset-url.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.6",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"yaml": ">=2.0.0"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
-
"@types/node": "^
|
|
88
|
+
"@types/node": "^26.0.0",
|
|
89
89
|
"@types/react": "^19.2.17",
|
|
90
90
|
"@types/react-dom": "^19.2.3",
|
|
91
91
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clsx } from 'clsx'
|
|
2
2
|
import { BarChart3, Wifi, WifiOff, Loader2 } from 'lucide-react'
|
|
3
3
|
import { AreaChart } from './AreaChart'
|
|
4
|
+
import { PaneLoader } from '../ui/PaneLoader'
|
|
4
5
|
import { MetricsSummary } from './MetricsSummary'
|
|
5
6
|
import { SeriesLegend } from './SeriesLegend'
|
|
6
7
|
import type { TimeSeries, ReferenceLine } from './types'
|
|
@@ -108,10 +109,7 @@ export function PrometheusChartsView({
|
|
|
108
109
|
if (statusLoading) {
|
|
109
110
|
if (!showEmptyState) return null
|
|
110
111
|
return (
|
|
111
|
-
<
|
|
112
|
-
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
113
|
-
Checking Prometheus availability...
|
|
114
|
-
</div>
|
|
112
|
+
<PaneLoader label="Checking Prometheus availability…" className="py-12" />
|
|
115
113
|
)
|
|
116
114
|
}
|
|
117
115
|
|
|
@@ -181,10 +179,7 @@ export function PrometheusChartsView({
|
|
|
181
179
|
{/* Chart area */}
|
|
182
180
|
<div className="min-h-[280px] flex-1 p-4">
|
|
183
181
|
{metricsLoading ? (
|
|
184
|
-
<
|
|
185
|
-
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
186
|
-
Loading metrics...
|
|
187
|
-
</div>
|
|
182
|
+
<PaneLoader label="Loading metrics…" className="min-h-[240px]" />
|
|
188
183
|
) : metricsError ? (
|
|
189
184
|
<div className="flex h-full items-center justify-center text-sm text-red-400">
|
|
190
185
|
Failed to load metrics: {(metricsError as Error).message}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, type ComponentType, type ReactNode } from 'react'
|
|
2
2
|
import { ArrowDownUp, ChevronDown, ChevronRight, Clock3, GitBranch, GitCommit, Loader2, Pause, Play, RefreshCw, Settings, Trash2, XCircle, Zap } from 'lucide-react'
|
|
3
|
+
import { PaneLoader } from '../ui/PaneLoader'
|
|
3
4
|
|
|
4
5
|
import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
|
|
5
6
|
import { GitOpsIssuesBand, GitOpsStatusStrip } from './insights'
|
|
@@ -487,9 +488,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
487
488
|
)}
|
|
488
489
|
|
|
489
490
|
{resourceLoading ? (
|
|
490
|
-
<
|
|
491
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading GitOps resource…
|
|
492
|
-
</div>
|
|
491
|
+
<PaneLoader label="Loading GitOps resource…" className="flex-1" />
|
|
493
492
|
) : resourceError ? (
|
|
494
493
|
<div className="p-4 text-sm text-red-500">Failed to load resource: {resourceError.message}</div>
|
|
495
494
|
) : (
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
HeartPulse,
|
|
13
13
|
LayoutGrid,
|
|
14
14
|
List,
|
|
15
|
-
Loader2,
|
|
16
15
|
Pause,
|
|
17
16
|
Play,
|
|
18
17
|
RefreshCw,
|
|
@@ -33,6 +32,7 @@ import { FacetSection, FacetButton } from '../ui/Facet'
|
|
|
33
32
|
import { SortableTh, TH_CLASS, type SortDir } from '../ui/SortableTh'
|
|
34
33
|
import { DistributionBar } from '../ui/DistributionBar'
|
|
35
34
|
import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
|
|
35
|
+
import { PaneLoader } from '../ui/PaneLoader'
|
|
36
36
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
37
37
|
import { getGitOpsResourceStatus } from './detail-helpers'
|
|
38
38
|
import { isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
@@ -754,9 +754,7 @@ export function GitOpsTableView({
|
|
|
754
754
|
{modeLabel(mode)} view is queued behind the application list.
|
|
755
755
|
</div>
|
|
756
756
|
) : loading && filteredRows.length === 0 ? (
|
|
757
|
-
<
|
|
758
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading GitOps applications...
|
|
759
|
-
</div>
|
|
757
|
+
<PaneLoader label="Loading GitOps applications…" className="h-full" />
|
|
760
758
|
) : error ? (
|
|
761
759
|
<div className="p-4 text-sm text-red-500">Failed to load GitOps applications: {error.message}</div>
|
|
762
760
|
) : filteredRows.length === 0 ? (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AlertTriangle, ChevronDown, ChevronRight, CircleAlert, Clock3, GitBranch, GitCommit, Info, Loader2, Plus, Trash2 } from 'lucide-react'
|
|
2
|
+
import { PaneLoader } from '../../ui/PaneLoader'
|
|
2
3
|
import { clsx } from 'clsx'
|
|
3
4
|
import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
4
5
|
import type { GitOpsChange, GitOpsHistoryItem, GitOpsInsight, GitOpsInsightRef, GitOpsIssue, GitOpsPlanItem, GitOpsRemediation, GitOpsResourceTree, GitOpsTreeNode } from '../../../types'
|
|
@@ -725,7 +726,7 @@ export function GitOpsChangesView({ insight, error, onOpenResource, focusKey, tr
|
|
|
725
726
|
return <InsightErrorState error={error} />
|
|
726
727
|
}
|
|
727
728
|
if (!insight) {
|
|
728
|
-
return <
|
|
729
|
+
return <PaneLoader label="Loading GitOps resources…" className="h-full" />
|
|
729
730
|
}
|
|
730
731
|
// Build plan metadata maps keyed by ref so each Change row can advertise
|
|
731
732
|
// its sync step, hook phase, and wave assignment. The plan and changes
|
|
@@ -1274,7 +1275,7 @@ interface GitOpsActivityInsightViewProps {
|
|
|
1274
1275
|
|
|
1275
1276
|
export function GitOpsActivityInsightView({ insight, error, onRollback }: GitOpsActivityInsightViewProps) {
|
|
1276
1277
|
if (error && !insight) return <InsightErrorState error={error} />
|
|
1277
|
-
if (!insight) return <
|
|
1278
|
+
if (!insight) return <PaneLoader label="Loading GitOps activity…" className="h-full" />
|
|
1278
1279
|
const canRollback = !!insight.capabilities?.rollback && !!onRollback
|
|
1279
1280
|
// Auto-sync makes rollback futile — the controller would re-sync to HEAD
|
|
1280
1281
|
// immediately. Argo's own Web UI disables the button in this state. Detect
|
|
@@ -1429,10 +1430,6 @@ function SectionHeader({ icon: Icon, title, hint }: { icon: typeof GitBranch; ti
|
|
|
1429
1430
|
)
|
|
1430
1431
|
}
|
|
1431
1432
|
|
|
1432
|
-
function CenteredText({ children }: { children: ReactNode }) {
|
|
1433
|
-
return <div className="flex h-full items-center justify-center text-sm text-theme-text-secondary">{children}</div>
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
1433
|
// Surfaced when the insights endpoint errors. Without this the subviews
|
|
1437
1434
|
// would render their "Loading…" placeholder forever, hiding the failure
|
|
1438
1435
|
// from the user and from the operator looking at logs.
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
type NodeTypes,
|
|
16
16
|
} from '@xyflow/react'
|
|
17
17
|
import '@xyflow/react/dist/style.css'
|
|
18
|
-
import { AlertTriangle, ChevronRight,
|
|
18
|
+
import { AlertTriangle, ChevronRight, Maximize, Search, X } from 'lucide-react'
|
|
19
|
+
import { PaneLoader } from '../../ui/PaneLoader'
|
|
19
20
|
import { clsx } from 'clsx'
|
|
20
21
|
|
|
21
22
|
import type { GitOpsResourceTree, GitOpsTreeNode, GitOpsTreeRef, HealthStatus } from '../../../types'
|
|
@@ -127,12 +128,7 @@ function GitOpsTreeGraphInner({
|
|
|
127
128
|
}, [onNodeClick])
|
|
128
129
|
|
|
129
130
|
if (loading) {
|
|
130
|
-
return
|
|
131
|
-
<div className="flex h-full items-center justify-center text-theme-text-secondary">
|
|
132
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
133
|
-
Loading GitOps resource tree...
|
|
134
|
-
</div>
|
|
135
|
-
)
|
|
131
|
+
return <PaneLoader label="Loading GitOps resource tree…" className="h-full" />
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
if (error) {
|
|
@@ -796,7 +796,7 @@ export function LogCore({
|
|
|
796
796
|
<div className={`flex-1 flex items-center justify-center ${palette.textTertiary}`}>
|
|
797
797
|
<div className="flex items-center gap-2">
|
|
798
798
|
<RotateCcw className="w-4 h-4 animate-spin" />
|
|
799
|
-
<span>Loading logs
|
|
799
|
+
<span>Loading logs…</span>
|
|
800
800
|
</div>
|
|
801
801
|
</div>
|
|
802
802
|
) : errorMessage ? (
|
|
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useEffect, useCallback, useRef, useContext, u
|
|
|
2
2
|
import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
3
3
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
4
4
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
5
|
+
import { RestrictedState } from '../ui/RestrictedState'
|
|
5
6
|
import type { TopPodMetrics, TopNodeMetrics } from '../../types'
|
|
6
7
|
import {
|
|
7
8
|
Search,
|
|
@@ -1840,6 +1841,9 @@ interface ResourcesViewProps {
|
|
|
1840
1841
|
// Lightweight counts for sidebar badges (from /api/resource-counts)
|
|
1841
1842
|
resourceCounts?: Record<string, number>
|
|
1842
1843
|
resourceForbidden?: string[]
|
|
1844
|
+
/** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
|
|
1845
|
+
* keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
|
|
1846
|
+
resourceReasons?: Record<string, string>
|
|
1843
1847
|
resourceUnavailable?: string[]
|
|
1844
1848
|
// Single query for the currently selected kind's full data
|
|
1845
1849
|
selectedKindQuery?: ResourceQueryResult
|
|
@@ -2058,6 +2062,7 @@ export function ResourcesView({
|
|
|
2058
2062
|
resourceQueries: resourceQueriesProp,
|
|
2059
2063
|
resourceCounts: resourceCountsProp,
|
|
2060
2064
|
resourceForbidden: resourceForbiddenProp,
|
|
2065
|
+
resourceReasons,
|
|
2061
2066
|
resourceUnavailable: resourceUnavailableProp,
|
|
2062
2067
|
selectedKindQuery: selectedKindQueryProp,
|
|
2063
2068
|
largeListGuard,
|
|
@@ -3232,7 +3237,6 @@ export function ResourcesView({
|
|
|
3232
3237
|
}, [resources])
|
|
3233
3238
|
const isLoading = selectedQuery?.isLoading ?? true
|
|
3234
3239
|
const selectedQueryError = selectedQuery?.error
|
|
3235
|
-
const isSelectedForbidden = isForbiddenError(selectedQueryError)
|
|
3236
3240
|
const refetchFn = selectedQuery?.refetch
|
|
3237
3241
|
const dataUpdatedAt = selectedQuery?.dataUpdatedAt
|
|
3238
3242
|
|
|
@@ -3294,6 +3298,23 @@ export function ResourcesView({
|
|
|
3294
3298
|
return result
|
|
3295
3299
|
}, [useNewCountsMode, resourceForbiddenProp, resourcesToCount, resourceQueries])
|
|
3296
3300
|
|
|
3301
|
+
// Render the restricted state when EITHER the list query 403s (namespaced
|
|
3302
|
+
// denials) OR the selected kind is in the counts `forbidden` set. Denied
|
|
3303
|
+
// cluster-scoped kinds return 200 with `[]` from the list endpoint, so the
|
|
3304
|
+
// 403 signal alone misses them — they'd fall through to "No <kind> found".
|
|
3305
|
+
const selectedKindCountKey = selectedKind.group
|
|
3306
|
+
? `${selectedKind.group}/${selectedKind.kind}`
|
|
3307
|
+
: selectedKind.kind
|
|
3308
|
+
// Actual rows win over a stale counts `forbidden` entry: a kind can be marked
|
|
3309
|
+
// forbidden/unavailable in counts (e.g. an informer not yet synced at counts
|
|
3310
|
+
// time) while the list query has since returned data — show the table, not
|
|
3311
|
+
// RestrictedState. A 403 on the list itself never carries rows, so it still
|
|
3312
|
+
// forces the restricted state.
|
|
3313
|
+
const selectedHasRows = Array.isArray(resources) && resources.length > 0
|
|
3314
|
+
const isSelectedForbidden =
|
|
3315
|
+
isForbiddenError(selectedQueryError) ||
|
|
3316
|
+
(!selectedHasRows && forbiddenKinds.has(selectedKindCountKey))
|
|
3317
|
+
|
|
3297
3318
|
// Reset sort and filters when kind changes (but not when syncing from URL navigation)
|
|
3298
3319
|
// Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
|
|
3299
3320
|
const prevKindRef = useRef(selectedKind.name)
|
|
@@ -4505,10 +4526,18 @@ export function ResourcesView({
|
|
|
4505
4526
|
{isLoading ? (
|
|
4506
4527
|
<PaneLoader className="absolute inset-0" />
|
|
4507
4528
|
) : isSelectedForbidden ? (
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4529
|
+
// overflow-y-auto + min-h-full: centered when the panel is short
|
|
4530
|
+
// (collapsed), scrollable (no top clip) when the RBAC snippet is
|
|
4531
|
+
// expanded on a shorter pane.
|
|
4532
|
+
<div className="absolute inset-0 overflow-y-auto">
|
|
4533
|
+
<div className="min-h-full flex items-center justify-center">
|
|
4534
|
+
<RestrictedState
|
|
4535
|
+
kindLabel={selectedKind.kind}
|
|
4536
|
+
group={selectedKind.group}
|
|
4537
|
+
resource={selectedKind.name}
|
|
4538
|
+
reason={resourceReasons?.[selectedKindCountKey]}
|
|
4539
|
+
/>
|
|
4540
|
+
</div>
|
|
4512
4541
|
</div>
|
|
4513
4542
|
) : largeListGuard ? (
|
|
4514
4543
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary px-6 text-center">
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { getPodPhaseDisplay } from './resource-utils'
|
|
2
|
+
import { getPodPhaseDisplay, getPodStatus } from './resource-utils'
|
|
3
|
+
|
|
4
|
+
describe('getPodStatus', () => {
|
|
5
|
+
it('keeps a Succeeded pod neutral despite a sidecar that OOMed before completion', () => {
|
|
6
|
+
const succeededWithOomedSidecar = {
|
|
7
|
+
status: {
|
|
8
|
+
phase: 'Succeeded',
|
|
9
|
+
containerStatuses: [
|
|
10
|
+
{ name: 'main', ready: false, state: { terminated: { reason: 'Completed', exitCode: 0 } } },
|
|
11
|
+
{ name: 'sidecar', ready: false, state: { terminated: { reason: 'OOMKilled', exitCode: 137 } } },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
const r = getPodStatus(succeededWithOomedSidecar)
|
|
16
|
+
expect(r.text).toBe('Completed')
|
|
17
|
+
expect(r.level).toBe('neutral')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('still flags a crash-looping (non-terminal) pod as unhealthy', () => {
|
|
21
|
+
const crashing = {
|
|
22
|
+
status: {
|
|
23
|
+
phase: 'Running',
|
|
24
|
+
containerStatuses: [{ name: 'app', ready: false, state: { waiting: { reason: 'CrashLoopBackOff' } } }],
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
expect(getPodStatus(crashing).level).toBe('unhealthy')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
3
30
|
|
|
4
31
|
const podRunningHealthy = {
|
|
5
32
|
status: {
|
|
@@ -192,12 +219,59 @@ describe('getPodPhaseDisplay', () => {
|
|
|
192
219
|
expect(r.level).toBe('unhealthy')
|
|
193
220
|
})
|
|
194
221
|
|
|
195
|
-
it('marks
|
|
222
|
+
it('marks a long-terminating pod as Terminating + degraded (stuck shutdown)', () => {
|
|
196
223
|
const r = getPodPhaseDisplay(podTerminating)
|
|
197
224
|
expect(r.text).toBe('Running — Terminating')
|
|
198
225
|
expect(r.level).toBe('degraded')
|
|
199
226
|
})
|
|
200
227
|
|
|
228
|
+
it('keeps a recently-terminating pod neutral (graceful shutdown in progress)', () => {
|
|
229
|
+
const recent = {
|
|
230
|
+
metadata: { deletionTimestamp: new Date(Date.now() - 30 * 1000).toISOString() },
|
|
231
|
+
status: { phase: 'Running', containerStatuses: [{ name: 'app', ready: true, restartCount: 0 }] },
|
|
232
|
+
}
|
|
233
|
+
const r = getPodPhaseDisplay(recent)
|
|
234
|
+
expect(r.text).toBe('Running — Terminating')
|
|
235
|
+
expect(r.level).toBe('neutral')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('treats a completing pod (container exited 0, not ready) as healthy', () => {
|
|
239
|
+
const completing = {
|
|
240
|
+
status: {
|
|
241
|
+
phase: 'Running',
|
|
242
|
+
containerStatuses: [
|
|
243
|
+
{ name: 'app', ready: false, restartCount: 0, state: { terminated: { reason: 'Completed', exitCode: 0 } } },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
const r = getPodPhaseDisplay(completing)
|
|
248
|
+
expect(r.text).toBe('Running')
|
|
249
|
+
expect(r.level).toBe('healthy')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('keeps a Failed pod unhealthy even while it is terminating', () => {
|
|
253
|
+
const failedTerminating = {
|
|
254
|
+
metadata: { deletionTimestamp: new Date().toISOString() },
|
|
255
|
+
status: { phase: 'Failed' },
|
|
256
|
+
}
|
|
257
|
+
const r = getPodPhaseDisplay(failedTerminating)
|
|
258
|
+
expect(r.text).toBe('Failed')
|
|
259
|
+
expect(r.level).toBe('unhealthy')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('surfaces a fatal container failure even while the pod is terminating', () => {
|
|
263
|
+
const terminatingCrash = {
|
|
264
|
+
metadata: { deletionTimestamp: new Date().toISOString() },
|
|
265
|
+
status: {
|
|
266
|
+
phase: 'Running',
|
|
267
|
+
containerStatuses: [{ name: 'app', ready: false, state: { waiting: { reason: 'CrashLoopBackOff' } } }],
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
const r = getPodPhaseDisplay(terminatingCrash)
|
|
271
|
+
expect(r.text).toBe('Running — CrashLoopBackOff')
|
|
272
|
+
expect(r.level).toBe('unhealthy')
|
|
273
|
+
})
|
|
274
|
+
|
|
201
275
|
it('falls back to Unknown for missing/unknown phase', () => {
|
|
202
276
|
expect(getPodPhaseDisplay({}).level).toBe('unknown')
|
|
203
277
|
expect(getPodPhaseDisplay({}).phase).toBe('Unknown')
|
|
@@ -206,10 +280,48 @@ describe('getPodPhaseDisplay', () => {
|
|
|
206
280
|
|
|
207
281
|
it('handles Succeeded/Pending/Failed phases', () => {
|
|
208
282
|
expect(getPodPhaseDisplay({ status: { phase: 'Succeeded' } }).level).toBe('neutral')
|
|
209
|
-
|
|
283
|
+
// A freshly-created Pending pod is benign (neutral), not degraded.
|
|
284
|
+
expect(getPodPhaseDisplay({ status: { phase: 'Pending' } }).level).toBe('neutral')
|
|
210
285
|
expect(getPodPhaseDisplay({ status: { phase: 'Failed' } }).level).toBe('unhealthy')
|
|
211
286
|
})
|
|
212
287
|
|
|
288
|
+
it('flags Unschedulable immediately, but gives plain young Pending a grace window', () => {
|
|
289
|
+
// The scheduler tried and failed — surface it now, no grace (matches backend).
|
|
290
|
+
const unsched = {
|
|
291
|
+
status: { phase: 'Pending', conditions: [{ type: 'PodScheduled', status: 'False', reason: 'Unschedulable' }] },
|
|
292
|
+
}
|
|
293
|
+
expect(getPodPhaseDisplay(unsched).text).toBe('Pending — Unschedulable')
|
|
294
|
+
expect(getPodPhaseDisplay(unsched).level).toBe('degraded')
|
|
295
|
+
|
|
296
|
+
// Plain Pending (not yet placed) is benign while young, degraded once stuck.
|
|
297
|
+
expect(getPodPhaseDisplay({ status: { phase: 'Pending' } }).level).toBe('neutral')
|
|
298
|
+
const stuckPending = {
|
|
299
|
+
metadata: { creationTimestamp: new Date(Date.now() - 10 * 60 * 1000).toISOString() },
|
|
300
|
+
status: { phase: 'Pending' },
|
|
301
|
+
}
|
|
302
|
+
expect(getPodPhaseDisplay(stuckPending).level).toBe('degraded')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('flags a failing init container immediately, even on a young Pending pod', () => {
|
|
306
|
+
const initFail = {
|
|
307
|
+
metadata: { creationTimestamp: new Date().toISOString() },
|
|
308
|
+
status: {
|
|
309
|
+
phase: 'Pending',
|
|
310
|
+
initContainerStatuses: [{ name: 'init', ready: false, state: { waiting: { reason: 'ImagePullBackOff' } } }],
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
const r = getPodPhaseDisplay(initFail)
|
|
314
|
+
expect(r.text).toBe('Pending — ImagePullBackOff')
|
|
315
|
+
expect(r.level).toBe('unhealthy')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('flags broadened fatal reasons like InvalidImageName', () => {
|
|
319
|
+
const bad = {
|
|
320
|
+
status: { phase: 'Pending', containerStatuses: [{ name: 'app', state: { waiting: { reason: 'InvalidImageName' } } }] },
|
|
321
|
+
}
|
|
322
|
+
expect(getPodPhaseDisplay(bad).level).toBe('unhealthy')
|
|
323
|
+
})
|
|
324
|
+
|
|
213
325
|
it('flags ErrImagePull as unhealthy', () => {
|
|
214
326
|
const r = getPodPhaseDisplay(podErrImagePull)
|
|
215
327
|
expect(r.text).toBe('Pending — ErrImagePull')
|
|
@@ -15,17 +15,24 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
|
|
|
15
15
|
const isSuspended = spec.suspend === true
|
|
16
16
|
const hasNeverRun = !status.lastScheduleTime
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
// "Recent failures" means the latest scheduled run didn't reach success.
|
|
19
|
+
// Only meaningful once nothing is running — while a job is active the most
|
|
20
|
+
// recent run is simply still in flight, not failing. With concurrencyPolicy
|
|
21
|
+
// Allow an older failed run can overlap a new active one; CronJob status only
|
|
22
|
+
// carries aggregate timestamps, so we accept missing that rare case here (the
|
|
23
|
+
// failed Job still surfaces in the job list / topology) rather than reviving
|
|
24
|
+
// the false positive that fired on every normal in-flight run.
|
|
25
|
+
const activeJobs = status.active?.length || 0
|
|
26
|
+
const lastSchedule = status.lastScheduleTime ? new Date(status.lastScheduleTime).getTime() : 0
|
|
27
|
+
const lastSuccess = status.lastSuccessfulTime ? new Date(status.lastSuccessfulTime).getTime() : 0
|
|
28
|
+
const recentFailures = activeJobs === 0 && lastSchedule > 0 && lastSchedule > lastSuccess
|
|
22
29
|
|
|
23
30
|
return (
|
|
24
31
|
<>
|
|
25
|
-
{/* Suspended
|
|
32
|
+
{/* Suspended is an intentional operator state, not a fault — keep it informational. */}
|
|
26
33
|
{isSuspended && (
|
|
27
34
|
<AlertBanner
|
|
28
|
-
variant="
|
|
35
|
+
variant="info"
|
|
29
36
|
icon={Pause}
|
|
30
37
|
title="CronJob Suspended"
|
|
31
38
|
message="No new jobs will be scheduled until this CronJob is resumed."
|
|
@@ -42,7 +49,7 @@ export function CronJobRenderer({ data, onNavigate }: CronJobRendererProps) {
|
|
|
42
49
|
)}
|
|
43
50
|
|
|
44
51
|
{/* Recent failures warning */}
|
|
45
|
-
{recentFailures && (
|
|
52
|
+
{recentFailures && !isSuspended && (
|
|
46
53
|
<AlertBanner
|
|
47
54
|
variant="error"
|
|
48
55
|
title="Recent Jobs Failing"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Radio, Server, Waypoints } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
|
+
import { Badge } from '../../ui/Badge'
|
|
4
5
|
import type { ResourceRef } from '../../../types'
|
|
5
6
|
|
|
6
7
|
interface EndpointSliceRendererProps {
|
|
@@ -69,7 +70,7 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
|
|
|
69
70
|
)}
|
|
70
71
|
</div>
|
|
71
72
|
<div className="flex items-center gap-2 shrink-0">
|
|
72
|
-
<
|
|
73
|
+
<Badge size="sm" protocol={port.protocol || 'TCP'}>{port.protocol || 'TCP'}</Badge>
|
|
73
74
|
<span className="font-mono text-theme-text-secondary">{port.port ?? '-'}</span>
|
|
74
75
|
</div>
|
|
75
76
|
</div>
|
|
@@ -92,9 +93,9 @@ export function EndpointSliceRenderer({ data, onNavigate }: EndpointSliceRendere
|
|
|
92
93
|
<div className="min-w-0 space-y-1">
|
|
93
94
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
94
95
|
{(endpoint.addresses || []).map((address: string) => (
|
|
95
|
-
<
|
|
96
|
+
<Badge key={address} size="sm" tone="structural" className="font-mono">
|
|
96
97
|
{address}
|
|
97
|
-
</
|
|
98
|
+
</Badge>
|
|
98
99
|
))}
|
|
99
100
|
</div>
|
|
100
101
|
{targetLabel && (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Globe, ArrowRight, Network, Filter } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceRefBadge } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
4
|
import type { ResourceRef } from '../../../types'
|
|
5
5
|
|
|
6
6
|
interface GRPCRouteRendererProps {
|
|
@@ -77,7 +77,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
|
|
|
77
77
|
hostnames.length > 0 ? (
|
|
78
78
|
<div className="flex flex-wrap gap-1">
|
|
79
79
|
{hostnames.map((h: string) => (
|
|
80
|
-
<
|
|
80
|
+
<Badge key={h} tone="structural">{h}</Badge>
|
|
81
81
|
))}
|
|
82
82
|
</div>
|
|
83
83
|
) : (
|
|
@@ -150,7 +150,7 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
|
|
|
150
150
|
{match.method && (
|
|
151
151
|
<>
|
|
152
152
|
{match.method.type && match.method.type !== 'Exact' && (
|
|
153
|
-
<
|
|
153
|
+
<Badge tone="accent1" size="sm">{match.method.type}</Badge>
|
|
154
154
|
)}
|
|
155
155
|
{match.method.service && (
|
|
156
156
|
<span>
|
|
@@ -200,9 +200,9 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
|
|
|
200
200
|
<div className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
|
|
201
201
|
<Filter className="w-3 h-3 text-theme-text-tertiary" />
|
|
202
202
|
{filters.map((filter: any, filterIdx: number) => (
|
|
203
|
-
<
|
|
203
|
+
<Badge key={filterIdx} tone="accent1" size="sm">
|
|
204
204
|
{filter.type}
|
|
205
|
-
</
|
|
205
|
+
</Badge>
|
|
206
206
|
))}
|
|
207
207
|
</div>
|
|
208
208
|
</div>
|
|
@@ -231,20 +231,14 @@ export function GRPCRouteRenderer({ data, onNavigate }: GRPCRouteRendererProps)
|
|
|
231
231
|
</div>
|
|
232
232
|
<div className="flex flex-wrap gap-2">
|
|
233
233
|
{accepted && (
|
|
234
|
-
<
|
|
235
|
-
'badge',
|
|
236
|
-
accepted.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
|
237
|
-
)}>
|
|
234
|
+
<Badge severity={accepted.status === 'True' ? 'success' : 'error'}>
|
|
238
235
|
{accepted.status === 'True' ? 'Accepted' : 'Not Accepted'}
|
|
239
|
-
</
|
|
236
|
+
</Badge>
|
|
240
237
|
)}
|
|
241
238
|
{resolved && (
|
|
242
|
-
<
|
|
243
|
-
'badge',
|
|
244
|
-
resolved.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
|
|
245
|
-
)}>
|
|
239
|
+
<Badge severity={resolved.status === 'True' ? 'success' : 'warning'}>
|
|
246
240
|
{resolved.status === 'True' ? 'Refs Resolved' : 'Unresolved Refs'}
|
|
247
|
-
</
|
|
241
|
+
</Badge>
|
|
248
242
|
)}
|
|
249
243
|
</div>
|
|
250
244
|
{accepted?.message && accepted.status === 'False' && (
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Cpu } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
|
-
import {
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
5
4
|
|
|
6
5
|
interface GatewayClassRendererProps {
|
|
7
6
|
data: any
|
|
@@ -35,16 +34,9 @@ export function GatewayClassRenderer({ data }: GatewayClassRendererProps) {
|
|
|
35
34
|
<Property
|
|
36
35
|
label="Accepted"
|
|
37
36
|
value={
|
|
38
|
-
<
|
|
39
|
-
'badge',
|
|
40
|
-
isAccepted
|
|
41
|
-
? 'bg-green-500/20 text-green-400'
|
|
42
|
-
: isNotAccepted
|
|
43
|
-
? 'bg-red-500/20 text-red-400'
|
|
44
|
-
: BADGE_INACTIVE
|
|
45
|
-
)}>
|
|
37
|
+
<Badge severity={isAccepted ? 'success' : isNotAccepted ? 'error' : 'neutral'}>
|
|
46
38
|
{isAccepted ? 'True' : isNotAccepted ? 'False' : 'Unknown'}
|
|
47
|
-
</
|
|
39
|
+
</Badge>
|
|
48
40
|
}
|
|
49
41
|
/>
|
|
50
42
|
</PropertyList>
|
|
@@ -2,14 +2,7 @@ import { Globe, Radio, Lock } from 'lucide-react'
|
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
5
|
-
|
|
6
|
-
const protocolColors: Record<string, string> = {
|
|
7
|
-
HTTP: 'bg-blue-500/20 text-blue-400',
|
|
8
|
-
HTTPS: 'bg-green-500/20 text-green-400',
|
|
9
|
-
TLS: 'bg-green-500/20 text-green-400',
|
|
10
|
-
TCP: 'bg-orange-500/20 text-orange-400',
|
|
11
|
-
UDP: 'bg-purple-500/20 text-purple-400',
|
|
12
|
-
}
|
|
5
|
+
import { Badge } from '../../ui/Badge'
|
|
13
6
|
|
|
14
7
|
interface GatewayRendererProps {
|
|
15
8
|
data: any
|
|
@@ -68,31 +61,17 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
|
|
|
68
61
|
<Property
|
|
69
62
|
label="Accepted"
|
|
70
63
|
value={
|
|
71
|
-
<
|
|
72
|
-
'badge',
|
|
73
|
-
isAccepted
|
|
74
|
-
? 'bg-green-500/20 text-green-400'
|
|
75
|
-
: isNotAccepted
|
|
76
|
-
? 'bg-red-500/20 text-red-400'
|
|
77
|
-
: BADGE_INACTIVE
|
|
78
|
-
)}>
|
|
64
|
+
<Badge severity={isAccepted ? 'success' : isNotAccepted ? 'error' : 'neutral'}>
|
|
79
65
|
{isAccepted ? 'True' : isNotAccepted ? 'False' : 'Unknown'}
|
|
80
|
-
</
|
|
66
|
+
</Badge>
|
|
81
67
|
}
|
|
82
68
|
/>
|
|
83
69
|
<Property
|
|
84
70
|
label="Programmed"
|
|
85
71
|
value={
|
|
86
|
-
<
|
|
87
|
-
'badge',
|
|
88
|
-
isProgrammed
|
|
89
|
-
? 'bg-green-500/20 text-green-400'
|
|
90
|
-
: isNotProgrammed
|
|
91
|
-
? 'bg-red-500/20 text-red-400'
|
|
92
|
-
: BADGE_INACTIVE
|
|
93
|
-
)}>
|
|
72
|
+
<Badge severity={isProgrammed ? 'success' : isNotProgrammed ? 'error' : 'neutral'}>
|
|
94
73
|
{isProgrammed ? 'True' : isNotProgrammed ? 'False' : 'Unknown'}
|
|
95
|
-
</
|
|
74
|
+
</Badge>
|
|
96
75
|
}
|
|
97
76
|
/>
|
|
98
77
|
</PropertyList>
|
|
@@ -133,23 +112,20 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
|
|
|
133
112
|
<div className="flex items-center gap-2">
|
|
134
113
|
{isHTTPS && <Lock className="w-3.5 h-3.5 text-green-400" />}
|
|
135
114
|
<span className="text-sm font-medium text-theme-text-primary">{listener.name}</span>
|
|
136
|
-
<
|
|
137
|
-
'px-1.5 py-0.5 rounded text-[10px] font-medium',
|
|
138
|
-
protocolColors[listener.protocol] || BADGE_INACTIVE
|
|
139
|
-
)}>
|
|
115
|
+
<Badge protocol={listener.protocol} size="sm">
|
|
140
116
|
{listener.protocol}:{listener.port}
|
|
141
|
-
</
|
|
117
|
+
</Badge>
|
|
142
118
|
</div>
|
|
143
119
|
<div className="flex items-center gap-1">
|
|
144
120
|
{isConflicted && (
|
|
145
|
-
<
|
|
121
|
+
<Badge severity="error" size="sm">
|
|
146
122
|
Conflicted
|
|
147
|
-
</
|
|
123
|
+
</Badge>
|
|
148
124
|
)}
|
|
149
125
|
{hasUnresolvedRefs && (
|
|
150
|
-
<
|
|
126
|
+
<Badge severity="warning" size="sm">
|
|
151
127
|
Unresolved Refs
|
|
152
|
-
</
|
|
128
|
+
</Badge>
|
|
153
129
|
)}
|
|
154
130
|
{listenerAccepted && (
|
|
155
131
|
<span className={clsx(
|