@skyhook-io/k8s-ui 1.8.5 → 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 +1 -1
- 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/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/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/ResourceRendererDispatch.tsx +10 -0
- package/src/components/topology/TopologyControls.tsx +41 -9
- package/src/components/ui/Badge.tsx +66 -3
- package/src/components/ui/ClusterName.tsx +12 -4
- package/src/components/ui/PaneLoader.tsx +6 -1
- package/src/types/core.ts +37 -0
- package/src/utils/asset-url.ts +13 -0
|
@@ -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
|
)}
|
|
@@ -35,11 +35,6 @@ function getJobProblems(data: any): string[] {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Check for suspended
|
|
39
|
-
if (spec.suspend) {
|
|
40
|
-
problems.push('Job is suspended — pods will not be created')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
38
|
return problems
|
|
44
39
|
}
|
|
45
40
|
|
|
@@ -62,6 +57,7 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
62
57
|
|
|
63
58
|
// Check if job completed successfully
|
|
64
59
|
const isComplete = conditions.some((c: any) => c.type === 'Complete' && c.status === 'True')
|
|
60
|
+
const isSuspended = spec.suspend === true
|
|
65
61
|
|
|
66
62
|
return (
|
|
67
63
|
<>
|
|
@@ -70,6 +66,11 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
70
66
|
<AlertBanner variant="error" title="Job Issues" items={problems} />
|
|
71
67
|
)}
|
|
72
68
|
|
|
69
|
+
{/* Suspended is an intentional state, not a fault — keep it informational. */}
|
|
70
|
+
{isSuspended && !hasProblems && (
|
|
71
|
+
<AlertBanner variant="info" title="Job Suspended" message="Pods will not be created until this Job is resumed." />
|
|
72
|
+
)}
|
|
73
|
+
|
|
73
74
|
{/* Success banner */}
|
|
74
75
|
{isComplete && !hasProblems && (
|
|
75
76
|
<AlertBanner variant="success" title="Job Completed Successfully" />
|
|
@@ -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} />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Globe, ArrowRight, Network } 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 SimpleRouteRendererProps {
|
|
@@ -81,7 +81,7 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
|
|
|
81
81
|
hostnames.length > 0 ? (
|
|
82
82
|
<div className="flex flex-wrap gap-1">
|
|
83
83
|
{hostnames.map((h: string) => (
|
|
84
|
-
<
|
|
84
|
+
<Badge key={h} tone="structural">{h}</Badge>
|
|
85
85
|
))}
|
|
86
86
|
</div>
|
|
87
87
|
) : (
|
|
@@ -163,20 +163,14 @@ export function SimpleRouteRenderer({ data, kind, onNavigate }: SimpleRouteRende
|
|
|
163
163
|
</div>
|
|
164
164
|
<div className="flex flex-wrap gap-2">
|
|
165
165
|
{accepted && (
|
|
166
|
-
<
|
|
167
|
-
'badge',
|
|
168
|
-
accepted.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
|
169
|
-
)}>
|
|
166
|
+
<Badge severity={accepted.status === 'True' ? 'success' : 'error'}>
|
|
170
167
|
{accepted.status === 'True' ? 'Accepted' : 'Not Accepted'}
|
|
171
|
-
</
|
|
168
|
+
</Badge>
|
|
172
169
|
)}
|
|
173
170
|
{resolved && (
|
|
174
|
-
<
|
|
175
|
-
'badge',
|
|
176
|
-
resolved.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
|
|
177
|
-
)}>
|
|
171
|
+
<Badge severity={resolved.status === 'True' ? 'success' : 'warning'}>
|
|
178
172
|
{resolved.status === 'True' ? 'Refs Resolved' : 'Unresolved Refs'}
|
|
179
|
-
</
|
|
173
|
+
</Badge>
|
|
180
174
|
)}
|
|
181
175
|
</div>
|
|
182
176
|
{accepted?.message && accepted.status === 'False' && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Globe, Route, Layers, Lock } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
3
4
|
|
|
4
5
|
interface TraefikIngressRouteRendererProps {
|
|
5
6
|
data: any
|
|
@@ -84,14 +85,14 @@ export function TraefikIngressRouteRenderer({ data, onNavigate }: TraefikIngress
|
|
|
84
85
|
{route.match || 'No match rule'}
|
|
85
86
|
</span>
|
|
86
87
|
{route.priority && (
|
|
87
|
-
<
|
|
88
|
+
<Badge tone="structural" size="sm" className="shrink-0">
|
|
88
89
|
priority: {route.priority}
|
|
89
|
-
</
|
|
90
|
+
</Badge>
|
|
90
91
|
)}
|
|
91
92
|
{route.kind && route.kind !== 'Rule' && (
|
|
92
|
-
<
|
|
93
|
+
<Badge tone="structural" size="sm" className="shrink-0">
|
|
93
94
|
{route.kind}
|
|
94
|
-
</
|
|
95
|
+
</Badge>
|
|
95
96
|
)}
|
|
96
97
|
</div>
|
|
97
98
|
|
|
@@ -110,9 +111,9 @@ export function TraefikIngressRouteRenderer({ data, onNavigate }: TraefikIngress
|
|
|
110
111
|
return (
|
|
111
112
|
<div key={si} className="flex items-center gap-2 text-xs">
|
|
112
113
|
{isTraefikSvc && (
|
|
113
|
-
<
|
|
114
|
+
<Badge kind="TraefikService" size="sm">
|
|
114
115
|
TraefikService
|
|
115
|
-
</
|
|
116
|
+
</Badge>
|
|
116
117
|
)}
|
|
117
118
|
<ResourceLink
|
|
118
119
|
name={svc.name}
|
|
@@ -152,9 +153,9 @@ export function TraefikIngressRouteRenderer({ data, onNavigate }: TraefikIngress
|
|
|
152
153
|
kind="middlewares"
|
|
153
154
|
namespace={mwNs}
|
|
154
155
|
label={
|
|
155
|
-
<
|
|
156
|
+
<Badge tone="accent1" size="sm" className="inline-block">
|
|
156
157
|
{mw.namespace && mw.namespace !== ns ? `${mw.namespace}/` : ''}{mw.name}
|
|
157
|
-
</
|
|
158
|
+
</Badge>
|
|
158
159
|
}
|
|
159
160
|
onNavigate={onNavigate}
|
|
160
161
|
/>
|
|
@@ -242,9 +243,9 @@ export function TraefikIngressRouteRenderer({ data, onNavigate }: TraefikIngress
|
|
|
242
243
|
onNavigate={onNavigate}
|
|
243
244
|
/>
|
|
244
245
|
{isCrossNs && (
|
|
245
|
-
<
|
|
246
|
+
<Badge tone="note" size="sm">
|
|
246
247
|
{mw.namespace}
|
|
247
|
-
</
|
|
248
|
+
</Badge>
|
|
248
249
|
)}
|
|
249
250
|
</div>
|
|
250
251
|
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { SlidersHorizontal, Layers, ShieldCheck } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
|
+
import { getMiddlewareType } from '../resource-utils-traefik'
|
|
5
|
+
|
|
6
|
+
interface TraefikMiddlewareRendererProps {
|
|
7
|
+
data: any
|
|
8
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Keys whose value is inline credential MATERIAL — redacted in the generic
|
|
12
|
+
// config view. Mirrors the backend's value-key set (pkg/ai/context/redact.go)
|
|
13
|
+
// plus `users` (basicAuth htpasswd). Deliberately excludes reference keys like
|
|
14
|
+
// `secret`/`secretName` (the NAME of a Secret, which is safe and useful to show).
|
|
15
|
+
const SECRET_VALUE_KEYS = new Set([
|
|
16
|
+
'password', 'passwd', 'passphrase', 'token', 'clientsecret', 'privatekey',
|
|
17
|
+
'apikey', 'apitoken', 'accesstoken', 'sessiontoken', 'secretaccesskey',
|
|
18
|
+
'secretkey', 'authtoken', 'bearertoken', 'users',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
function isSecretKey(key: string): boolean {
|
|
22
|
+
return SECRET_VALUE_KEYS.has(key.toLowerCase().replace(/[-_]/g, ''))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatScalar(v: any): string {
|
|
26
|
+
if (Array.isArray(v)) return v.length === 0 ? '[]' : v.join(', ')
|
|
27
|
+
if (typeof v === 'object' && v !== null) return JSON.stringify(v)
|
|
28
|
+
return String(v)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Deep-redact: replace any credential-keyed value with "(hidden)" at EVERY
|
|
32
|
+
// depth, so a secret under e.g. plugin.config.clientSecret can't survive a
|
|
33
|
+
// later JSON.stringify of a nested object. Non-secret values are kept — the
|
|
34
|
+
// drawer is not a secrecy boundary (the YAML tab shows the full spec the user
|
|
35
|
+
// already has RBAC to read); hiding only credential KEYS keeps the generic
|
|
36
|
+
// config legible for unknown/plugin middleware types.
|
|
37
|
+
function redactConfig(v: any): any {
|
|
38
|
+
if (Array.isArray(v)) return v.map(redactConfig)
|
|
39
|
+
if (v && typeof v === 'object') {
|
|
40
|
+
const out: Record<string, any> = {}
|
|
41
|
+
for (const [k, val] of Object.entries(v)) {
|
|
42
|
+
out[k] = isSecretKey(k) ? '(hidden)' : redactConfig(val)
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
return v
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Renders a middleware config object as a property list, collapsing nested
|
|
50
|
+
// objects/arrays to a compact one-line summary. Credential keys are redacted
|
|
51
|
+
// at every depth (redactConfig) before rendering.
|
|
52
|
+
function ConfigProperties({ config }: { config: Record<string, any> }) {
|
|
53
|
+
const safe = redactConfig(config) as Record<string, any>
|
|
54
|
+
const entries = Object.entries(safe)
|
|
55
|
+
if (entries.length === 0) {
|
|
56
|
+
return <Property label="Config" value="(empty)" />
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{entries.map(([key, value]) => {
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return <Property key={key} label={key} value={value.length === 0 ? '[]' : value.map(formatScalar).join(', ')} />
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'object' && value !== null) {
|
|
65
|
+
const inner = Object.entries(value)
|
|
66
|
+
.map(([k, v]) => `${k}: ${formatScalar(v)}`)
|
|
67
|
+
.join(' · ')
|
|
68
|
+
return <Property key={key} label={key} value={inner || '{}'} />
|
|
69
|
+
}
|
|
70
|
+
return <Property key={key} label={key} value={formatScalar(value)} />
|
|
71
|
+
})}
|
|
72
|
+
</>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function TraefikMiddlewareRenderer({ data, onNavigate }: TraefikMiddlewareRendererProps) {
|
|
77
|
+
const spec = data.spec || {}
|
|
78
|
+
const type = getMiddlewareType(data)
|
|
79
|
+
const ns = data.metadata?.namespace || ''
|
|
80
|
+
const kindLabel = data.kind || 'Middleware'
|
|
81
|
+
// A middleware spec carries exactly one top-level key (its type). For known
|
|
82
|
+
// types that's `type`; for unrecognized ones fall back to whatever key the
|
|
83
|
+
// spec actually has, so the generic config section isn't rendered empty.
|
|
84
|
+
const typeKey = type === 'unknown' ? Object.keys(spec)[0] : type
|
|
85
|
+
const config = (typeKey && spec[typeKey]) || {}
|
|
86
|
+
|
|
87
|
+
const isChain = type === 'chain'
|
|
88
|
+
// A MiddlewareTCP chain references MiddlewareTCP members, not Middleware.
|
|
89
|
+
const chainMemberKind = data.kind === 'MiddlewareTCP' ? 'middlewaretcps' : 'middlewares'
|
|
90
|
+
const isAuth = type === 'basicAuth' || type === 'digestAuth'
|
|
91
|
+
const isForwardAuth = type === 'forwardAuth'
|
|
92
|
+
const isErrors = type === 'errors'
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
{type === 'unknown' && (
|
|
97
|
+
<AlertBanner
|
|
98
|
+
variant="info"
|
|
99
|
+
title="Unrecognized middleware type"
|
|
100
|
+
message="This middleware uses a type Radar doesn't render specially (e.g. a plugin or a newer/commercial middleware). The raw configuration is shown below."
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<Section title={kindLabel} icon={SlidersHorizontal} defaultExpanded>
|
|
105
|
+
<PropertyList>
|
|
106
|
+
<Property label="Type" value={type === 'unknown' ? Object.keys(spec)[0] || 'unknown' : type} />
|
|
107
|
+
</PropertyList>
|
|
108
|
+
</Section>
|
|
109
|
+
|
|
110
|
+
{isChain ? (
|
|
111
|
+
<Section title="Chain" icon={Layers} defaultExpanded>
|
|
112
|
+
<div className="space-y-1">
|
|
113
|
+
{(config.middlewares || []).map((m: any, i: number) => (
|
|
114
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
115
|
+
<span className="text-theme-text-tertiary w-4 text-right">{i + 1}.</span>
|
|
116
|
+
<ResourceLink
|
|
117
|
+
name={m.name}
|
|
118
|
+
kind={chainMemberKind}
|
|
119
|
+
namespace={m.namespace || ns}
|
|
120
|
+
onNavigate={onNavigate}
|
|
121
|
+
/>
|
|
122
|
+
{m.namespace && m.namespace !== ns && (
|
|
123
|
+
<Badge tone="note" size="sm">{m.namespace}</Badge>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
{(config.middlewares || []).length === 0 && (
|
|
128
|
+
<span className="text-xs text-theme-text-tertiary">No middlewares in chain</span>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</Section>
|
|
132
|
+
) : isAuth ? (
|
|
133
|
+
<Section title="Authentication" icon={ShieldCheck} defaultExpanded>
|
|
134
|
+
<PropertyList>
|
|
135
|
+
{config.secret && (
|
|
136
|
+
<Property label="Secret" value={
|
|
137
|
+
<ResourceLink name={config.secret} kind="secrets" namespace={ns} onNavigate={onNavigate} />
|
|
138
|
+
} />
|
|
139
|
+
)}
|
|
140
|
+
{Array.isArray(config.users) && (
|
|
141
|
+
<Property label="Inline users" value={<span className="text-theme-text-tertiary italic">{config.users.length} — hidden</span>} />
|
|
142
|
+
)}
|
|
143
|
+
{config.usersFile && <Property label="Users file" value={config.usersFile} />}
|
|
144
|
+
{config.realm && <Property label="Realm" value={config.realm} />}
|
|
145
|
+
{config.removeHeader !== undefined && <Property label="Remove header" value={String(config.removeHeader)} />}
|
|
146
|
+
</PropertyList>
|
|
147
|
+
</Section>
|
|
148
|
+
) : isForwardAuth ? (
|
|
149
|
+
<Section title="Forward Auth" icon={ShieldCheck} defaultExpanded>
|
|
150
|
+
<PropertyList>
|
|
151
|
+
{config.address && <Property label="Address" value={config.address} />}
|
|
152
|
+
{config.trustForwardHeader !== undefined && <Property label="Trust forward header" value={String(config.trustForwardHeader)} />}
|
|
153
|
+
{Array.isArray(config.authResponseHeaders) && config.authResponseHeaders.length > 0 && (
|
|
154
|
+
<Property label="Auth response headers" value={config.authResponseHeaders.join(', ')} />
|
|
155
|
+
)}
|
|
156
|
+
{config.authResponseHeadersRegex && <Property label="Headers regex" value={config.authResponseHeadersRegex} />}
|
|
157
|
+
{config.tls?.caSecret && (
|
|
158
|
+
<Property label="CA secret" value={
|
|
159
|
+
<ResourceLink name={config.tls.caSecret} kind="secrets" namespace={ns} onNavigate={onNavigate} />
|
|
160
|
+
} />
|
|
161
|
+
)}
|
|
162
|
+
</PropertyList>
|
|
163
|
+
</Section>
|
|
164
|
+
) : isErrors ? (
|
|
165
|
+
<Section title="Errors" defaultExpanded>
|
|
166
|
+
<PropertyList>
|
|
167
|
+
{config.status && <Property label="Status" value={Array.isArray(config.status) ? config.status.join(', ') : String(config.status)} />}
|
|
168
|
+
{config.service?.name && (
|
|
169
|
+
<Property label="Service" value={
|
|
170
|
+
<ResourceLink name={config.service.name} kind="services" namespace={config.service.namespace || ns} onNavigate={onNavigate} />
|
|
171
|
+
} />
|
|
172
|
+
)}
|
|
173
|
+
{config.query && <Property label="Query" value={config.query} />}
|
|
174
|
+
</PropertyList>
|
|
175
|
+
</Section>
|
|
176
|
+
) : (
|
|
177
|
+
<Section title="Configuration" defaultExpanded>
|
|
178
|
+
<PropertyList>
|
|
179
|
+
<ConfigProperties config={config} />
|
|
180
|
+
</PropertyList>
|
|
181
|
+
</Section>
|
|
182
|
+
)}
|
|
183
|
+
</>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ArrowRightLeft } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
|
|
4
|
+
interface TraefikServersTransportRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Covers ServersTransport (HTTP) and ServersTransportTCP — overlapping shapes;
|
|
10
|
+
// fields absent on one kind simply don't render.
|
|
11
|
+
export function TraefikServersTransportRenderer({ data, onNavigate }: TraefikServersTransportRendererProps) {
|
|
12
|
+
const spec = data.spec || {}
|
|
13
|
+
const ns = data.metadata?.namespace || ''
|
|
14
|
+
const kindLabel = data.kind || 'ServersTransport'
|
|
15
|
+
const insecure = !!spec.insecureSkipVerify
|
|
16
|
+
const rootCAs: any[] = spec.rootCAsSecrets || []
|
|
17
|
+
const certs: any[] = spec.certificatesSecrets || []
|
|
18
|
+
const timeouts = spec.forwardingTimeouts
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
{insecure && (
|
|
23
|
+
<AlertBanner
|
|
24
|
+
variant="warning"
|
|
25
|
+
title="TLS verification disabled"
|
|
26
|
+
message="insecureSkipVerify is enabled — Traefik will not validate the upstream's certificate. Connections to the backend are vulnerable to interception."
|
|
27
|
+
/>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
<Section title={kindLabel} icon={ArrowRightLeft} defaultExpanded>
|
|
31
|
+
<PropertyList>
|
|
32
|
+
{spec.serverName && <Property label="Server Name (SNI)" value={spec.serverName} />}
|
|
33
|
+
<Property
|
|
34
|
+
label="Insecure Skip Verify"
|
|
35
|
+
value={<span className={insecure ? 'text-orange-400' : undefined}>{String(insecure)}</span>}
|
|
36
|
+
/>
|
|
37
|
+
{spec.peerCertURI && <Property label="Peer Cert URI" value={spec.peerCertURI} />}
|
|
38
|
+
{spec.maxIdleConnsPerHost !== undefined && <Property label="Max Idle Conns / Host" value={String(spec.maxIdleConnsPerHost)} />}
|
|
39
|
+
{spec.disableHTTP2 !== undefined && <Property label="Disable HTTP/2" value={String(spec.disableHTTP2)} />}
|
|
40
|
+
{spec.terminationDelay !== undefined && <Property label="Termination Delay" value={String(spec.terminationDelay)} />}
|
|
41
|
+
</PropertyList>
|
|
42
|
+
</Section>
|
|
43
|
+
|
|
44
|
+
{(rootCAs.length > 0 || certs.length > 0) && (
|
|
45
|
+
<Section title="Certificates" defaultExpanded>
|
|
46
|
+
<PropertyList>
|
|
47
|
+
{rootCAs.length > 0 && (
|
|
48
|
+
<Property label="Root CA Secrets" value={
|
|
49
|
+
<div className="flex flex-wrap gap-1">
|
|
50
|
+
{rootCAs.map((s: any, i: number) => {
|
|
51
|
+
const name = typeof s === 'string' ? s : s?.secret || s?.name
|
|
52
|
+
return name ? <ResourceLink key={i} name={name} kind="secrets" namespace={ns} onNavigate={onNavigate} /> : null
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
} />
|
|
56
|
+
)}
|
|
57
|
+
{certs.length > 0 && (
|
|
58
|
+
<Property label="Client Cert Secrets" value={
|
|
59
|
+
<div className="flex flex-wrap gap-1">
|
|
60
|
+
{certs.map((s: any, i: number) => {
|
|
61
|
+
const name = typeof s === 'string' ? s : s?.secret || s?.name
|
|
62
|
+
return name ? <ResourceLink key={i} name={name} kind="secrets" namespace={ns} onNavigate={onNavigate} /> : null
|
|
63
|
+
})}
|
|
64
|
+
</div>
|
|
65
|
+
} />
|
|
66
|
+
)}
|
|
67
|
+
</PropertyList>
|
|
68
|
+
</Section>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{timeouts && (
|
|
72
|
+
<Section title="Forwarding Timeouts">
|
|
73
|
+
<PropertyList>
|
|
74
|
+
{timeouts.dialTimeout !== undefined && <Property label="Dial" value={String(timeouts.dialTimeout)} />}
|
|
75
|
+
{timeouts.responseHeaderTimeout !== undefined && <Property label="Response Header" value={String(timeouts.responseHeaderTimeout)} />}
|
|
76
|
+
{timeouts.idleConnTimeout !== undefined && <Property label="Idle Conn" value={String(timeouts.idleConnTimeout)} />}
|
|
77
|
+
{timeouts.readIdleTimeout !== undefined && <Property label="Read Idle" value={String(timeouts.readIdleTimeout)} />}
|
|
78
|
+
{timeouts.pingTimeout !== undefined && <Property label="Ping" value={String(timeouts.pingTimeout)} />}
|
|
79
|
+
</PropertyList>
|
|
80
|
+
</Section>
|
|
81
|
+
)}
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
84
|
+
}
|