@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.
Files changed (85) hide show
  1. package/package.json +5 -5
  2. package/src/components/audit/AuditCard.tsx +18 -7
  3. package/src/components/audit/AuditCoveragePresentation.test.tsx +45 -0
  4. package/src/components/charts/AreaChart.test.tsx +173 -0
  5. package/src/components/charts/AreaChart.tsx +208 -53
  6. package/src/components/charts/PrometheusChartsView.test.tsx +36 -0
  7. package/src/components/charts/PrometheusChartsView.tsx +205 -76
  8. package/src/components/charts/SeriesLegend.tsx +8 -3
  9. package/src/components/charts/annotations.ts +59 -0
  10. package/src/components/charts/axis.test.ts +48 -0
  11. package/src/components/charts/axis.ts +62 -0
  12. package/src/components/charts/colors.test.ts +27 -0
  13. package/src/components/charts/colors.ts +23 -0
  14. package/src/components/charts/format.test.ts +19 -0
  15. package/src/components/charts/format.ts +21 -1
  16. package/src/components/charts/index.ts +3 -1
  17. package/src/components/charts/nearestSample.test.ts +31 -0
  18. package/src/components/charts/nearestSample.ts +15 -0
  19. package/src/components/charts/types.ts +12 -0
  20. package/src/components/checks/ChecksView.tsx +20 -2
  21. package/src/components/gitops/GitOpsDetailLayout.tsx +13 -0
  22. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +141 -0
  23. package/src/components/gitops/health-provenance.test.ts +54 -0
  24. package/src/components/gitops/health-provenance.ts +24 -0
  25. package/src/components/gitops/index.ts +2 -0
  26. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -21
  27. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +59 -8
  28. package/src/components/gitops/tree/merge.test.ts +34 -0
  29. package/src/components/gitops/tree/merge.ts +7 -0
  30. package/src/components/resources/HPADiagnosisSummary.test.tsx +178 -0
  31. package/src/components/resources/HPADiagnosisSummary.tsx +218 -0
  32. package/src/components/resources/ResourcesView.tsx +67 -2
  33. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  34. package/src/components/resources/index.ts +7 -0
  35. package/src/components/resources/renderers/AnalysisTemplateRenderer.tsx +150 -0
  36. package/src/components/resources/renderers/ConfigMapRenderer.tsx +8 -1
  37. package/src/components/resources/renderers/ExperimentRenderer.tsx +179 -0
  38. package/src/components/resources/renderers/HPARenderer.tsx +4 -57
  39. package/src/components/resources/renderers/LimitRangeRenderer.test.tsx +156 -0
  40. package/src/components/resources/renderers/LimitRangeRenderer.tsx +277 -0
  41. package/src/components/resources/renderers/NamespaceRenderer.tsx +24 -1
  42. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +9 -16
  43. package/src/components/resources/renderers/NetworkPolicyRenderer.test.tsx +75 -0
  44. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +50 -36
  45. package/src/components/resources/renderers/PodRenderer.tsx +11 -0
  46. package/src/components/resources/renderers/ReflectorSection.test.tsx +281 -0
  47. package/src/components/resources/renderers/ReflectorSection.tsx +296 -0
  48. package/src/components/resources/renderers/RolloutRenderer.test.tsx +117 -0
  49. package/src/components/resources/renderers/RolloutRenderer.tsx +219 -56
  50. package/src/components/resources/renderers/SecretRenderer.tsx +10 -3
  51. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -21
  52. package/src/components/resources/renderers/index.ts +3 -0
  53. package/src/components/resources/renderers/rollout/CanaryStepTimeline.tsx +199 -0
  54. package/src/components/resources/renderers/rollout/ReplicaSetProgression.tsx +116 -0
  55. package/src/components/resources/resource-utils.ts +18 -9
  56. package/src/components/shared/CreateResourceDialog.tsx +231 -11
  57. package/src/components/shared/DrainPlanDialog.test.tsx +154 -0
  58. package/src/components/shared/DrainPlanDialog.tsx +297 -0
  59. package/src/components/shared/ResourceActionsBar.tsx +53 -26
  60. package/src/components/shared/ResourceRendererDispatch.test.tsx +45 -0
  61. package/src/components/shared/ResourceRendererDispatch.tsx +27 -6
  62. package/src/components/shared/index.ts +4 -0
  63. package/src/components/topology/K8sResourceNode.test.ts +9 -0
  64. package/src/components/topology/K8sResourceNode.tsx +56 -5
  65. package/src/components/ui/Badge.tsx +5 -1
  66. package/src/components/ui/FetchResult.test.tsx +20 -2
  67. package/src/components/ui/FetchResult.tsx +33 -3
  68. package/src/components/ui/FreshnessControl.tsx +8 -2
  69. package/src/components/ui/index.ts +5 -0
  70. package/src/components/ui/monacoRuntime.ts +14 -0
  71. package/src/components/workload/WorkloadView.tsx +2 -2
  72. package/src/monaco-deep.d.ts +10 -0
  73. package/src/theme/components.css +16 -0
  74. package/src/types/core.ts +38 -11
  75. package/src/types/gitops-insights.ts +23 -0
  76. package/src/types/gitops-tree.ts +29 -0
  77. package/src/utils/index.ts +1 -0
  78. package/src/utils/navigation.test.ts +20 -1
  79. package/src/utils/navigation.ts +25 -0
  80. package/src/utils/network-policy.test.ts +42 -0
  81. package/src/utils/network-policy.ts +59 -0
  82. package/src/utils/reflector.ts +139 -0
  83. package/src/utils/yaml-file-import.test.ts +189 -0
  84. package/src/utils/yaml-file-import.ts +95 -0
  85. package/src/components/logs/JsonLogLine.tsx +0 -93
