@skyhook-io/k8s-ui 1.8.10 → 1.8.12

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 (168) hide show
  1. package/package.json +7 -6
  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 +697 -0
  6. package/src/components/applications/ApplicationDetail.tsx +3269 -474
  7. package/src/components/applications/ApplicationsView.tsx +62 -10
  8. package/src/components/applications/applications-view.test.tsx +47 -0
  9. package/src/components/applications/index.ts +1 -1
  10. package/src/components/audit/AuditAlerts.tsx +14 -10
  11. package/src/components/checks/ChecksView.tsx +2 -3
  12. package/src/components/checks/severity.ts +14 -10
  13. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  14. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  15. package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +13 -6
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
  21. package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
  22. package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
  23. package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
  24. package/src/components/gitops/insights/index.ts +1 -0
  25. package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
  26. package/src/components/gitops/insights/insights-helpers.ts +74 -1
  27. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
  28. package/src/components/issues/IssuesView.tsx +91 -52
  29. package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
  30. package/src/components/issues/ResourceIssuesSection.tsx +46 -144
  31. package/src/components/issues/index.ts +3 -0
  32. package/src/components/issues/issue-timing.ts +53 -0
  33. package/src/components/issues/issues.test.ts +102 -1
  34. package/src/components/issues/severity.ts +3 -0
  35. package/src/components/logs/LogCore.tsx +14 -4
  36. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  37. package/src/components/logs/useLogStream.ts +8 -1
  38. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  39. package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
  40. package/src/components/resources/ResourcesSidebar.tsx +46 -46
  41. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  42. package/src/components/resources/ResourcesView.tsx +233 -51
  43. package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
  44. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
  45. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
  46. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
  47. package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
  48. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
  49. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
  50. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  51. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
  53. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
  54. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
  55. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
  56. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
  57. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
  58. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
  59. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  60. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
  61. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  62. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
  63. package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
  64. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
  65. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
  66. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
  67. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
  68. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
  69. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  70. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
  71. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
  72. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
  73. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
  75. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
  76. package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
  77. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  78. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  79. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
  80. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  81. package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
  82. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  83. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
  84. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  85. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  86. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  87. package/src/components/resources/renderers/index.ts +1 -0
  88. package/src/components/resources/resource-utils-argo.test.ts +18 -1
  89. package/src/components/resources/resource-utils-argo.ts +8 -0
  90. package/src/components/resources/resource-utils-capi.ts +1 -1
  91. package/src/components/resources/resource-utils.ts +31 -0
  92. package/src/components/shared/DetailShell.tsx +2 -2
  93. package/src/components/shared/ResourceActionsBar.tsx +142 -111
  94. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  95. package/src/components/shared/UnifiedDiff.tsx +51 -0
  96. package/src/components/shared/index.ts +1 -0
  97. package/src/components/timeline/TimelineList.tsx +375 -354
  98. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  99. package/src/components/timeline/TimelineStrip.tsx +836 -0
  100. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  101. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  102. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  103. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  104. package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
  105. package/src/components/timeline/index.ts +23 -0
  106. package/src/components/timeline/scrubber-math.test.ts +235 -0
  107. package/src/components/timeline/scrubber-math.ts +249 -0
  108. package/src/components/timeline/shared.test.tsx +219 -0
  109. package/src/components/timeline/shared.tsx +232 -447
  110. package/src/components/timeline/timeline-filters.test.ts +196 -0
  111. package/src/components/timeline/timeline-filters.ts +180 -0
  112. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  113. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  114. package/src/components/timeline/timeline-live.test.ts +131 -0
  115. package/src/components/timeline/timeline-live.ts +77 -0
  116. package/src/components/topology/GroupNode.tsx +9 -2
  117. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  118. package/src/components/topology/K8sResourceNode.tsx +387 -243
  119. package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
  120. package/src/components/topology/TopologyControls.tsx +3 -1
  121. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  122. package/src/components/topology/TopologyGraph.tsx +149 -90
  123. package/src/components/topology/TopologyOverlayBar.tsx +30 -0
  124. package/src/components/topology/TopologySearch.tsx +25 -17
  125. package/src/components/topology/index.ts +2 -0
  126. package/src/components/topology/layout.ts +10 -2
  127. package/src/components/topology/topology.css +0 -92
  128. package/src/components/ui/Badge.tsx +6 -1
  129. package/src/components/ui/CodeViewer.tsx +2 -2
  130. package/src/components/ui/Collapse.tsx +59 -0
  131. package/src/components/ui/ConfirmDialog.tsx +7 -3
  132. package/src/components/ui/DialogPortal.tsx +31 -9
  133. package/src/components/ui/Input.tsx +24 -0
  134. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  135. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  136. package/src/components/ui/PaneLoader.tsx +10 -2
  137. package/src/components/ui/ResourceBar.tsx +68 -25
  138. package/src/components/ui/SearchBox.tsx +18 -4
  139. package/src/components/ui/SearchPillInput.tsx +2 -2
  140. package/src/components/ui/SelectMenu.tsx +81 -0
  141. package/src/components/ui/drawer-components.tsx +48 -32
  142. package/src/components/ui/index.ts +6 -0
  143. package/src/components/ui/severity-tone.ts +11 -4
  144. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  145. package/src/components/workload/WorkloadView.tsx +2706 -475
  146. package/src/components/workload/index.ts +1 -1
  147. package/src/theme/tailwind-theme.css +1 -0
  148. package/src/theme/variables.css +2 -1
  149. package/src/types/core.ts +57 -4
  150. package/src/types/gitops-insights.ts +69 -1
  151. package/src/types/gitops.ts +3 -3
  152. package/src/utils/application-history.test.ts +538 -0
  153. package/src/utils/application-history.ts +257 -0
  154. package/src/utils/application-topology.test.ts +198 -0
  155. package/src/utils/application-topology.ts +261 -0
  156. package/src/utils/applications.test.ts +201 -1
  157. package/src/utils/applications.ts +1182 -393
  158. package/src/utils/index.ts +3 -0
  159. package/src/utils/navigation.test.ts +32 -1
  160. package/src/utils/navigation.ts +70 -0
  161. package/src/utils/resource-hierarchy.test.ts +1024 -1
  162. package/src/utils/resource-hierarchy.ts +925 -267
  163. package/src/utils/resource-icons.ts +2 -0
  164. package/src/utils/topology-neighborhood.test.ts +202 -1
  165. package/src/utils/topology-neighborhood.ts +194 -13
  166. package/src/utils/workflow-execution.test.ts +556 -0
  167. package/src/utils/workflow-execution.ts +814 -0
  168. package/src/utils/workload-colors.ts +2 -3
