@skyhook-io/k8s-ui 1.0.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 +63 -0
- package/src/components/dock/BottomDock.tsx +204 -0
- package/src/components/dock/DockContext.tsx +212 -0
- package/src/components/dock/TerminalTab.tsx +333 -0
- package/src/components/dock/index.ts +3 -0
- package/src/components/gitops/GitOpsActions.tsx +227 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +221 -0
- package/src/components/gitops/ManagedResourcesList.tsx +210 -0
- package/src/components/gitops/SyncCountdown.tsx +146 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/logs/JsonLogLine.tsx +90 -0
- package/src/components/logs/LogCore.tsx +553 -0
- package/src/components/logs/LogToolbarSelects.tsx +74 -0
- package/src/components/logs/LogsViewer.tsx +146 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +263 -0
- package/src/components/logs/index.ts +13 -0
- package/src/components/logs/useLogBuffer.ts +138 -0
- package/src/components/logs/useLogSearch.ts +151 -0
- package/src/components/logs/useLogStream.ts +61 -0
- package/src/components/resources/ResourcesView.tsx +5494 -0
- package/src/components/resources/index.ts +17 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +166 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +233 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +89 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +194 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +79 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +68 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +209 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +123 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +111 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +164 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +140 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +153 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +221 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +124 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +86 -0
- package/src/components/resources/renderers/EventRenderer.tsx +175 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +119 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +172 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +242 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +263 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +68 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +218 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +476 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +154 -0
- package/src/components/resources/renderers/HPARenderer.tsx +164 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +303 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +167 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +104 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +175 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +176 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +125 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +84 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +112 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +288 -0
- package/src/components/resources/renderers/JobRenderer.tsx +103 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +157 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +188 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +148 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +90 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +136 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +151 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +70 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +309 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +142 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +232 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +97 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +74 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +138 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +220 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +151 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +352 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +93 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +208 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +282 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +153 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +108 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +98 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +138 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +126 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +63 -0
- package/src/components/resources/renderers/PodRenderer.tsx +677 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +50 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +62 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +71 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +97 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +192 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +227 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +75 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +98 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +121 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +321 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +111 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +81 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +65 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +98 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +195 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +82 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +257 -0
- package/src/components/resources/renderers/VPARenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +73 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +193 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +161 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +134 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +36 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +347 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +156 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +202 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +91 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +311 -0
- package/src/components/resources/renderers/argo-cells.tsx +118 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +164 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +161 -0
- package/src/components/resources/renderers/eso-cells.tsx +115 -0
- package/src/components/resources/renderers/flux-cells.tsx +253 -0
- package/src/components/resources/renderers/index.ts +105 -0
- package/src/components/resources/renderers/istio-cells.tsx +210 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +107 -0
- package/src/components/resources/renderers/keda-cells.tsx +94 -0
- package/src/components/resources/renderers/knative-cells.tsx +396 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +85 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +74 -0
- package/src/components/resources/renderers/traefik-cells.tsx +120 -0
- package/src/components/resources/renderers/trivy-cells.tsx +153 -0
- package/src/components/resources/renderers/trivy-shared.tsx +67 -0
- package/src/components/resources/renderers/velero-cells.tsx +164 -0
- package/src/components/resources/resource-utils-argo.ts +155 -0
- package/src/components/resources/resource-utils-certmanager.ts +198 -0
- package/src/components/resources/resource-utils-cnpg.ts +344 -0
- package/src/components/resources/resource-utils-eso.ts +385 -0
- package/src/components/resources/resource-utils-flux.ts +186 -0
- package/src/components/resources/resource-utils-istio.ts +372 -0
- package/src/components/resources/resource-utils-karpenter.ts +150 -0
- package/src/components/resources/resource-utils-keda.ts +203 -0
- package/src/components/resources/resource-utils-knative.ts +218 -0
- package/src/components/resources/resource-utils-kyverno.ts +224 -0
- package/src/components/resources/resource-utils-prometheus.ts +159 -0
- package/src/components/resources/resource-utils-traefik.ts +180 -0
- package/src/components/resources/resource-utils-trivy.ts +146 -0
- package/src/components/resources/resource-utils-velero.ts +293 -0
- package/src/components/resources/resource-utils.ts +1558 -0
- package/src/components/shared/EditableYamlView.tsx +302 -0
- package/src/components/shared/ResourceActionsBar.tsx +924 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +557 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/shared.tsx +614 -0
- package/src/components/topology/GroupNode.tsx +198 -0
- package/src/components/topology/K8sResourceNode.tsx +440 -0
- package/src/components/topology/TopologyControls.tsx +62 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +377 -0
- package/src/components/topology/TopologyGraph.tsx +748 -0
- package/src/components/topology/TopologySearch.tsx +304 -0
- package/src/components/topology/index.ts +7 -0
- package/src/components/topology/layout.ts +734 -0
- package/src/components/topology/layout.worker.ts +272 -0
- package/src/components/topology/topology.css +129 -0
- package/src/components/ui/CodeViewer.tsx +451 -0
- package/src/components/ui/ConfirmDialog.tsx +188 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +57 -0
- package/src/components/ui/HealthRing.tsx +72 -0
- package/src/components/ui/MetricsChart.tsx +259 -0
- package/src/components/ui/ResourceBar.tsx +68 -0
- package/src/components/ui/Toast.tsx +225 -0
- package/src/components/ui/Tooltip.tsx +165 -0
- package/src/components/ui/YamlEditor.tsx +280 -0
- package/src/components/ui/drawer-components.tsx +792 -0
- package/src/components/ui/index.ts +10 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +158 -0
- package/src/components/workload/WorkloadView.tsx +1109 -0
- package/src/components/workload/index.ts +2 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAnimatedUnmount.ts +30 -0
- package/src/hooks/useKeyboardShortcuts.tsx +336 -0
- package/src/hooks/useRefreshAnimation.ts +58 -0
- package/src/index.ts +35 -0
- package/src/types/core.ts +894 -0
- package/src/types/gitops.ts +458 -0
- package/src/types/index.ts +2 -0
- package/src/utils/animation.ts +40 -0
- package/src/utils/api-resources.ts +185 -0
- package/src/utils/badge-colors.ts +399 -0
- package/src/utils/context-name.ts +72 -0
- package/src/utils/download.ts +19 -0
- package/src/utils/format.ts +233 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/log-format.ts +300 -0
- package/src/utils/navigation.ts +119 -0
- package/src/utils/resource-hierarchy.ts +630 -0
- package/src/utils/resource-icons.ts +206 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import { useState, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
RefreshCw,
|
|
4
|
+
Terminal,
|
|
5
|
+
FileText,
|
|
6
|
+
Trash2,
|
|
7
|
+
Play,
|
|
8
|
+
Pause,
|
|
9
|
+
Box,
|
|
10
|
+
ChevronDown,
|
|
11
|
+
History,
|
|
12
|
+
GitCompare,
|
|
13
|
+
Code,
|
|
14
|
+
FileCode2,
|
|
15
|
+
X,
|
|
16
|
+
} from 'lucide-react'
|
|
17
|
+
import { createTwoFilesPatch } from 'diff'
|
|
18
|
+
import { clsx } from 'clsx'
|
|
19
|
+
import { ForceDeleteConfirmDialog } from '../ui/ForceDeleteConfirmDialog'
|
|
20
|
+
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
21
|
+
import { formatKindName } from '../ui/drawer-components'
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// ACTIONS BAR - Interactive buttons that change based on resource kind
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
interface ResourceActionsBarProps {
|
|
28
|
+
resource: SelectedResource
|
|
29
|
+
data: any
|
|
30
|
+
onClose?: () => void
|
|
31
|
+
hideLogs?: boolean
|
|
32
|
+
showYaml?: boolean
|
|
33
|
+
onToggleYaml?: () => void
|
|
34
|
+
|
|
35
|
+
// Capabilities (injected by platform)
|
|
36
|
+
canExec?: boolean
|
|
37
|
+
canViewLogs?: boolean
|
|
38
|
+
canPortForward?: boolean
|
|
39
|
+
|
|
40
|
+
// Dock actions (injected by platform)
|
|
41
|
+
onOpenTerminal?: (params: { namespace: string; podName: string; containerName: string; containers: string[] }) => void
|
|
42
|
+
onOpenLogs?: (params: { namespace: string; podName: string; containers: string[]; containerName?: string }) => void
|
|
43
|
+
onOpenWorkloadLogs?: (params: { namespace: string; workloadKind: string; workloadName: string }) => void
|
|
44
|
+
onCopyCommand?: (text: string, message: string, event: React.MouseEvent) => void
|
|
45
|
+
|
|
46
|
+
// Port forward render prop (injected by platform)
|
|
47
|
+
renderPortForward?: (props: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => React.ReactNode
|
|
48
|
+
|
|
49
|
+
// Delete
|
|
50
|
+
onDelete?: (params: { kind: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
51
|
+
isDeleting?: boolean
|
|
52
|
+
|
|
53
|
+
// Workload restart
|
|
54
|
+
onRestart?: (params: { kind: string; namespace: string; name: string }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
55
|
+
isRestarting?: boolean
|
|
56
|
+
|
|
57
|
+
// Rollback
|
|
58
|
+
revisions?: WorkloadRevision[]
|
|
59
|
+
revisionsLoading?: boolean
|
|
60
|
+
revisionsError?: Error | null
|
|
61
|
+
onRollback?: (params: { kind: string; namespace: string; name: string; revision: number }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
62
|
+
isRollingBack?: boolean
|
|
63
|
+
|
|
64
|
+
// CronJob actions
|
|
65
|
+
onTriggerCronJob?: (params: { namespace: string; name: string }) => void
|
|
66
|
+
isTriggeringCronJob?: boolean
|
|
67
|
+
onSuspendCronJob?: (params: { namespace: string; name: string }) => void
|
|
68
|
+
isSuspendingCronJob?: boolean
|
|
69
|
+
onResumeCronJob?: (params: { namespace: string; name: string }) => void
|
|
70
|
+
isResumingCronJob?: boolean
|
|
71
|
+
|
|
72
|
+
// Flux actions
|
|
73
|
+
onFluxReconcile?: (params: { kind: string; namespace: string; name: string }) => void
|
|
74
|
+
isFluxReconciling?: boolean
|
|
75
|
+
onFluxSyncWithSource?: (params: { kind: string; namespace: string; name: string }) => void
|
|
76
|
+
isFluxSyncing?: boolean
|
|
77
|
+
onFluxSuspend?: (params: { kind: string; namespace: string; name: string }) => void
|
|
78
|
+
isFluxSuspending?: boolean
|
|
79
|
+
onFluxResume?: (params: { kind: string; namespace: string; name: string }) => void
|
|
80
|
+
isFluxResuming?: boolean
|
|
81
|
+
|
|
82
|
+
// Argo actions
|
|
83
|
+
onArgoSync?: (params: { namespace: string; name: string }) => void
|
|
84
|
+
isArgoSyncing?: boolean
|
|
85
|
+
onArgoRefresh?: (params: { namespace: string; name: string; hard: boolean }) => void
|
|
86
|
+
isArgoRefreshing?: boolean
|
|
87
|
+
onArgoSuspend?: (params: { namespace: string; name: string }) => void
|
|
88
|
+
isArgoSuspending?: boolean
|
|
89
|
+
onArgoResume?: (params: { namespace: string; name: string }) => void
|
|
90
|
+
isArgoResuming?: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function ResourceActionsBar({
|
|
94
|
+
resource, data, onClose, hideLogs, showYaml, onToggleYaml,
|
|
95
|
+
canExec, canViewLogs, canPortForward,
|
|
96
|
+
onOpenTerminal, onOpenLogs: openLogs, onOpenWorkloadLogs: openWorkloadLogs, onCopyCommand,
|
|
97
|
+
renderPortForward,
|
|
98
|
+
onDelete, isDeleting,
|
|
99
|
+
onRestart, isRestarting,
|
|
100
|
+
revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
|
|
101
|
+
onTriggerCronJob, isTriggeringCronJob,
|
|
102
|
+
onSuspendCronJob, isSuspendingCronJob,
|
|
103
|
+
onResumeCronJob, isResumingCronJob,
|
|
104
|
+
onFluxReconcile, isFluxReconciling,
|
|
105
|
+
onFluxSyncWithSource, isFluxSyncing,
|
|
106
|
+
onFluxSuspend, isFluxSuspending,
|
|
107
|
+
onFluxResume, isFluxResuming,
|
|
108
|
+
onArgoSync, isArgoSyncing,
|
|
109
|
+
onArgoRefresh, isArgoRefreshing,
|
|
110
|
+
onArgoSuspend, isArgoSuspending,
|
|
111
|
+
onArgoResume, isArgoResuming,
|
|
112
|
+
}: ResourceActionsBarProps) {
|
|
113
|
+
const kind = resource.kind.toLowerCase()
|
|
114
|
+
|
|
115
|
+
// Delete confirmation state
|
|
116
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
117
|
+
|
|
118
|
+
// Rollback dialog state
|
|
119
|
+
const [showRevisions, setShowRevisions] = useState(false)
|
|
120
|
+
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind)
|
|
121
|
+
const hasMultipleRevisions = (revisionsList?.length ?? 0) > 1
|
|
122
|
+
|
|
123
|
+
function handleDeleteConfirm(force: boolean) {
|
|
124
|
+
onDelete?.(
|
|
125
|
+
{ kind: resource.kind, namespace: resource.namespace, name: resource.name, force },
|
|
126
|
+
{
|
|
127
|
+
onSuccess: () => {
|
|
128
|
+
setShowDeleteConfirm(false)
|
|
129
|
+
onClose?.()
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isRunning = kind === 'pods' ? data?.status?.phase === 'Running' : true
|
|
136
|
+
const containers = data?.spec?.containers?.map((c: any) => c.name) || []
|
|
137
|
+
|
|
138
|
+
const handleOpenTerminal = () => {
|
|
139
|
+
if (resource.namespace && resource.name && containers.length > 0) {
|
|
140
|
+
onOpenTerminal?.({
|
|
141
|
+
namespace: resource.namespace,
|
|
142
|
+
podName: resource.name,
|
|
143
|
+
containerName: containers[0],
|
|
144
|
+
containers,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const handleOpenLogs = (containerName?: string) => {
|
|
150
|
+
if (resource.namespace && resource.name && containers.length > 0) {
|
|
151
|
+
openLogs?.({
|
|
152
|
+
namespace: resource.namespace,
|
|
153
|
+
podName: resource.name,
|
|
154
|
+
containers,
|
|
155
|
+
containerName,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const [showLogsMenu, setShowLogsMenu] = useState(false)
|
|
161
|
+
const logsMenuTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
162
|
+
|
|
163
|
+
const handleLogsMouseEnter = () => {
|
|
164
|
+
if (logsMenuTimeout.current) clearTimeout(logsMenuTimeout.current)
|
|
165
|
+
if (containers.length > 1) setShowLogsMenu(true)
|
|
166
|
+
}
|
|
167
|
+
const handleLogsMouseLeave = () => {
|
|
168
|
+
logsMenuTimeout.current = setTimeout(() => setShowLogsMenu(false), 150)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="flex items-center gap-2 px-4 py-2 flex-wrap">
|
|
173
|
+
{/* Kind-specific actions (left) */}
|
|
174
|
+
{kind === 'pods' && (
|
|
175
|
+
<>
|
|
176
|
+
{isRunning && canExec && onOpenTerminal && (
|
|
177
|
+
<button
|
|
178
|
+
onClick={handleOpenTerminal}
|
|
179
|
+
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"
|
|
180
|
+
>
|
|
181
|
+
<Terminal className="w-3.5 h-3.5" />
|
|
182
|
+
Terminal
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
185
|
+
{canViewLogs && !hideLogs && openLogs && (
|
|
186
|
+
<div
|
|
187
|
+
className="relative"
|
|
188
|
+
onMouseEnter={handleLogsMouseEnter}
|
|
189
|
+
onMouseLeave={handleLogsMouseLeave}
|
|
190
|
+
>
|
|
191
|
+
<button
|
|
192
|
+
onClick={() => handleOpenLogs(containers.length === 1 ? containers[0] : undefined)}
|
|
193
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors"
|
|
194
|
+
>
|
|
195
|
+
<FileText className="w-3.5 h-3.5" />
|
|
196
|
+
Logs
|
|
197
|
+
{containers.length > 1 && <ChevronDown className="w-3 h-3 ml-0.5" />}
|
|
198
|
+
</button>
|
|
199
|
+
{showLogsMenu && containers.length > 1 && (
|
|
200
|
+
<div className="absolute top-full left-0 mt-1 min-w-[160px] py-1 bg-theme-surface border border-theme-border rounded-lg shadow-lg z-50">
|
|
201
|
+
{containers.map((container: string) => (
|
|
202
|
+
<button
|
|
203
|
+
key={container}
|
|
204
|
+
onClick={() => {
|
|
205
|
+
handleOpenLogs(container)
|
|
206
|
+
setShowLogsMenu(false)
|
|
207
|
+
}}
|
|
208
|
+
className="flex items-center gap-2 w-full px-3 py-1.5 text-xs text-theme-text-primary hover:bg-theme-hover transition-colors text-left"
|
|
209
|
+
>
|
|
210
|
+
<Box className="w-3 h-3 text-theme-text-tertiary shrink-0" />
|
|
211
|
+
<span className="truncate">{container}</span>
|
|
212
|
+
</button>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
{isRunning && canPortForward && resource.namespace && resource.name && renderPortForward && (
|
|
219
|
+
renderPortForward({
|
|
220
|
+
type: 'pod',
|
|
221
|
+
namespace: resource.namespace,
|
|
222
|
+
name: resource.name,
|
|
223
|
+
className: '!px-3 !py-1.5 !text-xs',
|
|
224
|
+
})
|
|
225
|
+
)}
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Service actions */}
|
|
230
|
+
{kind === 'services' && !data?.apiVersion?.includes('serving.knative.dev') && canPortForward && resource.namespace && resource.name && renderPortForward && (
|
|
231
|
+
renderPortForward({
|
|
232
|
+
type: 'service',
|
|
233
|
+
namespace: resource.namespace,
|
|
234
|
+
name: resource.name,
|
|
235
|
+
className: '!px-3 !py-1.5 !text-xs',
|
|
236
|
+
})
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Workload actions - restart, rollback, and logs */}
|
|
240
|
+
{['deployments', 'statefulsets', 'daemonsets', 'rollouts'].includes(kind) && (
|
|
241
|
+
<>
|
|
242
|
+
{onRestart && (
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => onRestart({
|
|
245
|
+
kind: resource.kind,
|
|
246
|
+
namespace: resource.namespace,
|
|
247
|
+
name: resource.name,
|
|
248
|
+
})}
|
|
249
|
+
disabled={isRestarting}
|
|
250
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors disabled:opacity-50"
|
|
251
|
+
>
|
|
252
|
+
<RefreshCw className={`w-3.5 h-3.5 ${isRestarting ? 'animate-spin' : ''}`} />
|
|
253
|
+
{isRestarting ? 'Restarting...' : 'Restart'}
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
{isRollbackKind && onRollback && (
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => setShowRevisions(true)}
|
|
259
|
+
disabled={!hasMultipleRevisions}
|
|
260
|
+
title={hasMultipleRevisions ? 'View revision history and rollback' : 'Only one revision exists'}
|
|
261
|
+
className={clsx(
|
|
262
|
+
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors",
|
|
263
|
+
hasMultipleRevisions
|
|
264
|
+
? "text-white bg-amber-600 hover:bg-amber-700"
|
|
265
|
+
: "text-theme-text-disabled bg-theme-elevated"
|
|
266
|
+
)}
|
|
267
|
+
>
|
|
268
|
+
<History className="w-3.5 h-3.5" />
|
|
269
|
+
Rollback
|
|
270
|
+
</button>
|
|
271
|
+
)}
|
|
272
|
+
{canViewLogs && !hideLogs && ['deployments', 'statefulsets', 'daemonsets'].includes(kind) && openWorkloadLogs && (
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => openWorkloadLogs({
|
|
275
|
+
namespace: resource.namespace,
|
|
276
|
+
workloadKind: kind,
|
|
277
|
+
workloadName: resource.name,
|
|
278
|
+
})}
|
|
279
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors"
|
|
280
|
+
>
|
|
281
|
+
<FileText className="w-3.5 h-3.5" />
|
|
282
|
+
Logs
|
|
283
|
+
</button>
|
|
284
|
+
)}
|
|
285
|
+
</>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* CronJob actions */}
|
|
289
|
+
{kind === 'cronjobs' && (
|
|
290
|
+
<>
|
|
291
|
+
{onTriggerCronJob && (
|
|
292
|
+
<button
|
|
293
|
+
onClick={() => onTriggerCronJob({
|
|
294
|
+
namespace: resource.namespace,
|
|
295
|
+
name: resource.name,
|
|
296
|
+
})}
|
|
297
|
+
disabled={isTriggeringCronJob}
|
|
298
|
+
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 disabled:opacity-50"
|
|
299
|
+
>
|
|
300
|
+
<Play className={`w-3.5 h-3.5 ${isTriggeringCronJob ? 'animate-pulse' : ''}`} />
|
|
301
|
+
{isTriggeringCronJob ? 'Triggering...' : 'Trigger'}
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
{data?.spec?.suspend ? (
|
|
305
|
+
onResumeCronJob && (
|
|
306
|
+
<button
|
|
307
|
+
onClick={() => onResumeCronJob({
|
|
308
|
+
namespace: resource.namespace,
|
|
309
|
+
name: resource.name,
|
|
310
|
+
})}
|
|
311
|
+
disabled={isResumingCronJob}
|
|
312
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors disabled:opacity-50"
|
|
313
|
+
>
|
|
314
|
+
<Play className="w-3.5 h-3.5" />
|
|
315
|
+
{isResumingCronJob ? 'Resuming...' : 'Resume'}
|
|
316
|
+
</button>
|
|
317
|
+
)
|
|
318
|
+
) : (
|
|
319
|
+
onSuspendCronJob && (
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => onSuspendCronJob({
|
|
322
|
+
namespace: resource.namespace,
|
|
323
|
+
name: resource.name,
|
|
324
|
+
})}
|
|
325
|
+
disabled={isSuspendingCronJob}
|
|
326
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors disabled:opacity-50"
|
|
327
|
+
>
|
|
328
|
+
<Pause className="w-3.5 h-3.5" />
|
|
329
|
+
{isSuspendingCronJob ? 'Suspending...' : 'Suspend'}
|
|
330
|
+
</button>
|
|
331
|
+
)
|
|
332
|
+
)}
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* FluxCD actions */}
|
|
337
|
+
{['gitrepositories', 'ocirepositories', 'helmrepositories', 'kustomizations', 'helmreleases', 'alerts'].includes(kind) && (
|
|
338
|
+
<FluxActions
|
|
339
|
+
resource={resource}
|
|
340
|
+
data={data}
|
|
341
|
+
onReconcile={onFluxReconcile}
|
|
342
|
+
isReconciling={isFluxReconciling}
|
|
343
|
+
onSyncWithSource={onFluxSyncWithSource}
|
|
344
|
+
isSyncing={isFluxSyncing}
|
|
345
|
+
onSuspend={onFluxSuspend}
|
|
346
|
+
isSuspending={isFluxSuspending}
|
|
347
|
+
onResume={onFluxResume}
|
|
348
|
+
isResuming={isFluxResuming}
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
|
|
352
|
+
{/* ArgoCD actions */}
|
|
353
|
+
{kind === 'applications' && (
|
|
354
|
+
<ArgoActions
|
|
355
|
+
resource={resource}
|
|
356
|
+
data={data}
|
|
357
|
+
onSync={onArgoSync}
|
|
358
|
+
isSyncing={isArgoSyncing}
|
|
359
|
+
onRefresh={onArgoRefresh}
|
|
360
|
+
isRefreshing={isArgoRefreshing}
|
|
361
|
+
onSuspend={onArgoSuspend}
|
|
362
|
+
isSuspending={isArgoSuspending}
|
|
363
|
+
onResume={onArgoResume}
|
|
364
|
+
isResuming={isArgoResuming}
|
|
365
|
+
/>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* Job logs */}
|
|
369
|
+
{kind === 'jobs' && onCopyCommand && (
|
|
370
|
+
<button
|
|
371
|
+
onClick={(e) => onCopyCommand(
|
|
372
|
+
`kubectl logs job/${resource.name} -n ${resource.namespace} -f`,
|
|
373
|
+
'Logs command copied',
|
|
374
|
+
e
|
|
375
|
+
)}
|
|
376
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors"
|
|
377
|
+
>
|
|
378
|
+
<FileText className="w-3.5 h-3.5" />
|
|
379
|
+
Logs
|
|
380
|
+
</button>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* Spacer pushes universal actions to the right */}
|
|
384
|
+
<div className="flex-1" />
|
|
385
|
+
|
|
386
|
+
{/* Universal actions (right-aligned) */}
|
|
387
|
+
{onToggleYaml && (
|
|
388
|
+
<button
|
|
389
|
+
onClick={onToggleYaml}
|
|
390
|
+
className={clsx(
|
|
391
|
+
'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors',
|
|
392
|
+
showYaml
|
|
393
|
+
? 'text-white bg-blue-600 hover:bg-blue-700'
|
|
394
|
+
: 'text-theme-text-secondary hover:text-theme-text-primary border border-theme-border hover:bg-theme-elevated'
|
|
395
|
+
)}
|
|
396
|
+
title="Toggle YAML view"
|
|
397
|
+
>
|
|
398
|
+
<FileCode2 className="w-3.5 h-3.5" />
|
|
399
|
+
YAML
|
|
400
|
+
</button>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{onDelete && (
|
|
404
|
+
<button
|
|
405
|
+
onClick={() => setShowDeleteConfirm(true)}
|
|
406
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-red-400 hover:text-white hover:bg-red-600 border border-red-400/50 hover:border-red-600 rounded-lg transition-colors"
|
|
407
|
+
>
|
|
408
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
409
|
+
Delete
|
|
410
|
+
</button>
|
|
411
|
+
)}
|
|
412
|
+
|
|
413
|
+
<ForceDeleteConfirmDialog
|
|
414
|
+
open={showDeleteConfirm}
|
|
415
|
+
onClose={() => setShowDeleteConfirm(false)}
|
|
416
|
+
onConfirm={handleDeleteConfirm}
|
|
417
|
+
resourceName={resource.name}
|
|
418
|
+
resourceKind={formatKindName(resource.kind)}
|
|
419
|
+
namespaceName={resource.namespace}
|
|
420
|
+
isLoading={isDeleting ?? false}
|
|
421
|
+
/>
|
|
422
|
+
|
|
423
|
+
{showRevisions && ['deployments', 'statefulsets', 'daemonsets'].includes(kind) && (
|
|
424
|
+
<RevisionHistoryDialog
|
|
425
|
+
kind={resource.kind}
|
|
426
|
+
namespace={resource.namespace}
|
|
427
|
+
name={resource.name}
|
|
428
|
+
open={showRevisions}
|
|
429
|
+
onClose={() => setShowRevisions(false)}
|
|
430
|
+
revisions={revisionsList}
|
|
431
|
+
isLoading={revisionsLoading}
|
|
432
|
+
error={revisionsError}
|
|
433
|
+
onRollback={onRollback}
|
|
434
|
+
isRollingBack={isRollingBack}
|
|
435
|
+
/>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// FLUX ACTIONS
|
|
443
|
+
// ============================================================================
|
|
444
|
+
|
|
445
|
+
function FluxActions({ resource, data, onReconcile, isReconciling, onSyncWithSource, isSyncing, onSuspend, isSuspending, onResume, isResuming }: {
|
|
446
|
+
resource: SelectedResource; data: any
|
|
447
|
+
onReconcile?: (params: { kind: string; namespace: string; name: string }) => void; isReconciling?: boolean
|
|
448
|
+
onSyncWithSource?: (params: { kind: string; namespace: string; name: string }) => void; isSyncing?: boolean
|
|
449
|
+
onSuspend?: (params: { kind: string; namespace: string; name: string }) => void; isSuspending?: boolean
|
|
450
|
+
onResume?: (params: { kind: string; namespace: string; name: string }) => void; isResuming?: boolean
|
|
451
|
+
}) {
|
|
452
|
+
const isSuspended = data?.spec?.suspend === true
|
|
453
|
+
const hasSource = resource.kind === 'kustomizations' || resource.kind === 'helmreleases'
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<>
|
|
457
|
+
{onReconcile && (
|
|
458
|
+
<button
|
|
459
|
+
onClick={() => onReconcile({
|
|
460
|
+
kind: resource.kind,
|
|
461
|
+
namespace: resource.namespace,
|
|
462
|
+
name: resource.name,
|
|
463
|
+
})}
|
|
464
|
+
disabled={isReconciling || isSuspended}
|
|
465
|
+
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 disabled:opacity-50"
|
|
466
|
+
title={isSuspended ? 'Cannot reconcile while suspended' : 'Trigger reconciliation'}
|
|
467
|
+
>
|
|
468
|
+
<RefreshCw className={`w-3.5 h-3.5 ${isReconciling ? 'animate-spin' : ''}`} />
|
|
469
|
+
{isReconciling ? 'Reconciling...' : 'Reconcile'}
|
|
470
|
+
</button>
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
{hasSource && onSyncWithSource && (
|
|
474
|
+
<button
|
|
475
|
+
onClick={() => onSyncWithSource({
|
|
476
|
+
kind: resource.kind,
|
|
477
|
+
namespace: resource.namespace,
|
|
478
|
+
name: resource.name,
|
|
479
|
+
})}
|
|
480
|
+
disabled={isSyncing || isSuspended}
|
|
481
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-lg transition-colors disabled:opacity-50"
|
|
482
|
+
title={isSuspended ? 'Cannot sync while suspended' : 'Fetch latest from source, then reconcile'}
|
|
483
|
+
>
|
|
484
|
+
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
|
|
485
|
+
{isSyncing ? 'Syncing...' : 'Sync with Source'}
|
|
486
|
+
</button>
|
|
487
|
+
)}
|
|
488
|
+
|
|
489
|
+
{isSuspended ? (
|
|
490
|
+
onResume && (
|
|
491
|
+
<button
|
|
492
|
+
onClick={() => onResume({
|
|
493
|
+
kind: resource.kind,
|
|
494
|
+
namespace: resource.namespace,
|
|
495
|
+
name: resource.name,
|
|
496
|
+
})}
|
|
497
|
+
disabled={isResuming}
|
|
498
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors disabled:opacity-50"
|
|
499
|
+
>
|
|
500
|
+
<Play className="w-3.5 h-3.5" />
|
|
501
|
+
{isResuming ? 'Resuming...' : 'Resume'}
|
|
502
|
+
</button>
|
|
503
|
+
)
|
|
504
|
+
) : (
|
|
505
|
+
onSuspend && (
|
|
506
|
+
<button
|
|
507
|
+
onClick={() => onSuspend({
|
|
508
|
+
kind: resource.kind,
|
|
509
|
+
namespace: resource.namespace,
|
|
510
|
+
name: resource.name,
|
|
511
|
+
})}
|
|
512
|
+
disabled={isSuspending}
|
|
513
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors disabled:opacity-50"
|
|
514
|
+
>
|
|
515
|
+
<Pause className="w-3.5 h-3.5" />
|
|
516
|
+
{isSuspending ? 'Suspending...' : 'Suspend'}
|
|
517
|
+
</button>
|
|
518
|
+
)
|
|
519
|
+
)}
|
|
520
|
+
</>
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ============================================================================
|
|
525
|
+
// ARGO ACTIONS
|
|
526
|
+
// ============================================================================
|
|
527
|
+
|
|
528
|
+
function ArgoActions({ resource, data, onSync, isSyncing, onRefresh, isRefreshing, onSuspend, isSuspending, onResume, isResuming }: {
|
|
529
|
+
resource: SelectedResource; data: any
|
|
530
|
+
onSync?: (params: { namespace: string; name: string }) => void; isSyncing?: boolean
|
|
531
|
+
onRefresh?: (params: { namespace: string; name: string; hard: boolean }) => void; isRefreshing?: boolean
|
|
532
|
+
onSuspend?: (params: { namespace: string; name: string }) => void; isSuspending?: boolean
|
|
533
|
+
onResume?: (params: { namespace: string; name: string }) => void; isResuming?: boolean
|
|
534
|
+
}) {
|
|
535
|
+
const hasAutomatedSync = !!data?.spec?.syncPolicy?.automated
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<>
|
|
539
|
+
{onSync && (
|
|
540
|
+
<button
|
|
541
|
+
onClick={() => onSync({
|
|
542
|
+
namespace: resource.namespace,
|
|
543
|
+
name: resource.name,
|
|
544
|
+
})}
|
|
545
|
+
disabled={isSyncing}
|
|
546
|
+
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 disabled:opacity-50"
|
|
547
|
+
title="Sync application"
|
|
548
|
+
>
|
|
549
|
+
<RefreshCw className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} />
|
|
550
|
+
{isSyncing ? 'Syncing...' : 'Sync'}
|
|
551
|
+
</button>
|
|
552
|
+
)}
|
|
553
|
+
|
|
554
|
+
{onRefresh && (
|
|
555
|
+
<button
|
|
556
|
+
onClick={() => onRefresh({
|
|
557
|
+
namespace: resource.namespace,
|
|
558
|
+
name: resource.name,
|
|
559
|
+
hard: false,
|
|
560
|
+
})}
|
|
561
|
+
disabled={isRefreshing}
|
|
562
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors disabled:opacity-50"
|
|
563
|
+
title="Refresh (re-read from git)"
|
|
564
|
+
>
|
|
565
|
+
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
566
|
+
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
|
567
|
+
</button>
|
|
568
|
+
)}
|
|
569
|
+
|
|
570
|
+
{hasAutomatedSync ? (
|
|
571
|
+
onSuspend && (
|
|
572
|
+
<button
|
|
573
|
+
onClick={() => onSuspend({
|
|
574
|
+
namespace: resource.namespace,
|
|
575
|
+
name: resource.name,
|
|
576
|
+
})}
|
|
577
|
+
disabled={isSuspending}
|
|
578
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-slate-600 hover:bg-slate-500 rounded-lg transition-colors disabled:opacity-50"
|
|
579
|
+
>
|
|
580
|
+
<Pause className="w-3.5 h-3.5" />
|
|
581
|
+
{isSuspending ? 'Suspending...' : 'Suspend'}
|
|
582
|
+
</button>
|
|
583
|
+
)
|
|
584
|
+
) : (
|
|
585
|
+
onResume && (
|
|
586
|
+
<button
|
|
587
|
+
onClick={() => onResume({
|
|
588
|
+
namespace: resource.namespace,
|
|
589
|
+
name: resource.name,
|
|
590
|
+
})}
|
|
591
|
+
disabled={isResuming}
|
|
592
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors disabled:opacity-50"
|
|
593
|
+
>
|
|
594
|
+
<Play className="w-3.5 h-3.5" />
|
|
595
|
+
{isResuming ? 'Enabling...' : 'Enable Auto-Sync'}
|
|
596
|
+
</button>
|
|
597
|
+
)
|
|
598
|
+
)}
|
|
599
|
+
</>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// REVISION HISTORY DIALOG
|
|
605
|
+
// ============================================================================
|
|
606
|
+
|
|
607
|
+
export function RevisionHistoryDialog({ kind, namespace, name, open, onClose, revisions, isLoading, error, onRollback, isRollingBack }: {
|
|
608
|
+
kind: string
|
|
609
|
+
namespace: string
|
|
610
|
+
name: string
|
|
611
|
+
open: boolean
|
|
612
|
+
onClose: () => void
|
|
613
|
+
revisions?: WorkloadRevision[]
|
|
614
|
+
isLoading?: boolean
|
|
615
|
+
error?: Error | null
|
|
616
|
+
onRollback?: (params: { kind: string; namespace: string; name: string; revision: number }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
617
|
+
isRollingBack?: boolean
|
|
618
|
+
}) {
|
|
619
|
+
const [confirmRevision, setConfirmRevision] = useState<number | null>(null)
|
|
620
|
+
const [diffRevision, setDiffRevision] = useState<number | null>(null)
|
|
621
|
+
|
|
622
|
+
if (!open) return null
|
|
623
|
+
|
|
624
|
+
const currentRevision = revisions?.find(r => r.isCurrent)
|
|
625
|
+
const selectedRevision = revisions?.find(r => r.number === diffRevision)
|
|
626
|
+
const hasDiffData = currentRevision?.template && selectedRevision?.template
|
|
627
|
+
|
|
628
|
+
function handleRollback(revision: number) {
|
|
629
|
+
onRollback?.(
|
|
630
|
+
{ kind, namespace, name, revision },
|
|
631
|
+
{
|
|
632
|
+
onSuccess: () => {
|
|
633
|
+
setConfirmRevision(null)
|
|
634
|
+
setDiffRevision(null)
|
|
635
|
+
onClose()
|
|
636
|
+
},
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function formatTimeAgo(dateStr: string): string {
|
|
642
|
+
const date = new Date(dateStr)
|
|
643
|
+
const now = new Date()
|
|
644
|
+
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
|
645
|
+
if (seconds < 60) return `${seconds}s ago`
|
|
646
|
+
const minutes = Math.floor(seconds / 60)
|
|
647
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
648
|
+
const hours = Math.floor(minutes / 60)
|
|
649
|
+
if (hours < 24) return `${hours}h ago`
|
|
650
|
+
const days = Math.floor(hours / 24)
|
|
651
|
+
return `${days}d ago`
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function getImageTag(image: string): string {
|
|
655
|
+
if (!image) return '-'
|
|
656
|
+
const parts = image.split(':')
|
|
657
|
+
if (parts.length > 1) return parts[parts.length - 1]
|
|
658
|
+
const slashParts = image.split('/')
|
|
659
|
+
return slashParts[slashParts.length - 1]
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return (
|
|
663
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
664
|
+
<div
|
|
665
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
666
|
+
onClick={isRollingBack ? undefined : () => { setDiffRevision(null); onClose() }}
|
|
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",
|
|
671
|
+
diffRevision ? "max-w-5xl w-full max-h-[85vh]" : "max-w-lg w-full"
|
|
672
|
+
)}>
|
|
673
|
+
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
674
|
+
<div className="flex items-center gap-2">
|
|
675
|
+
<History className="w-5 h-5 text-amber-500" />
|
|
676
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">Revision History</h3>
|
|
677
|
+
{diffRevision && currentRevision && (
|
|
678
|
+
<span className="flex items-center gap-1 ml-2 px-2 py-0.5 text-xs bg-blue-500/15 text-blue-400 rounded">
|
|
679
|
+
<GitCompare className="w-3 h-3" />
|
|
680
|
+
#{currentRevision.number} vs #{diffRevision}
|
|
681
|
+
</span>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
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>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
694
|
+
<div className={clsx("p-4 overflow-y-auto", diffRevision ? "max-h-48 shrink-0" : "max-h-80")}>
|
|
695
|
+
{isLoading && (
|
|
696
|
+
<div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
|
|
697
|
+
Loading revisions...
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
|
|
701
|
+
{error && (
|
|
702
|
+
<div className="flex items-center justify-center py-8 text-red-400 text-sm">
|
|
703
|
+
Failed to load revisions: {error instanceof Error ? error.message : 'Unknown error'}
|
|
704
|
+
</div>
|
|
705
|
+
)}
|
|
706
|
+
|
|
707
|
+
{revisions && revisions.length === 0 && (
|
|
708
|
+
<div className="flex items-center justify-center py-8 text-theme-text-secondary text-sm">
|
|
709
|
+
No revisions found
|
|
710
|
+
</div>
|
|
711
|
+
)}
|
|
712
|
+
|
|
713
|
+
{revisions && revisions.length > 0 && (
|
|
714
|
+
<table className="w-full text-sm">
|
|
715
|
+
<thead>
|
|
716
|
+
<tr className="text-theme-text-secondary text-left text-xs uppercase tracking-wider">
|
|
717
|
+
<th className="pb-2 pr-3 font-medium">Rev</th>
|
|
718
|
+
<th className="pb-2 pr-3 font-medium">Image</th>
|
|
719
|
+
<th className="pb-2 pr-3 font-medium">Age</th>
|
|
720
|
+
<th className="pb-2 font-medium text-right">Action</th>
|
|
721
|
+
</tr>
|
|
722
|
+
</thead>
|
|
723
|
+
<tbody>
|
|
724
|
+
{revisions.map((rev: WorkloadRevision) => (
|
|
725
|
+
<tr
|
|
726
|
+
key={rev.number}
|
|
727
|
+
className={clsx(
|
|
728
|
+
"border-t border-theme-border/50",
|
|
729
|
+
diffRevision === rev.number && "bg-blue-500/10"
|
|
730
|
+
)}
|
|
731
|
+
>
|
|
732
|
+
<td className="py-2 pr-3 text-theme-text-primary font-mono">
|
|
733
|
+
#{rev.number}
|
|
734
|
+
</td>
|
|
735
|
+
<td className="py-2 pr-3 text-theme-text-secondary font-mono truncate max-w-[180px]" title={rev.image}>
|
|
736
|
+
{getImageTag(rev.image)}
|
|
737
|
+
</td>
|
|
738
|
+
<td className="py-2 pr-3 text-theme-text-secondary whitespace-nowrap">
|
|
739
|
+
{formatTimeAgo(rev.createdAt)}
|
|
740
|
+
</td>
|
|
741
|
+
<td className="py-2 text-right">
|
|
742
|
+
<div className="flex items-center gap-1 justify-end">
|
|
743
|
+
{!rev.isCurrent && rev.template && currentRevision?.template && (
|
|
744
|
+
<button
|
|
745
|
+
onClick={() => setDiffRevision(diffRevision === rev.number ? null : rev.number)}
|
|
746
|
+
className={clsx(
|
|
747
|
+
"px-2 py-0.5 text-xs font-medium rounded transition-colors flex items-center gap-1",
|
|
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`}
|
|
753
|
+
>
|
|
754
|
+
<GitCompare className="w-3 h-3" />
|
|
755
|
+
Diff
|
|
756
|
+
</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
|
+
<button
|
|
781
|
+
onClick={() => setConfirmRevision(rev.number)}
|
|
782
|
+
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"
|
|
783
|
+
>
|
|
784
|
+
Rollback
|
|
785
|
+
</button>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
</td>
|
|
789
|
+
</tr>
|
|
790
|
+
))}
|
|
791
|
+
</tbody>
|
|
792
|
+
</table>
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
|
|
796
|
+
{diffRevision && hasDiffData && (
|
|
797
|
+
<RevisionDiffView
|
|
798
|
+
currentTemplate={currentRevision!.template!}
|
|
799
|
+
selectedTemplate={selectedRevision!.template!}
|
|
800
|
+
currentRevision={currentRevision!.number}
|
|
801
|
+
selectedRevision={diffRevision}
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<div className="flex items-center justify-end p-4 border-t border-theme-border shrink-0">
|
|
807
|
+
<button
|
|
808
|
+
onClick={() => { setDiffRevision(null); onClose() }}
|
|
809
|
+
disabled={isRollingBack}
|
|
810
|
+
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"
|
|
811
|
+
>
|
|
812
|
+
Close
|
|
813
|
+
</button>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ============================================================================
|
|
821
|
+
// REVISION DIFF VIEW
|
|
822
|
+
// ============================================================================
|
|
823
|
+
|
|
824
|
+
function stripAutoLabels(templateYaml: string): string {
|
|
825
|
+
return templateYaml
|
|
826
|
+
.split('\n')
|
|
827
|
+
.filter(line => !line.match(/^\s+pod-template-hash:/))
|
|
828
|
+
.join('\n')
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function RevisionDiffView({ currentTemplate, selectedTemplate, currentRevision, selectedRevision }: {
|
|
832
|
+
currentTemplate: string
|
|
833
|
+
selectedTemplate: string
|
|
834
|
+
currentRevision: number
|
|
835
|
+
selectedRevision: number
|
|
836
|
+
}) {
|
|
837
|
+
const [expanded, setExpanded] = useState(false)
|
|
838
|
+
|
|
839
|
+
const cleanCurrent = stripAutoLabels(currentTemplate)
|
|
840
|
+
const cleanSelected = stripAutoLabels(selectedTemplate)
|
|
841
|
+
|
|
842
|
+
const patch = createTwoFilesPatch(
|
|
843
|
+
`Revision #${currentRevision} (current)`,
|
|
844
|
+
`Revision #${selectedRevision}`,
|
|
845
|
+
cleanCurrent,
|
|
846
|
+
cleanSelected,
|
|
847
|
+
'', '',
|
|
848
|
+
expanded ? { context: 999999 } : { context: 3 }
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
const lines = patch.split('\n')
|
|
852
|
+
const diffLines = lines.filter(line =>
|
|
853
|
+
!line.startsWith('===') && !line.startsWith('Index:')
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
const hasChanges = diffLines.some(l => (l.startsWith('+') && !l.startsWith('+++')) || (l.startsWith('-') && !l.startsWith('---')))
|
|
857
|
+
|
|
858
|
+
return (
|
|
859
|
+
<div className="border-t border-theme-border flex flex-col shrink-0">
|
|
860
|
+
<div className="flex items-center justify-between px-4 py-2 bg-theme-elevated/50 text-xs text-theme-text-secondary shrink-0">
|
|
861
|
+
<div className="flex items-center gap-4">
|
|
862
|
+
<span className="flex items-center gap-1">
|
|
863
|
+
<span className="w-3 h-3 bg-red-500/20 border border-red-500/50 rounded" /> Revision #{currentRevision} (current)
|
|
864
|
+
</span>
|
|
865
|
+
<span className="flex items-center gap-1">
|
|
866
|
+
<span className="w-3 h-3 bg-green-500/20 border border-green-500/50 rounded" /> Revision #{selectedRevision}
|
|
867
|
+
</span>
|
|
868
|
+
</div>
|
|
869
|
+
{hasChanges && (
|
|
870
|
+
<button
|
|
871
|
+
onClick={() => setExpanded(!expanded)}
|
|
872
|
+
className="flex items-center gap-1 px-2 py-0.5 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded transition-colors"
|
|
873
|
+
>
|
|
874
|
+
<Code className="w-3 h-3" />
|
|
875
|
+
{expanded ? 'Show changes only' : 'Show full spec'}
|
|
876
|
+
</button>
|
|
877
|
+
)}
|
|
878
|
+
</div>
|
|
879
|
+
<div className="overflow-auto max-h-[400px]">
|
|
880
|
+
{hasChanges ? (
|
|
881
|
+
<pre className="text-xs font-mono p-0 m-0">
|
|
882
|
+
{diffLines.map((line, index) => {
|
|
883
|
+
const isAddition = line.startsWith('+') && !line.startsWith('+++')
|
|
884
|
+
const isDeletion = line.startsWith('-') && !line.startsWith('---')
|
|
885
|
+
const isHeader = line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')
|
|
886
|
+
|
|
887
|
+
return (
|
|
888
|
+
<div
|
|
889
|
+
key={index}
|
|
890
|
+
className={clsx(
|
|
891
|
+
'flex',
|
|
892
|
+
isAddition && 'bg-green-500/10',
|
|
893
|
+
isDeletion && 'bg-red-500/10',
|
|
894
|
+
isHeader && 'bg-blue-500/10'
|
|
895
|
+
)}
|
|
896
|
+
>
|
|
897
|
+
<span className="w-10 shrink-0 text-right pr-2 py-0.5 text-theme-text-disabled select-none border-r border-theme-border/50">
|
|
898
|
+
{index + 1}
|
|
899
|
+
</span>
|
|
900
|
+
<span
|
|
901
|
+
className={clsx(
|
|
902
|
+
'flex-1 px-3 py-0.5 whitespace-pre',
|
|
903
|
+
isAddition && 'text-green-400',
|
|
904
|
+
isDeletion && 'text-red-400',
|
|
905
|
+
isHeader && 'text-blue-400 font-medium',
|
|
906
|
+
!isAddition && !isDeletion && !isHeader && 'text-theme-text-secondary'
|
|
907
|
+
)}
|
|
908
|
+
>
|
|
909
|
+
{line || ' '}
|
|
910
|
+
</span>
|
|
911
|
+
</div>
|
|
912
|
+
)
|
|
913
|
+
})}
|
|
914
|
+
</pre>
|
|
915
|
+
) : (
|
|
916
|
+
<div className="flex flex-col items-center justify-center py-12 text-theme-text-tertiary">
|
|
917
|
+
<GitCompare className="w-8 h-8 mb-2 text-theme-text-disabled" />
|
|
918
|
+
<span className="text-sm">Templates are identical</span>
|
|
919
|
+
</div>
|
|
920
|
+
)}
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
)
|
|
924
|
+
}
|