@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
@@ -4,8 +4,11 @@ import { clsx } from 'clsx'
4
4
  import { AlertBanner, Section, PropertyList, Property, ConditionsSection, PodTemplateSection, type ConditionTone } from '../../ui/drawer-components'
5
5
  import { Tooltip } from '../../ui/Tooltip'
6
6
  import { ConfirmDialog } from '../../ui/ConfirmDialog'
7
- import { formatAge, getRolloutStep } from '../resource-utils'
8
- import { BADGE_INACTIVE } from '../../../utils/badge-colors'
7
+ import { formatAge, getRolloutStep, analysisPhaseLevel, healthColors } from '../resource-utils'
8
+ import { CanaryStepTimeline, BlueGreenTimeline } from './rollout/CanaryStepTimeline'
9
+ import { ReplicaSetProgression } from './rollout/ReplicaSetProgression'
10
+ import { LookupFailureNote } from './LookupFailureNote'
11
+ import type { WorkloadRevision, WorkloadPodInfo } from '../../../types'
9
12
 
10
13
  export type RolloutAction = 'abort' | 'retry' | 'promote' | 'promote-full' | 'skip-step'
11
14
 
@@ -38,12 +41,39 @@ export interface RolloutActionSpec {
38
41
  confirm?: RolloutActionConfirm
39
42
  }
40
43
 
44
+ export interface AnalysisRunHistoryEntry {
45
+ name: string
46
+ phase: string
47
+ message?: string
48
+ trigger?: string
49
+ stepIndex?: number
50
+ createdAt: string
51
+ metricsTotal: number
52
+ metricsPassing: number
53
+ metricsNotPassing: number
54
+ }
55
+
41
56
  interface RolloutRendererProps {
42
57
  data: any
43
58
  onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
44
59
  capabilities?: RolloutCapabilities
45
60
  onAction?: (action: RolloutAction) => void
46
61
  pendingAction?: RolloutAction | null
62
+ // Host-fetched — full AnalysisRun history (not just the 4 "active" slots
63
+ // rolloutAnalysisRuns already covers). Omitted entirely, the History
64
+ // subsection just doesn't render; a library consumer that skips the fetch
65
+ // loses nothing else.
66
+ analysisRunHistory?: AnalysisRunHistoryEntry[]
67
+ // The history fetch's error, so a refusal can be told apart from "no runs".
68
+ // Without it a caller lacking `list analysisruns` sees the section vanish and
69
+ // never learns the history exists.
70
+ analysisRunHistoryError?: unknown
71
+ // Host-fetched — the Rollout's ReplicaSet revision history + its pods, via
72
+ // the generic workload hooks (useWorkloadRevisions/useWorkloadPods) shared
73
+ // with every other workload kind, joined client-side by the host. Read-only
74
+ // here; the rollback action stays exclusively in the existing history dialog.
75
+ revisions?: WorkloadRevision[]
76
+ pods?: WorkloadPodInfo[]
47
77
  }
48
78
 
49
79
  // Has the controller reconciled the template as it stands? A rollback changes the
@@ -281,6 +311,94 @@ export function canaryStepLabel(step: any): string {
281
311
  return key ? `Unrecognized step: ${key}` : 'Unknown step'
282
312
  }
283
313
 
