@skyhook-io/k8s-ui 1.14.4 → 1.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/components/audit/AuditCard.tsx +18 -7
- package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
- package/src/components/charts/AreaChart.test.tsx +173 -0
- package/src/components/charts/AreaChart.tsx +208 -53
- package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
- package/src/components/charts/PrometheusChartsView.tsx +205 -76
- package/src/components/charts/SeriesLegend.tsx +8 -3
- package/src/components/charts/annotations.ts +59 -0
- package/src/components/charts/axis.test.ts +48 -0
- package/src/components/charts/axis.ts +62 -0
- package/src/components/charts/colors.test.ts +27 -0
- package/src/components/charts/colors.ts +23 -0
- package/src/components/charts/format.test.ts +19 -0
- package/src/components/charts/format.ts +21 -1
- package/src/components/charts/index.ts +3 -1
- package/src/components/charts/nearestSample.test.ts +31 -0
- package/src/components/charts/nearestSample.ts +15 -0
- package/src/components/charts/types.ts +12 -0
- package/src/components/checks/ChecksView.tsx +20 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -0
- 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/resources/HPADiagnosisSummary.test.tsx +178 -0
- package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
- package/src/components/resources/ResourcesView.tsx +67 -2
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/index.ts +7 -0
- package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
- package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -57
- 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/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/SecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -21
- 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 +116 -0
- package/src/components/resources/resource-utils.ts +18 -9
- package/src/components/shared/CreateResourceDialog.tsx +231 -11
- 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/topology/K8sResourceNode.test.ts +9 -0
- package/src/components/topology/K8sResourceNode.tsx +56 -5
- package/src/components/ui/Badge.tsx +5 -1
- package/src/components/ui/FetchResult.test.tsx +20 -2
- package/src/components/ui/FetchResult.tsx +33 -3
- package/src/components/ui/FreshnessControl.tsx +8 -2
- package/src/components/ui/index.ts +5 -0
- package/src/components/ui/monacoRuntime.ts +14 -0
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/monaco-deep.d.ts +10 -0
- package/src/theme/components.css +16 -0
- package/src/types/core.ts +38 -11
- package/src/types/gitops-insights.ts +23 -0
- package/src/types/gitops-tree.ts +29 -0
- 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
|
@@ -3,6 +3,7 @@ import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
|
|
|
3
3
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue'
|
|
4
4
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
5
5
|
import { RestrictedState } from '../ui/RestrictedState'
|
|
6
|
+
import { FetchResult } from '../ui/FetchResult'
|
|
6
7
|
import { Input } from '../ui/Input'
|
|
7
8
|
import type { TopPodMetrics, TopNodeMetrics, ContainerResourceMetrics } from '../../types'
|
|
8
9
|
import {
|
|
@@ -711,6 +712,23 @@ const KNOWN_COLUMNS: Record<string, Column[]> = {
|
|
|
711
712
|
{ key: 'metrics', label: 'Metrics', width: 'w-40', tooltip: 'Per-metric verdicts' },
|
|
712
713
|
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
713
714
|
],
|
|
715
|
+
analysistemplates: [
|
|
716
|
+
{ key: 'name', label: 'Name' },
|
|
717
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
718
|
+
{ key: 'metricCount', label: 'Metrics', width: 'w-24' },
|
|
719
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
720
|
+
],
|
|
721
|
+
clusteranalysistemplates: [
|
|
722
|
+
{ key: 'name', label: 'Name' },
|
|
723
|
+
{ key: 'metricCount', label: 'Metrics', width: 'w-24' },
|
|
724
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
725
|
+
],
|
|
726
|
+
experiments: [
|
|
727
|
+
{ key: 'name', label: 'Name' },
|
|
728
|
+
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
729
|
+
{ key: 'status', label: 'Phase', width: 'w-28' },
|
|
730
|
+
{ key: 'age', label: 'Age', width: 'w-24' },
|
|
731
|
+
],
|
|
714
732
|
workflows: [
|
|
715
733
|
{ key: 'name', label: 'Name' },
|
|
716
734
|
{ key: 'namespace', label: 'Namespace', width: 'w-48' },
|
|
@@ -2826,6 +2844,9 @@ const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
|
|
|
2826
2844
|
sbomreports: ['aquasecurity.github.io'],
|
|
2827
2845
|
vulnerabilityreports: ['aquasecurity.github.io'],
|
|
2828
2846
|
analysisruns: ['argoproj.io'],
|
|
2847
|
+
analysistemplates: ['argoproj.io'],
|
|
2848
|
+
clusteranalysistemplates: ['argoproj.io'],
|
|
2849
|
+
experiments: ['argoproj.io'],
|
|
2829
2850
|
applications: ['argoproj.io'],
|
|
2830
2851
|
applicationsets: ['argoproj.io'],
|
|
2831
2852
|
appprojects: ['argoproj.io'],
|
|
@@ -4987,11 +5008,13 @@ export function ResourcesView({
|
|
|
4987
5008
|
// forbidden/unavailable in counts (e.g. an informer not yet synced at counts
|
|
4988
5009
|
// time) while the list query has since returned data — show the table, not
|
|
4989
5010
|
// RestrictedState. A 403 on the list itself never carries rows, so it still
|
|
4990
|
-
// forces the restricted state.
|
|
5011
|
+
// forces the restricted state. A non-403 list error also beats the counts
|
|
5012
|
+
// entry: a kind whose sync deadline fired is reported unavailable by counts,
|
|
5013
|
+
// but the list knows it failed to load — that is not an RBAC answer.
|
|
4991
5014
|
const selectedHasRows = Array.isArray(resources) && resources.length > 0
|
|
4992
5015
|
const isSelectedForbidden =
|
|
4993
5016
|
isForbiddenError(selectedQueryError) ||
|
|
4994
|
-
(!selectedHasRows && forbiddenKinds.has(selectedKindCountKey))
|
|
5017
|
+
(!selectedHasRows && !selectedQueryError && forbiddenKinds.has(selectedKindCountKey))
|
|
4995
5018
|
|
|
4996
5019
|
// Reset sort and filters when kind changes (but not when syncing from URL navigation)
|
|
4997
5020
|
// Track previous kind to skip on mount (where the effect fires but kind hasn't actually changed)
|
|
@@ -6277,6 +6300,15 @@ export function ResourcesView({
|
|
|
6277
6300
|
/>
|
|
6278
6301
|
</div>
|
|
6279
6302
|
</div>
|
|
6303
|
+
) : selectedQueryError && !Array.isArray(resources) ? (
|
|
6304
|
+
// A failed fetch with no rows to fall back on must not reach the
|
|
6305
|
+
// empty state below — it would tell the user the cluster has none.
|
|
6306
|
+
<FetchResult
|
|
6307
|
+
loading={false}
|
|
6308
|
+
error={selectedQueryError}
|
|
6309
|
+
onRetry={selectedQuery?.refetch}
|
|
6310
|
+
className="absolute inset-0"
|
|
6311
|
+
/>
|
|
6280
6312
|
) : largeListGuard ? (
|
|
6281
6313
|
<div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary px-6 text-center">
|
|
6282
6314
|
<AlertTriangle className="w-8 h-8 text-amber-400 mb-2" />
|
|
@@ -7081,6 +7113,11 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
7081
7113
|
return <RolloutCell resource={resource} column={column} />
|
|
7082
7114
|
case 'analysisruns':
|
|
7083
7115
|
return <AnalysisRunCell resource={resource} column={column} />
|
|
7116
|
+
case 'analysistemplates':
|
|
7117
|
+
case 'clusteranalysistemplates':
|
|
7118
|
+
return <AnalysisTemplateCell resource={resource} column={column} />
|
|
7119
|
+
case 'experiments':
|
|
7120
|
+
return <ExperimentCell resource={resource} column={column} />
|
|
7084
7121
|
case 'workflows':
|
|
7085
7122
|
return <WorkflowCell resource={resource} column={column} />
|
|
7086
7123
|
case 'cronworkflows':
|
|
@@ -8746,6 +8783,23 @@ function RolloutCell({ resource, column }: { resource: any; column: string }) {
|
|
|
8746
8783
|
}
|
|
8747
8784
|
}
|
|
8748
8785
|
|
|
8786
|
+
// An Experiment reports status.phase from the same AnalysisPhase vocabulary
|
|
8787
|
+
// as AnalysisRun (Successful/Running/Pending/Inconclusive/Failed/Error), so
|
|
8788
|
+
// getAnalysisRunStatus's mapping applies directly — routing here instead of
|
|
8789
|
+
// GenericCell avoids the generic phase heuristic's HEALTHY_PHASES set, which
|
|
8790
|
+
// doesn't recognize 'Successful' and treats 'Running' as healthy (neither
|
|
8791
|
+
// true for this vocabulary).
|
|
8792
|
+
function ExperimentCell({ resource, column }: { resource: any; column: string }) {
|
|
8793
|
+
switch (column) {
|
|
8794
|
+
case 'status': {
|
|
8795
|
+
const status = getAnalysisRunStatus(resource)
|
|
8796
|
+
return <span className={clsx('badge', status.color)}>{status.text}</span>
|
|
8797
|
+
}
|
|
8798
|
+
default:
|
|
8799
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
8800
|
+
}
|
|
8801
|
+
}
|
|
8802
|
+
|
|
8749
8803
|
function AnalysisRunCell({ resource, column }: { resource: any; column: string }) {
|
|
8750
8804
|
switch (column) {
|
|
8751
8805
|
case 'status': {
|
|
@@ -8784,6 +8838,17 @@ function AnalysisRunCell({ resource, column }: { resource: any; column: string }
|
|
|
8784
8838
|
}
|
|
8785
8839
|
}
|
|
8786
8840
|
|
|
8841
|
+
function AnalysisTemplateCell({ resource, column }: { resource: any; column: string }) {
|
|
8842
|
+
switch (column) {
|
|
8843
|
+
case 'metricCount': {
|
|
8844
|
+
const count = (resource.spec?.metrics || []).length
|
|
8845
|
+
return <span className="text-sm text-theme-text-secondary">{count}</span>
|
|
8846
|
+
}
|
|
8847
|
+
default:
|
|
8848
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
|
|
8787
8852
|
function WorkflowCell({ resource, column }: { resource: any; column: string }) {
|
|
8788
8853
|
switch (column) {
|
|
8789
8854
|
case 'status': {
|
|
@@ -63,8 +63,8 @@ describe('curated column ownership', () => {
|
|
|
63
63
|
// Brace-counting, not parsing: assert the extraction actually saw the whole
|
|
64
64
|
// table, or a silently-missed key would let the guard above pass vacuously.
|
|
65
65
|
it('extracted every curated key', () => {
|
|
66
|
-
expect(CURATED.length).toBe(
|
|
67
|
-
expect(OWNED.size).toBe(
|
|
66
|
+
expect(CURATED.length).toBe(239)
|
|
67
|
+
expect(OWNED.size).toBe(201)
|
|
68
68
|
expect(CURATED).toContain('pods')
|
|
69
69
|
expect(CURATED).toContain('crossplanemanagedresources')
|
|
70
70
|
expect([...OWNED]).toContain('awsmanagedcontrolplanes')
|
|
@@ -121,7 +121,7 @@ describe('every declaration resolves', () => {
|
|
|
121
121
|
.map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
|
|
122
122
|
|
|
123
123
|
it('covers the whole ownership table', () => {
|
|
124
|
-
expect(DECLARED.length).toBe(
|
|
124
|
+
expect(DECLARED.length).toBe(201)
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export * from './resource-utils'
|
|
2
2
|
export * from './resource-utils-hpa'
|
|
3
|
+
export {
|
|
4
|
+
HPADiagnosisSummary,
|
|
5
|
+
hpaBadgeSeverity,
|
|
6
|
+
hpaReasonGroups,
|
|
7
|
+
isHPAReasonRedundant,
|
|
8
|
+
} from './HPADiagnosisSummary'
|
|
9
|
+
export type { HPADiagnosisSummaryProps, HPAReasonGroups } from './HPADiagnosisSummary'
|
|
3
10
|
export * from './resource-utils-argo'
|
|
4
11
|
export * from './resource-utils-certmanager'
|
|
5
12
|
export * from './resource-utils-cnpg'
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,11 +2,11 @@ import type { ReactNode } from 'react'
|
|
|
2
2
|
import { Activity, Cpu } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, ConditionsSection, ResourceLink, type ConditionTone } from '../../ui/drawer-components'
|
|
5
|
-
import { Badge
|
|
5
|
+
import { Badge } from '../../ui/Badge'
|
|
6
6
|
import { kindToPlural } from '../../../utils/navigation'
|
|
7
7
|
import { formatAge } from '../resource-utils'
|
|
8
|
-
import {
|
|
9
|
-
import type { HPADiagnosis
|
|
8
|
+
import { HPADiagnosisSummary } from '../HPADiagnosisSummary'
|
|
9
|
+
import type { HPADiagnosis } from '../../../types'
|
|
10
10
|
|
|
11
11
|
interface HPARendererProps {
|
|
12
12
|
data: any
|
|
@@ -16,23 +16,6 @@ interface HPARendererProps {
|
|
|
16
16
|
extraSections?: ReactNode
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function hpaBadgeSeverity(state: HPADiagnosisState): BadgeSeverity {
|
|
20
|
-
switch (hpaStateLevel(state)) {
|
|
21
|
-
case 'healthy':
|
|
22
|
-
return 'success'
|
|
23
|
-
case 'unhealthy':
|
|
24
|
-
return 'error'
|
|
25
|
-
case 'degraded':
|
|
26
|
-
return 'warning'
|
|
27
|
-
case 'alert':
|
|
28
|
-
return 'alert'
|
|
29
|
-
case 'neutral':
|
|
30
|
-
return 'info'
|
|
31
|
-
default:
|
|
32
|
-
return 'neutral'
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
19
|
function hpaConditionTone(condition: any): ConditionTone | undefined {
|
|
37
20
|
if (condition?.type === 'ScaledToZero') {
|
|
38
21
|
if (condition.status === 'False') return 'ok'
|
|
@@ -50,14 +33,6 @@ function hpaConditionTone(condition: any): ConditionTone | undefined {
|
|
|
50
33
|
return 'warning'
|
|
51
34
|
}
|
|
52
35
|
|
|
53
|
-
function formatReasonID(id: string): string {
|
|
54
|
-
return id.replace(/_/g, ' ')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isReasonMessageRedundant(state: HPADiagnosisState, reason: NonNullable<HPADiagnosis['reasons']>[number]): boolean {
|
|
58
|
-
return state === 'limited_max' && reason.id === 'limited_max'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: HPARendererProps) {
|
|
62
37
|
const status = data.status || {}
|
|
63
38
|
const spec = data.spec || {}
|
|
@@ -67,35 +42,7 @@ export function HPARenderer({ data, onNavigate, hpaDiagnosis, extraSections }: H
|
|
|
67
42
|
<>
|
|
68
43
|
{hpaDiagnosis && (
|
|
69
44
|
<Section title="Diagnosis" icon={Activity}>
|
|
70
|
-
<
|
|
71
|
-
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
72
|
-
<div className="min-w-0">
|
|
73
|
-
<div className="text-sm font-medium text-theme-text-primary">{hpaDiagnosis.summary}</div>
|
|
74
|
-
<div className="mt-1 text-xs text-theme-text-secondary">
|
|
75
|
-
{hpaDiagnosis.bounds.current}/{hpaDiagnosis.bounds.desired} replicas, bounds {hpaDiagnosis.bounds.min}-{hpaDiagnosis.bounds.max}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
<Badge severity={hpaBadgeSeverity(hpaDiagnosis.state)}>{hpaStateLabel(hpaDiagnosis.state)}</Badge>
|
|
79
|
-
</div>
|
|
80
|
-
{hpaDiagnosis.reasons && hpaDiagnosis.reasons.length > 0 && (
|
|
81
|
-
<div className="space-y-2">
|
|
82
|
-
{hpaDiagnosis.reasons.map((reason) => (
|
|
83
|
-
<div key={`${reason.id}-${reason.message}`} className="rounded border border-theme-border bg-theme-surface p-2">
|
|
84
|
-
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
85
|
-
<span className="font-medium text-theme-text-primary">Evidence</span>
|
|
86
|
-
<span className="text-theme-text-tertiary">{formatReasonID(reason.id)}</span>
|
|
87
|
-
{reason.conditionType && <span className="text-theme-text-tertiary">{reason.conditionType}</span>}
|
|
88
|
-
{reason.conditionReason && <span className="text-theme-text-tertiary">{reason.conditionReason}</span>}
|
|
89
|
-
{reason.detail && <span className="text-theme-text-tertiary">{reason.detail}</span>}
|
|
90
|
-
</div>
|
|
91
|
-
{!isReasonMessageRedundant(hpaDiagnosis.state, reason) && (
|
|
92
|
-
<div className="mt-1 text-xs text-theme-text-secondary">{reason.message}</div>
|
|
93
|
-
)}
|
|
94
|
-
</div>
|
|
95
|
-
))}
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
45
|
+
<HPADiagnosisSummary diagnosis={hpaDiagnosis} variant="detail" />
|
|
99
46
|
</Section>
|
|
100
47
|
)}
|
|
101
48
|
|