@skyhook-io/k8s-ui 1.14.5 → 1.14.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 (104) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.tsx +36 -4
  3. package/src/components/applications/ApplicationsView.tsx +2 -1
  4. package/src/components/audit/AuditAlerts.tsx +35 -37
  5. package/src/components/audit/AuditCard.tsx +18 -7
  6. package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
  7. package/src/components/audit/AuditFindingsTable.tsx +23 -27
  8. package/src/components/charts/AreaChart.test.tsx +13 -0
  9. package/src/components/charts/AreaChart.tsx +28 -25
  10. package/src/components/charts/PrometheusChartsView.tsx +41 -10
  11. package/src/components/charts/axis.test.ts +16 -0
  12. package/src/components/charts/axis.ts +3 -1
  13. package/src/components/charts/format.ts +4 -0
  14. package/src/components/charts/nearestSample.test.ts +31 -0
  15. package/src/components/charts/nearestSample.ts +15 -0
  16. package/src/components/checks/ChecksView.tsx +76 -28
  17. package/src/components/gitops/GitOpsDetailLayout.tsx +25 -9
  18. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
  19. package/src/components/gitops/health-provenance.test.ts +54 -0
  20. package/src/components/gitops/health-provenance.ts +24 -0
  21. package/src/components/gitops/index.ts +2 -0
  22. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
  23. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
  24. package/src/components/gitops/tree/merge.test.ts +34 -0
  25. package/src/components/gitops/tree/merge.ts +7 -0
  26. package/src/components/issues/IssuesView.tsx +35 -54
  27. package/src/components/logs/LogCore.tsx +40 -9
  28. package/src/components/resources/ResourcesSidebar.tsx +66 -78
  29. package/src/components/resources/ResourcesView.tsx +67 -2
  30. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  31. package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
  32. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
  33. package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
  34. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
  35. package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
  36. package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
  37. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
  38. package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
  39. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
  40. package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
  41. package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
  42. package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
  43. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
  44. package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
  46. package/src/components/resources/renderers/PodRenderer.tsx +11 -0
  47. package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
  48. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
  49. package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
  50. package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
  51. package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
  52. package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
  53. package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
  54. package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
  55. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
  56. package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
  57. package/src/components/resources/renderers/index.ts +3 -0
  58. package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
  59. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +123 -0
  60. package/src/components/resources/resource-utils.ts +18 -9
  61. package/src/components/shared/CreateResourceDialog.tsx +237 -14
  62. package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
  63. package/src/components/shared/DrainPlanDialog.tsx +297 -0
  64. package/src/components/shared/ResourceActionsBar.tsx +53 -26
  65. package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
  66. package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
  67. package/src/components/shared/index.ts +4 -0
  68. package/src/components/timeline/TimelineList.tsx +14 -11
  69. package/src/components/timeline/TimelineStrip.tsx +26 -4
  70. package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
  71. package/src/components/timeline/TimelineToolbar.tsx +43 -10
  72. package/src/components/topology/TopologyControls.tsx +27 -8
  73. package/src/components/topology/TopologyGraph.tsx +14 -5
  74. package/src/components/trace/ReachabilityView.tsx +17 -16
  75. package/src/components/ui/CardSection.tsx +79 -35
  76. package/src/components/ui/Collapse.test.tsx +74 -0
  77. package/src/components/ui/Collapse.tsx +115 -29
  78. package/src/components/ui/DialogPortal.tsx +7 -2
  79. package/src/components/ui/FetchResult.test.tsx +20 -2
  80. package/src/components/ui/FetchResult.tsx +33 -3
  81. package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
  82. package/src/components/ui/FreshnessControl.tsx +8 -2
  83. package/src/components/ui/RestrictedState.tsx +7 -4
  84. package/src/components/ui/RowActionMenu.tsx +20 -8
  85. package/src/components/ui/SelectMenu.tsx +18 -6
  86. package/src/components/ui/YamlEditor.tsx +7 -9
  87. package/src/components/ui/drawer-components.tsx +14 -21
  88. package/src/components/ui/index.ts +30 -22
  89. package/src/components/workload/WorkloadView.tsx +8 -12
  90. package/src/hooks/useAnimatedUnmount.ts +21 -3
  91. package/src/theme/components.css +20 -1
  92. package/src/types/core.ts +15 -1
  93. package/src/types/gitops-insights.ts +23 -0
  94. package/src/types/gitops-tree.ts +29 -0
  95. package/src/utils/animation.ts +93 -13
  96. package/src/utils/index.ts +1 -0
  97. package/src/utils/navigation.test.ts +20 -1
  98. package/src/utils/navigation.ts +25 -0
  99. package/src/utils/network-policy.test.ts +42 -0
  100. package/src/utils/network-policy.ts +59 -0
  101. package/src/utils/reflector.ts +139 -0
  102. package/src/utils/yaml-file-import.test.ts +189 -0
  103. package/src/utils/yaml-file-import.ts +95 -0
  104. package/src/components/logs/JsonLogLine.tsx +0 -93