314
+ /** AnalysisTemplate/ClusterAnalysisTemplate references on either a canary
315
+ * step's analysis.templates[] array, or a single Experiment spec.analyses[]
316
+ * entry — the Experiment entry carries templateName/clusterScope flat on
317
+ * itself rather than nested under its own templates array, so it's wrapped
318
+ * into a one-element list to share the same mapping below. Both shapes use
319
+ * the SAME field name for the ref (templateName) regardless of scope —
320
+ * clusterScope is a separate boolean on the entry, not a distinct field
321
+ * name, so it's read directly rather than inferred from which field is set. */
322
+ export function canaryStepTemplateRefs(step: any): Array<{ name: string; clusterScoped: boolean }> {
323
+ const templates = step?.analysis?.templates || step?.templates || (step?.templateName ? [step] : [])
324
+ return templates
325
+ .map((t: any) => (t.templateName ? { name: t.templateName, clusterScoped: !!t.clusterScope } : null))
326
+ .filter(Boolean)
327
+ }
328
+
329
+ /** Ordered phase list for a blueGreen Rollout — there's no steps[] array to
330
+ * iterate, so this derives an equivalent sequence from strategy config
331
+ * crossed with live status. */
332
+ export function blueGreenPhases(data: any): Array<{
333
+ label: string
334
+ state: 'completed' | 'current' | 'pending'
335
+ analysis?: { name?: string; status?: string }
336
+ }> {
337
+ const spec = data?.spec?.strategy?.blueGreen || {}
338
+ const status = data?.status || {}
339
+ const bg = status.blueGreen || {}
340
+ // previewSelector is transient — Argo Rollouts clears it once there's no
341
+ // active preview to track (a settled, fully-promoted blueGreen commonly
342
+ // has none), so activeSelector === previewSelector alone under-detects
343
+ // completion for anything but the narrow just-promoted window. currentPodHash
344
+ // is a generic, always-populated status field (used by every strategy, not
345
+ // just blueGreen) naming the newest ReplicaSet's hash — activeSelector
346
+ // matching IT is the more durable "the latest revision is live" signal,
347
+ // true both right after cutover and indefinitely after.
348
+ const promoted = !!bg.activeSelector && (bg.activeSelector === status.currentPodHash || bg.activeSelector === bg.previewSelector)
349
+
350
+ // scaleUpPreviewCheckPoint is only set in a narrow preview-replica-count
351
+ // configuration and is absent on most real blueGreen Rollouts (confirmed
352
+ // live) — pre-promotion analysis existing at all, or the Rollout having
353
+ // reached ANY blueGreen pause, are both proof the preview already scaled
354
+ // up (neither can happen before it does), so either is equally valid
355
+ // "done" evidence even when the checkpoint field itself is never set.
356
+ const previewScaledUp =
357
+ !!bg.scaleUpPreviewCheckPoint || !!bg.prePromotionAnalysisRunStatus || status.phase === 'Paused' || promoted
358
+
359
+ const steps: Array<{ label: string; done: boolean; analysis?: { name?: string; status?: string } }> = [
360
+ { label: 'Preview scaled up', done: previewScaledUp },
361
+ ]
362
+
363
+ if (spec.prePromotionAnalysis) {
364
+ steps.push({
365
+ label: 'Pre-promotion analysis',
366
+ done: bg.prePromotionAnalysisRunStatus?.status === 'Successful' || promoted,
367
+ analysis: bg.prePromotionAnalysisRunStatus,
368
+ })
369
+ }
370
+
371
+ steps.push({
372
+ label:
373
+ spec.autoPromotionEnabled === false
374
+ ? 'Awaiting manual promotion'
375
+ : spec.autoPromotionSeconds !== undefined
376
+ ? `Auto-promote after ${spec.autoPromotionSeconds}s`
377
+ : 'Awaiting promotion',
378
+ done: promoted,
379
+ })
380
+
381
+ steps.push({ label: 'Active cutover', done: promoted })
382
+
383
+ if (spec.postPromotionAnalysis) {
384
+ steps.push({
385
+ label: 'Post-promotion analysis',
386
+ done: bg.postPromotionAnalysisRunStatus?.status === 'Successful',
387
+ analysis: bg.postPromotionAnalysisRunStatus,
388
+ })
389
+ }
390
+
391
+ let currentAssigned = false
392
+ return steps.map((s) => {
393
+ if (s.done) return { label: s.label, state: 'completed' as const, analysis: s.analysis }
394
+ if (!currentAssigned) {
395
+ currentAssigned = true
396
+ return { label: s.label, state: 'current' as const, analysis: s.analysis }
397
+ }
398
+ return { label: s.label, state: 'pending' as const, analysis: s.analysis }
399
+ })
400
+ }
401
+
284
402
  /** Populated analysis slots, in the order the controller runs them. */
