@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.
- package/package.json +8 -2
- package/src/components/dock/BottomDock.tsx +14 -15
- package/src/components/dock/DockContext.tsx +40 -1
- package/src/components/dock/LocalTerminalTab.tsx +241 -0
- package/src/components/dock/NodeTerminalTab.tsx +129 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +18 -67
- package/src/components/logs/LogCore.tsx +30 -4
- package/src/components/logs/StructuredLogLine.tsx +168 -0
- package/src/components/logs/index.ts +1 -1
- package/src/components/logs/useLogBuffer.ts +16 -10
- package/src/components/resources/ResourcesView.tsx +186 -67
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
- package/src/components/resources/renderers/HPARenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +2 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +7 -6
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +3 -2
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.tsx +164 -8
- package/src/components/resources/renderers/VPARenderer.tsx +2 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +53 -55
- package/src/components/resources/renderers/contour-cells.tsx +48 -0
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/renderers/trivy-shared.tsx +9 -25
- package/src/components/resources/resource-utils-contour.ts +34 -0
- package/src/components/resources/resource-utils.ts +91 -0
- package/src/components/shared/ResourceActionsBar.tsx +276 -144
- package/src/components/shared/ResourceRendererDispatch.tsx +59 -5
- package/src/components/shared/index.ts +1 -1
- package/src/components/timeline/shared.tsx +8 -10
- package/src/components/topology/K8sResourceNode.tsx +1 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +3 -0
- package/src/components/topology/TopologyGraph.tsx +2 -17
- package/src/components/topology/layout.ts +1 -0
- package/src/components/topology/topology.css +1 -0
- package/src/components/ui/CodeViewer.tsx +1 -2
- package/src/components/ui/ConfirmDialog.tsx +85 -130
- package/src/components/ui/DialogPortal.tsx +82 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +84 -10
- package/src/components/ui/Toast.tsx +5 -5
- package/src/components/ui/drawer-components.tsx +28 -22
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/WorkloadView.tsx +30 -2
- package/src/theme/index.ts +3 -0
- package/src/theme/tailwind-theme.css +61 -0
- package/src/theme/variables.css +121 -0
- package/src/types/core.ts +13 -2
- package/src/utils/animation.ts +1 -1
- package/src/utils/badge-colors.ts +158 -123
- package/src/utils/log-format.ts +127 -35
- package/src/utils/navigation.test.ts +142 -0
- package/src/utils/navigation.ts +52 -47
- package/src/utils/resource-hierarchy.ts +5 -0
- 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
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
168
|
-
warning: { bg: 'bg-yellow-500/10', border: 'border-yellow-500/30', title: 'text-yellow-400', message: 'text-yellow-
|
|
169
|
-
info: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', title: 'text-blue-400', message: 'text-blue-
|
|
170
|
-
success: { bg: 'bg-green-500/10', border: 'border-green-500/30', title: 'text-green-400', message: 'text-green-
|
|
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-
|
|
457
|
-
if (k.includes('deployment')) return 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-
|
|
458
|
-
if (k.includes('service')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-
|
|
459
|
-
if (k.includes('gateway')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-
|
|
460
|
-
if (k.includes('route')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-
|
|
461
|
-
if (k.includes('ingress')) return 'bg-violet-500/20 text-violet-700 dark:text-violet-
|
|
462
|
-
if (k.includes('configmap')) return 'bg-amber-500/20 text-amber-700 dark:text-amber-
|
|
463
|
-
if (k.endsWith('report')) return 'bg-rose-500/20 text-rose-700 dark:text-rose-
|
|
464
|
-
if (k.includes('secret')) return 'bg-red-500/20 text-red-700 dark:text-red-
|
|
465
|
-
if (k.includes('daemonset')) return 'bg-teal-500/20 text-teal-700 dark:text-teal-
|
|
466
|
-
if (k.includes('statefulset')) return 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-
|
|
467
|
-
if (k.includes('replicaset')) return 'bg-green-500/20 text-green-700 dark:text-green-
|
|
468
|
-
if (k.includes('hpa') || k.includes('horizontalpodautoscaler')) return 'bg-pink-500/20 text-pink-700 dark:text-pink-
|
|
469
|
-
if (k.includes('cronjob')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-
|
|
470
|
-
if (k.includes('job')) return 'bg-orange-500/20 text-orange-700 dark:text-orange-
|
|
471
|
-
if (k.includes('node')) return 'bg-gray-500/20 text-gray-700 dark:text-gray-
|
|
472
|
-
if (k.includes('namespace')) return 'bg-blue-500/20 text-blue-700 dark:text-blue-
|
|
473
|
-
if (k.includes('persistentvolume')) return 'bg-purple-500/20 text-purple-700 dark:text-purple-
|
|
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)
|
|
@@ -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,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
|
package/src/utils/animation.ts
CHANGED
|
@@ -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 =
|
|
21
|
+
export const DURATION_DOCK = 150
|
|
22
22
|
/** Toast exit animation */
|
|
23
23
|
export const DURATION_TOAST_EXIT = 200
|
|
24
24
|
|