@@ -0,0 +1,150 @@
1
+ import { Activity, Server } from 'lucide-react'
2
+ import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
+ import { Badge } from '../../ui/Badge'
4
+
5
+ interface AnalysisTemplateRendererProps {
6
+ data: any
7
+ }
8
+
9
+ // Every metric.provider Argo Rollouts defines. Order matches the CRD's own
10
+ // AnalysisTemplate reference doc.
11
+ const PROVIDER_KEYS = [
12
+ 'prometheus', 'web', 'kubernetes', 'job', 'wavefront', 'newRelic',
13
+ 'datadog', 'cloudWatch', 'graphite', 'influxdb', 'skywalking', 'plugin',
14
+ ] as const
15
+
16
+ function detectProvider(metric: any): { name: string; details: any } | null {
17
+ const provider = metric?.provider
18
+ if (!provider) return null
19
+ for (const key of PROVIDER_KEYS) {
20
+ if (provider[key]) return { name: key, details: provider[key] }
21
+ }
22
+ const key = Object.keys(provider)[0]
23
+ return key ? { name: key, details: provider[key] } : null
24
+ }
25
+
26
+ function providerLabel(name: string): string {
27
+ switch (name) {
28
+ case 'web': return 'Web'
29
+ case 'newRelic': return 'New Relic'
30
+ case 'cloudWatch': return 'CloudWatch'
31
+ default: return name.charAt(0).toUpperCase() + name.slice(1)
32
+ }
33
+ }
34
+
35
+ // A metric's provider config is unbounded (a full Job PodSpec, a PromQL
36
+ // query, a plugin's arbitrary args) — show the handful of fields an operator
37
+ // actually scans for per provider, not a full dump.
38
+ function ProviderDetails({ name, details }: { name: string; details: any }) {
39
+ switch (name) {
40
+ case 'prometheus':
41
+ return (
42
+ <>
43
+ {details.address && <Property label="Address" value={details.address} />}
44
+ {details.query && <Property label="Query" value={<code className="text-xs">{details.query}</code>} />}
45
+ </>
46
+ )
47
+ case 'web':
48
+ return (
49
+ <>
50
+ {details.url && <Property label="URL" value={details.url} />}
51
+ {details.jsonPath && <Property label="JSON Path" value={<code className="text-xs">{details.jsonPath}</code>} />}
52
+ {details.method && <Property label="Method" value={details.method} />}
53
+ </>
54
+ )
55
+ case 'job':
56
+ return (
57
+ <Property
58
+ label="Job Image"
59
+ value={details.spec?.template?.spec?.containers?.[0]?.image}
60
+ />
61
+ )
62
+ case 'kubernetes':
63
+ return <Property label="Function" value={details.function} />
64
+ case 'datadog':
65
+ return details.query ? <Property label="Query" value={<code className="text-xs">{details.query}</code>} /> : null
66
+ case 'newRelic':
67
+ return details.query ? <Property label="Query" value={<code className="text-xs">{details.query}</code>} /> : null
68
+ case 'graphite':
69
+ return (
70
+ <>
71
+ {details.address && <Property label="Address" value={details.address} />}
72
+ {details.query && <Property label="Query" value={<code className="text-xs">{details.query}</code>} />}
73
+ </>
74
+ )
75
+ case 'influxdb':
76
+ return details.query ? <Property label="Query" value={<code className="text-xs">{details.query}</code>} /> : null
77
+ case 'wavefront':
78
+ return details.query ? <Property label="Query" value={<code className="text-xs">{details.query}</code>} /> : null
79
+ case 'plugin':
80
+ return null
81
+ default:
82
+ return null
83
+ }
84
+ }
85
+
86
+ // Pure spec renderer for AnalysisTemplate / ClusterAnalysisTemplate — no live
87
+ // status to show (that's the AnalysisRun this template gets instantiated
88
+ // into), just the metric definitions and args an operator configured.
89
+ export function AnalysisTemplateRenderer({ data }: AnalysisTemplateRendererProps) {
90
+ const spec = data?.spec || {}
91
+ const metrics: any[] = spec.metrics || []
92
+ const args: any[] = spec.args || []
93
+
94
+ return (
95
+ <>
96
+ {metrics.length > 0 && (
97
+ <Section title={`Metrics (${metrics.length})`} icon={Activity} defaultExpanded>
98
+ <div className="space-y-2">
99
+ {metrics.map((metric: any) => {
100
+ const provider = detectProvider(metric)
101
+ return (
102
+ <div key={metric.name} className="card-inner space-y-1.5">
103
+ <div className="flex items-center justify-between gap-2">
104
+ <span className="font-mono text-sm text-theme-text-primary">{metric.name}</span>
105
+ {provider && <Badge severity="neutral" size="sm">{providerLabel(provider.name)}</Badge>}
106
+ </div>
107
+ <PropertyList>
108
+ {metric.interval && <Property label="Interval" value={metric.interval} />}
109
+ {metric.count != null && <Property label="Count" value={metric.count} />}
110
+ {metric.initialDelay && <Property label="Initial Delay" value={metric.initialDelay} />}
111
+ {metric.successCondition && (
112
+ <Property label="Success if" value={<code className="text-xs">{metric.successCondition}</code>} />
113
+ )}
114
+ {metric.failureCondition && (
115
+ <Property label="Fail if" value={<code className="text-xs">{metric.failureCondition}</code>} />
116
+ )}
117
+ {metric.failureLimit != null && <Property label="Failure Limit" value={metric.failureLimit} />}
118
+ {metric.inconclusiveLimit != null && <Property label="Inconclusive Limit" value={metric.inconclusiveLimit} />}
119
+ {metric.consecutiveErrorLimit != null && (
120
+ <Property label="Consecutive Error Limit" value={metric.consecutiveErrorLimit} />
121
+ )}
122
+ {provider && <ProviderDetails name={provider.name} details={provider.details} />}
123
+ </PropertyList>
124
+ </div>
125
+ )
126
+ })}
127
+ </div>
128
+ </Section>
129
+ )}
130
+
131
+ {args.length > 0 && (
132
+ <Section title={`Arguments (${args.length})`} icon={Server}>
133
+ <PropertyList>
134
+ {args.map((arg: any) => (
135
+ <Property
136
+ key={arg.name}
137
+ label={arg.name}
138
+ value={arg.value ?? (arg.valueFrom ? 'from reference' : undefined)}
139
+ />
140
+ ))}
141
+ </PropertyList>
142
+ </Section>
143
+ )}
144
+
145
+ {metrics.length === 0 && args.length === 0 && (
146
+ <div className="p-4 text-sm text-theme-text-tertiary">This template declares no metrics.</div>
147
+ )}
148
+ </>
149
+ )
150
+ }
@@ -1,7 +1,8 @@
1
- import { useState, useMemo } from 'react'
2
- import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle, Search } from 'lucide-react'
1
+ import { useState, useMemo, useId } from 'react'
2
+ import { ShieldCheck, CheckCircle2, XCircle, Search } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
5
+ import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../../ui/Collapse'
5
6
  import { Input } from '../../ui/Input'
