@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.
- package/package.json +5 -5
- package/src/components/applications/ApplicationDetail.tsx +36 -4
- package/src/components/applications/ApplicationsView.tsx +2 -1
- package/src/components/audit/AuditAlerts.tsx +35 -37
- package/src/components/audit/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/audit/AuditFindingsTable.tsx +23 -27
- package/src/components/charts/AreaChart.test.tsx +13 -0
- package/src/components/charts/AreaChart.tsx +28 -25
- package/src/components/charts/PrometheusChartsView.tsx +41 -10
- package/src/components/charts/axis.test.ts +16 -0
- package/src/components/charts/axis.ts +3 -1
- package/src/components/charts/format.ts +4 -0
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/checks/ChecksView.tsx +76 -28
- package/src/components/gitops/GitOpsDetailLayout.tsx +25 -9
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
- package/src/components/gitops/health-provenance.test.ts +54 -0
- package/src/components/gitops/health-provenance.ts +24 -0
- package/src/components/gitops/index.ts +2 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
- package/src/components/gitops/tree/merge.test.ts +34 -0
- package/src/components/gitops/tree/merge.ts +7 -0
- package/src/components/issues/IssuesView.tsx +35 -54
- package/src/components/logs/LogCore.tsx +40 -9
- package/src/components/resources/ResourcesSidebar.tsx +66 -78
- package/src/components/resources/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +21 -15
- package/src/components/resources/renderers/CompositionRenderer.tsx +30 -10
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +16 -12
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/GenericRenderer.tsx +13 -7
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +6 -4
- package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
- package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
- package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
- package/src/components/resources/renderers/PodRenderer.tsx +11 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +7 -4
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +13 -15
- package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
- package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
- package/src/components/resources/renderers/SbomReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +10 -4
- package/src/components/resources/renderers/WorkloadRenderer.tsx +11 -1
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
- package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +123 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +237 -14
- package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
- package/src/components/shared/DrainPlanDialog.tsx +297 -0
- package/src/components/shared/ResourceActionsBar.tsx +53 -26
- package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
- package/src/components/shared/index.ts +4 -0
- package/src/components/timeline/TimelineList.tsx +14 -11
- package/src/components/timeline/TimelineStrip.tsx +26 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +3 -5
- package/src/components/timeline/TimelineToolbar.tsx +43 -10
- package/src/components/topology/TopologyControls.tsx +27 -8
- package/src/components/topology/TopologyGraph.tsx +14 -5
- package/src/components/trace/ReachabilityView.tsx +17 -16
- package/src/components/ui/CardSection.tsx +79 -35
- package/src/components/ui/Collapse.test.tsx +74 -0
- package/src/components/ui/Collapse.tsx +115 -29
- package/src/components/ui/DialogPortal.tsx +7 -2
- package/src/components/ui/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +7 -4
- package/src/components/ui/FreshnessControl.tsx +8 -2
- package/src/components/ui/RestrictedState.tsx +7 -4
- package/src/components/ui/RowActionMenu.tsx +20 -8
- package/src/components/ui/SelectMenu.tsx +18 -6
- package/src/components/ui/YamlEditor.tsx +7 -9
- package/src/components/ui/drawer-components.tsx +14 -21
- package/src/components/ui/index.ts +30 -22
- package/src/components/workload/WorkloadView.tsx +8 -12
- package/src/hooks/useAnimatedUnmount.ts +21 -3
- package/src/theme/components.css +20 -1
- package/src/types/core.ts +15 -1
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- package/src/utils/animation.ts +93 -13
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +20 -1
- package/src/utils/navigation.ts +25 -0
- package/src/utils/network-policy.test.ts +42 -0
- package/src/utils/network-policy.ts +59 -0
- package/src/utils/reflector.ts +139 -0
- package/src/utils/yaml-file-import.test.ts +189 -0
- package/src/utils/yaml-file-import.ts +95 -0
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
79
|
+
<CollapseChevron open={expanded} className="w-3.5 h-3.5" />
|
|
77
80
|
{pluralize(secrets.length, 'secret')}
|
|
78
81
|
</button>
|
|
79
|
-
{
|
|
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
|
</>
|