285
403
  export function rolloutAnalysisRuns(
286
404
  status: any
@@ -294,23 +412,10 @@ export function rolloutAnalysisRuns(
294
412
  }
295
413
 
296
414
  function analysisStatusClass(status?: string): string {
297
- switch (status) {
298
- case 'Successful':
299
- return 'status-healthy'
300
- case 'Running':
301
- case 'Pending':
302
- return 'status-degraded'
303
- case 'Inconclusive':
304
- return 'status-alert'
305
- case 'Failed':
306
- case 'Error':
307
- return 'status-unhealthy'
308
- default:
309
- return 'status-unknown'
310
- }
415
+ return healthColors[analysisPhaseLevel(status)]
311
416
  }
312
417
 
313
- export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pendingAction }: RolloutRendererProps) {
418
+ export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pendingAction, analysisRunHistory, analysisRunHistoryError, revisions, pods }: RolloutRendererProps) {
314
419
  const [confirming, setConfirming] = useState<RolloutActionSpec | null>(null)
315
420
  const status = data.status || {}
316
421
  const spec = data.spec || {}
@@ -339,6 +444,7 @@ export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pend
339
444
  const analysisRuns = rolloutAnalysisRuns(status)
340
445
  const problems = rolloutProblems(data)
341
446
  const actions = onAction ? rolloutActions(data, capabilities) : []
447
+ const blueGreenPhaseList = blueGreenStrategy ? blueGreenPhases(data) : []
342
448
 
343
449
  // Phase badge color
344
450
  const phaseColor = (() => {
@@ -443,7 +549,27 @@ export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pend
443
549
  <Property label="Canary Service" value={canaryStrategy.canaryService} />
444
550
  <Property label="Stable Service" value={canaryStrategy.stableService} />
445
551
  {status.canary?.currentExperiment && (
446
- <Property label="Running Experiment" value={status.canary.currentExperiment} />
552
+ <Property
553
+ label="Running Experiment"
554
+ value={
555
+ onNavigate ? (
556
+ <button
557
+ onClick={() =>
558
+ onNavigate({
559
+ kind: 'Experiment',
560
+ namespace: data?.metadata?.namespace ?? '',
561
+ name: status.canary.currentExperiment,
562
+ })
563
+ }
564
+ className="font-mono text-xs text-brand hover:underline"
565
+ >
566
+ {status.canary.currentExperiment}
567
+ </button>
568
+ ) : (
569
+ status.canary.currentExperiment
570
+ )
571
+ }
572
+ />
447
573
  )}
448
574
  </>
449
575
  ) : blueGreenStrategy ? (
@@ -584,54 +710,91 @@ export function RolloutRenderer({ data, onNavigate, capabilities, onAction, pend
584
710
  </Section>
585
711
  )}
586
712
 
587
- {/* Canary Steps visual */}
588
713
  {isCanary && steps.length > 0 && (
589
714
  <Section title={`Canary Steps (${steps.length})`} defaultExpanded>
590
- <div className="space-y-1">
591
- {steps.map((step: any, index: number) => {
592
- const isCompleted = currentStepIndex !== undefined && index < currentStepIndex
593
- const isCurrent = currentStepIndex !== undefined && index === currentStepIndex
594
- const isPending = currentStepIndex === undefined || index > currentStepIndex
595
-
596
- const stepLabel = canaryStepLabel(step)
715
+ <CanaryStepTimeline
716
+ steps={steps}
717
+ currentStepIndex={currentStepIndex}
718
+ stepAnalysisStatus={analysisRuns.find((r) => r.label === 'Step analysis')}
719
+ onNavigate={onNavigate}
720
+ namespace={data?.metadata?.namespace}
721
+ />
722
+ </Section>
723
+ )}
597
724
 
598
- return (
599
- <div
600
- key={index}
601
- className={clsx(
602
- 'flex items-center gap-2 px-2 py-1.5 rounded text-sm',
603
- isCurrent && 'bg-blue-500/10 border border-blue-500/30',
604
- isCompleted && 'opacity-80',
605
- isPending && 'opacity-50'
606
- )}
607
- >
608
- {/* Status indicator */}
609
- <span
610
- className={clsx(
611
- 'w-5 h-5 rounded-full flex items-center justify-center text-xs shrink-0',
612
- isCompleted && 'status-green',
613
- isCurrent && 'status-blue',
614
- isPending && BADGE_INACTIVE
615
- )}
616
- >
617
- {isCompleted ? '\u2713' : isCurrent ? '\u25CF' : '\u25CB'}
618
- </span>
725
+ {!isCanary && blueGreenStrategy && blueGreenPhaseList.length > 0 && (
726
+ <Section title="Progression" defaultExpanded>
727
+ <BlueGreenTimeline
728
+ phases={blueGreenPhaseList}
729
+ onNavigate={onNavigate}
730
+ namespace={data?.metadata?.namespace}
731
+ />
732
+ </Section>
733
+ )}
619
734
 
620
- {/* Step index */}
621
- <span className="text-theme-text-tertiary text-xs w-4 shrink-0">{index}</span>
735
+ {(revisions?.length ?? 0) > 0 && (
736
+ <Section title={`ReplicaSets (${revisions!.length})`}>
737
+ <ReplicaSetProgression
738
+ revisions={revisions!}
739
+ pods={pods}
740
+ isRollout
741
+ namespace={data?.metadata?.namespace ?? ''}
742
+ onNavigate={onNavigate}
743
+ />
744
+ </Section>
745
+ )}
622
746
 
623
- {/* Step label */}
747
+ {/* Backward-looking, so it sits below the live progression and stays collapsed */}
748
+ {(analysisRunHistory?.length || analysisRunHistoryError) && (
749
+ <Section
750
+ // Section reads defaultExpanded once, on mount, so a refusal arriving
751
+ // later would leave its note sealed inside a collapsed section that
752
+ // still reads as a healthy list. Keying on the refusal remounts the
753
+ // section so it opens the moment the list stops being trustworthy.
754
+ key={analysisRunHistoryError ? 'unreadable' : 'readable'}
755
+ title={analysisRunHistory?.length ? `AnalysisRun History (${analysisRunHistory.length})` : 'AnalysisRun History'}
756
+ icon={Activity}
757
+ // History is backward-looking, so it stays out of the way until it
758
+ // cannot be read.
759
+ defaultExpanded={!!analysisRunHistoryError}
760
+ >
761
+ <LookupFailureNote
762
+ errors={[analysisRunHistoryError]}
763
+ what="this Rollout’s analysis history"
764
+ incomplete={!!analysisRunHistory?.length}
765
+ />
766
+ <div className="space-y-1">
767
+ {(analysisRunHistory ?? []).map((run) => (
768
+ <div key={run.name} className="flex items-center gap-2 rounded px-2 py-1.5 text-sm">
769
+ <span className={clsx('badge', analysisStatusClass(run.phase))}>{run.phase || 'Unknown'}</span>
770
+ {onNavigate ? (
771
+ <button
772
+ onClick={() =>
773
+ onNavigate({ kind: 'AnalysisRun', namespace: data?.metadata?.namespace ?? '', name: run.name })
774
+ }
775
+ className="font-mono text-xs text-brand hover:underline"
776
+ >
777
+ {run.name}
778
+ </button>
779
+ ) : (
780
+ <span className="font-mono text-xs text-theme-text-secondary">{run.name}</span>
781
+ )}
782
+ {run.trigger && <span className="text-xs text-theme-text-tertiary">{run.trigger}</span>}
783
+ {run.metricsTotal > 0 && (
624
784
  <span
625
785
  className={clsx(
626
- 'text-sm',
627
- isCurrent ? 'text-theme-text-primary font-medium' : 'text-theme-text-secondary'
786
+ 'ml-auto text-xs',
787
+ run.metricsNotPassing > 0 ? 'text-amber-400' : 'text-theme-text-tertiary'
628
788
  )}
629
789
  >
630
- {stepLabel}
790
+ {run.metricsPassing}/{run.metricsTotal} passing
631
791
  </span>
632
- </div>
633
- )
634
- })}
792
+ )}
793
+ <span className={clsx('text-xs text-theme-text-tertiary', run.metricsTotal === 0 && 'ml-auto')}>
794
+ {formatAge(run.createdAt)}
795
+ </span>
796
+ </div>
797
+ ))}
635
798
  </div>
636
799
  </Section>
637
800
  )}
@@ -4,12 +4,15 @@ import { clsx } from 'clsx'
4
4
  import { stringify as yamlStringify } from 'yaml'
5
5
  import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
6
6
  import { ConfirmDialog } from '../../ui/ConfirmDialog'
7
- import type { SecretCertificateInfo, CertificateInfo } from '../../../types'
7
+ import { ReflectorSummary, ReflectorSection, isReflectorMirror, reflectorEditNotice } from './ReflectorSection'
8
+ import type { SecretCertificateInfo, CertificateInfo, Relationships, ResourceRef } from '../../../types'
8
9
  import { pluralize } from '../../../utils/pluralize'
9
10
  import { cleanResourceForYaml } from '../../../utils/yaml'
10
11
 
11
12
  interface SecretRendererProps {
12
13
  data: any
14
+ relationships?: Relationships
15
+ onNavigate?: (ref: ResourceRef) => void
13
16
  certificateInfo?: SecretCertificateInfo
14
17
  resourceData?: any
15
18
  onSaveSecretValue?: (yaml: string) => Promise<void>
@@ -22,7 +25,7 @@ function formatDate(dateStr: string): string {
22
25
  return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
23
26
  }
24
27
 
25
- export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecretValue, isSaving }: SecretRendererProps) {
28
+ export function SecretRenderer({ data, relationships, onNavigate, certificateInfo, resourceData, onSaveSecretValue, isSaving }: SecretRendererProps) {
26
29
  const [revealed, setRevealed] = useState<Set<string>>(new Set())
27
30
  const [copied, setCopied] = useState<string | null>(null)
28
31
  const [editingKey, setEditingKey] = useState<string | null>(null)
@@ -137,6 +140,8 @@ export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecr
137
140
  />
138
141
  )}
139
142
 
143
+ <ReflectorSummary data={data} reflection={relationships?.reflection} onNavigate={onNavigate} />
144
+
140
145
  {/* Certificate info section */}
141
146
  {certs && certs.length > 0 && (
142
147
  <>
@@ -260,6 +265,8 @@ export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecr
260
265
  </div>
261
266
  </Section>
262
267
 
268
+ <ReflectorSection data={data} reflection={relationships?.reflection} onNavigate={onNavigate} />
269
+
263
270
  {editingKey && (
264
271
  <ConfirmDialog
265
272
  open={showSaveConfirm}
@@ -267,7 +274,7 @@ export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecr
267
274
  onConfirm={() => handleSave(editingKey, editValue)}
268
275
  title="Update Secret"
269
276
  message={`Update key "${editingKey}" in secret "${data.metadata?.name || 'unknown'}"?`}
270
- details="This will modify the secret value in the cluster immediately."
277
+ details={isReflectorMirror(data) ? `This will modify the secret value immediately. ${reflectorEditNotice}` : "This will modify the secret value in the cluster immediately."}
271
278
  confirmLabel="Update"
272
279
  variant="warning"
273
280
  isLoading={isSaving}
@@ -6,11 +6,13 @@ import type { PolicyResourceResponse } from '../../../types/policy'
6
6
  import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
7
7
  import { DialogPortal } from '../../ui/DialogPortal'
8
8
  import { Tooltip } from '../../ui/Tooltip'
9
- import { Badge, type BadgeSeverity } from '../../ui/Badge'
9
+ import { Badge } from '../../ui/Badge'
10
10
  import type { RBACSubjectResponse, RBACPolicyRule, ResourceRef, HPADiagnosis, WorkloadPodInfo } from '../../../types'
11
11
  import { detectBlastRadius, rulePermissivenessScore } from '../../../utils/rbac-blast-radius'
12
12
  import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
13
- import { hpaStateLabel, hpaStateLevel } from '../resource-utils-hpa'
13
+ import { NamespaceLimitRangeLink } from './LimitRangeRenderer'
14
+ import { hpaStateLabel } from '../resource-utils-hpa'
15
+ import { hpaBadgeSeverity } from '../HPADiagnosisSummary'
14
16
  import {
15
17
  rbacVerbBadgeClass,
16
18
  rbacResourceBadgeClass,
@@ -54,6 +56,9 @@ interface WorkloadRendererProps {
54
56
  policyData?: PolicyResourceResponse | null
55
57
  policyLoading?: boolean
56
58
  policyError?: Error | null
59
+ /** Names of the LimitRanges in this workload's namespace. Absent while the
60
+ * lookup is pending or unreadable, which leaves the link unrendered. */
61
+ namespaceLimitRangeNames?: string[] | null
57
62
  }
58
63
 
59
64
  // Extract real problems from workload status (excludes normal rollout progress)
@@ -125,7 +130,7 @@ function compactHPASummary(diagnosis: HPADiagnosis): string {
125
130
  return diagnosis.summary
126
131
  }
127
132
 
128
- export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, workloadPods, onRequestRefresh, rbacData, rbacLoading, rbacError, policyData, policyLoading, policyError }: WorkloadRendererProps) {
133
+ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, workloadPods, onRequestRefresh, rbacData, rbacLoading, rbacError, policyData, policyLoading, policyError, namespaceLimitRangeNames }: WorkloadRendererProps) {
129
134
  const status = data.status || {}
130
135
  const spec = data.spec || {}
131
136
  const metadata = data.metadata || {}
@@ -374,6 +379,12 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
374
379
 
375
380
  <Section title="Pod Template" defaultExpanded={false}>
376
381
  <PodTemplateSection template={spec.template} />
382
+ <NamespaceLimitRangeLink
383
+ namespace={metadata.namespace || ''}
384
+ names={namespaceLimitRangeNames}
385
+ scope="workload"
386
+ onNavigate={onNavigate}
387
+ />
377
388
  </Section>
378
389
 
379
390
  <ConditionsSection conditions={status.conditions} />
@@ -407,29 +418,12 @@ function ScalerDiagnosisRow({ entry }: { entry: ScalerDiagnosis }) {
407
418
  if (!entry.diagnosis) return null
408
419
  return (
409
420
  <div className="rounded border border-theme-border bg-theme-surface px-2 py-1.5 text-xs leading-6 text-theme-text-secondary whitespace-normal break-normal">
410
- <Badge severity={badgeSeverityForHPA(entry.diagnosis)} size="sm" className="mr-1.5 align-middle">{hpaStateLabel(entry.diagnosis.state)}</Badge>
421
+ <Badge severity={hpaBadgeSeverity(entry.diagnosis.state)} size="sm" className="mr-1.5 align-middle">{hpaStateLabel(entry.diagnosis.state)}</Badge>
411
422
  <span className="align-middle whitespace-normal break-normal">{compactHPASummary(entry.diagnosis)}</span>
412
423
  </div>
413
424
  )
414
425
  }
415
426
 
416
- function badgeSeverityForHPA(diagnosis: HPADiagnosis): BadgeSeverity {
417
- switch (hpaStateLevel(diagnosis.state)) {
418
- case 'healthy':
419
- return 'success'
420
- case 'unhealthy':
421
- return 'error'
422
- case 'degraded':
423
- return 'warning'
424
- case 'alert':
425
- return 'alert'
426
- case 'neutral':
427
- return 'info'
428
- default:
429
- return 'neutral'
430
- }
431
- }
432
-
433
427
  // ============================================================================
434
428
  // WORKLOAD PERMISSIONS SECTION
435
429
  // ============================================================================
@@ -80,6 +80,7 @@ export * from './KyvernoPolicyShared'
80
80
  export * from './KyvernoCELPolicyRenderers'
81
81
  export * from './KyvernoExceptionRenderers'
82
82
  export * from './LeaseRenderer'
83
+ export * from './LimitRangeRenderer'
83
84
  export * from './LookupFailureNote'
84
85
  export * from './NetworkPolicyRenderer'
85
86
  export * from './NetworkPolicyDiagram'
@@ -102,6 +103,8 @@ export * from './RoleBindingRenderer'
102
103
  export * from './RoleRenderer'
103
104
  export * from './RolloutRenderer'
104
105
  export * from './AnalysisRunRenderer'
106
+ export * from './AnalysisTemplateRenderer'
107
+ export * from './ExperimentRenderer'
105
108
  export * from './RuntimeClassRenderer'
106
109
  export * from './SbomReportRenderer'
107
110
  export * from './SealedSecretRenderer'