@skyhook-io/radar-app 1.2.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
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
- // Don't clear selectedResource when navigating TO resources view (deep link from Helm)
952
- if (!navigatingToResources) {
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
- // Don't clear helm release when navigating TO helm (back button restores from URL)
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
- setSelectedResource(null)
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: (_, variables) => {
1613
- queryClient.invalidateQueries({ queryKey: ['resource', variables.kind, variables.namespace, variables.name] })
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';
@@ -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'), {