@skyhook-io/radar-app 1.2.0 → 1.2.2
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 +1 -1
- package/src/App.tsx +15 -7
- package/src/api/client.ts +18 -2
- package/src/api/quotas.ts +16 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +9 -0
- package/src/index.ts +9 -0
- package/src/utils/navigation.ts +27 -0
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -948,22 +948,30 @@ function AppInner() {
|
|
|
948
948
|
const navigatingToHelm = mainView === 'helm' && prevMainView.current !== 'helm'
|
|
949
949
|
prevMainView.current = mainView
|
|
950
950
|
|
|
951
|
-
//
|
|
952
|
-
|
|
951
|
+
// The URL is the source of truth for what's selected. A deep link
|
|
952
|
+
// (?resource=, ?release=) seeds the selection on mount; the effects that
|
|
953
|
+
// run during that same mount must not wipe a selection the URL still
|
|
954
|
+
// asserts. (On a real view switch the URL no longer carries the param, so
|
|
955
|
+
// the clear proceeds.) Without this, deep-linking straight to a Helm
|
|
956
|
+
// release lands on the release list with no drawer.
|
|
957
|
+
const params = new URLSearchParams(window.location.search)
|
|
958
|
+
if (!navigatingToResources && !params.has('resource')) {
|
|
953
959
|
setSelectedResource(null)
|
|
954
960
|
}
|
|
955
|
-
|
|
956
|
-
if (!navigatingToHelm) {
|
|
961
|
+
if (!navigatingToHelm && !params.has('release')) {
|
|
957
962
|
setSelectedHelmRelease(null)
|
|
958
963
|
}
|
|
959
964
|
setDrawerExpanded(false)
|
|
960
965
|
}, [mainView])
|
|
961
966
|
|
|
962
|
-
// Clear resource selection when namespaces change
|
|
967
|
+
// Clear resource selection when namespaces change — but keep a selection the
|
|
968
|
+
// URL still asserts (deep link, or a release/resource the user is viewing
|
|
969
|
+
// while they adjust the namespace scope filter).
|
|
963
970
|
useEffect(() => {
|
|
964
|
-
|
|
971
|
+
const params = new URLSearchParams(window.location.search)
|
|
972
|
+
if (!params.has('resource')) setSelectedResource(null)
|
|
973
|
+
if (!params.has('release')) setSelectedHelmRelease(null)
|
|
965
974
|
setDrawerExpanded(false)
|
|
966
|
-
setSelectedHelmRelease(null)
|
|
967
975
|
}, [namespacesKey])
|
|
968
976
|
|
|
969
977
|
// Filter topology based on visible kinds (uses displayedTopology which respects pause)
|
package/src/api/client.ts
CHANGED
|
@@ -1609,8 +1609,24 @@ export function useUpdateResource() {
|
|
|
1609
1609
|
errorMessage: 'Failed to update resource',
|
|
1610
1610
|
successMessage: 'Resource updated',
|
|
1611
1611
|
},
|
|
1612
|
-
onSuccess: (
|
|
1613
|
-
|
|
1612
|
+
onSuccess: (updated: any, variables) => {
|
|
1613
|
+
// The PUT goes straight to the apiserver and returns the authoritative
|
|
1614
|
+
// object, but the GET behind this query reads Radar's informer cache,
|
|
1615
|
+
// which lags a write by one watch round-trip. Seed the detail cache with
|
|
1616
|
+
// the PUT response so the edit shows immediately. Invalidating here
|
|
1617
|
+
// instead would trigger a refetch that races the seed and re-reads the
|
|
1618
|
+
// lagging cache — the change appears not to have taken effect.
|
|
1619
|
+
if (updated && typeof updated === 'object' && updated.metadata) {
|
|
1620
|
+
queryClient.setQueriesData(
|
|
1621
|
+
{ queryKey: ['resource', variables.kind, variables.namespace, variables.name] },
|
|
1622
|
+
(old: any) =>
|
|
1623
|
+
old && typeof old === 'object' && 'resource' in old
|
|
1624
|
+
? { ...old, resource: updated }
|
|
1625
|
+
: { resource: updated }
|
|
1626
|
+
)
|
|
1627
|
+
} else {
|
|
1628
|
+
queryClient.invalidateQueries({ queryKey: ['resource', variables.kind, variables.namespace, variables.name] })
|
|
1629
|
+
}
|
|
1614
1630
|
queryClient.invalidateQueries({ queryKey: ['resources', variables.kind] })
|
|
1615
1631
|
queryClient.invalidateQueries({ queryKey: ['topology'] })
|
|
1616
1632
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { fetchJSON } from './client'
|
|
3
|
+
|
|
4
|
+
// useNamespaceQuotas fetches a namespace's ResourceQuota objects via
|
|
5
|
+
// /api/resources/resourcequotas?namespace=<ns> (a bare array). Backs the
|
|
6
|
+
// NamespaceRenderer quota-usage section — quota saturation is otherwise
|
|
7
|
+
// surfaced nowhere in the UI, yet it's exactly why a namespace stops
|
|
8
|
+
// admitting new pods.
|
|
9
|
+
export function useNamespaceQuotas(namespace: string, enabled = true) {
|
|
10
|
+
return useQuery<any[]>({
|
|
11
|
+
queryKey: ['resourcequotas', namespace],
|
|
12
|
+
queryFn: () => fetchJSON<any[]>(`/resources/resourcequotas?namespace=${encodeURIComponent(namespace)}`),
|
|
13
|
+
enabled: enabled && !!namespace,
|
|
14
|
+
staleTime: 15000,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NamespaceRenderer as BaseNamespaceRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/NamespaceRenderer'
|
|
2
2
|
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
3
3
|
import { useRBACNamespace } from '../../../api/rbac'
|
|
4
|
+
import { useNamespaceQuotas } from '../../../api/quotas'
|
|
5
|
+
import { isForbiddenError } from '../../../api/client'
|
|
4
6
|
|
|
5
7
|
interface NamespaceRendererProps {
|
|
6
8
|
data: any
|
|
@@ -10,12 +12,19 @@ interface NamespaceRendererProps {
|
|
|
10
12
|
export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps) {
|
|
11
13
|
const name = data?.metadata?.name ?? ''
|
|
12
14
|
const { data: rbacData, isLoading, error } = useRBACNamespace(name, !!name)
|
|
15
|
+
const { data: quotaData, error: quotaError } = useNamespaceQuotas(name, !!name)
|
|
16
|
+
// 403 → the user can't see quotas; hide the section (same posture as the
|
|
17
|
+
// RBAC sections). Surface other errors (500/503) so a quota-constrained
|
|
18
|
+
// namespace doesn't silently render as quota-free.
|
|
19
|
+
const quotaErr = quotaError && !isForbiddenError(quotaError) ? (quotaError as Error) : null
|
|
13
20
|
return (
|
|
14
21
|
<BaseNamespaceRenderer
|
|
15
22
|
data={data}
|
|
16
23
|
rbacData={rbacData ?? null}
|
|
17
24
|
rbacLoading={isLoading}
|
|
18
25
|
rbacError={error as Error | null}
|
|
26
|
+
quotaData={quotaData}
|
|
27
|
+
quotaError={quotaErr}
|
|
19
28
|
onNavigate={onNavigate}
|
|
20
29
|
/>
|
|
21
30
|
)
|
package/src/index.ts
CHANGED
|
@@ -22,3 +22,12 @@ export { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay';
|
|
|
22
22
|
// kubeconfig ContextSwitcher without taking a direct dep on k8s-ui internals.
|
|
23
23
|
export { ClusterSwitcher } from '@skyhook-io/k8s-ui';
|
|
24
24
|
export type { ClusterSwitcherProps, ClusterSwitcherItem } from '@skyhook-io/k8s-ui';
|
|
25
|
+
|
|
26
|
+
// Deep-link builders — so consumers (Radar Hub) construct deep links into a
|
|
27
|
+
// cluster view without hand-rolling Radar's internal URL format, which drifts
|
|
28
|
+
// silently when Radar re-routes. `resourcePath` opens the detail drawer for any
|
|
29
|
+
// kind incl. cluster-scoped; `buildWorkloadPath` is the namespaced-workload
|
|
30
|
+
// full-page view. Both return basename-relative paths; embedders prepend their
|
|
31
|
+
// cluster prefix (e.g. /c/:id).
|
|
32
|
+
export { resourcePath, buildWorkloadPath } from './utils/navigation';
|
|
33
|
+
export type { SelectedResource } from '@skyhook-io/k8s-ui/types/core';
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
|
+
import { kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
2
3
|
import type { SelectedResource } from '@skyhook-io/k8s-ui/types/core'
|
|
3
4
|
|
|
4
5
|
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
@@ -8,6 +9,8 @@ export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
|
8
9
|
/**
|
|
9
10
|
* Build a /workload/:kind/:namespace/:name URL, preserving the API group as a
|
|
10
11
|
* query param so the WorkloadView can resolve CRDs with colliding kind names.
|
|
12
|
+
* Namespaced workloads only — WorkloadView requires a namespace. For arbitrary
|
|
13
|
+
* kinds (including cluster-scoped) use resourcePath.
|
|
11
14
|
*/
|
|
12
15
|
export function buildWorkloadPath(resource: SelectedResource): string {
|
|
13
16
|
const kind = encodeURIComponent(resource.kind)
|
|
@@ -17,6 +20,30 @@ export function buildWorkloadPath(resource: SelectedResource): string {
|
|
|
17
20
|
return resource.group ? `${base}?apiGroup=${encodeURIComponent(resource.group)}` : base
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Build a /resources/:plural?resource=:namespace/:name URL — the deep link that
|
|
25
|
+
* opens a resource's detail drawer in the resources view. Cluster-scoped
|
|
26
|
+
* resources use ?resource=:name (no slash); the API group rides in ?apiGroup=
|
|
27
|
+
* to disambiguate CRD/core kind collisions. This is the exact form the
|
|
28
|
+
* ResourcesView mount effect parses (the `?resource=` reader in
|
|
29
|
+
* packages/k8s-ui/src/components/resources/ResourcesView.tsx) — keep the two in
|
|
30
|
+
* lockstep.
|
|
31
|
+
*
|
|
32
|
+
* Unlike buildWorkloadPath, this opens the detail drawer for ANY kind,
|
|
33
|
+
* including cluster-scoped resources. Returns a basename-relative path;
|
|
34
|
+
* embedders (Radar Hub) prepend their cluster prefix (e.g. /c/:id).
|
|
35
|
+
*/
|
|
36
|
+
export function resourcePath(resource: SelectedResource): string {
|
|
37
|
+
const params = new URLSearchParams()
|
|
38
|
+
// No name → nothing to open; the kind list is the sane fallback.
|
|
39
|
+
if (resource.name) {
|
|
40
|
+
params.set('resource', resource.namespace ? `${resource.namespace}/${resource.name}` : resource.name)
|
|
41
|
+
}
|
|
42
|
+
if (resource.group) params.set('apiGroup', resource.group)
|
|
43
|
+
const query = params.toString()
|
|
44
|
+
return `/resources/${kindToPlural(resource.kind)}${query ? `?${query}` : ''}`
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
// radar-specific: open URL in system browser (desktop app support)
|
|
21
48
|
export function openExternal(url: string): void {
|
|
22
49
|
fetch(apiUrl('/desktop/open-url'), {
|