@skyhook-io/k8s-ui 1.1.0 → 1.2.0

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 (58) hide show
  1. package/package.json +8 -2
  2. package/src/components/dock/BottomDock.tsx +14 -15
  3. package/src/components/dock/DockContext.tsx +40 -1
  4. package/src/components/dock/LocalTerminalTab.tsx +241 -0
  5. package/src/components/dock/NodeTerminalTab.tsx +129 -0
  6. package/src/components/dock/TerminalTab.tsx +1 -1
  7. package/src/components/dock/index.ts +2 -0
  8. package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
  9. package/src/components/logs/LogCore.tsx +30 -4
  10. package/src/components/logs/StructuredLogLine.tsx +168 -0
  11. package/src/components/logs/index.ts +1 -1
  12. package/src/components/logs/useLogBuffer.ts +16 -10
  13. package/src/components/resources/ResourcesView.tsx +186 -67
  14. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
  15. package/src/components/resources/renderers/HPARenderer.tsx +2 -1
  16. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
  17. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
  18. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
  19. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
  20. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
  21. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
  22. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
  23. package/src/components/resources/renderers/PodRenderer.tsx +164 -8
  24. package/src/components/resources/renderers/VPARenderer.tsx +2 -1
  25. package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
  26. package/src/components/resources/renderers/contour-cells.tsx +48 -0
  27. package/src/components/resources/renderers/index.ts +2 -0
  28. package/src/components/resources/renderers/trivy-shared.tsx +9 -25
  29. package/src/components/resources/resource-utils-contour.ts +34 -0
  30. package/src/components/resources/resource-utils.ts +91 -0
  31. package/src/components/shared/ResourceActionsBar.tsx +276 -144
  32. package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
  33. package/src/components/shared/index.ts +1 -1
  34. package/src/components/timeline/shared.tsx +8 -10
  35. package/src/components/topology/K8sResourceNode.tsx +1 -0
  36. package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
  37. package/src/components/topology/TopologyGraph.tsx +2 -17
  38. package/src/components/topology/layout.ts +1 -0
  39. package/src/components/topology/topology.css +1 -0
  40. package/src/components/ui/CodeViewer.tsx +1 -2
  41. package/src/components/ui/ConfirmDialog.tsx +85 -130
  42. package/src/components/ui/DialogPortal.tsx +82 -0
  43. package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
  44. package/src/components/ui/Toast.tsx +5 -5
  45. package/src/components/ui/drawer-components.tsx +28 -22
  46. package/src/components/ui/index.ts +1 -0
  47. package/src/components/workload/WorkloadView.tsx +30 -2
  48. package/src/theme/index.ts +3 -0
  49. package/src/theme/tailwind-theme.css +61 -0
  50. package/src/theme/variables.css +121 -0
  51. package/src/types/core.ts +13 -2
  52. package/src/utils/animation.ts +1 -1
  53. package/src/utils/badge-colors.ts +158 -123
  54. package/src/utils/log-format.ts +127 -35
  55. package/src/utils/navigation.test.ts +142 -0
  56. package/src/utils/navigation.ts +52 -47
  57. package/src/utils/resource-hierarchy.ts +5 -0
  58. package/src/utils/resource-icons.ts +3 -0
@@ -1,5 +1,14 @@
1
- import { useState } from 'react'
1
+ import { useState, useMemo } from 'react'
2
+ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'
2
3
  import { ConfirmDialog } from './ConfirmDialog'
4
+ import { formatKindName } from './drawer-components'
5
+
6
+ export interface CascadeDependent {
7
+ kind: string
8
+ namespace: string
9
+ name: string
10
+ group?: string
11
+ }
3
12
 