@@ -0,0 +1,296 @@
1
+ import { useState } from "react";
2
+ import { Copy } from "lucide-react";
3
+ import type { ResourceRef } from "../../../types";
4
+ import { pluralize } from "../../../utils/pluralize";
5
+ import { formatRelativeAgeTime } from "../../../utils/format";
6
+ import {
7
+ hasReflectorDetails,
8
+ parseReflector,
9
+ type Reflection,
10
+ type ReflectorResource,
11
+ } from "../../../utils/reflector";
12
+ export {
13
+ hasReflectorDetails,
14
+ isReflectorMirror,
15
+ reflectorEditNotice,
16
+ } from "../../../utils/reflector";
17
+ import { Badge } from "../../ui/Badge";
18
+ import {
19
+ AlertBanner,
20
+ Property,
21
+ PropertyList,
22
+ ResourceLink,
23
+ Section,
24
+ } from "../../ui/drawer-components";
25
+
26
+ interface ReflectorSectionProps {
27
+ data: ReflectorResource;
28
+ reflection?: Reflection;
29
+ onNavigate?: (ref: ResourceRef) => void;
30
+ }
31
+
32
+ function NamespaceRule({
33
+ patterns,
34
+ selector,
35
+ }: {
36
+ patterns?: string;
37
+ selector?: string;
38
+ }) {
39
+ if (!patterns && !selector) return <>No namespace restriction configured</>;
40
+ return (
41
+ <span className="space-y-1 block">
42
+ {patterns && (
43
+ <span className="block break-all">
44
+ <span className="text-theme-text-tertiary">Name patterns: </span>
45
+ {patterns}
46
+ </span>
47
+ )}
48
+ {patterns && selector && (
49
+ <span className="block text-theme-text-tertiary">or</span>
50
+ )}
51
+ {selector && (
52
+ <span className="block break-all">
53
+ <span className="text-theme-text-tertiary">Label selector: </span>
54
+ {selector}
55
+ </span>
56
+ )}
57
+ </span>
58
+ );
59
+ }
60
+
61
+ export function ReflectorSection({
62
+ data,
63
+ reflection,
64
+ onNavigate,
65
+ }: ReflectorSectionProps) {
66
+ const [showAll, setShowAll] = useState(false);
67
+ if (!hasReflectorDetails(data, reflection)) return null;
68
+ const {
69
+ value,
70
+ hasDeclaration,
71
+ isMirror,
72
+ allowed,
73
+ autoEnabled,
74
+ automatic,
75
+ mirrors,
76
+ sourceSettings,
77
+ copiedVersion,
78
+ sourceVersion,
79
+ warnings,
80
+ } = parseReflector(data, reflection);
81
+
82
+ return (
83
+ <Section
84
+ title={
85
+ mirrors.length
86
+ ? `Reflector (${pluralize(mirrors.length, "visible mirror")})`
87
+ : "Reflector"
88
+ }
89
+ icon={Copy}
90
+ defaultExpanded={warnings.length > 0}
91
+ >
92
+ <div className="space-y-3">
93
+ <PropertyList>
94
+ <Property
95
+ label="Role"
96
+ value={
97
+ <Badge tone="structural">
98
+ {isMirror
99
+ ? automatic === true
100
+ ? "Automatic mirror"
101
+ : automatic === false
102
+ ? "Manual mirror"
103
+ : "Mirror"
104
+ : hasDeclaration
105
+ ? "Invalid source reference"
106
+ : "Source"}
107
+ </Badge>
108
+ }
109
+ />
110
+ {sourceSettings && (
111
+ <>
112
+ <Property
113
+ label="Reflection allowed"
114
+ value={
115
+ allowed === undefined
116
+ ? "Invalid annotation"
117
+ : allowed
118
+ ? "Yes"
119
+ : "No"
120
+ }
121
+ />
122
+ <Property
123
+ label="Allowed namespaces"
124
+ value={
125
+ <NamespaceRule
126
+ patterns={value("reflection-allowed-namespaces")}
127
+ selector={value("reflection-allowed-namespaces-selector")}
128
+ />
129
+ }
130
+ />
131
+ <Property
132
+ label="Automatic creation"
133
+ value={
134
+ autoEnabled === undefined
135
+ ? "Invalid annotation"
136
+ : autoEnabled
137
+ ? "Enabled"
138
+ : "Disabled"
139
+ }
140
+ />
141
+ {(autoEnabled ||
142
+ value("reflection-auto-namespaces") ||
143
+ value("reflection-auto-namespaces-selector")) && (
144
+ <Property
145
+ label="Automatic namespaces"
146
+ value={
147
+ <NamespaceRule
148
+ patterns={value("reflection-auto-namespaces")}
149
+ selector={value("reflection-auto-namespaces-selector")}
150
+ />
151
+ }
152
+ />
153
+ )}
154
+ <p className="text-xs text-theme-text-tertiary">
155
+ Automatic destinations must also satisfy the allowed rule.
156
+ Controller version and settings may further limit destinations.
157
+ </p>
158
+ </>
159
+ )}
160
+ {isMirror && (
161
+ <>
162
+ <Property
163
+ label="Copied source version"
164
+ value={copiedVersion || "No version recorded"}
165
+ />
166
+ <Property
167
+ label="Source version (snapshot)"
168
+ value={sourceVersion}
169
+ />
170
+ <Property
171
+ label="Recorded copy time"
172
+ value={
173
+ value("reflected-at") ? (
174
+ <span title={value("reflected-at")}>
175
+ {formatRelativeAgeTime(value("reflected-at"))}
176
+ </span>
177
+ ) : undefined
178
+ }
179
+ />
180
+ {copiedVersion && sourceVersion && (
181
+ <p className="text-xs text-theme-text-secondary">
182
+ {copiedVersion === sourceVersion
183
+ ? "The recorded version matches the source snapshot."
184
+ : "The recorded version differs from the source snapshot."}{" "}
185
+ Cached versions can lag reconciliation; matching versions do
186
+ not prove matching contents.
187
+ </p>
188
+ )}
189
+ </>
190
+ )}
191
+ </PropertyList>
192
+ {mirrors.length > 0 && (
193
+ <div className="space-y-2">
194
+ <div className="text-sm text-theme-text-secondary">
195
+ Visible mirrors{" "}
196
+ <Badge tone="structural" size="sm">
197
+ {mirrors.length}
198
+ </Badge>
199
+ </div>
200
+ <div className="space-y-1 max-h-60 overflow-y-auto text-sm">
201
+ {(showAll ? mirrors : mirrors.slice(0, 5)).map((ref) => (
202
+ <div key={`${ref.namespace}/${ref.name}`} className="break-all">
203
+ <ResourceLink
204
+ {...ref}
205
+ label={`${ref.namespace}/${ref.name}`}
206
+ onNavigate={onNavigate}
207
+ />
208
+ </div>
209
+ ))}
210
+ </div>
211
+ {mirrors.length > 5 && (
212
+ <button
213
+ className="text-xs text-accent-text hover:underline"
214
+ onClick={() => setShowAll(!showAll)}
215
+ >
216
+ {showAll
217
+ ? "Show fewer"
218
+ : `Show all ${mirrors.length} visible mirrors`}
219
+ </button>
220
+ )}
221
+ <p className="text-xs text-theme-text-tertiary">
222
+ Visible relationships only; other mirrors may exist.
223
+ </p>
224
+ </div>
225
+ )}
226
+ {sourceSettings && mirrors.length === 0 && (
227
+ <p className="text-xs text-theme-text-tertiary">
228
+ No mirrors visible in this view.
229
+ </p>
230
+ )}
231
+ </div>
232
+ </Section>
233
+ );
234
+ }
235
+
236
+ export function ReflectorSummary({
237
+ data,
238
+ reflection,
239
+ onNavigate,
240
+ }: ReflectorSectionProps) {
241
+ if (!hasReflectorDetails(data, reflection)) return null;
242
+ const { isMirror, declaration, source, warnings } = parseReflector(
243
+ data,
244
+ reflection,
245
+ );
246
+ if (!isMirror && warnings.length === 0) return null;
247
+ return (
248
+ <div className="space-y-2">
249
+ {isMirror && (
250
+ <AlertBanner
251
+ variant="info"
252
+ title="Reflector mirror"
253
+ message={
254
+ <>
255
+ <span className="block break-all">
256
+ Source:{" "}
257
+ {source ? (
258
+ <ResourceLink
259
+ {...source}
260
+ label={`${source.namespace}/${source.name}`}
261
+ onNavigate={onNavigate}
262
+ />
263
+ ) : (
264
+ declaration
265
+ )}
266
+ </span>
267
+ {!source && (
268
+ <span className="block">
269
+ Source is not available in this view; its existence and
270
+ version are unverified.
271
+ </span>
272
+ )}
273
+ <span className="block mt-1">
274
+ Edit the source for lasting changes. Local edits may persist
275
+ until the source changes, then be overwritten.
276
+ </span>
277
+ </>
278
+ }
279
+ />
280
+ )}
281
+ {warnings.length > 0 && (
282
+ <AlertBanner
283
+ variant="warning"
284
+ title="Check reflection configuration"
285
+ message={
286
+ <ul className="list-disc pl-4 space-y-1">
287
+ {warnings.map((warning) => (
288
+ <li key={warning}>{warning}</li>
289
+ ))}
290
+ </ul>
291
+ }
292
+ />
293
+ )}
294
+ </div>
295
+ );
296
+ }
@@ -4,9 +4,11 @@ import {
4
4
  RolloutRenderer,
5
5
  rolloutActions,
6
6
  canaryStepLabel,
7
+ canaryStepTemplateRefs,
7
8
  rolloutAnalysisRuns,
8
9
  rolloutConditionTone,
9
10
  rolloutProblems,
11
+ blueGreenPhases,
10
12
  type RolloutAction,
11
13
  type RolloutCapabilities,
12
14
  } from './RolloutRenderer'
