@skyhook-io/k8s-ui 1.8.9 → 1.8.11

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 (130) hide show
  1. package/package.json +5 -4
  2. package/src/assets/gitops/argocd.png +0 -0
  3. package/src/assets/gitops/flux.svg +7 -0
  4. package/src/components/applications/AppChips.tsx +10 -0
  5. package/src/components/applications/ApplicationDetail.test.tsx +569 -0
  6. package/src/components/applications/ApplicationDetail.tsx +2517 -450
  7. package/src/components/applications/ApplicationsList.tsx +4 -1
  8. package/src/components/applications/ApplicationsView.tsx +101 -18
  9. package/src/components/applications/applications-view.test.tsx +47 -0
  10. package/src/components/applications/index.ts +1 -1
  11. package/src/components/audit/AuditAlerts.tsx +14 -10
  12. package/src/components/checks/ChecksView.tsx +81 -51
  13. package/src/components/checks/severity.ts +39 -45
  14. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  15. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +14 -59
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
  22. package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
  23. package/src/components/gitops/insights/insights-helpers.ts +69 -1
  24. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
  25. package/src/components/issues/IssuesView.tsx +219 -143
  26. package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
  27. package/src/components/issues/ResourceIssuesSection.tsx +28 -128
  28. package/src/components/issues/index.ts +3 -2
  29. package/src/components/issues/issue-timing.ts +53 -0
  30. package/src/components/issues/issues.test.ts +146 -4
  31. package/src/components/issues/severity.ts +29 -32
  32. package/src/components/logs/LogCore.tsx +14 -4
  33. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  34. package/src/components/logs/useLogStream.ts +8 -1
  35. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  36. package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
  37. package/src/components/resources/ResourcesSidebar.tsx +70 -52
  38. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  39. package/src/components/resources/ResourcesView.tsx +243 -53
  40. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  42. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  43. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
  44. package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
  45. package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
  46. package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
  47. package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
  48. package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
  49. package/src/components/resources/renderers/PodRenderer.tsx +81 -70
  50. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  51. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  52. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  53. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  54. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  55. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  56. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  57. package/src/components/resources/renderers/index.ts +1 -0
  58. package/src/components/resources/resource-utils-capi.ts +1 -1
  59. package/src/components/resources/resource-utils.ts +31 -0
  60. package/src/components/shared/DetailShell.tsx +2 -2
  61. package/src/components/shared/ResourceActionsBar.tsx +140 -108
  62. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  63. package/src/components/timeline/TimelineList.tsx +371 -354
  64. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  65. package/src/components/timeline/TimelineStrip.tsx +836 -0
  66. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  67. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  68. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  69. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  70. package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
  71. package/src/components/timeline/index.ts +23 -0
  72. package/src/components/timeline/scrubber-math.test.ts +235 -0
  73. package/src/components/timeline/scrubber-math.ts +249 -0
  74. package/src/components/timeline/shared.test.tsx +219 -0
  75. package/src/components/timeline/shared.tsx +232 -447
  76. package/src/components/timeline/timeline-filters.test.ts +196 -0
  77. package/src/components/timeline/timeline-filters.ts +180 -0
  78. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  79. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  80. package/src/components/timeline/timeline-live.test.ts +131 -0
  81. package/src/components/timeline/timeline-live.ts +77 -0
  82. package/src/components/topology/GroupNode.tsx +9 -2
  83. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  84. package/src/components/topology/K8sResourceNode.tsx +387 -243
  85. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  86. package/src/components/topology/TopologyGraph.tsx +89 -16
  87. package/src/components/topology/TopologySearch.tsx +2 -2
  88. package/src/components/topology/layout.ts +10 -2
  89. package/src/components/topology/topology.css +0 -92
  90. package/src/components/ui/Badge.tsx +5 -0
  91. package/src/components/ui/BoardSkeleton.tsx +47 -0
  92. package/src/components/ui/CardSection.tsx +117 -0
  93. package/src/components/ui/CodeViewer.tsx +2 -2
  94. package/src/components/ui/Collapse.tsx +59 -0
  95. package/src/components/ui/ConfirmDialog.tsx +7 -3
  96. package/src/components/ui/DialogPortal.tsx +31 -9
  97. package/src/components/ui/Input.tsx +24 -0
  98. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  99. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  100. package/src/components/ui/PaneLoader.tsx +10 -2
  101. package/src/components/ui/ResourceBar.tsx +68 -25
  102. package/src/components/ui/SearchBox.tsx +18 -4
  103. package/src/components/ui/SearchPillInput.tsx +2 -2
  104. package/src/components/ui/Toast.tsx +1 -1
  105. package/src/components/ui/drawer-components.tsx +39 -26
  106. package/src/components/ui/index.ts +6 -0
  107. package/src/components/ui/severity-tone.ts +63 -0
  108. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  109. package/src/components/workload/WorkloadView.tsx +2682 -472
  110. package/src/components/workload/index.ts +1 -1
  111. package/src/theme/variables.css +15 -1
  112. package/src/types/core.ts +57 -4
  113. package/src/utils/api-resources.test.ts +54 -0
  114. package/src/utils/api-resources.ts +68 -1
  115. package/src/utils/application-topology.test.ts +198 -0
  116. package/src/utils/application-topology.ts +261 -0
  117. package/src/utils/applications.test.ts +167 -1
  118. package/src/utils/applications.ts +1157 -393
  119. package/src/utils/index.ts +2 -0
  120. package/src/utils/navigation.test.ts +32 -1
  121. package/src/utils/navigation.ts +70 -0
  122. package/src/utils/resource-hierarchy.test.ts +990 -1
  123. package/src/utils/resource-hierarchy.ts +905 -267
  124. package/src/utils/resource-icons.ts +2 -0
  125. package/src/utils/topology-neighborhood.test.ts +202 -1
  126. package/src/utils/topology-neighborhood.ts +194 -13
  127. package/src/utils/workflow-execution.test.ts +556 -0
  128. package/src/utils/workflow-execution.ts +814 -0
  129. package/src/utils/workload-colors.ts +2 -3
  130. package/tsconfig.json +2 -2
