@skyhook-io/k8s-ui 1.2.4 → 1.3.1

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 (129) hide show
  1. package/package.json +4 -3
  2. package/src/components/dock/BottomDock.tsx +24 -6
  3. package/src/components/dock/DockContext.tsx +9 -1
  4. package/src/components/dock/LocalTerminalTab.tsx +1 -1
  5. package/src/components/dock/NodeTerminalTab.tsx +1 -1
  6. package/src/components/dock/TerminalTab.tsx +2 -2
  7. package/src/components/gitops/GitOpsActions.tsx +12 -11
  8. package/src/components/gitops/GitOpsStatusBadge.tsx +6 -6
  9. package/src/components/logs/LogCore.tsx +8 -8
  10. package/src/components/logs/LogsViewer.tsx +8 -3
  11. package/src/components/logs/WorkloadLogsViewer.tsx +37 -9
  12. package/src/components/logs/useLogStream.ts +20 -0
  13. package/src/components/resources/ResourcesSidebar.tsx +10 -10
  14. package/src/components/resources/ResourcesView.tsx +49 -48
  15. package/src/components/resources/renderers/AlertRenderer.tsx +2 -2
  16. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +2 -2
  17. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -1
  18. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -3
  19. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -3
  20. package/src/components/resources/renderers/ChallengeRenderer.tsx +2 -2
  21. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +3 -3
  22. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +3 -3
  23. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -2
  24. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +1 -1
  25. package/src/components/resources/renderers/EventRenderer.tsx +1 -1
  26. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -2
  27. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +2 -2
  28. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -1
  29. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +5 -5
  30. package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -1
  31. package/src/components/resources/renderers/GatewayRenderer.tsx +3 -3
  32. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  33. package/src/components/resources/renderers/HPARenderer.tsx +1 -1
  34. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +5 -5
  35. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -1
  36. package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -1
  37. package/src/components/resources/renderers/IngressRenderer.tsx +1 -1
  38. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +10 -10
  39. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -1
  40. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +2 -2
  42. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +6 -6
  43. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -5
  44. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +5 -5
  45. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -2
  46. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +4 -4
  47. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -3
  48. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -3
  49. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +5 -5
  50. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +2 -2
  51. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +5 -5
  52. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +4 -4
  53. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +8 -8
  54. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +3 -3
  55. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -1
  56. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +2 -2
  57. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +6 -6
  58. package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -1
  59. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +4 -4
  60. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -6
  61. package/src/components/resources/renderers/NodeRenderer.tsx +5 -5
  62. package/src/components/resources/renderers/OrderRenderer.tsx +3 -3
  63. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -1
  64. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -1
  65. package/src/components/resources/renderers/PodMonitorRenderer.tsx +3 -3
  66. package/src/components/resources/renderers/PodRenderer.tsx +10 -10
  67. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  68. package/src/components/resources/renderers/RoleBindingRenderer.tsx +3 -3
  69. package/src/components/resources/renderers/RoleRenderer.tsx +8 -8
  70. package/src/components/resources/renderers/RolloutRenderer.tsx +1 -1
  71. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +2 -2
  72. package/src/components/resources/renderers/SealedSecretRenderer.tsx +2 -2
  73. package/src/components/resources/renderers/SecretRenderer.tsx +3 -3
  74. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -1
  75. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +3 -3
  76. package/src/components/resources/renderers/ServiceRenderer.tsx +1 -1
  77. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +5 -5
  78. package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -1
  79. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -1
  80. package/src/components/resources/renderers/VPARenderer.tsx +2 -2
  81. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -2
  82. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +6 -6
  83. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +5 -5
  84. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +6 -6
  85. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -1
  86. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +10 -10
  87. package/src/components/resources/renderers/WorkflowRenderer.tsx +2 -2
  88. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +2 -2
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +1 -1
  90. package/src/components/resources/renderers/argo-cells.tsx +3 -3
  91. package/src/components/resources/renderers/certmanager-cells.tsx +6 -6
  92. package/src/components/resources/renderers/cnpg-cells.tsx +5 -5
  93. package/src/components/resources/renderers/eso-cells.tsx +4 -4
  94. package/src/components/resources/renderers/flux-cells.tsx +6 -6
  95. package/src/components/resources/renderers/istio-cells.tsx +9 -9
  96. package/src/components/resources/renderers/karpenter-cells.tsx +3 -3
  97. package/src/components/resources/renderers/keda-cells.tsx +2 -2
  98. package/src/components/resources/renderers/knative-cells.tsx +2 -2
  99. package/src/components/resources/renderers/kyverno-cells.tsx +3 -3
  100. package/src/components/resources/renderers/prometheus-cells.tsx +3 -3
  101. package/src/components/resources/renderers/trivy-cells.tsx +4 -4
  102. package/src/components/resources/renderers/velero-cells.tsx +4 -4
  103. package/src/components/shared/EditableYamlView.tsx +1 -1
  104. package/src/components/shared/ResourceActionsBar.tsx +17 -17
  105. package/src/components/shared/ResourceRendererDispatch.tsx +8 -7
  106. package/src/components/timeline/DiffViewer.tsx +131 -0
  107. package/src/components/timeline/TimelineList.tsx +817 -0
  108. package/src/components/timeline/index.ts +2 -0
  109. package/src/components/topology/GroupNode.tsx +189 -79
  110. package/src/components/topology/K8sResourceNode.tsx +5 -5
  111. package/src/components/topology/TopologyControls.tsx +2 -2
  112. package/src/components/topology/TopologyGraph.tsx +362 -62
  113. package/src/components/topology/layout.ts +288 -92
  114. package/src/components/topology/topology.css +112 -1
  115. package/src/components/ui/Badge.tsx +178 -0
  116. package/src/components/ui/HealthRing.tsx +70 -35
  117. package/src/components/ui/MetricsChart.tsx +2 -2
  118. package/src/components/ui/Toast.tsx +74 -27
  119. package/src/components/ui/drawer-components.tsx +12 -31
  120. package/src/components/workload/ResourceDetailDrawer.tsx +4 -4
  121. package/src/components/workload/WorkloadView.tsx +10 -10
  122. package/src/theme/components.css +195 -0
  123. package/src/theme/tailwind-theme.css +13 -13
  124. package/src/theme/variables.css +46 -29
  125. package/src/types/core.ts +41 -0
  126. package/src/utils/badge-colors.ts +54 -205
  127. package/src/utils/download.ts +18 -3
  128. package/src/utils/log-format.ts +0 -8
  129. package/tsconfig.json +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.2.4",