6
7
  import { formatAge } from '../resource-utils'
7
8
  import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER } from './trivy-shared'
@@ -15,6 +16,10 @@ interface ClusterComplianceReportRendererProps {
15
16
  export function ClusterComplianceReportRenderer({ data }: ClusterComplianceReportRendererProps) {
16
17
  const [expandedSection, setExpandedSection] = useState(true)
17
18
  const [expandedControls, setExpandedControls] = useState<Set<string>>(new Set())
19
+ const sectionDisclosure = useDisclosure(expandedSection)
20
+ // Control rows are mapped inline, so they can't each call useDisclosure;
21
+ // one generated prefix plus the control id keeps aria-controls unique.
22
+ const controlPanelBase = useId()
18
23
  const [searchTerm, setSearchTerm] = useState('')
19
24
  const [showFailedOnly, setShowFailedOnly] = useState(false)
20
25
 
@@ -127,13 +132,14 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
127
132
  {sortedChecks.length > 0 && (
128
133
  <Section title="Controls">
129
134
  <button
135
+ {...sectionDisclosure.buttonProps}
130
136
  onClick={() => setExpandedSection(!expandedSection)}
131
137
  className="flex items-center gap-1 text-xs text-theme-text-secondary hover:text-theme-text-primary mb-2"
132
138
  >
133
- {expandedSection ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
139
+ <CollapseChevron open={expandedSection} className="w-3.5 h-3.5" />
134
140
  {sortedChecks.length} controls
135
141
  </button>
136
- {expandedSection && (
142
+ <Collapse open={expandedSection} unmountOnExit id={sectionDisclosure.panelId}>
137
143
  <>
138
144
  {/* Search and filter */}
139
145
  <div className="flex items-center gap-2 mb-2">
@@ -174,10 +180,14 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
174
180
  {filteredChecks.map((check: any) => {
175
181
  const hasFail = (check.totalFail || 0) > 0
176
182
  const controlDef = controlMap.get(check.id)
177
- const isExpanded = expandedControls.has(check.id)
183
+ // One predicate for header, caret and panel: a control with no
184
+ // definition has nothing to show, so it never reads as expanded.
185
+ const isExpanded = expandedControls.has(check.id) && !!controlDef
178
186
  return (
179
187
  <div key={check.id}>
180
188
  <button
189
+ aria-expanded={isExpanded}
190
+ aria-controls={disclosurePanelId(controlPanelBase, check.id)}
181
191
  onClick={() => toggleControl(check.id)}
182
192
  className="w-full flex items-center gap-2 py-1.5 px-1 rounded hover:bg-theme-hover/50 text-left"
183
193
  >
@@ -193,22 +203,18 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
193
203
  <span className="text-[10px] text-theme-text-tertiary shrink-0">
194
204
  {check.totalFail || 0}F / {check.totalPass || 0}P
195
205
  </span>
196
- {isExpanded ? (
197
- <ChevronDown className="w-3 h-3 text-theme-text-tertiary shrink-0" />
198
- ) : (
199
- <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
200
- )}
206
+ <CollapseChevron open={isExpanded} className="w-3 h-3" />
201
207
  </button>
202
- {isExpanded && controlDef && (
208
+ <Collapse open={isExpanded} mountLazily id={disclosurePanelId(controlPanelBase, check.id)}>
203
209
  <div className="ml-6 mr-1 mb-2 p-2.5 bg-theme-elevated/50 rounded border border-theme-border/50 space-y-2">
204
210
  <div className="text-[10px] font-mono text-theme-text-tertiary">{check.id}</div>
205
- {controlDef.description && (
211
+ {controlDef?.description && (
206
212
  <div>
207
213
  <div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-0.5">Description</div>
208
214
  <div className="text-xs text-theme-text-secondary">{controlDef.description}</div>
209
215
  </div>
210
216
  )}
211
- {controlDef.checks?.length > 0 && (
217
+ {controlDef?.checks?.length > 0 && (
212
218
  <div>
213
219
  <div className="text-[10px] font-medium text-theme-text-tertiary uppercase tracking-wider mb-0.5">Check IDs</div>
214
220
  <div className="flex flex-wrap gap-1">
@@ -219,7 +225,7 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
219
225
  </div>
220
226
  )}
221
227
  </div>
222
- )}
228
+ </Collapse>
223
229
  </div>
224
230
  )
225
231
  })}
@@ -230,7 +236,7 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
230
236
  )}
231
237
  </div>
232
238
  </>
233
- )}
239
+ </Collapse>
234
240
  </Section>
235
241
  )}
236
242
  </>
@@ -1,6 +1,8 @@
1
+ import { useState } from 'react'
1
2
  import { clsx } from 'clsx'
2
3
  import { Layers, GitBranch, FileText, Boxes, ScrollText } from 'lucide-react'
3
4
  import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
5
+ import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
4
6
  import { CodeViewer } from '../../ui/CodeViewer'
5
7
  import { kindToPlural } from '../../../utils/navigation'
6
8
 
@@ -122,16 +124,7 @@ function CompositionBody({ data, onNavigate, revision }: CompositionRendererProp
122
124
  />
123
125
  )}
124
126
  </PropertyList>
125
- {step.input && (
126
- <details className="mt-2">
127
- <summary className="text-xs text-theme-text-tertiary cursor-pointer hover:text-theme-text-secondary">
128
- Show input
129
- </summary>
130
- <div className="mt-1">
131
- <CodeViewer code={JSON.stringify(step.input, null, 2)} language="json" maxHeight="200px" />
132
- </div>
133
- </details>
134
- )}
127
+ {step.input && <StepInputDisclosure input={step.input} />}
135
128
  </div>
136
129
  )
137
130
  })}
