@skyhook-io/k8s-ui 1.1.1 → 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 +6 -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 +136 -24
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +207 -0
- package/src/components/resources/renderers/PodRenderer.tsx +164 -8
- 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 +135 -2
- package/src/components/shared/ResourceRendererDispatch.tsx +23 -5
- 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/layout.ts +1 -0
- package/src/components/topology/topology.css +1 -0
- package/src/components/ui/CodeViewer.tsx +1 -2
- 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/workload/WorkloadView.tsx +11 -3
- 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 +12 -0
- 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/resource-hierarchy.ts +5 -0
- package/src/utils/resource-icons.ts +3 -0
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
Trash2,
|
|
7
7
|
Play,
|
|
8
8
|
Pause,
|
|
9
|
+
Ban,
|
|
10
|
+
AlertTriangle,
|
|
9
11
|
Box,
|
|
10
12
|
ChevronDown,
|
|
11
13
|
History,
|
|
@@ -16,7 +18,8 @@ import {
|
|
|
16
18
|
} from 'lucide-react'
|
|
17
19
|
import { createTwoFilesPatch } from 'diff'
|
|
18
20
|
import { clsx } from 'clsx'
|
|
19
|
-
import { ForceDeleteConfirmDialog } from '../ui/ForceDeleteConfirmDialog'
|
|
21
|
+
import { ForceDeleteConfirmDialog, type CascadeDependent } from '../ui/ForceDeleteConfirmDialog'
|
|
22
|
+
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
20
23
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
21
24
|
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
22
25
|
import { formatKindName } from '../ui/drawer-components'
|
|
@@ -50,6 +53,8 @@ interface ResourceActionsBarProps {
|
|
|
50
53
|
// Delete
|
|
51
54
|
onDelete?: (params: { kind: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
52
55
|
isDeleting?: boolean
|
|
56
|
+
cascadeDependents?: CascadeDependent[]
|
|
57
|
+
cascadeLoading?: boolean
|
|
53
58
|
|
|
54
59
|
// Workload restart
|
|
55
60
|
onRestart?: (params: { kind: string; namespace: string; name: string }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
@@ -89,6 +94,18 @@ interface ResourceActionsBarProps {
|
|
|
89
94
|
isArgoSuspending?: boolean
|
|
90
95
|
onArgoResume?: (params: { namespace: string; name: string }) => void
|
|
91
96
|
isArgoResuming?: boolean
|
|
97
|
+
|
|
98
|
+
// Node debug shell
|
|
99
|
+
onOpenNodeTerminal?: (params: { nodeName: string }) => void
|
|
100
|
+
|
|
101
|
+
// Node operations (cordon/uncordon/drain)
|
|
102
|
+
canNodeWrite?: boolean
|
|
103
|
+
onCordonNode?: (params: { name: string }) => void
|
|
104
|
+
isCordoningNode?: boolean
|
|
105
|
+
onUncordonNode?: (params: { name: string }) => void
|
|
106
|
+
isUncordoningNode?: boolean
|
|
107
|
+
onDrainNode?: (params: { name: string; options?: { deleteEmptyDirData?: boolean; force?: boolean } }) => void
|
|
108
|
+
isDrainingNode?: boolean
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
export function ResourceActionsBar({
|
|
@@ -96,7 +113,7 @@ export function ResourceActionsBar({
|
|
|
96
113
|
canExec, canViewLogs, canPortForward,
|
|
97
114
|
onOpenTerminal, onOpenLogs: openLogs, onOpenWorkloadLogs: openWorkloadLogs, onCopyCommand,
|
|
98
115
|
renderPortForward,
|
|
99
|
-
onDelete, isDeleting,
|
|
116
|
+
onDelete, isDeleting, cascadeDependents, cascadeLoading,
|
|
100
117
|
onRestart, isRestarting,
|
|
101
118
|
revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
|
|
102
119
|
onTriggerCronJob, isTriggeringCronJob,
|
|
@@ -110,12 +127,22 @@ export function ResourceActionsBar({
|
|
|
110
127
|
onArgoRefresh, isArgoRefreshing,
|
|
111
128
|
onArgoSuspend, isArgoSuspending,
|
|
112
129
|
onArgoResume, isArgoResuming,
|
|
130
|
+
onOpenNodeTerminal,
|
|
131
|
+
canNodeWrite,
|
|
132
|
+
onCordonNode, isCordoningNode,
|
|
133
|
+
onUncordonNode, isUncordoningNode,
|
|
134
|
+
onDrainNode, isDrainingNode,
|
|
113
135
|
}: ResourceActionsBarProps) {
|
|
114
136
|
const kind = resource.kind.toLowerCase()
|
|
115
137
|
|
|
116
138
|
// Delete confirmation state
|
|
117
139
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
118
140
|
|
|
141
|
+
// Node operation confirmation state
|
|
142
|
+
const [showCordonConfirm, setShowCordonConfirm] = useState(false)
|
|
143
|
+
const [showDrainConfirm, setShowDrainConfirm] = useState(false)
|
|
144
|
+
const [drainForce, setDrainForce] = useState(false)
|
|
145
|
+
|
|
119
146
|
// Rollback dialog state
|
|
120
147
|
const [showRevisions, setShowRevisions] = useState(false)
|
|
121
148
|
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind)
|
|
@@ -227,6 +254,60 @@ export function ResourceActionsBar({
|
|
|
227
254
|
</>
|
|
228
255
|
)}
|
|
229
256
|
|
|
257
|
+
{/* Node actions */}
|
|
258
|
+
{kind === 'nodes' && (
|
|
259
|
+
<>
|
|
260
|
+
{canExec && onOpenNodeTerminal && (
|
|
261
|
+
<button
|
|
262
|
+
onClick={() => onOpenNodeTerminal({ nodeName: resource.name })}
|
|
263
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
|
264
|
+
>
|
|
265
|
+
<Terminal className="w-3.5 h-3.5" />
|
|
266
|
+
Debug Shell
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{canNodeWrite && (
|
|
271
|
+
<>
|
|
272
|
+
{data?.spec?.unschedulable ? (
|
|
273
|
+
onUncordonNode && (
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => onUncordonNode({ name: resource.name })}
|
|
276
|
+
disabled={isUncordoningNode}
|
|
277
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-emerald-600 hover:bg-emerald-700 rounded-lg transition-colors disabled:opacity-50"
|
|
278
|
+
>
|
|
279
|
+
<Play className={`w-3.5 h-3.5 ${isUncordoningNode ? 'animate-pulse' : ''}`} />
|
|
280
|
+
{isUncordoningNode ? 'Uncordoning...' : 'Uncordon'}
|
|
281
|
+
</button>
|
|
282
|
+
)
|
|
283
|
+
) : (
|
|
284
|
+
onCordonNode && (
|
|
285
|
+
<button
|
|
286
|
+
onClick={() => setShowCordonConfirm(true)}
|
|
287
|
+
disabled={isCordoningNode}
|
|
288
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-lg transition-colors disabled:opacity-50"
|
|
289
|
+
>
|
|
290
|
+
<Ban className={`w-3.5 h-3.5 ${isCordoningNode ? 'animate-pulse' : ''}`} />
|
|
291
|
+
{isCordoningNode ? 'Cordoning...' : 'Cordon'}
|
|
292
|
+
</button>
|
|
293
|
+
)
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{onDrainNode && (
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => setShowDrainConfirm(true)}
|
|
299
|
+
disabled={isDrainingNode}
|
|
300
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors disabled:opacity-50"
|
|
301
|
+
>
|
|
302
|
+
<AlertTriangle className={`w-3.5 h-3.5 ${isDrainingNode ? 'animate-pulse' : ''}`} />
|
|
303
|
+
{isDrainingNode ? 'Draining...' : 'Drain'}
|
|
304
|
+
</button>
|
|
305
|
+
)}
|
|
306
|
+
</>
|
|
307
|
+
)}
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
|
|
230
311
|
{/* Service actions */}
|
|
231
312
|
{kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && canPortForward && resource.namespace && resource.name && renderPortForward && (
|
|
232
313
|
renderPortForward({
|
|
@@ -419,8 +500,60 @@ export function ResourceActionsBar({
|
|
|
419
500
|
resourceKind={formatKindName(resource.kind)}
|
|
420
501
|
namespaceName={resource.namespace}
|
|
421
502
|
isLoading={isDeleting ?? false}
|
|
503
|
+
cascadeDependents={cascadeDependents}
|
|
504
|
+
cascadeLoading={cascadeLoading}
|
|
505
|
+
/>
|
|
506
|
+
|
|
507
|
+
{/* Node cordon confirmation */}
|
|
508
|
+
<ConfirmDialog
|
|
509
|
+
open={showCordonConfirm}
|
|
510
|
+
onClose={() => setShowCordonConfirm(false)}
|
|
511
|
+
onConfirm={() => {
|
|
512
|
+
onCordonNode?.({ name: resource.name })
|
|
513
|
+
setShowCordonConfirm(false)
|
|
514
|
+
}}
|
|
515
|
+
title="Cordon Node"
|
|
516
|
+
message={`Mark node "${resource.name}" as unschedulable? No new pods will be scheduled on this node.`}
|
|
517
|
+
confirmLabel="Cordon"
|
|
518
|
+
variant="warning"
|
|
519
|
+
isLoading={isCordoningNode}
|
|
422
520
|
/>
|
|
423
521
|
|
|
522
|
+
{/* Node drain confirmation */}
|
|
523
|
+
<ConfirmDialog
|
|
524
|
+
open={showDrainConfirm}
|
|
525
|
+
onClose={() => {
|
|
526
|
+
setShowDrainConfirm(false)
|
|
527
|
+
setDrainForce(false)
|
|
528
|
+
}}
|
|
529
|
+
onConfirm={() => {
|
|
530
|
+
onDrainNode?.({
|
|
531
|
+
name: resource.name,
|
|
532
|
+
options: { deleteEmptyDirData: true, force: drainForce || undefined },
|
|
533
|
+
})
|
|
534
|
+
setShowDrainConfirm(false)
|
|
535
|
+
setDrainForce(false)
|
|
536
|
+
}}
|
|
537
|
+
title="Drain Node"
|
|
538
|
+
message={`Cordon and evict all pods from node "${resource.name}"? DaemonSet pods will be skipped.`}
|
|
539
|
+
confirmLabel={isDrainingNode ? 'Draining...' : 'Drain'}
|
|
540
|
+
variant="danger"
|
|
541
|
+
isLoading={isDrainingNode}
|
|
542
|
+
isClosable
|
|
543
|
+
>
|
|
544
|
+
<div className="flex flex-col gap-2 text-sm text-theme-text-secondary">
|
|
545
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
546
|
+
<input
|
|
547
|
+
type="checkbox"
|
|
548
|
+
checked={drainForce}
|
|
549
|
+
onChange={(e) => setDrainForce(e.target.checked)}
|
|
550
|
+
className="rounded border-theme-border"
|
|
551
|
+
/>
|
|
552
|
+
Force (evict pods not managed by a controller)
|
|
553
|
+
</label>
|
|
554
|
+
</div>
|
|
555
|
+
</ConfirmDialog>
|
|
556
|
+
|
|
424
557
|
{showRevisions && ['deployments', 'statefulsets', 'daemonsets'].includes(kind) && (
|
|
425
558
|
<RevisionHistoryDialog
|
|
426
559
|
kind={resource.kind}
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
getKnativeConditionStatus,
|
|
65
65
|
getRevisionStatus,
|
|
66
66
|
} from '../resources/resource-utils-knative'
|
|
67
|
+
import { getHTTPProxyStatus } from '../resources/resource-utils-contour'
|
|
67
68
|
import {
|
|
68
69
|
PodRenderer,
|
|
69
70
|
WorkloadRenderer,
|
|
@@ -169,8 +170,9 @@ import {
|
|
|
169
170
|
RuntimeClassRenderer,
|
|
170
171
|
LeaseRenderer,
|
|
171
172
|
TraefikIngressRouteRenderer,
|
|
173
|
+
ContourHTTPProxyRenderer,
|
|
172
174
|
} from '../resources/renderers'
|
|
173
|
-
import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo } from '../../types'
|
|
175
|
+
import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo, ResolvedEnvFrom } from '../../types'
|
|
174
176
|
import type { CopyHandler } from '../ui/drawer-components'
|
|
175
177
|
|
|
176
178
|
/**
|
|
@@ -186,6 +188,7 @@ export interface RendererOverrides {
|
|
|
186
188
|
data: any; onCopy: CopyHandler; copied: string | null
|
|
187
189
|
onNavigate?: (ref: ResourceRef) => void
|
|
188
190
|
onOpenLogs?: (podName: string, containerName: string) => void
|
|
191
|
+
resolvedEnvFrom?: ResolvedEnvFrom
|
|
189
192
|
}>
|
|
190
193
|
NodeRenderer?: React.ComponentType<{
|
|
191
194
|
data: any; relationships?: Relationships
|
|
@@ -233,6 +236,7 @@ const KNOWN_KINDS = new Set([
|
|
|
233
236
|
'channels', 'inmemorychannels', 'subscriptions', 'sequences', 'parallels',
|
|
234
237
|
'knativeingresses', 'knativecertificates', 'serverlessservices', 'domainmappings',
|
|
235
238
|
'ingressroutes', 'ingressroutetcps', 'ingressrouteudps',
|
|
239
|
+
'httpproxies',
|
|
236
240
|
])
|
|
237
241
|
|
|
238
242
|
// ============================================================================
|
|
@@ -255,6 +259,10 @@ interface ResourceRendererDispatchProps {
|
|
|
255
259
|
showMetrics?: boolean
|
|
256
260
|
/** When provided, container-level Logs buttons call this instead of opening the dock */
|
|
257
261
|
onOpenLogs?: (podName: string, containerName: string) => void
|
|
262
|
+
/** Resolved ConfigMap/Secret data for envFrom expansion in PodRenderer */
|
|
263
|
+
resolvedEnvFrom?: ResolvedEnvFrom
|
|
264
|
+
/** Platform-specific renderer overrides (e.g. with hooks for metrics, exec, port-forward) */
|
|
265
|
+
rendererOverrides?: RendererOverrides
|
|
258
266
|
/** Optional hint shown in the Events section (e.g. link to Timeline tab) */
|
|
259
267
|
eventsHint?: React.ReactNode
|
|
260
268
|
/** When provided, sidebar sections (related resources, events, labels, annotations, metadata) are passed to this render prop instead of being rendered inline */
|
|
@@ -265,8 +273,6 @@ interface ResourceRendererDispatchProps {
|
|
|
265
273
|
eventsLoading?: boolean
|
|
266
274
|
/** Render prop for Prometheus metrics charts — injected by the platform wrapper */
|
|
267
275
|
renderMetrics?: (props: { kind: string; namespace: string; name: string }) => React.ReactNode
|
|
268
|
-
/** Platform-specific renderer overrides (e.g. with hooks for metrics, exec, port-forward) */
|
|
269
|
-
rendererOverrides?: RendererOverrides
|
|
270
276
|
}
|
|
271
277
|
|
|
272
278
|
export function ResourceRendererDispatch({
|
|
@@ -287,13 +293,13 @@ export function ResourceRendererDispatch({
|
|
|
287
293
|
events,
|
|
288
294
|
eventsLoading,
|
|
289
295
|
renderMetrics,
|
|
296
|
+
resolvedEnvFrom,
|
|
290
297
|
rendererOverrides,
|
|
291
298
|
}: ResourceRendererDispatchProps) {
|
|
292
299
|
const kind = resource.kind.toLowerCase()
|
|
293
300
|
|
|
294
301
|
const isKnownKind = KNOWN_KINDS.has(kind)
|
|
295
302
|
|
|
296
|
-
// Resolve renderer components — use platform override when provided, otherwise base
|
|
297
303
|
const PodComp = rendererOverrides?.PodRenderer ?? PodRenderer
|
|
298
304
|
const WorkloadComp = rendererOverrides?.WorkloadRenderer ?? WorkloadRenderer
|
|
299
305
|
const NodeComp = rendererOverrides?.NodeRenderer ?? NodeRenderer
|
|
@@ -313,7 +319,7 @@ export function ResourceRendererDispatch({
|
|
|
313
319
|
<div className={renderSidebar ? 'lg:flex' : ''}>
|
|
314
320
|
<div className={clsx('p-4 space-y-4', renderSidebar && 'lg:flex-1 lg:min-w-0')}>
|
|
315
321
|
{/* Kind-specific content - delegates to modular renderers */}
|
|
316
|
-
{kind === 'pods' && <PodComp data={data} onCopy={onCopy} copied={copied} onNavigate={onNavigate} onOpenLogs={onOpenLogs} />}
|
|
322
|
+
{kind === 'pods' && <PodComp data={data} onCopy={onCopy} copied={copied} onNavigate={onNavigate} onOpenLogs={onOpenLogs} resolvedEnvFrom={resolvedEnvFrom} />}
|
|
317
323
|
{['deployments', 'statefulsets', 'daemonsets'].includes(kind) && <WorkloadComp kind={kind} data={data} onNavigate={onNavigate} />}
|
|
318
324
|
{kind === 'replicasets' && <ReplicaSetRenderer data={data} />}
|
|
319
325
|
{kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && <ServiceComp data={data} onCopy={onCopy} copied={copied} />}
|
|
@@ -426,6 +432,9 @@ export function ResourceRendererDispatch({
|
|
|
426
432
|
{/* Traefik */}
|
|
427
433
|
{(kind === 'ingressroutes' || kind === 'ingressroutetcps' || kind === 'ingressrouteudps') && <TraefikIngressRouteRenderer data={data} onNavigate={onNavigate} />}
|
|
428
434
|
|
|
435
|
+
{/* Contour */}
|
|
436
|
+
{kind === 'httpproxies' && <ContourHTTPProxyRenderer data={data} onNavigate={onNavigate} />}
|
|
437
|
+
|
|
429
438
|
{/* Generic renderer for CRDs and unknown resource types */}
|
|
430
439
|
{!isKnownKind && <GenericRenderer data={data} />}
|
|
431
440
|
|
|
@@ -538,6 +547,15 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
538
547
|
if (k === 'peerauthentications') return getPeerAuthenticationStatus(data)
|
|
539
548
|
if (k === 'authorizationpolicies') return getAuthorizationPolicyStatus(data)
|
|
540
549
|
|
|
550
|
+
// Contour HTTPProxy
|
|
551
|
+
if (k === 'httpproxies') {
|
|
552
|
+
const s = getHTTPProxyStatus(data)
|
|
553
|
+
if (s.status === 'healthy') return { text: s.label, color: 'bg-green-500/20 text-green-400' }
|
|
554
|
+
if (s.status === 'unhealthy') return { text: s.label, color: 'bg-red-500/20 text-red-400' }
|
|
555
|
+
if (s.status === 'degraded') return { text: s.label, color: 'bg-yellow-500/20 text-yellow-400' }
|
|
556
|
+
return null
|
|
557
|
+
}
|
|
558
|
+
|
|
541
559
|
// Knative Revisions have custom status logic (scaled-to-zero, activating)
|
|
542
560
|
if (k === 'knativerevisions') {
|
|
543
561
|
const status = getRevisionStatus(data)
|
|
@@ -123,19 +123,19 @@ export function HealthSpanLegend() {
|
|
|
123
123
|
return (
|
|
124
124
|
<div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
|
|
125
125
|
<span className="flex items-center gap-1">
|
|
126
|
-
<span className="w-4 h-2 rounded-sm bg-green-500/60" />
|
|
126
|
+
<span className="w-4 h-2 rounded-sm bg-green-500/60 dark:bg-green-600/60" />
|
|
127
127
|
<span>Healthy</span>
|
|
128
128
|
</span>
|
|
129
129
|
<span className="flex items-center gap-1">
|
|
130
|
-
<span className="w-4 h-2 rounded-sm bg-blue-500/60" />
|
|
130
|
+
<span className="w-4 h-2 rounded-sm bg-blue-500/60 dark:bg-blue-500/60" />
|
|
131
131
|
<span>Rolling</span>
|
|
132
132
|
</span>
|
|
133
133
|
<span className="flex items-center gap-1">
|
|
134
|
-
<span className="w-4 h-2 rounded-sm bg-amber-500/60" />
|
|
134
|
+
<span className="w-4 h-2 rounded-sm bg-amber-500/60 dark:bg-[#b8861e]" />
|
|
135
135
|
<span>Degraded</span>
|
|
136
136
|
</span>
|
|
137
137
|
<span className="flex items-center gap-1">
|
|
138
|
-
<span className="w-4 h-2 rounded-sm bg-red-500/60" />
|
|
138
|
+
<span className="w-4 h-2 rounded-sm bg-red-500/60 dark:bg-red-500/60" />
|
|
139
139
|
<span>Unhealthy</span>
|
|
140
140
|
</span>
|
|
141
141
|
</div>
|
|
@@ -402,15 +402,13 @@ export function HealthSpan({ health, left, width, title, createdBefore }: Health
|
|
|
402
402
|
const getHealthColor = () => {
|
|
403
403
|
switch (health) {
|
|
404
404
|
case 'healthy':
|
|
405
|
-
return 'bg-green-500/60'
|
|
405
|
+
return 'bg-green-500/60 dark:bg-green-600/60'
|
|
406
406
|
case 'rolling':
|
|
407
|
-
|
|
408
|
-
return 'bg-blue-500/60'
|
|
407
|
+
return 'bg-blue-500/60 dark:bg-blue-500/60'
|
|
409
408
|
case 'degraded':
|
|
410
|
-
|
|
411
|
-
return 'bg-amber-500/60'
|
|
409
|
+
return 'bg-amber-500/60 dark:bg-[#b8861e]'
|
|
412
410
|
case 'unhealthy':
|
|
413
|
-
return 'bg-red-500/60'
|
|
411
|
+
return 'bg-red-500/60 dark:bg-red-500/60'
|
|
414
412
|
default:
|
|
415
413
|
// Unknown or other states
|
|
416
414
|
return 'bg-gray-400/40'
|
|
@@ -134,6 +134,7 @@ export const NODE_DIMENSIONS: Record<NodeKind, { width: number; height: number }
|
|
|
134
134
|
ServersTransportTCP: { width: 300, height: 56 },
|
|
135
135
|
TLSOption: { width: 280, height: 56 },
|
|
136
136
|
TLSStore: { width: 280, height: 56 },
|
|
137
|
+
HTTPProxy: { width: 280, height: 56 }, // Contour
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
|
|
@@ -77,6 +77,9 @@ const RESOURCE_KINDS: {
|
|
|
77
77
|
{ kind: 'ServersTransportTCP', label: 'Transport TCP', icon: getTopologyIcon('ServersTransportTCP'), color: 'text-cyan-400', category: 'custom' },
|
|
78
78
|
{ kind: 'TLSOption', label: 'TLS Option', icon: getTopologyIcon('TLSOption'), color: 'text-cyan-400', category: 'custom' },
|
|
79
79
|
{ kind: 'TLSStore', label: 'TLS Store', icon: getTopologyIcon('TLSStore'), color: 'text-cyan-400', category: 'custom' },
|
|
80
|
+
|
|
81
|
+
// Contour
|
|
82
|
+
{ kind: 'HTTPProxy', label: 'HTTPProxy', icon: getTopologyIcon('HTTPProxy'), color: 'text-violet-400', category: 'networking' },
|
|
80
83
|
]
|
|
81
84
|
|
|
82
85
|
const CATEGORIES = [
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
.topology-icon-nodepool { background: #6366f1; }
|
|
97
97
|
.topology-icon-nodeclaim { background: #6366f1; }
|
|
98
98
|
.topology-icon-nodeclass { background: #6366f1; }
|
|
99
|
+
.topology-icon-httpproxy { background: #7c3aed; }
|
|
99
100
|
|
|
100
101
|
/* Background dots - uses CSS variable for theme awareness */
|
|
101
102
|
.react-flow__background pattern circle {
|
|
@@ -401,8 +401,7 @@ export function CodeViewer({
|
|
|
401
401
|
</div>
|
|
402
402
|
|
|
403
403
|
<style>{`
|
|
404
|
-
:root
|
|
405
|
-
:root[data-theme="dark"] { --code-line-number: #484f58; }
|
|
404
|
+
:root { --code-line-number: light-dark(#8b949e, #484f58); }
|
|
406
405
|
.shiki-viewer pre {
|
|
407
406
|
margin: 0;
|
|
408
407
|
padding: 12px;
|
|
@@ -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)
|