3
+ "version": "1.3.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -22,6 +22,7 @@
22
22
  "./topology": "./src/components/topology/index.ts",
23
23
  "./theme/variables.css": "./src/theme/variables.css",
24
24
  "./theme/tailwind-theme.css": "./src/theme/tailwind-theme.css",
25
+ "./theme/components.css": "./src/theme/components.css",
25
26
  "./theme": "./src/theme/index.ts",
26
27
  "./topology.css": "./src/components/topology/topology.css"
27
28
  },
@@ -63,8 +64,8 @@
63
64
  "lucide-react": "^0.575.0",
64
65
  "react": "^19.2.4",
65
66
  "react-dom": "^19.2.4",
66
- "typescript": "^5.9.3",
67
+ "typescript": "^6.0.2",
67
68
  "vitest": "^4.0.18",
68
- "yaml": "^2.7.1"
69
+ "yaml": "^2.8.3"
69
70
  }
70
71
  }
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useRef, useEffect, ReactNode } from 'react'
2
2
  import { DURATION_DOCK } from '../../utils/animation'
3
- import { X, ChevronDown, ChevronUp, Terminal, FileText, Trash2, Layers, Maximize2, Minimize2 } from 'lucide-react'
3
+ import { X, ChevronDown, ChevronUp, Terminal, FileText, Trash2, Layers, Maximize2, Minimize2, Activity } from 'lucide-react'
4
4
  import { clsx } from 'clsx'
5
5
  import { useDock, DockTab } from './DockContext'
6
6
  import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
@@ -12,13 +12,25 @@ const MAXIMIZED_TOP_OFFSET = 48
12
12
 
13
13
  interface BottomDockProps {
14
14
  renderTabContent: (tab: DockTab, isActive: boolean) => ReactNode
15
+ /** Optional extra content rendered in the dock header bar (between tabs and action buttons) */
16
+ renderTabHeaderExtra?: (tab: DockTab) => ReactNode
15
17
  /** Offset from the left edge in px — use to avoid overlapping a fixed sidebar */
16
18
  leftOffset?: number
19
+ /** Override the default dock height in px */
20
+ defaultHeight?: number
17
21
  }
18
22
 