@@ -173,6 +175,46 @@ describe('RolloutRenderer step display', () => {
173
175
  })
174
176
  })
175
177
 
178
+ describe('RolloutRenderer analysis history availability', () => {
179
+ const rollout = canaryRollout({ phase: 'Healthy', currentStepIndex: 4 }, 4)
180
+ const forbidden = Object.assign(new Error('forbidden'), { status: 403 })
181
+ const oneRun = [
182
+ { name: 'run-1', phase: 'Successful', createdAt: '2026-01-01T00:00:00Z', metricsTotal: 1, metricsPassing: 1, metricsNotPassing: 0 },
183
+ ]
184
+ // Section keeps collapsed content in the DOM and flattens it to zero height,
185
+ // so "is the text present" says nothing about whether anyone can read it.
186
+ const historySection = (html: string) => html.slice(html.indexOf('AnalysisRun History'))
187
+ const isOpen = (html: string) => historySection(html).includes('grid-template-rows:1fr')
188
+
189
+ it('says the history could not be read instead of rendering nothing', () => {
190
+ const html = renderToString(<RolloutRenderer data={rollout} analysisRunHistoryError={forbidden} />)
191
+ expect(html).toContain('AnalysisRun History')
192
+ expect(html).toContain('permission')
193
+ expect(isOpen(html)).toBe(true)
194
+ })
195
+
196
+ // React Query keeps the last good data when a refetch fails, so a refusal can
197
+ // arrive on top of rows that are already on screen.
198
+ it('opens and calls itself partial when rows are already showing', () => {
199
+ const html = renderToString(
200
+ <RolloutRenderer data={rollout} analysisRunHistoryError={forbidden} analysisRunHistory={oneRun} />
201
+ )
202
+ expect(html).toContain('run-1')
203
+ expect(html).toContain('may be incomplete')
204
+ expect(isOpen(html)).toBe(true)
205
+ })
206
+
207
+ it('leaves a readable history collapsed, since it is backward-looking', () => {
208
+ const html = renderToString(<RolloutRenderer data={rollout} analysisRunHistory={oneRun} />)
209
+ expect(html).toContain('run-1')
210
+ expect(isOpen(html)).toBe(false)
211
+ })
212
+
213
+ it('renders no history section when there is neither history nor an error', () => {
214
+ expect(renderToString(<RolloutRenderer data={rollout} />)).not.toContain('AnalysisRun History')
215
+ })
216
+ })
217
+
176
218
  describe('rolloutProblems', () => {
177
219
  it('reports an aborted rollout once, not once per matching branch', () => {
178
220
  const problems = rolloutProblems(
@@ -403,6 +445,81 @@ describe('canaryStepLabel', () => {
403
445
  })
404
446
  })
405
447
 
448
+ describe('canaryStepTemplateRefs', () => {
449
+ it('reads a canary step analysis.templates[] entry', () => {
450
+ const refs = canaryStepTemplateRefs({ analysis: { templates: [{ templateName: 'success-rate' }] } })
451
+ expect(refs).toEqual([{ name: 'success-rate', clusterScoped: false }])
452
+ })
453
+
454
+ // Real Argo Rollouts schema uses ONE field name (templateName) for both
455
+ // namespaced and cluster-scoped refs, disambiguated by a separate
456
+ // clusterScope boolean — not a distinct clusterTemplateName field.
457
+ it('reads clusterScoped off the clusterScope flag, not a distinct field name', () => {
458
+ const refs = canaryStepTemplateRefs({ analysis: { templates: [{ templateName: 'shared-check', clusterScope: true }] } })
459
+ expect(refs).toEqual([{ name: 'shared-check', clusterScoped: true }])
460
+ })
461
+
462
+ // Experiment spec.analyses[] entries carry templateName/clusterScope flat
463
+ // on the entry itself, not nested under a templates[] array.
464
+ it('reads a flat Experiment spec.analyses[] entry', () => {
465
+ const refs = canaryStepTemplateRefs({ name: 'baseline-check', templateName: 'success-rate', clusterScope: false })
466
+ expect(refs).toEqual([{ name: 'success-rate', clusterScoped: false }])
467
+ })
468
+
469
+ it('reads a flat, cluster-scoped Experiment analysis entry', () => {
470
+ const refs = canaryStepTemplateRefs({ name: 'shared', templateName: 'shared-check', clusterScope: true })
471
+ expect(refs).toEqual([{ name: 'shared-check', clusterScoped: true }])
472
+ })
473
+
474
+ it('returns an empty list for a step with no analysis refs', () => {
475
+ expect(canaryStepTemplateRefs({ setWeight: 25 })).toEqual([])
476
+ expect(canaryStepTemplateRefs(null)).toEqual([])
477
+ })
478
+ })
479
+
480
+ describe('blueGreenPhases', () => {
481
+ // Argo Rollouts clears previewSelector once there's no active preview to
482
+ // track — a settled, fully-promoted blueGreen commonly has none, so
483
+ // activeSelector === previewSelector alone never fires here even though
484
+ // cutover genuinely completed.
485
+ it('treats activeSelector matching currentPodHash as promoted, even with an empty previewSelector', () => {
486
+ const data = {
487
+ spec: { strategy: { blueGreen: {} } },
488
+ status: {
489
+ currentPodHash: 'abc123',
490
+ blueGreen: { activeSelector: 'abc123', previewSelector: '' },
491
+ },
492
+ }
493
+ const phases = blueGreenPhases(data)
494
+ expect(phases.find((p) => p.label === 'Active cutover')?.state).toBe('completed')
495
+ expect(phases.every((p) => p.state !== 'current')).toBe(true)
496
+ })
497
+
498
+ it('still recognizes the narrower activeSelector === previewSelector window', () => {
499
+ const data = {
500
+ spec: { strategy: { blueGreen: {} } },
501
+ status: {
502
+ currentPodHash: 'def456',
503
+ blueGreen: { activeSelector: 'abc123', previewSelector: 'abc123' },
504
+ },
505
+ }
506
+ const phases = blueGreenPhases(data)
507
+ expect(phases.find((p) => p.label === 'Active cutover')?.state).toBe('completed')
508
+ })
509
+
510
+ it('is not promoted before either selector matches anything', () => {
511
+ const data = {
512
+ spec: { strategy: { blueGreen: {} } },
513
+ status: {
514
+ currentPodHash: 'abc123',
515
+ blueGreen: { activeSelector: 'old999', previewSelector: 'abc123' },
516
+ },
517
+ }
518
+ const phases = blueGreenPhases(data)
519
+ expect(phases.find((p) => p.label === 'Active cutover')?.state).not.toBe('completed')
520
+ })
521
+ })
522
+
406
523
  describe('rolloutConditionTone', () => {
407
524
  it('reads Paused=False as the healthy state', () => {
408
525
  expect(rolloutConditionTone({ type: 'Paused', status: 'False' })).toBe('ok')