@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Split, Copy } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
|
+
import { getTraefikServiceType } from '../resource-utils-traefik'
|
|
5
|
+
|
|
6
|
+
interface TraefikServiceRendererProps {
|
|
7
|
+
data: any
|
|
8
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ServiceRow({ svc, ns, weightPct, onNavigate }: {
|
|
12
|
+
svc: any
|
|
13
|
+
ns: string
|
|
14
|
+
weightPct?: number
|
|
15
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
16
|
+
}) {
|
|
17
|
+
const isTraefik = svc.kind === 'TraefikService'
|
|
18
|
+
const port = svc.port ? `:${svc.port}` : ''
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex items-center gap-2 text-xs">
|
|
21
|
+
{isTraefik && <Badge kind="TraefikService" size="sm">TraefikService</Badge>}
|
|
22
|
+
<ResourceLink
|
|
23
|
+
name={svc.name}
|
|
24
|
+
kind={isTraefik ? 'traefikservices' : 'services'}
|
|
25
|
+
namespace={svc.namespace || ns}
|
|
26
|
+
label={<span className="text-blue-400">{svc.name}{port}</span>}
|
|
27
|
+
onNavigate={onNavigate}
|
|
28
|
+
/>
|
|
29
|
+
{weightPct !== undefined && (
|
|
30
|
+
<div className="flex items-center gap-1.5 ml-auto">
|
|
31
|
+
<div className="w-20 h-1.5 bg-theme-hover rounded-full overflow-hidden">
|
|
32
|
+
<div className="h-full bg-skyhook-500" style={{ width: `${Math.min(100, weightPct)}%` }} />
|
|
33
|
+
</div>
|
|
34
|
+
<span className="text-theme-text-tertiary tabular-nums w-10 text-right">{weightPct.toFixed(0)}%</span>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function TraefikServiceRenderer({ data, onNavigate }: TraefikServiceRendererProps) {
|
|
42
|
+
const spec = data.spec || {}
|
|
43
|
+
const ns = data.metadata?.namespace || ''
|
|
44
|
+
const type = getTraefikServiceType(data)
|
|
45
|
+
|
|
46
|
+
const weighted = spec.weighted?.services || []
|
|
47
|
+
const totalWeight = weighted.reduce((sum: number, s: any) => sum + (typeof s.weight === 'number' ? s.weight : 0), 0)
|
|
48
|
+
const mirroring = spec.mirroring
|
|
49
|
+
const hrw = spec.highestRandomWeight?.services || []
|
|
50
|
+
|
|
51
|
+
const noBackends = weighted.length === 0 && !mirroring && hrw.length === 0
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
{noBackends && (
|
|
56
|
+
<AlertBanner variant="warning" title="No backend services" message="This TraefikService defines no weighted, mirroring, or HRW backends." />
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<Section title="TraefikService" icon={Split} defaultExpanded>
|
|
60
|
+
<PropertyList>
|
|
61
|
+
<Property label="Type" value={type} />
|
|
62
|
+
</PropertyList>
|
|
63
|
+
</Section>
|
|
64
|
+
|
|
65
|
+
{weighted.length > 0 && (
|
|
66
|
+
<Section title={`Weighted Services (${weighted.length})`} defaultExpanded>
|
|
67
|
+
<div className="space-y-1.5">
|
|
68
|
+
{weighted.map((s: any, i: number) => (
|
|
69
|
+
<ServiceRow
|
|
70
|
+
key={i}
|
|
71
|
+
svc={s}
|
|
72
|
+
ns={ns}
|
|
73
|
+
weightPct={typeof s.weight === 'number' && totalWeight > 0 ? (s.weight / totalWeight) * 100 : undefined}
|
|
74
|
+
onNavigate={onNavigate}
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</Section>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{mirroring && (
|
|
82
|
+
<Section title="Mirroring" icon={Copy} defaultExpanded>
|
|
83
|
+
<div className="space-y-2">
|
|
84
|
+
<div>
|
|
85
|
+
<div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-1">Primary</div>
|
|
86
|
+
<ServiceRow svc={mirroring} ns={ns} onNavigate={onNavigate} />
|
|
87
|
+
</div>
|
|
88
|
+
{(mirroring.mirrors || []).length > 0 && (
|
|
89
|
+
<div>
|
|
90
|
+
<div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-1">Mirrors</div>
|
|
91
|
+
<div className="space-y-1">
|
|
92
|
+
{mirroring.mirrors.map((m: any, i: number) => (
|
|
93
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
94
|
+
<ServiceRow svc={m} ns={ns} onNavigate={onNavigate} />
|
|
95
|
+
{m.percent !== undefined && (
|
|
96
|
+
<span className="text-theme-text-tertiary">{m.percent}%</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</Section>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{hrw.length > 0 && (
|
|
108
|
+
<Section title={`Highest Random Weight (${hrw.length})`} defaultExpanded>
|
|
109
|
+
<div className="space-y-1">
|
|
110
|
+
{hrw.map((s: any, i: number) => (
|
|
111
|
+
<ServiceRow key={i} svc={s} ns={ns} onNavigate={onNavigate} />
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</Section>
|
|
115
|
+
)}
|
|
116
|
+
</>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Lock } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
4
|
+
|
|
5
|
+
interface TraefikTLSOptionRendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TraefikTLSOptionRenderer({ data }: TraefikTLSOptionRendererProps) {
|
|
10
|
+
const spec = data.spec || {}
|
|
11
|
+
const ciphers: string[] = spec.cipherSuites || []
|
|
12
|
+
const curves: string[] = spec.curvePreferences || []
|
|
13
|
+
const alpn: string[] = spec.alpnProtocols || []
|
|
14
|
+
const clientAuth = spec.clientAuth
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<Section title="TLS Option" icon={Lock} defaultExpanded>
|
|
19
|
+
<PropertyList>
|
|
20
|
+
<Property label="Min Version" value={spec.minVersion || 'default (VersionTLS12)'} />
|
|
21
|
+
{spec.maxVersion && <Property label="Max Version" value={spec.maxVersion} />}
|
|
22
|
+
{spec.sniStrict !== undefined && <Property label="SNI Strict" value={String(spec.sniStrict)} />}
|
|
23
|
+
{spec.preferServerCipherSuites !== undefined && (
|
|
24
|
+
<Property label="Prefer Server Ciphers" value={String(spec.preferServerCipherSuites)} />
|
|
25
|
+
)}
|
|
26
|
+
{alpn.length > 0 && <Property label="ALPN Protocols" value={alpn.join(', ')} />}
|
|
27
|
+
</PropertyList>
|
|
28
|
+
</Section>
|
|
29
|
+
|
|
30
|
+
{ciphers.length > 0 && (
|
|
31
|
+
<Section title={`Cipher Suites (${ciphers.length})`}>
|
|
32
|
+
<div className="flex flex-wrap gap-1">
|
|
33
|
+
{ciphers.map((c, i) => (
|
|
34
|
+
<Badge key={i} tone="structural" size="sm" className="font-mono">{c}</Badge>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</Section>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{curves.length > 0 && (
|
|
41
|
+
<Section title={`Curve Preferences (${curves.length})`}>
|
|
42
|
+
<div className="flex flex-wrap gap-1">
|
|
43
|
+
{curves.map((c, i) => (
|
|
44
|
+
<Badge key={i} tone="structural" size="sm" className="font-mono">{c}</Badge>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</Section>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{clientAuth && (
|
|
51
|
+
<Section title="Client Authentication" defaultExpanded>
|
|
52
|
+
<PropertyList>
|
|
53
|
+
{clientAuth.clientAuthType && <Property label="Type" value={clientAuth.clientAuthType} />}
|
|
54
|
+
{Array.isArray(clientAuth.secretNames) && clientAuth.secretNames.length > 0 && (
|
|
55
|
+
<Property label="CA Secrets" value={clientAuth.secretNames.join(', ')} />
|
|
56
|
+
)}
|
|
57
|
+
</PropertyList>
|
|
58
|
+
</Section>
|
|
59
|
+
)}
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -422,7 +422,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
|
|
|
422
422
|
|
|
423
423
|
function ScalerDiagnosisRow({ entry }: { entry: ScalerDiagnosis }) {
|
|
424
424
|
if (entry.loading) {
|
|
425
|
-
return <div className="text-xs text-theme-text-tertiary">Loading autoscaler diagnosis
|
|
425
|
+
return <div className="text-xs text-theme-text-tertiary">Loading autoscaler diagnosis…</div>
|
|
426
426
|
}
|
|
427
427
|
if (entry.error) {
|
|
428
428
|
return <div className="text-xs text-theme-text-tertiary">Autoscaler diagnosis unavailable</div>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
// Ratchet guard for the badge token system (see DESIGN.md "Badges").
|
|
6
|
+
//
|
|
7
|
+
// Hand-rolled chip color strings (`bg-<color>-N/NN text-<color>-N`) bypass the
|
|
8
|
+
// theme and overload hues across intents. The migration to <Badge> is
|
|
9
|
+
// incremental: this test pins the set of renderer files that STILL contain
|
|
10
|
+
// hand-rolled chips. The set may SHRINK (clean a file → remove it here) but must
|
|
11
|
+
// never GROW — a new renderer must use <Badge>/tokens, not literal colors.
|
|
12
|
+
//
|
|
13
|
+
// When this fails because you cleaned a file: delete it from BASELINE.
|
|
14
|
+
// When it fails because you added a chip: use <Badge severity|kind|protocol|tone>
|
|
15
|
+
// instead (DESIGN.md has the decision tree).
|
|
16
|
+
|
|
17
|
+
const CHIP_RE =
|
|
18
|
+
/bg-(red|green|blue|yellow|orange|amber|purple|cyan|pink|indigo|emerald|teal|violet|rose|lime|sky)-[0-9]+\/[0-9]+ text-(red|green|blue|yellow|orange|amber|purple|cyan|pink|indigo|emerald|teal|violet|rose|lime|sky)-[0-9]+/
|
|
19
|
+
|
|
20
|
+
// Renderer files that still contain hand-rolled chips, pending migration.
|
|
21
|
+
// Ratchet: this list only shrinks. Do not add to it.
|
|
22
|
+
const BASELINE = new Set<string>([
|
|
23
|
+
'AlertRenderer.tsx', 'ArgoApplicationRenderer.tsx', 'CertificateRenderer.tsx',
|
|
24
|
+
'CertificateRequestRenderer.tsx', 'ChallengeRenderer.tsx', 'CiliumNetworkPolicyRenderer.tsx',
|
|
25
|
+
'ClusterExternalSecretRenderer.tsx', 'ClusterIssuerRenderer.tsx', 'ClusterNetworkPolicyRenderer.tsx',
|
|
26
|
+
'cnpg-cells.tsx', 'CNPGClusterRenderer.tsx', 'EventRenderer.tsx', 'GatewayRenderer.tsx',
|
|
27
|
+
'GenericRenderer.tsx', 'IngressClassRenderer.tsx', 'IstioPeerAuthenticationRenderer.tsx',
|
|
28
|
+
'IstioServiceEntryRenderer.tsx', 'KarpenterNodeClaimRenderer.tsx', 'knative-cells.tsx',
|
|
29
|
+
'KnativeEventingRenderer.tsx', 'KnativeRevisionRenderer.tsx', 'kyverno-cells.tsx',
|
|
30
|
+
'KyvernoPolicyReportRenderer.tsx', 'NetworkPolicyRenderer.tsx', 'NodeRenderer.tsx',
|
|
31
|
+
'OrderRenderer.tsx', 'PodRenderer.tsx', 'RoleRenderer.tsx', 'RolloutRenderer.tsx',
|
|
32
|
+
'SealedSecretRenderer.tsx', 'SecretRenderer.tsx', 'SecretStoreRenderer.tsx',
|
|
33
|
+
'VeleroBackupRenderer.tsx', 'VeleroRestoreRenderer.tsx', 'VeleroScheduleRenderer.tsx',
|
|
34
|
+
'VPARenderer.tsx', 'VulnerabilityReportRenderer.tsx', 'WebhookConfigRenderer.tsx',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const dir = fileURLToPath(new URL('.', import.meta.url))
|
|
38
|
+
|
|
39
|
+
function filesWithHandRolledChips(): string[] {
|
|
40
|
+
return readdirSync(dir)
|
|
41
|
+
.filter((f) => f.endsWith('.tsx') && !f.endsWith('.test.tsx'))
|
|
42
|
+
.filter((f) => CHIP_RE.test(readFileSync(`${dir}/${f}`, 'utf8')))
|
|
43
|
+
.sort()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('badge token system — no hand-rolled chip colors', () => {
|
|
47
|
+
const offenders = filesWithHandRolledChips()
|
|
48
|
+
|
|
49
|
+
it('does not introduce hand-rolled chips in new/migrated files (ratchet only shrinks)', () => {
|
|
50
|
+
const newOffenders = offenders.filter((f) => !BASELINE.has(f))
|
|
51
|
+
expect(
|
|
52
|
+
newOffenders,
|
|
53
|
+
`These renderers use hand-rolled chip colors. Use <Badge severity|kind|protocol|tone> ` +
|
|
54
|
+
`(see DESIGN.md "Badges"), not literal bg-/text- color strings:\n ${newOffenders.join('\n ')}`,
|
|
55
|
+
).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('baseline has no stale entries (remove files you have already cleaned)', () => {
|
|
59
|
+
const stale = [...BASELINE].filter((f) => !offenders.includes(f)).sort()
|
|
60
|
+
expect(
|
|
61
|
+
stale,
|
|
62
|
+
`These files are in BASELINE but no longer contain hand-rolled chips — ` +
|
|
63
|
+
`delete them from BASELINE in this test:\n ${stale.join('\n ')}`,
|
|
64
|
+
).toEqual([])
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Shield } from 'lucide-react'
|
|
4
4
|
import { Tooltip } from '../../ui/Tooltip'
|
|
5
|
+
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
5
6
|
import {
|
|
6
7
|
getHTTPProxyFQDN,
|
|
7
8
|
getHTTPProxyRouteCount,
|
|
@@ -36,11 +37,11 @@ export function HTTPProxyCell({ resource, column }: { resource: any; column: str
|
|
|
36
37
|
}
|
|
37
38
|
case 'status': {
|
|
38
39
|
const { label } = getHTTPProxyStatus(resource)
|
|
39
|
-
const
|
|
40
|
-
: label === 'Invalid' ? '
|
|
41
|
-
: label === 'Orphaned' ? '
|
|
42
|
-
: '
|
|
43
|
-
return <
|
|
40
|
+
const severity: BadgeSeverity = label === 'Valid' ? 'success'
|
|
41
|
+
: label === 'Invalid' ? 'error'
|
|
42
|
+
: label === 'Orphaned' ? 'warning'
|
|
43
|
+
: 'neutral'
|
|
44
|
+
return <Badge severity={severity}>{label}</Badge>
|
|
44
45
|
}
|
|
45
46
|
default:
|
|
46
47
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
@@ -114,6 +114,10 @@ export * from './WorkflowRenderer'
|
|
|
114
114
|
export * from './WorkflowTemplateRenderer'
|
|
115
115
|
export * from './WorkloadRenderer'
|
|
116
116
|
export * from './TraefikIngressRouteRenderer'
|
|
117
|
+
export * from './TraefikMiddlewareRenderer'
|
|
118
|
+
export * from './TraefikServiceRenderer'
|
|
119
|
+
export * from './TraefikTLSOptionRenderer'
|
|
120
|
+
export * from './TraefikServersTransportRenderer'
|
|
117
121
|
export * from './traefik-cells'
|
|
118
122
|
export * from './ContourHTTPProxyRenderer'
|
|
119
123
|
export * from './contour-cells'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Istio cell components for ResourcesView table
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import {
|
|
4
|
+
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
5
5
|
import {
|
|
6
6
|
getVirtualServiceStatus,
|
|
7
7
|
getVirtualServiceHosts,
|
|
@@ -26,17 +26,17 @@ import {
|
|
|
26
26
|
getAuthorizationPolicyRuleCount,
|
|
27
27
|
} from '../resource-utils-istio'
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
STRICT: '
|
|
31
|
-
PERMISSIVE: '
|
|
32
|
-
DISABLE: '
|
|
29
|
+
const modeSeverity: Record<string, BadgeSeverity> = {
|
|
30
|
+
STRICT: 'success',
|
|
31
|
+
PERMISSIVE: 'warning',
|
|
32
|
+
DISABLE: 'error',
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const
|
|
36
|
-
ALLOW: '
|
|
37
|
-
DENY: '
|
|
38
|
-
CUSTOM: '
|
|
39
|
-
AUDIT: '
|
|
35
|
+
const actionSeverity: Record<string, BadgeSeverity> = {
|
|
36
|
+
ALLOW: 'success',
|
|
37
|
+
DENY: 'error',
|
|
38
|
+
CUSTOM: 'info',
|
|
39
|
+
AUDIT: 'warning',
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function VirtualServiceCell({ resource, column }: { resource: any; column: string }) {
|
|
@@ -133,12 +133,9 @@ export function ServiceEntryCell({ resource, column }: { resource: any; column:
|
|
|
133
133
|
case 'location': {
|
|
134
134
|
const location = getServiceEntryLocation(resource)
|
|
135
135
|
return (
|
|
136
|
-
<
|
|
137
|
-
'badge',
|
|
138
|
-
location === 'MESH_EXTERNAL' ? 'bg-orange-500/20 text-orange-400' : 'bg-blue-500/20 text-blue-400'
|
|
139
|
-
)}>
|
|
136
|
+
<Badge tone={location === 'MESH_EXTERNAL' ? 'accent2' : 'accent1'}>
|
|
140
137
|
{location === 'MESH_EXTERNAL' ? 'External' : 'Internal'}
|
|
141
|
-
</
|
|
138
|
+
</Badge>
|
|
142
139
|
)
|
|
143
140
|
}
|
|
144
141
|
case 'ports': {
|
|
@@ -163,12 +160,9 @@ export function PeerAuthenticationCell({ resource, column }: { resource: any; co
|
|
|
163
160
|
case 'mode': {
|
|
164
161
|
const mode = getPeerAuthenticationMode(resource)
|
|
165
162
|
return (
|
|
166
|
-
<
|
|
167
|
-
'badge',
|
|
168
|
-
modeColors[mode] || BADGE_INACTIVE
|
|
169
|
-
)}>
|
|
163
|
+
<Badge severity={modeSeverity[mode] ?? 'neutral'}>
|
|
170
164
|
{mode}
|
|
171
|
-
</
|
|
165
|
+
</Badge>
|
|
172
166
|
)
|
|
173
167
|
}
|
|
174
168
|
case 'selector': {
|
|
@@ -193,12 +187,9 @@ export function AuthorizationPolicyCell({ resource, column }: { resource: any; c
|
|
|
193
187
|
case 'action': {
|
|
194
188
|
const action = getAuthorizationPolicyAction(resource)
|
|
195
189
|
return (
|
|
196
|
-
<
|
|
197
|
-
'badge',
|
|
198
|
-
actionColors[action] || BADGE_INACTIVE
|
|
199
|
-
)}>
|
|
190
|
+
<Badge severity={actionSeverity[action] ?? 'neutral'}>
|
|
200
191
|
{action}
|
|
201
|
-
</
|
|
192
|
+
</Badge>
|
|
202
193
|
)
|
|
203
194
|
}
|
|
204
195
|
case 'rules': {
|
|
@@ -283,7 +283,7 @@ export function getCNPGScheduledBackupStatus(resource: any): StatusBadge {
|
|
|
283
283
|
const isSuspended = resource.spec?.suspend === true
|
|
284
284
|
|
|
285
285
|
if (isSuspended) {
|
|
286
|
-
return { text: 'Suspended', color: healthColors.
|
|
286
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
// If we have a last schedule time, it's active
|