@skyhook-io/radar-app 1.8.6 → 1.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +11 -9
- package/src/App.tsx +221 -181
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.ts +262 -34
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +580 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +327 -26
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +121 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +15 -4
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.tsx +0 -3
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/settings/SettingsDialog.tsx +80 -23
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +51 -10
- package/src/components/timeline/TimelineView.tsx +701 -15
- package/src/components/timeline/TimelineView.urlparams.test.ts +294 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +319 -35
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +157 -0
- package/src/index.ts +12 -0
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo, useEffect, useCallback, useState } from 'react'
|
|
2
|
-
import { useQueries } from '@tanstack/react-query'
|
|
2
|
+
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { Terminal } from 'lucide-react'
|
|
@@ -12,23 +12,31 @@ import {
|
|
|
12
12
|
type GitOpsOwnerRef,
|
|
13
13
|
type GitOpsStatus,
|
|
14
14
|
type HelmOwnerRef,
|
|
15
|
+
type AppRow,
|
|
16
|
+
type ResourceOwnershipContext,
|
|
17
|
+
type ServingResourceDetail,
|
|
18
|
+
type AuditFinding,
|
|
15
19
|
gitOpsRouteForOwner,
|
|
16
20
|
gitOpsOwnerFromRelationships,
|
|
17
21
|
getGitOpsResourceStatus,
|
|
18
22
|
resolvedEnvFromKey,
|
|
19
23
|
} from '@skyhook-io/k8s-ui'
|
|
24
|
+
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
20
25
|
import type { SelectedResource, ResourceRef, Relationships, ResolvedEnvFrom } from '../../types'
|
|
21
|
-
import { kindToPlural,
|
|
26
|
+
import { kindToPlural, pluralToKind, relatedResourcePath, type NavigateToResource } from '../../utils/navigation'
|
|
22
27
|
import {
|
|
23
28
|
useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
|
|
24
29
|
useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
|
|
25
30
|
useRestartWorkload, useWorkloadRevisions, useRollbackWorkload,
|
|
31
|
+
useWorkloadPods,
|
|
26
32
|
useFluxReconcile, useFluxSyncWithSource, useFluxSuspend, useFluxResume,
|
|
27
33
|
useArgoSync, useArgoRefresh, useArgoSuspend, useArgoResume,
|
|
28
34
|
useCordonNode, useUncordonNode, useDrainNode,
|
|
29
35
|
useCascadeDeletePreview,
|
|
30
36
|
useResourceEvents,
|
|
31
37
|
useResource,
|
|
38
|
+
useWorkloadRuns,
|
|
39
|
+
useApplications,
|
|
32
40
|
fetchJSON,
|
|
33
41
|
} from '../../api/client'
|
|
34
42
|
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
@@ -38,10 +46,14 @@ import { RightsizingStrip } from '../resource/RightsizingStrip'
|
|
|
38
46
|
import { useResourceAudit, useResourceIssues, useResources } from '../../api/client'
|
|
39
47
|
import { AuditAlerts, ResourceIssuesSection } from '@skyhook-io/k8s-ui'
|
|
40
48
|
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
49
|
+
import { ScheduledWorkloadLogsViewer } from '../logs/ScheduledWorkloadLogsViewer'
|
|
41
50
|
import { LogsViewer } from '../logs/LogsViewer'
|
|
51
|
+
import { BatchExecutionFullscreen } from '../execution/BatchExecutionView'
|
|
52
|
+
import { workloadRunTimelineEvents } from '../execution/batch-timeline'
|
|
42
53
|
import { useCanUpdateSecrets, useCanNodeWrite, useNamespacedCapabilities, useIsLocalDeployment } from '../../contexts/CapabilitiesContext'
|
|
43
54
|
import { useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal } from '../dock'
|
|
44
|
-
import { PortForwardButton } from '../portforward/PortForwardButton'
|
|
55
|
+
import { PortForwardButton, PortForwardInlineButton } from '../portforward/PortForwardButton'
|
|
56
|
+
import { CurlButton, CurlPanel, isHttpishPort, defaultScheme, defaultPathForPort } from '../curl/ServiceCurlButton'
|
|
45
57
|
import { useToast } from '../ui/Toast'
|
|
46
58
|
import { Tooltip } from '../ui/Tooltip'
|
|
47
59
|
import { PodRenderer } from '../resources/renderers/PodRenderer'
|
|
@@ -59,9 +71,11 @@ import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
|
59
71
|
import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
|
|
60
72
|
import { useDesktopDownload } from '../../hooks/useDesktopDownload'
|
|
61
73
|
import { useCompareLauncher } from '../compare/useCompareLauncher'
|
|
74
|
+
import { useDiagnoseCustomization } from '../../context/DiagnoseCustomization'
|
|
62
75
|
import { apiVersionToGroup } from '../../utils/navigation'
|
|
63
76
|
|
|
64
77
|
type TabType = WorkloadTabType
|
|
78
|
+
const BATCH_EXECUTION_KINDS = new Set(['Job', 'CronJob', 'Workflow', 'CronWorkflow', 'WorkflowTemplate', 'ClusterWorkflowTemplate', 'ScaledJob'])
|
|
65
79
|
|
|
66
80
|
// Stable reference — web renderer wrappers inject platform hooks internally
|
|
67
81
|
const rendererOverrides: RendererOverrides = {
|
|
@@ -122,7 +136,7 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
122
136
|
}, [navigate])
|
|
123
137
|
|
|
124
138
|
const handleNavigate = useCallback((resource: SelectedResource) => {
|
|
125
|
-
navigate(
|
|
139
|
+
navigate(relatedResourcePath(resource))
|
|
126
140
|
}, [navigate])
|
|
127
141
|
|
|
128
142
|
// Hooks must run unconditionally — the invalid-URL guard comes after them.
|
|
@@ -169,6 +183,7 @@ interface WorkloadViewProps {
|
|
|
169
183
|
onCancelExpandIntent?: () => void
|
|
170
184
|
initialTab?: 'detail' | 'yaml'
|
|
171
185
|
group?: string
|
|
186
|
+
pushTabHistory?: boolean
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
@@ -210,6 +225,8 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
210
225
|
const uncordonMutation = useUncordonNode()
|
|
211
226
|
const drainMutation = useDrainNode()
|
|
212
227
|
|
|
228
|
+
const renderDiagnose = useDiagnoseCustomization()
|
|
229
|
+
|
|
213
230
|
return {
|
|
214
231
|
canExec,
|
|
215
232
|
canViewLogs,
|
|
@@ -222,6 +239,7 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
222
239
|
renderPortForward: ({ type, namespace: ns, name: n, className }: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => (
|
|
223
240
|
<PortForwardButton type={type} namespace={ns} name={n} className={className} />
|
|
224
241
|
),
|
|
242
|
+
renderDiagnose,
|
|
225
243
|
onDelete: (params: Parameters<typeof deleteMutation.mutate>[0], callbacks?: { onSuccess?: () => void }) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
226
244
|
isDeleting: deleteMutation.isPending,
|
|
227
245
|
cascadeDependents: cascadePreview?.dependents,
|
|
@@ -270,9 +288,12 @@ export function WorkloadView({
|
|
|
270
288
|
namespace,
|
|
271
289
|
name,
|
|
272
290
|
expanded = true,
|
|
291
|
+
pushTabHistory = false,
|
|
273
292
|
...rest
|
|
274
293
|
}: WorkloadViewProps) {
|
|
275
294
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
295
|
+
const apiKind = kindToPlural(kindProp)
|
|
296
|
+
const queryClient = useQueryClient()
|
|
276
297
|
|
|
277
298
|
// Tab state from URL query param — migrate legacy tab names
|
|
278
299
|
const rawTab = searchParams.get('tab')
|
|
@@ -280,26 +301,63 @@ export function WorkloadView({
|
|
|
280
301
|
: rawTab === 'events' ? 'timeline'
|
|
281
302
|
: (rawTab as TabType) || 'overview'
|
|
282
303
|
|
|
283
|
-
const handleTabChange = useCallback((tab: TabType) => {
|
|
304
|
+
const handleTabChange = useCallback((tab: TabType, opts?: { replace?: boolean }) => {
|
|
284
305
|
const params = new URLSearchParams(searchParams)
|
|
285
306
|
if (tab === 'overview') {
|
|
286
307
|
params.delete('tab')
|
|
287
308
|
} else {
|
|
288
309
|
params.set('tab', tab)
|
|
289
310
|
}
|
|
311
|
+
setSearchParams(params, { replace: opts?.replace ?? !pushTabHistory })
|
|
312
|
+
}, [pushTabHistory, searchParams, setSearchParams])
|
|
313
|
+
|
|
314
|
+
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')
|
|
290
319
|
setSearchParams(params, { replace: true })
|
|
291
320
|
}, [searchParams, setSearchParams])
|
|
292
321
|
|
|
322
|
+
const batchKind = pluralToKind(apiKind)
|
|
323
|
+
const batchExecution = BATCH_EXECUTION_KINDS.has(batchKind)
|
|
324
|
+
const batchRunsQuery = useWorkloadRuns(apiKind, namespace, name, expanded && batchExecution, {
|
|
325
|
+
refetchActive: true,
|
|
326
|
+
clusterScoped: batchKind === 'ClusterWorkflowTemplate',
|
|
327
|
+
})
|
|
328
|
+
const relatedTimelineEvents = useMemo(
|
|
329
|
+
() => workloadRunTimelineEvents(batchRunsQuery.data?.runs ?? []),
|
|
330
|
+
[batchRunsQuery.data?.runs],
|
|
331
|
+
)
|
|
332
|
+
|
|
293
333
|
// Fetch resource with relationships
|
|
294
|
-
const { data: resourceResponse, isLoading: resourceLoading, error: resourceError, refetch: refetchResource } = useResourceWithRelationships<any>(
|
|
334
|
+
const { data: resourceResponse, isLoading: resourceLoading, error: resourceError, refetch: refetchResource } = useResourceWithRelationships<any>(apiKind, namespace, name, rest.group)
|
|
295
335
|
const resource = resourceResponse?.resource
|
|
296
336
|
const relationships = resourceResponse?.relationships
|
|
337
|
+
const refetchResourceAndRuns = useCallback(async () => {
|
|
338
|
+
await Promise.all([
|
|
339
|
+
refetchResource(),
|
|
340
|
+
queryClient.refetchQueries({ queryKey: ['workload-runs', apiKind, namespace, name] }),
|
|
341
|
+
])
|
|
342
|
+
}, [apiKind, name, namespace, queryClient, refetchResource])
|
|
343
|
+
const podWorkloadOwner = useMemo(
|
|
344
|
+
() => podWorkloadOwnerFromRelationships(apiKind, namespace, relationships, resource),
|
|
345
|
+
[apiKind, namespace, relationships, resource],
|
|
346
|
+
)
|
|
347
|
+
const podOwnerAppsQuery = useApplications(
|
|
348
|
+
podWorkloadOwner?.namespace ? [podWorkloadOwner.namespace] : [],
|
|
349
|
+
{ enabled: Boolean(podWorkloadOwner?.namespace) },
|
|
350
|
+
)
|
|
351
|
+
const ownershipContext = useMemo(
|
|
352
|
+
() => buildPodOwnershipContext(podWorkloadOwner, podOwnerAppsQuery.data?.applications),
|
|
353
|
+
[podWorkloadOwner, podOwnerAppsQuery.data?.applications],
|
|
354
|
+
)
|
|
297
355
|
const certificateInfo = resourceResponse?.certificateInfo
|
|
298
356
|
const hpaDiagnosis = resourceResponse?.hpaDiagnosis
|
|
299
357
|
const relationshipGitopsOwner = useMemo(() => gitOpsOwnerFromRelationships(relationships), [relationships])
|
|
300
358
|
const inheritedGitOpsLookupRef = useMemo(
|
|
301
|
-
() => findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, { kind:
|
|
302
|
-
[relationships, relationshipGitopsOwner,
|
|
359
|
+
() => findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, { kind: apiKind, namespace, name, group: rest.group }),
|
|
360
|
+
[relationships, relationshipGitopsOwner, apiKind, namespace, name, rest.group],
|
|
303
361
|
)
|
|
304
362
|
const inheritedGitOpsResponse = useResourceWithRelationships<any>(
|
|
305
363
|
inheritedGitOpsLookupRef ? kindToPlural(inheritedGitOpsLookupRef.kind) : '',
|
|
@@ -353,7 +411,7 @@ export function WorkloadView({
|
|
|
353
411
|
)
|
|
354
412
|
|
|
355
413
|
// For pods: extract envFrom ConfigMap/Secret names and resolve their keys
|
|
356
|
-
const isPod =
|
|
414
|
+
const isPod = apiKind === 'pods'
|
|
357
415
|
const { envFromConfigMapNames, envFromSecretNames } = useMemo(() => {
|
|
358
416
|
if (!isPod || !resource) return { envFromConfigMapNames: [] as string[], envFromSecretNames: [] as string[] }
|
|
359
417
|
const cmNames = new Set<string>()
|
|
@@ -418,7 +476,7 @@ export function WorkloadView({
|
|
|
418
476
|
isLoading: resourceFocusedEventsLoading,
|
|
419
477
|
k8sError: resourceFocusedK8sError,
|
|
420
478
|
updatesError: resourceFocusedUpdatesError,
|
|
421
|
-
} = useResourceEvents(
|
|
479
|
+
} = useResourceEvents(apiKind, namespace, name)
|
|
422
480
|
|
|
423
481
|
// Fetch all events for this resource's namespace (only when expanded)
|
|
424
482
|
const { data: allEvents, isLoading: eventsLoading } = useChanges({
|
|
@@ -432,8 +490,60 @@ export function WorkloadView({
|
|
|
432
490
|
|
|
433
491
|
// RBAC
|
|
434
492
|
const canUpdateSecrets = useCanUpdateSecrets()
|
|
493
|
+
const { canPortForward } = useNamespacedCapabilities(namespace)
|
|
494
|
+
const isLocalDeployment = useIsLocalDeployment()
|
|
495
|
+
const showServingPortForward = canPortForward || !isLocalDeployment
|
|
496
|
+
const showServingCurl = !isLocalDeployment
|
|
497
|
+
const [servingCurl, setServingCurl] = useState<{ namespace: string; serviceName: string; port: number; closing: boolean } | null>(null)
|
|
498
|
+
const closeServingCurl = useCallback(() => {
|
|
499
|
+
setServingCurl((p) => (p ? { ...p, closing: true } : null))
|
|
500
|
+
window.setTimeout(() => setServingCurl((p) => (p?.closing ? null : p)), 220)
|
|
501
|
+
}, [])
|
|
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])
|
|
435
545
|
const updateResource = useUpdateResource()
|
|
436
|
-
const baseActionsBarProps = useActionsBarProps(
|
|
546
|
+
const baseActionsBarProps = useActionsBarProps(apiKind, namespace, name)
|
|
437
547
|
const desktopDownload = useDesktopDownload()
|
|
438
548
|
|
|
439
549
|
const resourceGroup = useMemo(
|
|
@@ -443,14 +553,16 @@ export function WorkloadView({
|
|
|
443
553
|
// Live Operational Issues for this resource. Fetched here (not inside the lead
|
|
444
554
|
// render-prop) so the count also gates `hasOperationalIssues` — which tells the
|
|
445
555
|
// renderers to suppress their own status-derived problems and avoid duplicates.
|
|
446
|
-
// Keyed on the
|
|
556
|
+
// Keyed on the stable API kind+group (same inputs as the resource fetch above),
|
|
447
557
|
// NOT the manifest-derived ones: deriving kind/group from the loaded resource
|
|
448
558
|
// would flip the query key when the manifest arrives, drop liveIssues, and flash
|
|
449
559
|
// the renderer banners. The backend canonicalizes a plural kind via discovery,
|
|
450
|
-
// so
|
|
451
|
-
const { data: liveIssues } = useResourceIssues(
|
|
560
|
+
// so using the normalized API kind resolves direct links and app navigation alike.
|
|
561
|
+
const { data: liveIssues } = useResourceIssues(apiKind, rest.group, namespace, name)
|
|
562
|
+
const { data: auditFindings } = useResourceAudit(apiKind, namespace, name)
|
|
563
|
+
const hasOperationalIssues = Boolean(liveIssues?.length)
|
|
452
564
|
const { onCompareTo, onCompareAcrossClusters, picker: comparePicker } = useCompareLauncher({
|
|
453
|
-
kind:
|
|
565
|
+
kind: apiKind,
|
|
454
566
|
namespace,
|
|
455
567
|
name,
|
|
456
568
|
// Prefer the URL-supplied group so Compare works even before the resource
|
|
@@ -491,6 +603,19 @@ export function WorkloadView({
|
|
|
491
603
|
},
|
|
492
604
|
[navigateRouter, searchParams],
|
|
493
605
|
)
|
|
606
|
+
const handleOpenApplication = useCallback(
|
|
607
|
+
(appKey: string) => {
|
|
608
|
+
const params = new URLSearchParams()
|
|
609
|
+
const namespaces = new Set((searchParams.get('namespaces') ?? '').split(',').map((ns) => ns.trim()).filter(Boolean))
|
|
610
|
+
if (ownershipContext?.application?.key === appKey && ownershipContext.workload.namespace) {
|
|
611
|
+
namespaces.add(ownershipContext.workload.namespace)
|
|
612
|
+
}
|
|
613
|
+
if (namespaces.size > 0) params.set('namespaces', Array.from(namespaces).join(','))
|
|
614
|
+
params.set('app', appKey)
|
|
615
|
+
navigateRouter({ pathname: '/applications', search: params.toString() })
|
|
616
|
+
},
|
|
617
|
+
[navigateRouter, ownershipContext, searchParams],
|
|
618
|
+
)
|
|
494
619
|
|
|
495
620
|
// Duplicate dialog
|
|
496
621
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
@@ -501,10 +626,46 @@ export function WorkloadView({
|
|
|
501
626
|
setDuplicateDialogOpen(true)
|
|
502
627
|
}, [])
|
|
503
628
|
|
|
629
|
+
const supportsWorkloadPods = ['deployments', 'statefulsets', 'daemonsets'].includes(apiKind)
|
|
630
|
+
const workloadPodsQuery = useWorkloadPods(
|
|
631
|
+
supportsWorkloadPods ? apiKind : '',
|
|
632
|
+
namespace,
|
|
633
|
+
name,
|
|
634
|
+
)
|
|
635
|
+
const servingRefs = useMemo(() => collectServingRefs(relationships), [relationships])
|
|
636
|
+
const servingQueries = useQueries({
|
|
637
|
+
queries: servingRefs.map((ref) => {
|
|
638
|
+
const pluralKind = kindToPlural(ref.kind)
|
|
639
|
+
const ns = ref.namespace || '_'
|
|
640
|
+
const params = new URLSearchParams()
|
|
641
|
+
if (ref.group) params.set('group', ref.group)
|
|
642
|
+
const queryString = params.toString()
|
|
643
|
+
return {
|
|
644
|
+
queryKey: ['resource', pluralKind, ref.namespace, ref.name, ref.group],
|
|
645
|
+
queryFn: () => fetchJSON<any>(`/resources/${pluralKind}/${ns}/${ref.name}${queryString ? `?${queryString}` : ''}`),
|
|
646
|
+
enabled: expanded && Boolean(ref.kind && ref.name),
|
|
647
|
+
staleTime: 30000,
|
|
648
|
+
}
|
|
649
|
+
}),
|
|
650
|
+
})
|
|
651
|
+
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
|
+
}),
|
|
662
|
+
[servingRefs, servingQueries],
|
|
663
|
+
)
|
|
664
|
+
|
|
504
665
|
return (
|
|
505
666
|
<>
|
|
506
667
|
<BaseWorkloadView
|
|
507
|
-
kind={
|
|
668
|
+
kind={apiKind}
|
|
508
669
|
namespace={namespace}
|
|
509
670
|
name={name}
|
|
510
671
|
expanded={expanded}
|
|
@@ -512,14 +673,23 @@ export function WorkloadView({
|
|
|
512
673
|
// Data
|
|
513
674
|
resource={resource}
|
|
514
675
|
relationships={relationships}
|
|
676
|
+
ownershipContext={ownershipContext}
|
|
677
|
+
onOpenApplication={handleOpenApplication}
|
|
515
678
|
certificateInfo={certificateInfo}
|
|
516
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}
|
|
517
686
|
isLoading={resourceLoading}
|
|
518
687
|
resourceError={resourceError}
|
|
519
|
-
refetch={
|
|
688
|
+
refetch={refetchResourceAndRuns}
|
|
520
689
|
// Timeline
|
|
521
690
|
allEvents={allEvents}
|
|
522
|
-
|
|
691
|
+
relatedTimelineEvents={relatedTimelineEvents}
|
|
692
|
+
eventsLoading={eventsLoading || (batchExecution && batchRunsQuery.isLoading)}
|
|
523
693
|
topology={topology}
|
|
524
694
|
resourceFocusedK8sEvents={resourceFocusedK8sEvents}
|
|
525
695
|
resourceFocusedUpdates={resourceFocusedUpdates}
|
|
@@ -536,7 +706,24 @@ export function WorkloadView({
|
|
|
536
706
|
activeTab={migratedTab}
|
|
537
707
|
onTabChange={handleTabChange}
|
|
538
708
|
// Render props
|
|
539
|
-
renderLogsTab={(props) => <LogsTabContent {...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}
|
|
718
|
+
selectedRunKey={selectedRunKey}
|
|
719
|
+
canViewLogs={baseActionsBarProps.canViewLogs}
|
|
720
|
+
onSelectRun={handleSelectedRunChange}
|
|
721
|
+
onSwitchToLogs={() => handleTabChange('logs')}
|
|
722
|
+
onSwitchToTimeline={() => handleTabChange('timeline')}
|
|
723
|
+
onNavigateToResource={rest.onNavigateToResource}
|
|
724
|
+
/>
|
|
725
|
+
) : null
|
|
726
|
+
}
|
|
540
727
|
renderRelatedYaml={(ref) => <RelatedResourceYaml key={`${ref.kind}/${ref.namespace}/${ref.name}`} target={ref} />}
|
|
541
728
|
renderMetricsTab={({ kind, namespace: ns, name: n }) => (
|
|
542
729
|
<MetricsTabContent kind={kind} namespace={ns} name={n} resource={resource} expanded={expanded} />
|
|
@@ -551,8 +738,8 @@ export function WorkloadView({
|
|
|
551
738
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
552
739
|
renderOverviewExtra={({ kind: k, namespace: ns, name: n }) => (
|
|
553
740
|
<>
|
|
554
|
-
<AuditSection kind={k} namespace={ns} name={n} />
|
|
555
741
|
<FluxSourceConsumersSection kind={k} namespace={ns} name={n} />
|
|
742
|
+
<AuditOverviewSection findings={auditFindings ?? []} onViewAll={() => navigateRouter('/checks')} />
|
|
556
743
|
</>
|
|
557
744
|
)}
|
|
558
745
|
renderOverviewLead={() => (
|
|
@@ -571,7 +758,7 @@ export function WorkloadView({
|
|
|
571
758
|
}
|
|
572
759
|
/>
|
|
573
760
|
)}
|
|
574
|
-
hasOperationalIssues={
|
|
761
|
+
hasOperationalIssues={hasOperationalIssues}
|
|
575
762
|
onOpenGitOpsResource={gitopsOwnerQuery.data ? handleOpenGitOpsResource : undefined}
|
|
576
763
|
resolvedGitOpsOwner={gitopsOwner}
|
|
577
764
|
gitOpsOwnerVerified={gitOpsOwnerVerified}
|
|
@@ -597,6 +784,26 @@ export function WorkloadView({
|
|
|
597
784
|
)
|
|
598
785
|
}
|
|
599
786
|
|
|
787
|
+
function collectServingRefs(relationships: Relationships | undefined): ResourceRef[] {
|
|
788
|
+
if (!relationships) return []
|
|
789
|
+
return dedupeRefs([
|
|
790
|
+
...(relationships.services ?? []),
|
|
791
|
+
...(relationships.ingresses ?? []),
|
|
792
|
+
...(relationships.gateways ?? []),
|
|
793
|
+
...(relationships.routes ?? []),
|
|
794
|
+
])
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function dedupeRefs(refs: ResourceRef[]): ResourceRef[] {
|
|
798
|
+
const seen = new Set<string>()
|
|
799
|
+
return refs.filter((ref) => {
|
|
800
|
+
const key = `${ref.kind}/${ref.namespace}/${ref.name}/${ref.group ?? ''}`
|
|
801
|
+
if (seen.has(key)) return false
|
|
802
|
+
seen.add(key)
|
|
803
|
+
return true
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
|
|
600
807
|
function resolveGitOpsOwner(owner: GitOpsOwnerRef | null, argoApplications: any[] | undefined): GitOpsOwnerRef | null {
|
|
601
808
|
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
602
809
|
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
@@ -624,6 +831,60 @@ function findInheritedGitOpsLookupRef(
|
|
|
624
831
|
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
625
832
|
}
|
|
626
833
|
|
|
834
|
+
const POD_OWNERSHIP_WORKLOAD_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs', 'rollouts'])
|
|
835
|
+
|
|
836
|
+
function podWorkloadOwnerFromRelationships(kind: string, namespace: string, relationships: Relationships | undefined, resource: any): ResourceRef | null {
|
|
837
|
+
if (kindToPlural(kind).toLowerCase() !== 'pods') return null
|
|
838
|
+
|
|
839
|
+
if (relationships?.deployment) return relationships.deployment
|
|
840
|
+
|
|
841
|
+
const managedWorkload = relationships?.managedBy?.find((ref) => isPodOwnershipWorkloadRef(ref))
|
|
842
|
+
if (managedWorkload) return managedWorkload
|
|
843
|
+
|
|
844
|
+
if (relationships?.owner && isPodOwnershipWorkloadRef(relationships.owner)) return relationships.owner
|
|
845
|
+
|
|
846
|
+
return podControllerOwnerFromMetadata(namespace, resource)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function isPodOwnershipWorkloadRef(ref: ResourceRef): boolean {
|
|
850
|
+
return POD_OWNERSHIP_WORKLOAD_KINDS.has(kindToPlural(ref.kind).toLowerCase())
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function podControllerOwnerFromMetadata(namespace: string, resource: any): ResourceRef | null {
|
|
854
|
+
const ownerRefs = resource?.metadata?.ownerReferences
|
|
855
|
+
if (!Array.isArray(ownerRefs)) return null
|
|
856
|
+
const owner = ownerRefs.find((ref) => ref?.controller === true) ?? null
|
|
857
|
+
if (!owner?.kind || !owner?.name) return null
|
|
858
|
+
if (!isPodOwnershipWorkloadRef({ kind: owner.kind, namespace, name: owner.name })) return null
|
|
859
|
+
return {
|
|
860
|
+
kind: owner.kind,
|
|
861
|
+
namespace,
|
|
862
|
+
name: owner.name,
|
|
863
|
+
group: apiVersionToGroup(owner.apiVersion),
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function buildPodOwnershipContext(
|
|
868
|
+
workload: ResourceRef | null,
|
|
869
|
+
apps: AppRow[] | undefined,
|
|
870
|
+
): ResourceOwnershipContext | undefined {
|
|
871
|
+
if (!workload) return undefined
|
|
872
|
+
const matches = (apps ?? []).filter((app) =>
|
|
873
|
+
(app.workloads ?? []).some((candidate) => sameWorkload(candidate, workload)),
|
|
874
|
+
)
|
|
875
|
+
const app = matches.length === 1 ? matches[0] : null
|
|
876
|
+
return {
|
|
877
|
+
workload,
|
|
878
|
+
application: app ? { key: app.key, name: app.name } : undefined,
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
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
|
|
886
|
+
}
|
|
887
|
+
|
|
627
888
|
function nativeHelmOwnerFromRelationships(relationships: Relationships | undefined, fallbackNamespace: string): HelmOwnerRef | null {
|
|
628
889
|
const ref = relationships?.managedBy?.[0]
|
|
629
890
|
if (!ref || !isNativeHelmManager(ref)) return null
|
|
@@ -699,7 +960,8 @@ function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
|
699
960
|
// LOGS TAB — platform-specific (uses data-fetching hooks)
|
|
700
961
|
// ============================================================================
|
|
701
962
|
|
|
702
|
-
const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet'])
|
|
963
|
+
const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet', 'Job', 'Workflow'])
|
|
964
|
+
const SCHEDULED_LOG_KINDS = new Set(['CronJob', 'CronWorkflow', 'WorkflowTemplate', 'ClusterWorkflowTemplate', 'ScaledJob'])
|
|
703
965
|
|
|
704
966
|
function LogsTabContent({
|
|
705
967
|
kind,
|
|
@@ -712,6 +974,8 @@ function LogsTabContent({
|
|
|
712
974
|
onSelectPod,
|
|
713
975
|
initialContainer,
|
|
714
976
|
onConsumeInitialContainer,
|
|
977
|
+
selectedRunKey,
|
|
978
|
+
onSelectRun,
|
|
715
979
|
}: {
|
|
716
980
|
kind: string
|
|
717
981
|
apiKind: string
|
|
@@ -723,12 +987,22 @@ function LogsTabContent({
|
|
|
723
987
|
onSelectPod: (name: string | null) => void
|
|
724
988
|
initialContainer: string | null
|
|
725
989
|
onConsumeInitialContainer: () => void
|
|
990
|
+
selectedRunKey: string
|
|
991
|
+
onSelectRun: (runKey: string) => void
|
|
726
992
|
}) {
|
|
727
|
-
|
|
993
|
+
if (SCHEDULED_LOG_KINDS.has(kind)) {
|
|
994
|
+
return (
|
|
995
|
+
<div className="h-full">
|
|
996
|
+
<ScheduledWorkloadLogsViewer kind={apiKind} namespace={namespace} name={name} selectedRunKey={selectedRunKey} onSelectRun={onSelectRun} />
|
|
997
|
+
</div>
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Workload kinds with stable pod selectors use the aggregated workload logs viewer
|
|
728
1002
|
if (WORKLOAD_LOG_KINDS.has(kind)) {
|
|
729
1003
|
return (
|
|
730
1004
|
<div className="h-full">
|
|
731
|
-
<WorkloadLogsViewer kind={apiKind} namespace={namespace} name={name} />
|
|
1005
|
+
<WorkloadLogsViewer kind={apiKind} namespace={namespace} name={name} autoStream={shouldAutoStreamWorkloadLogs(kind, resource)} />
|
|
732
1006
|
</div>
|
|
733
1007
|
)
|
|
734
1008
|
}
|
|
@@ -750,6 +1024,17 @@ function LogsTabContent({
|
|
|
750
1024
|
)
|
|
751
1025
|
}
|
|
752
1026
|
|
|
1027
|
+
function shouldAutoStreamWorkloadLogs(kind: string, resource: any): boolean {
|
|
1028
|
+
if (kind === 'Job') {
|
|
1029
|
+
return (resource?.status?.active ?? 0) > 0
|
|
1030
|
+
}
|
|
1031
|
+
if (kind === 'Workflow') {
|
|
1032
|
+
const phase = resource?.status?.phase
|
|
1033
|
+
return phase === 'Running' || phase === 'Pending'
|
|
1034
|
+
}
|
|
1035
|
+
return true
|
|
1036
|
+
}
|
|
1037
|
+
|
|
753
1038
|
function PodLogsTab({ namespace, name, resource, initialContainer, onConsumeInitialContainer }: {
|
|
754
1039
|
namespace: string
|
|
755
1040
|
name: string
|
|
@@ -860,11 +1145,9 @@ function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialCon
|
|
|
860
1145
|
)
|
|
861
1146
|
}
|
|
862
1147
|
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (!findings || findings.length === 0) return null
|
|
867
|
-
return <AuditAlerts findings={findings} onViewAll={() => navigate('/checks')} />
|
|
1148
|
+
function AuditOverviewSection({ findings, onViewAll }: { findings: AuditFinding[]; onViewAll: () => void }) {
|
|
1149
|
+
if (findings.length === 0) return null
|
|
1150
|
+
return <AuditAlerts findings={findings} onViewAll={onViewAll} />
|
|
868
1151
|
}
|
|
869
1152
|
|
|
870
1153
|
// FluxSourceConsumersSection lists the reconcilers (Kustomization, HelmRelease)
|
|
@@ -917,15 +1200,16 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
|
|
|
917
1200
|
|
|
918
1201
|
if (consumers.length === 0) {
|
|
919
1202
|
return (
|
|
920
|
-
<
|
|
921
|
-
|
|
922
|
-
|
|
1203
|
+
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
1204
|
+
<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>
|
|
1206
|
+
</section>
|
|
923
1207
|
)
|
|
924
1208
|
}
|
|
925
1209
|
|
|
926
1210
|
return (
|
|
927
|
-
<
|
|
928
|
-
<h3 className="mb-
|
|
1211
|
+
<section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
|
|
1212
|
+
<h3 className="mb-3 text-sm font-semibold text-theme-text-primary">
|
|
929
1213
|
Consumed by ({consumers.length})
|
|
930
1214
|
</h3>
|
|
931
1215
|
<div className="flex flex-wrap gap-1.5">
|
|
@@ -944,7 +1228,7 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
|
|
|
944
1228
|
</Tooltip>
|
|
945
1229
|
))}
|
|
946
1230
|
</div>
|
|
947
|
-
</
|
|
1231
|
+
</section>
|
|
948
1232
|
)
|
|
949
1233
|
}
|
|
950
1234
|
|