@skyhook-io/radar-app 1.12.2 → 1.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/App.tsx +34 -15
- package/src/api/client.images.test.ts +63 -0
- package/src/api/client.ts +208 -33
- package/src/api/client.yaml.test.ts +3 -3
- package/src/api/version-check.test.ts +78 -0
- package/src/components/CloudConnectFlow.tsx +46 -26
- package/src/components/CloudFunnelButton.tsx +166 -129
- package/src/components/ConnectionErrorView.test.tsx +21 -1
- package/src/components/ConnectionErrorView.tsx +7 -8
- package/src/components/applications/ApplicationsView.tsx +10 -9
- package/src/components/audit/AuditView.tsx +6 -3
- package/src/components/audit/UpgradeReadinessView.test.ts +26 -2
- package/src/components/audit/UpgradeReadinessView.tsx +19 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +14 -10
- package/src/components/gitops/GitOpsView.tsx +8 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +4 -3
- package/src/components/helm/OwnedResources.tsx +10 -2
- package/src/components/home/ClusterHealthCard.test.ts +31 -0
- package/src/components/home/ClusterHealthCard.tsx +60 -1
- package/src/components/home/HomeView.tsx +36 -11
- package/src/components/home/MCPSetupDialog.tsx +5 -4
- package/src/components/home/RadarVersionLine.test.tsx +145 -0
- package/src/components/home/RadarVersionLine.tsx +137 -0
- package/src/components/resources/PodFilesystemModal.tsx +54 -2
- package/src/components/resources/ResourcesView.tsx +31 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +13 -5
- package/src/components/settings/SettingsDialog.tsx +21 -4
- package/src/components/ui/ErrorBoundary.test.tsx +55 -0
- package/src/components/ui/ErrorBoundary.tsx +17 -2
- package/src/components/ui/UpdateNotification.test.tsx +49 -0
- package/src/components/ui/UpdateNotification.tsx +6 -2
- package/src/components/workload/WorkloadView.test.ts +60 -0
- package/src/components/workload/WorkloadView.tsx +270 -29
- package/src/contexts/CapabilitiesContext.test.tsx +29 -0
- package/src/contexts/CapabilitiesContext.tsx +7 -3
- package/src/utils/navigation.test.ts +45 -0
- package/src/utils/navigation.ts +5 -5
- package/src/utils/topology-selection.ts +3 -2
- package/src/utils/version.test.ts +37 -0
- package/src/utils/version.ts +56 -0
|
@@ -23,11 +23,17 @@ import {
|
|
|
23
23
|
getGitOpsResourceStatus,
|
|
24
24
|
isDiagnoseKind,
|
|
25
25
|
isRolloutKind,
|
|
26
|
+
canSetWorkloadImages,
|
|
27
|
+
isCoreBatchJob,
|
|
28
|
+
type ManagedImageSource,
|
|
29
|
+
type WorkloadImageTarget,
|
|
26
30
|
} from '@skyhook-io/k8s-ui'
|
|
27
31
|
import type { ServicePortRenderProps } from '@skyhook-io/k8s-ui/components/resources/renderers/ServiceRenderer'
|
|
28
|
-
import type { SelectedResource, ResourceRef, Relationships } from '../../types'
|
|
32
|
+
import type { SelectedResource, ResourceRef, Relationships, ResourceWithRelationships } from '../../types'
|
|
29
33
|
import {
|
|
30
34
|
kindToPlural,
|
|
35
|
+
kindToPluralWithGroup,
|
|
36
|
+
apiVersionToGroup,
|
|
31
37
|
pluralToKind,
|
|
32
38
|
relatedResourcePath,
|
|
33
39
|
buildWorkloadPath,
|
|
@@ -45,6 +51,9 @@ import {
|
|
|
45
51
|
useSuspendCronJob,
|
|
46
52
|
useResumeCronJob,
|
|
47
53
|
useRestartWorkload,
|
|
54
|
+
fetchWorkloadImages,
|
|
55
|
+
fetchResourceWithRelationships,
|
|
56
|
+
useSetWorkloadImages,
|
|
48
57
|
useWorkloadRevisions,
|
|
49
58
|
useRollbackWorkload,
|
|
50
59
|
useRolloutAction,
|
|
@@ -131,7 +140,6 @@ import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
|
|
|
131
140
|
import { useDesktopDownload } from '../../hooks/useDesktopDownload'
|
|
132
141
|
import { useCompareLauncher } from '../compare/useCompareLauncher'
|
|
133
142
|
import { useDiagnoseCustomization } from '../../context/DiagnoseCustomization'
|
|
134
|
-
import { apiVersionToGroup } from '../../utils/navigation'
|
|
135
143
|
|
|
136
144
|
type TabType = WorkloadTabType
|
|
137
145
|
const BATCH_EXECUTION_KINDS = new Set([
|
|
@@ -277,6 +285,17 @@ interface WorkloadViewProps {
|
|
|
277
285
|
pushTabHistory?: boolean
|
|
278
286
|
}
|
|
279
287
|
|
|
288
|
+
interface ImageTargetOwnershipContext {
|
|
289
|
+
root: {
|
|
290
|
+
resource: string
|
|
291
|
+
namespace: string
|
|
292
|
+
name: string
|
|
293
|
+
}
|
|
294
|
+
target: WorkloadImageTarget
|
|
295
|
+
response: ResourceWithRelationships<Record<string, unknown>>
|
|
296
|
+
inheritedResponse?: ResourceWithRelationships<Record<string, unknown>>
|
|
297
|
+
}
|
|
298
|
+
|
|
280
299
|
function useActionsBarProps(
|
|
281
300
|
kind: string,
|
|
282
301
|
namespace: string,
|
|
@@ -285,11 +304,13 @@ function useActionsBarProps(
|
|
|
285
304
|
cascadeEnabled: boolean,
|
|
286
305
|
) {
|
|
287
306
|
const { showCopied } = useToast()
|
|
307
|
+
const { features } = useCapabilitiesContext()
|
|
288
308
|
const openTerminal = useOpenTerminal()
|
|
289
309
|
const openLogs = useOpenLogs()
|
|
290
310
|
const openWorkloadLogs = useOpenWorkloadLogs()
|
|
291
311
|
const openNodeTerminal = useOpenNodeTerminal()
|
|
292
|
-
const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
|
|
312
|
+
const { canExec, canViewLogs, canPortForward, workloadWrites, workloadWritesPending } = useNamespacedCapabilities(namespace)
|
|
313
|
+
const queryClient = useQueryClient()
|
|
293
314
|
// Live forward when local+RBAC; otherwise (in-cluster/Cloud) still surface the
|
|
294
315
|
// copy-paste kubectl command. The button picks live vs. copy by deployment mode.
|
|
295
316
|
const isLocal = useIsLocalDeployment()
|
|
@@ -297,20 +318,37 @@ function useActionsBarProps(
|
|
|
297
318
|
|
|
298
319
|
const deleteMutation = useDeleteResource()
|
|
299
320
|
const restartWorkloadMutation = useRestartWorkload()
|
|
321
|
+
const setWorkloadImagesMutation = useSetWorkloadImages()
|
|
300
322
|
const rollbackMutation = useRollbackWorkload()
|
|
301
|
-
|
|
323
|
+
// Only the revision dialog drives this one, and it explains a failed promote in place
|
|
324
|
+
// with the retry — the generic action toast would be a second, vaguer voice.
|
|
325
|
+
const rolloutDialogPromote = useRolloutAction({ reportErrors: false })
|
|
302
326
|
const triggerCronJobMutation = useTriggerCronJob()
|
|
303
327
|
const suspendCronJobMutation = useSuspendCronJob()
|
|
304
328
|
const resumeCronJobMutation = useResumeCronJob()
|
|
305
329
|
|
|
306
|
-
const
|
|
307
|
-
const isRollout =
|
|
308
|
-
const
|
|
330
|
+
const isRolloutShape = isRolloutKind(kind)
|
|
331
|
+
const isRollout = isRolloutShape && group === 'argoproj.io'
|
|
332
|
+
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind.toLowerCase()) || isRollout
|
|
333
|
+
const { data: rolloutCapabilities, isPending: rolloutCapabilitiesPending } = useRolloutCapabilities(namespace, name, isRollout)
|
|
309
334
|
// Restart and Rollback are the generic workload buttons, so a Rollout has to
|
|
310
335
|
// withhold the callback the way promote-full does. Permissive until the probe
|
|
311
336
|
// answers — withholding while it loads would flicker the shared buttons.
|
|
312
337
|
const rolloutAllows = (verb: 'restart' | 'rollback') =>
|
|
313
338
|
!isRollout || !rolloutCapabilities || rolloutCapabilities[verb]
|
|
339
|
+
const imageUpdatesSupported = features?.workloadImages === true
|
|
340
|
+
const canSetImages = isRolloutShape
|
|
341
|
+
? imageUpdatesSupported && isRollout && !rolloutCapabilitiesPending && rolloutCapabilities?.setImage === true
|
|
342
|
+
: !workloadWritesPending && canSetWorkloadImages({ name: kind, group }, workloadWrites, imageUpdatesSupported)
|
|
343
|
+
const loadImages = useCallback(
|
|
344
|
+
(params: { kind: string; namespace: string; name: string }) =>
|
|
345
|
+
queryClient.fetchQuery({
|
|
346
|
+
queryKey: ['workload-images', params.kind, params.namespace, params.name],
|
|
347
|
+
queryFn: () => fetchWorkloadImages(params.kind, params.namespace, params.name),
|
|
348
|
+
staleTime: 0,
|
|
349
|
+
}),
|
|
350
|
+
[queryClient],
|
|
351
|
+
)
|
|
314
352
|
const {
|
|
315
353
|
data: revisionsList,
|
|
316
354
|
isLoading: revisionsLoading,
|
|
@@ -381,6 +419,12 @@ function useActionsBarProps(
|
|
|
381
419
|
restartWorkloadMutation.mutate(params)
|
|
382
420
|
: undefined,
|
|
383
421
|
isRestarting: restartWorkloadMutation.isPending,
|
|
422
|
+
onLoadImages: canSetImages ? loadImages : undefined,
|
|
423
|
+
onSetImages: canSetImages
|
|
424
|
+
? (params: Parameters<typeof setWorkloadImagesMutation.mutateAsync>[0]) =>
|
|
425
|
+
setWorkloadImagesMutation.mutateAsync(params)
|
|
426
|
+
: undefined,
|
|
427
|
+
isSettingImages: setWorkloadImagesMutation.isPending,
|
|
384
428
|
revisions: revisionsList,
|
|
385
429
|
revisionsLoading,
|
|
386
430
|
revisionsError: revisionsError ?? null,
|
|
@@ -396,7 +440,7 @@ function useActionsBarProps(
|
|
|
396
440
|
// mutateAsync, not mutate: the revision dialog awaits this before closing.
|
|
397
441
|
onRolloutPromoteFull: rolloutCapabilities?.promoteFull
|
|
398
442
|
? (params: { namespace: string; name: string }) =>
|
|
399
|
-
|
|
443
|
+
rolloutDialogPromote.mutateAsync({ action: 'promote-full', ...params })
|
|
400
444
|
: undefined,
|
|
401
445
|
onTriggerCronJob: (params: Parameters<typeof triggerCronJobMutation.mutate>[0]) =>
|
|
402
446
|
triggerCronJobMutation.mutate(params),
|
|
@@ -453,8 +497,11 @@ export function WorkloadView({
|
|
|
453
497
|
...rest
|
|
454
498
|
}: WorkloadViewProps) {
|
|
455
499
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
456
|
-
const apiKind =
|
|
500
|
+
const apiKind = kindToPluralWithGroup(kindProp, rest.group ?? '')
|
|
457
501
|
const queryClient = useQueryClient()
|
|
502
|
+
const [imageTargetOwnership, setImageTargetOwnership] =
|
|
503
|
+
useState<ImageTargetOwnershipContext | null>(null)
|
|
504
|
+
const imageOwnershipRequestRef = useRef(0)
|
|
458
505
|
|
|
459
506
|
// Tab state from URL query param — migrate legacy tab names
|
|
460
507
|
const rawTab = searchParams.get('tab')
|
|
@@ -492,7 +539,8 @@ export function WorkloadView({
|
|
|
492
539
|
)
|
|
493
540
|
|
|
494
541
|
const batchKind = pluralToKind(apiKind)
|
|
495
|
-
const batchExecution = BATCH_EXECUTION_KINDS.has(batchKind)
|
|
542
|
+
const batchExecution = BATCH_EXECUTION_KINDS.has(batchKind) &&
|
|
543
|
+
(batchKind !== 'Job' || isCoreBatchJob(apiKind, rest.group))
|
|
496
544
|
const batchRunsQuery = useWorkloadRuns(apiKind, namespace, name, expanded && batchExecution, {
|
|
497
545
|
refetchActive: true,
|
|
498
546
|
clusterScoped: batchKind === 'ClusterWorkflowTemplate',
|
|
@@ -535,6 +583,9 @@ export function WorkloadView({
|
|
|
535
583
|
queryClient.refetchQueries({
|
|
536
584
|
queryKey: ['workload-runs', apiKind, namespace, name],
|
|
537
585
|
}),
|
|
586
|
+
queryClient.refetchQueries({
|
|
587
|
+
queryKey: ['workload-pods', apiKind, namespace, name],
|
|
588
|
+
}),
|
|
538
589
|
])
|
|
539
590
|
}, [apiKind, name, namespace, queryClient, refetchResource])
|
|
540
591
|
const podWorkloadOwner = useMemo(
|
|
@@ -551,6 +602,16 @@ export function WorkloadView({
|
|
|
551
602
|
)
|
|
552
603
|
const certificateInfo = resourceResponse?.certificateInfo
|
|
553
604
|
const hpaDiagnosis = resourceResponse?.hpaDiagnosis
|
|
605
|
+
const activeImageTargetOwnership = useMemo(
|
|
606
|
+
() =>
|
|
607
|
+
imageTargetOwnership &&
|
|
608
|
+
imageTargetOwnership.root.resource.toLowerCase() === apiKind.toLowerCase() &&
|
|
609
|
+
imageTargetOwnership.root.namespace === namespace &&
|
|
610
|
+
imageTargetOwnership.root.name === name
|
|
611
|
+
? imageTargetOwnership
|
|
612
|
+
: null,
|
|
613
|
+
[apiKind, imageTargetOwnership, name, namespace],
|
|
614
|
+
)
|
|
554
615
|
const relationshipGitopsOwner = useMemo(
|
|
555
616
|
() => gitOpsOwnerFromRelationships(relationships),
|
|
556
617
|
[relationships],
|
|
@@ -566,7 +627,7 @@ export function WorkloadView({
|
|
|
566
627
|
[relationships, relationshipGitopsOwner, apiKind, namespace, name, rest.group],
|
|
567
628
|
)
|
|
568
629
|
const inheritedGitOpsResponse = useResourceWithRelationships<any>(
|
|
569
|
-
inheritedGitOpsLookupRef ?
|
|
630
|
+
inheritedGitOpsLookupRef ? kindToPluralWithGroup(inheritedGitOpsLookupRef.kind, inheritedGitOpsLookupRef.group ?? '') : '',
|
|
570
631
|
inheritedGitOpsLookupRef?.namespace ?? '',
|
|
571
632
|
inheritedGitOpsLookupRef?.name ?? '',
|
|
572
633
|
inheritedGitOpsLookupRef?.group,
|
|
@@ -600,7 +661,30 @@ export function WorkloadView({
|
|
|
600
661
|
const helmSourceResource = relationshipHelmOwner
|
|
601
662
|
? resource
|
|
602
663
|
: inheritedGitOpsResponse.data?.resource
|
|
603
|
-
const
|
|
664
|
+
const targetRelationshipGitopsOwner = useMemo(
|
|
665
|
+
() => gitOpsOwnerFromRelationships(activeImageTargetOwnership?.response.relationships),
|
|
666
|
+
[activeImageTargetOwnership?.response.relationships],
|
|
667
|
+
)
|
|
668
|
+
const targetInheritedGitopsOwner = useMemo(
|
|
669
|
+
() =>
|
|
670
|
+
gitOpsOwnerFromRelationships(
|
|
671
|
+
activeImageTargetOwnership?.inheritedResponse?.relationships,
|
|
672
|
+
),
|
|
673
|
+
[activeImageTargetOwnership?.inheritedResponse?.relationships],
|
|
674
|
+
)
|
|
675
|
+
const targetRawGitopsOwner = targetRelationshipGitopsOwner ?? targetInheritedGitopsOwner
|
|
676
|
+
const targetRelationshipHelmOwner = nativeHelmOwnerFromRelationships(
|
|
677
|
+
activeImageTargetOwnership?.response.relationships,
|
|
678
|
+
activeImageTargetOwnership?.target.namespace ?? namespace,
|
|
679
|
+
)
|
|
680
|
+
const targetInheritedHelmOwner = nativeHelmOwnerFromRelationships(
|
|
681
|
+
activeImageTargetOwnership?.inheritedResponse?.relationships,
|
|
682
|
+
activeImageTargetOwnership?.target.namespace ?? namespace,
|
|
683
|
+
)
|
|
684
|
+
const targetHelmOwner = targetRelationshipHelmOwner ?? targetInheritedHelmOwner
|
|
685
|
+
const shouldResolveArgoOwner =
|
|
686
|
+
(rawGitopsOwner?.tool === 'argocd' && !rawGitopsOwner.namespace) ||
|
|
687
|
+
(targetRawGitopsOwner?.tool === 'argocd' && !targetRawGitopsOwner.namespace)
|
|
604
688
|
const { data: argoApplications } = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
605
689
|
enabled: shouldResolveArgoOwner,
|
|
606
690
|
})
|
|
@@ -608,6 +692,10 @@ export function WorkloadView({
|
|
|
608
692
|
() => resolveGitOpsOwner(rawGitopsOwner, argoApplications),
|
|
609
693
|
[rawGitopsOwner, argoApplications],
|
|
610
694
|
)
|
|
695
|
+
const targetGitopsOwner = useMemo(
|
|
696
|
+
() => resolveGitOpsOwner(targetRawGitopsOwner, argoApplications),
|
|
697
|
+
[argoApplications, targetRawGitopsOwner],
|
|
698
|
+
)
|
|
611
699
|
const gitopsOwnerGroup = gitopsOwner ? gitOpsOwnerGroup(gitopsOwner) : ''
|
|
612
700
|
const shouldFetchGitOpsOwner = Boolean(gitopsOwner?.namespace)
|
|
613
701
|
const gitopsOwnerQuery = useResource<any>(
|
|
@@ -616,6 +704,17 @@ export function WorkloadView({
|
|
|
616
704
|
gitopsOwner?.name ?? '',
|
|
617
705
|
gitopsOwnerGroup,
|
|
618
706
|
)
|
|
707
|
+
const targetGitopsOwnerGroup = targetGitopsOwner ? gitOpsOwnerGroup(targetGitopsOwner) : ''
|
|
708
|
+
const shouldFetchTargetGitOpsOwner = Boolean(
|
|
709
|
+
activeImageTargetOwnership && targetGitopsOwner?.namespace,
|
|
710
|
+
)
|
|
711
|
+
const targetGitopsOwnerQuery = useResource<Record<string, unknown>>(
|
|
712
|
+
shouldFetchTargetGitOpsOwner ? targetGitopsOwner!.kind : '',
|
|
713
|
+
targetGitopsOwner?.namespace ?? '',
|
|
714
|
+
targetGitopsOwner?.name ?? '',
|
|
715
|
+
targetGitopsOwnerGroup,
|
|
716
|
+
{ enabled: shouldFetchTargetGitOpsOwner },
|
|
717
|
+
)
|
|
619
718
|
const gitOpsOwnerStatus = useMemo(
|
|
620
719
|
() => deriveGitOpsOwnerStatus(gitopsOwner, gitopsOwnerQuery.data),
|
|
621
720
|
[gitopsOwner, gitopsOwnerQuery.data],
|
|
@@ -768,11 +867,6 @@ export function WorkloadView({
|
|
|
768
867
|
// pass one.
|
|
769
868
|
group: effectiveGroup,
|
|
770
869
|
})
|
|
771
|
-
const actionsBarProps = useMemo(
|
|
772
|
-
() => ({ ...baseActionsBarProps, onCompareTo, onCompareAcrossClusters }),
|
|
773
|
-
[baseActionsBarProps, onCompareTo, onCompareAcrossClusters],
|
|
774
|
-
)
|
|
775
|
-
|
|
776
870
|
const handleUpdateResource = useCallback(
|
|
777
871
|
async (params: Parameters<typeof updateResource.mutateAsync>[0]) => {
|
|
778
872
|
await updateResource.mutateAsync(params)
|
|
@@ -823,6 +917,136 @@ export function WorkloadView({
|
|
|
823
917
|
},
|
|
824
918
|
[navigateRouter, searchParams],
|
|
825
919
|
)
|
|
920
|
+
const loadImagesWithTargetOwnership = useCallback(
|
|
921
|
+
async (params: { kind: string; namespace: string; name: string }) => {
|
|
922
|
+
const request = ++imageOwnershipRequestRef.current
|
|
923
|
+
const inventory = await baseActionsBarProps.onLoadImages!(params)
|
|
924
|
+
const targetDiffers =
|
|
925
|
+
inventory.target.resource.toLowerCase() !== params.kind.toLowerCase() ||
|
|
926
|
+
inventory.target.namespace !== params.namespace ||
|
|
927
|
+
inventory.target.name !== params.name
|
|
928
|
+
if (!targetDiffers) {
|
|
929
|
+
if (request === imageOwnershipRequestRef.current) {
|
|
930
|
+
setImageTargetOwnership(null)
|
|
931
|
+
}
|
|
932
|
+
return inventory
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const fetchRelationships = (
|
|
936
|
+
resource: string,
|
|
937
|
+
targetNamespace: string,
|
|
938
|
+
targetName: string,
|
|
939
|
+
group?: string,
|
|
940
|
+
) =>
|
|
941
|
+
queryClient.fetchQuery({
|
|
942
|
+
queryKey: ['resource', resource, targetNamespace, targetName, group],
|
|
943
|
+
queryFn: () =>
|
|
944
|
+
fetchResourceWithRelationships<Record<string, unknown>>(
|
|
945
|
+
resource,
|
|
946
|
+
targetNamespace,
|
|
947
|
+
targetName,
|
|
948
|
+
group,
|
|
949
|
+
),
|
|
950
|
+
staleTime: 0,
|
|
951
|
+
})
|
|
952
|
+
const response = await fetchRelationships(
|
|
953
|
+
inventory.target.resource,
|
|
954
|
+
inventory.target.namespace,
|
|
955
|
+
inventory.target.name,
|
|
956
|
+
inventory.target.group,
|
|
957
|
+
)
|
|
958
|
+
const inheritedRef = findInheritedGitOpsLookupRef(
|
|
959
|
+
response.relationships,
|
|
960
|
+
gitOpsOwnerFromRelationships(response.relationships),
|
|
961
|
+
{
|
|
962
|
+
kind: inventory.target.kind,
|
|
963
|
+
namespace: inventory.target.namespace,
|
|
964
|
+
name: inventory.target.name,
|
|
965
|
+
group: inventory.target.group,
|
|
966
|
+
},
|
|
967
|
+
)
|
|
968
|
+
const inheritedResponse = inheritedRef
|
|
969
|
+
? await fetchRelationships(
|
|
970
|
+
kindToPluralWithGroup(inheritedRef.kind, inheritedRef.group ?? ''),
|
|
971
|
+
inheritedRef.namespace,
|
|
972
|
+
inheritedRef.name,
|
|
973
|
+
inheritedRef.group,
|
|
974
|
+
)
|
|
975
|
+
: undefined
|
|
976
|
+
if (request === imageOwnershipRequestRef.current) {
|
|
977
|
+
setImageTargetOwnership({
|
|
978
|
+
root: {
|
|
979
|
+
resource: params.kind,
|
|
980
|
+
namespace: params.namespace,
|
|
981
|
+
name: params.name,
|
|
982
|
+
},
|
|
983
|
+
target: inventory.target,
|
|
984
|
+
response,
|
|
985
|
+
inheritedResponse,
|
|
986
|
+
})
|
|
987
|
+
}
|
|
988
|
+
return inventory
|
|
989
|
+
},
|
|
990
|
+
[baseActionsBarProps.onLoadImages, queryClient, setImageTargetOwnership],
|
|
991
|
+
)
|
|
992
|
+
const imageGitopsOwner = activeImageTargetOwnership ? targetGitopsOwner : gitopsOwner
|
|
993
|
+
const imageHelmOwner = activeImageTargetOwnership ? targetHelmOwner : helmOwner
|
|
994
|
+
const imageGitopsOwnerData = activeImageTargetOwnership
|
|
995
|
+
? targetGitopsOwnerQuery.data
|
|
996
|
+
: gitopsOwnerQuery.data
|
|
997
|
+
const managedImageSources = useMemo<ManagedImageSource[] | undefined>(() => {
|
|
998
|
+
if (!activeImageTargetOwnership && inheritedGitOpsLookupRef && (inheritedGitOpsResponse.isPending || inheritedGitOpsResponse.isError)) {
|
|
999
|
+
return undefined
|
|
1000
|
+
}
|
|
1001
|
+
const sources: ManagedImageSource[] = []
|
|
1002
|
+
if (imageGitopsOwner) {
|
|
1003
|
+
sources.push({
|
|
1004
|
+
type: 'GitOps',
|
|
1005
|
+
label: imageGitopsOwner.namespace
|
|
1006
|
+
? `${imageGitopsOwner.namespace}/${imageGitopsOwner.name}`
|
|
1007
|
+
: imageGitopsOwner.name,
|
|
1008
|
+
onOpen: imageGitopsOwnerData
|
|
1009
|
+
? () => handleOpenGitOpsResource(imageGitopsOwner)
|
|
1010
|
+
: undefined,
|
|
1011
|
+
})
|
|
1012
|
+
}
|
|
1013
|
+
if (imageHelmOwner) {
|
|
1014
|
+
sources.push({
|
|
1015
|
+
type: 'Helm',
|
|
1016
|
+
label: `${imageHelmOwner.namespace}/${imageHelmOwner.name}`,
|
|
1017
|
+
onOpen: () => handleOpenHelmRelease(imageHelmOwner),
|
|
1018
|
+
})
|
|
1019
|
+
}
|
|
1020
|
+
return sources
|
|
1021
|
+
}, [
|
|
1022
|
+
handleOpenGitOpsResource,
|
|
1023
|
+
handleOpenHelmRelease,
|
|
1024
|
+
activeImageTargetOwnership,
|
|
1025
|
+
imageGitopsOwner,
|
|
1026
|
+
imageGitopsOwnerData,
|
|
1027
|
+
imageHelmOwner,
|
|
1028
|
+
inheritedGitOpsLookupRef,
|
|
1029
|
+
inheritedGitOpsResponse.isError,
|
|
1030
|
+
inheritedGitOpsResponse.isPending,
|
|
1031
|
+
])
|
|
1032
|
+
const actionsBarProps = useMemo(
|
|
1033
|
+
() => ({
|
|
1034
|
+
...baseActionsBarProps,
|
|
1035
|
+
onLoadImages: baseActionsBarProps.onLoadImages
|
|
1036
|
+
? loadImagesWithTargetOwnership
|
|
1037
|
+
: undefined,
|
|
1038
|
+
onCompareTo,
|
|
1039
|
+
onCompareAcrossClusters,
|
|
1040
|
+
managedImageSources,
|
|
1041
|
+
}),
|
|
1042
|
+
[
|
|
1043
|
+
baseActionsBarProps,
|
|
1044
|
+
loadImagesWithTargetOwnership,
|
|
1045
|
+
managedImageSources,
|
|
1046
|
+
onCompareTo,
|
|
1047
|
+
onCompareAcrossClusters,
|
|
1048
|
+
],
|
|
1049
|
+
)
|
|
826
1050
|
const handleOpenApplication = useCallback(
|
|
827
1051
|
(appKey: string) => {
|
|
828
1052
|
const params = new URLSearchParams()
|
|
@@ -854,7 +1078,7 @@ export function WorkloadView({
|
|
|
854
1078
|
[],
|
|
855
1079
|
)
|
|
856
1080
|
|
|
857
|
-
const supportsWorkloadPods = ['deployments', 'statefulsets', 'daemonsets'].includes(apiKind)
|
|
1081
|
+
const supportsWorkloadPods = ['deployments', 'statefulsets', 'daemonsets', 'rollouts'].includes(apiKind)
|
|
858
1082
|
const workloadPodsQuery = useWorkloadPods(supportsWorkloadPods ? apiKind : '', namespace, name)
|
|
859
1083
|
const workloadAwaitsCapacity =
|
|
860
1084
|
karpenter?.state === 'available' &&
|
|
@@ -862,7 +1086,7 @@ export function WorkloadView({
|
|
|
862
1086
|
const servingRefs = useMemo(() => collectServingRefs(relationships), [relationships])
|
|
863
1087
|
const servingQueries = useQueries({
|
|
864
1088
|
queries: servingRefs.map((ref) => {
|
|
865
|
-
const pluralKind =
|
|
1089
|
+
const pluralKind = kindToPluralWithGroup(ref.kind, ref.group ?? '')
|
|
866
1090
|
const ns = ref.namespace || '_'
|
|
867
1091
|
const params = new URLSearchParams()
|
|
868
1092
|
if (ref.group) params.set('group', ref.group)
|
|
@@ -953,12 +1177,15 @@ export function WorkloadView({
|
|
|
953
1177
|
renderLogsTab={(props) => (
|
|
954
1178
|
<LogsTabContent
|
|
955
1179
|
{...props}
|
|
1180
|
+
group={effectiveGroup}
|
|
956
1181
|
selectedRunKey={selectedRunKey}
|
|
957
1182
|
onSelectRun={handleSelectedRunChange}
|
|
958
1183
|
/>
|
|
959
1184
|
)}
|
|
960
1185
|
renderExpandedOverview={({ kind: k, apiKind, namespace: ns, name: n, resource: res }) =>
|
|
961
|
-
BATCH_EXECUTION_KINDS.has(k) &&
|
|
1186
|
+
BATCH_EXECUTION_KINDS.has(k) &&
|
|
1187
|
+
(k !== 'Job' || isCoreBatchJob(apiKind, effectiveGroup)) &&
|
|
1188
|
+
res ? (
|
|
962
1189
|
<BatchExecutionFullscreen
|
|
963
1190
|
kind={k}
|
|
964
1191
|
apiKind={apiKind}
|
|
@@ -1054,7 +1281,7 @@ export function WorkloadView({
|
|
|
1054
1281
|
rest.onNavigateToResource
|
|
1055
1282
|
? (ref) =>
|
|
1056
1283
|
rest.onNavigateToResource?.({
|
|
1057
|
-
kind:
|
|
1284
|
+
kind: kindToPluralWithGroup(ref.kind, ref.group ?? ''),
|
|
1058
1285
|
namespace: ref.namespace ?? '',
|
|
1059
1286
|
name: ref.name,
|
|
1060
1287
|
group: ref.group ?? '',
|
|
@@ -1082,11 +1309,12 @@ export function WorkloadView({
|
|
|
1082
1309
|
initialYaml={duplicateYaml}
|
|
1083
1310
|
title="Duplicate Resource"
|
|
1084
1311
|
onCreated={(result) => {
|
|
1312
|
+
const group = apiVersionToGroup(result.apiVersion)
|
|
1085
1313
|
rest.onNavigateToResource?.({
|
|
1086
|
-
kind:
|
|
1314
|
+
kind: kindToPluralWithGroup(result.kind, group),
|
|
1087
1315
|
namespace: result.namespace,
|
|
1088
1316
|
name: result.name,
|
|
1089
|
-
group
|
|
1317
|
+
group,
|
|
1090
1318
|
})
|
|
1091
1319
|
}}
|
|
1092
1320
|
/>
|
|
@@ -1126,7 +1354,7 @@ function resolveGitOpsOwner(
|
|
|
1126
1354
|
return namespace ? { ...owner, namespace } : owner
|
|
1127
1355
|
}
|
|
1128
1356
|
|
|
1129
|
-
function findInheritedGitOpsLookupRef(
|
|
1357
|
+
export function findInheritedGitOpsLookupRef(
|
|
1130
1358
|
relationships: Relationships | undefined,
|
|
1131
1359
|
directOwner: GitOpsOwnerRef | null,
|
|
1132
1360
|
current: ResourceRef,
|
|
@@ -1237,7 +1465,8 @@ function nativeHelmOwnerFromRelationships(
|
|
|
1237
1465
|
|
|
1238
1466
|
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
1239
1467
|
return (
|
|
1240
|
-
|
|
1468
|
+
kindToPluralWithGroup(ref.kind, ref.group ?? '') ===
|
|
1469
|
+
kindToPluralWithGroup(current.kind, current.group ?? '') &&
|
|
1241
1470
|
ref.namespace === current.namespace &&
|
|
1242
1471
|
ref.name === current.name &&
|
|
1243
1472
|
(ref.group ?? '') === (current.group ?? '')
|
|
@@ -1321,6 +1550,7 @@ const SCHEDULED_LOG_KINDS = new Set([
|
|
|
1321
1550
|
function LogsTabContent({
|
|
1322
1551
|
kind,
|
|
1323
1552
|
apiKind,
|
|
1553
|
+
group,
|
|
1324
1554
|
namespace,
|
|
1325
1555
|
name,
|
|
1326
1556
|
resource,
|
|
@@ -1334,6 +1564,7 @@ function LogsTabContent({
|
|
|
1334
1564
|
}: {
|
|
1335
1565
|
kind: string
|
|
1336
1566
|
apiKind: string
|
|
1567
|
+
group?: string
|
|
1337
1568
|
namespace: string
|
|
1338
1569
|
name: string
|
|
1339
1570
|
resource: any
|
|
@@ -1360,7 +1591,7 @@ function LogsTabContent({
|
|
|
1360
1591
|
}
|
|
1361
1592
|
|
|
1362
1593
|
// Workload kinds with stable pod selectors use the aggregated workload logs viewer
|
|
1363
|
-
if (WORKLOAD_LOG_KINDS.has(kind)) {
|
|
1594
|
+
if (WORKLOAD_LOG_KINDS.has(kind) && (kind !== 'Job' || isCoreBatchJob(apiKind, group))) {
|
|
1364
1595
|
return (
|
|
1365
1596
|
<div className="h-full">
|
|
1366
1597
|
<WorkloadLogsViewer
|
|
@@ -1986,7 +2217,17 @@ function DiagnoseTabContent({
|
|
|
1986
2217
|
runNonce={runNonce}
|
|
1987
2218
|
testedAt={testedAt}
|
|
1988
2219
|
clusterChangedSinceTest={clusterChanged}
|
|
1989
|
-
onNavigateToResource={
|
|
2220
|
+
onNavigateToResource={
|
|
2221
|
+
onNavigate
|
|
2222
|
+
? (ref) =>
|
|
2223
|
+
onNavigate({
|
|
2224
|
+
kind: kindToPluralWithGroup(ref.kind, ref.group ?? ''),
|
|
2225
|
+
namespace: ref.namespace ?? '',
|
|
2226
|
+
name: ref.name,
|
|
2227
|
+
group: ref.group ?? '',
|
|
2228
|
+
})
|
|
2229
|
+
: undefined
|
|
2230
|
+
}
|
|
1990
2231
|
/>
|
|
1991
2232
|
<InClusterConsentDialog
|
|
1992
2233
|
open={pendingRunPath !== null}
|
|
@@ -2248,7 +2489,7 @@ function RelatedResourceYaml({
|
|
|
2248
2489
|
target: { kind: string; namespace: string; name: string; group?: string }
|
|
2249
2490
|
}) {
|
|
2250
2491
|
const { data, isLoading, error } = useResource<any>(
|
|
2251
|
-
|
|
2492
|
+
kindToPluralWithGroup(target.kind, target.group ?? ''),
|
|
2252
2493
|
target.namespace,
|
|
2253
2494
|
target.name,
|
|
2254
2495
|
target.group,
|
|
@@ -2264,7 +2505,7 @@ function RelatedResourceYaml({
|
|
|
2264
2505
|
return (
|
|
2265
2506
|
<EditableYamlView
|
|
2266
2507
|
resource={{
|
|
2267
|
-
kind:
|
|
2508
|
+
kind: kindToPluralWithGroup(target.kind, target.group ?? ''),
|
|
2268
2509
|
namespace: target.namespace,
|
|
2269
2510
|
name: target.name,
|
|
2270
2511
|
group: target.group,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
vi.mock('../api/client', () => ({
|
|
5
|
+
useCapabilities: () => ({ data: undefined, error: null }),
|
|
6
|
+
useNamespaceCapabilities: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
import { CapabilitiesProvider, useCapabilitiesContext } from './CapabilitiesContext'
|
|
10
|
+
|
|
11
|
+
function LocalTerminalCapability() {
|
|
12
|
+
return <span>{String(useCapabilitiesContext().localTerminal)}</span>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('CapabilitiesContext local terminal defaults', () => {
|
|
16
|
+
it('fails closed when a standalone surface has no provider', () => {
|
|
17
|
+
expect(renderToStaticMarkup(<LocalTerminalCapability />)).toBe('<span>false</span>')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('preserves the local loading default inside CapabilitiesProvider', () => {
|
|
21
|
+
const markup = renderToStaticMarkup(
|
|
22
|
+
<CapabilitiesProvider>
|
|
23
|
+
<LocalTerminalCapability />
|
|
24
|
+
</CapabilitiesProvider>,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
expect(markup).toBe('<span>true</span>')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -46,7 +46,10 @@ const restrictedCapabilities: Capabilities = {
|
|
|
46
46
|
deployment: { mode: 'local' },
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const CapabilitiesContext = createContext<Capabilities>(
|
|
49
|
+
const CapabilitiesContext = createContext<Capabilities>({
|
|
50
|
+
...defaultCapabilities,
|
|
51
|
+
localTerminal: false,
|
|
52
|
+
})
|
|
50
53
|
|
|
51
54
|
export function CapabilitiesProvider({ children }: { children: ReactNode }) {
|
|
52
55
|
const { data: capabilities, error } = useCapabilities()
|
|
@@ -145,7 +148,7 @@ export function useHasLimitedAccess(): boolean {
|
|
|
145
148
|
// capability check; callers use global capability values until it resolves.
|
|
146
149
|
export function useNamespacedCapabilities(namespace: string | undefined) {
|
|
147
150
|
const globalCaps = useContext(CapabilitiesContext)
|
|
148
|
-
const { data: nsCaps, error } = useNamespaceCapabilities(namespace, globalCaps)
|
|
151
|
+
const { data: nsCaps, error, isPending } = useNamespaceCapabilities(namespace, globalCaps)
|
|
149
152
|
|
|
150
153
|
if (error) {
|
|
151
154
|
console.warn(`Failed to fetch namespace capabilities for ${namespace}, using global:`, error)
|
|
@@ -156,5 +159,6 @@ export function useNamespacedCapabilities(namespace: string | undefined) {
|
|
|
156
159
|
canViewLogs: nsCaps?.logs ?? globalCaps.logs,
|
|
157
160
|
canPortForward: nsCaps?.portForward ?? globalCaps.portForward,
|
|
158
161
|
workloadWrites: nsCaps?.workloadWrites ?? globalCaps.workloadWrites,
|
|
159
|
-
|
|
162
|
+
workloadWritesPending: Boolean(namespace && isPending),
|
|
163
|
+
}), [globalCaps.exec, globalCaps.logs, globalCaps.portForward, globalCaps.workloadWrites, isPending, namespace, nsCaps])
|
|
160
164
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
initNavigationMap,
|
|
4
|
+
resetNavigationMap,
|
|
5
|
+
} from '@skyhook-io/k8s-ui/utils/navigation'
|
|
6
|
+
import { relatedResourcePath, resourcePath } from './navigation'
|
|
7
|
+
|
|
8
|
+
afterEach(resetNavigationMap)
|
|
9
|
+
|
|
10
|
+
describe('resource navigation paths', () => {
|
|
11
|
+
it('routes the native PodGroup by its scheduling API group', () => {
|
|
12
|
+
initNavigationMap([
|
|
13
|
+
{
|
|
14
|
+
group: 'scheduling.k8s.io',
|
|
15
|
+
version: 'v1beta1',
|
|
16
|
+
kind: 'PodGroup',
|
|
17
|
+
name: 'podgroups',
|
|
18
|
+
namespaced: true,
|
|
19
|
+
isCrd: false,
|
|
20
|
+
verbs: ['get', 'list'],
|
|
21
|
+
},
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
resourcePath({
|
|
26
|
+
kind: 'PodGroup',
|
|
27
|
+
group: 'scheduling.k8s.io',
|
|
28
|
+
namespace: 'default',
|
|
29
|
+
name: 'batch',
|
|
30
|
+
}),
|
|
31
|
+
).toBe(
|
|
32
|
+
'/resources/podgroups?resource=default%2Fbatch&apiGroup=scheduling.k8s.io',
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('preserves Radar virtual PodGroup navigation without an API group', () => {
|
|
37
|
+
expect(
|
|
38
|
+
relatedResourcePath({
|
|
39
|
+
kind: 'PodGroup',
|
|
40
|
+
namespace: 'default',
|
|
41
|
+
name: 'batch',
|
|
42
|
+
}),
|
|
43
|
+
).toBe('/workload/pods/default/batch')
|
|
44
|
+
})
|
|
45
|
+
})
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
|
-
import { apiVersionToGroup,
|
|
2
|
+
import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
3
3
|
import type { SelectedResource, Topology } from '@skyhook-io/k8s-ui/types/core'
|
|
4
4
|
import type { SearchHit } from '../api/client'
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ export function searchHitToSelectedResource(hit: SearchHit): SelectedResource {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
17
|
-
export { kindToPlural, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
17
|
+
export { kindToPlural, kindToPluralWithGroup, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
18
18
|
export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
19
19
|
|
|
20
20
|
const NETWORK_POLICY_TOPOLOGY_KINDS = new Set([
|
|
@@ -48,7 +48,7 @@ export function getNetworkPolicyResourceTarget(topology: Topology | null): { kin
|
|
|
48
48
|
|
|
49
49
|
const group = networkPolicyGroup(node)
|
|
50
50
|
const target = {
|
|
51
|
-
kind:
|
|
51
|
+
kind: kindToPluralWithGroup(node.kind, group ?? ''),
|
|
52
52
|
...(group ? { group } : {}),
|
|
53
53
|
}
|
|
54
54
|
targets.set(`${target.kind}\u0000${target.group ?? ''}`, target)
|
|
@@ -96,13 +96,13 @@ export function resourcePath(resource: SelectedResource): string {
|
|
|
96
96
|
}
|
|
97
97
|
if (resource.group) params.set('apiGroup', resource.group)
|
|
98
98
|
const query = params.toString()
|
|
99
|
-
return `/resources/${
|
|
99
|
+
return `/resources/${kindToPluralWithGroup(resource.kind, resource.group ?? '')}${query ? `?${query}` : ''}`
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const FULLSCREEN_RESOURCE_KINDS = new Set(['pods', 'deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs', 'nodes'])
|
|
103
103
|
|
|
104
104
|
export function relatedResourcePath(resource: SelectedResource): string {
|
|
105
|
-
const apiKind =
|
|
105
|
+
const apiKind = kindToPluralWithGroup(resource.kind, resource.group ?? '').toLowerCase()
|
|
106
106
|
if (FULLSCREEN_RESOURCE_KINDS.has(apiKind)) {
|
|
107
107
|
return buildWorkloadPath({ ...resource, kind: apiKind })
|
|
108
108
|
}
|