@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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationsView.tsx +2 -0
  3. package/src/components/audit/AuditAlerts.tsx +4 -0
  4. package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
  5. package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
  6. package/src/components/audit/AuditFindingsTable.tsx +4 -0
  7. package/src/components/audit/index.ts +1 -0
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
  9. package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
  10. package/src/components/gitops/GitOpsTableView.tsx +3 -1
  11. package/src/components/issues/IssuesView.tsx +9 -36
  12. package/src/components/issues/ResourceIssuesSection.tsx +142 -0
  13. package/src/components/issues/diagnostic.ts +64 -0
  14. package/src/components/issues/index.ts +2 -1
  15. package/src/components/issues/severity.ts +10 -9
  16. package/src/components/issues/types.ts +5 -0
  17. package/src/components/resources/ResourcesView.tsx +38 -2
  18. package/src/components/resources/cron-to-human.test.ts +41 -0
  19. package/src/components/resources/get-pod-phase-display.test.ts +115 -3
  20. package/src/components/resources/get-pod-problems.test.ts +18 -0
  21. package/src/components/resources/health-golden.test.ts +66 -0
  22. package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
  23. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
  24. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
  25. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
  26. package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
  27. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
  28. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
  29. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
  30. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
  31. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
  32. package/src/components/resources/renderers/JobRenderer.tsx +12 -7
  33. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  34. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
  35. package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
  36. package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
  37. package/src/components/resources/renderers/PodRenderer.tsx +28 -9
  38. package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
  39. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
  40. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
  41. package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
  42. package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
  43. package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
  44. package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
  45. package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
  46. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
  47. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
  48. package/src/components/resources/renderers/contour-cells.tsx +6 -5
  49. package/src/components/resources/renderers/index.ts +4 -0
  50. package/src/components/resources/renderers/istio-cells.tsx +16 -25
  51. package/src/components/resources/resource-utils-argo.test.ts +23 -0
  52. package/src/components/resources/resource-utils-argo.ts +5 -1
  53. package/src/components/resources/resource-utils-cnpg.ts +1 -1
  54. package/src/components/resources/resource-utils-keda.ts +17 -7
  55. package/src/components/resources/resource-utils.ts +206 -71
  56. package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
  57. package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
  58. package/src/components/timeline/shared.tsx +15 -4
  59. package/src/components/topology/K8sResourceNode.tsx +28 -1
  60. package/src/components/topology/TopologyControls.tsx +41 -9
  61. package/src/components/topology/layout.ts +11 -5
  62. package/src/components/ui/Badge.tsx +66 -3
  63. package/src/components/ui/ClusterName.tsx +12 -4
  64. package/src/components/ui/PaneLoader.tsx +30 -7
  65. package/src/components/ui/drawer-components.test.tsx +35 -0
  66. package/src/components/ui/drawer-components.tsx +13 -1
  67. package/src/components/workload/WorkloadView.tsx +35 -5
  68. package/src/types/core.ts +136 -2
  69. package/src/utils/applications.test.ts +55 -1
  70. package/src/utils/applications.ts +28 -7
  71. package/src/utils/asset-url.ts +13 -0
  72. 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 : SEVERITY_BADGE_BORDERED.error
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="text-red-400 flex items-center gap-1">
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
- <ConditionsSection conditions={data.status?.conditions} />
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-center gap-2">
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
- <div className="text-xs text-theme-text-secondary mt-1">
113
- {port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
114
- {port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
115
- </div>
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
- <span key={h} className="badge bg-theme-elevated text-theme-text-secondary">{h}</span>
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
- <span className={clsx(
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
- </span>
168
+ </Badge>
172
169
  )}
173
170
  {resolved && (
174
- <span className={clsx(
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
- </span>
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
- <span className="px-1.5 py-0.5 bg-theme-hover rounded text-[10px] text-theme-text-secondary shrink-0">
88
+ <Badge tone="structural" size="sm" className="shrink-0">
88
89
  priority: {route.priority}
89
- </span>
90
+ </Badge>
90
91
  )}
91
92
  {route.kind && route.kind !== 'Rule' && (
92
- <span className="px-1.5 py-0.5 bg-theme-hover rounded text-[10px] text-theme-text-secondary shrink-0">
93
+ <Badge tone="structural" size="sm" className="shrink-0">
93
94
  {route.kind}
94
- </span>
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
- <span className="px-1.5 py-0.5 bg-cyan-500/10 text-cyan-400 rounded text-[10px]">
114
+ <Badge kind="TraefikService" size="sm">
114
115
  TraefikService
115
- </span>
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
- <span className="px-1.5 py-0.5 bg-purple-500/10 text-purple-400 rounded text-[10px] inline-block">
156
+ <Badge tone="accent1" size="sm" className="inline-block">
156
157
  {mw.namespace && mw.namespace !== ns ? `${mw.namespace}/` : ''}{mw.name}
157
- </span>
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
- <span className="px-1.5 py-0.5 bg-yellow-500/10 text-yellow-400 rounded text-[10px]">
246
+ <Badge tone="note" size="sm">
246
247
  {mw.namespace}
247
- </span>
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
+ }