@skyhook-io/radar-app 1.8.7 → 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/package.json +4 -4
- package/src/App.tsx +58 -50
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +1222 -234
- package/src/api/timelineSource.ts +4 -2
- package/src/components/applications/ApplicationsView.tsx +613 -219
- 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/diagnose/AISettings.tsx +7 -12
- 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/HelmCompareRoute.tsx +1 -2
- package/src/components/helm/ManifestDiffViewer.tsx +1 -31
- package/src/components/helm/ValuesDiffPreview.tsx +2 -3
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +319 -123
- 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 +1268 -318
- package/src/components/timeline/TimelineList.tsx +35 -8
- package/src/components/timeline/TimelineView.tsx +156 -26
- package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
- package/src/components/workload/WorkloadView.tsx +711 -328
- package/src/index.css +5 -1
- package/src/main.tsx +1 -1
|
@@ -23,15 +23,37 @@ import {
|
|
|
23
23
|
} from '@skyhook-io/k8s-ui'
|
|
24
24
|
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
25
25
|
import type { SelectedResource, ResourceRef, Relationships, ResolvedEnvFrom } from '../../types'
|
|
26
|
-
import { kindToPlural, pluralToKind, relatedResourcePath, type NavigateToResource } from '../../utils/navigation'
|
|
27
26
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
31
45
|
useWorkloadPods,
|
|
32
|
-
useFluxReconcile,
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
useFluxReconcile,
|
|
47
|
+
useFluxSyncWithSource,
|
|
48
|
+
useFluxSuspend,
|
|
49
|
+
useFluxResume,
|
|
50
|
+
useArgoSync,
|
|
51
|
+
useArgoRefresh,
|
|
52
|
+
useArgoSuspend,
|
|
53
|
+
useArgoResume,
|
|
54
|
+
useCordonNode,
|
|
55
|
+
useUncordonNode,
|
|
56
|
+
useDrainNode,
|
|
35
57
|
useCascadeDeletePreview,
|
|
36
58
|
useResourceEvents,
|
|
37
59
|
useResource,
|
|
@@ -42,7 +64,9 @@ import {
|
|
|
42
64
|
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
43
65
|
import { PrometheusChartsGrid } from '../resource/PrometheusChartsGrid'
|
|
44
66
|
import { RestartEventLane } from '../resource/RestartChart'
|
|
45
|
-
import { RightsizingStrip } from '../resource/RightsizingStrip'
|
|
67
|
+
import { RightsizingPanel, RightsizingStrip } from '../resource/RightsizingStrip'
|
|
68
|
+
import { WorkloadCostTab } from '../cost/WorkloadCostTab'
|
|
69
|
+
import { isOpenCostWorkloadKind } from '../cost/kinds'
|
|
46
70
|
import { useResourceAudit, useResourceIssues, useResources } from '../../api/client'
|
|
47
71
|
import { AuditAlerts, ResourceIssuesSection } from '@skyhook-io/k8s-ui'
|
|
48
72
|
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
@@ -50,10 +74,21 @@ import { ScheduledWorkloadLogsViewer } from '../logs/ScheduledWorkloadLogsViewer
|
|
|
50
74
|
import { LogsViewer } from '../logs/LogsViewer'
|
|
51
75
|
import { BatchExecutionFullscreen } from '../execution/BatchExecutionView'
|
|
52
76
|
import { workloadRunTimelineEvents } from '../execution/batch-timeline'
|
|
53
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
useCanUpdateSecrets,
|
|
79
|
+
useCanNodeWrite,
|
|
80
|
+
useNamespacedCapabilities,
|
|
81
|
+
useIsLocalDeployment,
|
|
82
|
+
} from '../../contexts/CapabilitiesContext'
|
|
54
83
|
import { useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal } from '../dock'
|
|
55
84
|
import { PortForwardButton, PortForwardInlineButton } from '../portforward/PortForwardButton'
|
|
56
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
CurlButton,
|
|
87
|
+
CurlPanel,
|
|
88
|
+
isHttpishPort,
|
|
89
|
+
defaultScheme,
|
|
90
|
+
defaultPathForPort,
|
|
91
|
+
} from '../curl/ServiceCurlButton'
|
|
57
92
|
import { useToast } from '../ui/Toast'
|
|
58
93
|
import { Tooltip } from '../ui/Tooltip'
|
|
59
94
|
import { PodRenderer } from '../resources/renderers/PodRenderer'
|
|
@@ -75,11 +110,23 @@ import { useDiagnoseCustomization } from '../../context/DiagnoseCustomization'
|
|
|
75
110
|
import { apiVersionToGroup } from '../../utils/navigation'
|
|
76
111
|
|
|
77
112
|
type TabType = WorkloadTabType
|
|
78
|
-
const BATCH_EXECUTION_KINDS = new Set([
|
|
113
|
+
const BATCH_EXECUTION_KINDS = new Set([
|
|
114
|
+
'Job',
|
|
115
|
+
'CronJob',
|
|
116
|
+
'Workflow',
|
|
117
|
+
'CronWorkflow',
|
|
118
|
+
'WorkflowTemplate',
|
|
119
|
+
'ClusterWorkflowTemplate',
|
|
120
|
+
'ScaledJob',
|
|
121
|
+
])
|
|
79
122
|
|
|
80
123
|
// Stable reference — web renderer wrappers inject platform hooks internally
|
|
81
124
|
const rendererOverrides: RendererOverrides = {
|
|
82
|
-
PodRenderer,
|
|
125
|
+
PodRenderer,
|
|
126
|
+
NodeRenderer,
|
|
127
|
+
ServiceRenderer,
|
|
128
|
+
WorkloadRenderer,
|
|
129
|
+
CompositeRenderer,
|
|
83
130
|
ServiceAccountRenderer,
|
|
84
131
|
RoleRenderer,
|
|
85
132
|
RoleBindingRenderer,
|
|
@@ -111,7 +158,11 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
111
158
|
// collapsed three-segment form (/workload/:kind/:name) for older links.
|
|
112
159
|
const parts = location.pathname.replace(/^\//, '').split('/')
|
|
113
160
|
const decode = (s: string): string => {
|
|
114
|
-
try {
|
|
161
|
+
try {
|
|
162
|
+
return decodeURIComponent(s)
|
|
163
|
+
} catch {
|
|
164
|
+
return s
|
|
165
|
+
}
|
|
115
166
|
}
|
|
116
167
|
const kind = decode(parts[1] ?? '')
|
|
117
168
|
let namespace: string
|
|
@@ -135,9 +186,12 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
135
186
|
}
|
|
136
187
|
}, [navigate])
|
|
137
188
|
|
|
138
|
-
const handleNavigate = useCallback(
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
const handleNavigate = useCallback(
|
|
190
|
+
(resource: SelectedResource) => {
|
|
191
|
+
navigate(relatedResourcePath(resource))
|
|
192
|
+
},
|
|
193
|
+
[navigate],
|
|
194
|
+
)
|
|
141
195
|
|
|
142
196
|
// Hooks must run unconditionally — the invalid-URL guard comes after them.
|
|
143
197
|
// Namespace is empty for cluster-scoped resources, so only kind + name are required.
|
|
@@ -206,7 +260,11 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
206
260
|
const resumeCronJobMutation = useResumeCronJob()
|
|
207
261
|
|
|
208
262
|
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind.toLowerCase())
|
|
209
|
-
const {
|
|
263
|
+
const {
|
|
264
|
+
data: revisionsList,
|
|
265
|
+
isLoading: revisionsLoading,
|
|
266
|
+
error: revisionsError,
|
|
267
|
+
} = useWorkloadRevisions(kind.toLowerCase(), namespace, name, isRollbackKind)
|
|
210
268
|
|
|
211
269
|
const fluxReconcileMutation = useFluxReconcile()
|
|
212
270
|
const fluxSyncWithSourceMutation = useFluxSyncWithSource()
|
|
@@ -218,7 +276,12 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
218
276
|
const argoSuspendMutation = useArgoSuspend()
|
|
219
277
|
const argoResumeMutation = useArgoResume()
|
|
220
278
|
|
|
221
|
-
const { data: cascadePreview, isLoading: cascadeLoading } = useCascadeDeletePreview(
|
|
279
|
+
const { data: cascadePreview, isLoading: cascadeLoading } = useCascadeDeletePreview(
|
|
280
|
+
kind,
|
|
281
|
+
namespace,
|
|
282
|
+
name,
|
|
283
|
+
true,
|
|
284
|
+
)
|
|
222
285
|
|
|
223
286
|
const canNodeWrite = useCanNodeWrite()
|
|
224
287
|
const cordonMutation = useCordonNode()
|
|
@@ -235,50 +298,80 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
235
298
|
onOpenLogs: openLogs,
|
|
236
299
|
onOpenWorkloadLogs: openWorkloadLogs,
|
|
237
300
|
onOpenNodeTerminal: openNodeTerminal,
|
|
238
|
-
onCopyCommand: (text: string, message: string, event: React.MouseEvent) =>
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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} />,
|
|
242
314
|
renderDiagnose,
|
|
243
|
-
onDelete: (
|
|
315
|
+
onDelete: (
|
|
316
|
+
params: Parameters<typeof deleteMutation.mutate>[0],
|
|
317
|
+
callbacks?: { onSuccess?: () => void },
|
|
318
|
+
) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
244
319
|
isDeleting: deleteMutation.isPending,
|
|
245
320
|
cascadeDependents: cascadePreview?.dependents,
|
|
246
321
|
cascadeLoading,
|
|
247
|
-
onRestart: (params: Parameters<typeof restartWorkloadMutation.mutate>[0]) =>
|
|
322
|
+
onRestart: (params: Parameters<typeof restartWorkloadMutation.mutate>[0]) =>
|
|
323
|
+
restartWorkloadMutation.mutate(params),
|
|
248
324
|
isRestarting: restartWorkloadMutation.isPending,
|
|
249
325
|
revisions: revisionsList,
|
|
250
326
|
revisionsLoading,
|
|
251
327
|
revisionsError: revisionsError ?? null,
|
|
252
|
-
onRollback: (
|
|
328
|
+
onRollback: (
|
|
329
|
+
params: Parameters<typeof rollbackMutation.mutate>[0],
|
|
330
|
+
callbacks?: { onSuccess?: () => void },
|
|
331
|
+
) => rollbackMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
253
332
|
isRollingBack: rollbackMutation.isPending,
|
|
254
|
-
onTriggerCronJob: (params: Parameters<typeof triggerCronJobMutation.mutate>[0]) =>
|
|
333
|
+
onTriggerCronJob: (params: Parameters<typeof triggerCronJobMutation.mutate>[0]) =>
|
|
334
|
+
triggerCronJobMutation.mutate(params),
|
|
255
335
|
isTriggeringCronJob: triggerCronJobMutation.isPending,
|
|
256
|
-
onSuspendCronJob: (params: Parameters<typeof suspendCronJobMutation.mutate>[0]) =>
|
|
336
|
+
onSuspendCronJob: (params: Parameters<typeof suspendCronJobMutation.mutate>[0]) =>
|
|
337
|
+
suspendCronJobMutation.mutate(params),
|
|
257
338
|
isSuspendingCronJob: suspendCronJobMutation.isPending,
|
|
258
|
-
onResumeCronJob: (params: Parameters<typeof resumeCronJobMutation.mutate>[0]) =>
|
|
339
|
+
onResumeCronJob: (params: Parameters<typeof resumeCronJobMutation.mutate>[0]) =>
|
|
340
|
+
resumeCronJobMutation.mutate(params),
|
|
259
341
|
isResumingCronJob: resumeCronJobMutation.isPending,
|
|
260
|
-
onFluxReconcile: (params: Parameters<typeof fluxReconcileMutation.mutate>[0]) =>
|
|
342
|
+
onFluxReconcile: (params: Parameters<typeof fluxReconcileMutation.mutate>[0]) =>
|
|
343
|
+
fluxReconcileMutation.mutate(params),
|
|
261
344
|
isFluxReconciling: fluxReconcileMutation.isPending,
|
|
262
|
-
onFluxSyncWithSource: (params: Parameters<typeof fluxSyncWithSourceMutation.mutate>[0]) =>
|
|
345
|
+
onFluxSyncWithSource: (params: Parameters<typeof fluxSyncWithSourceMutation.mutate>[0]) =>
|
|
346
|
+
fluxSyncWithSourceMutation.mutate(params),
|
|
263
347
|
isFluxSyncing: fluxSyncWithSourceMutation.isPending,
|
|
264
|
-
onFluxSuspend: (params: Parameters<typeof fluxSuspendMutation.mutate>[0]) =>
|
|
348
|
+
onFluxSuspend: (params: Parameters<typeof fluxSuspendMutation.mutate>[0]) =>
|
|
349
|
+
fluxSuspendMutation.mutate(params),
|
|
265
350
|
isFluxSuspending: fluxSuspendMutation.isPending,
|
|
266
|
-
onFluxResume: (params: Parameters<typeof fluxResumeMutation.mutate>[0]) =>
|
|
351
|
+
onFluxResume: (params: Parameters<typeof fluxResumeMutation.mutate>[0]) =>
|
|
352
|
+
fluxResumeMutation.mutate(params),
|
|
267
353
|
isFluxResuming: fluxResumeMutation.isPending,
|
|
268
|
-
onArgoSync: (params: Parameters<typeof argoSyncMutation.mutate>[0]) =>
|
|
354
|
+
onArgoSync: (params: Parameters<typeof argoSyncMutation.mutate>[0]) =>
|
|
355
|
+
argoSyncMutation.mutate(params),
|
|
269
356
|
isArgoSyncing: argoSyncMutation.isPending,
|
|
270
|
-
onArgoRefresh: (params: Parameters<typeof argoRefreshMutation.mutate>[0]) =>
|
|
357
|
+
onArgoRefresh: (params: Parameters<typeof argoRefreshMutation.mutate>[0]) =>
|
|
358
|
+
argoRefreshMutation.mutate(params),
|
|
271
359
|
isArgoRefreshing: argoRefreshMutation.isPending,
|
|
272
|
-
onArgoSuspend: (params: Parameters<typeof argoSuspendMutation.mutate>[0]) =>
|
|
360
|
+
onArgoSuspend: (params: Parameters<typeof argoSuspendMutation.mutate>[0]) =>
|
|
361
|
+
argoSuspendMutation.mutate(params),
|
|
273
362
|
isArgoSuspending: argoSuspendMutation.isPending,
|
|
274
|
-
onArgoResume: (params: Parameters<typeof argoResumeMutation.mutate>[0]) =>
|
|
363
|
+
onArgoResume: (params: Parameters<typeof argoResumeMutation.mutate>[0]) =>
|
|
364
|
+
argoResumeMutation.mutate(params),
|
|
275
365
|
isArgoResuming: argoResumeMutation.isPending,
|
|
276
366
|
canNodeWrite,
|
|
277
|
-
onCordonNode: (params: Parameters<typeof cordonMutation.mutate>[0]) =>
|
|
367
|
+
onCordonNode: (params: Parameters<typeof cordonMutation.mutate>[0]) =>
|
|
368
|
+
cordonMutation.mutate(params),
|
|
278
369
|
isCordoningNode: cordonMutation.isPending,
|
|
279
|
-
onUncordonNode: (params: Parameters<typeof uncordonMutation.mutate>[0]) =>
|
|
370
|
+
onUncordonNode: (params: Parameters<typeof uncordonMutation.mutate>[0]) =>
|
|
371
|
+
uncordonMutation.mutate(params),
|
|
280
372
|
isUncordoningNode: uncordonMutation.isPending,
|
|
281
|
-
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
|
|
373
|
+
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
|
|
374
|
+
drainMutation.mutate(params),
|
|
282
375
|
isDrainingNode: drainMutation.isPending,
|
|
283
376
|
}
|
|
284
377
|
}
|
|
@@ -297,27 +390,36 @@ export function WorkloadView({
|
|
|
297
390
|
|
|
298
391
|
// Tab state from URL query param — migrate legacy tab names
|
|
299
392
|
const rawTab = searchParams.get('tab')
|
|
300
|
-
const migratedTab: TabType =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
params
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
)
|
|
313
412
|
|
|
314
413
|
const selectedRunKey = searchParams.get('run') ?? ''
|
|
315
|
-
const handleSelectedRunChange = useCallback(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
+
)
|
|
321
423
|
|
|
322
424
|
const batchKind = pluralToKind(apiKind)
|
|
323
425
|
const batchExecution = BATCH_EXECUTION_KINDS.has(batchKind)
|
|
@@ -331,13 +433,20 @@ export function WorkloadView({
|
|
|
331
433
|
)
|
|
332
434
|
|
|
333
435
|
// Fetch resource with relationships
|
|
334
|
-
const {
|
|
436
|
+
const {
|
|
437
|
+
data: resourceResponse,
|
|
438
|
+
isLoading: resourceLoading,
|
|
439
|
+
error: resourceError,
|
|
440
|
+
refetch: refetchResource,
|
|
441
|
+
} = useResourceWithRelationships<any>(apiKind, namespace, name, rest.group)
|
|
335
442
|
const resource = resourceResponse?.resource
|
|
336
443
|
const relationships = resourceResponse?.relationships
|
|
337
444
|
const refetchResourceAndRuns = useCallback(async () => {
|
|
338
445
|
await Promise.all([
|
|
339
446
|
refetchResource(),
|
|
340
|
-
queryClient.refetchQueries({
|
|
447
|
+
queryClient.refetchQueries({
|
|
448
|
+
queryKey: ['workload-runs', apiKind, namespace, name],
|
|
449
|
+
}),
|
|
341
450
|
])
|
|
342
451
|
}, [apiKind, name, namespace, queryClient, refetchResource])
|
|
343
452
|
const podWorkloadOwner = useMemo(
|
|
@@ -354,9 +463,18 @@ export function WorkloadView({
|
|
|
354
463
|
)
|
|
355
464
|
const certificateInfo = resourceResponse?.certificateInfo
|
|
356
465
|
const hpaDiagnosis = resourceResponse?.hpaDiagnosis
|
|
357
|
-
const relationshipGitopsOwner = useMemo(
|
|
466
|
+
const relationshipGitopsOwner = useMemo(
|
|
467
|
+
() => gitOpsOwnerFromRelationships(relationships),
|
|
468
|
+
[relationships],
|
|
469
|
+
)
|
|
358
470
|
const inheritedGitOpsLookupRef = useMemo(
|
|
359
|
-
() =>
|
|
471
|
+
() =>
|
|
472
|
+
findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, {
|
|
473
|
+
kind: apiKind,
|
|
474
|
+
namespace,
|
|
475
|
+
name,
|
|
476
|
+
group: rest.group,
|
|
477
|
+
}),
|
|
360
478
|
[relationships, relationshipGitopsOwner, apiKind, namespace, name, rest.group],
|
|
361
479
|
)
|
|
362
480
|
const inheritedGitOpsResponse = useResourceWithRelationships<any>(
|
|
@@ -370,19 +488,34 @@ export function WorkloadView({
|
|
|
370
488
|
[inheritedGitOpsResponse.data?.relationships],
|
|
371
489
|
)
|
|
372
490
|
const relationshipHelmOwner = useMemo(
|
|
373
|
-
() =>
|
|
491
|
+
() =>
|
|
492
|
+
nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
|
|
374
493
|
[relationships, resource?.metadata?.namespace, namespace],
|
|
375
494
|
)
|
|
376
495
|
const inheritedHelmOwner = useMemo(
|
|
377
|
-
() =>
|
|
378
|
-
|
|
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
|
+
],
|
|
379
506
|
)
|
|
380
507
|
const rawGitopsOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
|
|
381
|
-
const gitOpsSourceResource = relationshipGitopsOwner
|
|
508
|
+
const gitOpsSourceResource = relationshipGitopsOwner
|
|
509
|
+
? resource
|
|
510
|
+
: inheritedGitOpsResponse.data?.resource
|
|
382
511
|
const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
|
|
383
|
-
const helmSourceResource = relationshipHelmOwner
|
|
512
|
+
const helmSourceResource = relationshipHelmOwner
|
|
513
|
+
? resource
|
|
514
|
+
: inheritedGitOpsResponse.data?.resource
|
|
384
515
|
const shouldResolveArgoOwner = rawGitopsOwner?.tool === 'argocd' && !rawGitopsOwner.namespace
|
|
385
|
-
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
516
|
+
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
517
|
+
enabled: shouldResolveArgoOwner,
|
|
518
|
+
})
|
|
386
519
|
const gitopsOwner = useMemo(
|
|
387
520
|
() => resolveGitOpsOwner(rawGitopsOwner, argoApplications),
|
|
388
521
|
[rawGitopsOwner, argoApplications],
|
|
@@ -400,7 +533,9 @@ export function WorkloadView({
|
|
|
400
533
|
[gitopsOwner, gitopsOwnerQuery.data],
|
|
401
534
|
)
|
|
402
535
|
const gitOpsOwnerVerified = Boolean(gitopsOwner?.namespace && gitopsOwnerQuery.data)
|
|
403
|
-
const gitOpsOwnerPending = Boolean(
|
|
536
|
+
const gitOpsOwnerPending = Boolean(
|
|
537
|
+
gitopsOwner?.namespace && gitopsOwnerQuery.isLoading && !gitopsOwnerQuery.data,
|
|
538
|
+
)
|
|
404
539
|
const gitOpsOwnerSource = useMemo(
|
|
405
540
|
() => describeGitOpsOwnerSource(rawGitopsOwner, gitOpsSourceResource),
|
|
406
541
|
[rawGitopsOwner, gitOpsSourceResource],
|
|
@@ -413,17 +548,27 @@ export function WorkloadView({
|
|
|
413
548
|
// For pods: extract envFrom ConfigMap/Secret names and resolve their keys
|
|
414
549
|
const isPod = apiKind === 'pods'
|
|
415
550
|
const { envFromConfigMapNames, envFromSecretNames } = useMemo(() => {
|
|
416
|
-
if (!isPod || !resource)
|
|
551
|
+
if (!isPod || !resource)
|
|
552
|
+
return {
|
|
553
|
+
envFromConfigMapNames: [] as string[],
|
|
554
|
+
envFromSecretNames: [] as string[],
|
|
555
|
+
}
|
|
417
556
|
const cmNames = new Set<string>()
|
|
418
557
|
const secretNames = new Set<string>()
|
|
419
|
-
const containers = [
|
|
558
|
+
const containers = [
|
|
559
|
+
...(resource.spec?.containers || []),
|
|
560
|
+
...(resource.spec?.initContainers || []),
|
|
561
|
+
]
|
|
420
562
|
for (const c of containers) {
|
|
421
|
-
for (const ef of
|
|
563
|
+
for (const ef of c.envFrom || []) {
|
|
422
564
|
if (ef.configMapRef?.name) cmNames.add(ef.configMapRef.name)
|
|
423
565
|
if (ef.secretRef?.name) secretNames.add(ef.secretRef.name)
|
|
424
566
|
}
|
|
425
567
|
}
|
|
426
|
-
return {
|
|
568
|
+
return {
|
|
569
|
+
envFromConfigMapNames: Array.from(cmNames),
|
|
570
|
+
envFromSecretNames: Array.from(secretNames),
|
|
571
|
+
}
|
|
427
572
|
}, [isPod, resource])
|
|
428
573
|
|
|
429
574
|
const configMapQueries = useQueries({
|
|
@@ -445,28 +590,44 @@ export function WorkloadView({
|
|
|
445
590
|
})
|
|
446
591
|
|
|
447
592
|
const resolvedEnvFrom = useMemo(() => {
|
|
448
|
-
if (!isPod || (envFromConfigMapNames.length === 0 && envFromSecretNames.length === 0))
|
|
593
|
+
if (!isPod || (envFromConfigMapNames.length === 0 && envFromSecretNames.length === 0))
|
|
594
|
+
return undefined
|
|
449
595
|
const result: ResolvedEnvFrom = {}
|
|
450
596
|
envFromConfigMapNames.forEach((n, i) => {
|
|
451
597
|
// Single-resource endpoint returns { resource, relationships } wrapper
|
|
452
598
|
const cm = configMapQueries[i]?.data?.resource ?? configMapQueries[i]?.data
|
|
453
|
-
if (cm)
|
|
599
|
+
if (cm)
|
|
600
|
+
result[resolvedEnvFromKey('configmap', n)] = {
|
|
601
|
+
keys: Object.keys(cm.data || {}),
|
|
602
|
+
values: cm.data || {},
|
|
603
|
+
isSecret: false,
|
|
604
|
+
}
|
|
454
605
|
})
|
|
455
606
|
envFromSecretNames.forEach((n, i) => {
|
|
456
607
|
const secret = secretQueries[i]?.data?.resource ?? secretQueries[i]?.data
|
|
457
608
|
if (secret) {
|
|
458
609
|
const decodedValues: Record<string, string> = {}
|
|
459
610
|
for (const [k, v] of Object.entries(secret.data || {})) {
|
|
460
|
-
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,
|
|
461
621
|
}
|
|
462
|
-
result[resolvedEnvFromKey('secret', n)] = { keys: Object.keys(decodedValues), values: decodedValues, isSecret: true }
|
|
463
622
|
}
|
|
464
623
|
})
|
|
465
624
|
return Object.keys(result).length > 0 ? result : undefined
|
|
466
625
|
}, [isPod, envFromConfigMapNames, envFromSecretNames, configMapQueries, secretQueries])
|
|
467
626
|
|
|
468
627
|
// Fetch topology for hierarchy building (only when expanded)
|
|
469
|
-
const { data: topology } = useTopology([namespace], 'resources', {
|
|
628
|
+
const { data: topology } = useTopology([namespace], 'resources', {
|
|
629
|
+
enabled: expanded,
|
|
630
|
+
})
|
|
470
631
|
|
|
471
632
|
// Always fetched so Recent Events populates on drawer open; allEvents below is
|
|
472
633
|
// gated on expanded because it's namespace-wide and expensive.
|
|
@@ -494,54 +655,74 @@ export function WorkloadView({
|
|
|
494
655
|
const isLocalDeployment = useIsLocalDeployment()
|
|
495
656
|
const showServingPortForward = canPortForward || !isLocalDeployment
|
|
496
657
|
const showServingCurl = !isLocalDeployment
|
|
497
|
-
const [servingCurl, setServingCurl] = useState<{
|
|
658
|
+
const [servingCurl, setServingCurl] = useState<{
|
|
659
|
+
namespace: string
|
|
660
|
+
serviceName: string
|
|
661
|
+
port: number
|
|
662
|
+
closing: boolean
|
|
663
|
+
} | null>(null)
|
|
498
664
|
const closeServingCurl = useCallback(() => {
|
|
499
665
|
setServingCurl((p) => (p ? { ...p, closing: true } : null))
|
|
500
666
|
window.setTimeout(() => setServingCurl((p) => (p?.closing ? null : p)), 220)
|
|
501
667
|
}, [])
|
|
502
|
-
const renderServicePortAction = useCallback(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
+
)
|
|
545
726
|
const updateResource = useUpdateResource()
|
|
546
727
|
const baseActionsBarProps = useActionsBarProps(apiKind, namespace, name)
|
|
547
728
|
const desktopDownload = useDesktopDownload()
|
|
@@ -558,10 +739,14 @@ export function WorkloadView({
|
|
|
558
739
|
// would flip the query key when the manifest arrives, drop liveIssues, and flash
|
|
559
740
|
// the renderer banners. The backend canonicalizes a plural kind via discovery,
|
|
560
741
|
// so using the normalized API kind resolves direct links and app navigation alike.
|
|
561
|
-
const { data: liveIssues } = useResourceIssues(apiKind, rest.group, namespace, name)
|
|
742
|
+
const { data: liveIssues, isPending: issuesPending } = useResourceIssues(apiKind, rest.group, namespace, name)
|
|
562
743
|
const { data: auditFindings } = useResourceAudit(apiKind, namespace, name)
|
|
563
744
|
const hasOperationalIssues = Boolean(liveIssues?.length)
|
|
564
|
-
const {
|
|
745
|
+
const {
|
|
746
|
+
onCompareTo,
|
|
747
|
+
onCompareAcrossClusters,
|
|
748
|
+
picker: comparePicker,
|
|
749
|
+
} = useCompareLauncher({
|
|
565
750
|
kind: apiKind,
|
|
566
751
|
namespace,
|
|
567
752
|
name,
|
|
@@ -575,9 +760,12 @@ export function WorkloadView({
|
|
|
575
760
|
[baseActionsBarProps, onCompareTo, onCompareAcrossClusters],
|
|
576
761
|
)
|
|
577
762
|
|
|
578
|
-
const handleUpdateResource = useCallback(
|
|
579
|
-
|
|
580
|
-
|
|
763
|
+
const handleUpdateResource = useCallback(
|
|
764
|
+
async (params: { kind: string; namespace: string; name: string; yaml: string }) => {
|
|
765
|
+
await updateResource.mutateAsync(params)
|
|
766
|
+
},
|
|
767
|
+
[updateResource],
|
|
768
|
+
)
|
|
581
769
|
|
|
582
770
|
const navigateRouter = useNavigate()
|
|
583
771
|
const handleOpenGitOpsResource = useCallback(
|
|
@@ -585,7 +773,10 @@ export function WorkloadView({
|
|
|
585
773
|
const params = new URLSearchParams()
|
|
586
774
|
const namespaces = searchParams.get('namespaces')
|
|
587
775
|
if (namespaces) params.set('namespaces', namespaces)
|
|
588
|
-
navigateRouter({
|
|
776
|
+
navigateRouter({
|
|
777
|
+
pathname: gitOpsRouteForOwner(ref),
|
|
778
|
+
search: params.toString(),
|
|
779
|
+
})
|
|
589
780
|
},
|
|
590
781
|
[navigateRouter, searchParams],
|
|
591
782
|
)
|
|
@@ -606,7 +797,12 @@ export function WorkloadView({
|
|
|
606
797
|
const handleOpenApplication = useCallback(
|
|
607
798
|
(appKey: string) => {
|
|
608
799
|
const params = new URLSearchParams()
|
|
609
|
-
const namespaces = new Set(
|
|
800
|
+
const namespaces = new Set(
|
|
801
|
+
(searchParams.get('namespaces') ?? '')
|
|
802
|
+
.split(',')
|
|
803
|
+
.map((ns) => ns.trim())
|
|
804
|
+
.filter(Boolean),
|
|
805
|
+
)
|
|
610
806
|
if (ownershipContext?.application?.key === appKey && ownershipContext.workload.namespace) {
|
|
611
807
|
namespaces.add(ownershipContext.workload.namespace)
|
|
612
808
|
}
|
|
@@ -621,17 +817,16 @@ export function WorkloadView({
|
|
|
621
817
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
622
818
|
const [duplicateYaml, setDuplicateYaml] = useState('')
|
|
623
819
|
|
|
624
|
-
const handleDuplicate = useCallback(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
)
|
|
628
827
|
|
|
629
828
|
const supportsWorkloadPods = ['deployments', 'statefulsets', 'daemonsets'].includes(apiKind)
|
|
630
|
-
const workloadPodsQuery = useWorkloadPods(
|
|
631
|
-
supportsWorkloadPods ? apiKind : '',
|
|
632
|
-
namespace,
|
|
633
|
-
name,
|
|
634
|
-
)
|
|
829
|
+
const workloadPodsQuery = useWorkloadPods(supportsWorkloadPods ? apiKind : '', namespace, name)
|
|
635
830
|
const servingRefs = useMemo(() => collectServingRefs(relationships), [relationships])
|
|
636
831
|
const servingQueries = useQueries({
|
|
637
832
|
queries: servingRefs.map((ref) => {
|
|
@@ -642,144 +837,179 @@ export function WorkloadView({
|
|
|
642
837
|
const queryString = params.toString()
|
|
643
838
|
return {
|
|
644
839
|
queryKey: ['resource', pluralKind, ref.namespace, ref.name, ref.group],
|
|
645
|
-
queryFn: () =>
|
|
840
|
+
queryFn: () =>
|
|
841
|
+
fetchJSON<any>(
|
|
842
|
+
`/resources/${pluralKind}/${ns}/${ref.name}${queryString ? `?${queryString}` : ''}`,
|
|
843
|
+
),
|
|
646
844
|
enabled: expanded && Boolean(ref.kind && ref.name),
|
|
647
845
|
staleTime: 30000,
|
|
648
846
|
}
|
|
649
847
|
}),
|
|
650
848
|
})
|
|
651
849
|
const servingResources = useMemo<ServingResourceDetail[]>(
|
|
652
|
-
() =>
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
+
}),
|
|
662
861
|
[servingRefs, servingQueries],
|
|
663
862
|
)
|
|
664
863
|
|
|
665
864
|
return (
|
|
666
865
|
<>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<BatchExecutionFullscreen
|
|
713
|
-
kind={k}
|
|
714
|
-
apiKind={apiKind}
|
|
715
|
-
namespace={ns}
|
|
716
|
-
name={n}
|
|
717
|
-
resource={res}
|
|
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}
|
|
718
911
|
selectedRunKey={selectedRunKey}
|
|
719
|
-
canViewLogs={baseActionsBarProps.canViewLogs}
|
|
720
912
|
onSelectRun={handleSelectedRunChange}
|
|
721
|
-
onSwitchToLogs={() => handleTabChange('logs')}
|
|
722
|
-
onSwitchToTimeline={() => handleTabChange('timeline')}
|
|
723
|
-
onNavigateToResource={rest.onNavigateToResource}
|
|
724
913
|
/>
|
|
725
|
-
)
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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}
|
|
783
1013
|
</>
|
|
784
1014
|
)
|
|
785
1015
|
}
|
|
@@ -804,7 +1034,10 @@ function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
|
804
1034
|
})
|
|
805
1035
|
}
|
|
806
1036
|
|
|
807
|
-
function resolveGitOpsOwner(
|
|
1037
|
+
function resolveGitOpsOwner(
|
|
1038
|
+
owner: GitOpsOwnerRef | null,
|
|
1039
|
+
argoApplications: any[] | undefined,
|
|
1040
|
+
): GitOpsOwnerRef | null {
|
|
808
1041
|
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
809
1042
|
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
810
1043
|
if (matches.length !== 1) return owner
|
|
@@ -818,9 +1051,8 @@ function findInheritedGitOpsLookupRef(
|
|
|
818
1051
|
current: ResourceRef,
|
|
819
1052
|
): ResourceRef | null {
|
|
820
1053
|
if (directOwner) return null
|
|
821
|
-
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
822
|
-
!gitOpsOwnerFromRelationships({ managedBy: [ref] })
|
|
823
|
-
&& !isNativeHelmManager(ref)
|
|
1054
|
+
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
1055
|
+
(ref) => !gitOpsOwnerFromRelationships({ managedBy: [ref] }) && !isNativeHelmManager(ref),
|
|
824
1056
|
)
|
|
825
1057
|
const candidates = [
|
|
826
1058
|
relationships?.deployment,
|
|
@@ -831,9 +1063,21 @@ function findInheritedGitOpsLookupRef(
|
|
|
831
1063
|
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
832
1064
|
}
|
|
833
1065
|
|
|
834
|
-
const POD_OWNERSHIP_WORKLOAD_KINDS = new Set([
|
|
1066
|
+
const POD_OWNERSHIP_WORKLOAD_KINDS = new Set([
|
|
1067
|
+
'deployments',
|
|
1068
|
+
'statefulsets',
|
|
1069
|
+
'daemonsets',
|
|
1070
|
+
'jobs',
|
|
1071
|
+
'cronjobs',
|
|
1072
|
+
'rollouts',
|
|
1073
|
+
])
|
|
835
1074
|
|
|
836
|
-
function podWorkloadOwnerFromRelationships(
|
|
1075
|
+
function podWorkloadOwnerFromRelationships(
|
|
1076
|
+
kind: string,
|
|
1077
|
+
namespace: string,
|
|
1078
|
+
relationships: Relationships | undefined,
|
|
1079
|
+
resource: any,
|
|
1080
|
+
): ResourceRef | null {
|
|
837
1081
|
if (kindToPlural(kind).toLowerCase() !== 'pods') return null
|
|
838
1082
|
|
|
839
1083
|
if (relationships?.deployment) return relationships.deployment
|
|
@@ -841,7 +1085,8 @@ function podWorkloadOwnerFromRelationships(kind: string, namespace: string, rela
|
|
|
841
1085
|
const managedWorkload = relationships?.managedBy?.find((ref) => isPodOwnershipWorkloadRef(ref))
|
|
842
1086
|
if (managedWorkload) return managedWorkload
|
|
843
1087
|
|
|
844
|
-
if (relationships?.owner && isPodOwnershipWorkloadRef(relationships.owner))
|
|
1088
|
+
if (relationships?.owner && isPodOwnershipWorkloadRef(relationships.owner))
|
|
1089
|
+
return relationships.owner
|
|
845
1090
|
|
|
846
1091
|
return podControllerOwnerFromMetadata(namespace, resource)
|
|
847
1092
|
}
|
|
@@ -855,7 +1100,14 @@ function podControllerOwnerFromMetadata(namespace: string, resource: any): Resou
|
|
|
855
1100
|
if (!Array.isArray(ownerRefs)) return null
|
|
856
1101
|
const owner = ownerRefs.find((ref) => ref?.controller === true) ?? null
|
|
857
1102
|
if (!owner?.kind || !owner?.name) return null
|
|
858
|
-
if (
|
|
1103
|
+
if (
|
|
1104
|
+
!isPodOwnershipWorkloadRef({
|
|
1105
|
+
kind: owner.kind,
|
|
1106
|
+
namespace,
|
|
1107
|
+
name: owner.name,
|
|
1108
|
+
})
|
|
1109
|
+
)
|
|
1110
|
+
return null
|
|
859
1111
|
return {
|
|
860
1112
|
kind: owner.kind,
|
|
861
1113
|
namespace,
|
|
@@ -879,13 +1131,21 @@ function buildPodOwnershipContext(
|
|
|
879
1131
|
}
|
|
880
1132
|
}
|
|
881
1133
|
|
|
882
|
-
function sameWorkload(
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
+
)
|
|
886
1143
|
}
|
|
887
1144
|
|
|
888
|
-
function nativeHelmOwnerFromRelationships(
|
|
1145
|
+
function nativeHelmOwnerFromRelationships(
|
|
1146
|
+
relationships: Relationships | undefined,
|
|
1147
|
+
fallbackNamespace: string,
|
|
1148
|
+
): HelmOwnerRef | null {
|
|
889
1149
|
const ref = relationships?.managedBy?.[0]
|
|
890
1150
|
if (!ref || !isNativeHelmManager(ref)) return null
|
|
891
1151
|
return {
|
|
@@ -895,10 +1155,12 @@ function nativeHelmOwnerFromRelationships(relationships: Relationships | undefin
|
|
|
895
1155
|
}
|
|
896
1156
|
|
|
897
1157
|
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
898
|
-
return
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
+
)
|
|
902
1164
|
}
|
|
903
1165
|
|
|
904
1166
|
function isNativeHelmManager(ref: ResourceRef): boolean {
|
|
@@ -911,8 +1173,14 @@ function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any):
|
|
|
911
1173
|
const annotations = resource.metadata?.annotations ?? {}
|
|
912
1174
|
|
|
913
1175
|
if (owner.tool === 'fluxcd') {
|
|
914
|
-
const nameKey =
|
|
915
|
-
|
|
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'
|
|
916
1184
|
if (labels[nameKey] || labels[nsKey]) {
|
|
917
1185
|
return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
|
|
918
1186
|
}
|
|
@@ -961,7 +1229,13 @@ function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
|
961
1229
|
// ============================================================================
|
|
962
1230
|
|
|
963
1231
|
const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'Workflow'])
|
|
964
|
-
const SCHEDULED_LOG_KINDS = new Set([
|
|
1232
|
+
const SCHEDULED_LOG_KINDS = new Set([
|
|
1233
|
+
'CronJob',
|
|
1234
|
+
'CronWorkflow',
|
|
1235
|
+
'WorkflowTemplate',
|
|
1236
|
+
'ClusterWorkflowTemplate',
|
|
1237
|
+
'ScaledJob',
|
|
1238
|
+
])
|
|
965
1239
|
|
|
966
1240
|
function LogsTabContent({
|
|
967
1241
|
kind,
|
|
@@ -993,7 +1267,13 @@ function LogsTabContent({
|
|
|
993
1267
|
if (SCHEDULED_LOG_KINDS.has(kind)) {
|
|
994
1268
|
return (
|
|
995
1269
|
<div className="h-full">
|
|
996
|
-
<ScheduledWorkloadLogsViewer
|
|
1270
|
+
<ScheduledWorkloadLogsViewer
|
|
1271
|
+
kind={apiKind}
|
|
1272
|
+
namespace={namespace}
|
|
1273
|
+
name={name}
|
|
1274
|
+
selectedRunKey={selectedRunKey}
|
|
1275
|
+
onSelectRun={onSelectRun}
|
|
1276
|
+
/>
|
|
997
1277
|
</div>
|
|
998
1278
|
)
|
|
999
1279
|
}
|
|
@@ -1002,14 +1282,27 @@ function LogsTabContent({
|
|
|
1002
1282
|
if (WORKLOAD_LOG_KINDS.has(kind)) {
|
|
1003
1283
|
return (
|
|
1004
1284
|
<div className="h-full">
|
|
1005
|
-
<WorkloadLogsViewer
|
|
1285
|
+
<WorkloadLogsViewer
|
|
1286
|
+
kind={apiKind}
|
|
1287
|
+
namespace={namespace}
|
|
1288
|
+
name={name}
|
|
1289
|
+
autoStream={shouldAutoStreamWorkloadLogs(kind, resource)}
|
|
1290
|
+
/>
|
|
1006
1291
|
</div>
|
|
1007
1292
|
)
|
|
1008
1293
|
}
|
|
1009
1294
|
|
|
1010
1295
|
// Individual Pod — use LogsViewer with container list from resource data
|
|
1011
1296
|
if (kind === 'Pod') {
|
|
1012
|
-
return
|
|
1297
|
+
return (
|
|
1298
|
+
<PodLogsTab
|
|
1299
|
+
namespace={namespace}
|
|
1300
|
+
name={name}
|
|
1301
|
+
resource={resource}
|
|
1302
|
+
initialContainer={initialContainer}
|
|
1303
|
+
onConsumeInitialContainer={onConsumeInitialContainer}
|
|
1304
|
+
/>
|
|
1305
|
+
)
|
|
1013
1306
|
}
|
|
1014
1307
|
|
|
1015
1308
|
// Other kinds with associated pods (Jobs, CronJobs, ReplicaSets, etc.) — pod selector + LogsViewer
|
|
@@ -1035,7 +1328,13 @@ function shouldAutoStreamWorkloadLogs(kind: string, resource: any): boolean {
|
|
|
1035
1328
|
return true
|
|
1036
1329
|
}
|
|
1037
1330
|
|
|
1038
|
-
function PodLogsTab({
|
|
1331
|
+
function PodLogsTab({
|
|
1332
|
+
namespace,
|
|
1333
|
+
name,
|
|
1334
|
+
resource,
|
|
1335
|
+
initialContainer,
|
|
1336
|
+
onConsumeInitialContainer,
|
|
1337
|
+
}: {
|
|
1039
1338
|
namespace: string
|
|
1040
1339
|
name: string
|
|
1041
1340
|
resource: any
|
|
@@ -1074,7 +1373,13 @@ function PodLogsTab({ namespace, name, resource, initialContainer, onConsumeInit
|
|
|
1074
1373
|
)
|
|
1075
1374
|
}
|
|
1076
1375
|
|
|
1077
|
-
function MultiPodLogsTab({
|
|
1376
|
+
function MultiPodLogsTab({
|
|
1377
|
+
pods,
|
|
1378
|
+
namespace,
|
|
1379
|
+
selectedPod,
|
|
1380
|
+
onSelectPod,
|
|
1381
|
+
initialContainer,
|
|
1382
|
+
}: {
|
|
1078
1383
|
pods: ResourceRef[]
|
|
1079
1384
|
namespace: string
|
|
1080
1385
|
selectedPod: string | null
|
|
@@ -1087,10 +1392,12 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
1087
1392
|
}
|
|
1088
1393
|
}, [pods, selectedPod, onSelectPod])
|
|
1089
1394
|
|
|
1090
|
-
const podNamespace = pods.find(p => p.name === selectedPod)?.namespace || namespace
|
|
1395
|
+
const podNamespace = pods.find((p) => p.name === selectedPod)?.namespace || namespace
|
|
1091
1396
|
|
|
1092
1397
|
// Fetch container list for the selected pod
|
|
1093
|
-
const { data: logsData } = usePodLogs(podNamespace, selectedPod || '', {
|
|
1398
|
+
const { data: logsData } = usePodLogs(podNamespace, selectedPod || '', {
|
|
1399
|
+
tailLines: 1,
|
|
1400
|
+
})
|
|
1094
1401
|
const containers = logsData?.containers || []
|
|
1095
1402
|
|
|
1096
1403
|
// A terminated pod (common for Job/CronJob children) has nothing to follow —
|
|
@@ -1113,7 +1420,7 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
1113
1420
|
<div className="h-full flex flex-col">
|
|
1114
1421
|
{pods.length > 1 && (
|
|
1115
1422
|
<div className="shrink-0 border-b border-theme-border bg-theme-surface/50 px-4 py-2 flex gap-2 overflow-x-auto">
|
|
1116
|
-
{pods.map(pod => (
|
|
1423
|
+
{pods.map((pod) => (
|
|
1117
1424
|
<button
|
|
1118
1425
|
key={pod.name}
|
|
1119
1426
|
onClick={() => onSelectPod(pod.name)}
|
|
@@ -1121,7 +1428,7 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
1121
1428
|
'px-3 py-1.5 text-sm rounded-lg whitespace-nowrap transition-colors',
|
|
1122
1429
|
selectedPod === pod.name
|
|
1123
1430
|
? 'bg-blue-500 text-theme-text-primary'
|
|
1124
|
-
: 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
|
|
1431
|
+
: 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover',
|
|
1125
1432
|
)}
|
|
1126
1433
|
>
|
|
1127
1434
|
{pod.name.length > 40 ? '...' + pod.name.slice(-37) : pod.name}
|
|
@@ -1145,7 +1452,13 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
1145
1452
|
)
|
|
1146
1453
|
}
|
|
1147
1454
|
|
|
1148
|
-
function AuditOverviewSection({
|
|
1455
|
+
function AuditOverviewSection({
|
|
1456
|
+
findings,
|
|
1457
|
+
onViewAll,
|
|
1458
|
+
}: {
|
|
1459
|
+
findings: AuditFinding[]
|
|
1460
|
+
onViewAll: () => void
|
|
1461
|
+
}) {
|
|
1149
1462
|
if (findings.length === 0) return null
|
|
1150
1463
|
return <AuditAlerts findings={findings} onViewAll={onViewAll} />
|
|
1151
1464
|
}
|
|
@@ -1167,7 +1480,15 @@ function AuditOverviewSection({ findings, onViewAll }: { findings: AuditFinding[
|
|
|
1167
1480
|
// …), since the hook has no `enabled` flag and can't be conditionally called
|
|
1168
1481
|
// (Rules of Hooks). The hooks only need to run when the focused resource is
|
|
1169
1482
|
// actually a Flux source CR.
|
|
1170
|
-
function FluxSourceConsumersSection({
|
|
1483
|
+
function FluxSourceConsumersSection({
|
|
1484
|
+
kind,
|
|
1485
|
+
namespace,
|
|
1486
|
+
name,
|
|
1487
|
+
}: {
|
|
1488
|
+
kind: string
|
|
1489
|
+
namespace: string
|
|
1490
|
+
name: string
|
|
1491
|
+
}) {
|
|
1171
1492
|
// The inner WorkloadView de-pluralizes the URL's plural form, which gives
|
|
1172
1493
|
// "Gitrepository" (single-uppercase) rather than the wire-correct
|
|
1173
1494
|
// "GitRepository" — so we match lowercase. spec.sourceRef.kind on consumers
|
|
@@ -1177,24 +1498,55 @@ function FluxSourceConsumersSection({ kind, namespace, name }: { kind: string; n
|
|
|
1177
1498
|
return <FluxSourceConsumersInner sourceKind={sourceKind} namespace={namespace} name={name} />
|
|
1178
1499
|
}
|
|
1179
1500
|
|
|
1180
|
-
function FluxSourceConsumersInner({
|
|
1501
|
+
function FluxSourceConsumersInner({
|
|
1502
|
+
sourceKind,
|
|
1503
|
+
namespace,
|
|
1504
|
+
name,
|
|
1505
|
+
}: {
|
|
1506
|
+
sourceKind: string
|
|
1507
|
+
namespace: string
|
|
1508
|
+
name: string
|
|
1509
|
+
}) {
|
|
1181
1510
|
const navigate = useNavigate()
|
|
1182
|
-
const { data: kustomizations } = useResources<any>(
|
|
1183
|
-
|
|
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
|
+
)
|
|
1184
1521
|
|
|
1185
|
-
const consumers: Array<{
|
|
1522
|
+
const consumers: Array<{
|
|
1523
|
+
kind: 'Kustomization' | 'HelmRelease'
|
|
1524
|
+
namespace: string
|
|
1525
|
+
name: string
|
|
1526
|
+
plural: string
|
|
1527
|
+
}> = []
|
|
1186
1528
|
for (const k of kustomizations ?? []) {
|
|
1187
1529
|
const ref = k?.spec?.sourceRef ?? {}
|
|
1188
1530
|
const refNs = ref.namespace || k?.metadata?.namespace
|
|
1189
1531
|
if (ref.kind === sourceKind && ref.name === name && refNs === namespace) {
|
|
1190
|
-
consumers.push({
|
|
1532
|
+
consumers.push({
|
|
1533
|
+
kind: 'Kustomization',
|
|
1534
|
+
namespace: k.metadata.namespace,
|
|
1535
|
+
name: k.metadata.name,
|
|
1536
|
+
plural: 'kustomizations',
|
|
1537
|
+
})
|
|
1191
1538
|
}
|
|
1192
1539
|
}
|
|
1193
1540
|
for (const h of helmReleases ?? []) {
|
|
1194
1541
|
const ref = h?.spec?.chart?.spec?.sourceRef ?? {}
|
|
1195
1542
|
const refNs = ref.namespace || h?.metadata?.namespace
|
|
1196
1543
|
if (ref.kind === sourceKind && ref.name === name && refNs === namespace) {
|
|
1197
|
-
consumers.push({
|
|
1544
|
+
consumers.push({
|
|
1545
|
+
kind: 'HelmRelease',
|
|
1546
|
+
namespace: h.metadata.namespace,
|
|
1547
|
+
name: h.metadata.name,
|
|
1548
|
+
plural: 'helmreleases',
|
|
1549
|
+
})
|
|
1198
1550
|
}
|
|
1199
1551
|
}
|
|
1200
1552
|
|
|
@@ -1202,7 +1554,9 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
|
|
|
1202
1554
|
return (
|
|
1203
1555
|
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
1204
1556
|
<h3 className="mb-2 text-sm font-semibold text-theme-text-primary">Consumed by</h3>
|
|
1205
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
1557
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
1558
|
+
No Kustomization or HelmRelease references this source.
|
|
1559
|
+
</p>
|
|
1206
1560
|
</section>
|
|
1207
1561
|
)
|
|
1208
1562
|
}
|
|
@@ -1218,13 +1572,21 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
|
|
|
1218
1572
|
key={`${c.kind}/${c.namespace}/${c.name}`}
|
|
1219
1573
|
content={`${c.kind} ${c.namespace}/${c.name}`}
|
|
1220
1574
|
>
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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>
|
|
1228
1590
|
</Tooltip>
|
|
1229
1591
|
))}
|
|
1230
1592
|
</div>
|
|
@@ -1235,7 +1597,13 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
|
|
|
1235
1597
|
// Drawer mode: single chart + category tabs (compact for ~500px width).
|
|
1236
1598
|
// Full-screen mode: multi-chart grid so CPU + Memory + Network can be
|
|
1237
1599
|
// compared side-by-side without tab switching.
|
|
1238
|
-
function MetricsTabContent({
|
|
1600
|
+
function MetricsTabContent({
|
|
1601
|
+
kind,
|
|
1602
|
+
namespace,
|
|
1603
|
+
name,
|
|
1604
|
+
resource,
|
|
1605
|
+
expanded,
|
|
1606
|
+
}: {
|
|
1239
1607
|
kind: string
|
|
1240
1608
|
namespace: string
|
|
1241
1609
|
name: string
|
|
@@ -1253,12 +1621,7 @@ function MetricsTabContent({ kind, namespace, name, resource, expanded }: {
|
|
|
1253
1621
|
</div>
|
|
1254
1622
|
)}
|
|
1255
1623
|
<div className="flex-1 min-h-0">
|
|
1256
|
-
<PrometheusChartsGrid
|
|
1257
|
-
kind={kind}
|
|
1258
|
-
namespace={namespace}
|
|
1259
|
-
name={name}
|
|
1260
|
-
resource={resource}
|
|
1261
|
-
/>
|
|
1624
|
+
<PrometheusChartsGrid kind={kind} namespace={namespace} name={name} resource={resource} />
|
|
1262
1625
|
</div>
|
|
1263
1626
|
</div>
|
|
1264
1627
|
)
|
|
@@ -1266,17 +1629,15 @@ function MetricsTabContent({ kind, namespace, name, resource, expanded }: {
|
|
|
1266
1629
|
|
|
1267
1630
|
// Drawer fallback: single chart with tabs + restart lane below. The chart's
|
|
1268
1631
|
// time-range selector is mirrored to the restart lane so they stay aligned.
|
|
1269
|
-
return
|
|
1270
|
-
<DrawerMetricsContent
|
|
1271
|
-
kind={kind}
|
|
1272
|
-
namespace={namespace}
|
|
1273
|
-
name={name}
|
|
1274
|
-
resource={resource}
|
|
1275
|
-
/>
|
|
1276
|
-
)
|
|
1632
|
+
return <DrawerMetricsContent kind={kind} namespace={namespace} name={name} resource={resource} />
|
|
1277
1633
|
}
|
|
1278
1634
|
|
|
1279
|
-
function DrawerMetricsContent({
|
|
1635
|
+
function DrawerMetricsContent({
|
|
1636
|
+
kind,
|
|
1637
|
+
namespace,
|
|
1638
|
+
name,
|
|
1639
|
+
resource,
|
|
1640
|
+
}: {
|
|
1280
1641
|
kind: string
|
|
1281
1642
|
namespace: string
|
|
1282
1643
|
name: string
|
|
@@ -1288,7 +1649,14 @@ function DrawerMetricsContent({ kind, namespace, name, resource }: {
|
|
|
1288
1649
|
return (
|
|
1289
1650
|
<div className="flex flex-col h-full">
|
|
1290
1651
|
<div className="flex-1 min-h-0">
|
|
1291
|
-
<PrometheusCharts
|
|
1652
|
+
<PrometheusCharts
|
|
1653
|
+
kind={kind}
|
|
1654
|
+
namespace={namespace}
|
|
1655
|
+
name={name}
|
|
1656
|
+
showEmptyState
|
|
1657
|
+
resource={resource}
|
|
1658
|
+
onTimeRangeChange={setChartRange}
|
|
1659
|
+
/>
|
|
1292
1660
|
</div>
|
|
1293
1661
|
{showRestartLane && (
|
|
1294
1662
|
<div className="px-4 pb-4">
|
|
@@ -1314,18 +1682,33 @@ const FLUX_SOURCE_KIND_BY_LOWER = new Map<string, string>([
|
|
|
1314
1682
|
// Read-only manifest view for an object in the workload's neighborhood (the
|
|
1315
1683
|
// YAML tab's object rail). Read-only by design — editing an arbitrary related
|
|
1316
1684
|
// object belongs on that resource's own page.
|
|
1317
|
-
function RelatedResourceYaml({
|
|
1318
|
-
|
|
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
|
+
)
|
|
1319
1696
|
const [copied, setCopied] = useState(false)
|
|
1320
1697
|
const handleCopy = useCallback((text: string) => {
|
|
1321
1698
|
navigator.clipboard.writeText(text)
|
|
1322
1699
|
setCopied(true)
|
|
1323
1700
|
setTimeout(() => setCopied(false), 1500)
|
|
1324
1701
|
}, [])
|
|
1325
|
-
if (!data)
|
|
1702
|
+
if (!data)
|
|
1703
|
+
return <FetchResult loading={isLoading} error={error as Error | null} className="h-32" />
|
|
1326
1704
|
return (
|
|
1327
1705
|
<EditableYamlView
|
|
1328
|
-
resource={{
|
|
1706
|
+
resource={{
|
|
1707
|
+
kind: kindToPlural(target.kind),
|
|
1708
|
+
namespace: target.namespace,
|
|
1709
|
+
name: target.name,
|
|
1710
|
+
group: target.group,
|
|
1711
|
+
}}
|
|
1329
1712
|
data={data}
|
|
1330
1713
|
onCopy={handleCopy}
|
|
1331
1714
|
copied={copied}
|