@skyhook-io/k8s-ui 1.8.5 → 1.8.7
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 +1 -1
- package/src/components/applications/ApplicationsView.tsx +2 -0
- package/src/components/audit/AuditAlerts.tsx +4 -0
- package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
- package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
- package/src/components/audit/AuditFindingsTable.tsx +4 -0
- package/src/components/audit/index.ts +1 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
- package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
- package/src/components/gitops/GitOpsTableView.tsx +3 -1
- package/src/components/issues/IssuesView.tsx +9 -36
- package/src/components/issues/ResourceIssuesSection.tsx +142 -0
- package/src/components/issues/diagnostic.ts +64 -0
- package/src/components/issues/index.ts +2 -1
- package/src/components/issues/severity.ts +10 -9
- package/src/components/issues/types.ts +5 -0
- package/src/components/resources/ResourcesView.tsx +38 -2
- package/src/components/resources/cron-to-human.test.ts +41 -0
- package/src/components/resources/get-pod-phase-display.test.ts +115 -3
- package/src/components/resources/get-pod-problems.test.ts +18 -0
- package/src/components/resources/health-golden.test.ts +66 -0
- 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 +12 -7
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
- package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
- package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
- package/src/components/resources/renderers/PodRenderer.tsx +28 -9
- package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
- 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 +6 -3
- 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-argo.test.ts +23 -0
- package/src/components/resources/resource-utils-argo.ts +5 -1
- package/src/components/resources/resource-utils-cnpg.ts +1 -1
- package/src/components/resources/resource-utils-keda.ts +17 -7
- package/src/components/resources/resource-utils.ts +206 -71
- package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
- package/src/components/timeline/shared.tsx +15 -4
- package/src/components/topology/K8sResourceNode.tsx +28 -1
- package/src/components/topology/TopologyControls.tsx +41 -9
- package/src/components/topology/layout.ts +11 -5
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/PaneLoader.tsx +30 -7
- package/src/components/ui/drawer-components.test.tsx +35 -0
- package/src/components/ui/drawer-components.tsx +13 -1
- package/src/components/workload/WorkloadView.tsx +35 -5
- package/src/types/core.ts +136 -2
- package/src/utils/applications.test.ts +55 -1
- package/src/utils/applications.ts +28 -7
- package/src/utils/asset-url.ts +13 -0
- package/src/utils/badge-colors.ts +7 -0
|
@@ -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 HTTPRouteRendererProps {
|
|
@@ -77,7 +77,7 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
|
|
|
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
|
) : (
|
|
@@ -148,9 +148,9 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
|
|
|
148
148
|
{matches.map((match: any, matchIdx: number) => (
|
|
149
149
|
<div key={matchIdx} className="text-xs text-theme-text-secondary flex flex-wrap items-center gap-1.5">
|
|
150
150
|
{match.method && (
|
|
151
|
-
<
|
|
151
|
+
<Badge tone="structural" size="sm">
|
|
152
152
|
{match.method}
|
|
153
|
-
</
|
|
153
|
+
</Badge>
|
|
154
154
|
)}
|
|
155
155
|
{match.path && (
|
|
156
156
|
<span>
|
|
@@ -201,9 +201,9 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
|
|
|
201
201
|
<Filter className="w-3 h-3 text-theme-text-tertiary" />
|
|
202
202
|
{filters.map((filter: any, filterIdx: number) => (
|
|
203
203
|
<span key={filterIdx} className="inline-flex items-center gap-1">
|
|
204
|
-
<
|
|
204
|
+
<Badge tone="accent1" size="sm">
|
|
205
205
|
{filter.type}
|
|
206
|
-
</
|
|
206
|
+
</Badge>
|
|
207
207
|
{filter.type === 'RequestHeaderModifier' && filter.requestHeaderModifier && (
|
|
208
208
|
<span className="text-theme-text-tertiary">
|
|
209
209
|
{summarizeHeaderModifier(filter.requestHeaderModifier)}
|
|
@@ -263,20 +263,14 @@ export function HTTPRouteRenderer({ data, onNavigate }: HTTPRouteRendererProps)
|
|
|
263
263
|
</div>
|
|
264
264
|
<div className="flex flex-wrap gap-2">
|
|
265
265
|
{accepted && (
|
|
266
|
-
<
|
|
267
|
-
'badge',
|
|
268
|
-
accepted.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
|
269
|
-
)}>
|
|
266
|
+
<Badge severity={accepted.status === 'True' ? 'success' : 'error'}>
|
|
270
267
|
{accepted.status === 'True' ? 'Accepted' : 'Not Accepted'}
|
|
271
|
-
</
|
|
268
|
+
</Badge>
|
|
272
269
|
)}
|
|
273
270
|
{resolved && (
|
|
274
|
-
<
|
|
275
|
-
'badge',
|
|
276
|
-
resolved.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
|
|
277
|
-
)}>
|
|
271
|
+
<Badge severity={resolved.status === 'True' ? 'success' : 'warning'}>
|
|
278
272
|
{resolved.status === 'True' ? 'Refs Resolved' : 'Unresolved Refs'}
|
|
279
|
-
</
|
|
273
|
+
</Badge>
|
|
280
274
|
)}
|
|
281
275
|
</div>
|
|
282
276
|
{accepted?.message && accepted.status === 'False' && (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Database, CheckCircle2, Shield } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
4
|
import { formatAge } from '../resource-utils'
|
|
5
5
|
import { formatBytes } from '../../../utils/format'
|
|
6
6
|
import { GitOpsStatusBadge, SyncCountdown } from '../../gitops'
|
|
@@ -58,12 +58,9 @@ export function HelmRepositoryRenderer({ data }: HelmRepositoryRendererProps) {
|
|
|
58
58
|
<Property
|
|
59
59
|
label="Type"
|
|
60
60
|
value={
|
|
61
|
-
<
|
|
62
|
-
'badge',
|
|
63
|
-
isOCI ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
|
|
64
|
-
)}>
|
|
61
|
+
<Badge tone={isOCI ? 'accent2' : 'accent1'}>
|
|
65
62
|
{isOCI ? 'OCI' : 'HTTP'}
|
|
66
|
-
</
|
|
63
|
+
</Badge>
|
|
67
64
|
}
|
|
68
65
|
/>
|
|
69
66
|
{spec.provider && <Property label="Provider" value={spec.provider} />}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Shield } from 'lucide-react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, KeyValueBadgeList } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
4
4
|
import {
|
|
5
5
|
getAuthorizationPolicyAction,
|
|
6
6
|
getAuthorizationPolicyRules,
|
|
7
7
|
getAuthorizationPolicySelector,
|
|
8
8
|
} from '../resource-utils-istio'
|
|
9
|
-
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
ALLOW: '
|
|
13
|
-
DENY: '
|
|
14
|
-
CUSTOM: '
|
|
15
|
-
AUDIT: '
|
|
10
|
+
const actionSeverity: Record<string, BadgeSeverity> = {
|
|
11
|
+
ALLOW: 'success',
|
|
12
|
+
DENY: 'error',
|
|
13
|
+
CUSTOM: 'info',
|
|
14
|
+
AUDIT: 'warning',
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
interface IstioAuthorizationPolicyRendererProps {
|
|
@@ -51,12 +50,9 @@ export function IstioAuthorizationPolicyRenderer({ data }: IstioAuthorizationPol
|
|
|
51
50
|
<Section title="Authorization Policy" icon={Shield} defaultExpanded>
|
|
52
51
|
<PropertyList>
|
|
53
52
|
<Property label="Action" value={
|
|
54
|
-
<
|
|
55
|
-
'badge',
|
|
56
|
-
actionColors[action] || BADGE_INACTIVE
|
|
57
|
-
)}>
|
|
53
|
+
<Badge severity={actionSeverity[action] ?? 'neutral'}>
|
|
58
54
|
{action}
|
|
59
|
-
</
|
|
55
|
+
</Badge>
|
|
60
56
|
} />
|
|
61
57
|
<Property label="Scope" value={hasSelector ? 'Workload-scoped' : 'Namespace-wide'} />
|
|
62
58
|
<Property label="Rules" value={String(rules.length)} />
|
|
@@ -88,24 +84,24 @@ export function IstioAuthorizationPolicyRenderer({ data }: IstioAuthorizationPol
|
|
|
88
84
|
{rule.from.map((f: any, fi: number) => (
|
|
89
85
|
<div key={fi} className="flex flex-wrap gap-1 mb-1">
|
|
90
86
|
{f.source?.principals?.map((p: string, pi: number) => (
|
|
91
|
-
<
|
|
87
|
+
<Badge key={`p-${pi}`} tone="structural" size="sm">
|
|
92
88
|
principal: {p}
|
|
93
|
-
</
|
|
89
|
+
</Badge>
|
|
94
90
|
))}
|
|
95
91
|
{f.source?.namespaces?.map((n: string, ni: number) => (
|
|
96
|
-
<
|
|
92
|
+
<Badge key={`n-${ni}`} tone="structural" size="sm">
|
|
97
93
|
namespace: {n}
|
|
98
|
-
</
|
|
94
|
+
</Badge>
|
|
99
95
|
))}
|
|
100
96
|
{f.source?.ipBlocks?.map((ip: string, ipi: number) => (
|
|
101
|
-
<
|
|
97
|
+
<Badge key={`ip-${ipi}`} tone="structural" size="sm">
|
|
102
98
|
IP: {ip}
|
|
103
|
-
</
|
|
99
|
+
</Badge>
|
|
104
100
|
))}
|
|
105
101
|
{f.source?.requestPrincipals?.map((rp: string, rpi: number) => (
|
|
106
|
-
<
|
|
102
|
+
<Badge key={`rp-${rpi}`} tone="structural" size="sm">
|
|
107
103
|
reqPrincipal: {rp}
|
|
108
|
-
</
|
|
104
|
+
</Badge>
|
|
109
105
|
))}
|
|
110
106
|
</div>
|
|
111
107
|
))}
|
|
@@ -119,24 +115,24 @@ export function IstioAuthorizationPolicyRenderer({ data }: IstioAuthorizationPol
|
|
|
119
115
|
{rule.to.map((t: any, ti: number) => (
|
|
120
116
|
<div key={ti} className="flex flex-wrap gap-1 mb-1">
|
|
121
117
|
{t.operation?.hosts?.map((h: string, hi: number) => (
|
|
122
|
-
<
|
|
118
|
+
<Badge key={`h-${hi}`} tone="structural" size="sm">
|
|
123
119
|
host: {h}
|
|
124
|
-
</
|
|
120
|
+
</Badge>
|
|
125
121
|
))}
|
|
126
122
|
{t.operation?.ports?.map((p: string, pi: number) => (
|
|
127
|
-
<
|
|
123
|
+
<Badge key={`p-${pi}`} tone="structural" size="sm">
|
|
128
124
|
port: {p}
|
|
129
|
-
</
|
|
125
|
+
</Badge>
|
|
130
126
|
))}
|
|
131
127
|
{t.operation?.methods?.map((m: string, mi: number) => (
|
|
132
|
-
<
|
|
128
|
+
<Badge key={`m-${mi}`} tone="structural" size="sm">
|
|
133
129
|
method: {m}
|
|
134
|
-
</
|
|
130
|
+
</Badge>
|
|
135
131
|
))}
|
|
136
132
|
{t.operation?.paths?.map((p: string, pi: number) => (
|
|
137
|
-
<
|
|
133
|
+
<Badge key={`path-${pi}`} tone="structural" size="sm">
|
|
138
134
|
path: {p}
|
|
139
|
-
</
|
|
135
|
+
</Badge>
|
|
140
136
|
))}
|
|
141
137
|
</div>
|
|
142
138
|
))}
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import { Globe, Lock } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, KeyValueBadgeList } from '../../ui/drawer-components'
|
|
4
|
+
import { Badge } from '../../ui/Badge'
|
|
4
5
|
import {
|
|
5
6
|
getIstioGatewayStatus,
|
|
6
7
|
getIstioGatewayServers,
|
|
7
8
|
getIstioGatewaySelector,
|
|
8
9
|
} from '../resource-utils-istio'
|
|
9
|
-
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
10
|
-
|
|
11
|
-
const protocolColors: Record<string, string> = {
|
|
12
|
-
HTTP: 'bg-blue-500/20 text-blue-400',
|
|
13
|
-
HTTPS: 'bg-green-500/20 text-green-400',
|
|
14
|
-
HTTP2: 'bg-blue-500/20 text-blue-400',
|
|
15
|
-
GRPC: 'bg-purple-500/20 text-purple-400',
|
|
16
|
-
TCP: 'bg-orange-500/20 text-orange-400',
|
|
17
|
-
TLS: 'bg-green-500/20 text-green-400',
|
|
18
|
-
MONGO: 'bg-emerald-500/20 text-emerald-400',
|
|
19
|
-
MYSQL: 'bg-cyan-500/20 text-cyan-400',
|
|
20
|
-
}
|
|
21
10
|
|
|
22
11
|
interface IstioGatewayRendererProps {
|
|
23
12
|
data: any
|
|
@@ -72,12 +61,9 @@ export function IstioGatewayRenderer({ data }: IstioGatewayRendererProps) {
|
|
|
72
61
|
<span className="text-sm font-medium text-theme-text-primary">
|
|
73
62
|
{server.port.name || `Port ${server.port.number}`}
|
|
74
63
|
</span>
|
|
75
|
-
<
|
|
76
|
-
'px-1.5 py-0.5 rounded text-[10px] font-medium',
|
|
77
|
-
protocolColors[protocol] || BADGE_INACTIVE
|
|
78
|
-
)}>
|
|
64
|
+
<Badge protocol={protocol} size="sm">
|
|
79
65
|
{protocol}:{server.port.number}
|
|
80
|
-
</
|
|
66
|
+
</Badge>
|
|
81
67
|
</div>
|
|
82
68
|
</div>
|
|
83
69
|
|
|
@@ -93,16 +79,23 @@ export function IstioGatewayRenderer({ data }: IstioGatewayRendererProps) {
|
|
|
93
79
|
<>
|
|
94
80
|
<div>
|
|
95
81
|
<span className="text-theme-text-tertiary">TLS Mode: </span>
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
82
|
+
<Badge
|
|
83
|
+
// Every TLS server row already shows a teal HTTPS/TLS protocol pill, so
|
|
84
|
+
// a teal mode badge would collide. PASSTHROUGH uses `note` (the mode where
|
|
85
|
+
// the gateway does not terminate TLS — the one worth flagging) to stay clear.
|
|
86
|
+
tone={
|
|
87
|
+
server.tls.mode === 'SIMPLE'
|
|
88
|
+
? 'accent1'
|
|
89
|
+
: server.tls.mode === 'MUTUAL' || server.tls.mode === 'ISTIO_MUTUAL'
|
|
90
|
+
? 'accent2'
|
|
91
|
+
: server.tls.mode === 'PASSTHROUGH' || server.tls.mode === 'AUTO_PASSTHROUGH'
|
|
92
|
+
? 'note'
|
|
93
|
+
: 'structural'
|
|
94
|
+
}
|
|
95
|
+
size="sm"
|
|
96
|
+
>
|
|
104
97
|
{server.tls.mode || 'SIMPLE'}
|
|
105
|
-
</
|
|
98
|
+
</Badge>
|
|
106
99
|
</div>
|
|
107
100
|
{server.tls.credentialName && (
|
|
108
101
|
<div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Network, Route } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { Badge } from '../../ui/Badge'
|
|
4
5
|
import {
|
|
5
6
|
getVirtualServiceStatus,
|
|
6
7
|
getVirtualServiceHostsList,
|
|
@@ -62,7 +63,7 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
|
|
|
62
63
|
{gateways.map((gw, i) => {
|
|
63
64
|
// Gateway reference can be "namespace/name" or just "name" or "mesh"
|
|
64
65
|
if (gw === 'mesh') {
|
|
65
|
-
return <
|
|
66
|
+
return <Badge key={i} tone="structural" size="sm">mesh</Badge>
|
|
66
67
|
}
|
|
67
68
|
const parts = gw.split('/')
|
|
68
69
|
const gwNs = parts.length === 2 ? parts[0] : ns
|
|
@@ -95,19 +96,19 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
|
|
|
95
96
|
{route.name || `Route ${i + 1}`}
|
|
96
97
|
</span>
|
|
97
98
|
{route.timeout && (
|
|
98
|
-
<
|
|
99
|
+
<Badge tone="structural" size="sm">
|
|
99
100
|
timeout: {route.timeout}
|
|
100
|
-
</
|
|
101
|
+
</Badge>
|
|
101
102
|
)}
|
|
102
103
|
{route.fault && (
|
|
103
|
-
<
|
|
104
|
+
<Badge severity="warning" size="sm">
|
|
104
105
|
fault injection
|
|
105
|
-
</
|
|
106
|
+
</Badge>
|
|
106
107
|
)}
|
|
107
108
|
{route.mirror && (
|
|
108
|
-
<
|
|
109
|
+
<Badge tone="note" size="sm">
|
|
109
110
|
mirroring
|
|
110
|
-
</
|
|
111
|
+
</Badge>
|
|
111
112
|
)}
|
|
112
113
|
</div>
|
|
113
114
|
|
|
@@ -119,19 +120,19 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
|
|
|
119
120
|
{route.match.map((m: any, mi: number) => (
|
|
120
121
|
<div key={mi} className="flex flex-wrap gap-1 text-xs">
|
|
121
122
|
{m.uri && (
|
|
122
|
-
<
|
|
123
|
+
<Badge tone="structural" size="sm">
|
|
123
124
|
URI {Object.keys(m.uri)[0]}: {Object.values(m.uri)[0] as string}
|
|
124
|
-
</
|
|
125
|
+
</Badge>
|
|
125
126
|
)}
|
|
126
127
|
{m.method && (
|
|
127
|
-
<
|
|
128
|
+
<Badge tone="structural" size="sm">
|
|
128
129
|
Method: {m.method.exact || Object.values(m.method)[0]}
|
|
129
|
-
</
|
|
130
|
+
</Badge>
|
|
130
131
|
)}
|
|
131
132
|
{m.headers && Object.entries(m.headers).map(([hk, hv]: [string, any]) => (
|
|
132
|
-
<
|
|
133
|
+
<Badge key={hk} tone="structural" size="sm">
|
|
133
134
|
Header {hk}: {typeof hv === 'object' ? Object.values(hv)[0] as string : String(hv)}
|
|
134
|
-
</
|
|
135
|
+
</Badge>
|
|
135
136
|
))}
|
|
136
137
|
</div>
|
|
137
138
|
))}
|
|
@@ -174,9 +175,9 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
|
|
|
174
175
|
<span className="text-theme-text-tertiary">:{destPort}</span>
|
|
175
176
|
)}
|
|
176
177
|
{destSubset && (
|
|
177
|
-
<
|
|
178
|
+
<Badge tone="structural" size="sm">
|
|
178
179
|
subset: {destSubset}
|
|
179
|
-
</
|
|
180
|
+
</Badge>
|
|
180
181
|
)}
|
|
181
182
|
</div>
|
|
182
183
|
)
|
|
@@ -201,16 +202,16 @@ export function IstioVirtualServiceRenderer({ data, onNavigate }: IstioVirtualSe
|
|
|
201
202
|
{route.fault && (
|
|
202
203
|
<div className="flex flex-wrap gap-2 text-xs">
|
|
203
204
|
{route.fault.delay && (
|
|
204
|
-
<
|
|
205
|
+
<Badge severity="warning" size="sm">
|
|
205
206
|
Delay: {route.fault.delay.fixedDelay}
|
|
206
207
|
{route.fault.delay.percentage?.value && ` (${route.fault.delay.percentage.value}%)`}
|
|
207
|
-
</
|
|
208
|
+
</Badge>
|
|
208
209
|
)}
|
|
209
210
|
{route.fault.abort && (
|
|
210
|
-
<
|
|
211
|
+
<Badge severity="error" size="sm">
|
|
211
212
|
Abort: HTTP {route.fault.abort.httpStatus}
|
|
212
213
|
{route.fault.abort.percentage?.value && ` (${route.fault.abort.percentage.value}%)`}
|
|
213
|
-
</
|
|
214
|
+
</Badge>
|
|
214
215
|
)}
|
|
215
216
|
</div>
|
|
216
217
|
)}
|
|
@@ -25,8 +25,12 @@ function getJobProblems(data: any): string[] {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Check for pod failures without terminal condition yet
|
|
29
|
-
|
|
28
|
+
// Check for pod failures without terminal condition yet. A Job that already
|
|
29
|
+
// completed successfully (Complete condition) keeps its earlier failed pod
|
|
30
|
+
// attempts in status.failed — those are retries, not a problem — so don't flag
|
|
31
|
+
// them, or the drawer would read red while the table badge is calm neutral.
|
|
32
|
+
const completeCondition = conditions.find((c: any) => c.type === 'Complete' && c.status === 'True')
|
|
33
|
+
if (!failedCondition && !completeCondition && status.failed > 0) {
|
|
30
34
|
const remaining = (spec.backoffLimit ?? 6) - status.failed
|
|
31
35
|
if (remaining > 0) {
|
|
32
36
|
problems.push(`${status.failed} pod(s) failed — ${remaining} retries remaining`)
|
|
@@ -35,11 +39,6 @@ function getJobProblems(data: any): string[] {
|
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
// Check for suspended
|
|
39
|
-
if (spec.suspend) {
|
|
40
|
-
problems.push('Job is suspended — pods will not be created')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
42
|
return problems
|
|
44
43
|
}
|
|
45
44
|
|
|
@@ -62,6 +61,7 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
62
61
|
|
|
63
62
|
// Check if job completed successfully
|
|
64
63
|
const isComplete = conditions.some((c: any) => c.type === 'Complete' && c.status === 'True')
|
|
64
|
+
const isSuspended = spec.suspend === true
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
67
|
<>
|
|
@@ -70,6 +70,11 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
70
70
|
<AlertBanner variant="error" title="Job Issues" items={problems} />
|
|
71
71
|
)}
|
|
72
72
|
|
|
73
|
+
{/* Suspended is an intentional state, not a fault — keep it informational. */}
|
|
74
|
+
{isSuspended && !hasProblems && (
|
|
75
|
+
<AlertBanner variant="info" title="Job Suspended" message="Pods will not be created until this Job is resumed." />
|
|
76
|
+
)}
|
|
77
|
+
|
|
73
78
|
{/* Success banner */}
|
|
74
79
|
{isComplete && !hasProblems && (
|
|
75
80
|
<AlertBanner variant="success" title="Job Completed Successfully" />
|
|
@@ -62,9 +62,9 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
|
|
|
62
62
|
)}
|
|
63
63
|
{isPaused && (
|
|
64
64
|
<AlertBanner
|
|
65
|
-
variant="
|
|
65
|
+
variant="info"
|
|
66
66
|
title="Scaling Paused"
|
|
67
|
-
message="Autoscaling is paused via annotation."
|
|
67
|
+
message="Autoscaling is paused via annotation. This is intentional — resume by removing the paused annotation."
|
|
68
68
|
/>
|
|
69
69
|
)}
|
|
70
70
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Network, ShieldCheck, Server, Globe } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, KnativeNotReadyBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { Badge } from '../../ui/Badge'
|
|
4
5
|
import { kindToPlural } from '../../../utils/navigation'
|
|
5
6
|
import { getKnativeConditionStatus } from '../resource-utils-knative'
|
|
6
7
|
|
|
@@ -43,14 +44,14 @@ export function KnativeIngressRenderer({ data }: RendererProps) {
|
|
|
43
44
|
<div key={i} className="card-inner-lg">
|
|
44
45
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
45
46
|
{(rule.hosts || []).map((host: string, hi: number) => (
|
|
46
|
-
<
|
|
47
|
+
<Badge key={hi} size="sm" tone="structural">
|
|
47
48
|
{host}
|
|
48
|
-
</
|
|
49
|
+
</Badge>
|
|
49
50
|
))}
|
|
50
51
|
{rule.visibility && (
|
|
51
|
-
<
|
|
52
|
+
<Badge size="sm" tone="structural">
|
|
52
53
|
{rule.visibility}
|
|
53
|
-
</
|
|
54
|
+
</Badge>
|
|
54
55
|
)}
|
|
55
56
|
</div>
|
|
56
57
|
{rule.http?.paths && rule.http.paths.length > 0 && (
|
|
@@ -117,9 +118,9 @@ export function KnativeCertificateRenderer({ data }: RendererProps) {
|
|
|
117
118
|
<Section title={`DNS Names (${dnsNames.length})`} defaultExpanded>
|
|
118
119
|
<div className="flex flex-wrap gap-1">
|
|
119
120
|
{dnsNames.map((name: string, i: number) => (
|
|
120
|
-
<
|
|
121
|
+
<Badge key={i} size="sm" tone="structural">
|
|
121
122
|
{name}
|
|
122
|
-
</
|
|
123
|
+
</Badge>
|
|
123
124
|
))}
|
|
124
125
|
</div>
|
|
125
126
|
</Section>
|
|
@@ -157,12 +158,9 @@ export function ServerlessServiceRenderer({ data, onNavigate }: RendererProps) {
|
|
|
157
158
|
</span>
|
|
158
159
|
} />
|
|
159
160
|
<Property label="Mode" value={mode ? (
|
|
160
|
-
<
|
|
161
|
-
'badge',
|
|
162
|
-
mode === 'Proxy' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-green-500/20 text-green-400'
|
|
163
|
-
)}>
|
|
161
|
+
<Badge tone={mode === 'Proxy' ? 'accent1' : 'accent2'}>
|
|
164
162
|
{mode}
|
|
165
|
-
</
|
|
163
|
+
</Badge>
|
|
166
164
|
) : undefined} />
|
|
167
165
|
<Property label="Activators" value={numActivators != null ? String(numActivators) : undefined} />
|
|
168
166
|
<Property label="Protocol" value={protocolType} />
|
|
@@ -26,16 +26,12 @@ function formatStorage(value: string | undefined): string {
|
|
|
26
26
|
return formatMemory(value)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Extract problems from node status
|
|
29
|
+
// Extract genuine problems from node status. Cordoned (unschedulable) is
|
|
30
|
+
// deliberately NOT included here — it's an intentional operator action
|
|
31
|
+
// (cordon/drain), surfaced separately as a calm advisory, not a red error.
|
|
30
32
|
function getNodeProblems(data: any): string[] {
|
|
31
33
|
const problems: string[] = []
|
|
32
34
|
const conditions = data.status?.conditions || []
|
|
33
|
-
const spec = data.spec || {}
|
|
34
|
-
|
|
35
|
-
// Check if unschedulable
|
|
36
|
-
if (spec.unschedulable) {
|
|
37
|
-
problems.push('Node is cordoned (unschedulable)')
|
|
38
|
-
}
|
|
39
35
|
|
|
40
36
|
for (const cond of conditions) {
|
|
41
37
|
// NotReady is a problem when status is not True
|
|
@@ -77,6 +73,7 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
77
73
|
// Check for problems
|
|
78
74
|
const problems = getNodeProblems(data)
|
|
79
75
|
const hasProblems = problems.length > 0
|
|
76
|
+
const isCordoned = !!spec.unschedulable
|
|
80
77
|
|
|
81
78
|
// Extract platform info from labels
|
|
82
79
|
const instanceType = labels['node.kubernetes.io/instance-type']
|
|
@@ -88,11 +85,23 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
88
85
|
|
|
89
86
|
return (
|
|
90
87
|
<>
|
|
91
|
-
{/* Problems alert - shown at top when there are issues */}
|
|
88
|
+
{/* Problems alert - shown at top when there are genuine issues */}
|
|
92
89
|
{hasProblems && (
|
|
93
90
|
<AlertBanner variant="error" title="Issues Detected" items={problems} />
|
|
94
91
|
)}
|
|
95
92
|
|
|
93
|
+
{/* Cordoned is intentional but consequential — it removes scheduling
|
|
94
|
+
capacity and a forgotten cordon strands a node. So it's a warning (amber),
|
|
95
|
+
matching the node table badge + the Cordoned audit check — NOT the calm
|
|
96
|
+
sky of a no-op intentional state (suspended/idle), and not a red error. */}
|
|
97
|
+
{isCordoned && (
|
|
98
|
+
<AlertBanner
|
|
99
|
+
variant="warning"
|
|
100
|
+
title="Cordoned (unschedulable)"
|
|
101
|
+
message="New pods won't be scheduled here. Uncordon to resume scheduling."
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
|
|
96
105
|
{/* Node Info */}
|
|
97
106
|
<Section title="Node Info" icon={Server}>
|
|
98
107
|
<PropertyList>
|
|
@@ -28,10 +28,10 @@ export function PVCRenderer({ data, onNavigate, extraSections }: PVCRendererProp
|
|
|
28
28
|
const annotations = data.metadata?.annotations || {}
|
|
29
29
|
const phase = status.phase
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// Lost is a genuine failure (bound volume disappeared). Pending is a normal
|
|
32
|
+
// lifecycle state (provisioning / WaitForFirstConsumer), surfaced calmly below.
|
|
32
33
|
const isLost = phase === 'Lost'
|
|
33
34
|
const isPending = phase === 'Pending'
|
|
34
|
-
const hasProblems = isLost || isPending
|
|
35
35
|
|
|
36
36
|
// Provisioner info from annotations
|
|
37
37
|
const provisioner = annotations['volume.kubernetes.io/storage-provisioner']
|
|
@@ -42,7 +42,7 @@ export function PVCRenderer({ data, onNavigate, extraSections }: PVCRendererProp
|
|
|
42
42
|
return (
|
|
43
43
|
<>
|
|
44
44
|
{/* Problem alerts */}
|
|
45
|
-
{
|
|
45
|
+
{isLost && (
|
|
46
46
|
<AlertBanner
|
|
47
47
|
variant="error"
|
|
48
48
|
title="Issues Detected"
|
|
@@ -50,11 +50,11 @@ export function PVCRenderer({ data, onNavigate, extraSections }: PVCRendererProp
|
|
|
50
50
|
/>
|
|
51
51
|
)}
|
|
52
52
|
|
|
53
|
-
{
|
|
53
|
+
{isPending && (
|
|
54
54
|
<AlertBanner
|
|
55
|
-
variant="
|
|
56
|
-
title="
|
|
57
|
-
message="
|
|
55
|
+
variant="info"
|
|
56
|
+
title="Pending — awaiting binding"
|
|
57
|
+
message="Not yet bound to a volume. This is normal while provisioning, and expected indefinitely for a WaitForFirstConsumer StorageClass until a Pod that mounts this claim is scheduled."
|
|
58
58
|
/>
|
|
59
59
|
)}
|
|
60
60
|
|