@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.
Files changed (48) hide show
  1. package/package.json +4 -4
  2. package/src/App.tsx +58 -50
  3. package/src/api/client.argoResourceSync.test.ts +69 -0
  4. package/src/api/client.rightsizing.test.ts +32 -0
  5. package/src/api/client.ts +1222 -234
  6. package/src/api/timelineSource.ts +4 -2
  7. package/src/components/applications/ApplicationsView.tsx +613 -219
  8. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  9. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  10. package/src/components/cost/CostTrendChart.tsx +103 -72
  11. package/src/components/cost/CostView.test.ts +12 -0
  12. package/src/components/cost/CostView.tsx +494 -229
  13. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  14. package/src/components/cost/CostViewTabs.tsx +40 -0
  15. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  16. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  17. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  18. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  19. package/src/components/cost/cloud-console.test.ts +39 -0
  20. package/src/components/cost/cloud-console.ts +81 -0
  21. package/src/components/cost/errors.ts +8 -0
  22. package/src/components/cost/format.test.ts +27 -0
  23. package/src/components/cost/format.ts +46 -0
  24. package/src/components/cost/kinds.ts +5 -0
  25. package/src/components/diagnose/AISettings.tsx +7 -12
  26. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  27. package/src/components/gitops/GitOpsView.tsx +81 -14
  28. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  29. package/src/components/helm/HelmCompareRoute.tsx +1 -2
  30. package/src/components/helm/ManifestDiffViewer.tsx +1 -31
  31. package/src/components/helm/ValuesDiffPreview.tsx +2 -3
  32. package/src/components/home/CostCard.tsx +21 -36
  33. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  34. package/src/components/resource/RightsizingStrip.tsx +319 -123
  35. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  36. package/src/components/rightsizing/copy.test.ts +56 -0
  37. package/src/components/rightsizing/model.test.ts +227 -0
  38. package/src/components/rightsizing/model.ts +158 -0
  39. package/src/components/rightsizing/presentation.test.ts +104 -0
  40. package/src/components/rightsizing/presentation.ts +94 -0
  41. package/src/components/settings/MyPermissionsDialog.tsx +66 -116
  42. package/src/components/settings/SettingsDialog.tsx +1268 -318
  43. package/src/components/timeline/TimelineList.tsx +35 -8
  44. package/src/components/timeline/TimelineView.tsx +156 -26
  45. package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
  46. package/src/components/workload/WorkloadView.tsx +711 -328
  47. package/src/index.css +5 -1
  48. 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
- useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
29
- useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
30
- useRestartWorkload, useWorkloadRevisions, useRollbackWorkload,
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, useFluxSyncWithSource, useFluxSuspend, useFluxResume,
33
- useArgoSync, useArgoRefresh, useArgoSuspend, useArgoResume,
34
- useCordonNode, useUncordonNode, useDrainNode,
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 { useCanUpdateSecrets, useCanNodeWrite, useNamespacedCapabilities, useIsLocalDeployment } from '../../contexts/CapabilitiesContext'
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 { CurlButton, CurlPanel, isHttpishPort, defaultScheme, defaultPathForPort } from '../curl/ServiceCurlButton'
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(['Job', 'CronJob', 'Workflow', 'CronWorkflow', 'WorkflowTemplate', 'ClusterWorkflowTemplate', 'ScaledJob'])
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, NodeRenderer, ServiceRenderer, WorkloadRenderer, CompositeRenderer,
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 { return decodeURIComponent(s) } catch { return s }
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((resource: SelectedResource) => {
139
- navigate(relatedResourcePath(resource))
140
- }, [navigate])
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 { data: revisionsList, isLoading: revisionsLoading, error: revisionsError } = useWorkloadRevisions(kind.toLowerCase(), namespace, name, isRollbackKind)
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(kind, namespace, name, true)
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) => showCopied(text, message, event),
239
- renderPortForward: ({ type, namespace: ns, name: n, className }: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => (
240
- <PortForwardButton type={type} namespace={ns} name={n} className={className} />
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: (params: Parameters<typeof deleteMutation.mutate>[0], callbacks?: { onSuccess?: () => void }) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
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]) => restartWorkloadMutation.mutate(params),
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: (params: Parameters<typeof rollbackMutation.mutate>[0], callbacks?: { onSuccess?: () => void }) => rollbackMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
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]) => triggerCronJobMutation.mutate(params),
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]) => suspendCronJobMutation.mutate(params),
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]) => resumeCronJobMutation.mutate(params),
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]) => fluxReconcileMutation.mutate(params),
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]) => fluxSyncWithSourceMutation.mutate(params),
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]) => fluxSuspendMutation.mutate(params),
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]) => fluxResumeMutation.mutate(params),
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]) => argoSyncMutation.mutate(params),
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]) => argoRefreshMutation.mutate(params),
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]) => argoSuspendMutation.mutate(params),
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]) => argoResumeMutation.mutate(params),
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]) => cordonMutation.mutate(params),
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]) => uncordonMutation.mutate(params),
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]) => drainMutation.mutate(params),
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 = rawTab === 'info' ? 'overview'
301
- : rawTab === 'events' ? 'timeline'
302
- : (rawTab as TabType) || 'overview'
303
-
304
- const handleTabChange = useCallback((tab: TabType, opts?: { replace?: boolean }) => {
305
- const params = new URLSearchParams(searchParams)
306
- if (tab === 'overview') {
307
- params.delete('tab')
308
- } else {
309
- params.set('tab', tab)
310
- }
311
- setSearchParams(params, { replace: opts?.replace ?? !pushTabHistory })
312
- }, [pushTabHistory, searchParams, setSearchParams])
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((runKey: string) => {
316
- const params = new URLSearchParams(searchParams)
317
- if (runKey) params.set('run', runKey)
318
- else params.delete('run')
319
- setSearchParams(params, { replace: true })
320
- }, [searchParams, setSearchParams])
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 { data: resourceResponse, isLoading: resourceLoading, error: resourceError, refetch: refetchResource } = useResourceWithRelationships<any>(apiKind, namespace, name, rest.group)
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({ queryKey: ['workload-runs', apiKind, namespace, name] }),
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(() => gitOpsOwnerFromRelationships(relationships), [relationships])
466
+ const relationshipGitopsOwner = useMemo(
467
+ () => gitOpsOwnerFromRelationships(relationships),
468
+ [relationships],
469
+ )
358
470
  const inheritedGitOpsLookupRef = useMemo(
359
- () => findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, { kind: apiKind, namespace, name, group: rest.group }),
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
- () => nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
491
+ () =>
492
+ nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace),
374
493
  [relationships, resource?.metadata?.namespace, namespace],
375
494
  )
