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