@skyhook-io/radar-app 1.16.0 → 1.16.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.16.
|
|
3
|
+
"version": "1.16.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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"yaml": "^2.9.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@skyhook-io/k8s-ui": ">=1.16.
|
|
45
|
+
"@skyhook-io/k8s-ui": ">=1.16.1",
|
|
46
46
|
"@tanstack/react-query": ">=5",
|
|
47
47
|
"@xyflow/react": ">=12.0.0",
|
|
48
48
|
"clsx": ">=2",
|
package/src/api/client.ts
CHANGED
|
@@ -1782,6 +1782,7 @@ export function useRadarFeature(feature: RadarFeature) {
|
|
|
1782
1782
|
const support = radarFeatureSupport(feature, capabilities, currentVersion)
|
|
1783
1783
|
|
|
1784
1784
|
return {
|
|
1785
|
+
support,
|
|
1785
1786
|
gatedKey: support === 'unsupported' ? ['radar-feature-unsupported'] : [],
|
|
1786
1787
|
guard: <T,>(request: () => Promise<T>): Promise<T> =>
|
|
1787
1788
|
guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
|
|
@@ -2946,14 +2947,28 @@ function workloadHistoryPath(kind: string, namespace: string, name: string, grou
|
|
|
2946
2947
|
// The newest page of one workload's history: the workload, what it owns, K8s
|
|
2947
2948
|
// Events about those, and the resources attached to it. Keyed under
|
|
2948
2949
|
// "changes" so the live-update invalidation refreshes it.
|
|
2950
|
+
// A Radar that predates the scoped history endpoint settles as unsupported;
|
|
2951
|
+
// WorkloadView then falls back to the namespace's changes.
|
|
2949
2952
|
export function useWorkloadHistory(kind: string, namespace: string, name: string, group?: string, enabled = true) {
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
+
const { guard, gatedKey, support } = useRadarFeature("workloadHistory");
|
|
2954
|
+
const query = useQuery<WorkloadHistoryPage>({
|
|
2955
|
+
queryKey: ["changes", "workload-history", kind, namespace, name, group ?? "", ...gatedKey],
|
|
2956
|
+
queryFn: ({ signal }) =>
|
|
2957
|
+
guard(() => fetchJSON<WorkloadHistoryPage>(workloadHistoryPath(kind, namespace, name, group), signal)),
|
|
2953
2958
|
staleTime: 5000,
|
|
2954
|
-
|
|
2959
|
+
retry: shouldRetryRadarQuery,
|
|
2960
|
+
refetchInterval: (query) =>
|
|
2961
|
+
query.state.error instanceof RadarFeatureUnsupportedError ? false : CHANGES_REFRESH_INTERVAL_MS,
|
|
2955
2962
|
enabled,
|
|
2956
2963
|
});
|
|
2964
|
+
// A probe answered by an older Radar settles as unsupported and stops
|
|
2965
|
+
// polling; once /capabilities confirms the feature (the agent was upgraded),
|
|
2966
|
+
// ask again rather than staying on the fallback.
|
|
2967
|
+
const { error, refetch } = query;
|
|
2968
|
+
useEffect(() => {
|
|
2969
|
+
if (support === "supported" && error instanceof RadarFeatureUnsupportedError) void refetch();
|
|
2970
|
+
}, [support, error, refetch]);
|
|
2971
|
+
return query;
|
|
2957
2972
|
}
|
|
2958
2973
|
|
|
2959
2974
|
export function fetchWorkloadHistoryPage(kind: string, namespace: string, name: string, group: string | undefined, beforeSeq: number): Promise<WorkloadHistoryPage> {
|
|
@@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'
|
|
|
4
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
5
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
|
6
6
|
import { getRadarUpgradeRequirement } from '@skyhook-io/k8s-ui'
|
|
7
|
-
import { ApiError, isNotFoundError, useApplications, useCapacityPoolDetail, useDrainPlan, usePodEnvironment } from './client'
|
|
7
|
+
import { ApiError, isNotFoundError, useApplications, useCapacityPoolDetail, useDrainPlan, usePodEnvironment, useWorkloadHistory } from './client'
|
|
8
8
|
import { getApiBase } from './config'
|
|
9
9
|
import { usePolicyResource } from './policy'
|
|
10
10
|
import { isRadarFeatureUnsupported } from './radarFeatures'
|
|
@@ -188,3 +188,36 @@ it('never asks a Radar older than v1.8 for applications', async () => {
|
|
|
188
188
|
expect(element.textContent).toBe('upgrade')
|
|
189
189
|
expect(asked('/applications')).toBe(false)
|
|
190
190
|
})
|
|
191
|
+
|
|
192
|
+
function History() {
|
|
193
|
+
const { data, error } = useWorkloadHistory('deployments', 'shop', 'web', 'apps')
|
|
194
|
+
return <span>{data ? 'data' : isRadarFeatureUnsupported(error, 'workloadHistory') ? 'fallback' : 'pending'}</span>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
it('never asks a Radar whose capabilities omit workload history', async () => {
|
|
198
|
+
client.setQueryData(['capabilities'], { features: { policyResource: true } })
|
|
199
|
+
await render(<History />)
|
|
200
|
+
expect(element.textContent).toBe('fallback')
|
|
201
|
+
expect(asked('/history')).toBe(false)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it("reads the router's 404 on workload history as unsupported before capabilities answer", async () => {
|
|
205
|
+
host.radarVersion = undefined
|
|
206
|
+
chiRoutes = ['/workloads/deployments/shop/web/history']
|
|
207
|
+
await render(<History />)
|
|
208
|
+
expect(element.textContent).toBe('fallback')
|
|
209
|
+
expect(requested.filter((url) => url.includes('/history'))).toHaveLength(1)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('asks again once capabilities confirm workload history after a probe hit an older Radar', async () => {
|
|
213
|
+
host.radarVersion = undefined
|
|
214
|
+
chiRoutes = ['/workloads/deployments/shop/web/history']
|
|
215
|
+
await render(<History />)
|
|
216
|
+
expect(element.textContent).toBe('fallback')
|
|
217
|
+
|
|
218
|
+
chiRoutes = []
|
|
219
|
+
await act(async () => { client.setQueryData(['capabilities'], { features: { workloadHistory: true } }) })
|
|
220
|
+
await settle()
|
|
221
|
+
await settle()
|
|
222
|
+
expect(requested.filter((url) => url.includes('/history'))).toHaveLength(2)
|
|
223
|
+
})
|
|
@@ -56,6 +56,15 @@ describe('radarFeatureSupport', () => {
|
|
|
56
56
|
})
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
describe('workload history, whose flag shipped with its endpoint', () => {
|
|
60
|
+
it('trusts a loaded capabilities answer, and probes before it arrives', () => {
|
|
61
|
+
expect(radarFeatureSupport('workloadHistory', { features: { workloadHistory: true } }, undefined)).toBe('supported')
|
|
62
|
+
expect(radarFeatureSupport('workloadHistory', { features: { policyResource: true } }, 'v1.15.0')).toBe('unsupported')
|
|
63
|
+
expect(radarFeatureSupport('workloadHistory', { features: {} }, 'dev')).toBe('unsupported')
|
|
64
|
+
expect(radarFeatureSupport('workloadHistory', undefined, 'v1.15.0')).toBe('unknown')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
59
68
|
describe('radarSpecSupport for a feature whose flag shipped with its endpoint', () => {
|
|
60
69
|
const driftAlerts: RadarFeatureSpec = { label: 'Drift alerts', flag: 'resourceIssues', flagShippedWithEndpoint: true }
|
|
61
70
|
|
package/src/api/radarFeatures.ts
CHANGED
|
@@ -34,6 +34,7 @@ export const RADAR_FEATURES = {
|
|
|
34
34
|
upgradeReadiness: { label: 'Upgrade impact reports', minimumVersion: 'v1.9.0' },
|
|
35
35
|
drainPlan: { label: 'Drain plans', minimumVersion: 'v1.14.0' },
|
|
36
36
|
applications: { label: 'Applications', minimumVersion: 'v1.7.7' },
|
|
37
|
+
workloadHistory: { label: 'Workload history', flag: 'workloadHistory', flagShippedWithEndpoint: true },
|
|
37
38
|
} as const satisfies Record<string, RadarFeatureSpec>
|
|
38
39
|
|
|
39
40
|
export type RadarFeature = keyof typeof RADAR_FEATURES
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import {
|
|
50
50
|
useWorkloadHistory,
|
|
51
51
|
fetchWorkloadHistoryPage,
|
|
52
|
+
useChanges,
|
|
52
53
|
useResourceWithRelationships,
|
|
53
54
|
usePodLogs,
|
|
54
55
|
useTopology,
|
|
@@ -803,18 +804,32 @@ export function WorkloadView({
|
|
|
803
804
|
// colliding kind) under the wrong group and refetch once it settles.
|
|
804
805
|
const historyGroupSettled = Boolean(rest.group) || resource !== undefined || resourceError != null
|
|
805
806
|
const historyQuery = useWorkloadHistory(apiKind, namespace, name, effectiveGroup, expanded && historyGroupSettled)
|
|
807
|
+
// A Radar without the scoped history endpoint gets the namespace's newest
|
|
808
|
+
// changes instead, which the timeline filters to this workload.
|
|
809
|
+
const historyFallback = isRadarFeatureUnsupported(historyQuery.error, 'workloadHistory')
|
|
810
|
+
const namespaceChanges = useChanges({
|
|
811
|
+
namespaces: [namespace],
|
|
812
|
+
timeRange: 'all',
|
|
813
|
+
includeK8sEvents: true,
|
|
814
|
+
includeManaged: true,
|
|
815
|
+
limit: 10000,
|
|
816
|
+
enabled: expanded && historyFallback,
|
|
817
|
+
})
|
|
806
818
|
const {
|
|
807
|
-
events:
|
|
819
|
+
events: scopedEvents,
|
|
808
820
|
truncated: historyTruncated,
|
|
809
821
|
loadOlder: loadOlderHistory,
|
|
810
822
|
loadingOlder: loadingOlderHistory,
|
|
811
823
|
olderError: olderHistoryError,
|
|
812
824
|
} = useHistoryPaging(
|
|
813
825
|
`${effectiveGroup ?? ''}/${apiKind}/${namespace}/${name}`,
|
|
814
|
-
historyQuery.data,
|
|
826
|
+
historyFallback ? undefined : historyQuery.data,
|
|
815
827
|
(beforeSeq) => fetchWorkloadHistoryPage(apiKind, namespace, name, effectiveGroup, beforeSeq),
|
|
816
828
|
)
|
|
817
|
-
const
|
|
829
|
+
const allEvents = historyFallback ? namespaceChanges.data : scopedEvents
|
|
830
|
+
const eventsLoading = historyFallback
|
|
831
|
+
? namespaceChanges.isLoading
|
|
832
|
+
: historyQuery.isLoading || (expanded && !historyGroupSettled)
|
|
818
833
|
|
|
819
834
|
// RBAC
|
|
820
835
|
const canUpdateSecrets = useCanUpdateSecrets()
|
|
@@ -1211,14 +1226,14 @@ export function WorkloadView({
|
|
|
1211
1226
|
refetch={refetchResourceAndRuns}
|
|
1212
1227
|
// Timeline
|
|
1213
1228
|
allEvents={allEvents}
|
|
1214
|
-
historyTruncated={historyTruncated}
|
|
1215
|
-
onLoadOlderHistory={loadOlderHistory}
|
|
1216
|
-
loadingOlderHistory={loadingOlderHistory}
|
|
1217
|
-
olderHistoryError={olderHistoryError}
|
|
1218
|
-
historyScoped
|
|
1219
|
-
historyIncomplete={Boolean(historyQuery.data?.incomplete)}
|
|
1220
|
-
historyError={historyQuery.error as Error | null}
|
|
1221
|
-
onRetryHistory={() => void historyQuery.refetch()}
|
|
1229
|
+
historyTruncated={!historyFallback && historyTruncated}
|
|
1230
|
+
onLoadOlderHistory={historyFallback ? undefined : loadOlderHistory}
|
|
1231
|
+
loadingOlderHistory={!historyFallback && loadingOlderHistory}
|
|
1232
|
+
olderHistoryError={historyFallback ? null : olderHistoryError}
|
|
1233
|
+
historyScoped={!historyFallback}
|
|
1234
|
+
historyIncomplete={!historyFallback && Boolean(historyQuery.data?.incomplete)}
|
|
1235
|
+
historyError={(historyFallback ? namespaceChanges.error : historyQuery.error) as Error | null}
|
|
1236
|
+
onRetryHistory={() => void (historyFallback ? namespaceChanges.refetch() : historyQuery.refetch())}
|
|
1222
1237
|
relatedTimelineEvents={relatedTimelineEvents}
|
|
1223
1238
|
eventsLoading={eventsLoading || (batchExecution && batchKind !== 'JobSet' && batchRunsQuery.isLoading)}
|
|
1224
1239
|
topology={topology}
|