@skyhook-io/radar-app 1.14.4 → 1.14.6
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 +2 -9
- package/package.json +6 -6
- package/src/App.tsx +91 -328
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +233 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +14 -8
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/diagnose/AIMarkdown.tsx +8 -3
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/AssessmentCard.tsx +249 -237
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
- package/src/components/diagnose/InvestigationView.tsx +4 -1
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/investigationCase.test.tsx +56 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
- package/src/components/diagnose/investigationEvidence/observations.ts +8 -5
- package/src/components/diagnose/investigationEvidence/types.ts +1 -1
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +159 -2
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +42 -0
- package/src/components/execution/JobSetAdmission.tsx +17 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.tsx +2 -5
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +207 -83
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/empty-state.test.tsx +67 -0
- package/src/components/rightsizing/model.test.ts +120 -2
- package/src/components/rightsizing/model.ts +36 -13
- package/src/components/rightsizing/notices.test.tsx +176 -0
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/SettingsDialog.tsx +157 -42
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +18 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/Markdown.tsx +33 -6
- package/src/components/ui/command-items.ts +0 -3
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +36 -18
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +18 -1
- package/src/utils/navigation.ts +12 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/components/ui/CommandPalette.tsx +0 -261
|
@@ -146,9 +146,6 @@ const VIEW_ENTRIES: {
|
|
|
146
146
|
{ view: "cost", label: "Cost", icon: Coins, shortcut: "g c" },
|
|
147
147
|
];
|
|
148
148
|
|
|
149
|
-
// The static command-palette items (Views, Resource Kinds, Contexts,
|
|
150
|
-
// Namespaces, Actions) — shared by the centered modal (embedded) and the
|
|
151
|
-
// standalone omnibar so the two never drift.
|
|
152
149
|
export function useCommandItems(cb: CommandItemCallbacks): CommandItem[] {
|
|
153
150
|
const { data: namespacesData } = useNamespaces();
|
|
154
151
|
const { data: contexts } = useContexts();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { AlertTriangle } from 'lucide-react'
|
|
3
|
+
import { pluralize } from '@skyhook-io/k8s-ui'
|
|
4
|
+
import { useSwitchContext, fetchSessionCounts, type SessionCounts } from '../api/client'
|
|
5
|
+
import { useContextSwitch } from '../context/ContextSwitchContext'
|
|
6
|
+
import { useToast } from './ui/Toast'
|
|
7
|
+
import { useDock } from './dock'
|
|
8
|
+
import type { ContextInfo, SelectedResource } from '../types'
|
|
9
|
+
import { parseContextForSwitcher } from '../utils/context-name'
|
|
10
|
+
|
|
11
|
+
function shouldSuppressSwitchErrorToast(error: unknown): boolean {
|
|
12
|
+
const message = error instanceof Error ? error.message : ''
|
|
13
|
+
return message.includes('cluster connection failed:')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PendingSwitch {
|
|
17
|
+
context: ContextInfo
|
|
18
|
+
openAfter?: SelectedResource
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The one way to switch contexts from the UI: the switching overlay, a
|
|
22
|
+
// confirmation when port-forwards or terminals would be killed, and an error
|
|
23
|
+
// toast when the switch fails. `openAfter` is opened once the new context is
|
|
24
|
+
// live. Callers render `confirmDialog`.
|
|
25
|
+
export function useContextSwitchFlow() {
|
|
26
|
+
const switchContext = useSwitchContext()
|
|
27
|
+
const { startSwitch, endSwitch, setOpenAfterSwitch } = useContextSwitch()
|
|
28
|
+
const { showError } = useToast()
|
|
29
|
+
const { tabs } = useDock()
|
|
30
|
+
const [pending, setPending] = useState<PendingSwitch | null>(null)
|
|
31
|
+
const [sessionCounts, setSessionCounts] = useState<SessionCounts | null>(null)
|
|
32
|
+
|
|
33
|
+
const performSwitch = async ({ context, openAfter }: PendingSwitch) => {
|
|
34
|
+
const parsed = parseContextForSwitcher(context)
|
|
35
|
+
startSwitch({
|
|
36
|
+
raw: parsed.raw,
|
|
37
|
+
provider: parsed.provider,
|
|
38
|
+
account: parsed.account,
|
|
39
|
+
region: parsed.region,
|
|
40
|
+
clusterName: parsed.clusterName,
|
|
41
|
+
})
|
|
42
|
+
setOpenAfterSwitch(openAfter ? { context: context.name, resource: openAfter } : null)
|
|
43
|
+
try {
|
|
44
|
+
await switchContext.mutateAsync({ name: context.name })
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to switch context:', error)
|
|
47
|
+
setOpenAfterSwitch(null)
|
|
48
|
+
endSwitch()
|
|
49
|
+
// Backend may not transition to StateDisconnected on client-side errors
|
|
50
|
+
// (network, timeout) — without this toast the user gets no feedback.
|
|
51
|
+
if (!shouldSuppressSwitchErrorToast(error)) {
|
|
52
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
53
|
+
showError('Failed to switch context', message)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const requestSwitch = async (context: ContextInfo, openAfter?: SelectedResource) => {
|
|
59
|
+
if (context.isCurrent || switchContext.isPending) return
|
|
60
|
+
// Active sessions (port forwards from API + terminal tabs from dock) get
|
|
61
|
+
// a confirmation prompt — switching contexts kills both.
|
|
62
|
+
try {
|
|
63
|
+
const counts = await fetchSessionCounts()
|
|
64
|
+
const terminalTabs = tabs.filter(t => t.type === 'terminal').length
|
|
65
|
+
const total = counts.portForwards + terminalTabs
|
|
66
|
+
if (total > 0) {
|
|
67
|
+
setSessionCounts({ ...counts, execSessions: terminalTabs, total })
|
|
68
|
+
setPending({ context, openAfter })
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Session-counts is best-effort; failing it shouldn't block the user.
|
|
73
|
+
// But warn — if there ARE active sessions we couldn't see, the switch
|
|
74
|
+
// will silently kill them.
|
|
75
|
+
console.error('Failed to check sessions:', error)
|
|
76
|
+
showError(
|
|
77
|
+
'Could not check active sessions',
|
|
78
|
+
'Switching anyway. Any open port-forwards or terminals will be terminated.',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
performSwitch({ context, openAfter })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const confirm = () => {
|
|
85
|
+
if (pending) performSwitch(pending)
|
|
86
|
+
setPending(null)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cancel = () => {
|
|
90
|
+
setPending(null)
|
|
91
|
+
setSessionCounts(null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const confirmDialog: ReactNode = pending && sessionCounts ? (
|
|
95
|
+
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50">
|
|
96
|
+
<div className="bg-theme-surface border border-theme-border rounded-lg shadow-xl max-w-md mx-4 overflow-hidden">
|
|
97
|
+
<div className="px-4 py-3 border-b border-theme-border flex items-center gap-2">
|
|
98
|
+
<AlertTriangle className="w-5 h-5 text-amber-400" />
|
|
99
|
+
<span className="font-medium text-theme-text-primary">Active Sessions</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="px-4 py-4">
|
|
102
|
+
<p className="text-sm text-theme-text-secondary mb-3">
|
|
103
|
+
Switching contexts will terminate active sessions:
|
|
104
|
+
</p>
|
|
105
|
+
<ul className="text-sm text-theme-text-primary space-y-1 mb-4">
|
|
106
|
+
{sessionCounts.portForwards > 0 && (
|
|
107
|
+
<li className="flex items-center gap-2">
|
|
108
|
+
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
|
|
109
|
+
{pluralize(sessionCounts.portForwards, 'port forward')}
|
|
110
|
+
</li>
|
|
111
|
+
)}
|
|
112
|
+
{sessionCounts.execSessions > 0 && (
|
|
113
|
+
<li className="flex items-center gap-2">
|
|
114
|
+
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
|
115
|
+
{pluralize(sessionCounts.execSessions, 'terminal session')}
|
|
116
|
+
</li>
|
|
117
|
+
)}
|
|
118
|
+
</ul>
|
|
119
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
120
|
+
Switch to: <span className="text-theme-text-secondary">{parseContextForSwitcher(pending.context).clusterName}</span>
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="px-4 py-3 border-t border-theme-border flex justify-end gap-2">
|
|
124
|
+
<button
|
|
125
|
+
onClick={cancel}
|
|
126
|
+
className="px-3 py-1.5 text-sm rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary transition-colors"
|
|
127
|
+
>
|
|
128
|
+
Cancel
|
|
129
|
+
</button>
|
|
130
|
+
<button
|
|
131
|
+
onClick={confirm}
|
|
132
|
+
className="px-3 py-1.5 text-sm rounded-md bg-amber-500 hover:bg-amber-600 text-white transition-colors"
|
|
133
|
+
>
|
|
134
|
+
Switch Anyway
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
) : null
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
requestSwitch,
|
|
143
|
+
isPending: switchContext.isPending,
|
|
144
|
+
error: switchContext.isError ? switchContext.error : null,
|
|
145
|
+
confirmDialog,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import { gitOpsOwnerFromRelationships } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import type { Relationships } from '../../types'
|
|
5
|
-
import { findInheritedGitOpsLookupRef } from './WorkloadView'
|
|
5
|
+
import { findInheritedGitOpsLookupRef, supportsBatchExecution } from './WorkloadView'
|
|
6
6
|
|
|
7
7
|
describe('findInheritedGitOpsLookupRef', () => {
|
|
8
8
|
it('follows a referenced ReplicaSet to its parent workload for inherited ownership', () => {
|
|
@@ -58,3 +58,19 @@ describe('findInheritedGitOpsLookupRef', () => {
|
|
|
58
58
|
).toBeNull()
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
|
+
|
|
62
|
+
describe('supportsBatchExecution', () => {
|
|
63
|
+
it('enables only the supported JobSet API identity', () => {
|
|
64
|
+
expect(supportsBatchExecution('JobSet', 'jobsets', 'jobset.x-k8s.io', 'jobset.x-k8s.io/v1alpha2')).toBe(true)
|
|
65
|
+
expect(supportsBatchExecution('JobSet', 'jobsets', 'jobset.x-k8s.io')).toBe(false)
|
|
66
|
+
expect(supportsBatchExecution('JobSet', 'jobsets', 'example.io', 'example.io/v1alpha2')).toBe(false)
|
|
67
|
+
expect(supportsBatchExecution('JobSet', 'jobsets', 'example.io', 'jobset.x-k8s.io/v1alpha2')).toBe(false)
|
|
68
|
+
expect(supportsBatchExecution('JobSet', 'jobsets', 'jobset.x-k8s.io', 'jobset.x-k8s.io/v1beta1')).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('preserves the core Job collision guard and existing scheduled kinds', () => {
|
|
72
|
+
expect(supportsBatchExecution('Job', 'jobs', 'batch')).toBe(true)
|
|
73
|
+
expect(supportsBatchExecution('Job', 'jobs', 'example.io')).toBe(false)
|
|
74
|
+
expect(supportsBatchExecution('CronJob', 'cronjobs', 'batch')).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { RayClusterRenderer } from '../resources/renderers/RayClusterRenderer'
|
|
2
|
+
import { RayServiceRenderer } from '../resources/renderers/RayServiceRenderer'
|
|
3
|
+
import { KueueWorkloadRenderer } from '../resources/renderers/KueueWorkloadRenderer'
|
|
1
4
|
import { useMemo, useEffect, useCallback, useRef, useState } from 'react'
|
|
2
5
|
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
3
6
|
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
@@ -29,6 +32,7 @@ import {
|
|
|
29
32
|
type WorkloadImageTarget,
|
|
30
33
|
} from '@skyhook-io/k8s-ui'
|
|
31
34
|
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
35
|
+
import { isJobSetV1Alpha2 } from '@skyhook-io/k8s-ui/components/resources/resource-utils-jobset-lws'
|
|
32
36
|
import type { SelectedResource, ResourceRef, Relationships, ResourceWithRelationships } from '../../types'
|
|
33
37
|
import {
|
|
34
38
|
kindToPlural,
|
|
@@ -121,6 +125,7 @@ import { ServiceAccountRenderer } from '../resources/renderers/ServiceAccountRen
|
|
|
121
125
|
import { RoleRenderer } from '../resources/renderers/RoleRenderer'
|
|
122
126
|
import { RoleBindingRenderer } from '../resources/renderers/RoleBindingRenderer'
|
|
123
127
|
import { NamespaceRenderer } from '../resources/renderers/NamespaceRenderer'
|
|
128
|
+
import { CAPIClusterRenderer } from '../resources/renderers/CAPIClusterRenderer'
|
|
124
129
|
import { HPARenderer } from '../resources/renderers/HPARenderer'
|
|
125
130
|
import { PVCRenderer } from '../resources/renderers/PVCRenderer'
|
|
126
131
|
import { RolloutRenderer } from '../resources/renderers/RolloutRenderer'
|
|
@@ -152,10 +157,24 @@ const BATCH_EXECUTION_KINDS = new Set([
|
|
|
152
157
|
'WorkflowTemplate',
|
|
153
158
|
'ClusterWorkflowTemplate',
|
|
154
159
|
'ScaledJob',
|
|
160
|
+
'JobSet',
|
|
155
161
|
])
|
|
156
162
|
|
|
163
|
+
export function supportsBatchExecution(kind: string, apiKind: string, group?: string, apiVersion?: string): boolean {
|
|
164
|
+
if (!BATCH_EXECUTION_KINDS.has(kind)) return false
|
|
165
|
+
if (kind === 'Job') return isCoreBatchJob(apiKind, group)
|
|
166
|
+
if (kind === 'JobSet') {
|
|
167
|
+
return group === 'jobset.x-k8s.io' && isJobSetV1Alpha2({ kind, apiVersion })
|
|
168
|
+
}
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
157
172
|
// Stable reference — web renderer wrappers inject platform hooks internally
|
|
158
173
|
const rendererOverrides: RendererOverrides = {
|
|
174
|
+
RayServiceRenderer,
|
|
175
|
+
RayClusterRenderer,
|
|
176
|
+
KueueWorkloadRenderer,
|
|
177
|
+
CAPIClusterRenderer,
|
|
159
178
|
PodRenderer,
|
|
160
179
|
KarpenterNodePoolRenderer,
|
|
161
180
|
NodeRenderer,
|
|
@@ -548,18 +567,6 @@ export function WorkloadView({
|
|
|
548
567
|
[searchParams, setSearchParams],
|
|
549
568
|
)
|
|
550
569
|
|
|
551
|
-
const batchKind = pluralToKind(apiKind)
|
|
552
|
-
const batchExecution = BATCH_EXECUTION_KINDS.has(batchKind) &&
|
|
553
|
-
(batchKind !== 'Job' || isCoreBatchJob(apiKind, rest.group))
|
|
554
|
-
const batchRunsQuery = useWorkloadRuns(apiKind, namespace, name, expanded && batchExecution, {
|
|
555
|
-
refetchActive: true,
|
|
556
|
-
clusterScoped: batchKind === 'ClusterWorkflowTemplate',
|
|
557
|
-
})
|
|
558
|
-
const relatedTimelineEvents = useMemo(
|
|
559
|
-
() => workloadRunTimelineEvents(batchRunsQuery.data?.runs ?? []),
|
|
560
|
-
[batchRunsQuery.data?.runs],
|
|
561
|
-
)
|
|
562
|
-
|
|
563
570
|
// Fetch resource with relationships
|
|
564
571
|
const {
|
|
565
572
|
data: resourceResponse,
|
|
@@ -579,6 +586,16 @@ export function WorkloadView({
|
|
|
579
586
|
// of itself. Fall back to the group derived from the fetched resource, then
|
|
580
587
|
// to undefined (which the gate treats as "group unknown → allow").
|
|
581
588
|
const effectiveGroup = rest.group || resourceGroup || undefined
|
|
589
|
+
const batchKind = pluralToKind(apiKind)
|
|
590
|
+
const batchExecution = supportsBatchExecution(batchKind, apiKind, effectiveGroup, resource?.apiVersion)
|
|
591
|
+
const batchRunsQuery = useWorkloadRuns(apiKind, namespace, name, expanded && batchExecution, {
|
|
592
|
+
refetchActive: true,
|
|
593
|
+
clusterScoped: batchKind === 'ClusterWorkflowTemplate',
|
|
594
|
+
})
|
|
595
|
+
const relatedTimelineEvents = useMemo(
|
|
596
|
+
() => batchKind === 'JobSet' ? [] : workloadRunTimelineEvents(batchRunsQuery.data?.runs ?? []),
|
|
597
|
+
[batchKind, batchRunsQuery.data?.runs],
|
|
598
|
+
)
|
|
582
599
|
// Reachability for a workload IS the reachability of the Services in front of
|
|
583
600
|
// it - a Deployment has no address of its own. Empty for a workload nothing
|
|
584
601
|
// selects, which correctly leaves the tab hidden: there is no path to trace.
|
|
@@ -595,6 +612,7 @@ export function WorkloadView({
|
|
|
595
612
|
}),
|
|
596
613
|
queryClient.refetchQueries({
|
|
597
614
|
queryKey: ['workload-pods', apiKind, namespace, name],
|
|
615
|
+
...(apiKind === 'rayclusters' ? { type: 'active' as const } : {}),
|
|
598
616
|
}),
|
|
599
617
|
])
|
|
600
618
|
}, [apiKind, name, namespace, queryClient, refetchResource])
|
|
@@ -761,7 +779,7 @@ export function WorkloadView({
|
|
|
761
779
|
isLoading: resourceFocusedEventsLoading,
|
|
762
780
|
k8sError: resourceFocusedK8sError,
|
|
763
781
|
updatesError: resourceFocusedUpdatesError,
|
|
764
|
-
} = useResourceEvents(apiKind, namespace, name)
|
|
782
|
+
} = useResourceEvents(apiKind, namespace, name, effectiveGroup)
|
|
765
783
|
|
|
766
784
|
// Fetch all events for this resource's namespace (only when expanded)
|
|
767
785
|
const { data: allEvents, isLoading: eventsLoading } = useChanges({
|
|
@@ -868,7 +886,7 @@ export function WorkloadView({
|
|
|
868
886
|
// the renderer banners. The backend canonicalizes a plural kind via discovery,
|
|
869
887
|
// so using the normalized API kind resolves direct links and app navigation alike.
|
|
870
888
|
const { data: liveIssues, isPending: issuesPending } = useResourceIssues(apiKind, rest.group, namespace, name)
|
|
871
|
-
const { data: auditFindings } = useResourceAudit(apiKind, namespace, name)
|
|
889
|
+
const { data: auditFindings } = useResourceAudit(apiKind, namespace, name, effectiveGroup)
|
|
872
890
|
const hasOperationalIssues = Boolean(liveIssues?.length)
|
|
873
891
|
const {
|
|
874
892
|
onCompareTo,
|
|
@@ -1169,7 +1187,7 @@ export function WorkloadView({
|
|
|
1169
1187
|
// Timeline
|
|
1170
1188
|
allEvents={allEvents}
|
|
1171
1189
|
relatedTimelineEvents={relatedTimelineEvents}
|
|
1172
|
-
eventsLoading={eventsLoading || (batchExecution && batchRunsQuery.isLoading)}
|
|
1190
|
+
eventsLoading={eventsLoading || (batchExecution && batchKind !== 'JobSet' && batchRunsQuery.isLoading)}
|
|
1173
1191
|
topology={topology}
|
|
1174
1192
|
resourceFocusedK8sEvents={resourceFocusedK8sEvents}
|
|
1175
1193
|
resourceFocusedUpdates={resourceFocusedUpdates}
|
|
@@ -1199,8 +1217,7 @@ export function WorkloadView({
|
|
|
1199
1217
|
/>
|
|
1200
1218
|
)}
|
|
1201
1219
|
renderExpandedOverview={({ kind: k, apiKind, namespace: ns, name: n, resource: res }) =>
|
|
1202
|
-
|
|
1203
|
-
(k !== 'Job' || isCoreBatchJob(apiKind, effectiveGroup)) &&
|
|
1220
|
+
supportsBatchExecution(k, apiKind, effectiveGroup, res?.apiVersion) &&
|
|
1204
1221
|
res ? (
|
|
1205
1222
|
<BatchExecutionFullscreen
|
|
1206
1223
|
kind={k}
|
|
@@ -1561,6 +1578,7 @@ const SCHEDULED_LOG_KINDS = new Set([
|
|
|
1561
1578
|
'WorkflowTemplate',
|
|
1562
1579
|
'ClusterWorkflowTemplate',
|
|
1563
1580
|
'ScaledJob',
|
|
1581
|
+
'JobSet',
|
|
1564
1582
|
])
|
|
1565
1583
|
|
|
1566
1584
|
function LogsTabContent({
|
|
@@ -1592,7 +1610,7 @@ function LogsTabContent({
|
|
|
1592
1610
|
selectedRunKey: string
|
|
1593
1611
|
onSelectRun: (runKey: string) => void
|
|
1594
1612
|
}) {
|
|
1595
|
-
if (SCHEDULED_LOG_KINDS.has(kind)) {
|
|
1613
|
+
if (SCHEDULED_LOG_KINDS.has(kind) && supportsBatchExecution(kind, apiKind, group, resource?.apiVersion)) {
|
|
1596
1614
|
return (
|
|
1597
1615
|
<div className="h-full">
|
|
1598
1616
|
<ScheduledWorkloadLogsViewer
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useRef, ReactNode } from 'react'
|
|
2
|
+
import type { SelectedResource } from '../types'
|
|
2
3
|
|
|
3
4
|
export interface ParsedContextInfo {
|
|
4
5
|
raw: string
|
|
@@ -15,6 +16,11 @@ interface ContextSwitchState {
|
|
|
15
16
|
startSwitch: (context: ParsedContextInfo) => void
|
|
16
17
|
updateProgress: (message: string) => void
|
|
17
18
|
endSwitch: () => void
|
|
19
|
+
// A resource to open once a switch to `context` is live. The app's context
|
|
20
|
+
// reset takes it, so the open can't race the reset that clears the old
|
|
21
|
+
// cluster's drawer and URL. A change to any other context discards it.
|
|
22
|
+
setOpenAfterSwitch: (target: { context: string; resource: SelectedResource } | null) => void
|
|
23
|
+
takeOpenAfterSwitch: (context: string) => SelectedResource | null
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
const ContextSwitchContext = createContext<ContextSwitchState | null>(null)
|
|
@@ -40,8 +46,18 @@ export function ContextSwitchProvider({ children }: { children: ReactNode }) {
|
|
|
40
46
|
setProgressMessage(null)
|
|
41
47
|
}, [])
|
|
42
48
|
|
|
49
|
+
const openAfterSwitch = useRef<{ context: string; resource: SelectedResource } | null>(null)
|
|
50
|
+
const setOpenAfterSwitch = useCallback((target: { context: string; resource: SelectedResource } | null) => {
|
|
51
|
+
openAfterSwitch.current = target
|
|
52
|
+
}, [])
|
|
53
|
+
const takeOpenAfterSwitch = useCallback((context: string) => {
|
|
54
|
+
const target = openAfterSwitch.current
|
|
55
|
+
openAfterSwitch.current = null
|
|
56
|
+
return target && target.context === context ? target.resource : null
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
43
59
|
return (
|
|
44
|
-
<ContextSwitchContext.Provider value={{ isSwitching, targetContext, progressMessage, startSwitch, updateProgress, endSwitch }}>
|
|
60
|
+
<ContextSwitchContext.Provider value={{ isSwitching, targetContext, progressMessage, startSwitch, updateProgress, endSwitch, setOpenAfterSwitch, takeOpenAfterSwitch }}>
|
|
45
61
|
{children}
|
|
46
62
|
</ContextSwitchContext.Provider>
|
|
47
63
|
)
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
// Slot-based customization of Radar's top nav.
|
|
2
|
-
//
|
|
3
|
-
// Lets library consumers (e.g. Radar Hub) swap out the brand area, the
|
|
4
|
-
// context picker, and append items on the right of the action bar —
|
|
5
|
-
// without forking App.tsx or building a parallel nav.
|
|
6
|
-
//
|
|
7
|
-
// The `embedded` flag hides chrome that only makes sense for Radar's
|
|
8
|
-
// standalone OSS binary: GitHub star link, update-from-GitHub notifier,
|
|
9
|
-
// Radar's own OIDC/proxy-mode UserMenu. Consumers typically provide
|
|
10
|
-
// their own auth UI via `rightExtras`.
|
|
11
|
-
//
|
|
12
|
-
// Default (no provider): Radar renders its standalone nav unchanged.
|
|
13
1
|
import { createContext, useContext } from 'react';
|
|
14
2
|
import type { ReactNode } from 'react';
|
|
15
3
|
|
|
@@ -21,11 +9,9 @@ import type { ReactNode } from 'react';
|
|
|
21
9
|
*/
|
|
22
10
|
export type FleetTakeoverTarget = 'issues' | 'gitops' | 'checks' | 'certs';
|
|
23
11
|
|
|
24
|
-
interface
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/** Replaces the ContextSwitcher (kubeconfig-context picker). */
|
|
28
|
-
contextSlot?: ReactNode;
|
|
12
|
+
export interface NavCustomization {
|
|
13
|
+
/** Radar Hub owns all chrome; Radar renders only the active view. */
|
|
14
|
+
embedded?: boolean;
|
|
29
15
|
/**
|
|
30
16
|
* When set, a "Compare across clusters" option is added to the Compare
|
|
31
17
|
* button in resource action bars. The host returns the URL that should
|
|
@@ -50,8 +36,7 @@ interface NavCustomizationBase {
|
|
|
50
36
|
* canonical surfaces rather than a second, diverging per-cluster copy:
|
|
51
37
|
* - 'issues' → the Active Issues panel + cluster-health issues count
|
|
52
38
|
* - 'gitops' → the GitOps controllers card
|
|
53
|
-
* - 'checks' → the Cluster Audit card (and any route to /audit
|
|
54
|
-
* `clusterChecksHref` folded in here)
|
|
39
|
+
* - 'checks' → the Cluster Audit card (and any route to /audit)
|
|
55
40
|
* - 'certs' → the Certificate Health card
|
|
56
41
|
*
|
|
57
42
|
* View-shaped targets (issues / gitops / checks) are honored for every entry:
|
|
@@ -64,13 +49,6 @@ interface NavCustomizationBase {
|
|
|
64
49
|
* falls back to `window.location` (a hard reload).
|
|
65
50
|
*/
|
|
66
51
|
fleetTakeoverHref?: (target: FleetTakeoverTarget) => string | undefined;
|
|
67
|
-
/**
|
|
68
|
-
* @deprecated Superseded by `fleetTakeoverHref('checks')`. Kept so consumers
|
|
69
|
-
* still on the pre-1.7 hook keep working (App.tsx folds it into the 'checks'
|
|
70
|
-
* target) — this makes adding `fleetTakeoverHref` an additive, non-breaking
|
|
71
|
-
* change. Remove in a major release once all consumers have migrated.
|
|
72
|
-
*/
|
|
73
|
-
clusterChecksHref?: () => string;
|
|
74
52
|
/**
|
|
75
53
|
* Optional smooth navigator for host-owned URLs. When the host takes a
|
|
76
54
|
* destination over (`fleetTakeoverHref`, `crossClusterCompareHref`), Radar
|
|
@@ -82,40 +60,8 @@ interface NavCustomizationBase {
|
|
|
82
60
|
* OSS / other hosts are unaffected.
|
|
83
61
|
*/
|
|
84
62
|
onHostNavigate?: (url: string) => void;
|
|
85
|
-
/**
|
|
86
|
-
* Chrome level for embedded hosts. Default ('full', or omitted) renders
|
|
87
|
-
* Radar's top bar + the view-switcher. 'none' suppresses BOTH — the host
|
|
88
|
-
* drives view navigation and cluster/namespace scope from its OWN chrome, and
|
|
89
|
-
* Radar renders just the active view's content full-bleed. Radar Hub uses this
|
|
90
|
-
* to surface per-cluster views (Topology / Resources / Traffic / Cost) that
|
|
91
|
-
* don't aggregate to the fleet as native cloud destinations under one chrome,
|
|
92
|
-
* gated by a cluster picker — instead of a second, redundant in-cluster nav.
|
|
93
|
-
* Only meaningful with `embedded: true`.
|
|
94
|
-
*/
|
|
95
|
-
chrome?: 'full' | 'none';
|
|
96
63
|
}
|
|
97
64
|
|
|
98
|
-
/**
|
|
99
|
-
* Slot-based customization of Radar's top nav.
|
|
100
|
-
*
|
|
101
|
-
* Standalone-mode consumers pass `embedded: false` (or omit it) and may
|
|
102
|
-
* optionally append items via `rightExtras`. Embedded-mode consumers must
|
|
103
|
-
* supply `rightExtras` — Radar's OSS chrome (GitHub star, update notifier,
|
|
104
|
-
* built-in UserMenu) is hidden, so the host app owns the right side of the
|
|
105
|
-
* nav and must render its own user/auth UI there.
|
|
106
|
-
*/
|
|
107
|
-
export type NavCustomization =
|
|
108
|
-
| (NavCustomizationBase & {
|
|
109
|
-
embedded?: false;
|
|
110
|
-
/** Appended to the right of the action bar (before the UserMenu). */
|
|
111
|
-
rightExtras?: ReactNode;
|
|
112
|
-
})
|
|
113
|
-
| (NavCustomizationBase & {
|
|
114
|
-
embedded: true;
|
|
115
|
-
/** Required in embedded mode: Radar's own UserMenu is hidden. */
|
|
116
|
-
rightExtras: ReactNode;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
65
|
const NavCustomizationContext = createContext<NavCustomization>({});
|
|
120
66
|
|
|
121
67
|
export function NavCustomizationProvider({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { loadPreferences, persistPreferences } from '../api/preferences'
|
|
3
3
|
|
|
4
4
|
type Theme = 'dark' | 'light'
|
|
5
5
|
|
|
@@ -34,14 +34,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
34
34
|
const setTheme = (newTheme: Theme) => {
|
|
35
35
|
setThemeState(newTheme)
|
|
36
36
|
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
|
|
37
|
-
|
|
38
|
-
method: 'PUT',
|
|
39
|
-
credentials: getCredentialsMode(),
|
|
40
|
-
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
41
|
-
body: JSON.stringify({ theme: newTheme }),
|
|
42
|
-
}).then((res) => {
|
|
43
|
-
if (!res.ok) console.warn('[settings] Failed to persist theme:', res.status)
|
|
44
|
-
}).catch((err) => console.warn('[settings] Failed to persist theme:', err))
|
|
37
|
+
persistPreferences({ theme: newTheme }).catch((err) => console.warn('[settings] Failed to persist theme:', err))
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
const toggleTheme = () => {
|
|
@@ -56,8 +49,7 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
|
56
49
|
|
|
57
50
|
// Sync theme from server (persisted settings survive port changes in desktop app)
|
|
58
51
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
.then((res) => res.ok ? res.json() : null)
|
|
52
|
+
loadPreferences()
|
|
61
53
|
.then((data) => {
|
|
62
54
|
if (data?.theme && (data.theme === 'dark' || data.theme === 'light') && data.theme !== theme) {
|
|
63
55
|
setThemeState(data.theme)
|
|
@@ -21,8 +21,8 @@ interface UseClusterLoadStateResult {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Tracks cluster-data warmup (deferred/partial dashboard load) once the main
|
|
24
|
-
// connection is usable. Standalone
|
|
25
|
-
//
|
|
24
|
+
// connection is usable. Standalone renders it in Radar's topbar; Radar Hub
|
|
25
|
+
// receives it via onClusterLoadStateChange;
|
|
26
26
|
// a chromeless host without a callback gets a fallback row on Home.
|
|
27
27
|
export function useClusterLoadState({
|
|
28
28
|
namespaces,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { loadPreferences, persistPreferences } from '../api/preferences'
|
|
3
3
|
|
|
4
4
|
export interface PinnedKind {
|
|
5
5
|
name: string // plural name for API calls, e.g. "pods", "deployments"
|
|
@@ -25,8 +25,7 @@ function savePinned(pinned: PinnedKind[]) {
|
|
|
25
25
|
} catch {
|
|
26
26
|
// ignore storage errors
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
.then((res) => { if (!res.ok) console.warn('[settings] Failed to persist pinned kinds:', res.status) })
|
|
28
|
+
persistPreferences({ pinnedKinds: pinned })
|
|
30
29
|
.catch((err) => console.warn('[settings] Failed to persist pinned kinds:', err))
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -39,8 +38,7 @@ export function usePinnedKinds() {
|
|
|
39
38
|
|
|
40
39
|
// Sync from server (persisted settings survive port changes in desktop app)
|
|
41
40
|
useEffect(() => {
|
|
42
|
-
|
|
43
|
-
.then((res) => res.ok ? res.json() : null)
|
|
41
|
+
loadPreferences()
|
|
44
42
|
.then((data) => {
|
|
45
43
|
if (data?.pinnedKinds?.length && loadPinned().length === 0) {
|
|
46
44
|
setPinned(data.pinnedKinds)
|
package/src/utils/auditBadges.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function isBadgeWorthy(
|
|
|
30
30
|
/**
|
|
31
31
|
* buildAuditSeverityMap keys badge-worthy findings by the same resource key the
|
|
32
32
|
* backend stamps onto topology nodes (`node.data.auditKey`): `group|Kind|ns|name`,
|
|
33
|
-
*
|
|
33
|
+
* using the resource’s actual API group.
|
|
34
34
|
*/
|
|
35
35
|
export function buildAuditSeverityMap(
|
|
36
36
|
findings: AuditFinding[] | undefined,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { Topology } from '@skyhook-io/k8s-ui/types/core'
|
|
1
2
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
3
|
import {
|
|
3
4
|
initNavigationMap,
|
|
4
5
|
resetNavigationMap,
|
|
5
6
|
} from '@skyhook-io/k8s-ui/utils/navigation'
|
|
6
|
-
import { relatedResourcePath, resourcePath } from './navigation'
|
|
7
|
+
import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath } from './navigation'
|
|
7
8
|
|
|
8
9
|
afterEach(resetNavigationMap)
|
|
9
10
|
|
|
@@ -43,3 +44,19 @@ describe('resource navigation paths', () => {
|
|
|
43
44
|
).toBe('/workload/pods/default/batch')
|
|
44
45
|
})
|
|
45
46
|
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
describe('JobSet investigation navigation', () => {
|
|
50
|
+
it('preserves selected member and tab on the exact JobSet route', () => {
|
|
51
|
+
expect(relatedResourcePath({ kind: 'jobsets', namespace: 'training', name: 'wide', group: 'jobset.x-k8s.io', run: 'jobs/training/worker-240', tab: 'logs' })).toBe('/workload/jobsets/training/wide?apiGroup=jobset.x-k8s.io&run=jobs%2Ftraining%2Fworker-240&tab=logs')
|
|
52
|
+
})
|
|
53
|
+
it('leaves colliding JobSet kinds in their group-aware drawer', () => {
|
|
54
|
+
expect(relatedResourcePath({ kind: 'jobsets', namespace: 'training', name: 'wide', group: 'other.example' })).toBe('/resources/jobsets?resource=training%2Fwide&apiGroup=other.example')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
it('uses the Kubernetes kind for a Calico aggregate target', () => {
|
|
60
|
+
expect(getNetworkPolicyResourceTarget({nodes: [{id: 'policy', kind: 'CalicoNetworkPolicy', name: 'allow', status: 'healthy', data: {apiVersion: 'crd.projectcalico.org/v1', resourceKind: 'NetworkPolicy'}}], edges: []} as unknown as Topology))
|
|
61
|
+
.toEqual({kind: 'networkpolicies', group: 'crd.projectcalico.org'})
|
|
62
|
+
})
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
2
|
import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
3
|
+
import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
|
|
3
4
|
import type { SelectedResource, Topology } from '@skyhook-io/k8s-ui/types/core'
|
|
4
5
|
import type { SearchHit } from '../api/client'
|
|
5
6
|
|
|
@@ -48,7 +49,7 @@ export function getNetworkPolicyResourceTarget(topology: Topology | null): { kin
|
|
|
48
49
|
|
|
49
50
|
const group = networkPolicyGroup(node)
|
|
50
51
|
const target = {
|
|
51
|
-
kind: kindToPluralWithGroup(node
|
|
52
|
+
kind: kindToPluralWithGroup(topologyNodeResourceKind(node), group ?? ''),
|
|
52
53
|
...(group ? { group } : {}),
|
|
53
54
|
}
|
|
54
55
|
targets.set(`${target.kind}\u0000${target.group ?? ''}`, target)
|
|
@@ -67,12 +68,18 @@ export function getNetworkPolicyResourceTarget(topology: Topology | null): { kin
|
|
|
67
68
|
* positional and WorkloadViewRoute can parse it back. '_' is safe — it's not a
|
|
68
69
|
* valid DNS-1123 namespace label, so it can never collide with a real one.
|
|
69
70
|
*/
|
|
70
|
-
export
|
|
71
|
+
export type ResourceNavigationTarget = SelectedResource & { run?: string; tab?: string }
|
|
72
|
+
|
|
73
|
+
export function buildWorkloadPath(resource: ResourceNavigationTarget): string {
|
|
71
74
|
const kind = encodeURIComponent(resource.kind)
|
|
72
75
|
const namespace = encodeURIComponent(resource.namespace || '_')
|
|
73
76
|
const name = encodeURIComponent(resource.name)
|
|
74
77
|
const base = `/workload/${kind}/${namespace}/${name}`
|
|
75
|
-
|
|
78
|
+
const params = new URLSearchParams()
|
|
79
|
+
if (resource.group) params.set('apiGroup', resource.group)
|
|
80
|
+
if (resource.run) params.set('run', resource.run)
|
|
81
|
+
if (resource.tab) params.set('tab', resource.tab)
|
|
82
|
+
return params.size ? `${base}?${params}` : base
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
/**
|
|
@@ -101,9 +108,9 @@ export function resourcePath(resource: SelectedResource): string {
|
|
|
101
108
|
|
|
102
109
|
const FULLSCREEN_RESOURCE_KINDS = new Set(['pods', 'deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs', 'nodes'])
|
|
103
110
|
|
|
104
|
-
export function relatedResourcePath(resource:
|
|
111
|
+
export function relatedResourcePath(resource: ResourceNavigationTarget): string {
|
|
105
112
|
const apiKind = kindToPluralWithGroup(resource.kind, resource.group ?? '').toLowerCase()
|
|
106
|
-
if (FULLSCREEN_RESOURCE_KINDS.has(apiKind)) {
|
|
113
|
+
if (FULLSCREEN_RESOURCE_KINDS.has(apiKind) || (apiKind === 'jobsets' && resource.group === 'jobset.x-k8s.io')) {
|
|
107
114
|
return buildWorkloadPath({ ...resource, kind: apiKind })
|
|
108
115
|
}
|
|
109
116
|
return resourcePath(resource)
|