@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,7 +1,7 @@
|
|
|
1
1
|
import { useState, type ReactNode, type JSX } from 'react'
|
|
2
2
|
import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen, List, Eye, EyeOff, Shield } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
|
|
5
5
|
import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
|
|
6
6
|
import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
|
|
7
7
|
import {
|
|
@@ -274,9 +274,12 @@ export function PodRenderer({
|
|
|
274
274
|
const podName = data.metadata?.name
|
|
275
275
|
const isRunning = data.status?.phase === 'Running'
|
|
276
276
|
|
|
277
|
-
// Check for problems
|
|
277
|
+
// Check for problems. Suppressed when the detail already shows the dedicated
|
|
278
|
+
// Operational Issues section (the Issues pipeline covers the same pod failures,
|
|
279
|
+
// richer) — avoids showing the same crashloop twice.
|
|
280
|
+
const operationalIssuesShown = useOperationalIssuesShown()
|
|
278
281
|
const podProblems = getPodProblems(data)
|
|
279
|
-
const hasProblems = podProblems.length > 0
|
|
282
|
+
const hasProblems = podProblems.length > 0 && !operationalIssuesShown
|
|
280
283
|
|
|
281
284
|
// Image filesystem modal state
|
|
282
285
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
|
@@ -548,6 +551,10 @@ export function PodRenderer({
|
|
|
548
551
|
const lastTermination = status?.lastState?.terminated
|
|
549
552
|
const currentWaiting = status?.state?.waiting
|
|
550
553
|
const currentTerminated = status?.state?.terminated
|
|
554
|
+
// A container that exited 0 (a completed Job pod) is a success, not a
|
|
555
|
+
// failure — tone its badges/text sky, not red, so the drawer agrees
|
|
556
|
+
// with the calm "Completed" table badge instead of screaming red.
|
|
557
|
+
const terminatedOk = currentTerminated?.exitCode === 0
|
|
551
558
|
|
|
552
559
|
return (
|
|
553
560
|
<div key={container.name} className="card-inner-lg">
|
|
@@ -586,14 +593,17 @@ export function PodRenderer({
|
|
|
586
593
|
)}
|
|
587
594
|
<span className={clsx(
|
|
588
595
|
'badge',
|
|
589
|
-
isReady ? SEVERITY_BADGE_BORDERED.success :
|
|
596
|
+
isReady ? SEVERITY_BADGE_BORDERED.success :
|
|
597
|
+
terminatedOk ? SEVERITY_BADGE_BORDERED.info :
|
|
598
|
+
SEVERITY_BADGE_BORDERED.error
|
|
590
599
|
)}>
|
|
591
|
-
{isReady ? 'Ready' : 'Not Ready'}
|
|
600
|
+
{isReady ? 'Ready' : terminatedOk ? 'Completed' : 'Not Ready'}
|
|
592
601
|
</span>
|
|
593
602
|
<span className={clsx(
|
|
594
603
|
'badge',
|
|
595
604
|
stateKey === 'running' ? SEVERITY_BADGE_BORDERED.success :
|
|
596
605
|
stateKey === 'waiting' ? SEVERITY_BADGE_BORDERED.warning :
|
|
606
|
+
terminatedOk ? SEVERITY_BADGE_BORDERED.info :
|
|
597
607
|
SEVERITY_BADGE_BORDERED.error
|
|
598
608
|
)}>
|
|
599
609
|
{stateKey}
|
|
@@ -621,9 +631,10 @@ export function PodRenderer({
|
|
|
621
631
|
)}
|
|
622
632
|
</div>
|
|
623
633
|
)}
|
|
624
|
-
{/* Show current terminated reason
|
|
634
|
+
{/* Show current terminated reason — sky for a clean exit-0
|
|
635
|
+
completion, red only for a genuine failure. */}
|
|
625
636
|
{currentTerminated?.reason && (
|
|
626
|
-
<div className=
|
|
637
|
+
<div className={clsx('flex items-center gap-1', terminatedOk ? 'text-sky-500 dark:text-sky-400' : 'text-red-400')}>
|
|
627
638
|
<span className="font-medium">Terminated: {currentTerminated.reason}</span>
|
|
628
639
|
{currentTerminated.exitCode !== undefined && currentTerminated.exitCode !== 0 && (
|
|
629
640
|
<span className="text-theme-text-tertiary">(exit code {currentTerminated.exitCode})</span>
|
|
@@ -795,8 +806,16 @@ export function PodRenderer({
|
|
|
795
806
|
</Section>
|
|
796
807
|
)}
|
|
797
808
|
|
|
798
|
-
{/* Conditions
|
|
799
|
-
|
|
809
|
+
{/* Conditions. A completed pod's Ready/ContainersReady flip to False with
|
|
810
|
+
reason "PodCompleted" — that's expected for a finished pod, not a failure,
|
|
811
|
+
so tone it neutral (gray) instead of red. Gated on the PodCompleted reason
|
|
812
|
+
so a genuinely not-ready pod (any other reason) still reads red. */}
|
|
813
|
+
<ConditionsSection
|
|
814
|
+
conditions={data.status?.conditions}
|
|
815
|
+
getConditionTone={(cond) =>
|
|
816
|
+
cond?.status === 'False' && cond?.reason === 'PodCompleted' ? 'unknown' : undefined
|
|
817
|
+
}
|
|
818
|
+
/>
|
|
800
819
|
|
|
801
820
|
{/* Permissions (via ServiceAccount) — placed below the diagnostic-
|
|
802
821
|
* signal sections (status, containers, resource usage, conditions)
|
|
@@ -10,7 +10,11 @@ interface ServiceRendererProps {
|
|
|
10
10
|
endpointSlices?: any[]
|
|
11
11
|
endpointSlicesLoading?: boolean
|
|
12
12
|
onNavigate?: (ref: ResourceRef) => void
|
|
13
|
-
renderPortAction?: (props: { namespace: string; serviceName: string; port: number; protocol: string }) => ReactNode
|
|
13
|
+
renderPortAction?: (props: { namespace: string; serviceName: string; port: number; protocol: string; name?: string; appProtocol?: string }) => ReactNode
|
|
14
|
+
/** Optional full-width content rendered inside a port's card, below its header
|
|
15
|
+
* (e.g. an inline probe panel). Lets a host attach a port-scoped panel in the
|
|
16
|
+
* drawer flow rather than as a separate overlay. */
|
|
17
|
+
renderPortPanel?: (props: { namespace: string; serviceName: string; port: number; protocol: string; name?: string; appProtocol?: string }) => ReactNode
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
function endpointSliceAddressCount(slice: any): number {
|
|
@@ -28,7 +32,7 @@ function endpointSliceReadyClass(ready: number, total: number): string {
|
|
|
28
32
|
return 'status-unhealthy'
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpointSlicesLoading, onNavigate, renderPortAction }: ServiceRendererProps) {
|
|
35
|
+
export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpointSlicesLoading, onNavigate, renderPortAction, renderPortPanel }: ServiceRendererProps) {
|
|
32
36
|
const spec = data.spec || {}
|
|
33
37
|
const ports = spec.ports || []
|
|
34
38
|
const lbIngress = data.status?.loadBalancer?.ingress || []
|
|
@@ -95,24 +99,34 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
|
|
|
95
99
|
<div className="space-y-2">
|
|
96
100
|
{ports.map((port: any, i: number) => (
|
|
97
101
|
<div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
|
|
98
|
-
<div className="flex items-center justify-between">
|
|
99
|
-
<div className="flex items-
|
|
102
|
+
<div className="flex items-center justify-between gap-2">
|
|
103
|
+
<div className="flex items-baseline gap-x-2 gap-y-0.5 min-w-0 flex-wrap">
|
|
100
104
|
<span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
|
|
101
105
|
<span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
|
|
106
|
+
<span className="text-xs text-theme-text-secondary font-mono">
|
|
107
|
+
{port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
|
|
108
|
+
{port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
|
|
109
|
+
</span>
|
|
102
110
|
</div>
|
|
103
|
-
<div className="flex items-center gap-2">
|
|
111
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
104
112
|
{renderPortAction?.({
|
|
105
113
|
namespace,
|
|
106
114
|
serviceName,
|
|
107
115
|
port: port.port,
|
|
108
116
|
protocol: port.protocol || 'TCP',
|
|
117
|
+
name: port.name,
|
|
118
|
+
appProtocol: port.appProtocol,
|
|
109
119
|
})}
|
|
110
120
|
</div>
|
|
111
121
|
</div>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
{renderPortPanel?.({
|
|
123
|
+
namespace,
|
|
124
|
+
serviceName,
|
|
125
|
+
port: port.port,
|
|
126
|
+
protocol: port.protocol || 'TCP',
|
|
127
|
+
name: port.name,
|
|
128
|
+
appProtocol: port.appProtocol,
|
|
129
|
+
})}
|
|
116
130
|
</div>
|
|
117
131
|
))}
|
|
118
132
|
</div>
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|