19
- export function BottomDock({ renderTabContent, leftOffset = 0 }: BottomDockProps) {
20
- const { tabs, activeTabId, isExpanded, removeTab, setActiveTab, toggleExpanded, closeAll } = useDock()
21
- const [height, setHeight] = useState(DEFAULT_HEIGHT)
23
+ export function BottomDock({ renderTabContent, renderTabHeaderExtra, leftOffset: leftOffsetProp, defaultHeight }: BottomDockProps) {
24
+ const { tabs, activeTabId, isExpanded, leftOffset: leftOffsetCtx, removeTab, setActiveTab, toggleExpanded, closeAll } = useDock()
25
+ const leftOffset = leftOffsetProp ?? leftOffsetCtx
26
+ const [height, setHeight] = useState(defaultHeight ?? DEFAULT_HEIGHT)
27
+ const prevDefaultHeight = useRef(defaultHeight)
28
+ useEffect(() => {
29
+ if (defaultHeight != null && defaultHeight !== prevDefaultHeight.current) {
30
+ setHeight(defaultHeight)
31
+ }
32
+ prevDefaultHeight.current = defaultHeight
33
+ }, [defaultHeight])
22
34
  const [isMaximized, setIsMaximized] = useState(false)
23
35
  const isDragging = useRef(false)
24
36
  const startY = useRef(0)
@@ -91,7 +103,7 @@ export function BottomDock({ renderTabContent, leftOffset = 0 }: BottomDockProps
91
103
  >
92
104
  {isExpanded && !isMaximized && (
93
105
  <div
94
- className="absolute top-0 left-0 right-0 h-1 cursor-ns-resize hover:bg-blue-500/50 transition-colors"
106
+ className="absolute top-0 left-0 right-0 h-1 cursor-ns-resize hover:bg-skyhook-500/50 transition-colors"
95
107
  onMouseDown={handleMouseDown}
96
108
  />
97
109
  )}
@@ -112,6 +124,12 @@ export function BottomDock({ renderTabContent, leftOffset = 0 }: BottomDockProps
112
124
  ))}
113
125
  </div>
114
126
 
127
+ {/* Per-tab extra header content */}
128
+ {renderTabHeaderExtra && activeTabId && (() => {
129
+ const activeTab = tabs.find(t => t.id === activeTabId)
130
+ return activeTab ? renderTabHeaderExtra(activeTab) : null
131
+ })()}
132
+
115
133
  <div className="flex items-center gap-1 ml-2">
116
134
  {tabs.length > 1 && (
117
135
  <button
@@ -175,7 +193,7 @@ function TabButton({
175
193
  onClose: () => void
176
194
  }) {
177
195
  const Icon = tab.type === 'terminal' || tab.type === 'node-terminal' || tab.type === 'local-terminal'
178
- ? Terminal : tab.type === 'workload-logs' ? Layers : FileText
196
+ ? Terminal : tab.type === 'workload-logs' ? Layers : tab.type === 'traffic-flows' ? Activity : FileText
179
197
 
180
198
  return (
181
199
  <div
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'
2
2
 
3
- export type DockTabType = 'terminal' | 'logs' | 'workload-logs' | 'node-terminal' | 'local-terminal'
3
+ export type DockTabType = 'terminal' | 'logs' | 'workload-logs' | 'node-terminal' | 'local-terminal' | 'traffic-flows'
4
4
 
5
5
  export interface DockTab {
6
6
  id: string
@@ -26,11 +26,13 @@ export interface DockContextValue {
26
26
  tabs: DockTab[]
27
27
  activeTabId: string | null
28
28
  isExpanded: boolean
29
+ leftOffset: number
29
30
  addTab: (tab: Omit<DockTab, 'id'>) => string
30
31
  removeTab: (id: string) => void
31
32
  setActiveTab: (id: string) => void
32
33
  toggleExpanded: () => void
33
34
  setExpanded: (expanded: boolean) => void
35
+ setLeftOffset: (offset: number) => void
34
36
  closeAll: () => void
35
37
  }
36
38
 
@@ -42,6 +44,7 @@ export function DockProvider({ children }: { children: ReactNode }) {
42
44
  const [tabs, setTabs] = useState<DockTab[]>([])
43
45
  const [activeTabId, setActiveTabId] = useState<string | null>(null)
44
46
  const [isExpanded, setIsExpanded] = useState(false)
47
+ const [leftOffset, setLeftOffset] = useState(0)
45
48
  // Keep a ref to the latest tabs for deduplication without stale-closure issues
46
49
  const tabsRef = useRef<DockTab[]>(tabs)
47
50
  useEffect(() => { tabsRef.current = tabs }, [tabs])
@@ -62,6 +65,9 @@ export function DockProvider({ children }: { children: ReactNode }) {
62
65
  if (t.type === 'local-terminal') {
63
66
  return false // Allow multiple local terminals
64
67
  }
68
+ if (t.type === 'traffic-flows') {
69
+ return true // Singleton — always reuse existing tab
70
+ }
65
71
  return t.namespace === tabData.namespace &&
66
72
  t.podName === tabData.podName &&
67
73
  t.containerName === tabData.containerName
@@ -117,11 +123,13 @@ export function DockProvider({ children }: { children: ReactNode }) {
117
123
  tabs,
118
124
  activeTabId,
119
125
  isExpanded,
126
+ leftOffset,
120
127
  addTab,
121
128
  removeTab,
122
129
  setActiveTab,
123
130
  toggleExpanded,
124
131
  setExpanded: setIsExpanded,
132
+ setLeftOffset,
125
133
  closeAll,
126
134
  }}>
127
135
  {children}
@@ -227,7 +227,7 @@ export function LocalTerminalTab({
227
227
  <div className="text-xs text-theme-text-disabled mb-3">{error}</div>
228
228
  <button
229
229
  onClick={connect}
230
- className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
230
+ className="flex items-center gap-2 px-3 py-1.5 btn-brand text-xs rounded"
231
231
  >
232
232
  <RefreshCw className="w-3 h-3" />
233
233
  Retry
@@ -101,7 +101,7 @@ export function NodeTerminalTab({
101
101
  <div className="text-xs text-theme-text-tertiary text-center max-w-md break-all">{error}</div>
102
102
  <button
103
103
  onClick={createPod}
104
- className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
104
+ className="flex items-center gap-2 px-3 py-1.5 btn-brand text-xs rounded"
105
105
  >
106
106
  <RefreshCw className="w-3 h-3" />
107
107
  Retry
@@ -293,7 +293,7 @@ export function TerminalTab({
293
293
  <button
294
294
  onClick={handleCreateDebugContainer}
295
295
  disabled={isCreatingDebug}
296
- className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 disabled:opacity-50"
296
+ className="flex items-center gap-2 px-3 py-1.5 btn-brand text-xs rounded"
297
297
  >
298
298
  {isCreatingDebug ? (
299
299
  <><RefreshCw className="w-3 h-3 animate-spin" />Creating debug container...</>
@@ -317,7 +317,7 @@ export function TerminalTab({
317
317
  <div className="text-xs text-theme-text-disabled mb-3">{error}</div>
318
318
  <button
319
319
  onClick={connect}
320
- className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
320
+ className="flex items-center gap-2 px-3 py-1.5 btn-brand text-xs rounded"
321
321
  >
322
322
  <RefreshCw className="w-3 h-3" />
323
323
  Retry
@@ -1,5 +1,6 @@
1
1
  import { RefreshCw, Pause, Play, Loader2 } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
+ import { SEVERITY_BADGE } from '../../utils/badge-colors'
3
4
 
4
5
  interface GitOpsActionsProps {
5
6
  /** The GitOps tool type */
@@ -53,10 +54,10 @@ export function GitOpsActions({
53
54
  'flex items-center rounded font-medium transition-colors',
54
55
  buttonClass,
55
56
  isSyncing
56
- ? 'bg-blue-500/20 text-blue-400 cursor-wait'
57
+ ? `${SEVERITY_BADGE.info} cursor-wait`
57
58
  : suspended
58
59
  ? 'bg-theme-elevated text-theme-text-tertiary cursor-not-allowed'
59
- : 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30'
60
+ : `${SEVERITY_BADGE.info} hover:bg-sky-500/30`
60
61
  )}
61
62
  title={suspended ? 'Cannot sync while suspended' : syncLabel}
62
63
  >
@@ -79,8 +80,8 @@ export function GitOpsActions({
79
80
  'flex items-center rounded font-medium transition-colors',
80
81
  buttonClass,
81
82
  isSuspending
82
- ? 'bg-green-500/20 text-green-400 cursor-wait'
83
- : 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
83
+ ? `${SEVERITY_BADGE.success} cursor-wait`
84
+ : `${SEVERITY_BADGE.success} hover:bg-emerald-500/30`
84
85
  )}
85
86
  >
86
87
  {isSuspending ? (
@@ -100,8 +101,8 @@ export function GitOpsActions({
100
101
  'flex items-center rounded font-medium transition-colors',
101
102
  buttonClass,
102
103
  isSuspending
103
- ? 'bg-yellow-500/20 text-yellow-400 cursor-wait'
104
- : 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
104
+ ? `${SEVERITY_BADGE.warning} cursor-wait`
105
+ : `${SEVERITY_BADGE.warning} hover:bg-amber-500/30`
105
106
  )}
106
107
  >
107
108
  {isSuspending ? (
@@ -147,7 +148,7 @@ export function SyncButton({
147
148
  buttonClass,
148
149
  loading || disabled
149
150
  ? 'bg-theme-elevated text-theme-text-tertiary cursor-not-allowed'
150
- : 'bg-blue-500/20 text-blue-400 hover:bg-blue-500/30'
151
+ : `${SEVERITY_BADGE.info} hover:bg-sky-500/30`
151
152
  )}
152
153
  >
153
154
  {loading ? (
@@ -190,8 +191,8 @@ export function SuspendToggle({
190
191
  'flex items-center rounded font-medium transition-colors',
191
192
  buttonClass,
192
193
  loading
193
- ? 'bg-green-500/20 text-green-400 cursor-wait'
194
- : 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
194
+ ? `${SEVERITY_BADGE.success} cursor-wait`
195
+ : `${SEVERITY_BADGE.success} hover:bg-emerald-500/30`
195
196
  )}
196
197
  >
197
198
  {loading ? (
@@ -212,8 +213,8 @@ export function SuspendToggle({
212
213
  'flex items-center rounded font-medium transition-colors',
213
214
  buttonClass,
214
215
  loading
215
- ? 'bg-yellow-500/20 text-yellow-400 cursor-wait'
216
- : 'bg-yellow-500/20 text-yellow-400 hover:bg-yellow-500/30'
216
+ ? `${SEVERITY_BADGE.warning} cursor-wait`
217
+ : `${SEVERITY_BADGE.warning} hover:bg-amber-500/30`
217
218
  )}
218
219
  >
219
220
  {loading ? (
@@ -21,7 +21,7 @@ export function GitOpsStatusBadge({ status, showHealth = true, compact = false }
21
21
  if (compact) {
22
22
  return (
23
23
  <span
24
- className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', colorClass)}
24
+ className={clsx('badge', colorClass)}
25
25
  title={status.message}
26
26
  >
27
27
  <Icon className="w-3 h-3" />
@@ -37,7 +37,7 @@ export function GitOpsStatusBadge({ status, showHealth = true, compact = false }
37
37
  return (
38
38
  <div className="flex items-center gap-2">
39
39
  <span
40
- className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', colorClass)}
40
+ className={clsx('badge', colorClass)}
41
41
  title={status.message}
42
42
  >
43
43
  <Icon className="w-3.5 h-3.5" />
@@ -94,7 +94,7 @@ function HealthIndicator({ health }: HealthIndicatorProps) {
94
94
 
95
95
  return (
96
96
  <span
97
- className={clsx('px-1.5 py-0.5 rounded text-xs inline-flex items-center gap-1', color)}
97
+ className={clsx('badge-sm', color)}
98
98
  title={`Health: ${label}`}
99
99
  >
100
100
  <Icon className="w-3 h-3" />
@@ -126,7 +126,7 @@ function getHealthInfo(health: GitOpsHealthStatus) {
126
126
  export function SyncStatusBadge({ sync, suspended }: { sync: SyncStatus; suspended?: boolean }) {
127
127
  if (suspended) {
128
128
  return (
129
- <span className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', SEVERITY_BADGE_BORDERED.warning)}>
129
+ <span className={clsx('badge', SEVERITY_BADGE_BORDERED.warning)}>
130
130
  <Pause className="w-3 h-3" />
131
131
  Suspended
132
132
  </span>
@@ -137,7 +137,7 @@ export function SyncStatusBadge({ sync, suspended }: { sync: SyncStatus; suspend
137
137
  const Icon = config.icon
138
138
 
139
139
  return (
140
- <span className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', config.color)}>
140
+ <span className={clsx('badge', config.color)}>
141
141
  <Icon className={clsx('w-3 h-3', sync === 'Reconciling' && 'animate-spin')} />
142
142
  {config.label}
143
143
  </span>
@@ -164,7 +164,7 @@ export function HealthStatusBadge({ health }: { health: GitOpsHealthStatus }) {
164
164
  const { icon: Icon, color, label } = getHealthInfo(health)
165
165
 
166
166
  return (
167
- <span className={clsx('px-2 py-0.5 rounded text-xs font-medium inline-flex items-center gap-1', color)}>
167
+ <span className={clsx('badge', color)}>
168
168
  <Icon className={clsx('w-3 h-3', health === 'Progressing' && 'animate-spin')} />
169
169
  {label}
170
170
  </span>
@@ -240,7 +240,7 @@ export function LogCore({
240
240
  <button
241
241
  onClick={() => setExpandAllStructured(prev => !prev)}
242
242
  className={`p-1.5 rounded transition-colors ${
243
- expandAllStructured ? 'bg-blue-600/50 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
243
+ expandAllStructured ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
244
244
  }`}
245
245
  >
246
246
  <Braces className="w-4 h-4" />
@@ -253,7 +253,7 @@ export function LogCore({
253
253
  <button
254
254
  onClick={toggleTimestamps}
255
255
  className={`p-1.5 rounded transition-colors ${
256
- showTimestamps ? 'bg-blue-600/50 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
256
+ showTimestamps ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
257
257
  }`}
258
258
  >
259
259
  <Clock className="w-4 h-4" />
@@ -265,7 +265,7 @@ export function LogCore({
265
265
  <button
266
266
  onClick={toggleWrap}
267
267
  className={`p-1.5 rounded transition-colors ${
268
- wordWrap ? 'bg-blue-600/50 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
268
+ wordWrap ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
269
269
  }`}
270
270
  >
271
271
  <WrapText className="w-4 h-4" />
@@ -277,7 +277,7 @@ export function LogCore({
277
277
  <button
278
278
  onClick={() => search.isOpen ? search.close() : search.open()}
279
279
  className={`p-1.5 rounded transition-colors ${
280
- search.isOpen ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
280
+ search.isOpen ? 'btn-brand-toggle' : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
281
281
  }`}
282
282
  >
283
283
  <Search className="w-4 h-4" />
@@ -365,7 +365,7 @@ export function LogCore({
365
365
  <button
366
366
  onClick={search.toggleRegex}
367
367
  className={`p-1 rounded transition-colors ${
368
- search.isRegex ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
368
+ search.isRegex ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
369
369
  }`}
370
370
  >
371
371
  <Regex className="w-3.5 h-3.5" />
@@ -377,7 +377,7 @@ export function LogCore({
377
377
  <button
378
378
  onClick={search.toggleCaseSensitive}
379
379
  className={`p-1 rounded transition-colors ${
380
- search.isCaseSensitive ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
380
+ search.isCaseSensitive ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
381
381
  }`}
382
382
  >
383
383
  <CaseSensitive className="w-3.5 h-3.5" />
@@ -389,7 +389,7 @@ export function LogCore({
389
389
  <button
390
390
  onClick={search.toggleFilterMode}
391
391
  className={`p-1 rounded transition-colors ${
392
- search.isFilterMode ? 'bg-blue-600 text-theme-text-primary' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
392
+ search.isFilterMode ? 'btn-brand-toggle' : 'text-theme-text-tertiary hover:text-theme-text-secondary'
393
393
  }`}
394
394
  >
395
395
  <Filter className="w-3.5 h-3.5" />
@@ -484,7 +484,7 @@ export function LogCore({
484
484
  {!atBottom && (
485
485
  <button
486
486
  onClick={scrollToBottom}
487
- className="absolute bottom-4 right-14 px-3 py-1.5 bg-blue-600 text-white text-xs rounded-full shadow-lg hover:bg-blue-700 z-10"
487
+ className="absolute bottom-4 right-14 px-3 py-1.5 btn-brand text-xs rounded-full shadow-lg z-10"
488
488
  >
489
489
  Scroll to bottom
490
490
  </button>
@@ -25,6 +25,8 @@ export interface LogsViewerProps {
25
25
  fetchLogs: (params: LogsFetchParams) => Promise<{ [container: string]: string }>
26
26
  /** If provided, the stream button is enabled. Called to open an SSE connection. */
27
27
  createStream?: (params: Omit<LogsFetchParams, 'previous'>) => EventSource
28
+ /** Override the download mechanism (e.g. for desktop apps where blob URLs fail). */
29
+ overrideDownload?: (content: string, mime: string, filename: string) => void
28
30
  /** Force dark mode on the logs container (default: true) */
29
31
  forceDark?: boolean
30
32
  }
@@ -36,6 +38,7 @@ export function LogsViewer({
36
38
  initialContainer,
37
39
  fetchLogs,
38
40
  createStream,
41
+ overrideDownload,
39
42
  forceDark,
40
43
  }: LogsViewerProps) {
41
44
  const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
@@ -105,12 +108,14 @@ export function LogsViewer({
105
108
  mime = 'text/plain'
106
109
  }
107
110
  try {
108
- triggerDownload(content, mime, filename)
109
- showSuccess('Log download started', `Saving ${filename}. Check your browser or desktop Downloads location.`)
111
+ triggerDownload(content, mime, filename, overrideDownload)
112
+ if (!overrideDownload) {
113
+ showSuccess('Log download started', `Saving ${filename}. Check your browser Downloads.`)
114
+ }
110
115
  } catch (err) {
111
116
  showError('Failed to download logs', err instanceof Error ? err.message : 'Unknown download error')
112
117
  }
113
- }, [entries, podName, selectedContainer, showError, showSuccess])
118
+ }, [entries, podName, selectedContainer, overrideDownload, showError, showSuccess])
114
119
 
115
120
  const toolbarExtra = (
116
121
  <>
@@ -42,6 +42,8 @@ export interface WorkloadLogsViewerProps {
42
42
  * Called to open an SSE connection for the whole workload.
43
43
  */
44
44
  createStream?: (params: WorkloadLogsFetchParams) => EventSource
45
+ /** Override the download mechanism (e.g. for desktop apps where blob URLs fail). */
46
+ overrideDownload?: (content: string, mime: string, filename: string) => void
45
47
  /** Force dark mode on the logs container (default: true) */
46
48
  forceDark?: boolean
47
49
  }
@@ -51,7 +53,7 @@ const POD_COLORS = [
51
53
  'text-pink-400', 'text-cyan-400', 'text-orange-400', 'text-lime-400',
52
54
  ]
53
55
 
54
- export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }: WorkloadLogsViewerProps) {
56
+ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark }: WorkloadLogsViewerProps) {
55
57
  const [selectedContainer, setSelectedContainer] = useState<string>('')
56
58
  const [pods, setPods] = useState<WorkloadPodInfo[]>([])
57
59
  const [selectedPods, setSelectedPods] = useState<Set<string>>(new Set())
@@ -131,6 +133,29 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }:
131
133
  })
132
134
  }
133
135
  },
136
+ onPodAdded: (data: any) => {
137
+ if (data?.pods) {
138
+ const newPods = data.pods as WorkloadPodInfo[]
139
+ setPods(prev => {
140
+ const existing = new Set(prev.map(p => p.name))
141
+ const toAdd = newPods.filter(p => !existing.has(p.name))
142
+ return toAdd.length > 0 ? [...prev, ...toAdd] : prev
143
+ })
144
+ setSelectedPods(prev => {
145
+ const next = new Set(prev)
146
+ newPods.forEach(p => next.add(p.name))
147
+ return next
148
+ })
149
+ }
150
+ },
151
+ onPodRemoved: (data: any) => {
152
+ if (data?.pod) {
153
+ const removedName = data.pod as string
154
+ setPods(prev => prev.filter(p => p.name !== removedName))
155
+ // Don't remove from selectedPods — keep old log entries visible
156
+ // while new pod logs start flowing in
157
+ }
158
+ },
134
159
  },
135
160
  'Workload log stream error',
136
161
  )
@@ -152,8 +177,9 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }:
152
177
  }, [])
153
178
 
154
179
  const toggleAllPods = useCallback(() => {
155
- setSelectedPods(selectedPods.size === pods.length ? new Set() : new Set(pods.map(p => p.name)))
156
- }, [selectedPods.size, pods])
180
+ const allSelected = pods.every(p => selectedPods.has(p.name))
181
+ setSelectedPods(allSelected ? new Set() : new Set(pods.map(p => p.name)))
182
+ }, [selectedPods, pods])
157
183
 
158
184
  const filteredEntries = useMemo(
159
185
  () => entries.filter(e => !e.pod || selectedPods.has(e.pod)),
@@ -182,12 +208,14 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }:
182
208
  mime = 'text/plain'
183
209
  }
184
210
  try {
185
- triggerDownload(content, mime, filename)
186
- showSuccess('Log download started', `Saving ${filename}. Check your browser or desktop Downloads location.`)
211
+ triggerDownload(content, mime, filename, overrideDownload)
212
+ if (!overrideDownload) {
213
+ showSuccess('Log download started', `Saving ${filename}. Check your browser Downloads.`)
214
+ }
187
215
  } catch (err) {
188
216
  showError('Failed to download logs', err instanceof Error ? err.message : 'Unknown download error')
189
217
  }
190
- }, [filteredEntries, name, showError, showSuccess])
218
+ }, [filteredEntries, name, overrideDownload, showError, showSuccess])
191
219
 
192
220
  const toolbarExtra = (
193
221
  <>
@@ -196,11 +224,11 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }:
196
224
  <button
197
225
  onClick={() => setShowPodFilter(v => !v)}
198
226
  className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors ${
199
- showPodFilter ? 'bg-blue-600 text-theme-text-primary' : 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
227
+ showPodFilter ? 'btn-brand-toggle' : 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
200
228
  }`}
201
229
  >
202
230
  <Filter className="w-3 h-3" />
203
- <span>{selectedPods.size}/{pods.length} pods</span>
231
+ <span>{pods.filter(p => selectedPods.has(p.name)).length}/{pods.length} pods</span>
204
232
  <ChevronDown className="w-3 h-3" />
205
233
  </button>
206
234
 
@@ -208,7 +236,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }:
208
236
  <div className="absolute top-full left-0 mt-1 w-64 bg-theme-elevated border border-theme-border rounded-lg shadow-lg z-50 max-h-64 overflow-y-auto">
209
237
  <div className="p-2 border-b border-theme-border">
210
238
  <button onClick={toggleAllPods} className="text-xs text-blue-400 hover:text-blue-300">
211
- {selectedPods.size === pods.length ? 'Deselect all' : 'Select all'}
239
+ {pods.every(p => selectedPods.has(p.name)) ? 'Deselect all' : 'Select all'}
212
240
  </button>
213
241
  </div>
214
242
  {pods.map(pod => (
@@ -6,6 +6,10 @@ export interface LogStreamHandlers {
6
6
  onConnected?: (data: unknown) => void
7
7
  /** Called for each log event with parsed event data */
8
8
  onLog: (data: unknown) => void
9
+ /** Called when new pods are discovered during streaming (workload logs only) */
10
+ onPodAdded?: (data: unknown) => void
11
+ /** Called when pods are terminated during streaming (workload logs only) */
12
+ onPodRemoved?: (data: unknown) => void
9
13
  }
10
14
 
11
15
  /**
@@ -45,6 +49,22 @@ export function useLogStream() {
45
49
  }
46
50
  })
47
51
 
52
+ es.addEventListener('pod_added', (event) => {
53
+ if (handlers.onPodAdded) {
54
+ try { handlers.onPodAdded(JSON.parse((event as MessageEvent).data)) } catch (e) {
55
+ console.error('Failed to parse pod_added event:', e)
56
+ }
57
+ }
58
+ })
59
+
60
+ es.addEventListener('pod_removed', (event) => {
61
+ if (handlers.onPodRemoved) {
62
+ try { handlers.onPodRemoved(JSON.parse((event as MessageEvent).data)) } catch (e) {
63
+ console.error('Failed to parse pod_removed event:', e)
64
+ }
65
+ }
66
+ })
67
+
48
68
  es.addEventListener('end', () => setIsStreaming(false))
49
69
 
50
70
  es.addEventListener('error', (event) => {
@@ -109,7 +109,7 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
109
109
  className={clsx(
110
110
  'w-full flex items-center gap-2 px-2 xl:px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
111
111
  isSelected
112
- ? 'bg-blue-500/20 text-blue-700 dark:text-blue-300'
112
+ ? 'selection-strong selection-text'
113
113
  : forbidden
114
114
  ? 'text-theme-text-disabled hover:bg-theme-elevated hover:text-theme-text-secondary'
115
115
  : 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary'
@@ -146,8 +146,8 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
146
146
  </Tooltip>
147
147
  ) : (
148
148
  <span className={clsx(
149
- 'text-xs py-0.5 rounded text-center',
150
- isSelected ? 'bg-blue-500/30 text-blue-700 dark:text-blue-300' : 'bg-theme-elevated',
149
+ 'text-xs py-0.5 rounded text-center font-mono',
150
+ isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated',
151
151
  count < 1000 ? 'w-8' : 'w-9'
152
152
  )}>
153
153
  {count}
@@ -357,7 +357,7 @@ export function ResourcesSidebar({
357
357
  }, [effectiveSelectedKind.name, effectiveSelectedKind.group])
358
358
 
359
359
  return (
360
- <div className={clsx('w-56 2xl:w-72 bg-theme-surface border-r border-theme-border overflow-y-auto overflow-x-hidden shrink-0', className)}>
360
+ <div className={clsx('w-56 2xl:w-72 bg-theme-surface dark:bg-theme-base border-r border-theme-border overflow-y-auto overflow-x-hidden shrink-0', className)}>
361
361
  <div className="px-2 py-2 border-b border-theme-border">
362
362
  <div className="relative">
363
363
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
@@ -367,7 +367,7 @@ export function ResourcesSidebar({
367
367
  value={kindFilter}
368
368
  onChange={(e) => setKindFilter(e.target.value)}
369
369
  onKeyDown={(e) => { if (e.key === 'Escape') { setKindFilter(''); (e.target as HTMLInputElement).blur() } }}
370
- className="w-full pl-7 pr-7 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
370
+ className="w-full pl-7 pr-7 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-skyhook-500"
371
371
  />
372
372
  {kindFilter && (
373
373
  <button
@@ -393,7 +393,7 @@ export function ResourcesSidebar({
393
393
  )}
394
394
  <span className="flex-1 text-left">Favorites</span>
395
395
  {!favoritesExpanded && pinned.length > 0 && (
396
- <span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center', pinned.length < 1000 ? 'w-8' : 'w-9')}>
396
+ <span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center font-mono', pinned.length < 1000 ? 'w-8' : 'w-9')}>
397
397
  {pinned.length}
398
398
  </span>
399
399
  )}
@@ -444,7 +444,7 @@ export function ResourcesSidebar({
444
444
  )}
445
445
  <span className="flex-1 text-left">{category.name}</span>
446
446
  {!isExpanded && (
447
- <span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center', category.total < 1000 ? 'w-8' : 'w-9')}>
447
+ <span className={clsx('text-xs py-0.5 rounded bg-theme-elevated text-theme-text-secondary font-normal normal-case text-center font-mono', category.total < 1000 ? 'w-8' : 'w-9')}>
448
448
  {category.total}
449
449
  </span>
450
450
  )}
@@ -497,15 +497,15 @@ export function ResourcesSidebar({
497
497
  className={clsx(
498
498
  'w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
499
499
  isSelected
500
- ? 'bg-blue-500/20 text-blue-700 dark:text-blue-300'
500
+ ? 'selection-strong selection-text'
501
501
  : 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary'
502
502
  )}
503
503
  >
504
504
  <Icon className="w-4 h-4 shrink-0" />
505
505
  <span className="flex-1 text-left">{type.label}</span>
506
506
  <span className={clsx(
507
- 'text-xs px-2 py-0.5 rounded',
508
- isSelected ? 'bg-blue-500/30 text-blue-700 dark:text-blue-300' : 'bg-theme-elevated'
507
+ 'badge font-mono',
508
+ isSelected ? 'bg-skyhook-500/30 selection-text' : 'bg-theme-elevated'
509
509
  )}>
510
510
  {count}
511
511
  </span>