@@ -216,3 +209,30 @@ export function CompositionRevisionRenderer({ data, onNavigate }: CompositionRev
216
209
  </>
217
210
  )
218
211
  }
212
+
213
+ // Per-step input viewer. A native <details> looked nothing like the rest of
214
+ // the drawer's disclosures and skipped the shared open/close motion. The
215
+ // viewer renders on first open: one highlighted JSON pane per pipeline step
216
+ // is real work to do for steps nobody expands.
217
+ function StepInputDisclosure({ input }: { input: unknown }) {
218
+ const [open, setOpen] = useState(false)
219
+ const { panelId, buttonProps } = useDisclosure(open)
220
+ return (
221
+ <div className="mt-2">
222
+ <button
223
+ {...buttonProps}
224
+ type="button"
225
+ onClick={() => setOpen((v) => !v)}
226
+ className="flex items-center gap-1 text-xs text-theme-text-tertiary hover:text-theme-text-secondary"
227
+ >
228
+ <CollapseChevron open={open} className="h-3 w-3" />
229
+ Show input
230
+ </button>
231
+ <Collapse open={open} mountLazily id={panelId}>
232
+ <div className="mt-1">
233
+ <CodeViewer code={JSON.stringify(input, null, 2)} language="json" maxHeight="200px" />
234
+ </div>
235
+ </Collapse>
236
+ </div>
237
+ )
238
+ }
@@ -1,7 +1,8 @@
1
- import { useState, useMemo } from 'react'
2
- import { ShieldCheck, ChevronDown, ChevronRight, CheckCircle2, XCircle } from 'lucide-react'
1
+ import { useState, useMemo, useId } from 'react'
2
+ import { ShieldCheck, CheckCircle2, XCircle } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
+ import { Collapse, CollapseChevron, useDisclosure, disclosurePanelId } from '../../ui/Collapse'
5
6
  import { formatAge } from '../resource-utils'
