@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Contour HTTPProxy CRD utility functions for resource list cells and detail renderers
|
|
2
|
+
|
|
3
|
+
export function getHTTPProxyFQDN(resource: any): string {
|
|
4
|
+
return resource.spec?.virtualhost?.fqdn || '-'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getHTTPProxyStatus(resource: any): { status: string; label: string } {
|
|
8
|
+
const currentStatus = resource.status?.currentStatus?.toLowerCase()
|
|
9
|
+
if (currentStatus === 'valid') return { status: 'healthy', label: 'Valid' }
|
|
10
|
+
if (currentStatus === 'invalid') return { status: 'unhealthy', label: 'Invalid' }
|
|
11
|
+
if (currentStatus === 'orphaned') return { status: 'degraded', label: 'Orphaned' }
|
|
12
|
+
return { status: 'unknown', label: '-' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getHTTPProxyRouteCount(resource: any): number {
|
|
16
|
+
return resource.spec?.routes?.length || 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getHTTPProxyServiceCount(resource: any): number {
|
|
20
|
+
let count = 0
|
|
21
|
+
for (const route of resource.spec?.routes || []) {
|
|
22
|
+
count += (route.services || []).length
|
|
23
|
+
}
|
|
24
|
+
count += (resource.spec?.tcpproxy?.services || []).length
|
|
25
|
+
return count
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getHTTPProxyIncludeCount(resource: any): number {
|
|
29
|
+
return resource.spec?.includes?.length || 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasHTTPProxyTLS(resource: any): boolean {
|
|
33
|
+
return !!resource.spec?.virtualhost?.tls
|
|
34
|
+
}
|
|
@@ -197,6 +197,97 @@ export function getPodRestarts(pod: any): number {
|
|
|
197
197
|
return containerStatuses.reduce((sum: number, c: any) => sum + (c.restartCount || 0), 0)
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
export interface ContainerSquareState {
|
|
201
|
+
name: string
|
|
202
|
+
status: 'ready' | 'running' | 'waiting' | 'completed' | 'terminated' | 'unknown'
|
|
203
|
+
restarts: number
|
|
204
|
+
reason?: string
|
|
205
|
+
message?: string
|
|
206
|
+
exitCode?: number
|
|
207
|
+
startedAt?: string
|
|
208
|
+
finishedAt?: string
|
|
209
|
+
isInit?: boolean
|
|
210
|
+
/** Last termination info — crucial for debugging CrashLoopBackOff */
|
|
211
|
+
lastTermination?: {
|
|
212
|
+
reason?: string
|
|
213
|
+
exitCode?: number
|
|
214
|
+
startedAt?: string
|
|
215
|
+
finishedAt?: string
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function getContainerSquareStates(pod: any): ContainerSquareState[] {
|
|
220
|
+
const result: ContainerSquareState[] = []
|
|
221
|
+
const initStatuses = pod.status?.initContainerStatuses || []
|
|
222
|
+
const containerStatuses = pod.status?.containerStatuses || []
|
|
223
|
+
const specContainers = pod.spec?.containers || []
|
|
224
|
+
|
|
225
|
+
for (const cs of initStatuses) {
|
|
226
|
+
const stateKey = cs.state ? Object.keys(cs.state)[0] : 'unknown'
|
|
227
|
+
let status: ContainerSquareState['status'] = 'unknown'
|
|
228
|
+
if (stateKey === 'terminated' && cs.state?.terminated?.exitCode === 0) {
|
|
229
|
+
status = 'completed'
|
|
230
|
+
} else if (stateKey === 'running') {
|
|
231
|
+
status = cs.ready ? 'ready' : 'running'
|
|
232
|
+
} else if (stateKey === 'waiting') {
|
|
233
|
+
status = 'waiting'
|
|
234
|
+
} else if (stateKey === 'terminated') {
|
|
235
|
+
status = 'terminated'
|
|
236
|
+
}
|
|
237
|
+
const stateDetail = cs.state?.[stateKey]
|
|
238
|
+
const lastTerm = cs.lastState?.terminated
|
|
239
|
+
result.push({
|
|
240
|
+
name: cs.name,
|
|
241
|
+
status,
|
|
242
|
+
restarts: cs.restartCount || 0,
|
|
243
|
+
reason: stateDetail?.reason,
|
|
244
|
+
message: stateDetail?.message,
|
|
245
|
+
exitCode: stateDetail?.exitCode,
|
|
246
|
+
startedAt: stateDetail?.startedAt,
|
|
247
|
+
finishedAt: stateDetail?.finishedAt,
|
|
248
|
+
isInit: true,
|
|
249
|
+
lastTermination: lastTerm ? { reason: lastTerm.reason, exitCode: lastTerm.exitCode, startedAt: lastTerm.startedAt, finishedAt: lastTerm.finishedAt } : undefined,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (containerStatuses.length > 0) {
|
|
254
|
+
for (const cs of containerStatuses) {
|
|
255
|
+
const stateKey = cs.state ? Object.keys(cs.state)[0] : 'unknown'
|
|
256
|
+
let status: ContainerSquareState['status'] = 'unknown'
|
|
257
|
+
if (stateKey === 'running' && cs.ready) {
|
|
258
|
+
status = 'ready'
|
|
259
|
+
} else if (stateKey === 'running') {
|
|
260
|
+
status = 'running'
|
|
261
|
+
} else if (stateKey === 'terminated' && cs.state?.terminated?.exitCode === 0) {
|
|
262
|
+
status = 'completed'
|
|
263
|
+
} else if (stateKey === 'terminated') {
|
|
264
|
+
status = 'terminated'
|
|
265
|
+
} else if (stateKey === 'waiting') {
|
|
266
|
+
status = 'waiting'
|
|
267
|
+
}
|
|
268
|
+
const stateDetail = cs.state?.[stateKey]
|
|
269
|
+
const lastTerm = cs.lastState?.terminated
|
|
270
|
+
result.push({
|
|
271
|
+
name: cs.name,
|
|
272
|
+
status,
|
|
273
|
+
restarts: cs.restartCount || 0,
|
|
274
|
+
reason: stateDetail?.reason,
|
|
275
|
+
message: stateDetail?.message,
|
|
276
|
+
exitCode: stateDetail?.exitCode,
|
|
277
|
+
startedAt: stateDetail?.startedAt,
|
|
278
|
+
finishedAt: stateDetail?.finishedAt,
|
|
279
|
+
lastTermination: lastTerm ? { reason: lastTerm.reason, exitCode: lastTerm.exitCode, startedAt: lastTerm.startedAt, finishedAt: lastTerm.finishedAt } : undefined,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
for (const c of specContainers) {
|
|
284
|
+
result.push({ name: c.name, status: 'unknown', restarts: 0 })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result
|
|
289
|
+
}
|
|
290
|
+
|
|
200
291
|
// ============================================================================
|
|
201
292
|
// WORKLOAD UTILITIES (Deployment, StatefulSet, DaemonSet, ReplicaSet)
|
|
202
293
|
// ============================================================================
|
|
@@ -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,9 @@ 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'
|
|
23
|
+
import { DialogPortal } from '../ui/DialogPortal'
|
|
20
24
|
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
21
25
|
import { formatKindName } from '../ui/drawer-components'
|
|
22
26
|
|
|
@@ -49,6 +53,8 @@ interface ResourceActionsBarProps {
|
|
|
49
53
|
// Delete
|
|
50
54
|
onDelete?: (params: { kind: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
51
55
|
isDeleting?: boolean
|
|
56
|
+
cascadeDependents?: CascadeDependent[]
|
|
57
|
+
cascadeLoading?: boolean
|
|
52
58
|
|
|
53
59
|
// Workload restart
|
|
54
60
|
onRestart?: (params: { kind: string; namespace: string; name: string }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
@@ -88,6 +94,18 @@ interface ResourceActionsBarProps {
|
|
|
88
94
|
isArgoSuspending?: boolean
|
|
89
95
|
onArgoResume?: (params: { namespace: string; name: string }) => void
|
|
90
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
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
export function ResourceActionsBar({
|
|
@@ -95,7 +113,7 @@ export function ResourceActionsBar({
|
|
|
95
113
|
canExec, canViewLogs, canPortForward,
|
|
96
114
|
onOpenTerminal, onOpenLogs: openLogs, onOpenWorkloadLogs: openWorkloadLogs, onCopyCommand,
|
|
97
115
|
renderPortForward,
|
|
98
|
-
onDelete, isDeleting,
|
|
116
|
+
onDelete, isDeleting, cascadeDependents, cascadeLoading,
|
|
99
117
|
onRestart, isRestarting,
|
|
100
118
|
revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
|
|
101
119
|
onTriggerCronJob, isTriggeringCronJob,
|
|
@@ -109,12 +127,22 @@ export function ResourceActionsBar({
|
|
|
109
127
|
onArgoRefresh, isArgoRefreshing,
|
|
110
128
|
onArgoSuspend, isArgoSuspending,
|
|
111
129
|
onArgoResume, isArgoResuming,
|
|
130
|
+
onOpenNodeTerminal,
|
|
131
|
+
canNodeWrite,
|
|
132
|
+
onCordonNode, isCordoningNode,
|
|
133
|
+
onUncordonNode, isUncordoningNode,
|
|
134
|
+
onDrainNode, isDrainingNode,
|
|
112
135
|
}: ResourceActionsBarProps) {
|
|
113
136
|
const kind = resource.kind.toLowerCase()
|
|
114
137
|
|
|
115
138
|
// Delete confirmation state
|
|
116
139
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
117
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
|
+
|
|
118
146
|
// Rollback dialog state
|
|
119
147
|
const [showRevisions, setShowRevisions] = useState(false)
|
|
120
148
|
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind)
|
|
@@ -226,6 +254,60 @@ export function ResourceActionsBar({
|
|
|
226
254
|
</>
|
|
227
255
|
)}
|
|
228
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
|
+
|
|
229
311
|
{/* Service actions */}
|
|
230
312
|
{kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && canPortForward && resource.namespace && resource.name && renderPortForward && (
|
|
231
313
|
renderPortForward({
|
|
@@ -418,8 +500,60 @@ export function ResourceActionsBar({
|
|
|
418
500
|
resourceKind={formatKindName(resource.kind)}
|
|
419
501
|
namespaceName={resource.namespace}
|
|
420
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}
|
|
421
520
|
/>
|
|
422
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
|
+
|
|
423
557
|
{showRevisions && ['deployments', 'statefulsets', 'daemonsets'].includes(kind) && (
|
|
424
558
|
<RevisionHistoryDialog
|
|
425
559
|
kind={resource.kind}
|
|
@@ -619,7 +753,7 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
619
753
|
const [confirmRevision, setConfirmRevision] = useState<number | null>(null)
|
|
620
754
|
const [diffRevision, setDiffRevision] = useState<number | null>(null)
|
|
621
755
|
|
|
622
|
-
|
|
756
|
+
const handleClose = () => { setDiffRevision(null); onClose() }
|
|
623
757
|
|
|
624
758
|
const currentRevision = revisions?.find(r => r.isCurrent)
|
|
625
759
|
const selectedRevision = revisions?.find(r => r.number === diffRevision)
|
|
@@ -660,160 +794,158 @@ export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, re
|
|
|
660
794
|
}
|
|
661
795
|
|
|
662
796
|
return (
|
|
663
|
-
<
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
<div className={clsx(
|
|
670
|
-
"relative bg-theme-surface border border-theme-border rounded-lg shadow-2xl mx-4 outline-none flex flex-col",
|
|
797
|
+
<DialogPortal
|
|
798
|
+
open={open}
|
|
799
|
+
onClose={handleClose}
|
|
800
|
+
closable={!isRollingBack}
|
|
801
|
+
className={clsx(
|
|
802
|
+
"flex flex-col",
|
|
671
803
|
diffRevision ? "max-w-5xl w-full max-h-[85vh]" : "max-w-lg w-full"
|
|
672
|
-
)}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
<button
|
|
685
|
-
onClick={() => { setDiffRevision(null); onClose() }}
|
|
686
|
-
disabled={isRollingBack}
|
|
687
|
-
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
|
|
688
|
-
>
|
|
689
|
-
<X className="w-5 h-5" />
|
|
690
|
-
</button>
|
|
804
|
+
)}
|
|
805
|
+
>
|
|
806
|
+
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
807
|
+
<div className="flex items-center gap-2">
|
|
808
|
+
<History className="w-5 h-5 text-amber-500" />
|
|
809
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">Revision History</h3>
|
|
810
|
+
{diffRevision && currentRevision && (
|
|
811
|
+
<span className="flex items-center gap-1 ml-2 px-2 py-0.5 text-xs bg-blue-500/15 text-blue-400 rounded">
|
|
812
|
+
<GitCompare className="w-3 h-3" />
|
|
813
|
+
#{currentRevision.number} vs #{diffRevision}
|
|
814
|
+
</span>
|
|
815
|
+
)}
|
|
691
816
|
</div>
|
|
817
|
+
<button
|
|
818
|
+
onClick={handleClose}
|
|
819
|
+
disabled={isRollingBack}
|
|
820
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
|
|
821
|
+
>
|
|
822
|
+
<X className="w-5 h-5" />
|
|
823
|
+
</button>
|
|
824
|
+
</div>
|
|
692
825
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
826
|
+
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
827
|
+
<div className={clsx("p-4 overflow-y-auto", diffRevision ? "max-h-48 shrink-0" : "max-h-80")}>
|
|
828
|
+
{isLoading && (
|
|
829
|
+
<div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
|
|
830
|
+
Loading revisions...
|
|
831
|
+
</div>
|
|
832
|
+
)}
|
|
833
|
+
|
|
834
|
+
{error && (
|
|
835
|
+
<div className="flex items-center justify-center py-8 text-red-400 text-sm">
|
|
836
|
+
Failed to load revisions: {error instanceof Error ? error.message : 'Unknown error'}
|
|
837
|
+
</div>
|
|
838
|
+
)}
|
|
839
|
+
|
|
840
|
+
{revisions && revisions.length === 0 && (
|
|
841
|
+
<div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
|
|
842
|
+
No revisions found
|
|
843
|
+
</div>
|
|
844
|
+
)}
|
|
845
|
+
|
|
846
|
+
{revisions && revisions.length > 0 && (
|
|
847
|
+
<table className="w-full text-sm">
|
|
848
|
+
<thead>
|
|
849
|
+
<tr className="text-theme-text-secondary text-left text-xs uppercase tracking-wider">
|
|
850
|
+
<th className="pb-2 pr-3 font-medium">Rev</th>
|
|
851
|
+
<th className="pb-2 pr-3 font-medium">Image</th>
|
|
852
|
+
<th className="pb-2 pr-3 font-medium">Age</th>
|
|
853
|
+
<th className="pb-2 font-medium text-right">Action</th>
|
|
854
|
+
</tr>
|
|
855
|
+
</thead>
|
|
856
|
+
<tbody>
|
|
857
|
+
{revisions.map((rev: WorkloadRevision) => (
|
|
858
|
+
<tr
|
|
859
|
+
key={rev.number}
|
|
860
|
+
className={clsx(
|
|
861
|
+
"border-t border-theme-border/50",
|
|
862
|
+
diffRevision === rev.number && "bg-blue-500/10"
|
|
863
|
+
)}
|
|
864
|
+
>
|
|
865
|
+
<td className="py-2 pr-3 text-theme-text-primary font-mono">
|
|
866
|
+
#{rev.number}
|
|
867
|
+
</td>
|
|
868
|
+
<td className="py-2 pr-3 text-theme-text-secondary font-mono truncate max-w-[180px]" title={rev.image}>
|
|
869
|
+
{getImageTag(rev.image)}
|
|
870
|
+
</td>
|
|
871
|
+
<td className="py-2 pr-3 text-theme-text-secondary whitespace-nowrap">
|
|
872
|
+
{formatTimeAgo(rev.createdAt)}
|
|
873
|
+
</td>
|
|
874
|
+
<td className="py-2 text-right">
|
|
875
|
+
<div className="flex items-center gap-1 justify-end">
|
|
876
|
+
{!rev.isCurrent && rev.template && currentRevision?.template && (
|
|
877
|
+
<button
|
|
878
|
+
onClick={() => setDiffRevision(diffRevision === rev.number ? null : rev.number)}
|
|
879
|
+
className={clsx(
|
|
880
|
+
"px-2 py-0.5 text-xs font-medium rounded transition-colors flex items-center gap-1",
|
|
881
|
+
diffRevision === rev.number
|
|
882
|
+
? "bg-blue-500/20 text-blue-400 border border-blue-400/50"
|
|
883
|
+
: "text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 border border-transparent"
|
|
884
|
+
)}
|
|
885
|
+
title={`Compare with current revision`}
|
|
886
|
+
>
|
|
887
|
+
<GitCompare className="w-3 h-3" />
|
|
888
|
+
Diff
|
|
889
|
+
</button>
|
|
890
|
+
)}
|
|
891
|
+
{rev.isCurrent ? (
|
|
892
|
+
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-green-500/20 text-green-400 rounded">
|
|
893
|
+
Current
|
|
894
|
+
</span>
|
|
895
|
+
) : confirmRevision === rev.number ? (
|
|
896
|
+
<>
|
|
744
897
|
<button
|
|
745
|
-
onClick={() =>
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
diffRevision === rev.number
|
|
749
|
-
? "bg-blue-500/20 text-blue-400 border border-blue-400/50"
|
|
750
|
-
: "text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 border border-transparent"
|
|
751
|
-
)}
|
|
752
|
-
title={`Compare with current revision`}
|
|
898
|
+
onClick={() => handleRollback(rev.number)}
|
|
899
|
+
disabled={isRollingBack}
|
|
900
|
+
className="px-2 py-0.5 text-xs font-medium text-white bg-amber-600 hover:bg-amber-700 rounded transition-colors disabled:opacity-50"
|
|
753
901
|
>
|
|
754
|
-
|
|
755
|
-
Diff
|
|
902
|
+
{isRollingBack ? 'Rolling back...' : 'Confirm'}
|
|
756
903
|
</button>
|
|
757
|
-
)}
|
|
758
|
-
{rev.isCurrent ? (
|
|
759
|
-
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-green-500/20 text-green-400 rounded">
|
|
760
|
-
Current
|
|
761
|
-
</span>
|
|
762
|
-
) : confirmRevision === rev.number ? (
|
|
763
|
-
<>
|
|
764
|
-
<button
|
|
765
|
-
onClick={() => handleRollback(rev.number)}
|
|
766
|
-
disabled={isRollingBack}
|
|
767
|
-
className="px-2 py-0.5 text-xs font-medium text-white bg-amber-600 hover:bg-amber-700 rounded transition-colors disabled:opacity-50"
|
|
768
|
-
>
|
|
769
|
-
{isRollingBack ? 'Rolling back...' : 'Confirm'}
|
|
770
|
-
</button>
|
|
771
|
-
<button
|
|
772
|
-
onClick={() => setConfirmRevision(null)}
|
|
773
|
-
disabled={isRollingBack}
|
|
774
|
-
className="px-2 py-0.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary rounded transition-colors disabled:opacity-50"
|
|
775
|
-
>
|
|
776
|
-
Cancel
|
|
777
|
-
</button>
|
|
778
|
-
</>
|
|
779
|
-
) : (
|
|
780
904
|
<button
|
|
781
|
-
onClick={() => setConfirmRevision(
|
|
782
|
-
|
|
905
|
+
onClick={() => setConfirmRevision(null)}
|
|
906
|
+
disabled={isRollingBack}
|
|
907
|
+
className="px-2 py-0.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary rounded transition-colors disabled:opacity-50"
|
|
783
908
|
>
|
|
784
|
-
|
|
909
|
+
Cancel
|
|
785
910
|
</button>
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
selectedRevision={diffRevision}
|
|
802
|
-
/>
|
|
911
|
+
</>
|
|
912
|
+
) : (
|
|
913
|
+
<button
|
|
914
|
+
onClick={() => setConfirmRevision(rev.number)}
|
|
915
|
+
className="px-2 py-0.5 text-xs font-medium text-amber-400 hover:text-white hover:bg-amber-600 border border-amber-400/50 hover:border-amber-600 rounded transition-colors"
|
|
916
|
+
>
|
|
917
|
+
Rollback
|
|
918
|
+
</button>
|
|
919
|
+
)}
|
|
920
|
+
</div>
|
|
921
|
+
</td>
|
|
922
|
+
</tr>
|
|
923
|
+
))}
|
|
924
|
+
</tbody>
|
|
925
|
+
</table>
|
|
803
926
|
)}
|
|
804
927
|
</div>
|
|
805
928
|
|
|
806
|
-
|
|
807
|
-
<
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
</div>
|
|
929
|
+
{diffRevision && hasDiffData && (
|
|
930
|
+
<RevisionDiffView
|
|
931
|
+
currentTemplate={currentRevision!.template!}
|
|
932
|
+
selectedTemplate={selectedRevision!.template!}
|
|
933
|
+
currentRevision={currentRevision!.number}
|
|
934
|
+
selectedRevision={diffRevision}
|
|
935
|
+
/>
|
|
936
|
+
)}
|
|
815
937
|
</div>
|
|
816
|
-
|
|
938
|
+
|
|
939
|
+
<div className="flex items-center justify-end p-4 border-t border-theme-border shrink-0">
|
|
940
|
+
<button
|
|
941
|
+
onClick={handleClose}
|
|
942
|
+
disabled={isRollingBack}
|
|
943
|
+
className="px-4 py-2 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors disabled:opacity-50"
|
|
944
|
+
>
|
|
945
|
+
Close
|
|
946
|
+
</button>
|
|
947
|
+
</div>
|
|
948
|
+
</DialogPortal>
|
|
817
949
|
)
|
|
818
950
|
}
|
|
819
951
|
|