@skyhook-io/radar-app 1.8.6 → 1.8.8
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/README.md +7 -1
- package/package.json +12 -10
- package/src/App.tsx +278 -230
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1462 -246
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +775 -80
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +103 -72
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +494 -229
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +116 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +81 -14
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +320 -127
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +66 -116
- package/src/components/settings/SettingsDialog.tsx +1290 -283
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +81 -13
- package/src/components/timeline/TimelineView.tsx +842 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +912 -245
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +162 -1
- package/src/index.ts +12 -0
- package/src/main.tsx +1 -1
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo, useEffect, useCallback, useState } from 'react'
|
|
2
|
-
import { useQueries } from '@tanstack/react-query'
|
|
2
|
+
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { Terminal } from 'lucide-react'
|
|
@@ -12,36 +12,83 @@ import {
|
|
|
12
12
|
type GitOpsOwnerRef,
|
|
13
13
|
type GitOpsStatus,
|
|
14
14
|
type HelmOwnerRef,
|
|
15
|
+
type AppRow,
|
|
16
|
+
type ResourceOwnershipContext,
|
|
17
|
+
type ServingResourceDetail,
|
|
18
|
+
type AuditFinding,
|
|
15
19
|
gitOpsRouteForOwner,
|
|
16
20
|
gitOpsOwnerFromRelationships,
|
|
17
21
|
getGitOpsResourceStatus,
|
|
18
22
|
resolvedEnvFromKey,
|
|
19
23
|
} from '@skyhook-io/k8s-ui'
|
|
24
|
+
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
20
25
|
import type { SelectedResource, ResourceRef, Relationships, ResolvedEnvFrom } from '../../types'
|
|
21
|
-
import { kindToPlural, buildWorkloadPath, type NavigateToResource } from '../../utils/navigation'
|
|
22
26
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
kindToPlural,
|
|
28
|
+
pluralToKind,
|
|
29
|
+
relatedResourcePath,
|
|
30
|
+
type NavigateToResource,
|
|
31
|
+
} from '../../utils/navigation'
|
|
32
|
+
import {
|
|
33
|
+
useChanges,
|
|
34
|
+
useResourceWithRelationships,
|
|
35
|
+
usePodLogs,
|
|
36
|
+
useTopology,
|
|
37
|
+
useUpdateResource,
|
|
38
|
+
useDeleteResource,
|
|
39
|
+
useTriggerCronJob,
|
|
40
|
+
useSuspendCronJob,
|
|
41
|
+
useResumeCronJob,
|
|
42
|
+
useRestartWorkload,
|
|
43
|
+
useWorkloadRevisions,
|
|
44
|
+
useRollbackWorkload,
|
|
45
|
+
useWorkloadPods,
|
|
46
|
+
useFluxReconcile,
|
|
47
|
+
useFluxSyncWithSource,
|
|
48
|
+
useFluxSuspend,
|
|
49
|
+
useFluxResume,
|
|
50
|
+
useArgoSync,
|
|
51
|
+
useArgoRefresh,
|
|
52
|
+
useArgoSuspend,
|
|
53
|
+
useArgoResume,
|
|
54
|
+
useCordonNode,
|
|
55
|
+
useUncordonNode,
|
|
56
|
+
useDrainNode,
|
|
29
57
|
useCascadeDeletePreview,
|
|
30
58
|
useResourceEvents,
|
|
31
59
|
useResource,
|
|
60
|
+
useWorkloadRuns,
|
|
61
|
+
useApplications,
|
|
32
62
|
fetchJSON,
|
|
33
63
|
} from '../../api/client'
|
|
34
64
|
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
35
65
|
import { PrometheusChartsGrid } from '../resource/PrometheusChartsGrid'
|
|
36
66
|
import { RestartEventLane } from '../resource/RestartChart'
|
|
37
|
-
import { RightsizingStrip } from '../resource/RightsizingStrip'
|
|
67
|
+
import { RightsizingPanel, RightsizingStrip } from '../resource/RightsizingStrip'
|
|
68
|
+
import { WorkloadCostTab } from '../cost/WorkloadCostTab'
|
|
69
|
+
import { isOpenCostWorkloadKind } from '../cost/kinds'
|
|
38
70
|
import { useResourceAudit, useResourceIssues, useResources } from '../../api/client'
|
|
39
71
|
import { AuditAlerts, ResourceIssuesSection } from '@skyhook-io/k8s-ui'
|
|
40
72
|
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
73
|
+
import { ScheduledWorkloadLogsViewer } from '../logs/ScheduledWorkloadLogsViewer'
|
|
41
74
|
import { LogsViewer } from '../logs/LogsViewer'
|
|
42
|
-
import {
|
|
75
|
+
import { BatchExecutionFullscreen } from '../execution/BatchExecutionView'
|
|
76
|
+
import { workloadRunTimelineEvents } from '../execution/batch-timeline'
|
|
77
|
+
import {
|
|
78
|
+
useCanUpdateSecrets,
|
|
79
|
+
useCanNodeWrite,
|
|
80
|
+
useNamespacedCapabilities,
|
|
81
|
+
useIsLocalDeployment,
|
|
82
|
+
} from '../../contexts/CapabilitiesContext'
|
|
43
83
|
import { useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal } from '../dock'
|
|
44
|
-
import { PortForwardButton } from '../portforward/PortForwardButton'
|
|
84
|
+
import { PortForwardButton, PortForwardInlineButton } from '../portforward/PortForwardButton'
|
|
85
|
+
import {
|
|
86
|
+
CurlButton,
|
|
87
|
+
CurlPanel,
|
|
88
|
+
isHttpishPort,
|
|
89
|
+
defaultScheme,
|
|
90
|
+
defaultPathForPort,
|
|
91
|
+
} from '../curl/ServiceCurlButton'
|
|
45
92
|
import { useToast } from '../ui/Toast'
|
|
46
93
|
import { Tooltip } from '../ui/Tooltip'
|
|
47
94
|
import { PodRenderer } from '../resources/renderers/PodRenderer'
|
|
@@ -59,13 +106,27 @@ import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
|
59
106
|
import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
|
|
60
107
|
import { useDesktopDownload } from '../../hooks/useDesktopDownload'
|
|
61
108
|
import { useCompareLauncher } from '../compare/useCompareLauncher'
|
|
109
|
+
import { useDiagnoseCustomization } from '../../context/DiagnoseCustomization'
|
|
62
110
|
import { apiVersionToGroup } from '../../utils/navigation'
|
|
63
111
|
|
|
64
112
|
type TabType = WorkloadTabType
|
|
113
|
+
const BATCH_EXECUTION_KINDS = new Set([
|
|
114
|
+
'Job',
|
|
115
|
+
'CronJob',
|
|
116
|
+
'Workflow',
|
|
117
|
+
'CronWorkflow',
|
|
118
|
+
'WorkflowTemplate',
|
|
119
|
+
'ClusterWorkflowTemplate',
|
|
120
|
+
'ScaledJob',
|
|
121
|
+
])
|
|
65
122
|
|
|
66
123
|
// Stable reference — web renderer wrappers inject platform hooks internally
|
|
67
124
|
const rendererOverrides: RendererOverrides = {
|
|
68
|
-
PodRenderer,
|
|
125
|
+
PodRenderer,
|
|
126
|
+
NodeRenderer,
|
|
127
|
+
ServiceRenderer,
|
|
128
|
+
WorkloadRenderer,
|
|
129
|
+
CompositeRenderer,
|
|
69
130
|
ServiceAccountRenderer,
|
|
70
131
|
RoleRenderer,
|
|
71
132
|
RoleBindingRenderer,
|
|
@@ -97,7 +158,11 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
97
158
|
// collapsed three-segment form (/workload/:kind/:name) for older links.
|
|
98
159
|
const parts = location.pathname.replace(/^\//, '').split('/')
|
|
99
160
|
const decode = (s: string): string => {
|
|
100
|
-
try {
|
|
161
|
+
try {
|
|
162
|
+
return decodeURIComponent(s)
|
|
163
|
+
} catch {
|
|
164
|
+
return s
|
|
165
|
+
}
|
|
101
166
|
}
|
|
102
167
|
const kind = decode(parts[1] ?? '')
|
|
103
168
|
let namespace: string
|
|
@@ -121,9 +186,12 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
121
186
|
}
|
|
122
187
|
}, [navigate])
|
|
123
188
|
|
|
124
|
-
const handleNavigate = useCallback(
|
|
125
|
-
|
|
126
|
-
|
|
189
|
+
const handleNavigate = useCallback(
|
|
190
|
+
(resource: SelectedResource) => {
|
|
191
|
+
navigate(relatedResourcePath(resource))
|
|
192
|
+
},
|
|
193
|
+
[navigate],
|
|
194
|
+
)
|
|
127
195
|
|
|
128
196
|
// Hooks must run unconditionally — the invalid-URL guard comes after them.
|
|
129
197
|
// Namespace is empty for cluster-scoped resources, so only kind + name are required.
|
|
@@ -169,6 +237,7 @@ interface WorkloadViewProps {
|
|
|
169
237
|
onCancelExpandIntent?: () => void
|
|
170
238
|
initialTab?: 'detail' | 'yaml'
|
|
171
239
|
group?: string
|
|
240
|
+
pushTabHistory?: boolean
|
|
172
241
|
}
|
|
173
242
|
|
|
174
243
|
function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
@@ -191,7 +260,11 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
191
260
|
const resumeCronJobMutation = useResumeCronJob()
|
|
192
261
|
|
|
193
262
|
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind.toLowerCase())
|
|
194
|
-
const {
|
|
263
|
+
const {
|
|
264
|
+
data: revisionsList,
|
|
265
|
+
isLoading: revisionsLoading,
|
|
266
|
+
error: revisionsError,
|
|
267
|
+
} = useWorkloadRevisions(kind.toLowerCase(), namespace, name, isRollbackKind)
|
|
195
268
|
|
|
196
269
|
const fluxReconcileMutation = useFluxReconcile()
|
|
197
270
|
const fluxSyncWithSourceMutation = useFluxSyncWithSource()
|
|
@@ -203,13 +276,20 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
203
276
|
const argoSuspendMutation = useArgoSuspend()
|
|
204
277
|
const argoResumeMutation = useArgoResume()
|
|
205
278
|
|
|
206
|
-
const { data: cascadePreview, isLoading: cascadeLoading } = useCascadeDeletePreview(
|
|
279
|
+
const { data: cascadePreview, isLoading: cascadeLoading } = useCascadeDeletePreview(
|
|
280
|
+
kind,
|
|
281
|
+
namespace,
|
|
282
|
+
name,
|
|
283
|
+
true,
|
|
284
|
+
)
|
|
207
285
|
|
|
208
286
|
const canNodeWrite = useCanNodeWrite()
|
|
209
287
|
const cordonMutation = useCordonNode()
|
|
210
288
|
const uncordonMutation = useUncordonNode()
|
|
211
289
|
const drainMutation = useDrainNode()
|
|
212
290
|
|
|
291
|
+
const renderDiagnose = useDiagnoseCustomization()
|
|
292
|
+
|
|
213
293
|
return {
|
|
214
294
|
canExec,
|
|
215
295
|
canViewLogs,
|
|
@@ -218,49 +298,80 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
218
298
|
onOpenLogs: openLogs,
|
|
219
299
|
onOpenWorkloadLogs: openWorkloadLogs,
|
|
220
300
|
onOpenNodeTerminal: openNodeTerminal,
|
|
221
|
-
onCopyCommand: (text: string, message: string, event: React.MouseEvent) =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
301
|
+
onCopyCommand: (text: string, message: string, event: React.MouseEvent) =>
|
|
302
|
+
showCopied(text, message, event),
|
|
303
|
+
renderPortForward: ({
|
|
304
|
+
type,
|
|
305
|
+
namespace: ns,
|
|
306
|
+
name: n,
|
|
307
|
+
className,
|
|
308
|
+
}: {
|
|
309
|
+
type: 'pod' | 'service'
|
|
310
|
+
namespace: string
|
|
311
|
+
name: string
|
|
312
|
+
className?: string
|
|
313
|
+
}) => <PortForwardButton type={type} namespace={ns} name={n} className={className} />,
|
|
314
|
+
renderDiagnose,
|
|
315
|
+
onDelete: (
|
|
316
|
+
params: Parameters<typeof deleteMutation.mutate>[0],
|
|
317
|
+
callbacks?: { onSuccess?: () => void },
|
|
318
|
+
) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
226
319
|
isDeleting: deleteMutation.isPending,
|
|
227
320
|
cascadeDependents: cascadePreview?.dependents,
|
|
228
321
|
cascadeLoading,
|
|
229
|
-
onRestart: (params: Parameters<typeof restartWorkloadMutation.mutate>[0]) =>
|
|
322
|
+
onRestart: (params: Parameters<typeof restartWorkloadMutation.mutate>[0]) =>
|
|
323
|
+
restartWorkloadMutation.mutate(params),
|
|
230
324
|
isRestarting: restartWorkloadMutation.isPending,
|
|
231
325
|
revisions: revisionsList,
|
|
232
326
|
revisionsLoading,
|
|
233
327
|
revisionsError: revisionsError ?? null,
|
|
234
|
-
onRollback: (
|
|
328
|
+
onRollback: (
|
|
329
|
+
params: Parameters<typeof rollbackMutation.mutate>[0],
|
|
330
|
+
callbacks?: { onSuccess?: () => void },
|
|
331
|
+
) => rollbackMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
235
332
|
isRollingBack: rollbackMutation.isPending,
|
|
236
|
-
onTriggerCronJob: (params: Parameters<typeof triggerCronJobMutation.mutate>[0]) =>
|
|
333
|
+
onTriggerCronJob: (params: Parameters<typeof triggerCronJobMutation.mutate>[0]) =>
|
|
334
|
+
triggerCronJobMutation.mutate(params),
|
|
237
335
|
isTriggeringCronJob: triggerCronJobMutation.isPending,
|
|
238
|
-
onSuspendCronJob: (params: Parameters<typeof suspendCronJobMutation.mutate>[0]) =>
|
|
336
|
+
onSuspendCronJob: (params: Parameters<typeof suspendCronJobMutation.mutate>[0]) =>
|
|
337
|
+
suspendCronJobMutation.mutate(params),
|
|
239
338
|
isSuspendingCronJob: suspendCronJobMutation.isPending,
|
|
240
|
-
onResumeCronJob: (params: Parameters<typeof resumeCronJobMutation.mutate>[0]) =>
|
|
339
|
+
onResumeCronJob: (params: Parameters<typeof resumeCronJobMutation.mutate>[0]) =>
|
|
340
|
+
resumeCronJobMutation.mutate(params),
|
|
241
341
|
isResumingCronJob: resumeCronJobMutation.isPending,
|
|
242
|
-
onFluxReconcile: (params: Parameters<typeof fluxReconcileMutation.mutate>[0]) =>
|
|
342
|
+
onFluxReconcile: (params: Parameters<typeof fluxReconcileMutation.mutate>[0]) =>
|
|
343
|
+
fluxReconcileMutation.mutate(params),
|
|
243
344
|
isFluxReconciling: fluxReconcileMutation.isPending,
|
|
244
|
-
onFluxSyncWithSource: (params: Parameters<typeof fluxSyncWithSourceMutation.mutate>[0]) =>
|
|
345
|
+
onFluxSyncWithSource: (params: Parameters<typeof fluxSyncWithSourceMutation.mutate>[0]) =>
|
|
346
|
+
fluxSyncWithSourceMutation.mutate(params),
|
|
245
347
|
isFluxSyncing: fluxSyncWithSourceMutation.isPending,
|
|
246
|
-
onFluxSuspend: (params: Parameters<typeof fluxSuspendMutation.mutate>[0]) =>
|
|
348
|
+
onFluxSuspend: (params: Parameters<typeof fluxSuspendMutation.mutate>[0]) =>
|
|
349
|
+
fluxSuspendMutation.mutate(params),
|
|
247
350
|
isFluxSuspending: fluxSuspendMutation.isPending,
|
|
248
|
-
onFluxResume: (params: Parameters<typeof fluxResumeMutation.mutate>[0]) =>
|
|
351
|
+
onFluxResume: (params: Parameters<typeof fluxResumeMutation.mutate>[0]) =>
|
|
352
|
+
fluxResumeMutation.mutate(params),
|
|
249
353
|
isFluxResuming: fluxResumeMutation.isPending,
|
|
250
|
-
onArgoSync: (params: Parameters<typeof argoSyncMutation.mutate>[0]) =>
|
|
354
|
+
onArgoSync: (params: Parameters<typeof argoSyncMutation.mutate>[0]) =>
|
|
355
|
+
argoSyncMutation.mutate(params),
|
|
251
356
|
isArgoSyncing: argoSyncMutation.isPending,
|
|
252
|
-
onArgoRefresh: (params: Parameters<typeof argoRefreshMutation.mutate>[0]) =>
|
|
357
|
+
onArgoRefresh: (params: Parameters<typeof argoRefreshMutation.mutate>[0]) =>
|
|
358
|
+
argoRefreshMutation.mutate(params),
|
|
253
359
|
isArgoRefreshing: argoRefreshMutation.isPending,
|
|
254
|
-
onArgoSuspend: (params: Parameters<typeof argoSuspendMutation.mutate>[0]) =>
|
|
360
|
+
onArgoSuspend: (params: Parameters<typeof argoSuspendMutation.mutate>[0]) =>
|
|
361
|
+
argoSuspendMutation.mutate(params),
|
|
255
362
|
isArgoSuspending: argoSuspendMutation.isPending,
|
|
256
|
-
onArgoResume: (params: Parameters<typeof argoResumeMutation.mutate>[0]) =>
|
|
363
|
+
onArgoResume: (params: Parameters<typeof argoResumeMutation.mutate>[0]) =>
|
|
364
|
+
argoResumeMutation.mutate(params),
|
|
257
365
|
isArgoResuming: argoResumeMutation.isPending,
|
|
258
366
|
canNodeWrite,
|
|
259
|
-
onCordonNode: (params: Parameters<typeof cordonMutation.mutate>[0]) =>
|
|
367
|
+
onCordonNode: (params: Parameters<typeof cordonMutation.mutate>[0]) =>
|
|
368
|
+
cordonMutation.mutate(params),
|
|
260
369
|
isCordoningNode: cordonMutation.isPending,
|
|
261
|
-
onUncordonNode: (params: Parameters<typeof uncordonMutation.mutate>[0]) =>
|
|
370
|
+
onUncordonNode: (params: Parameters<typeof uncordonMutation.mutate>[0]) =>
|
|
371
|
+
uncordonMutation.mutate(params),
|
|
262
372
|
isUncordoningNode: uncordonMutation.isPending,
|
|
263
|
-
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
|
|
373
|
+
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
|
|
374
|
+
drainMutation.mutate(params),
|
|
264
375
|
isDrainingNode: drainMutation.isPending,
|
|
265
376
|
}
|
|
266
377
|
}
|
|
@@ -270,36 +381,101 @@ export function WorkloadView({
|
|
|
270
381
|
namespace,
|
|
271
382
|
name,
|
|
272
383
|
expanded = true,
|
|
384
|
+
pushTabHistory = false,
|
|
273
385
|
...rest
|
|
274
386
|
}: WorkloadViewProps) {
|
|
275
387
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
388
|
+
const apiKind = kindToPlural(kindProp)
|
|
389
|
+
const queryClient = useQueryClient()
|
|
276
390
|
|
|
277
391
|
// Tab state from URL query param — migrate legacy tab names
|
|
278
392
|
const rawTab = searchParams.get('tab')
|
|
279
|
-
const migratedTab: TabType =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
params
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
393
|
+
const migratedTab: TabType =
|
|
394
|
+
rawTab === 'info'
|
|
395
|
+
? 'overview'
|
|
396
|
+
: rawTab === 'events'
|
|
397
|
+
? 'timeline'
|
|
398
|
+
: (rawTab as TabType) || 'overview'
|
|
399
|
+
|
|
400
|
+
const handleTabChange = useCallback(
|
|
401
|
+
(tab: TabType, opts?: { replace?: boolean }) => {
|
|
402
|
+
const params = new URLSearchParams(searchParams)
|
|
403
|
+
if (tab === 'overview') {
|
|
404
|
+
params.delete('tab')
|
|
405
|
+
} else {
|
|
406
|
+
params.set('tab', tab)
|
|
407
|
+
}
|
|
408
|
+
setSearchParams(params, { replace: opts?.replace ?? !pushTabHistory })
|
|
409
|
+
},
|
|
410
|
+
[pushTabHistory, searchParams, setSearchParams],
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
const selectedRunKey = searchParams.get('run') ?? ''
|
|
414
|
+
const handleSelectedRunChange = useCallback(
|
|
415
|
+
(runKey: string) => {
|
|
416
|
+
const params = new URLSearchParams(searchParams)
|
|
417
|
+
if (runKey) params.set('run', runKey)
|
|
418
|
+
else params.delete('run')
|
|
419
|
+
setSearchParams(params, { replace: true })
|
|
420
|
+
},
|
|
421
|
+
[searchParams, setSearchParams],
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
const batchKind = pluralToKind(apiKind)
|
|
425
|
+
const batchExecution = BATCH_EXECUTION_KINDS.has(batchKind)
|
|
426
|
+
const batchRunsQuery = useWorkloadRuns(apiKind, namespace, name, expanded && batchExecution, {
|
|
427
|
+
refetchActive: true,
|
|
428
|
+
clusterScoped: batchKind === 'ClusterWorkflowTemplate',
|
|
429
|
+
})
|
|
430
|
+
const relatedTimelineEvents = useMemo(
|
|
431
|
+
() => workloadRunTimelineEvents(batchRunsQuery.data?.runs ?? []),
|
|
432
|
+
[batchRunsQuery.data?.runs],
|
|
433
|
+
)
|
|
292
434
|
|
|
293
435
|
// Fetch resource with relationships
|
|
294
|
-
const {
|
|
436
|
+
const {
|
|
437
|
+
data: resourceResponse,
|
|
438
|
+
isLoading: resourceLoading,
|
|
439
|
+
error: resourceError,
|
|
440
|
+
refetch: refetchResource,
|
|
441
|
+
} = useResourceWithRelationships<any>(apiKind, namespace, name, rest.group)
|
|
295
442
|
const resource = resourceResponse?.resource
|
|
296
443
|
const relationships = resourceResponse?.relationships
|
|
444
|
+
const refetchResourceAndRuns = useCallback(async () => {
|
|
445
|
+
await Promise.all([
|
|
446
|
+
refetchResource(),
|
|
447
|
+
queryClient.refetchQueries({
|
|
448
|
+
queryKey: ['workload-runs', apiKind, namespace, name],
|
|
449
|
+
}),
|
|
450
|
+
])
|
|
451
|
+
}, [apiKind, name, namespace, queryClient, refetchResource])
|
|
452
|
+
const podWorkloadOwner = useMemo(
|
|
453
|
+
() => podWorkloadOwnerFromRelationships(apiKind, namespace, relationships, resource),
|
|
454
|
+
[apiKind, namespace, relationships, resource],
|
|
455
|
+
)
|
|
456
|
+
const podOwnerAppsQuery = useApplications(
|
|
457
|
+
podWorkloadOwner?.namespace ? [podWorkloadOwner.namespace] : [],
|
|
458
|
+
{ enabled: Boolean(podWorkloadOwner?.namespace) },
|
|
459
|
+
)
|
|
460
|
+
const ownershipContext = useMemo(
|
|
461
|
+
() => buildPodOwnershipContext(podWorkloadOwner, podOwnerAppsQuery.data?.applications),
|
|
462
|
+
[podWorkloadOwner, podOwnerAppsQuery.data?.applications],
|
|
463
|
+
)
|
|
297
464
|
const certificateInfo = resourceResponse?.certificateInfo
|
|
298
465
|
const hpaDiagnosis = resourceResponse?.hpaDiagnosis
|
|
299
|
-
const relationshipGitopsOwner = useMemo(
|
|
466
|
+
const relationshipGitopsOwner = useMemo(
|
|
467
|
+
() => gitOpsOwnerFromRelationships(relationships),
|
|
468
|
+
[relationships],
|
|
469
|
+
)
|
|
300
470
|
const inheritedGitOpsLookupRef = useMemo(
|
|
301
|
-
() =>
|
|
302
|
-
|
|
471
|
+
() =>
|
|
472
|
+
findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, {
|
|
473
|
+
kind: apiKind,
|
|
474
|
+
namespace,
|
|
475
|
+
name,
|
|
476
|
+
group: rest.group,
|
|
477
|
+
}),
|
|
478
|
+
[relationships, relationshipGitopsOwner, apiKind, namespace, name, rest.group],
|
|
303
479
|
)
|
|
304
480
|
const inheritedGitOpsResponse = useResourceWithRelationships<any>(
|
|
305
481
|
inheritedGitOpsLookupRef ? kindToPlural(inheritedGitOpsLookupRef.kind) : '',
|
|
@@ -312,19 +488,34 @@ export function WorkloadView({
|
|
|
312
488
|
[inheritedGitOpsResponse.data?.relationships],
|
|
313
489
|
)
|
|
314
490
|
const relationshipHelmOwner = useMemo(
|
|
315
|
-
() =>
|
|
491
|
+
() =>
|
|
492
|
+
nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
|
|
316
493
|
[relationships, resource?.metadata?.namespace, namespace],
|
|
317
494
|
)
|
|
318
495
|
const inheritedHelmOwner = useMemo(
|
|
319
|
-
() =>
|
|
320
|
-
|
|
496
|
+
() =>
|
|
497
|
+
nativeHelmOwnerFromRelationships(
|
|
498
|
+
inheritedGitOpsResponse.data?.relationships,
|
|
499
|
+
inheritedGitOpsResponse.data?.resource?.metadata?.namespace ?? namespace,
|
|
500
|
+
),
|
|
501
|
+
[
|
|
502
|
+
inheritedGitOpsResponse.data?.relationships,
|
|
503
|
+
inheritedGitOpsResponse.data?.resource?.metadata?.namespace,
|
|
504
|
+
namespace,
|
|
505
|
+
],
|
|
321
506
|
)
|
|
322
507
|
const rawGitopsOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
|
|
323
|
-
const gitOpsSourceResource = relationshipGitopsOwner
|
|
508
|
+
const gitOpsSourceResource = relationshipGitopsOwner
|
|
509
|
+
? resource
|
|
510
|
+
: inheritedGitOpsResponse.data?.resource
|
|
324
511
|
const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
|
|
325
|
-
const helmSourceResource = relationshipHelmOwner
|
|
512
|
+
const helmSourceResource = relationshipHelmOwner
|
|
513
|
+
? resource
|
|
514
|
+
: inheritedGitOpsResponse.data?.resource
|
|
326
515
|
const shouldResolveArgoOwner = rawGitopsOwner?.tool === 'argocd' && !rawGitopsOwner.namespace
|
|
327
|
-
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
516
|
+
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
517
|
+
enabled: shouldResolveArgoOwner,
|
|
518
|
+
})
|
|
328
519
|
const gitopsOwner = useMemo(
|
|
329
520
|
() => resolveGitOpsOwner(rawGitopsOwner, argoApplications),
|
|
330
521
|
[rawGitopsOwner, argoApplications],
|
|
@@ -342,7 +533,9 @@ export function WorkloadView({
|
|
|
342
533
|
[gitopsOwner, gitopsOwnerQuery.data],
|
|
343
534
|
)
|
|
344
535
|
const gitOpsOwnerVerified = Boolean(gitopsOwner?.namespace && gitopsOwnerQuery.data)
|
|
345
|
-
const gitOpsOwnerPending = Boolean(
|
|
536
|
+
const gitOpsOwnerPending = Boolean(
|
|
537
|
+
gitopsOwner?.namespace && gitopsOwnerQuery.isLoading && !gitopsOwnerQuery.data,
|
|
538
|
+
)
|
|
346
539
|
const gitOpsOwnerSource = useMemo(
|
|
347
540
|
() => describeGitOpsOwnerSource(rawGitopsOwner, gitOpsSourceResource),
|
|
348
541
|
[rawGitopsOwner, gitOpsSourceResource],
|
|
@@ -353,19 +546,29 @@ export function WorkloadView({
|
|
|
353
546
|
)
|
|
354
547
|
|
|
355
548
|
// For pods: extract envFrom ConfigMap/Secret names and resolve their keys
|
|
356
|
-
const isPod =
|
|
549
|
+
const isPod = apiKind === 'pods'
|
|
357
550
|
const { envFromConfigMapNames, envFromSecretNames } = useMemo(() => {
|
|
358
|
-
if (!isPod || !resource)
|
|
551
|
+
if (!isPod || !resource)
|
|
552
|
+
return {
|
|
553
|
+
envFromConfigMapNames: [] as string[],
|
|
554
|
+
envFromSecretNames: [] as string[],
|
|
555
|
+
}
|
|
359
556
|
const cmNames = new Set<string>()
|
|
360
557
|
const secretNames = new Set<string>()
|
|
361
|
-
const containers = [
|
|
558
|
+
const containers = [
|
|
559
|
+
...(resource.spec?.containers || []),
|
|
560
|
+
...(resource.spec?.initContainers || []),
|
|
561
|
+
]
|
|
362
562
|
for (const c of containers) {
|
|
363
|
-
for (const ef of
|
|
563
|
+
for (const ef of c.envFrom || []) {
|
|
364
564
|
if (ef.configMapRef?.name) cmNames.add(ef.configMapRef.name)
|
|
365
565
|
if (ef.secretRef?.name) secretNames.add(ef.secretRef.name)
|
|
366
566
|
}
|
|
367
567
|
}
|
|
368
|
-
return {
|
|
568
|
+
return {
|
|
569
|
+
envFromConfigMapNames: Array.from(cmNames),
|
|
570
|
+
envFromSecretNames: Array.from(secretNames),
|
|
571
|
+
}
|
|
369
572
|
}, [isPod, resource])
|
|
370
573
|
|
|
371
574
|
const configMapQueries = useQueries({
|
|
@@ -387,28 +590,44 @@ export function WorkloadView({
|
|
|
387
590
|
})
|
|
388
591
|
|
|
389
592
|
const resolvedEnvFrom = useMemo(() => {
|
|
390
|
-
if (!isPod || (envFromConfigMapNames.length === 0 && envFromSecretNames.length === 0))
|
|
593
|
+
if (!isPod || (envFromConfigMapNames.length === 0 && envFromSecretNames.length === 0))
|
|
594
|
+
return undefined
|
|
391
595
|
const result: ResolvedEnvFrom = {}
|
|
392
596
|
envFromConfigMapNames.forEach((n, i) => {
|
|
393
597
|
// Single-resource endpoint returns { resource, relationships } wrapper
|
|
394
598
|
const cm = configMapQueries[i]?.data?.resource ?? configMapQueries[i]?.data
|
|
395
|
-
if (cm)
|
|
599
|
+
if (cm)
|
|
600
|
+
result[resolvedEnvFromKey('configmap', n)] = {
|
|
601
|
+
keys: Object.keys(cm.data || {}),
|
|
602
|
+
values: cm.data || {},
|
|
603
|
+
isSecret: false,
|
|
604
|
+
}
|
|
396
605
|
})
|
|
397
606
|
envFromSecretNames.forEach((n, i) => {
|
|
398
607
|
const secret = secretQueries[i]?.data?.resource ?? secretQueries[i]?.data
|
|
399
608
|
if (secret) {
|
|
400
609
|
const decodedValues: Record<string, string> = {}
|
|
401
610
|
for (const [k, v] of Object.entries(secret.data || {})) {
|
|
402
|
-
try {
|
|
611
|
+
try {
|
|
612
|
+
decodedValues[k] = atob(v as string)
|
|
613
|
+
} catch {
|
|
614
|
+
decodedValues[k] = v as string
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
result[resolvedEnvFromKey('secret', n)] = {
|
|
618
|
+
keys: Object.keys(decodedValues),
|
|
619
|
+
values: decodedValues,
|
|
620
|
+
isSecret: true,
|
|
403
621
|
}
|
|
404
|
-
result[resolvedEnvFromKey('secret', n)] = { keys: Object.keys(decodedValues), values: decodedValues, isSecret: true }
|
|
405
622
|
}
|
|
406
623
|
})
|
|
407
624
|
return Object.keys(result).length > 0 ? result : undefined
|
|
408
625
|
}, [isPod, envFromConfigMapNames, envFromSecretNames, configMapQueries, secretQueries])
|
|
409
626
|
|
|
410
627
|
// Fetch topology for hierarchy building (only when expanded)
|
|
411
|
-
const { data: topology } = useTopology([namespace], 'resources', {
|
|
628
|
+
const { data: topology } = useTopology([namespace], 'resources', {
|
|
629
|
+
enabled: expanded,
|
|
630
|
+
})
|
|
412
631
|
|
|
413
632
|
// Always fetched so Recent Events populates on drawer open; allEvents below is
|
|
414
633
|
// gated on expanded because it's namespace-wide and expensive.
|
|
@@ -418,7 +637,7 @@ export function WorkloadView({
|
|
|
418
637
|
isLoading: resourceFocusedEventsLoading,
|
|
419
638
|
k8sError: resourceFocusedK8sError,
|
|
420
639
|
updatesError: resourceFocusedUpdatesError,
|
|
421
|
-
} = useResourceEvents(
|
|
640
|
+
} = useResourceEvents(apiKind, namespace, name)
|
|
422
641
|
|
|
423
642
|
// Fetch all events for this resource's namespace (only when expanded)
|
|
424
643
|
const { data: allEvents, isLoading: eventsLoading } = useChanges({
|
|
@@ -432,8 +651,80 @@ export function WorkloadView({
|
|
|
432
651
|
|
|
433
652
|
// RBAC
|
|
434
653
|
const canUpdateSecrets = useCanUpdateSecrets()
|
|
654
|
+
const { canPortForward } = useNamespacedCapabilities(namespace)
|
|
655
|
+
const isLocalDeployment = useIsLocalDeployment()
|
|
656
|
+
const showServingPortForward = canPortForward || !isLocalDeployment
|
|
657
|
+
const showServingCurl = !isLocalDeployment
|
|
658
|
+
const [servingCurl, setServingCurl] = useState<{
|
|
659
|
+
namespace: string
|
|
660
|
+
serviceName: string
|
|
661
|
+
port: number
|
|
662
|
+
closing: boolean
|
|
663
|
+
} | null>(null)
|
|
664
|
+
const closeServingCurl = useCallback(() => {
|
|
665
|
+
setServingCurl((p) => (p ? { ...p, closing: true } : null))
|
|
666
|
+
window.setTimeout(() => setServingCurl((p) => (p?.closing ? null : p)), 220)
|
|
667
|
+
}, [])
|
|
668
|
+
const renderServicePortAction = useCallback(
|
|
669
|
+
(props: ServicePortRenderProps) => {
|
|
670
|
+
const active =
|
|
671
|
+
servingCurl?.namespace === props.namespace &&
|
|
672
|
+
servingCurl?.serviceName === props.serviceName &&
|
|
673
|
+
servingCurl?.port === props.port &&
|
|
674
|
+
!servingCurl.closing
|
|
675
|
+
return (
|
|
676
|
+
<>
|
|
677
|
+
{showServingCurl &&
|
|
678
|
+
isHttpishPort(props.port, props.name, props.appProtocol, props.protocol) && (
|
|
679
|
+
<CurlButton
|
|
680
|
+
active={active}
|
|
681
|
+
onClick={() => {
|
|
682
|
+
if (active) closeServingCurl()
|
|
683
|
+
else
|
|
684
|
+
setServingCurl({
|
|
685
|
+
namespace: props.namespace,
|
|
686
|
+
serviceName: props.serviceName,
|
|
687
|
+
port: props.port,
|
|
688
|
+
closing: false,
|
|
689
|
+
})
|
|
690
|
+
}}
|
|
691
|
+
/>
|
|
692
|
+
)}
|
|
693
|
+
{showServingPortForward && (
|
|
694
|
+
<PortForwardInlineButton
|
|
695
|
+
namespace={props.namespace}
|
|
696
|
+
serviceName={props.serviceName}
|
|
697
|
+
port={props.port}
|
|
698
|
+
protocol={props.protocol}
|
|
699
|
+
/>
|
|
700
|
+
)}
|
|
701
|
+
</>
|
|
702
|
+
)
|
|
703
|
+
},
|
|
704
|
+
[closeServingCurl, servingCurl, showServingCurl, showServingPortForward],
|
|
705
|
+
)
|
|
706
|
+
const renderServicePortPanel = useCallback(
|
|
707
|
+
(props: ServicePortRenderProps) => {
|
|
708
|
+
const active =
|
|
709
|
+
servingCurl?.namespace === props.namespace &&
|
|
710
|
+
servingCurl?.serviceName === props.serviceName &&
|
|
711
|
+
servingCurl?.port === props.port
|
|
712
|
+
return active ? (
|
|
713
|
+
<CurlPanel
|
|
714
|
+
namespace={props.namespace}
|
|
715
|
+
serviceName={props.serviceName}
|
|
716
|
+
port={props.port}
|
|
717
|
+
initialScheme={defaultScheme(props.port, props.name, props.appProtocol)}
|
|
718
|
+
initialPath={defaultPathForPort(props.port, props.name, props.appProtocol)}
|
|
719
|
+
open={!servingCurl.closing}
|
|
720
|
+
onClose={closeServingCurl}
|
|
721
|
+
/>
|
|
722
|
+
) : null
|
|
723
|
+
},
|
|
724
|
+
[closeServingCurl, servingCurl],
|
|
725
|
+
)
|
|
435
726
|
const updateResource = useUpdateResource()
|
|
436
|
-
const baseActionsBarProps = useActionsBarProps(
|
|
727
|
+
const baseActionsBarProps = useActionsBarProps(apiKind, namespace, name)
|
|
437
728
|
const desktopDownload = useDesktopDownload()
|
|
438
729
|
|
|
439
730
|
const resourceGroup = useMemo(
|
|
@@ -443,14 +734,20 @@ export function WorkloadView({
|
|
|
443
734
|
// Live Operational Issues for this resource. Fetched here (not inside the lead
|
|
444
735
|
// render-prop) so the count also gates `hasOperationalIssues` — which tells the
|
|
445
736
|
// renderers to suppress their own status-derived problems and avoid duplicates.
|
|
446
|
-
// Keyed on the
|
|
737
|
+
// Keyed on the stable API kind+group (same inputs as the resource fetch above),
|
|
447
738
|
// NOT the manifest-derived ones: deriving kind/group from the loaded resource
|
|
448
739
|
// would flip the query key when the manifest arrives, drop liveIssues, and flash
|
|
449
740
|
// the renderer banners. The backend canonicalizes a plural kind via discovery,
|
|
450
|
-
// so
|
|
451
|
-
const { data: liveIssues } = useResourceIssues(
|
|
452
|
-
const {
|
|
453
|
-
|
|
741
|
+
// so using the normalized API kind resolves direct links and app navigation alike.
|
|
742
|
+
const { data: liveIssues, isPending: issuesPending } = useResourceIssues(apiKind, rest.group, namespace, name)
|
|
743
|
+
const { data: auditFindings } = useResourceAudit(apiKind, namespace, name)
|
|
744
|
+
const hasOperationalIssues = Boolean(liveIssues?.length)
|
|
745
|
+
const {
|
|
746
|
+
onCompareTo,
|
|
747
|
+
onCompareAcrossClusters,
|
|
748
|
+
picker: comparePicker,
|
|
749
|
+
} = useCompareLauncher({
|
|
750
|
+
kind: apiKind,
|
|
454
751
|
namespace,
|
|
455
752
|
name,
|
|
456
753
|
// Prefer the URL-supplied group so Compare works even before the resource
|
|
@@ -463,9 +760,12 @@ export function WorkloadView({
|
|
|
463
760
|
[baseActionsBarProps, onCompareTo, onCompareAcrossClusters],
|
|
464
761
|
)
|
|
465
762
|
|
|
466
|
-
const handleUpdateResource = useCallback(
|
|
467
|
-
|
|
468
|
-
|
|
763
|
+
const handleUpdateResource = useCallback(
|
|
764
|
+
async (params: { kind: string; namespace: string; name: string; yaml: string }) => {
|
|
765
|
+
await updateResource.mutateAsync(params)
|
|
766
|
+
},
|
|
767
|
+
[updateResource],
|
|
768
|
+
)
|
|
469
769
|
|
|
470
770
|
const navigateRouter = useNavigate()
|
|
471
771
|
const handleOpenGitOpsResource = useCallback(
|
|
@@ -473,7 +773,10 @@ export function WorkloadView({
|
|
|
473
773
|
const params = new URLSearchParams()
|
|
474
774
|
const namespaces = searchParams.get('namespaces')
|
|
475
775
|
if (namespaces) params.set('namespaces', namespaces)
|
|
476
|
-
navigateRouter({
|
|
776
|
+
navigateRouter({
|
|
777
|
+
pathname: gitOpsRouteForOwner(ref),
|
|
778
|
+
search: params.toString(),
|
|
779
|
+
})
|
|
477
780
|
},
|
|
478
781
|
[navigateRouter, searchParams],
|
|
479
782
|
)
|
|
@@ -491,113 +794,250 @@ export function WorkloadView({
|
|
|
491
794
|
},
|
|
492
795
|
[navigateRouter, searchParams],
|
|
493
796
|
)
|
|
797
|
+
const handleOpenApplication = useCallback(
|
|
798
|
+
(appKey: string) => {
|
|
799
|
+
const params = new URLSearchParams()
|
|
800
|
+
const namespaces = new Set(
|
|
801
|
+
(searchParams.get('namespaces') ?? '')
|
|
802
|
+
.split(',')
|
|
803
|
+
.map((ns) => ns.trim())
|
|
804
|
+
.filter(Boolean),
|
|
805
|
+
)
|
|
806
|
+
if (ownershipContext?.application?.key === appKey && ownershipContext.workload.namespace) {
|
|
807
|
+
namespaces.add(ownershipContext.workload.namespace)
|
|
808
|
+
}
|
|
809
|
+
if (namespaces.size > 0) params.set('namespaces', Array.from(namespaces).join(','))
|
|
810
|
+
params.set('app', appKey)
|
|
811
|
+
navigateRouter({ pathname: '/applications', search: params.toString() })
|
|
812
|
+
},
|
|
813
|
+
[navigateRouter, ownershipContext, searchParams],
|
|
814
|
+
)
|
|
494
815
|
|
|
495
816
|
// Duplicate dialog
|
|
496
817
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
497
818
|
const [duplicateYaml, setDuplicateYaml] = useState('')
|
|
498
819
|
|
|
499
|
-
const handleDuplicate = useCallback(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
820
|
+
const handleDuplicate = useCallback(
|
|
821
|
+
(params: { kind: string; namespace: string; name: string; yaml: string }) => {
|
|
822
|
+
setDuplicateYaml(cleanYamlForDuplicate(params.yaml))
|
|
823
|
+
setDuplicateDialogOpen(true)
|
|
824
|
+
},
|
|
825
|
+
[],
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
const supportsWorkloadPods = ['deployments', 'statefulsets', 'daemonsets'].includes(apiKind)
|
|
829
|
+
const workloadPodsQuery = useWorkloadPods(supportsWorkloadPods ? apiKind : '', namespace, name)
|
|
830
|
+
const servingRefs = useMemo(() => collectServingRefs(relationships), [relationships])
|
|
831
|
+
const servingQueries = useQueries({
|
|
832
|
+
queries: servingRefs.map((ref) => {
|
|
833
|
+
const pluralKind = kindToPlural(ref.kind)
|
|
834
|
+
const ns = ref.namespace || '_'
|
|
835
|
+
const params = new URLSearchParams()
|
|
836
|
+
if (ref.group) params.set('group', ref.group)
|
|
837
|
+
const queryString = params.toString()
|
|
838
|
+
return {
|
|
839
|
+
queryKey: ['resource', pluralKind, ref.namespace, ref.name, ref.group],
|
|
840
|
+
queryFn: () =>
|
|
841
|
+
fetchJSON<any>(
|
|
842
|
+
`/resources/${pluralKind}/${ns}/${ref.name}${queryString ? `?${queryString}` : ''}`,
|
|
843
|
+
),
|
|
844
|
+
enabled: expanded && Boolean(ref.kind && ref.name),
|
|
845
|
+
staleTime: 30000,
|
|
846
|
+
}
|
|
847
|
+
}),
|
|
848
|
+
})
|
|
849
|
+
const servingResources = useMemo<ServingResourceDetail[]>(
|
|
850
|
+
() =>
|
|
851
|
+
servingRefs.map((ref, index) => {
|
|
852
|
+
const query = servingQueries[index]
|
|
853
|
+
const data = query?.data?.resource ?? query?.data
|
|
854
|
+
return {
|
|
855
|
+
ref,
|
|
856
|
+
resource: data,
|
|
857
|
+
loading: query?.isLoading ?? false,
|
|
858
|
+
error: (query?.error as Error | null) ?? null,
|
|
859
|
+
}
|
|
860
|
+
}),
|
|
861
|
+
[servingRefs, servingQueries],
|
|
862
|
+
)
|
|
503
863
|
|
|
504
864
|
return (
|
|
505
865
|
<>
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
866
|
+
<BaseWorkloadView
|
|
867
|
+
kind={apiKind}
|
|
868
|
+
namespace={namespace}
|
|
869
|
+
name={name}
|
|
870
|
+
expanded={expanded}
|
|
871
|
+
{...rest}
|
|
872
|
+
// Data
|
|
873
|
+
resource={resource}
|
|
874
|
+
relationships={relationships}
|
|
875
|
+
ownershipContext={ownershipContext}
|
|
876
|
+
onOpenApplication={handleOpenApplication}
|
|
877
|
+
certificateInfo={certificateInfo}
|
|
878
|
+
hpaDiagnosis={hpaDiagnosis}
|
|
879
|
+
workloadPods={supportsWorkloadPods ? workloadPodsQuery.data?.pods : undefined}
|
|
880
|
+
workloadPodsLoading={supportsWorkloadPods ? workloadPodsQuery.isLoading : false}
|
|
881
|
+
workloadPodsError={supportsWorkloadPods ? (workloadPodsQuery.error as Error | null) : null}
|
|
882
|
+
servingResources={servingResources}
|
|
883
|
+
renderServicePortAction={renderServicePortAction}
|
|
884
|
+
renderServicePortPanel={renderServicePortPanel}
|
|
885
|
+
isLoading={resourceLoading}
|
|
886
|
+
resourceError={resourceError}
|
|
887
|
+
refetch={refetchResourceAndRuns}
|
|
888
|
+
// Timeline
|
|
889
|
+
allEvents={allEvents}
|
|
890
|
+
relatedTimelineEvents={relatedTimelineEvents}
|
|
891
|
+
eventsLoading={eventsLoading || (batchExecution && batchRunsQuery.isLoading)}
|
|
892
|
+
topology={topology}
|
|
893
|
+
resourceFocusedK8sEvents={resourceFocusedK8sEvents}
|
|
894
|
+
resourceFocusedUpdates={resourceFocusedUpdates}
|
|
895
|
+
resourceFocusedEventsLoading={resourceFocusedEventsLoading}
|
|
896
|
+
resourceFocusedK8sError={resourceFocusedK8sError}
|
|
897
|
+
resourceFocusedUpdatesError={resourceFocusedUpdatesError}
|
|
898
|
+
// Capabilities
|
|
899
|
+
canUpdateSecrets={canUpdateSecrets}
|
|
900
|
+
// Mutations
|
|
901
|
+
onUpdateResource={handleUpdateResource}
|
|
902
|
+
isUpdatingResource={updateResource.isPending}
|
|
903
|
+
updateResourceError={updateResource.error?.message ?? null}
|
|
904
|
+
// Tab state (URL-synced)
|
|
905
|
+
activeTab={migratedTab}
|
|
906
|
+
onTabChange={handleTabChange}
|
|
907
|
+
// Render props
|
|
908
|
+
renderLogsTab={(props) => (
|
|
909
|
+
<LogsTabContent
|
|
910
|
+
{...props}
|
|
911
|
+
selectedRunKey={selectedRunKey}
|
|
912
|
+
onSelectRun={handleSelectedRunChange}
|
|
913
|
+
/>
|
|
914
|
+
)}
|
|
915
|
+
renderExpandedOverview={({ kind: k, apiKind, namespace: ns, name: n, resource: res }) =>
|
|
916
|
+
BATCH_EXECUTION_KINDS.has(k) && res ? (
|
|
917
|
+
<BatchExecutionFullscreen
|
|
918
|
+
kind={k}
|
|
919
|
+
apiKind={apiKind}
|
|
920
|
+
namespace={ns}
|
|
921
|
+
name={n}
|
|
922
|
+
resource={res}
|
|
923
|
+
selectedRunKey={selectedRunKey}
|
|
924
|
+
canViewLogs={baseActionsBarProps.canViewLogs}
|
|
925
|
+
onSelectRun={handleSelectedRunChange}
|
|
926
|
+
onSwitchToLogs={() => handleTabChange('logs')}
|
|
927
|
+
onSwitchToTimeline={() => handleTabChange('timeline')}
|
|
928
|
+
onNavigateToResource={rest.onNavigateToResource}
|
|
929
|
+
/>
|
|
930
|
+
) : null
|
|
931
|
+
}
|
|
932
|
+
renderRelatedYaml={(ref) => (
|
|
933
|
+
<RelatedResourceYaml key={`${ref.kind}/${ref.namespace}/${ref.name}`} target={ref} />
|
|
934
|
+
)}
|
|
935
|
+
renderMetricsTab={({ kind, namespace: ns, name: n }) => (
|
|
936
|
+
<MetricsTabContent
|
|
937
|
+
kind={kind}
|
|
938
|
+
namespace={ns}
|
|
939
|
+
name={n}
|
|
940
|
+
resource={resource}
|
|
941
|
+
expanded={expanded}
|
|
942
|
+
/>
|
|
943
|
+
)}
|
|
944
|
+
renderCostTab={({ kind, namespace: ns, name: n }) => (
|
|
945
|
+
<div className="space-y-4">
|
|
946
|
+
<RightsizingPanel kind={kind} namespace={ns} name={n} />
|
|
947
|
+
<WorkloadCostTab kind={kind} namespace={ns} name={n} />
|
|
948
|
+
</div>
|
|
949
|
+
)}
|
|
950
|
+
isMetricsAvailable={(kind, res) =>
|
|
951
|
+
isPrometheusSupported(kind) && !(kind === 'Pod' && res?.status?.phase === 'Pending')
|
|
952
|
+
}
|
|
953
|
+
isCostAvailable={(kind) => isOpenCostWorkloadKind(kind)}
|
|
954
|
+
onDuplicate={handleDuplicate}
|
|
955
|
+
onDownload={desktopDownload}
|
|
956
|
+
actionsBarProps={actionsBarProps}
|
|
957
|
+
rendererOverrides={rendererOverrides}
|
|
958
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
959
|
+
renderOverviewExtra={({ kind: k, namespace: ns, name: n }) => (
|
|
960
|
+
<>
|
|
961
|
+
<FluxSourceConsumersSection kind={k} namespace={ns} name={n} />
|
|
962
|
+
<AuditOverviewSection
|
|
963
|
+
findings={auditFindings ?? []}
|
|
964
|
+
onViewAll={() => navigateRouter('/checks')}
|
|
965
|
+
/>
|
|
966
|
+
</>
|
|
967
|
+
)}
|
|
968
|
+
renderOverviewLead={() => (
|
|
969
|
+
<ResourceIssuesSection
|
|
970
|
+
issues={liveIssues}
|
|
971
|
+
subjectResource={{ kind: apiKind, namespace, name, group: rest.group }}
|
|
972
|
+
onResourceClick={
|
|
973
|
+
rest.onNavigateToResource
|
|
974
|
+
? (ref) =>
|
|
975
|
+
rest.onNavigateToResource?.({
|
|
976
|
+
kind: kindToPlural(ref.kind),
|
|
977
|
+
namespace: ref.namespace ?? '',
|
|
978
|
+
name: ref.name,
|
|
979
|
+
group: ref.group ?? '',
|
|
980
|
+
})
|
|
981
|
+
: undefined
|
|
982
|
+
}
|
|
983
|
+
/>
|
|
984
|
+
)}
|
|
985
|
+
hasOperationalIssues={hasOperationalIssues}
|
|
986
|
+
operationalIssuesPending={issuesPending}
|
|
987
|
+
onOpenGitOpsResource={gitopsOwnerQuery.data ? handleOpenGitOpsResource : undefined}
|
|
988
|
+
resolvedGitOpsOwner={gitopsOwner}
|
|
989
|
+
gitOpsOwnerVerified={gitOpsOwnerVerified}
|
|
990
|
+
gitOpsOwnerPending={gitOpsOwnerPending}
|
|
991
|
+
gitOpsOwnerSource={gitOpsOwnerSource}
|
|
992
|
+
gitOpsOwnerStatus={gitOpsOwnerStatus}
|
|
993
|
+
helmOwner={helmOwner}
|
|
994
|
+
helmOwnerSource={helmOwnerSource}
|
|
995
|
+
onOpenHelmRelease={handleOpenHelmRelease}
|
|
996
|
+
onNavigateGitOpsPath={handleNavigateGitOpsPath}
|
|
997
|
+
/>
|
|
998
|
+
<CreateResourceDialog
|
|
999
|
+
open={duplicateDialogOpen}
|
|
1000
|
+
onClose={() => setDuplicateDialogOpen(false)}
|
|
1001
|
+
initialYaml={duplicateYaml}
|
|
1002
|
+
title="Duplicate Resource"
|
|
1003
|
+
onCreated={(result) => {
|
|
1004
|
+
rest.onNavigateToResource?.({
|
|
1005
|
+
kind: kindToPlural(result.kind),
|
|
1006
|
+
namespace: result.namespace,
|
|
1007
|
+
name: result.name,
|
|
1008
|
+
group: '',
|
|
1009
|
+
})
|
|
1010
|
+
}}
|
|
1011
|
+
/>
|
|
1012
|
+
{comparePicker}
|
|
596
1013
|
</>
|
|
597
1014
|
)
|
|
598
1015
|
}
|
|
599
1016
|
|
|
600
|
-
function
|
|
1017
|
+
function collectServingRefs(relationships: Relationships | undefined): ResourceRef[] {
|
|
1018
|
+
if (!relationships) return []
|
|
1019
|
+
return dedupeRefs([
|
|
1020
|
+
...(relationships.services ?? []),
|
|
1021
|
+
...(relationships.ingresses ?? []),
|
|
1022
|
+
...(relationships.gateways ?? []),
|
|
1023
|
+
...(relationships.routes ?? []),
|
|
1024
|
+
])
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
1028
|
+
const seen = new Set<string>()
|
|
1029
|
+
return refs.filter((ref) => {
|
|
1030
|
+
const key = `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
1031
|
+
if (seen.has(key)) return false
|
|
1032
|
+
seen.add(key)
|
|
1033
|
+
return true
|
|
1034
|
+
})
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function resolveGitOpsOwner(
|
|
1038
|
+
owner: GitOpsOwnerRef | null,
|
|
1039
|
+
argoApplications: any[] | undefined,
|
|
1040
|
+
): GitOpsOwnerRef | null {
|
|
601
1041
|
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
602
1042
|
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
603
1043
|
if (matches.length !== 1) return owner
|
|
@@ -611,9 +1051,8 @@ function findInheritedGitOpsLookupRef(
|
|
|
611
1051
|
current: ResourceRef,
|
|
612
1052
|
): ResourceRef | null {
|
|
613
1053
|
if (directOwner) return null
|
|
614
|
-
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
615
|
-
!gitOpsOwnerFromRelationships({ managedBy: [ref] })
|
|
616
|
-
&& !isNativeHelmManager(ref)
|
|
1054
|
+
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
1055
|
+
(ref) => !gitOpsOwnerFromRelationships({ managedBy: [ref] }) && !isNativeHelmManager(ref),
|
|
617
1056
|
)
|
|
618
1057
|
const candidates = [
|
|
619
1058
|
relationships?.deployment,
|
|
@@ -624,7 +1063,89 @@ function findInheritedGitOpsLookupRef(
|
|
|
624
1063
|
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
625
1064
|
}
|
|
626
1065
|
|
|
627
|
-
|
|
1066
|
+
const POD_OWNERSHIP_WORKLOAD_KINDS = new Set([
|
|
1067
|
+
'deployments',
|
|
1068
|
+
'statefulsets',
|
|
1069
|
+
'daemonsets',
|
|
1070
|
+
'jobs',
|
|
1071
|
+
'cronjobs',
|
|
1072
|
+
'rollouts',
|
|
1073
|
+
])
|
|
1074
|
+
|
|
1075
|
+
function podWorkloadOwnerFromRelationships(
|
|
1076
|
+
kind: string,
|
|
1077
|
+
namespace: string,
|
|
1078
|
+
relationships: Relationships | undefined,
|
|
1079
|
+
resource: any,
|
|
1080
|
+
): ResourceRef | null {
|
|
1081
|
+
if (kindToPlural(kind).toLowerCase() !== 'pods') return null
|
|
1082
|
+
|
|
1083
|
+
if (relationships?.deployment) return relationships.deployment
|
|
1084
|
+
|
|
1085
|
+
const managedWorkload = relationships?.managedBy?.find((ref) => isPodOwnershipWorkloadRef(ref))
|
|
1086
|
+
if (managedWorkload) return managedWorkload
|
|
1087
|
+
|
|
1088
|
+
if (relationships?.owner && isPodOwnershipWorkloadRef(relationships.owner))
|
|
1089
|
+
return relationships.owner
|
|
1090
|
+
|
|
1091
|
+
return podControllerOwnerFromMetadata(namespace, resource)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function isPodOwnershipWorkloadRef(ref: ResourceRef): boolean {
|
|
1095
|
+
return POD_OWNERSHIP_WORKLOAD_KINDS.has(kindToPlural(ref.kind).toLowerCase())
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function podControllerOwnerFromMetadata(namespace: string, resource: any): ResourceRef | null {
|
|
1099
|
+
const ownerRefs = resource?.metadata?.ownerReferences
|
|
1100
|
+
if (!Array.isArray(ownerRefs)) return null
|
|
1101
|
+
const owner = ownerRefs.find((ref) => ref?.controller === true) ?? null
|
|
1102
|
+
if (!owner?.kind || !owner?.name) return null
|
|
1103
|
+
if (
|
|
1104
|
+
!isPodOwnershipWorkloadRef({
|
|
1105
|
+
kind: owner.kind,
|
|
1106
|
+
namespace,
|
|
1107
|
+
name: owner.name,
|
|
1108
|
+
})
|
|
1109
|
+
)
|
|
1110
|
+
return null
|
|
1111
|
+
return {
|
|
1112
|
+
kind: owner.kind,
|
|
1113
|
+
namespace,
|
|
1114
|
+
name: owner.name,
|
|
1115
|
+
group: apiVersionToGroup(owner.apiVersion),
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function buildPodOwnershipContext(
|
|
1120
|
+
workload: ResourceRef | null,
|
|
1121
|
+
apps: AppRow[] | undefined,
|
|
1122
|
+
): ResourceOwnershipContext | undefined {
|
|
1123
|
+
if (!workload) return undefined
|
|
1124
|
+
const matches = (apps ?? []).filter((app) =>
|
|
1125
|
+
(app.workloads ?? []).some((candidate) => sameWorkload(candidate, workload)),
|
|
1126
|
+
)
|
|
1127
|
+
const app = matches.length === 1 ? matches[0] : null
|
|
1128
|
+
return {
|
|
1129
|
+
workload,
|
|
1130
|
+
application: app ? { key: app.key, name: app.name } : undefined,
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function sameWorkload(
|
|
1135
|
+
candidate: { kind: string; namespace: string; name: string },
|
|
1136
|
+
workload: ResourceRef,
|
|
1137
|
+
): boolean {
|
|
1138
|
+
return (
|
|
1139
|
+
kindToPlural(candidate.kind).toLowerCase() === kindToPlural(workload.kind).toLowerCase() &&
|
|
1140
|
+
candidate.namespace === workload.namespace &&
|
|
1141
|
+
candidate.name === workload.name
|
|
1142
|
+
)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function nativeHelmOwnerFromRelationships(
|
|
1146
|
+
relationships: Relationships | undefined,
|
|
1147
|
+
fallbackNamespace: string,
|
|
1148
|
+
): HelmOwnerRef | null {
|
|
628
1149
|
const ref = relationships?.managedBy?.[0]
|
|
629
1150
|
if (!ref || !isNativeHelmManager(ref)) return null
|
|
630
1151
|
return {
|
|
@@ -634,10 +1155,12 @@ function nativeHelmOwnerFromRelationships(relationships: Relationships | undefin
|
|
|
634
1155
|
}
|
|
635
1156
|
|
|
636
1157
|
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
637
|
-
return
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1158
|
+
return (
|
|
1159
|
+
kindToPlural(ref.kind) === kindToPlural(current.kind) &&
|
|
1160
|
+
ref.namespace === current.namespace &&
|
|
1161
|
+
ref.name === current.name &&
|
|
1162
|
+
(ref.group ?? '') === (current.group ?? '')
|
|
1163
|
+
)
|
|
641
1164
|
}
|
|
642
1165
|
|
|
643
1166
|
function isNativeHelmManager(ref: ResourceRef): boolean {
|
|
@@ -650,8 +1173,14 @@ function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any):
|
|
|
650
1173
|
const annotations = resource.metadata?.annotations ?? {}
|
|
651
1174
|
|
|
652
1175
|
if (owner.tool === 'fluxcd') {
|
|
653
|
-
const nameKey =
|
|
654
|
-
|
|
1176
|
+
const nameKey =
|
|
1177
|
+
owner.kind === 'helmreleases'
|
|
1178
|
+
? 'helm.toolkit.fluxcd.io/name'
|
|
1179
|
+
: 'kustomize.toolkit.fluxcd.io/name'
|
|
1180
|
+
const nsKey =
|
|
1181
|
+
owner.kind === 'helmreleases'
|
|
1182
|
+
? 'helm.toolkit.fluxcd.io/namespace'
|
|
1183
|
+
: 'kustomize.toolkit.fluxcd.io/namespace'
|
|
655
1184
|
if (labels[nameKey] || labels[nsKey]) {
|
|
656
1185
|
return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
|
|
657
1186
|
}
|
|
@@ -699,7 +1228,14 @@ function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
|
699
1228
|
// LOGS TAB — platform-specific (uses data-fetching hooks)
|
|
700
1229
|
// ============================================================================
|
|
701
1230
|
|
|
702
|
-
const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet'])
|
|
1231
|
+
const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'Workflow'])
|
|
1232
|
+
const SCHEDULED_LOG_KINDS = new Set([
|
|
1233
|
+
'CronJob',
|
|
1234
|
+
'CronWorkflow',
|
|
1235
|
+
'WorkflowTemplate',
|
|
1236
|
+
'ClusterWorkflowTemplate',
|
|
1237
|
+
'ScaledJob',
|
|
1238
|
+
])
|
|
703
1239
|
|
|
704
1240
|
function LogsTabContent({
|
|
705
1241
|
kind,
|
|
@@ -712,6 +1248,8 @@ function LogsTabContent({
|
|
|
712
1248
|
onSelectPod,
|
|
713
1249
|
initialContainer,
|
|
714
1250
|
onConsumeInitialContainer,
|
|
1251
|
+
selectedRunKey,
|
|
1252
|
+
onSelectRun,
|
|
715
1253
|
}: {
|
|
716
1254
|
kind: string
|
|
717
1255
|
apiKind: string
|
|
@@ -723,19 +1261,48 @@ function LogsTabContent({
|
|
|
723
1261
|
onSelectPod: (name: string | null) => void
|
|
724
1262
|
initialContainer: string | null
|
|
725
1263
|
onConsumeInitialContainer: () => void
|
|
1264
|
+
selectedRunKey: string
|
|
1265
|
+
onSelectRun: (runKey: string) => void
|
|
726
1266
|
}) {
|
|
727
|
-
|
|
1267
|
+
if (SCHEDULED_LOG_KINDS.has(kind)) {
|
|
1268
|
+
return (
|
|
1269
|
+
<div className="h-full">
|
|
1270
|
+
<ScheduledWorkloadLogsViewer
|
|
1271
|
+
kind={apiKind}
|
|
1272
|
+
namespace={namespace}
|
|
1273
|
+
name={name}
|
|
1274
|
+
selectedRunKey={selectedRunKey}
|
|
1275
|
+
onSelectRun={onSelectRun}
|
|
1276
|
+
/>
|
|
1277
|
+
</div>
|
|
1278
|
+
)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Workload kinds with stable pod selectors use the aggregated workload logs viewer
|
|
728
1282
|
if (WORKLOAD_LOG_KINDS.has(kind)) {
|
|
729
1283
|
return (
|
|
730
1284
|
<div className="h-full">
|
|
731
|
-
<WorkloadLogsViewer
|
|
1285
|
+
<WorkloadLogsViewer
|
|
1286
|
+
kind={apiKind}
|
|
1287
|
+
namespace={namespace}
|
|
1288
|
+
name={name}
|
|
1289
|
+
autoStream={shouldAutoStreamWorkloadLogs(kind, resource)}
|
|
1290
|
+
/>
|
|
732
1291
|
</div>
|
|
733
1292
|
)
|
|
734
1293
|
}
|
|
735
1294
|
|
|
736
1295
|
// Individual Pod — use LogsViewer with container list from resource data
|
|
737
1296
|
if (kind === 'Pod') {
|
|
738
|
-
return
|
|
1297
|
+
return (
|
|
1298
|
+
<PodLogsTab
|
|
1299
|
+
namespace={namespace}
|
|
1300
|
+
name={name}
|
|
1301
|
+
resource={resource}
|
|
1302
|
+
initialContainer={initialContainer}
|
|
1303
|
+
onConsumeInitialContainer={onConsumeInitialContainer}
|
|
1304
|
+
/>
|
|
1305
|
+
)
|
|
739
1306
|
}
|
|
740
1307
|
|
|
741
1308
|
// Other kinds with associated pods (Jobs, CronJobs, ReplicaSets, etc.) — pod selector + LogsViewer
|
|
@@ -750,7 +1317,24 @@ function LogsTabContent({
|
|
|
750
1317
|
)
|
|
751
1318
|
}
|
|
752
1319
|
|
|
753
|
-
function
|
|
1320
|
+
function shouldAutoStreamWorkloadLogs(kind: string, resource: any): boolean {
|
|
1321
|
+
if (kind === 'Job') {
|
|
1322
|
+
return (resource?.status?.active ?? 0) > 0
|
|
1323
|
+
}
|
|
1324
|
+
if (kind === 'Workflow') {
|
|
1325
|
+
const phase = resource?.status?.phase
|
|
1326
|
+
return phase === 'Running' || phase === 'Pending'
|
|
1327
|
+
}
|
|
1328
|
+
return true
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function PodLogsTab({
|
|
1332
|
+
namespace,
|
|
1333
|
+
name,
|
|
1334
|
+
resource,
|
|
1335
|
+
initialContainer,
|
|
1336
|
+
onConsumeInitialContainer,
|
|
1337
|
+
}: {
|
|
754
1338
|
namespace: string
|
|
755
1339
|
name: string
|
|
756
1340
|
resource: any
|
|
@@ -789,7 +1373,13 @@ function PodLogsTab({ namespace, name, resource, initialContainer, onConsumeInit
|
|
|
789
1373
|
)
|
|
790
1374
|
}
|
|
791
1375
|
|
|
792
|
-
function MultiPodLogsTab({
|
|
1376
|
+
function MultiPodLogsTab({
|
|
1377
|
+
pods,
|
|
1378
|
+
namespace,
|
|
1379
|
+
selectedPod,
|
|
1380
|
+
onSelectPod,
|
|
1381
|
+
initialContainer,
|
|
1382
|
+
}: {
|
|
793
1383
|
pods: ResourceRef[]
|
|
794
1384
|
namespace: string
|
|
795
1385
|
selectedPod: string | null
|
|
@@ -802,10 +1392,12 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
802
1392
|
}
|
|
803
1393
|
}, [pods, selectedPod, onSelectPod])
|
|
804
1394
|
|
|
805
|
-
const podNamespace = pods.find(p => p.name === selectedPod)?.namespace || namespace
|
|
1395
|
+
const podNamespace = pods.find((p) => p.name === selectedPod)?.namespace || namespace
|
|
806
1396
|
|
|
807
1397
|
// Fetch container list for the selected pod
|
|
808
|
-
const { data: logsData } = usePodLogs(podNamespace, selectedPod || '', {
|
|
1398
|
+
const { data: logsData } = usePodLogs(podNamespace, selectedPod || '', {
|
|
1399
|
+
tailLines: 1,
|
|
1400
|
+
})
|
|
809
1401
|
const containers = logsData?.containers || []
|
|
810
1402
|
|
|
811
1403
|
// A terminated pod (common for Job/CronJob children) has nothing to follow —
|
|
@@ -828,7 +1420,7 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
828
1420
|
<div className="h-full flex flex-col">
|
|
829
1421
|
{pods.length > 1 && (
|
|
830
1422
|
<div className="shrink-0 border-b border-theme-border bg-theme-surface/50 px-4 py-2 flex gap-2 overflow-x-auto">
|
|
831
|
-
{pods.map(pod => (
|
|
1423
|
+
{pods.map((pod) => (
|
|
832
1424
|
<button
|
|
833
1425
|
key={pod.name}
|
|
834
1426
|
onClick={() => onSelectPod(pod.name)}
|
|
@@ -836,7 +1428,7 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
836
1428
|
'px-3 py-1.5 text-sm rounded-lg whitespace-nowrap transition-colors',
|
|
837
1429
|
selectedPod === pod.name
|
|
838
1430
|
? 'bg-blue-500 text-theme-text-primary'
|
|
839
|
-
: 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
|
|
1431
|
+
: 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover',
|
|
840
1432
|
)}
|
|
841
1433
|
>
|
|
842
1434
|
{pod.name.length > 40 ? '...' + pod.name.slice(-37) : pod.name}
|
|
@@ -860,11 +1452,15 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
860
1452
|
)
|
|
861
1453
|
}
|
|
862
1454
|
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1455
|
+
function AuditOverviewSection({
|
|
1456
|
+
findings,
|
|
1457
|
+
onViewAll,
|
|
1458
|
+
}: {
|
|
1459
|
+
findings: AuditFinding[]
|
|
1460
|
+
onViewAll: () => void
|
|
1461
|
+
}) {
|
|
1462
|
+
if (findings.length === 0) return null
|
|
1463
|
+
return <AuditAlerts findings={findings} onViewAll={onViewAll} />
|
|
868
1464
|
}
|
|
869
1465
|
|
|
870
1466
|
// FluxSourceConsumersSection lists the reconcilers (Kustomization, HelmRelease)
|
|
@@ -884,7 +1480,15 @@ function AuditSection({ kind, namespace, name }: { kind: string; namespace: stri
|
|
|
884
1480
|
// …), since the hook has no `enabled` flag and can't be conditionally called
|
|
885
1481
|
// (Rules of Hooks). The hooks only need to run when the focused resource is
|
|
886
1482
|
// actually a Flux source CR.
|
|
887
|
-
function FluxSourceConsumersSection({
|
|
1483
|
+
function FluxSourceConsumersSection({
|
|
1484
|
+
kind,
|
|
1485
|
+
namespace,
|
|
1486
|
+
name,
|
|
1487
|
+
}: {
|
|
1488
|
+
kind: string
|
|
1489
|
+
namespace: string
|
|
1490
|
+
name: string
|
|
1491
|
+
}) {
|
|
888
1492
|
// The inner WorkloadView de-pluralizes the URL's plural form, which gives
|
|
889
1493
|
// "Gitrepository" (single-uppercase) rather than the wire-correct
|
|
890
1494
|
// "GitRepository" — so we match lowercase. spec.sourceRef.kind on consumers
|
|
@@ -894,38 +1498,72 @@ function FluxSourceConsumersSection({ kind, namespace, name }: { kind: string; n
|
|
|
894
1498
|
return <FluxSourceConsumersInner sourceKind={sourceKind} namespace={namespace} name={name} />
|
|
895
1499
|
}
|
|
896
1500
|
|
|
897
|
-
function FluxSourceConsumersInner({
|
|
1501
|
+
function FluxSourceConsumersInner({
|
|
1502
|
+
sourceKind,
|
|
1503
|
+
namespace,
|
|
1504
|
+
name,
|
|
1505
|
+
}: {
|
|
1506
|
+
sourceKind: string
|
|
1507
|
+
namespace: string
|
|
1508
|
+
name: string
|
|
1509
|
+
}) {
|
|
898
1510
|
const navigate = useNavigate()
|
|
899
|
-
const { data: kustomizations } = useResources<any>(
|
|
900
|
-
|
|
1511
|
+
const { data: kustomizations } = useResources<any>(
|
|
1512
|
+
'kustomizations',
|
|
1513
|
+
undefined,
|
|
1514
|
+
'kustomize.toolkit.fluxcd.io',
|
|
1515
|
+
)
|
|
1516
|
+
const { data: helmReleases } = useResources<any>(
|
|
1517
|
+
'helmreleases',
|
|
1518
|
+
undefined,
|
|
1519
|
+
'helm.toolkit.fluxcd.io',
|
|
1520
|
+
)
|
|
901
1521
|
|
|
902
|
-
const consumers: Array<{
|
|
1522
|
+
const consumers: Array<{
|
|
1523
|
+
kind: 'Kustomization' | 'HelmRelease'
|
|
1524
|
+
namespace: string
|
|
1525
|
+
name: string
|
|
1526
|
+
plural: string
|
|
1527
|
+
}> = []
|
|
903
1528
|
for (const k of kustomizations ?? []) {
|
|
904
1529
|
const ref = k?.spec?.sourceRef ?? {}
|
|
905
1530
|
const refNs = ref.namespace || k?.metadata?.namespace
|
|
906
1531
|
if (ref.kind === sourceKind && ref.name === name && refNs === namespace) {
|
|
907
|
-
consumers.push({
|
|
1532
|
+
consumers.push({
|
|
1533
|
+
kind: 'Kustomization',
|
|
1534
|
+
namespace: k.metadata.namespace,
|
|
1535
|
+
name: k.metadata.name,
|
|
1536
|
+
plural: 'kustomizations',
|
|
1537
|
+
})
|
|
908
1538
|
}
|
|
909
1539
|
}
|
|
910
1540
|
for (const h of helmReleases ?? []) {
|
|
911
1541
|
const ref = h?.spec?.chart?.spec?.sourceRef ?? {}
|
|
912
1542
|
const refNs = ref.namespace || h?.metadata?.namespace
|
|
913
1543
|
if (ref.kind === sourceKind && ref.name === name && refNs === namespace) {
|
|
914
|
-
consumers.push({
|
|
1544
|
+
consumers.push({
|
|
1545
|
+
kind: 'HelmRelease',
|
|
1546
|
+
namespace: h.metadata.namespace,
|
|
1547
|
+
name: h.metadata.name,
|
|
1548
|
+
plural: 'helmreleases',
|
|
1549
|
+
})
|
|
915
1550
|
}
|
|
916
1551
|
}
|
|
917
1552
|
|
|
918
1553
|
if (consumers.length === 0) {
|
|
919
1554
|
return (
|
|
920
|
-
<
|
|
921
|
-
|
|
922
|
-
|
|
1555
|
+
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
1556
|
+
<h3 className="mb-2 text-sm font-semibold text-theme-text-primary">Consumed by</h3>
|
|
1557
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
1558
|
+
No Kustomization or HelmRelease references this source.
|
|
1559
|
+
</p>
|
|
1560
|
+
</section>
|
|
923
1561
|
)
|
|
924
1562
|
}
|
|
925
1563
|
|
|
926
1564
|
return (
|
|
927
|
-
<
|
|
928
|
-
<h3 className="mb-
|
|
1565
|
+
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
1566
|
+
<h3 className="mb-3 text-sm font-semibold text-theme-text-primary">
|
|
929
1567
|
Consumed by ({consumers.length})
|
|
930
1568
|
</h3>
|
|
931
1569
|
<div className="flex flex-wrap gap-1.5">
|
|
@@ -934,24 +1572,38 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
|
|
|
934
1572
|
key={`${c.kind}/${c.namespace}/${c.name}`}
|
|
935
1573
|
content={`${c.kind} ${c.namespace}/${c.name}`}
|
|
936
1574
|
>
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1575
|
+
<button
|
|
1576
|
+
onClick={() =>
|
|
1577
|
+
navigate(
|
|
1578
|
+
`/gitops/detail/${c.plural}/${encodeURIComponent(c.namespace)}/${encodeURIComponent(c.name)}`,
|
|
1579
|
+
)
|
|
1580
|
+
}
|
|
1581
|
+
className="inline-flex items-center gap-1.5 rounded border border-theme-border bg-theme-surface px-1.5 py-0.5 text-[11px] text-theme-text-secondary hover:border-skyhook-500/60 hover:text-skyhook-500 transition-colors"
|
|
1582
|
+
>
|
|
1583
|
+
<span className="text-theme-text-tertiary">
|
|
1584
|
+
{c.kind === 'HelmRelease' ? 'HR' : 'K'}
|
|
1585
|
+
</span>
|
|
1586
|
+
<span>
|
|
1587
|
+
{c.namespace}/{c.name}
|
|
1588
|
+
</span>
|
|
1589
|
+
</button>
|
|
944
1590
|
</Tooltip>
|
|
945
1591
|
))}
|
|
946
1592
|
</div>
|
|
947
|
-
</
|
|
1593
|
+
</section>
|
|
948
1594
|
)
|
|
949
1595
|
}
|
|
950
1596
|
|
|
951
1597
|
// Drawer mode: single chart + category tabs (compact for ~500px width).
|
|
952
1598
|
// Full-screen mode: multi-chart grid so CPU + Memory + Network can be
|
|
953
1599
|
// compared side-by-side without tab switching.
|
|
954
|
-
function MetricsTabContent({
|
|
1600
|
+
function MetricsTabContent({
|
|
1601
|
+
kind,
|
|
1602
|
+
namespace,
|
|
1603
|
+
name,
|
|
1604
|
+
resource,
|
|
1605
|
+
expanded,
|
|
1606
|
+
}: {
|
|
955
1607
|
kind: string
|
|
956
1608
|
namespace: string
|
|
957
1609
|
name: string
|
|
@@ -969,12 +1621,7 @@ function MetricsTabContent({ kind, namespace, name, resource, expanded }: {
|
|
|
969
1621
|
</div>
|
|
970
1622
|
)}
|
|
971
1623
|
<div className="flex-1 min-h-0">
|
|
972
|
-
<PrometheusChartsGrid
|
|
973
|
-
kind={kind}
|
|
974
|
-
namespace={namespace}
|
|
975
|
-
name={name}
|
|
976
|
-
resource={resource}
|
|
977
|
-
/>
|
|
1624
|
+
<PrometheusChartsGrid kind={kind} namespace={namespace} name={name} resource={resource} />
|
|
978
1625
|
</div>
|
|
979
1626
|
</div>
|
|
980
1627
|
)
|
|
@@ -982,17 +1629,15 @@ function MetricsTabContent({ kind, namespace, name, resource, expanded }: {
|
|
|
982
1629
|
|
|
983
1630
|
// Drawer fallback: single chart with tabs + restart lane below. The chart's
|
|
984
1631
|
// time-range selector is mirrored to the restart lane so they stay aligned.
|
|
985
|
-
return
|
|
986
|
-
<DrawerMetricsContent
|
|
987
|
-
kind={kind}
|
|
988
|
-
namespace={namespace}
|
|
989
|
-
name={name}
|
|
990
|
-
resource={resource}
|
|
991
|
-
/>
|
|
992
|
-
)
|
|
1632
|
+
return <DrawerMetricsContent kind={kind} namespace={namespace} name={name} resource={resource} />
|
|
993
1633
|
}
|
|
994
1634
|
|
|
995
|
-
function DrawerMetricsContent({
|
|
1635
|
+
function DrawerMetricsContent({
|
|
1636
|
+
kind,
|
|
1637
|
+
namespace,
|
|
1638
|
+
name,
|
|
1639
|
+
resource,
|
|
1640
|
+
}: {
|
|
996
1641
|
kind: string
|
|
997
1642
|
namespace: string
|
|
998
1643
|
name: string
|
|
@@ -1004,7 +1649,14 @@ function DrawerMetricsContent({ kind, namespace, name, resource }: {
|
|
|
1004
1649
|
return (
|
|
1005
1650
|
<div className="flex flex-col h-full">
|
|
1006
1651
|
<div className="flex-1 min-h-0">
|
|
1007
|
-
<PrometheusCharts
|
|
1652
|
+
<PrometheusCharts
|
|
1653
|
+
kind={kind}
|
|
1654
|
+
namespace={namespace}
|
|
1655
|
+
name={name}
|
|
1656
|
+
showEmptyState
|
|
1657
|
+
resource={resource}
|
|
1658
|
+
onTimeRangeChange={setChartRange}
|
|
1659
|
+
/>
|
|
1008
1660
|
</div>
|
|
1009
1661
|
{showRestartLane && (
|
|
1010
1662
|
<div className="px-4 pb-4">
|
|
@@ -1030,18 +1682,33 @@ const FLUX_SOURCE_KIND_BY_LOWER = new Map<string, string>([
|
|
|
1030
1682
|
// Read-only manifest view for an object in the workload's neighborhood (the
|
|
1031
1683
|
// YAML tab's object rail). Read-only by design — editing an arbitrary related
|
|
1032
1684
|
// object belongs on that resource's own page.
|
|
1033
|
-
function RelatedResourceYaml({
|
|
1034
|
-
|
|
1685
|
+
function RelatedResourceYaml({
|
|
1686
|
+
target,
|
|
1687
|
+
}: {
|
|
1688
|
+
target: { kind: string; namespace: string; name: string; group?: string }
|
|
1689
|
+
}) {
|
|
1690
|
+
const { data, isLoading, error } = useResource<any>(
|
|
1691
|
+
kindToPlural(target.kind),
|
|
1692
|
+
target.namespace,
|
|
1693
|
+
target.name,
|
|
1694
|
+
target.group,
|
|
1695
|
+
)
|
|
1035
1696
|
const [copied, setCopied] = useState(false)
|
|
1036
1697
|
const handleCopy = useCallback((text: string) => {
|
|
1037
1698
|
navigator.clipboard.writeText(text)
|
|
1038
1699
|
setCopied(true)
|
|
1039
1700
|
setTimeout(() => setCopied(false), 1500)
|
|
1040
1701
|
}, [])
|
|
1041
|
-
if (!data)
|
|
1702
|
+
if (!data)
|
|
1703
|
+
return <FetchResult loading={isLoading} error={error as Error | null} className="h-32" />
|
|
1042
1704
|
return (
|
|
1043
1705
|
<EditableYamlView
|
|
1044
|
-
resource={{
|
|
1706
|
+
resource={{
|
|
1707
|
+
kind: kindToPlural(target.kind),
|
|
1708
|
+
namespace: target.namespace,
|
|
1709
|
+
name: target.name,
|
|
1710
|
+
group: target.group,
|
|
1711
|
+
}}
|
|
1045
1712
|
data={data}
|
|
1046
1713
|
onCopy={handleCopy}
|
|
1047
1714
|
copied={copied}
|