@skyhook-io/radar-app 1.9.7 → 1.10.0

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.9.7",
3
+ "version": "1.10.0",
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",
@@ -41,7 +41,7 @@
41
41
  "yaml": "^2.9.0"
42
42
  },
43
43
  "peerDependencies": {
44
- "@skyhook-io/k8s-ui": ">=1.11.0",
44
+ "@skyhook-io/k8s-ui": ">=1.12.0",
45
45
  "@tanstack/react-query": ">=5",
46
46
  "@xyflow/react": ">=12.0.0",
47
47
  "clsx": ">=2",
@@ -0,0 +1,23 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import type { PolicyResourceResponse } from '@skyhook-io/k8s-ui'
3
+ import { fetchJSON } from './client'
4
+
5
+ // /api/policy/resource/{kind}/{namespace}/{name}
6
+ //
7
+ // Policy results change when the engine rescans rather than on every render, so
8
+ // a short stale window keeps drawer navigation instant without going stale in a
9
+ // way an operator would notice.
10
+ export function usePolicyResource(kind: string, namespace: string, name: string, enabled = true) {
11
+ return useQuery<PolicyResourceResponse>({
12
+ queryKey: ['policy', 'resource', kind, namespace, name],
13
+ queryFn: () =>
14
+ fetchJSON<PolicyResourceResponse>(
15
+ `/policy/resource/${encodeURIComponent(kind)}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`,
16
+ ),
17
+ enabled: enabled && !!kind && !!namespace && !!name,
18
+ staleTime: 15000,
19
+ // A 403 is a settled answer about this identity, not a blip — retrying
20
+ // would just repeat the denial on every drawer open.
21
+ retry: false,
22
+ })
23
+ }
@@ -1,11 +1,14 @@
1
1
  import { useMemo } from 'react'
2
- import { useQueries } from '@tanstack/react-query'
2
+ import { useQueries, useQuery } from '@tanstack/react-query'
3
3
  import {
4
4
  CompositeRenderer as BaseCompositeRenderer,
5
5
  type ComposedRefStatus,
6
+ type BoundXRStatus,
6
7
  } from '@skyhook-io/k8s-ui/components/resources/renderers/CompositeRenderer'
7
8
  import {
8
9
  getCrossplaneResourceRefs,
10
+ getBoundXRRef,
11
+ isClaim,
9
12
  type CrossplaneResourceRef,
10
13
  } from '@skyhook-io/k8s-ui/components/resources/resource-utils-crossplane'
11
14
  import { getResourceStatus } from '@skyhook-io/k8s-ui'
@@ -41,7 +44,53 @@ function groupFromApiVersion(apiVersion: string | undefined): string {
41
44
  * it yet) is a normal state for a freshly-applied Composite, not a failure.
42
45
  */
43
46
  export function CompositeRenderer({ data, onNavigate }: CompositeRendererProps) {
44
- const refs = useMemo<CrossplaneResourceRef[]>(() => getCrossplaneResourceRefs(data), [data])
47
+ // A v1 Claim carries only a singular spec.resourceRef to its bound XR; the
48
+ // composed-resource refs live on that XR. Follow the ref, fetch the XR, and
49
+ // read its refs — otherwise the claim panel reads its own (absent) resourceRefs
50
+ // and shows "No composed resources" for a claim that has composed plenty.
51
+ const boundXRRef = useMemo(() => (isClaim(data) ? getBoundXRRef(data) : null), [data])
52
+ const boundXRQuery = useQuery({
53
+ queryKey: [
54
+ 'bound-xr',
55
+ groupFromApiVersion(boundXRRef?.apiVersion),
56
+ boundXRRef?.kind ?? '',
57
+ boundXRRef?.namespace ?? '',
58
+ boundXRRef?.name ?? '',
59
+ ],
60
+ queryFn: async () => {
61
+ const ns = boundXRRef!.namespace || '_'
62
+ const plural = kindToPlural(boundXRRef!.kind)
63
+ const group = groupFromApiVersion(boundXRRef!.apiVersion)
64
+ const query = group ? `?group=${encodeURIComponent(group)}` : ''
65
+ return fetchJSON<{ resource: any }>(`/resources/${plural}/${ns}/${boundXRRef!.name}${query}`)
66
+ },
67
+ staleTime: 30000,
68
+ retry: false,
69
+ enabled: Boolean(boundXRRef?.kind && boundXRRef?.name),
70
+ })
71
+
72
+ const refs = useMemo<CrossplaneResourceRef[]>(() => {
73
+ // For a claim, refs come from the bound XR once it's fetched; for an XR/MR
74
+ // viewed directly, they're on `data` itself.
75
+ if (boundXRRef) return getCrossplaneResourceRefs(boundXRQuery.data?.resource)
76
+ return getCrossplaneResourceRefs(data)
77
+ }, [boundXRRef, boundXRQuery.data, data])
78
+
79
+ // Surface the bound-XR fetch state so the empty composed-resources branch can
80
+ // tell "XR still loading / unreadable" apart from "claim genuinely has none".
81
+ const boundXRStatus = useMemo<BoundXRStatus | undefined>(() => {
82
+ if (!boundXRRef) return undefined
83
+ if (boundXRQuery.isLoading) return { loading: true }
84
+ if (boundXRQuery.isError) {
85
+ if (boundXRQuery.error instanceof ApiError && boundXRQuery.error.status === 404) {
86
+ return { missing: true }
87
+ }
88
+ const message =
89
+ boundXRQuery.error instanceof Error ? boundXRQuery.error.message : 'Failed to fetch bound composite'
90
+ return { error: true, errorMessage: message }
91
+ }
92
+ return undefined
93
+ }, [boundXRRef, boundXRQuery.isLoading, boundXRQuery.isError, boundXRQuery.error])
45
94
 
46
95
  const queries = useQueries({
47
96
  queries: refs.map(ref => {
@@ -97,5 +146,13 @@ export function CompositeRenderer({ data, onNavigate }: CompositeRendererProps)
97
146
  return map
98
147
  }, [refs, queries])
99
148
 
100
- return <BaseCompositeRenderer data={data} onNavigate={onNavigate} composedRefStatuses={composedRefStatuses} />
149
+ return (
150
+ <BaseCompositeRenderer
151
+ data={data}
152
+ onNavigate={onNavigate}
153
+ composedRefStatuses={composedRefStatuses}
154
+ composedRefs={refs}
155
+ boundXRStatus={boundXRStatus}
156
+ />
157
+ )
101
158
  }
@@ -6,6 +6,7 @@ import { useOpenTerminal, useOpenLogs } from '../../dock'
6
6
  import { useCapabilitiesContext, useNamespacedCapabilities, useIsLocalDeployment } from '../../../contexts/CapabilitiesContext'
7
7
  import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodEnvironment, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus, useRevealPodEnvironment } from '../../../api/client'
8
8
  import { useRBACSubject } from '../../../api/rbac'
9
+ import { usePolicyResource } from '../../../api/policy'
9
10
  import { podAwaitsScheduling } from '../../capacity/podDemandGate'
10
11
  import { PortForwardInlineButton } from '../../portforward/PortForwardButton'
11
12
  import { ImageFilesystemModal } from '../ImageFilesystemModal'
@@ -74,6 +75,10 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
74
75
  'ServiceAccount', namespace ?? '', saName, !!namespace,
75
76
  )
76
77
 
78
+ const { data: policyData, isLoading: policyLoading, error: policyError } = usePolicyResource(
79
+ 'pods', namespace ?? '', podName, !!namespace && !!podName,
80
+ )
81
+
77
82
  return (
78
83
  <BasePodRenderer
79
84
  data={data}
@@ -102,6 +107,9 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
102
107
  rbacData={rbacData ?? null}
103
108
  rbacLoading={rbacLoading}
104
109
  rbacError={rbacError as Error | null}
110
+ policyData={policyData ?? null}
111
+ policyLoading={policyLoading}
112
+ policyError={policyError as Error | null}
105
113
  canExec={canExec}
106
114
  canViewLogs={canViewLogs}
107
115
  canPortForward={showPortForward}
@@ -2,6 +2,7 @@ import { WorkloadRenderer as BaseWorkloadRenderer } from '@skyhook-io/k8s-ui/com
2
2
  import { useNavigate } from 'react-router-dom'
3
3
  import { useScaleWorkload, fetchJSON } from '../../../api/client'
4
4
  import { useRBACSubject } from '../../../api/rbac'
5
+ import { usePolicyResource } from '../../../api/policy'
5
6
  import { useQueries, useQueryClient } from '@tanstack/react-query'
6
7
  import { kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
7
8
  import type { Relationships, ResourceRef, ResourceWithRelationships } from '../../../types'
@@ -42,6 +43,11 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: Wor
42
43
  const { data: rbacData, isLoading: rbacLoading, error: rbacError } = useRBACSubject(
43
44
  'ServiceAccount', namespace, saName, !!namespace,
44
45
  )
46
+ // Plural form: the endpoint resolves URL plurals to kinds the same way the
47
+ // audit drill-down does, so "deployments" and "Deployment" both work.
48
+ const { data: policyData, isLoading: policyLoading, error: policyError } = usePolicyResource(
49
+ kindToPlural(kind), namespace, metadata.name || '', !!namespace && !!metadata.name,
50
+ )
45
51
  const hpaRefs = (scaleBlockedBy ?? []).filter(ref => {
46
52
  const refKind = ref.kind.toLowerCase()
47
53
  return refKind === 'horizontalpodautoscaler' || refKind === 'hpa'
@@ -80,6 +86,9 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: Wor
80
86
  rbacData={rbacData ?? null}
81
87
  rbacLoading={rbacLoading}
82
88
  rbacError={rbacError as Error | null}
89
+ policyData={policyData ?? null}
90
+ policyLoading={policyLoading}
91
+ policyError={policyError as Error | null}
83
92
  scaleBlockedBy={scaleBlockedBy}
84
93
  scalerDiagnostics={scalerDiagnostics}
85
94
  onScale={async (replicas) => {
@@ -373,7 +373,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
373
373
  <div className="flex flex-wrap gap-1">
374
374
  {([
375
375
  { label: '2xx', active: SEVERITY_BADGE.success },
376
- { label: '3xx', active: SEVERITY_BADGE.neutral },
376
+ { label: '3xx', active: SEVERITY_BADGE.info },
377
377
  { label: '4xx', active: SEVERITY_BADGE.warning },
378
378
  { label: '5xx', active: SEVERITY_BADGE.error },
379
379
  ] as const).map(({ label, active }) => (
@@ -20,7 +20,7 @@ import type { AggregatedFlow } from '../../types'
20
20
  import { clsx } from 'clsx'
21
21
  import { X, ArrowRight, Globe, Server, Activity, Puzzle } from 'lucide-react'
22
22
  import { isClusterAddon, type AddonMode } from './TrafficView'
23
- import { SEVERITY_BADGE, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
23
+ import { SEVERITY_BADGE, SEVERITY_DOT, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
24
24
  import { getNamespaceColor } from '../../utils/traffic-colors'
25
25
  import { Tooltip } from '../ui/Tooltip'
26
26
 
@@ -87,10 +87,10 @@ function latencyColor(ms: number): string {
87
87
  }
88
88
 
89
89
  const STATUS_COLORS: Record<string, { bg: string; text: string }> = {
90
- '2xx': { bg: 'bg-emerald-500', text: SEVERITY_TEXT.success },
91
- '3xx': { bg: 'bg-amber-500', text: SEVERITY_TEXT.neutral },
92
- '4xx': { bg: 'bg-amber-500', text: SEVERITY_TEXT.warning },
93
- '5xx': { bg: 'bg-red-500', text: SEVERITY_TEXT.error },
90
+ '2xx': { bg: SEVERITY_DOT.success, text: SEVERITY_TEXT.success },
91
+ '3xx': { bg: SEVERITY_DOT.info, text: SEVERITY_TEXT.info },
92
+ '4xx': { bg: SEVERITY_DOT.warning, text: SEVERITY_TEXT.warning },
93
+ '5xx': { bg: SEVERITY_DOT.error, text: SEVERITY_TEXT.error },
94
94
  }
95
95
 
96
96
  const VERDICT_BADGE: Record<string, string> = {