@@ -150,6 +150,12 @@ export function ResourceActionsBar({
150
150
  onDrainNode, isDrainingNode,
151
151
  }: ResourceActionsBarProps) {
152
152
  const kind = resource.kind.toLowerCase()
153
+ const canOpenWorkloadLogs = Boolean(
154
+ canViewLogs &&
155
+ !hideLogs &&
156
+ openWorkloadLogs &&
157
+ ['deployments', 'statefulsets', 'daemonsets', 'jobs', 'workflows', 'cronjobs', 'cronworkflows', 'workflowtemplates', 'clusterworkflowtemplates', 'scaledjobs'].includes(kind)
158
+ )
153
159
 
154
160
  // Delete confirmation state
155
161
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
@@ -362,33 +368,21 @@ export function ResourceActionsBar({
362
368
  </button>
363
369
  )}
364
370
  {isRollbackKind && onRollback && (
365
- <button
366
- onClick={() => setShowRevisions(true)}
367
- disabled={!hasMultipleRevisions}
368
- title={hasMultipleRevisions ? 'View revision history and rollback' : 'Only one revision exists'}
369
- className={clsx(
370
- "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors",
371
- hasMultipleRevisions
372
- ? "text-white bg-amber-600 hover:bg-amber-700"
373
- : "text-theme-text-disabled bg-theme-elevated"
374
- )}
375
- >
376
- <History className="w-3.5 h-3.5" />
377
- Rollback
378
- </button>
379
- )}
380
- {canViewLogs && !hideLogs && ['deployments', 'statefulsets', 'daemonsets'].includes(kind) && openWorkloadLogs && (
381
- <button
382
- onClick={() => openWorkloadLogs({
383
- namespace: resource.namespace,
384
- workloadKind: kind,
385
- workloadName: resource.name,
386
- })}
387
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand-muted rounded-lg"
388
- >
389
- <FileText className="w-3.5 h-3.5" />
390
- Logs
391
- </button>
371
+ <Tooltip content={hasMultipleRevisions ? 'View revision history and rollback' : 'Only one revision exists'} delay={150}>
372
+ <button
373
+ onClick={() => setShowRevisions(true)}
374
+ disabled={!hasMultipleRevisions}
375
+ className={clsx(
376
+ "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors",
377
+ hasMultipleRevisions
378
+ ? "text-white bg-amber-600 hover:bg-amber-700"
379
+ : "text-theme-text-disabled bg-theme-elevated"
380
+ )}
381
+ >
382
+ <History className="w-3.5 h-3.5" />
383
+ Rollback
384
+ </button>
385
+ </Tooltip>
392
386
  )}
393
387
  </>
394
388
  )}
@@ -473,8 +467,22 @@ export function ResourceActionsBar({
473
467
  />
474
468
  )}
475
469
 