376
495
  const inheritedHelmOwner = useMemo(
377
- () => nativeHelmOwnerFromRelationships(inheritedGitOpsResponse.data?.relationships, inheritedGitOpsResponse.data?.resource?.metadata?.namespace ?? namespace),
378
- [inheritedGitOpsResponse.data?.relationships, inheritedGitOpsResponse.data?.resource?.metadata?.namespace, namespace],
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 ? resource : inheritedGitOpsResponse.data?.resource
508
+ const gitOpsSourceResource = relationshipGitopsOwner
509
+ ? resource
510
+ : inheritedGitOpsResponse.data?.resource
382
511
  const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
383
- const helmSourceResource = relationshipHelmOwner ? resource : inheritedGitOpsResponse.data?.resource
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', { enabled: shouldResolveArgoOwner })
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(gitopsOwner?.namespace && gitopsOwnerQuery.isLoading && !gitopsOwnerQuery.data)
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) return { envFromConfigMapNames: [] as string[], envFromSecretNames: [] as string[] }
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 = [...(resource.spec?.containers || []), ...(resource.spec?.initContainers || [])]
558
+ const containers = [
559
+ ...(resource.spec?.containers || []),
560
+ ...(resource.spec?.initContainers || []),
561
+ ]
420
562
  for (const c of containers) {
421
- for (const ef of (c.envFrom || [])) {
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 { envFromConfigMapNames: Array.from(cmNames), envFromSecretNames: Array.from(secretNames) }
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)) return undefined
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) result[resolvedEnvFromKey('configmap', n)] = { keys: Object.keys(cm.data || {}), values: cm.data || {}, isSecret: false }
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 { decodedValues[k] = atob(v as string) } catch { decodedValues[k] = v as string }
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', { enabled: expanded })
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<{ namespace: string; serviceName: string; port: number; closing: boolean } | null>(null)
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((props: ServicePortRenderProps) => {
503
- const active = servingCurl?.namespace === props.namespace &&
504
- servingCurl?.serviceName === props.serviceName &&
505
- servingCurl?.port === props.port &&
506
- !servingCurl.closing
507
- return (
508
- <>
509
- {showServingCurl && isHttpishPort(props.port, props.name, props.appProtocol, props.protocol) && (
510
- <CurlButton
511
- active={active}
512
- onClick={() => {
513
- if (active) closeServingCurl()
514
- else setServingCurl({ namespace: props.namespace, serviceName: props.serviceName, port: props.port, closing: false })
515
- }}
516
- />
517
- )}
518
- {showServingPortForward && (
519
- <PortForwardInlineButton
520
- namespace={props.namespace}
521
- serviceName={props.serviceName}
522
- port={props.port}
523
- protocol={props.protocol}
524
- />
525
- )}
526
- </>
527
- )
528
- }, [closeServingCurl, servingCurl, showServingCurl, showServingPortForward])
529
- const renderServicePortPanel = useCallback((props: ServicePortRenderProps) => {
530
- const active = servingCurl?.namespace === props.namespace &&
531
- servingCurl?.serviceName === props.serviceName &&
532
- servingCurl?.port === props.port
533
- return active ? (
534
- <CurlPanel
535
- namespace={props.namespace}
536
- serviceName={props.serviceName}
537
- port={props.port}
538
- initialScheme={defaultScheme(props.port, props.name, props.appProtocol)}
539
- initialPath={defaultPathForPort(props.port, props.name, props.appProtocol)}
540
- open={!servingCurl.closing}
541
- onClose={closeServingCurl}
542
- />
543
- ) : null
544
- }, [closeServingCurl, servingCurl])
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 { onCompareTo, onCompareAcrossClusters, picker: comparePicker } = useCompareLauncher({
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(async (params: { kind: string; namespace: string; name: string; yaml: string }) => {
579
- await updateResource.mutateAsync(params)
580
- }, [updateResource])
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({ pathname: gitOpsRouteForOwner(ref), search: params.toString() })
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((searchParams.get('namespaces') ?? '').split(',').map((ns) => ns.trim()).filter(Boolean))
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((params: { kind: string; namespace: string; name: string; yaml: string }) => {
625
- setDuplicateYaml(cleanYamlForDuplicate(params.yaml))
626
- setDuplicateDialogOpen(true)
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: () => fetchJSON<any>(`/resources/${pluralKind}/${ns}/${ref.name}${queryString ? `?${queryString}` : ''}`),
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
- () => servingRefs.map((ref, index) => {
653
- const query = servingQueries[index]
654
- const data = query?.data?.resource ?? query?.data
655
- return {
656
- ref,
657
- resource: data,
658
- loading: query?.isLoading ?? false,
659
- error: (query?.error as Error | null) ?? null,
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
- <BaseWorkloadView
668
- kind={apiKind}
669
- namespace={namespace}
670
- name={name}
671
- expanded={expanded}
672
- {...rest}
673
- // Data
674
- resource={resource}
675
- relationships={relationships}
676
- ownershipContext={ownershipContext}
677
- onOpenApplication={handleOpenApplication}
678
- certificateInfo={certificateInfo}
679
- hpaDiagnosis={hpaDiagnosis}
680
- workloadPods={supportsWorkloadPods ? workloadPodsQuery.data?.pods : undefined}
681
- workloadPodsLoading={supportsWorkloadPods ? workloadPodsQuery.isLoading : false}
682
- workloadPodsError={supportsWorkloadPods ? (workloadPodsQuery.error as Error | null) : null}
683
- servingResources={servingResources}
684
- renderServicePortAction={renderServicePortAction}
685
- renderServicePortPanel={renderServicePortPanel}
686
- isLoading={resourceLoading}
687
- resourceError={resourceError}
688
- refetch={refetchResourceAndRuns}
689
- // Timeline
690
- allEvents={allEvents}
691
- relatedTimelineEvents={relatedTimelineEvents}
692
- eventsLoading={eventsLoading || (batchExecution && batchRunsQuery.isLoading)}
693
- topology={topology}
694
- resourceFocusedK8sEvents={resourceFocusedK8sEvents}
695
- resourceFocusedUpdates={resourceFocusedUpdates}
696
- resourceFocusedEventsLoading={resourceFocusedEventsLoading}
697
- resourceFocusedK8sError={resourceFocusedK8sError}
698
- resourceFocusedUpdatesError={resourceFocusedUpdatesError}
699
- // Capabilities
700
- canUpdateSecrets={canUpdateSecrets}
701
- // Mutations
702
- onUpdateResource={handleUpdateResource}
703
- isUpdatingResource={updateResource.isPending}
704
- updateResourceError={updateResource.error?.message ?? null}
705
- // Tab state (URL-synced)
706
- activeTab={migratedTab}
707
- onTabChange={handleTabChange}
708
- // Render props
709
- renderLogsTab={(props) => <LogsTabContent {...props} selectedRunKey={selectedRunKey} onSelectRun={handleSelectedRunChange} />}
710
- renderExpandedOverview={({ kind: k, apiKind, namespace: ns, name: n, resource: res }) =>
711
- BATCH_EXECUTION_KINDS.has(k) && res ? (
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
- ) : null
726
- }
727
- renderRelatedYaml={(ref) => <RelatedResourceYaml key={`${ref.kind}/${ref.namespace}/${ref.name}`} target={ref} />}
728
- renderMetricsTab={({ kind, namespace: ns, name: n }) => (
729
- <MetricsTabContent kind={kind} namespace={ns} name={n} resource={resource} expanded={expanded} />
730
- )}
731
- isMetricsAvailable={(kind, res) =>
732
- isPrometheusSupported(kind) && !(kind === 'Pod' && res?.status?.phase === 'Pending')
733
- }
734
- onDuplicate={handleDuplicate}
735
- onDownload={desktopDownload}
736
- actionsBarProps={actionsBarProps}
737
- rendererOverrides={rendererOverrides}
738
- resolvedEnvFrom={resolvedEnvFrom}
739
- renderOverviewExtra={({ kind: k, namespace: ns, name: n }) => (
740
- <>
741
- <FluxSourceConsumersSection kind={k} namespace={ns} name={n} />
742
- <AuditOverviewSection findings={auditFindings ?? []} onViewAll={() => navigateRouter('/checks')} />
743
- </>
744
- )}
745
- renderOverviewLead={() => (
746
- <ResourceIssuesSection
747
- issues={liveIssues}
748
- onResourceClick={
749
- rest.onNavigateToResource
750
- ? (ref) =>
751
- rest.onNavigateToResource?.({
752
- kind: kindToPlural(ref.kind),
753
- namespace: ref.namespace ?? '',
754
- name: ref.name,
755
- group: ref.group ?? '',
756
- })
757
- : undefined
758
- }
759
- />
760
- )}
761
- hasOperationalIssues={hasOperationalIssues}
762
- onOpenGitOpsResource={gitopsOwnerQuery.data ? handleOpenGitOpsResource : undefined}
763
- resolvedGitOpsOwner={gitopsOwner}
764
- gitOpsOwnerVerified={gitOpsOwnerVerified}
765
- gitOpsOwnerPending={gitOpsOwnerPending}
766
- gitOpsOwnerSource={gitOpsOwnerSource}
767
- gitOpsOwnerStatus={gitOpsOwnerStatus}
768
- helmOwner={helmOwner}
769
- helmOwnerSource={helmOwnerSource}
770
- onOpenHelmRelease={handleOpenHelmRelease}
771
- onNavigateGitOpsPath={handleNavigateGitOpsPath}
772
- />
773
- <CreateResourceDialog
774
- open={duplicateDialogOpen}
775
- onClose={() => setDuplicateDialogOpen(false)}
776
- initialYaml={duplicateYaml}
777
- title="Duplicate Resource"
778
- onCreated={(result) => {
779
- rest.onNavigateToResource?.({ kind: kindToPlural(result.kind), namespace: result.namespace, name: result.name, group: '' })
780
- }}
781
- />
782
- {comparePicker}
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(owner: GitOpsOwnerRef | null, argoApplications: any[] | undefined): GitOpsOwnerRef | null {
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((ref) =>
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(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs', 'rollouts'])
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(kind: string, namespace: string, relationships: Relationships | undefined, resource: any): ResourceRef | null {
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)) return 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 (!isPodOwnershipWorkloadRef({ kind: owner.kind, namespace, name: owner.name })) return null
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(candidate: { kind: string; namespace: string; name: string }, workload: ResourceRef): boolean {
883
- return kindToPlural(candidate.kind).toLowerCase() === kindToPlural(workload.kind).toLowerCase()
884
- && candidate.namespace === workload.namespace
885
- && candidate.name === workload.name
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(relationships: Relationships | undefined, fallbackNamespace: string): HelmOwnerRef | null {
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 kindToPlural(ref.kind) === kindToPlural(current.kind)
899
- && ref.namespace === current.namespace
900
- && ref.name === current.name
901
- && (ref.group ?? '') === (current.group ?? '')
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 = owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/name' : 'kustomize.toolkit.fluxcd.io/name'
915
- const nsKey = owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/namespace' : 'kustomize.toolkit.fluxcd.io/namespace'
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(['CronJob', 'CronWorkflow', 'WorkflowTemplate', 'ClusterWorkflowTemplate', 'ScaledJob'])
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 kind={apiKind} namespace={namespace} name={name} selectedRunKey={selectedRunKey} onSelectRun={onSelectRun} />
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 kind={apiKind} namespace={namespace} name={name} autoStream={shouldAutoStreamWorkloadLogs(kind, resource)} />
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 <PodLogsTab namespace={namespace} name={name} resource={resource} initialContainer={initialContainer} onConsumeInitialContainer={onConsumeInitialContainer} />
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({ namespace, name, resource, initialContainer, onConsumeInitialContainer }: {
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({ pods, namespace, selectedPod, onSelectPod, initialContainer }: {
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 || '', { tailLines: 1 })
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({ findings, onViewAll }: { findings: AuditFinding[]; onViewAll: () => void }) {
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({ kind, namespace, name }: { kind: string; namespace: string; name: string }) {
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({ sourceKind, namespace, name }: { sourceKind: string; namespace: string; name: string }) {
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>('kustomizations', undefined, 'kustomize.toolkit.fluxcd.io')
1183
- const { data: helmReleases } = useResources<any>('helmreleases', undefined, 'helm.toolkit.fluxcd.io')
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<{ kind: 'Kustomization' | 'HelmRelease'; namespace: string; name: string; plural: string }> = []
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({ kind: 'Kustomization', namespace: k.metadata.namespace, name: k.metadata.name, plural: 'kustomizations' })
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({ kind: 'HelmRelease', namespace: h.metadata.namespace, name: h.metadata.name, plural: 'helmreleases' })
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">No Kustomization or HelmRelease references this source.</p>
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
- <button
1222
- onClick={() => navigate(`/gitops/detail/${c.plural}/${encodeURIComponent(c.namespace)}/${encodeURIComponent(c.name)}`)}
1223
- 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"
1224
- >
1225
- <span className="text-theme-text-tertiary">{c.kind === 'HelmRelease' ? 'HR' : 'K'}</span>
1226
- <span>{c.namespace}/{c.name}</span>
1227
- </button>
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({ kind, namespace, name, resource, expanded }: {
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({ kind, namespace, name, resource }: {
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 kind={kind} namespace={namespace} name={name} showEmptyState resource={resource} onTimeRangeChange={setChartRange} />
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({ target }: { target: { kind: string; namespace: string; name: string; group?: string } }) {
1318
- const { data, isLoading, error } = useResource<any>(kindToPlural(target.kind), target.namespace, target.name, target.group)
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) return <FetchResult loading={isLoading} error={error as Error | null} className="h-32" />
1702
+ if (!data)
1703
+ return <FetchResult loading={isLoading} error={error as Error | null} className="h-32" />
1326
1704
  return (
1327
1705
  <EditableYamlView
1328
- resource={{ kind: kindToPlural(target.kind), namespace: target.namespace, name: target.name, group: target.group }}
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}