@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 +2 -2
- package/src/api/policy.ts +23 -0
- package/src/components/resources/CompositeRenderer.tsx +60 -3
- package/src/components/resources/renderers/PodRenderer.tsx +8 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +9 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +1 -1
- package/src/components/traffic/TrafficGraph.tsx +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
91
|
-
'3xx': { bg:
|
|
92
|
-
'4xx': { bg:
|
|
93
|
-
'5xx': { bg:
|
|
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> = {
|