470
+ {canOpenWorkloadLogs && (
471
+ <button
472
+ onClick={() => openWorkloadLogs?.({
473
+ namespace: resource.namespace,
474
+ workloadKind: kind,
475
+ workloadName: resource.name,
476
+ })}
477
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand-muted rounded-lg"
478
+ >
479
+ <FileText className="w-3.5 h-3.5" />
480
+ Logs
481
+ </button>
482
+ )}
483
+
476
484
  {/* Job logs */}
477
- {kind === 'jobs' && onCopyCommand && (
485
+ {kind === 'jobs' && onCopyCommand && (!canViewLogs || !openWorkloadLogs) && (
478
486
  <button
479
487
  onClick={(e) => onCopyCommand(
480
488
  `kubectl logs job/${resource.name} -n ${resource.namespace} -f`,
@@ -488,26 +496,43 @@ export function ResourceActionsBar({
488
496
  </button>
489
497
  )}
490
498
 
491
- {/* Spacer pushes universal actions to the right */}
492
- <div className="flex-1" />
493
-
494
- {/* Universal actions (right-aligned) */}
495
- {onToggleYaml && (
499
+ {kind === 'workflows' && onCopyCommand && (!canViewLogs || !openWorkloadLogs) && (
496
500
  <button
497
- onClick={onToggleYaml}
498
- className={clsx(
499
- 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors',
500
- showYaml
501
- ? 'btn-brand'
502
- : 'text-theme-text-secondary hover:text-theme-text-primary border border-theme-border-light hover:bg-theme-elevated'
501
+ onClick={(e) => onCopyCommand(
502
+ `argo logs ${resource.name} -n ${resource.namespace}`,
503
+ 'Logs command copied',
504
+ e
503
505
  )}
504
- title="Toggle YAML view"
506
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand-muted rounded-lg"
505
507
  >
506
- <FileCode2 className="w-3.5 h-3.5" />
507
- YAML
508
+ <FileText className="w-3.5 h-3.5" />
509
+ Logs
508
510
  </button>
509
511
  )}
510
512
 
513
+ {/* Spacer pushes universal actions to the right */}
514
+ <div className="flex-1" />
515
+
516
+ {/* Universal actions (right-aligned). The AI/Diagnose action is NOT here — it
517
+ lives in the detail header chrome (see WorkloadView), set apart from these
518
+ imperative ops. */}
519
+ {onToggleYaml && (
520
+ <Tooltip content="Toggle YAML view" delay={150}>
521
+ <button
522
+ onClick={onToggleYaml}
523
+ className={clsx(
524
+ 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors',
525
+ showYaml
526
+ ? 'btn-brand'
527
+ : 'text-theme-text-secondary hover:text-theme-text-primary border border-theme-border-light hover:bg-theme-elevated'
528
+ )}
529
+ >
530
+ <FileCode2 className="w-3.5 h-3.5" />
531
+ YAML
532
+ </button>
533
+ </Tooltip>
534
+ )}
535
+
511
536
  {(onCompareTo || onCompareAcrossClusters) && (
512
537
  <div
513
538
  className="relative"
@@ -669,35 +694,37 @@ function FluxActions({ resource, data, onReconcile, isReconciling, onSyncWithSou
669
694
  return (
670
695
  <>
671
696
  {onReconcile && (
672
- <button
673
- onClick={() => onReconcile({
674
- kind: resource.kind,
675
- namespace: resource.namespace,
676
- name: resource.name,
677
- })}
678
- disabled={isReconciling || isSuspended}
679
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand rounded-lg"
680
- title={isSuspended ? 'Cannot reconcile while suspended' : 'Trigger reconciliation'}
681
- >
682
- <RefreshCw className={`w-3.5 h-3.5 ${isReconciling ? 'animate-spin' : ''}`} />
683
- {isReconciling ? 'Reconciling...' : 'Reconcile'}
684
- </button>
697
+ <Tooltip content={isSuspended ? 'Cannot reconcile while suspended' : 'Trigger reconciliation'} delay={150}>
698
+ <button
699
+ onClick={() => onReconcile({
700
+ kind: resource.kind,
701
+ namespace: resource.namespace,
702
+ name: resource.name,
703
+ })}
704
+ disabled={isReconciling || isSuspended}
705
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand rounded-lg"
706
+ >
707
+ <RefreshCw className={`w-3.5 h-3.5 ${isReconciling ? 'animate-spin' : ''}`} />
708
+ {isReconciling ? 'Reconciling...' : 'Reconcile'}
709
+ </button>
710
+ </Tooltip>
685
711
  )}
686
712
 
687
713
  {hasSource && onSyncWithSource && (
688
- <button
689
- onClick={() => onSyncWithSource({
690
- kind: resource.kind,
691
- namespace: resource.namespace,
692
- name: resource.name,
693
- })}
694
- disabled={isSyncing || isSuspended}
695
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors disabled:opacity-50"
696
- title={isSuspended ? 'Cannot sync while suspended' : 'Fetch latest from source, then reconcile'}
697
- >
698
- <RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
699
- {isSyncing ? 'Syncing...' : 'Sync with Source'}
700
- </button>
714
+ <Tooltip content={isSuspended ? 'Cannot sync while suspended' : 'Fetch latest from source, then reconcile'} delay={150}>
715
+ <button
716
+ onClick={() => onSyncWithSource({
717
+ kind: resource.kind,
718
+ namespace: resource.namespace,
719
+ name: resource.name,
720
+ })}
721
+ disabled={isSyncing || isSuspended}
722
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors disabled:opacity-50"
723
+ >
724
+ <RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
725
+ {isSyncing ? 'Syncing...' : 'Sync with Source'}
726
+ </button>
727
+ </Tooltip>
701
728
  )}
702
729
 
703
730
  {isSuspended ? (
@@ -751,34 +778,36 @@ function ArgoActions({ resource, data, onSync, isSyncing, onRefresh, isRefreshin
751
778
  return (
752
779
  <>
753
780
  {onSync && (
754
- <button
755
- onClick={() => onSync({
756
- namespace: resource.namespace,
757
- name: resource.name,
758
- })}
759
- disabled={isSyncing}
760
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand rounded-lg"
761
- title="Sync application"
762
- >
763
- <RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
764
- {isSyncing ? 'Syncing...' : 'Sync'}
765
- </button>
781
+ <Tooltip content="Sync application" delay={150}>
782
+ <button
783
+ onClick={() => onSync({
784
+ namespace: resource.namespace,
785
+ name: resource.name,
786
+ })}
787
+ disabled={isSyncing}
788
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand rounded-lg"
789
+ >
790
+ <RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
791
+ {isSyncing ? 'Syncing...' : 'Sync'}
792
+ </button>
793
+ </Tooltip>
766
794
  )}
767
795
 
768
796
  {onRefresh && (
769
- <button
770
- onClick={() => onRefresh({
771
- namespace: resource.namespace,
772
- name: resource.name,
773
- hard: false,
774
- })}
775
- disabled={isRefreshing}
776
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand-muted rounded-lg"
777
- title="Refresh (re-read from git)"
778
- >
779
- <RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
780
- {isRefreshing ? 'Refreshing...' : 'Refresh'}
781
- </button>
797
+ <Tooltip content="Refresh (re-read from git)" delay={150}>
798
+ <button
799
+ onClick={() => onRefresh({
800
+ namespace: resource.namespace,
801
+ name: resource.name,
802
+ hard: false,
803
+ })}
804
+ disabled={isRefreshing}
805
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand-muted rounded-lg"
806
+ >
807
+ <RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
808
+ {isRefreshing ? 'Refreshing...' : 'Refresh'}
809
+ </button>
810
+ </Tooltip>
782
811
  )}
783
812
 
784
813
  {hasAutomatedSync ? (
@@ -945,8 +974,10 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
945
974
  <td className="py-2 pr-3 text-theme-text-primary font-mono">
946
975
  #{rev.number}
947
976
  </td>
948
- <td className="py-2 pr-3 text-theme-text-secondary font-mono truncate max-w-[180px]" title={rev.image}>
949
- {getImageTag(rev.image)}
977
+ <td className="py-2 pr-3 text-theme-text-secondary font-mono max-w-[180px]">
978
+ <Tooltip content={rev.image} delay={300} wrapperClassName="min-w-0">
979
+ <span className="truncate">{getImageTag(rev.image)}</span>
980
+ </Tooltip>
950
981
  </td>
951
982
  <td className="py-2 pr-3 text-theme-text-secondary whitespace-nowrap">
952
983
  {formatTimeAgo(rev.createdAt)}
@@ -954,19 +985,20 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
954
985
  <td className="py-2 text-right">
955
986
  <div className="flex items-center gap-1 justify-end">
956
987
  {!rev.isCurrent && rev.template && currentRevision?.template && (
957
- <button
958
- onClick={() => setDiffRevision(diffRevision === rev.number ? null : rev.number)}
959
- className={clsx(
960
- "px-2 py-0.5 text-xs font-medium rounded transition-colors flex items-center gap-1",
961
- diffRevision === rev.number
962
- ? "bg-blue-500/20 text-blue-400 border border-blue-400/50"
963
- : "text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 border border-transparent"
964
- )}
965
- title={`Compare with current revision`}
966
- >
967
- <GitCompare className="w-3 h-3" />
968
- Diff
969
- </button>
988
+ <Tooltip content="Compare with current revision" delay={150}>
989
+ <button
990
+ onClick={() => setDiffRevision(diffRevision === rev.number ? null : rev.number)}
991
+ className={clsx(
992
+ "px-2 py-0.5 text-xs font-medium rounded transition-colors flex items-center gap-1",
993
+ diffRevision === rev.number
994
+ ? "bg-blue-500/20 text-blue-400 border border-blue-400/50"
995
+ : "text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 border border-transparent"
996
+ )}
997
+ >
998
+ <GitCompare className="w-3 h-3" />
999
+ Diff
1000
+ </button>
1001
+ </Tooltip>
970
1002
  )}
971
1003
  {rev.isCurrent ? (
972
1004
  <span className="badge status-healthy">
@@ -11,6 +11,7 @@ import {
11
11
  getPVCStatus,
12
12
  getRolloutStatus,
13
13
  getWorkflowStatus,
14
+ getCronWorkflowStatus,
14
15
  getCertificateStatus,
15
16
  getPVStatus,
16
17
  getClusterIssuerStatus,
@@ -82,6 +83,7 @@ import {
82
83
  SecretRenderer,
83
84
  JobRenderer,
84
85
  CronJobRenderer,
86
+ CronWorkflowRenderer,
85
87
  HPARenderer,
86
88
  NodeRenderer,
87
89
  PVCRenderer,
@@ -316,12 +318,12 @@ export interface RendererOverrides {
316
318
  // Known resource types with specific renderers (module-level to avoid re-allocation)
317
319
  const KNOWN_KINDS = new Set([
318
320
  'pods', 'deployments', 'statefulsets', 'daemonsets', 'replicasets',
319
- 'services', 'endpointslices', 'ingresses', 'configmaps', 'secrets', 'jobs', 'cronjobs',
321
+ 'services', 'endpointslices', 'ingresses', 'configmaps', 'secrets', 'jobs', 'cronjobs', 'cronworkflows',
320
322
  'hpas', 'horizontalpodautoscalers', 'nodes', 'persistentvolumeclaims',
321
323
  'rollouts', 'certificates', 'workflows', 'persistentvolumes',
322
324
  'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
323
325
  'orders', 'challenges',
324
- 'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates',
326
+ 'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates', 'clusterworkflowtemplates',
325
327
  'networkpolicies', 'networkpolicy',
326
328
  'ciliumnetworkpolicies', 'ciliumnetworkpolicy', 'ciliumclusterwidenetworkpolicies', 'ciliumclusterwidenetworkpolicy',
327
329
  'clusternetworkpolicies', 'clusternetworkpolicy',
@@ -406,6 +408,8 @@ interface ResourceRendererDispatchProps {
406
408
  eventsHint?: React.ReactNode
407
409
  /** When provided, sidebar sections (related resources, events, labels, annotations, metadata) are passed to this render prop instead of being rendered inline */
408
410
  renderSidebar?: (sections: React.ReactNode) => React.ReactNode
411
+ /** Additional overview content that belongs in the main content column after the resource renderer sections. */
412
+ mainFooter?: React.ReactNode
409
413
  /** K8s events for the focused resource — always shown (no toggle hides them)
410
414
  * so resource history can't go missing. */
411
415
  events?: TimelineEvent[]
@@ -440,6 +444,7 @@ export function ResourceRendererDispatch({
440
444
  onOpenLogs,
441
445
  eventsHint,
442
446
  renderSidebar,
447
+ mainFooter,
443
448
  events,
444
449
  eventsLoading,
445
450
  updates,
@@ -527,12 +532,13 @@ export function ResourceRendererDispatch({
527
532
  {kind === 'secrets' && <SecretRenderer data={data} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
528
533
  {kind === 'jobs' && <JobRenderer data={data} />}
529
534
  {kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
535
+ {kind === 'cronworkflows' && <CronWorkflowRenderer data={data} onNavigate={onNavigate} />}
530
536
  {(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <HPAComp data={data} onNavigate={onNavigate} hpaDiagnosis={hpaDiagnosis} />}
531
537
  {kind === 'nodes' && <NodeComp data={data} relationships={relationships} />}
532
538
  {kind === 'persistentvolumeclaims' && <PVCComp data={data} onNavigate={onNavigate} />}
533
539
  {kind === 'rollouts' && <RolloutRenderer data={data} />}
534
540
  {kind === 'certificates' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <CertificateRenderer data={data} />}
535
- {kind === 'workflows' && <WorkflowRenderer data={data} />}
541
+ {kind === 'workflows' && <WorkflowRenderer data={data} onNavigate={onNavigate} />}
536
542
  {kind === 'persistentvolumes' && <PersistentVolumeRenderer data={data} onNavigate={onNavigate} />}
537
543
  {kind === 'storageclasses' && <StorageClassRenderer data={data} />}
538
544
  {kind === 'certificaterequests' && <CertificateRequestRenderer data={data} />}
@@ -547,7 +553,7 @@ export function ResourceRendererDispatch({
547
553
  {kind === 'tcproutes' && <SimpleRouteRenderer data={data} kind="TCPRoute" onNavigate={onNavigate} />}
548
554
  {kind === 'tlsroutes' && <SimpleRouteRenderer data={data} kind="TLSRoute" onNavigate={onNavigate} />}
549
555
  {kind === 'sealedsecrets' && <SealedSecretRenderer data={data} onNavigate={onNavigate} />}
550
- {kind === 'workflowtemplates' && <WorkflowTemplateRenderer data={data} />}
556
+ {(kind === 'workflowtemplates' || kind === 'clusterworkflowtemplates') && <WorkflowTemplateRenderer data={data} />}
551
557
  {(kind === 'networkpolicies' || kind === 'networkpolicy') && <NetworkPolicyRenderer data={data} />}
552
558
  {(kind === 'ciliumnetworkpolicies' || kind === 'ciliumnetworkpolicy' || kind === 'ciliumclusterwidenetworkpolicies' || kind === 'ciliumclusterwidenetworkpolicy') && <CiliumNetworkPolicyRenderer data={data} />}
553
559
  {(kind === 'clusternetworkpolicies' || kind === 'clusternetworkpolicy') && <ClusterNetworkPolicyRenderer data={data} />}
@@ -712,6 +718,7 @@ export function ResourceRendererDispatch({
712
718
  {!renderSidebar && sidebarContent}
713
719
  </>
714
720
  )}
721
+ {mainFooter}
715
722
  </div>
716
723
  {renderSidebar && sidebarContent && renderSidebar(sidebarContent)}
717
724
  </div>
@@ -722,6 +729,27 @@ export function ResourceRendererDispatch({
722
729
  // RESOURCE STATUS HELPER
723
730
  // ============================================================================
724
731
 
732
+ // Coarse health hint for the per-resource Diagnose entry point: should the action
733
+ // read as an urgent "Diagnose" (resource has a live problem) or a quiet "ask AI"?
734
+ // Derived from the same status the detail badge shows, so the button matches what
735
+ // the user sees. We key off the StatusBadge `level` (the workload/pod status fns
736
+ // return it) — NOT the color string, which varies by helper. 'unknown' for kinds
737
+ // without a level (we don't assert "Diagnose" when we can't tell → quiet variant).
738
+ export type DiagnoseHealthHint = 'problem' | 'healthy' | 'unknown'
739
+ export function diagnoseHealthHint(kind: string, data: any): DiagnoseHealthHint {
740
+ const st = getResourceStatus(kind, data) as { level?: string } | null
741
+ switch (st?.level) {
742
+ case 'unhealthy':
743
+ case 'degraded':
744
+ case 'alert':
745
+ return 'problem'
746
+ case 'healthy':
747
+ return 'healthy'
748
+ default:
749
+ return 'unknown'
750
+ }
751
+ }
752
+
725
753
  export function getResourceStatus(kind: string, data: any): { text: string; color: string } | null {
726
754
  if (!data) return null
727
755
  const k = kind.toLowerCase()
@@ -752,6 +780,7 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
752
780
  if (k === 'persistentvolumeclaims') return getPVCStatus(data)
753
781
  if (k === 'rollouts') return getRolloutStatus(data)
754
782
  if (k === 'workflows') return getWorkflowStatus(data)
783
+ if (k === 'cronworkflows') return getCronWorkflowStatus(data)
755
784
  if (k === 'certificates') {
756
785
  if (data.apiVersion?.includes('networking.internal.knative.dev')) {
757
786
  const status = getKnativeConditionStatus(data)