4
13
  interface ForceDeleteConfirmDialogProps {
5
14
  open: boolean
@@ -9,6 +18,8 @@ interface ForceDeleteConfirmDialogProps {
9
18
  resourceKind: string
10
19
  namespaceName: string
11
20
  isLoading: boolean
21
+ cascadeDependents?: CascadeDependent[]
22
+ cascadeLoading?: boolean
12
23
  }
13
24
 
14
25
  export function ForceDeleteConfirmDialog({
@@ -19,6 +30,8 @@ export function ForceDeleteConfirmDialog({
19
30
  resourceKind,
20
31
  namespaceName,
21
32
  isLoading,
33
+ cascadeDependents,
34
+ cascadeLoading,
22
35
  }: ForceDeleteConfirmDialogProps) {
23
36
  const [forceDelete, setForceDelete] = useState(false)
24
37
 
@@ -43,15 +56,76 @@ export function ForceDeleteConfirmDialog({
43
56
  variant="danger"
44
57
  isLoading={isLoading}
45
58
  >
46
- <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
47
- <input
48
- type="checkbox"
49
- checked={forceDelete}
50
- onChange={(e) => setForceDelete(e.target.checked)}
51
- className="w-4 h-4 rounded border-theme-border bg-theme-base text-red-600 focus:ring-red-500 focus:ring-offset-0"
52
- />
53
- <span>Force delete (strips finalizers and bypasses grace period)</span>
54
- </label>
59
+ <div className="flex flex-col gap-3 pb-1">
60
+ {cascadeLoading && (
61
+ <div className="flex items-center gap-2 text-xs text-theme-text-tertiary">
62
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
63
+ Checking for dependent resources...
64
+ </div>
65
+ )}
66
+
67
+ {!cascadeLoading && cascadeDependents && cascadeDependents.length > 0 && (
68
+ <CascadeDependentsList dependents={cascadeDependents} />
69
+ )}
70
+
71
+ <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
72
+ <input
73
+ type="checkbox"
74
+ checked={forceDelete}
75
+ onChange={(e) => setForceDelete(e.target.checked)}
76
+ className="w-4 h-4 rounded border-theme-border bg-theme-base text-red-600 focus:ring-red-500 focus:ring-offset-0"
77
+ />
78
+ <span>Force delete (strips finalizers and bypasses grace period)</span>
79
+ </label>
80
+ </div>
55
81
  </ConfirmDialog>
56
82
  )
57
83
  }
84
+
85
+ const MAX_NAMES_PER_KIND = 8
86
+
87
+ function CascadeDependentsList({ dependents }: { dependents: CascadeDependent[] }) {
88
+ const [expanded, setExpanded] = useState(false)
89
+
90
+ const grouped = useMemo(() => {
91
+ const map = new Map<string, string[]>()
92
+ for (const dep of dependents) {
93
+ const kind = dep.kind
94
+ if (!map.has(kind)) map.set(kind, [])
95
+ map.get(kind)!.push(dep.name)
96
+ }
97
+ return Array.from(map.entries()).sort((a, b) => a[0].localeCompare(b[0]))
98
+ }, [dependents])
99
+
100
+ return (
101
+ <div className="rounded border border-amber-500/30 bg-amber-500/5">
102
+ <button
103
+ type="button"
104
+ onClick={() => setExpanded(!expanded)}
105
+ className="flex items-center gap-2 w-full px-3 py-2 text-left text-xs font-medium text-amber-400 hover:bg-amber-500/10 transition-colors"
106
+ >
107
+ {expanded ? <ChevronDown className="w-3.5 h-3.5 shrink-0" /> : <ChevronRight className="w-3.5 h-3.5 shrink-0" />}
108
+ <span>
109
+ Will also delete {dependents.length} dependent {dependents.length === 1 ? 'resource' : 'resources'}
110
+ </span>
111
+ </button>
112
+
113
+ {expanded && (
114
+ <div className="px-3 pb-2.5 space-y-1.5">
115
+ {grouped.map(([kind, names]) => (
116
+ <div key={kind} className="text-xs">
117
+ <span className="font-medium text-theme-text-primary">{formatKindName(kind)}</span>
118
+ <span className="text-theme-text-tertiary ml-1">({names.length})</span>
119
+ <div className="ml-3 mt-0.5 text-theme-text-secondary font-mono break-all">
120
+ {names.slice(0, MAX_NAMES_PER_KIND).join(', ')}
121
+ {names.length > MAX_NAMES_PER_KIND && (
122
+ <span className="text-theme-text-tertiary"> +{names.length - MAX_NAMES_PER_KIND} more</span>
123
+ )}
124
+ </div>
125
+ </div>
126
+ ))}
127
+ </div>
128
+ )}
129
+ </div>
130
+ )
131
+ }
@@ -166,7 +166,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
166
166
  isError
167
167
  ? 'bg-red-950/90 border-red-800/50'
168
168
  : isSuccess
169
- ? 'bg-emerald-950/90 border-emerald-700/50 dark:bg-emerald-950/90 dark:border-emerald-700/50'
169
+ ? 'bg-green-950/90 border-green-700/50'
170
170
  : 'bg-theme-surface border-theme-border'
171
171
  )}
172
172
  style={style}
@@ -189,13 +189,13 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
189
189
  {/* Content */}
190
190
  <div className="flex-1 min-w-0">
191
191
  <div className="flex items-center gap-2">
192
- <span className={clsx('text-sm font-medium', isError ? 'text-red-200' : isSuccess ? 'text-emerald-100' : 'text-theme-text-primary')}>
192
+ <span className={clsx('text-sm font-medium', isError ? 'text-red-200' : isSuccess ? 'text-green-100' : 'text-theme-text-primary')}>
193
193
  {toast.message}
194
194
  </span>
195
- {!isError && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-emerald-300' : 'text-green-400')} />}
195
+ {!isError && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-green-300' : 'text-green-400')} />}
196
196
  </div>
197
197
  {toast.detail && (
198
- <p className={clsx('mt-1 text-xs', isError ? 'text-red-300/80' : isSuccess ? 'text-emerald-300/80' : 'text-theme-text-secondary')}>
198
+ <p className={clsx('mt-1 text-xs', isError ? 'text-red-300/80' : isSuccess ? 'text-green-300/80' : 'text-theme-text-secondary')}>
199
199
  {toast.detail}
200
200
  </p>
201
201
  )}
@@ -214,7 +214,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
214
214
  isError
215
215
  ? 'text-red-400 hover:text-red-300 hover:bg-red-900/50'
216
216
  : isSuccess
217
- ? 'text-emerald-400 hover:text-emerald-300 hover:bg-emerald-900/50'
217
+ ? 'text-green-400 hover:text-green-300 hover:bg-green-900/50'
218
218
  : 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated'
219
219
  )}
220
220
  >
@@ -164,10 +164,10 @@ export function ConditionsSection({ conditions }: { conditions?: any[] }) {
164
164
  // ============================================================================
165
165
 
166
166
  const ALERT_COLORS = {
167
- error: { bg: 'bg-red-500/10', border: 'border-red-500/30', title: 'text-red-400', message: 'text-red-300/80', list: 'text-red-300', bullet: 'text-red-400/60' },
168
- warning: { bg: 'bg-yellow-500/10', border: 'border-yellow-500/30', title: 'text-yellow-400', message: 'text-yellow-300/80', list: 'text-yellow-300', bullet: 'text-yellow-400/60' },
169
- info: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', title: 'text-blue-400', message: 'text-blue-300/80', list: 'text-blue-300', bullet: 'text-blue-400/60' },
170
- success: { bg: 'bg-green-500/10', border: 'border-green-500/30', title: 'text-green-400', message: 'text-green-300/80', list: 'text-green-300', bullet: 'text-green-400/60' },
167
+ error: { bg: 'bg-red-500/10', border: 'border-red-500/30', title: 'text-red-700 dark:text-red-400', message: 'text-red-600/80 dark:text-red-400/80', list: 'text-red-600 dark:text-red-400', bullet: 'text-red-500/60 dark:text-red-400/60' },
168
+ warning: { bg: 'bg-yellow-500/10', border: 'border-yellow-500/30', title: 'text-yellow-700 dark:text-yellow-400', message: 'text-yellow-600/80 dark:text-yellow-400/80', list: 'text-yellow-600 dark:text-yellow-400', bullet: 'text-yellow-500/60 dark:text-yellow-400/60' },
169
+ info: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', title: 'text-blue-700 dark:text-blue-400', message: 'text-blue-600/80 dark:text-blue-400/80', list: 'text-blue-600 dark:text-blue-400', bullet: 'text-blue-500/60 dark:text-blue-400/60' },
170
+ success: { bg: 'bg-green-500/10', border: 'border-green-500/30', title: 'text-green-700 dark:text-green-400', message: 'text-green-600/80 dark:text-green-400/80', list: 'text-green-600 dark:text-green-400', bullet: 'text-green-500/60 dark:text-green-400/60' },
171
171
  } as const
172
172
 
173
173
  const DEFAULT_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -453,24 +453,24 @@ export function AppInfoSection({ data }: { data: any }) {
453
453
  export function getKindColor(kind: string): string {
454
454
  const k = kind.toLowerCase()
455
455
  // Use darker text for light mode contrast, brighter for dark mode
456
- if (k.includes('pod')) return 'bg-lime-500/20 text-lime-700 dark:text-lime-300 border-lime-500/30'
457
- if (k.includes('deployment')) return 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 border-emerald-500/30'
458
- if (k.includes('service')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30'
459
- if (k.includes('gateway')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-300 border-violet-500/30'
460
- if (k.includes('route')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30'
461
- if (k.includes('ingress')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-300 border-violet-500/30'
462
- if (k.includes('configmap')) return 'bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/30'
463
- if (k.endsWith('report')) return 'bg-rose-500/20 text-rose-700 dark:text-rose-300 border-rose-500/30'
464
- if (k.includes('secret')) return 'bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30'
465
- if (k.includes('daemonset')) return 'bg-teal-500/20 text-teal-700 dark:text-teal-300 border-teal-500/30'
466
- if (k.includes('statefulset')) return 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border-cyan-500/30'
467
- if (k.includes('replicaset')) return 'bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30'
468
- if (k.includes('hpa') || k.includes('horizontalpodautoscaler')) return 'bg-pink-500/20 text-pink-700 dark:text-pink-300 border-pink-500/30'
469
- if (k.includes('cronjob')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30'
470
- if (k.includes('job')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30'
471
- if (k.includes('node')) return 'bg-gray-500/20 text-gray-700 dark:text-gray-300 border-gray-500/30'
472
- if (k.includes('namespace')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30'
473
- if (k.includes('persistentvolume')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30'
456
+ if (k.includes('pod')) return 'bg-lime-500/20 text-lime-700 dark:text-lime-400 border-lime-500/30'
457
+ if (k.includes('deployment')) return 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-400 border-emerald-500/30'
458
+ if (k.includes('service')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-500/30'
459
+ if (k.includes('gateway')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-400 border-violet-500/30'
460
+ if (k.includes('route')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-400 border-purple-500/30'
461
+ if (k.includes('ingress')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-400 border-violet-500/30'
462
+ if (k.includes('configmap')) return 'bg-amber-500/20 text-amber-700 dark:text-amber-400 border-amber-500/30'
463
+ if (k.endsWith('report')) return 'bg-rose-500/20 text-rose-700 dark:text-rose-400 border-rose-500/30'
464
+ if (k.includes('secret')) return 'bg-red-500/20 text-red-700 dark:text-red-400 border-red-500/30'
465
+ if (k.includes('daemonset')) return 'bg-teal-500/20 text-teal-700 dark:text-teal-400 border-teal-500/30'
466
+ if (k.includes('statefulset')) return 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-400 border-cyan-500/30'
467
+ if (k.includes('replicaset')) return 'bg-green-500/20 text-green-700 dark:text-green-400 border-green-500/30'
468
+ if (k.includes('hpa') || k.includes('horizontalpodautoscaler')) return 'bg-pink-500/20 text-pink-700 dark:text-pink-400 border-pink-500/30'
469
+ if (k.includes('cronjob')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-400 border-orange-500/30'
470
+ if (k.includes('job')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-400 border-orange-500/30'
471
+ if (k.includes('node')) return 'bg-gray-500/20 text-gray-700 dark:text-gray-400 border-gray-500/30'
472
+ if (k.includes('namespace')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-500/30'
473
+ if (k.includes('persistentvolume')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-400 border-purple-500/30'
474
474
  // Default color for CRDs
475
475
  return 'bg-fuchsia-500/20 text-fuchsia-700 dark:text-fuchsia-300 border-fuchsia-500/30'
476
476
  }
@@ -485,11 +485,17 @@ export function formatKindName(kind: string): string {
485
485
  secrets: 'Secret', jobs: 'Job', cronjobs: 'CronJob', hpas: 'HPA',
486
486
  horizontalpodautoscalers: 'HPA', nodes: 'Node', namespaces: 'Namespace',
487
487
  persistentvolumeclaims: 'PVC', persistentvolumes: 'PV',
488
+ httpproxies: 'HTTPProxy',
488
489
  }
489
490
  if (names[k]) return names[k]
490
491
 
491
492
  // For unknown kinds (CRDs), use the original kind name
492
493
  // or format it nicely if it's a plural name
494
+ if (k.endsWith('ies')) {
495
+ // Handle -ies → -y (e.g., httpproxies → Httpproxy)
496
+ const singular = kind.slice(0, -3) + 'y'
497
+ return singular.charAt(0).toUpperCase() + singular.slice(1)
498
+ }
493
499
  if (kind.endsWith('s') && !kind.endsWith('ss')) {
494
500
  // Try to singularize simple plurals
495
501
  const singular = kind.slice(0, -1)
@@ -1,4 +1,5 @@
1
1
  export { Tooltip } from './Tooltip'
2
+ export { DialogPortal } from './DialogPortal'
2
3
  export { ConfirmDialog } from './ConfirmDialog'
3
4
  export { HealthRing } from './HealthRing'
4
5
  export { MetricsChart, MetricsSparkline } from './MetricsChart'
@@ -17,7 +17,7 @@ import {
17
17
  X,
18
18
  BarChart3,
19
19
  } from 'lucide-react'
20
- import type { TimelineEvent, ResourceRef, Relationships, SelectedResource } from '../../types'
20
+ import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom } from '../../types'
21
21
  import type { NavigateToResource } from '../../utils/navigation'
22
22
  import { refToSelectedResource, pluralToKind } from '../../utils/navigation'
23
23
  import { isChangeEvent, isHistoricalEvent } from '../../types'
@@ -38,7 +38,7 @@ import {
38
38
  } from '../timeline/shared'
39
39
  import { ResourceActionsBar } from '../shared/ResourceActionsBar'
40
40
  import { EditableYamlView, SaveSuccessAnimation } from '../shared/EditableYamlView'
41
- import { ResourceRendererDispatch, getResourceStatus } from '../shared/ResourceRendererDispatch'
41
+ import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } from '../shared/ResourceRendererDispatch'
42
42
  import { getKindColor, formatKindName } from '../ui/drawer-components'
43
43
 
44
44
  type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
@@ -94,6 +94,8 @@ interface WorkloadViewProps {
94
94
  onUpdateResource?: (params: { kind: string; namespace: string; name: string; yaml: string }) => Promise<void>
95
95
  /** Whether the resource is being updated */
96
96
  isUpdatingResource?: boolean
97
+ /** Error message from the last update attempt */
98
+ updateResourceError?: string | null
97
99
 
98
100
  // ── Tab state (optional URL sync) ────────────────────────────────────────
99
101
  /** Controlled active tab. If not provided, managed internally. */
@@ -123,6 +125,10 @@ interface WorkloadViewProps {
123
125
  // ── ResourceActionsBar props (passed through) ────────────────────────────
124
126
  /** All props for the actions bar (forwarded as-is) */
125
127
  actionsBarProps?: Record<string, any>
128
+ /** Platform-specific renderer overrides (e.g. with hooks for metrics, exec, port-forward) */
129
+ rendererOverrides?: RendererOverrides
130
+ /** Resolved ConfigMap/Secret data for envFrom expansion in PodRenderer */
131
+ resolvedEnvFrom?: ResolvedEnvFrom
126
132
  }
127
133
 
128
134
  export function WorkloadView({
@@ -152,6 +158,7 @@ export function WorkloadView({
152
158
  // Mutations
153
159
  onUpdateResource,
154
160
  isUpdatingResource,
161
+ updateResourceError,
155
162
  // Tab state
156
163
  activeTab: controlledTab,
157
164
  onTabChange,
@@ -161,6 +168,10 @@ export function WorkloadView({
161
168
  isMetricsAvailable,
162
169
  // Actions bar
163
170
  actionsBarProps,
171
+ // Renderer overrides
172
+ rendererOverrides,
173
+ // Pod env expansion
174
+ resolvedEnvFrom,
164
175
  }: WorkloadViewProps) {
165
176
  // Normalize kind: URL has plural lowercase, internal logic uses singular PascalCase
166
177
  const kind = pluralToKind(kindProp)
@@ -416,6 +427,9 @@ export function WorkloadView({
416
427
  onCopy={(text) => copyToClipboard(text, 'yaml')}
417
428
  copied={copied === 'yaml'}
418
429
  onSaved={handleSaved}
430
+ onSave={onUpdateResource}
431
+ isSaving={isUpdatingResource}
432
+ saveError={updateResourceError}
419
433
  />
420
434
  ) : (
421
435
  <ResourceRendererDispatch
@@ -428,6 +442,9 @@ export function WorkloadView({
428
442
  onNavigate={onNavigateToResource ? (ref) => onNavigateToResource(refToSelectedResource(ref)) : undefined}
429
443
  onSaveSecretValue={canUpdateSecrets ? handleSaveSecretValue : undefined}
430
444
  isSavingSecret={isUpdatingResource}
445
+ rendererOverrides={rendererOverrides}
446
+ resolvedEnvFrom={resolvedEnvFrom}
447
+ renderMetrics={renderMetricsTab}
431
448
  />
432
449
  )}
433
450
  </div>
@@ -566,6 +583,8 @@ export function WorkloadView({
566
583
  isSavingSecret={isUpdatingResource}
567
584
  onOpenLogs={handleOpenLogs}
568
585
  onSwitchToTimeline={() => handleSetTab('timeline')}
586
+ rendererOverrides={rendererOverrides}
587
+ resolvedEnvFrom={resolvedEnvFrom}
569
588
  />
570
589
  )}
571
590
  {activeTab === 'timeline' && (
@@ -613,6 +632,9 @@ export function WorkloadView({
613
632
  onCopy={(text) => copyToClipboard(text, 'yaml')}
614
633
  copied={copied === 'yaml'}
615
634
  onSaved={handleSaved}
635
+ onSave={onUpdateResource}
636
+ isSaving={isUpdatingResource}
637
+ saveError={updateResourceError}
616
638
  />
617
639
  )}
618
640
  </div>
@@ -1044,6 +1066,8 @@ function InfoTab({
1044
1066
  isSavingSecret,
1045
1067
  onOpenLogs,
1046
1068
  onSwitchToTimeline,
1069
+ rendererOverrides,
1070
+ resolvedEnvFrom,
1047
1071
  }: {
1048
1072
  resource: any
1049
1073
  selectedResource: SelectedResource
@@ -1056,6 +1080,8 @@ function InfoTab({
1056
1080
  isSavingSecret?: boolean
1057
1081
  onOpenLogs?: (podName: string, containerName: string) => void
1058
1082
  onSwitchToTimeline?: () => void
1083
+ rendererOverrides?: RendererOverrides
1084
+ resolvedEnvFrom?: ResolvedEnvFrom
1059
1085
  }) {
1060
1086
  if (isLoading) {
1061
1087
  return (
@@ -1088,6 +1114,8 @@ function InfoTab({
1088
1114
  showCommonSections={true}
1089
1115
  showMetrics={false}
1090
1116
  onOpenLogs={onOpenLogs}
1117
+ rendererOverrides={rendererOverrides}
1118
+ resolvedEnvFrom={resolvedEnvFrom}
1091
1119
  eventsHint={onSwitchToTimeline && (
1092
1120
  <button
1093
1121
  onClick={onSwitchToTimeline}
@@ -0,0 +1,3 @@
1
+ // Theme barrel export
2
+ // CSS files are imported directly: @import "@skyhook-io/k8s-ui/theme/variables.css"
3
+ // This file exists for consumers that want a JS entry point for the theme module.
@@ -0,0 +1,61 @@
1
+ /*
2
+ * @skyhook-io/k8s-ui Tailwind Theme
3
+ *
4
+ * Maps CSS variables to Tailwind v4 @theme tokens.
5
+ * Enables utility classes: bg-theme-surface, text-theme-text-primary, border-accent, etc.
6
+ * Import after variables.css.
7
+ */
8
+
9
+ @theme inline {
10
+ /* Backgrounds */
11
+ --color-theme-base: var(--bg-base);
12
+ --color-theme-surface: var(--bg-surface);
13
+ --color-theme-elevated: var(--bg-elevated);
14
+ --color-theme-hover: var(--bg-hover);
15
+ --color-theme-active: var(--bg-active);
16
+
17
+ /* Text */
18
+ --color-theme-text-primary: var(--text-primary);
19
+ --color-theme-text-secondary: var(--text-secondary);
20
+ --color-theme-text-tertiary: var(--text-tertiary);
21
+ --color-theme-text-disabled: var(--text-disabled);
22
+
23
+ /* Borders */
24
+ --color-theme-border: var(--border-default);
25
+ --color-theme-border-light: var(--border-light);
26
+ --color-theme-border-subtle: var(--border-subtle);
27
+
28
+ /* Accent */
29
+ --color-accent: var(--accent);
30
+ --color-accent-light: var(--accent-light);
31
+ --color-accent-muted: var(--accent-muted);
32
+ --color-accent-text: var(--accent-text);
33
+
34
+ /* Skyhook brand */
35
+ --color-skyhook: #2D7AFF;
36
+ --color-skyhook-50: #E6F0FF;
37
+ --color-skyhook-100: #CCE0FF;
38
+ --color-skyhook-200: #99C2FF;
39
+ --color-skyhook-300: #66A3FF;
40
+ --color-skyhook-400: #3385FF;
41
+ --color-skyhook-500: #2D7AFF;
42
+ --color-skyhook-600: #0052CC;
43
+ --color-skyhook-700: #003D99;
44
+ --color-skyhook-800: #002966;
45
+ --color-skyhook-900: #001433;
46
+
47
+ /* K8s resource type colors */
48
+ --color-k8s-internet: #2D7AFF;
49
+ --color-k8s-ingress: #8b5cf6;
50
+ --color-k8s-service: #3b82f6;
51
+ --color-k8s-deployment: #10b981;
52
+ --color-k8s-daemonset: #14b8a6;
53
+ --color-k8s-statefulset: #06b6d4;
54
+ --color-k8s-replicaset: #22c55e;
55
+ --color-k8s-pod: #84cc16;
56
+ --color-k8s-configmap: #f59e0b;
57
+ --color-k8s-secret: #ef4444;
58
+ --color-k8s-hpa: #ec4899;
59
+ --color-k8s-job: #a855f7;
60
+ --color-k8s-cronjob: #d946ef;
61
+ }
@@ -0,0 +1,121 @@
1
+ /*
2
+ * @skyhook-io/k8s-ui Theme Variables
3
+ *
4
+ * Single source of truth for the theme contract.
5
+ * Uses light-dark() for dual-value definitions.
6
+ * Consumers @import this file, then override any variables via CSS cascade.
7
+ *
8
+ * Toggle mechanism:
9
+ * document.documentElement.classList.toggle('dark', isDark)
10
+ * document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'
11
+ */
12
+
13
+ :root {
14
+ color-scheme: light dark;
15
+
16
+ /* ── Brand ── */
17
+ --color-brand: #2D7AFF;
18
+ --color-brand-light: light-dark(#1A5FCC, #5A9AFF);
19
+
20
+ /* ── Semantic status (theme-independent) ── */
21
+ --color-success: #22C55E;
22
+ --color-success-light: #4ADE80;
23
+ --color-success-dark: #16A34A;
24
+ --color-warning: #F59E0B;
25
+ --color-warning-light: #FBBF24;
26
+ --color-warning-dark: #D97706;
27
+ --color-error: #EF4444;
28
+ --color-error-light: #F87171;
29
+ --color-error-dark: #DC2626;
30
+ --color-info: #3B82F6;
31
+
32
+ /* ── Backgrounds ── */
33
+ --bg-base: light-dark(#f7f9fc, #0c1425);
34
+ --bg-surface: light-dark(#ffffff, #162033);
35
+ --bg-elevated: light-dark(#edf2f8, #1e2d45);
36
+ --bg-hover: light-dark(#e2e8f0, #2a3d58);
37
+ --bg-active: light-dark(#dbeafe, #1e3a5f);
38
+
39
+ /* ── Text ── */
40
+ --text-primary: light-dark(#0f172a, #f8fafc);
41
+ --text-secondary: light-dark(#475569, #cbd5e1);
42
+ --text-tertiary: light-dark(#64748b, #94a3b8);
43
+ --text-disabled: light-dark(#94a3b8, #64748b);
44
+
45
+ /* ── Borders ── */
46
+ --border-default: light-dark(#e2e8f0, #1e2d45);
47
+ --border-light: light-dark(#cbd5e1, #2a3d58);
48
+ --border-subtle: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.02));
49
+ --border-focus: var(--color-brand);
50
+
51
+ /* ── Accent ── */
52
+ --accent: var(--color-brand);
53
+ --accent-light: var(--color-brand-light);
54
+ --accent-muted: light-dark(rgba(45, 122, 255, 0.1), rgba(45, 122, 255, 0.2));
55
+ --accent-text: light-dark(#1d4ed8, #93c5fd);
56
+
57
+ /* ── Shadows (light defaults — can't use light-dark() for comma-separated values) ── */
58
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02);
59
+ --shadow-md: 0 4px 12px -2px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.03);
60
+ --shadow-lg: 0 12px 32px -6px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.04);
61
+
62
+ /* ── Topology ── */
63
+ --topology-node-border: light-dark(rgba(0, 0, 0, 0.10), rgba(255, 255, 255, 0.08));
64
+ --topology-node-shadow: 0 2px 6px rgba(0,0,0,0.05), 0 6px 16px rgba(0,0,0,0.04);
65
+ --topology-bg-dots: light-dark(rgba(148, 163, 184, 0.25), #1e2d45);
66
+
67
+ /* Topology edge colors (theme-independent identity colors) */
68
+ --edge-routes-to: #22c55e;
69
+ --edge-exposes: #3b82f6;
70
+ --edge-manages: #64748b;
71
+ --edge-configures: #f59e0b;
72
+ --edge-uses: #ec4899;
73
+
74
+ /* ── Group styling ── */
75
+ --group-border-namespace: light-dark(rgba(59, 130, 246, 0.3), rgba(59, 130, 246, 0.4));
76
+ --group-border-app: light-dark(rgba(16, 185, 129, 0.3), rgba(16, 185, 129, 0.4));
77
+ --group-border-label: light-dark(rgba(245, 158, 11, 0.3), rgba(245, 158, 11, 0.4));
78
+
79
+ --group-header-namespace: light-dark(rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.2));
80
+ --group-header-app: light-dark(rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.2));
81
+ --group-header-label: light-dark(rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.2));
82
+
83
+ --group-label-namespace: light-dark(#1d4ed8, #93c5fd);
84
+ --group-label-app: light-dark(#047857, #6ee7b7);
85
+ --group-label-label: light-dark(#b45309, #fcd34d);
86
+
87
+ --group-icon-namespace: light-dark(#2563eb, #60a5fa);
88
+ --group-icon-app: light-dark(#059669, #34d399);
89
+ --group-icon-label: light-dark(#d97706, #fbbf24);
90
+
91
+ /* ── ANSI terminal colors (theme-aware for log viewer) ── */
92
+ --ansi-black: light-dark(#3b3b3b, #4c4c4c);
93
+ --ansi-red: light-dark(#cd3131, #cd3131);
94
+ --ansi-green: light-dark(#00a854, #0dbc79);
95
+ --ansi-yellow: light-dark(#b8860b, #e5e510);
96
+ --ansi-blue: light-dark(#2472c8, #2472c8);
97
+ --ansi-magenta: light-dark(#bc3fbc, #bc3fbc);
98
+ --ansi-cyan: light-dark(#11a8cd, #11a8cd);
99
+ --ansi-white: light-dark(#e5e5e5, #e5e5e5);
100
+ --ansi-bright-black: light-dark(#555555, #767676);
101
+ --ansi-bright-red: light-dark(#f14c4c, #f14c4c);
102
+ --ansi-bright-green: light-dark(#17b96a, #23d18b);
103
+ --ansi-bright-yellow: light-dark(#c49700, #f5f543);
104
+ --ansi-bright-blue: light-dark(#3b8eea, #3b8eea);
105
+ --ansi-bright-magenta: light-dark(#d670d6, #d670d6);
106
+ --ansi-bright-cyan: light-dark(#29b8db, #29b8db);
107
+ --ansi-bright-white: light-dark(#e5e5e5, #e5e5e5);
108
+
109
+ /* ── Scrollbar ── */
110
+ --scrollbar-track: light-dark(#f1f5f9, #162033);
111
+ --scrollbar-thumb: light-dark(#cbd5e1, #2a3d58);
112
+ --scrollbar-thumb-hover: light-dark(#94a3b8, #3d5474);
113
+ }
114
+
115
+ /* Dark overrides for comma-separated values that can't use light-dark() */
116
+ .dark {
117
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.2), 0 1px 2px rgba(0,0,0,0.1);
118
+ --shadow-md: 0 4px 12px -2px rgba(0,0,0,0.25), 0 2px 4px rgba(0,0,0,0.1);
119
+ --shadow-lg: 0 12px 32px -6px rgba(0,0,0,0.3), 0 4px 8px rgba(0,0,0,0.1);
120
+ --topology-node-shadow: 0 2px 6px rgba(0,0,0,0.2), 0 6px 16px rgba(0,0,0,0.15);
121
+ }
package/src/types/core.ts CHANGED
@@ -25,11 +25,13 @@ export interface ResourcePermissions {
25
25
  // Feature capabilities based on RBAC permissions
26
26
  export interface Capabilities {
27
27
  exec: boolean // Terminal feature (pods/exec)
28
+ localTerminal: boolean // Local terminal available (not in-cluster, not disabled)
28
29
  logs: boolean // Log viewer (pods/log)
29
30
  portForward: boolean // Port forwarding (pods/portforward)
30
31
  secrets: boolean // List secrets
31
32
  secretsUpdate: boolean // Update secrets (inline editing)
32
33
  helmWrite: boolean // Helm write operations (install, upgrade, rollback, uninstall, apply values)
34
+ nodeWrite: boolean // Node write operations (cordon, uncordon, drain)
33
35
  mcpEnabled: boolean // MCP server is running
34
36
  resources?: ResourcePermissions // Per-resource-type permissions
35
37
  }
@@ -95,6 +97,7 @@ export type CoreNodeKind =
95
97
  | 'ServersTransportTCP' // Traefik ServersTransportTCP
96
98
  | 'TLSOption' // Traefik TLSOption
97
99
  | 'TLSStore' // Traefik TLSStore
100
+ | 'HTTPProxy' // Contour HTTPProxy
98
101
 
99
102
  // NodeKind can be a core kind or any arbitrary CRD kind string
100
103
  export type NodeKind = CoreNodeKind | (string & {})
@@ -146,10 +149,9 @@ export interface Topology {
146
149
  nodes: TopologyNode[]
147
150
  edges: TopologyEdge[]
148
151
  warnings?: string[] // Warnings about resources that failed to load
149
- truncated?: boolean // True if topology was truncated due to size limit
150
- totalNodes?: number // Total nodes before truncation (only set if truncated)
151
152
  largeCluster?: boolean // True if cluster exceeds large cluster threshold
152
153
  hiddenKinds?: string[] // Resource kinds auto-hidden for performance
154
+ requiresNamespaceFilter?: boolean // True if cluster is too large for all-namespace topology
153
155
  crdDiscoveryStatus?: 'idle' | 'discovering' | 'ready' // CRD discovery status
154
156
  }
155
157
 
@@ -329,6 +331,14 @@ export interface SelectedResource {
329
331
  group?: string // API group for CRDs (e.g., 'metrics.k8s.io')
330
332
  }
331
333
 
334
+ // Resolved envFrom ConfigMap/Secret data for pod env var expansion
335
+ export interface ResolvedEnvFromEntry {
336
+ keys: string[]
337
+ values: Record<string, string>
338
+ isSecret: boolean
339
+ }
340
+ export type ResolvedEnvFrom = Record<string, ResolvedEnvFromEntry>
341
+
332
342
  // Resource reference (for relationships)
333
343
  export interface ResourceRef {
334
344
  kind: string
@@ -745,6 +755,7 @@ export interface TrafficHelmChartInfo {
745
755
  repoUrl: string
746
756
  chartName: string
747
757
  version?: string
758
+ defaultValues?: Record<string, unknown>
748
759
  }
749
760
 
750
761
  // Recommendation for installing a traffic source
@@ -18,7 +18,7 @@ export const CSS_EASE = 'cubic-bezier(0.32, 0.72, 0, 1)'
18
18
  /** Standard UI transitions: overlays, panels, toasts */
19
19
  export const DURATION_NORMAL = 300
20
20
  /** Dock height, subtle layout shifts */
21
- export const DURATION_DOCK = 250
21
+ export const DURATION_DOCK = 150
22
22
  /** Toast exit animation */
23
23
  export const DURATION_TOAST_EXIT = 200
24
24