@@ -2,7 +2,7 @@ import { clsx } from 'clsx'
2
2
  import { type ReactNode } from 'react'
3
3
  import { Tooltip } from './Tooltip'
4
4
 
5
- type ColorScheme = 'utilization' | 'count'
5
+ type ColorScheme = 'utilization' | 'count' | 'quiet'
6
6
 
7
7
  interface ResourceBarProps {
8
8
  /** Formatted usage string (e.g., "847m", "2.1Gi", "17") */
@@ -11,50 +11,93 @@ interface ResourceBarProps {
11
11
  total: string
12
12
  /** Usage percentage (0–100) */
13
13
  percent: number
14
- /** Color scheme: "utilization" (green/yellow/red) or "count" (blue/yellow) */
14
+ /** Color scheme:
15
+ * - "utilization": green/yellow/red — node/workload detail surfaces
16
+ * - "count": blue/yellow
17
+ * - "quiet": dashboard-calibrated — brand fill, amber only at genuine
18
+ * scheduling-pressure levels (≥90%), never red. High utilization is a
19
+ * bin-packing goal, not an incident; alarming it trains amber-blindness. */
15
20
  colorScheme?: ColorScheme
16
- /** Optional marker line position (0–100), e.g., request as % of limit */
21
+ /** Optional marker line position (0–100), e.g., requests as % of allocatable.
22
+ * Under "quiet" the marker itself turns amber at ≥95% — a cluster whose
23
+ * requests are at capacity is full to the scheduler even when usage is low. */
17
24
  markerPercent?: number
18
25
  /** Optional tooltip content shown on hover */
19
26
  tooltip?: ReactNode
27
+ /** Layout: "stacked" (default — numbers row above the track) or "inline"
28
+ * (one row: label · track · percent) for dense cards. Inline requires
29
+ * `label` and omits the used/total numbers row — put them in `tooltip`. */
30
+ layout?: 'stacked' | 'inline'
31
+ /** Short leading label for the inline layout (e.g. "CPU", "MEM"). */
32
+ label?: string
20
33
  }
21
34
 
22
35
  function getBarColor(percent: number, scheme: ColorScheme): string {
23
36
  if (scheme === 'count') {
24
37
  return percent > 90 ? 'bg-yellow-500' : 'bg-blue-500'
25
38
  }
39
+ if (scheme === 'quiet') {
40
+ return percent >= 90 ? 'bg-amber-500' : 'bg-emerald-400'
41
+ }
26
42
  if (percent > 85) return 'bg-red-500'
27
43
  if (percent > 60) return 'bg-yellow-500'
28
44
  return 'bg-green-500'
29
45
  }
30
46
 
31
- export function ResourceBar({ used, total, percent, colorScheme = 'utilization', markerPercent, tooltip }: ResourceBarProps) {
32
- const bar = (
33
- <div className="flex flex-col gap-0.5 min-w-0">
34
- <div className="flex items-baseline justify-between gap-1">
35
- <span className="text-xs font-mono text-theme-text-secondary truncate">
36
- {used} / {total}
37
- </span>
38
- <span className="text-[10px] font-mono text-theme-text-tertiary shrink-0">
47
+ function getMarkerColor(markerPercent: number, scheme: ColorScheme): string {
48
+ if (scheme === 'quiet' && markerPercent >= 95) return 'bg-amber-500'
49
+ return 'bg-theme-text-primary/70'
50
+ }
51
+
52
+ export function ResourceBar({
53
+ used,
54
+ total,
55
+ percent,
56
+ colorScheme = 'utilization',
57
+ markerPercent,
58
+ tooltip,
59
+ layout = 'stacked',
60
+ label,
61
+ }: ResourceBarProps) {
62
+ const track = (
63
+ <div className={clsx('relative', layout === 'inline' && 'min-w-0 flex-1')}>
64
+ <div className={clsx('rounded-full border border-theme-border bg-theme-elevated overflow-hidden', layout === 'inline' ? 'h-1' : 'h-1.5')}>
65
+ <div
66
+ className={clsx('h-full rounded-full transition-[width] duration-300 ease-out', getBarColor(percent, colorScheme))}
67
+ style={{ width: `${Math.min(percent, 100)}%` }}
68
+ />
69
+ </div>
70
+ {markerPercent != null && markerPercent > 0 && markerPercent <= 100 && (
71
+ <div
72
+ className={clsx('absolute -top-[1px] h-[calc(100%+2px)] w-[2px]', getMarkerColor(markerPercent, colorScheme))}
73
+ style={{ left: `${markerPercent}%` }}
74
+ />
75
+ )}
76
+ </div>
77
+ )
78
+
79
+ const bar =
80
+ layout === 'inline' ? (
81
+ <div className="flex items-center gap-1.5 min-w-0">
82
+ <span className="w-7 shrink-0 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">{label}</span>
83
+ {track}
84
+ <span className="w-8 shrink-0 text-right text-[10.5px] font-mono tabular-nums text-theme-text-secondary">
39
85
  {Math.round(percent)}%
40
86
  </span>
41
87
  </div>
42
- <div className="relative">
43
- <div className="h-1.5 rounded-full border border-theme-border bg-theme-elevated overflow-hidden">
44
- <div
45
- className={clsx('h-full rounded-full transition-[width] duration-300 ease-out', getBarColor(percent, colorScheme))}
46
- style={{ width: `${Math.min(percent, 100)}%` }}
47
- />
88
+ ) : (
89
+ <div className="flex flex-col gap-0.5 min-w-0">
90
+ <div className="flex items-baseline justify-between gap-1">
91
+ <span className="text-xs font-mono text-theme-text-secondary truncate">
92
+ {used} / {total}
93
+ </span>
94
+ <span className="text-[10px] font-mono text-theme-text-tertiary shrink-0">
95
+ {Math.round(percent)}%
96
+ </span>
48
97
  </div>
49
- {markerPercent != null && markerPercent > 0 && markerPercent <= 100 && (
50
- <div
51
- className="absolute -top-[1px] h-[calc(100%+2px)] w-[2px] bg-theme-text-primary/70"
52
- style={{ left: `${markerPercent}%` }}
53
- />
54
- )}
98
+ {track}
55
99
  </div>
56
- </div>
57
- )
100
+ )
58
101
 
59
102
  if (tooltip) {
60
103
  return (
@@ -1,6 +1,7 @@
1
- import { useRef } from 'react'
1
+ import { useRef, type RefObject } from 'react'
2
2
  import { Search, X } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
+ import { Input } from './Input'
4
5
  import { useRegisterShortcut, type ShortcutScope } from '../../hooks/useKeyboardShortcuts'
5
6
 
6
7
  /** The standard list-view search box: themed input with a `/`-to-focus
@@ -17,6 +18,9 @@ export function SearchBox({
17
18
  className,
18
19
  onEnter,
19
20
  onArrowDown,
21
+ inputRef: externalRef,
22
+ shortcutEnabled = true,
23
+ onBlur,
20
24
  }: {
21
25
  value: string
22
26
  onChange: (value: string) => void
@@ -31,8 +35,17 @@ export function SearchBox({
31
35
  onEnter?: () => void
32
36
  /** ArrowDown in the box — hand focus off to list keyboard navigation. */
33
37
  onArrowDown?: () => void
38
+ /** Externally-owned input ref — lets a collapsible wrapper focus the box
39
+ * after it mounts. Falls back to an internal ref when omitted. */
40
+ inputRef?: RefObject<HTMLInputElement | null>
41
+ /** Set false when a wrapper owns the `/` shortcut, so it isn't registered
42
+ * twice under the same id. */
43
+ shortcutEnabled?: boolean
44
+ /** Blur handler — a collapsible wrapper uses it to fold back when empty. */
45
+ onBlur?: () => void
34
46
  }) {
35
- const inputRef = useRef<HTMLInputElement>(null)
47
+ const internalRef = useRef<HTMLInputElement>(null)
48
+ const inputRef = externalRef ?? internalRef
36
49
 
37
50
  useRegisterShortcut({
38
51
  id: shortcutId,
@@ -40,18 +53,19 @@ export function SearchBox({
40
53
  description: 'Focus search',
41
54
  category: 'Search',
42
55
  scope,
56
+ enabled: shortcutEnabled,
43
57
  handler: () => inputRef.current?.focus(),
44
58
  })
45
59
 
46
60
  return (
47
61
  <div className={clsx('relative', className)}>
48
62
  <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-tertiary" />
49
- <input
63
+ <Input
50
64
  ref={inputRef}
51
- type="text"
52
65
  value={value}
53
66
  placeholder={placeholder}
54
67
  onChange={(e) => onChange(e.target.value)}
68
+ onBlur={onBlur}
55
69
  onKeyDown={(e) => {
56
70
  if (e.key === 'Escape') {
57
71
  inputRef.current?.blur()
@@ -2,6 +2,7 @@ import { useRef, useState, useMemo, useEffect, useCallback } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { X } from 'lucide-react'
4
4
  import { clsx } from 'clsx'
5
+ import { Input } from './Input'
5
6
 
6
7
  // A structured search input: free-text plus removable `key:value` modifier
7
8
  // PILLS (ns:, kind:, label:, …). Typing a complete modifier token and pressing
@@ -236,9 +237,8 @@ export function SearchPillInput({
236
237
  less
237
238
  </button>
238
239
  )}
239
- <input
240
+ <Input
240
241
  ref={inputRef}
241
- type="text"
242
242
  value={text}
243
243
  onChange={(e) => { setDismissed(false); onChange({ text: e.target.value, pills }) }}
244
244
  onKeyDown={handleKeyDown}
@@ -0,0 +1,81 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Check, ChevronDown } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+
5
+ export interface SelectMenuOption {
6
+ value: string
7
+ label: string
8
+ }
9
+
10
+ export function SelectMenu({
11
+ value,
12
+ options,
13
+ onChange,
14
+ ariaLabel,
15
+ className,
16
+ }: {
17
+ value: string
18
+ options: SelectMenuOption[]
19
+ onChange: (value: string) => void
20
+ ariaLabel: string
21
+ className?: string
22
+ }) {
23
+ const [open, setOpen] = useState(false)
24
+ const rootRef = useRef<HTMLDivElement>(null)
25
+ const selected = options.find((option) => option.value === value) ?? options[0]
26
+
27
+ useEffect(() => {
28
+ if (!open) return
29
+ const onPointerDown = (event: MouseEvent) => {
30
+ if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
31
+ }
32
+ const onKeyDown = (event: KeyboardEvent) => {
33
+ if (event.key === 'Escape') setOpen(false)
34
+ }
35
+ document.addEventListener('mousedown', onPointerDown)
36
+ document.addEventListener('keydown', onKeyDown)
37
+ return () => {
38
+ document.removeEventListener('mousedown', onPointerDown)
39
+ document.removeEventListener('keydown', onKeyDown)
40
+ }
41
+ }, [open])
42
+
43
+ return (
44
+ <div ref={rootRef} className={clsx('relative', className)}>
45
+ <button
46
+ type="button"
47
+ aria-label={ariaLabel}
48
+ aria-haspopup="listbox"
49
+ aria-expanded={open}
50
+ onClick={() => setOpen((current) => !current)}
51
+ className="flex h-8 w-full items-center justify-between gap-2 rounded-md border border-theme-border bg-theme-elevated px-2.5 text-xs text-theme-text-primary transition-colors hover:bg-theme-hover"
52
+ >
53
+ <span className="truncate">{selected?.label}</span>
54
+ <ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', open && 'rotate-180')} />
55
+ </button>
56
+ {open && (
57
+ <div role="listbox" className="absolute right-0 top-full z-50 mt-1 min-w-full overflow-hidden rounded-md border border-theme-border bg-theme-surface py-1 shadow-theme-lg">
58
+ {options.map((option) => {
59
+ const active = option.value === value
60
+ return (
61
+ <button
62
+ key={option.value}
63
+ type="button"
64
+ role="option"
65
+ aria-selected={active}
66
+ onClick={() => {
67
+ onChange(option.value)
68
+ setOpen(false)
69
+ }}
70
+ className="flex w-full items-center gap-2 whitespace-nowrap px-2.5 py-1.5 text-left text-xs text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary"
71
+ >
72
+ <Check className={clsx('h-3.5 w-3.5 shrink-0 text-accent', !active && 'opacity-0')} />
73
+ {option.label}
74
+ </button>
75
+ )
76
+ })}
77
+ </div>
78
+ )}
79
+ </div>
80
+ )
81
+ }
@@ -1,4 +1,4 @@
1
- import { useState, createContext, useContext } from 'react'
1
+ import { createContext, useContext, useState } from 'react'
2
2
  import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import { formatAge, formatDuration, formatResources } from '../resources/resource-utils'
@@ -410,7 +410,7 @@ export function AlertBanner({ variant, icon, title, message, items, children }:
410
410
  {hasBody ? (
411
411
  <div className="flex-1 min-w-0">
412
412
  <div className={clsx('text-sm font-medium', colors.title, items && 'mb-1')}>{title}</div>
413
- {message && <div className={clsx('text-xs mt-1 break-all', colors.message)}>{message}</div>}
413
+ {message && <div className={clsx('text-xs mt-1 break-words', colors.message)}>{message}</div>}
414
414
  {items && items.length > 0 && (
415
415
  <ul className={clsx('text-xs space-y-1', colors.list)}>
416
416
  {items.map((item, i) => (
@@ -446,11 +446,14 @@ export interface Problem {
446
446
  }
447
447
 
448
448
  /** Displays a list of problem alerts (warnings and errors) */
449
- // True when the resource detail is already rendering a dedicated, authoritative
450
- // "Operational Issues" section (the Issues pipeline — richer cause/action). Only
451
- // renderers whose problems the pipeline COMPREHENSIVELY covers read this and drop
452
- // their own problem array so the same failure isn't shown twice — today that's
453
- // PodRenderer and WorkloadRenderer (workload + pod runtime). It is deliberately
449
+ // True when the resource detail is rendering (or still fetching) a dedicated,
450
+ // authoritative "Operational Issues" section (the Issues pipeline — richer
451
+ // cause/action). Renderers whose problems the pipeline COMPREHENSIVELY covers read
452
+ // this and drop their own condition banner so the same failure isn't shown twice.
453
+ // It stays true WHILE the live-issues fetch is pending: a banner the pipeline will
454
+ // cover would otherwise flash on first paint before the issues arrive and hide it;
455
+ // once the fetch settles with no issues (e.g. a CRD the pipeline doesn't watch),
456
+ // it flips false and the renderer's banner appears cleanly. It is deliberately
454
457
  // NOT wired into ProblemAlerts: that component is used only by GitOps renderers
455
458
  // (Argo/Flux), whose Degraded/OutOfSync/revision-mismatch banners the pipeline
456
459
  // does not fully emit (e.g. OutOfSync only for automated Argo apps) — suppressing
@@ -532,9 +535,11 @@ function ContainerResourcesLine({ resources }: { resources: any }) {
532
535
  const effective = getEffectiveResources(resources)
533
536
  if (Object.keys(effective).length === 0) return null
534
537
  return (
535
- <div className="text-xs text-theme-text-tertiary mt-1" title="Effective requests (requests, falling back to limits)">
536
- Resources: {formatResources(effective)}
537
- </div>
538
+ <Tooltip content="Effective requests (requests, falling back to limits)" delay={150}>
539
+ <div className="text-xs text-theme-text-tertiary mt-1">
540
+ Resources: {formatResources(effective)}
541
+ </div>
542
+ </Tooltip>
538
543
  )
539
544
  }
540
545
 
@@ -551,11 +556,15 @@ export function PodTemplateSection({ template }: { template: any }) {
551
556
  {initContainers.map((c: any) => (
552
557
  <div key={c.name} className="card-inner text-sm border-l-2 border-yellow-500/40">
553
558
  <div className="font-medium text-theme-text-primary">{c.name}</div>
554
- <div className="text-xs text-theme-text-secondary truncate" title={c.image}>{c.image}</div>
559
+ <Tooltip content={c.image} delay={300} wrapperClassName="block min-w-0">
560
+ <div className="text-xs text-theme-text-secondary truncate">{c.image}</div>
561
+ </Tooltip>
555
562
  {(c.command || c.args) && (
556
- <div className="text-xs text-theme-text-tertiary font-mono mt-1 truncate" title={[...(c.command || []), ...(c.args || [])].join(' ')}>
557
- $ {[...(c.command || []), ...(c.args || [])].join(' ')}
558
- </div>
563
+ <Tooltip content={[...(c.command || []), ...(c.args || [])].join(' ')} delay={300} wrapperClassName="block min-w-0">
564
+ <div className="text-xs text-theme-text-tertiary font-mono mt-1 truncate">
565
+ $ {[...(c.command || []), ...(c.args || [])].join(' ')}
566
+ </div>
567
+ </Tooltip>
559
568
  )}
560
569
  <ContainerResourcesLine resources={c.resources} />
561
570
  </div>
@@ -566,7 +575,9 @@ export function PodTemplateSection({ template }: { template: any }) {
566
575
  {containers.map((c: any) => (
567
576
  <div key={c.name} className="card-inner text-sm">
568
577
  <div className="font-medium text-theme-text-primary">{c.name}</div>
569
- <div className="text-xs text-theme-text-secondary truncate" title={c.image}>{c.image}</div>
578
+ <Tooltip content={c.image} delay={300} wrapperClassName="block min-w-0">
579
+ <div className="text-xs text-theme-text-secondary truncate">{c.image}</div>
580
+ </Tooltip>
570
581
  {c.ports && (
571
582
  <div className="text-xs text-theme-text-tertiary mt-1">
572
583
  Ports: {c.ports.map((p: any) => `${p.name ? `${p.name}: ` : ''}${p.containerPort}/${p.protocol || 'TCP'}`).join(', ')}
@@ -786,6 +797,7 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
786
797
  (relationships.configRefs && relationships.configRefs.length > 0) ||
787
798
  (relationships.consumers && relationships.consumers.length > 0) ||
788
799
  (relationships.scalers && relationships.scalers.length > 0) ||
800
+ (relationships.storageRefs && relationships.storageRefs.length > 0) ||
789
801
  (relationships.pdbs && relationships.pdbs.length > 0) ||
790
802
  (relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
791
803
  (relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
@@ -829,6 +841,9 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
829
841
  {relationships.scalers && relationships.scalers.length > 0 && (
830
842
  <RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
831
843
  )}
844
+ {relationships.storageRefs && relationships.storageRefs.length > 0 && (
845
+ <RelationshipGroup label="Storage" refs={dedupeRefs(relationships.storageRefs)} onNavigate={onNavigate} />
846
+ )}
832
847
  {relationships.pdbs && relationships.pdbs.length > 0 && (
833
848
  <RelationshipGroup label="Disruption Budget" refs={dedupeRefs(relationships.pdbs)} onNavigate={onNavigate} />
834
849
  )}
@@ -905,30 +920,31 @@ export function ResourceRefBadge({ resourceRef, onClick, wrapAtSeparator }: Reso
905
920
  const layoutClass = wrapAtSeparator
906
921
  ? 'badge max-w-full min-w-0 flex-wrap items-center whitespace-normal text-left leading-tight'
907
922
  : 'badge'
923
+ const tooltip = `${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`
908
924
 
909
925
  if (onClick) {
910
926
  return (
911
- <button
912
- onClick={() => onClick(resourceRef)}
913
- className={clsx(
914
- layoutClass,
915
- 'hover:brightness-[0.92] dark:hover:brightness-125 transition-[filter]',
916
- kindClass
917
- )}
918
- title={`${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`}
919
- >
920
- {content}
921
- </button>
927
+ <Tooltip content={tooltip} delay={150}>
928
+ <button
929
+ onClick={() => onClick(resourceRef)}
930
+ className={clsx(
931
+ layoutClass,
932
+ 'hover:brightness-[0.92] dark:hover:brightness-125 transition-[filter]',
933
+ kindClass
934
+ )}
935
+ >
936
+ {content}
937
+ </button>
938
+ </Tooltip>
922
939
  )
923
940
  }
924
941
 
925
942
  return (
926
- <span
927
- className={clsx(layoutClass, kindClass)}
928
- title={`${resourceRef.kind}: ${resourceRef.namespace}/${resourceRef.name}`}
929
- >
930
- {content}
931
- </span>
943
+ <Tooltip content={tooltip} delay={300}>
944
+ <span className={clsx(layoutClass, kindClass)}>
945
+ {content}
946
+ </span>
947
+ </Tooltip>
932
948
  )
933
949
  }
934
950
 
@@ -10,6 +10,9 @@ export type { EmptyStateTone, EmptyStateVariant } from './EmptyState'
10
10
  export { RestrictedState } from './RestrictedState'
11
11
  export { FetchResult } from './FetchResult'
12
12
  export { SearchBox } from './SearchBox'
13
+ export { SelectMenu } from './SelectMenu'
14
+ export type { SelectMenuOption } from './SelectMenu'
15
+ export { Input } from './Input'
13
16
  export { FilterPill } from './FilterPill'
14
17
  export type { FilterPillTone } from './FilterPill'
15
18
  export { StatusDot, mapHealthToTone } from './status-tone'
@@ -19,6 +22,7 @@ export { ConfirmDialog } from './ConfirmDialog'
19
22
  export { HealthRing } from './HealthRing'
20
23
  export { MetricsChart, MetricsSparkline } from './MetricsChart'
21
24
  export * from './drawer-components'
25
+ export { Collapse, CollapseChevron } from './Collapse'
22
26
  export { ResourceBar } from './ResourceBar'
23
27
  export { ForceDeleteConfirmDialog } from './ForceDeleteConfirmDialog'
24
28
  export { ToastProvider, useToast, showApiError, showApiSuccess } from './Toast'
@@ -40,3 +44,5 @@ export { SearchPillInput } from './SearchPillInput'
40
44
  export type { SearchModifier, SearchPillInputProps } from './SearchPillInput'
41
45
  export { CardSection, CardBody, TerminalBlock, renderProse, NEUTRAL_CHIP_CLASS, KIND_CHIP_CLASS } from './CardSection'
42
46
  export type { CardSectionTone } from './CardSection'
47
+ export { MultiSelectPicker } from './MultiSelectPicker'
48
+ export type { MultiSelectPickerProps } from './MultiSelectPicker'
@@ -7,13 +7,16 @@
7
7
  // Literal class strings (no template interpolation) so Tailwind's scanner sees
8
8
  // every utility.
9
9
 
10
- export type SeverityTone = 'red' | 'orange' | 'amber' | 'slate'
10
+ export type SeverityTone = 'red' | 'orange' | 'amber' | 'yellow' | 'slate'
11
11
 
12
- // Text color — leading severity glyph + emphasis text.
12
+ // Text color — leading severity glyph + emphasis text. Yellow takes the darker
13
+ // -700 step in light mode: its low luminance is illegible at -600 on a light
14
+ // surface, where red/orange/amber still clear.
13
15
  export const TONE_TEXT_CLASS: Record<SeverityTone, string> = {
14
16
  red: 'text-red-600 dark:text-red-400',
15
17
  orange: 'text-orange-600 dark:text-orange-400',
16
18
  amber: 'text-amber-600 dark:text-amber-400',
19
+ yellow: 'text-yellow-700 dark:text-yellow-400',
17
20
  slate: 'text-slate-500 dark:text-slate-400',
18
21
  }
19
22
 
@@ -22,6 +25,7 @@ export const TONE_FILL_CLASS: Record<SeverityTone, string> = {
22
25
  red: 'bg-red-500',
23
26
  orange: 'bg-orange-500',
24
27
  amber: 'bg-amber-500',
28
+ yellow: 'bg-yellow-500',
25
29
  slate: 'bg-slate-400',
26
30
  }
27
31
 
@@ -31,17 +35,19 @@ export const TONE_RAIL_CLASS: Record<SeverityTone, string> = {
31
35
  red: 'border-l-red-500 hover:bg-red-50/40 dark:hover:bg-red-950/20',
32
36
  orange: 'border-l-orange-500 hover:bg-orange-50/40 dark:hover:bg-orange-950/20',
33
37
  amber: 'border-l-amber-500 hover:bg-amber-50/30 dark:hover:bg-amber-950/15',
38
+ yellow: 'border-l-yellow-500 hover:bg-yellow-50/30 dark:hover:bg-yellow-950/15',
34
39
  slate: 'border-l-slate-300 dark:border-l-slate-600 hover:bg-theme-hover/40',
35
40
  }
36
41
 
37
42
  // Solid severity pill — the loud focus signal on an expanded card's header band
38
43
  // (collapsed cards keep the soft BADGE pill so a long queue stays calm).
39
- // The warm mid-tones (orange/amber) take dark text: white on orange-600/amber-500
40
- // sits at ~3.5:1/~2.1:1, below WCAG for the 12px pill text.
44
+ // The warm mid-tones (orange/amber/yellow) take dark text: white on them sits
45
+ // below WCAG for the 12px pill text.
41
46
  export const TONE_SOLID_CLASS: Record<SeverityTone, string> = {
42
47
  red: 'bg-red-600 text-white',
43
48
  orange: 'bg-orange-500 text-orange-950',
44
49
  amber: 'bg-amber-400 text-amber-950',
50
+ yellow: 'bg-yellow-400 text-yellow-950',
45
51
  slate: 'bg-slate-500 text-white dark:bg-slate-600',
46
52
  }
47
53
 
@@ -52,5 +58,6 @@ export const TONE_HEADER_BAND_CLASS: Record<SeverityTone, string> = {
52
58
  red: 'border-l-red-500 bg-red-50 dark:bg-red-950/40',
53
59
  orange: 'border-l-orange-500 bg-orange-50 dark:bg-orange-950/30',
54
60
  amber: 'border-l-amber-500 bg-amber-50 dark:bg-amber-950/30',
61
+ yellow: 'border-l-yellow-500 bg-yellow-50 dark:bg-yellow-950/30',
55
62
  slate: 'border-l-slate-300 dark:border-l-slate-600 bg-slate-50 dark:bg-slate-900/40',
56
63
  }
@@ -32,6 +32,9 @@ interface ResourceDetailDrawerProps {
32
32
  headerHeight?: number
33
33
  /** Left offset to exclude (e.g. sidebar width) so expanded mode doesn't cover the sidebar (default: 0) */
34
34
  leftOffset?: number
35
+ /** Right inset in px to leave clear (e.g. a docked side panel) so the drawer sits
36
+ * beside it instead of under it. Shared "right-side surface" contract (default: 0). */
37
+ rightInset?: number
35
38
  /** Render the content inside the drawer */
36
39
  children: (props: {
37
40
  resource: SelectedResource
@@ -81,7 +84,7 @@ function usePrefersReducedMotion(): boolean {
81
84
  return reduced
82
85
  }
83
86
 
84
- export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab, isOpen = true, expanded, onCollapse, onExpand, canCollapseToDrawer = true, onNavigateToResource, headerHeight: headerHeightProp, leftOffset = 0, children }: ResourceDetailDrawerProps) {
87
+ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab, isOpen = true, expanded, onCollapse, onExpand, canCollapseToDrawer = true, onNavigateToResource, headerHeight: headerHeightProp, leftOffset = 0, rightInset = 0, children }: ResourceDetailDrawerProps) {
85
88
  const [drawerWidth, setDrawerWidth] = useState(() => getDefaultWidth(resource.kind))
86
89
  const [isResizing, setIsResizing] = useState(false)
87
90
  const resizeStartX = useRef(0)
@@ -225,7 +228,7 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
225
228
  // During the pre-mount frames (transitioning but not yet armed) hold the frame
226
229
  // at its START width so the width transition fires only once the incoming
227
230
  // layer has painted; otherwise it snaps to the target.
228
- const fullW = `calc(100% - ${leftOffset}px)`
231
+ const fullW = `calc(100% - ${leftOffset}px - ${rightInset}px)`
229
232
  const startWidth = settledExpanded.current ? fullW : drawerWidth
230
233
  const targetWidth = expanded ? fullW : drawerWidth
231
234
  const containerWidth = transitioning && !crossfadeArmed ? startWidth : targetWidth
@@ -274,6 +277,7 @@ export function ResourceDetailDrawer({ resource, onClose, onNavigate, initialTab
274
277
  style={{
275
278
  width: containerWidth,
276
279
  top: headerHeight,
280
+ right: rightInset, // leave room for a docked side panel (AI), else flush right
277
281
  height: `calc(100% - ${headerHeight}px - ${dockInset}px)`,
278
282
  // Inline so the morph duration/easing is controlled precisely (and stays
279
283
  // in lockstep with the crossfade + JS window). Width only animates when