6
7
  import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER, TrivyAlertBanner } from './trivy-shared'
7
8
  import { BADGE_INACTIVE } from '../../../utils/badge-colors'
@@ -65,6 +66,10 @@ function groupChecks(checks: any[]): GroupedCheck[] {
65
66
  export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererProps) {
66
67
  const [expandedSection, setExpandedSection] = useState(true)
67
68
  const [expandedChecks, setExpandedChecks] = useState<Set<string>>(new Set())
69
+ const sectionDisclosure = useDisclosure(expandedSection)
70
+ // Check rows are mapped inline, so they can't each call useDisclosure; one
71
+ // generated prefix plus the check id keeps aria-controls unique.
72
+ const checkPanelBase = useId()
68
73
 
69
74
  const report = data.report || {}
70
75
  const summary = report.summary || {}
@@ -149,19 +154,22 @@ export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererPro
149
154
  {grouped.length > 0 && (
150
155
  <Section title="Checks">
151
156
  <button
157
+ {...sectionDisclosure.buttonProps}
152
158
  onClick={() => setExpandedSection(!expandedSection)}
153
159
  className="flex items-center gap-1 text-xs text-theme-text-secondary hover:text-theme-text-primary mb-2"
154
160
  >
155
- {expandedSection ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
161
+ <CollapseChevron open={expandedSection} className="w-3.5 h-3.5" />
156
162
  {grouped.length} unique checks{containerCount > 0 ? ` (${containerCount} containers scanned)` : ''}
157
163
  </button>
158
- {expandedSection && (
164
+ <Collapse open={expandedSection} unmountOnExit id={sectionDisclosure.panelId}>
159
165
  <div className="space-y-0.5">
160
166
  {grouped.map((group) => {
161
167
  const isExpanded = expandedChecks.has(group.checkID)
162
168
  return (
163
169
  <div key={group.checkID}>
164
170
  <button
171
+ aria-expanded={isExpanded}
172
+ aria-controls={disclosurePanelId(checkPanelBase, group.checkID)}
165
173
  onClick={() => toggleCheck(group.checkID)}
166
174
  className="w-full flex items-center gap-2 py-1.5 px-1 rounded hover:bg-theme-hover/50 text-left"
167
175
  >
@@ -177,13 +185,9 @@ export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererPro
177
185
  {group.count > 1 && (
178
186
  <span className="text-[10px] text-theme-text-tertiary shrink-0">{group.count} containers</span>
179
187
  )}
180
- {isExpanded ? (
181
- <ChevronDown className="w-3 h-3 text-theme-text-tertiary shrink-0" />
182
- ) : (
183
- <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />
184
- )}
188
+ <CollapseChevron open={isExpanded} className="w-3 h-3" />
185
189
  </button>
186
- {isExpanded && (
190
+ <Collapse open={isExpanded} mountLazily id={disclosurePanelId(checkPanelBase, group.checkID)}>
187
191
  <div className="ml-6 mr-1 mb-2 p-2.5 bg-theme-elevated/50 rounded border border-theme-border/50 space-y-2">
188
192
  <div className="text-[10px] font-mono text-theme-text-tertiary">{group.checkID}</div>
189
193
  {group.description && (
@@ -209,12 +213,12 @@ export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererPro
209
213
  </div>
210
214
  )}
211
215
  </div>
212
- )}
216
+ </Collapse>
213
217
  </div>
214
218
  )
215
219
  })}
216
220
  </div>
217
- )}
221
+ </Collapse>
218
222
  </Section>
219
223
  )}
220
224
  </>
@@ -2,10 +2,14 @@ import { useState, useRef, useEffect } from 'react'
2
2
  import { AlertTriangle, Copy, Check } from 'lucide-react'
3
3
  import { Section, ExpandableSection } from '../../ui/drawer-components'
4
4
  import { Tooltip } from '../../ui/Tooltip'
5
+ import type { Relationships, ResourceRef } from '../../../types'
6
+ import { ReflectorSummary, ReflectorSection } from './ReflectorSection'
5
7
  import { formatBytes } from '../../../utils/format'
6
8
 
7
9
  interface ConfigMapRendererProps {
8
10
  data: any
11
+ relationships?: Relationships
12
+ onNavigate?: (ref: ResourceRef) => void
9
13
  }
10
14
 
11
15
  // Short = single-line and under 120 chars — render inline
@@ -46,7 +50,7 @@ function CopyButton({ text }: { text: string }) {
46
50
  )
47
51
  }
48
52
 
49
- export function ConfigMapRenderer({ data }: ConfigMapRendererProps) {
53
+ export function ConfigMapRenderer({ data, relationships, onNavigate }: ConfigMapRendererProps) {
50
54
  const dataEntries = Object.entries((data.data || {}) as Record<string, string>)
51
55
  const binaryDataKeys = Object.keys(data.binaryData || {})
52
56
 
@@ -62,6 +66,7 @@ export function ConfigMapRenderer({ data }: ConfigMapRendererProps) {
62
66
 
63
67
  return (
64
68
  <>
69
+ <ReflectorSummary data={data} reflection={relationships?.reflection} onNavigate={onNavigate} />
65
70
  <Section title={`Data (${dataEntries.length + binaryDataKeys.length} keys)`} defaultExpanded>
66
71
  <div className="space-y-3">
67
72
  {/* Short values: compact table */}
@@ -113,6 +118,8 @@ export function ConfigMapRenderer({ data }: ConfigMapRendererProps) {
113
118
  </div>
114
119
  </Section>
115
120
 
121
+ <ReflectorSection data={data} reflection={relationships?.reflection} onNavigate={onNavigate} />
122
+
116
123
  {data.immutable && (
117
124
  <div className="flex items-center gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded text-yellow-400 text-sm">
118
125
  <AlertTriangle className="w-4 h-4" />
@@ -0,0 +1,179 @@
1
+ import { Activity, Boxes, Server } from 'lucide-react'
2
+ import { clsx } from 'clsx'
3
+ import { Section, PropertyList, Property, AlertBanner, ConditionsSection } from '../../ui/drawer-components'
4
+ import { formatAge } from '../resource-utils'
5
+ import { canaryStepTemplateRefs } from './RolloutRenderer'
6
+
7
+ interface ExperimentRendererProps {
8
+ data: any
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
10
+ }
11
+
12
+ function phaseClass(phase?: string): string {
13
+ switch (phase) {
14
+ case 'Successful':
15
+ return 'status-healthy'
16
+ case 'Running':
17
+ case 'Progressing':
18
+ case 'Pending':
19
+ return 'status-degraded'
20
+ case 'Inconclusive':
21
+ return 'status-alert'
22
+ case 'Failed':
23
+ case 'Error':
24
+ return 'status-unhealthy'
25
+ default:
26
+ return 'status-unknown'
27
+ }
28
+ }
29
+
30
+ // Experiments run one or more Deployment-shaped templates side by side
31
+ // (often unweighted "just try it" baselines, not always traffic-split) for
32
+ // spec.duration, optionally gated by the same AnalysisTemplate references a
33
+ // canary analysis step uses. No live demo fixture exists for this CRD (see
34
+ // scripts/rollouts-demo/README.md) — built against the public schema.
35
+ export function ExperimentRenderer({ data, onNavigate }: ExperimentRendererProps) {
36
+ const spec = data?.spec || {}
37
+ const status = data?.status || {}
38
+ const phase = status.phase || 'Unknown'
39
+ const templates: any[] = spec.templates || []
40
+ const templateStatuses: any[] = status.templateStatuses || []
41
+ const analyses: any[] = spec.analyses || []
42
+ // The controller's real field is analysisRuns (verified live) — each entry
43
+ // is {name, analysisRun, phase}, not the analysisRunStatuses/status shape
44
+ // the public docs' examples suggest.
45
+ const analysisRunStatuses: any[] = status.analysisRuns || []
46
+
47
+ const templateStatusByName = new Map(templateStatuses.map((t) => [t.name, t]))
48
+ const analysisRunStatusByName = new Map(analysisRunStatuses.map((a) => [a.name, a]))
49
+
50
+ const ownerRollout = (data?.metadata?.ownerReferences || []).find((ref: any) => ref.kind === 'Rollout')
51
+
52
+ return (
53
+ <>
54
+ {(phase === 'Failed' || phase === 'Error') && (
55
+ <AlertBanner
56
+ variant="error"
57
+ title={phase === 'Error' ? 'Experiment could not run' : 'Experiment failed'}
58
+ message={status.message || 'One or more templates or analyses did not succeed.'}
59
+ />
60
+ )}
61
+
62
+ {phase === 'Inconclusive' && (
63
+ <AlertBanner
64
+ variant="warning"
65
+ title="Experiment inconclusive"
66
+ message={status.message || 'An analysis matched neither its success nor failure condition.'}
67
+ />
68
+ )}
69
+
70
+ <Section title="Status" icon={Server}>
71
+ <PropertyList>
72
+ <Property label="Phase" value={<span className={clsx('badge', phaseClass(phase))}>{phase}</span>} />
73
+ <Property label="Duration" value={spec.duration} />
74
+ <Property label="Message" value={status.message} />
75
+ <Property label="Available At" value={status.availableAt ? formatAge(status.availableAt) : undefined} />
76
+ {ownerRollout && (
77
+ <Property
78
+ label="Rollout"
79
+ value={
80
+ onNavigate ? (
81
+ <button
82
+ onClick={() => onNavigate({ kind: 'Rollout', namespace: data?.metadata?.namespace ?? '', name: ownerRollout.name })}
83
+ className="text-brand hover:underline"
84
+ >
85
+ {ownerRollout.name}
86
+ </button>
87
+ ) : (
88
+ ownerRollout.name
89
+ )
90
+ }
91
+ />
92
+ )}
93
+ </PropertyList>
94
+ </Section>
95
+
96
+ {templates.length > 0 && (
97
+ <Section title={`Templates (${templates.length})`} icon={Boxes} defaultExpanded>
98
+ <div className="space-y-2">
99
+ {templates.map((tmpl: any) => {
100
+ const tmplStatus = templateStatusByName.get(tmpl.name)
101
+ return (
102
+ <div key={tmpl.name} className="card-inner space-y-1.5">
103
+ <div className="flex items-center justify-between gap-2">
104
+ <span className="font-mono text-sm text-theme-text-primary">{tmpl.name}</span>
105
+ {tmplStatus?.status && <span className={clsx('badge-sm', phaseClass(tmplStatus.status))}>{tmplStatus.status}</span>}
106
+ </div>
107
+ <PropertyList>
108
+ <Property label="Replicas" value={tmpl.replicas} />
109
+ {tmpl.weight !== undefined && <Property label="Weight" value={`${tmpl.weight}%`} />}
110
+ {tmplStatus && (
111
+ <Property
112
+ label="Ready"
113
+ value={`${tmplStatus.readyReplicas ?? 0}/${tmplStatus.replicas ?? tmpl.replicas ?? 0}`}
114
+ />
115
+ )}
116
+ {tmplStatus?.podTemplateHash && <Property label="Pod Hash" value={tmplStatus.podTemplateHash} />}
117
+ </PropertyList>
118
+ </div>
119
+ )
120
+ })}
121
+ </div>
122
+ </Section>
123
+ )}
124
+
125
+ {analyses.length > 0 && (
126
+ <Section title={`Analyses (${analyses.length})`} icon={Activity} defaultExpanded>
127
+ <PropertyList>
128
+ {analyses.map((analysis: any) => {
129
+ const runStatus = analysisRunStatusByName.get(analysis.name)
130
+ const refs = canaryStepTemplateRefs(analysis)
131
+ return (
132
+ <Property
133
+ key={analysis.name}
134
+ label={analysis.name}
135
+ value={
136
+ <span className="flex flex-wrap items-center gap-2">
137
+ {runStatus?.phase && (
138
+ <span className={clsx('badge-sm', phaseClass(runStatus.phase))}>{runStatus.phase}</span>
139
+ )}
140
+ {refs.map((ref) =>
141
+ onNavigate ? (
142
+ <button
143
+ key={ref.name}
144
+ onClick={() =>
145
+ onNavigate({
146
+ kind: ref.clusterScoped ? 'ClusterAnalysisTemplate' : 'AnalysisTemplate',
147
+ namespace: ref.clusterScoped ? '' : data?.metadata?.namespace ?? '',
148
+ name: ref.name,
149
+ })
150
+ }
151
+ className="font-mono text-xs text-brand hover:underline"
152
+ >
153
+ {ref.name}
154
+ </button>
155
+ ) : (
156
+ <span key={ref.name} className="font-mono text-xs text-theme-text-secondary">{ref.name}</span>
157
+ )
158
+ )}
159
+ {runStatus?.analysisRun && onNavigate && (
160
+ <button
161
+ onClick={() => onNavigate({ kind: 'AnalysisRun', namespace: data?.metadata?.namespace ?? '', name: runStatus.analysisRun })}
162
+ className="font-mono text-xs text-theme-text-tertiary hover:underline"
163
+ >
164
+ run: {runStatus.analysisRun}
165
+ </button>
166
+ )}
167
+ </span>
168
+ }
169
+ />
170
+ )
171
+ })}
172
+ </PropertyList>
173
+ </Section>
174
+ )}
175
+
176
+ <ConditionsSection conditions={status.conditions} />
177
+ </>
178
+ )
179
+ }
@@ -1,7 +1,8 @@
1
1
  import { useState } from 'react'
2
- import { KeyRound, ChevronDown, ChevronRight } from 'lucide-react'
2
+ import { KeyRound } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { Section, PropertyList, Property } from '../../ui/drawer-components'
5
+ import { Collapse, CollapseChevron, useDisclosure } from '../../ui/Collapse'
5
6
  import { formatAge } from '../resource-utils'
6
7
  import { SEVERITY_BADGE_COLORS, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
7
8
  import { pluralize } from '../../../utils/pluralize'
@@ -16,6 +17,7 @@ const INITIAL_SHOW_COUNT = 50
16
17
  export function ExposedSecretReportRenderer({ data }: ExposedSecretReportRendererProps) {
17
18
  const [showAll, setShowAll] = useState(false)
18
19
  const [expanded, setExpanded] = useState(true)
20
+ const { panelId, buttonProps } = useDisclosure(expanded)
19
21
 
20
22
  const report = data.report || {}
21
23
  const summary = report.summary || {}
@@ -70,13 +72,17 @@ export function ExposedSecretReportRenderer({ data }: ExposedSecretReportRendere
70
72
  {secrets.length > 0 && (
71
73
  <Section title="Exposed Secrets">
72
74
  <button
75
+ {...buttonProps}
73
76
  onClick={() => setExpanded(!expanded)}
74
77
  className="flex items-center gap-1 text-xs text-theme-text-secondary hover:text-theme-text-primary mb-2"
75
78
  >
76
- {expanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
79
+ <CollapseChevron open={expanded} className="w-3.5 h-3.5" />
77
80
  {pluralize(secrets.length, 'secret')}
78
81
  </button>
79
- {expanded && (
82
+ {/* The table can run to hundreds of rows; unmount it while closed
83
+ so a collapsed report stays cheap. Filters live above, so
84
+ nothing is lost. */}
85
+ <Collapse open={expanded} unmountOnExit id={panelId}>
80
86
  <div className="overflow-x-auto -mx-1">
81
87
  <table className="w-full text-xs">
82
88
  <thead>
@@ -113,7 +119,7 @@ export function ExposedSecretReportRenderer({ data }: ExposedSecretReportRendere
113
119
  </button>
114
120
  )}
115
121
  </div>
116
- )}
122
+ </Collapse>
117
123
  </Section>
118
124
  )}
119
125
  </>