@skyhook-io/radar-app 1.0.1 → 1.1.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 +6 -6
- package/src/App.tsx +144 -25
- package/src/api/client.ts +158 -8
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/index.ts +1 -1
- package/src/components/gitops/GitOpsView.tsx +2441 -0
- package/src/components/gitops/RollbackDialog.tsx +107 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +144 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +20 -3
- package/src/components/helm/HelmView.tsx +9 -1
- package/src/components/helm/OwnedResources.tsx +2 -2
- package/src/components/home/GitOpsControllersCard.tsx +108 -0
- package/src/components/home/HomeView.tsx +9 -1
- package/src/components/resources/ResourcesView.tsx +27 -2
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/timeline/TimelineSwimlanes.tsx +20 -6
- package/src/components/ui/CommandPalette.tsx +6 -4
- package/src/components/ui/ShortcutHelpOverlay.tsx +2 -1
- package/src/components/ui/UpdateNotification.tsx +36 -19
- package/src/components/workload/WorkloadView.tsx +126 -10
- package/src/types.ts +2 -0
- package/src/utils/navigation.ts +14 -1
package/src/api/client.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type {
|
|
|
23
23
|
InstallChartRequest,
|
|
24
24
|
ArtifactHubSearchResult,
|
|
25
25
|
ArtifactHubChartDetail,
|
|
26
|
+
GitOpsResourceTree,
|
|
27
|
+
GitOpsInsight,
|
|
26
28
|
} from '../types'
|
|
27
29
|
import type { GitOpsOperationResponse } from '../types/gitops'
|
|
28
30
|
import { getApiBase, getAuthHeaders, getCredentialsMode, getBasename, routePath } from './config'
|
|
@@ -256,6 +258,31 @@ export interface DashboardNetworkPolicyCoverage {
|
|
|
256
258
|
totalWorkloads: number
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
export interface DashboardGitOpsControllers {
|
|
262
|
+
// Aggregate roll-up across all detected controllers.
|
|
263
|
+
status: 'healthy' | 'degraded' | 'crashing'
|
|
264
|
+
controllers: DashboardGitOpsController[]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface DashboardGitOpsController {
|
|
268
|
+
name: string
|
|
269
|
+
// Tool vocabulary aligns with the backend `ctrlTool*` constants in
|
|
270
|
+
// internal/server/dashboard_gitops.go and the GitOps tree-builder
|
|
271
|
+
// tags in pkg/gitops/tree/graph.go (`gitopsTool`). Keep these three
|
|
272
|
+
// surfaces in sync — diverging vocabulary across surfaces was a real
|
|
273
|
+
// source of confusion until consolidated.
|
|
274
|
+
tool: 'argocd' | 'fluxcd'
|
|
275
|
+
namespace: string
|
|
276
|
+
ready: number
|
|
277
|
+
total: number
|
|
278
|
+
// Per-controller status (aggregate is in the parent and excludes
|
|
279
|
+
// 'pending' — it normalizes into 'degraded' there).
|
|
280
|
+
status: 'healthy' | 'degraded' | 'crashing' | 'pending'
|
|
281
|
+
// Reason for the crash when status === 'crashing'. Common values:
|
|
282
|
+
// "CrashLoopBackOff", "Error". Empty for non-crashing states.
|
|
283
|
+
crashReason?: string
|
|
284
|
+
}
|
|
285
|
+
|
|
259
286
|
export interface DashboardResponse {
|
|
260
287
|
cluster: DashboardCluster
|
|
261
288
|
health: DashboardHealth
|
|
@@ -270,6 +297,7 @@ export interface DashboardResponse {
|
|
|
270
297
|
certificateHealth: DashboardCertificateHealth | null
|
|
271
298
|
networkPolicyCoverage: DashboardNetworkPolicyCoverage | null
|
|
272
299
|
audit: DashboardAudit | null
|
|
300
|
+
gitopsControllers: DashboardGitOpsControllers | null
|
|
273
301
|
nodeVersionSkew: { versions: Record<string, string[]>; minVersion: string; maxVersion: string } | null
|
|
274
302
|
deferredLoading?: boolean // True while deferred informers (secrets, events, etc.) are still syncing
|
|
275
303
|
partialData?: string[] // Critical kinds promoted at first paint that haven't yet finished syncing (live-filtered)
|
|
@@ -783,6 +811,46 @@ export function useTopology(namespaces: string[], viewMode: string = 'resources'
|
|
|
783
811
|
})
|
|
784
812
|
}
|
|
785
813
|
|
|
814
|
+
export function useGitOpsTree(kind: string, namespace: string, name: string, group?: string, namespaces: string[] = []) {
|
|
815
|
+
const ns = namespace || '_'
|
|
816
|
+
const params = new URLSearchParams()
|
|
817
|
+
if (group) params.set('group', group)
|
|
818
|
+
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
819
|
+
const queryString = params.toString()
|
|
820
|
+
|
|
821
|
+
return useQuery<GitOpsResourceTree>({
|
|
822
|
+
queryKey: ['gitops-tree', kind, namespace, name, group, namespaces],
|
|
823
|
+
queryFn: () => fetchJSON(`/gitops/tree/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
824
|
+
enabled: Boolean(kind && name),
|
|
825
|
+
staleTime: 5000,
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Poll fast (2s) while a sync/rollback is in flight so the user sees the
|
|
830
|
+
// outcome quickly; otherwise rely on staleTime + manual refetch. Argo flips
|
|
831
|
+
// operationState.phase from "Running" -> Succeeded/Failed when done, so this
|
|
832
|
+
// auto-quiesces on completion.
|
|
833
|
+
const INSIGHTS_RUNNING_POLL_MS = 2000
|
|
834
|
+
|
|
835
|
+
export function useGitOpsInsights(kind: string, namespace: string, name: string, group?: string, namespaces: string[] = []) {
|
|
836
|
+
const ns = namespace || '_'
|
|
837
|
+
const params = new URLSearchParams()
|
|
838
|
+
if (group) params.set('group', group)
|
|
839
|
+
if (namespaces.length > 0) params.set('namespaces', namespaces.join(','))
|
|
840
|
+
const queryString = params.toString()
|
|
841
|
+
|
|
842
|
+
return useQuery<GitOpsInsight>({
|
|
843
|
+
queryKey: ['gitops-insights', kind, namespace, name, group, namespaces],
|
|
844
|
+
queryFn: () => fetchJSON(`/gitops/insights/${kind}/${ns}/${name}${queryString ? `?${queryString}` : ''}`),
|
|
845
|
+
enabled: Boolean(kind && name),
|
|
846
|
+
staleTime: 5000,
|
|
847
|
+
refetchInterval: (query) => {
|
|
848
|
+
const phase = query.state.data?.summary?.operationPhase
|
|
849
|
+
return phase === 'Running' ? INSIGHTS_RUNNING_POLL_MS : false
|
|
850
|
+
},
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
|
|
786
854
|
// Generic resource fetching - returns resource with relationships
|
|
787
855
|
// Uses '_' as placeholder for cluster-scoped resources (empty namespace)
|
|
788
856
|
export function useResource<T>(kind: string, namespace: string, name: string, group?: string) {
|
|
@@ -2232,6 +2300,7 @@ export function useArtifactHubChart(repoName: string, chartName: string, version
|
|
|
2232
2300
|
|
|
2233
2301
|
interface GitOpsMutationConfig<TVariables> {
|
|
2234
2302
|
getPath: (variables: TVariables) => string
|
|
2303
|
+
getBody?: (variables: TVariables) => unknown
|
|
2235
2304
|
errorMessage: string
|
|
2236
2305
|
successMessage: string
|
|
2237
2306
|
getInvalidateKeys: (variables: TVariables) => (string | undefined)[][]
|
|
@@ -2248,6 +2317,8 @@ function createGitOpsMutation<TVariables>(config: GitOpsMutationConfig<TVariable
|
|
|
2248
2317
|
mutationFn: async (variables: TVariables): Promise<GitOpsOperationResponse> => {
|
|
2249
2318
|
const response = await apiFetch(`${getApiBase()}${config.getPath(variables)}`, {
|
|
2250
2319
|
method: 'POST',
|
|
2320
|
+
headers: config.getBody ? { 'Content-Type': 'application/json' } : undefined,
|
|
2321
|
+
body: config.getBody ? JSON.stringify(config.getBody(variables)) : undefined,
|
|
2251
2322
|
})
|
|
2252
2323
|
if (!response.ok) {
|
|
2253
2324
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
@@ -2270,16 +2341,46 @@ function createGitOpsMutation<TVariables>(config: GitOpsMutationConfig<TVariable
|
|
|
2270
2341
|
|
|
2271
2342
|
// Common variable types
|
|
2272
2343
|
type FluxResourceVars = { kind: string; namespace: string; name: string }
|
|
2344
|
+
// ArgoAppVars identifies the target Application. Used by mutations that don't
|
|
2345
|
+
// take a body (terminate, suspend, resume, refresh).
|
|
2273
2346
|
type ArgoAppVars = { namespace: string; name: string }
|
|
2347
|
+
// ArgoSyncVars extends ArgoAppVars with the sync request body fields. Only
|
|
2348
|
+
// useArgoSync sends these — splitting the type prevents callers from passing
|
|
2349
|
+
// resources/revision/prune to mutations that would silently drop them.
|
|
2350
|
+
type ArgoSyncVars = ArgoAppVars & {
|
|
2351
|
+
resources?: Array<{ group?: string; kind: string; namespace?: string; name: string }>
|
|
2352
|
+
revision?: string
|
|
2353
|
+
prune?: boolean
|
|
2354
|
+
dryRun?: boolean
|
|
2355
|
+
force?: boolean
|
|
2356
|
+
applyOnly?: boolean
|
|
2357
|
+
// Free-form Argo SyncOption strings, e.g. "Replace=true",
|
|
2358
|
+
// "ServerSideApply=true", "PruneLast=true". Caller is responsible for
|
|
2359
|
+
// spelling.
|
|
2360
|
+
syncOptions?: string[]
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// ArgoRollbackVars targets a specific Argo history entry by ID. Prune and
|
|
2364
|
+
// DryRun mirror the sync flags so the rollback dialog can offer the same
|
|
2365
|
+
// safety net.
|
|
2366
|
+
type ArgoRollbackVars = ArgoAppVars & {
|
|
2367
|
+
id: number
|
|
2368
|
+
prune?: boolean
|
|
2369
|
+
dryRun?: boolean
|
|
2370
|
+
}
|
|
2274
2371
|
|
|
2275
2372
|
// Standard invalidation patterns
|
|
2276
2373
|
const fluxInvalidateKeys = (v: FluxResourceVars) => [
|
|
2277
2374
|
['resources', v.kind],
|
|
2278
2375
|
['resource', v.kind, v.namespace, v.name],
|
|
2376
|
+
['gitops-tree', v.kind, v.namespace, v.name],
|
|
2377
|
+
['gitops-insights', v.kind, v.namespace, v.name],
|
|
2279
2378
|
]
|
|
2280
2379
|
const argoInvalidateKeys = (v: ArgoAppVars) => [
|
|
2281
2380
|
['resources', 'applications'],
|
|
2282
2381
|
['resource', 'applications', v.namespace, v.name],
|
|
2382
|
+
['gitops-tree', 'applications', v.namespace, v.name],
|
|
2383
|
+
['gitops-insights', 'applications', v.namespace, v.name],
|
|
2283
2384
|
]
|
|
2284
2385
|
|
|
2285
2386
|
// ============================================================================
|
|
@@ -2324,13 +2425,30 @@ export const useFluxSyncWithSource = createGitOpsMutation<FluxResourceVars>({
|
|
|
2324
2425
|
// ArgoCD API hooks
|
|
2325
2426
|
// ============================================================================
|
|
2326
2427
|
|
|
2327
|
-
export const useArgoSync = createGitOpsMutation<
|
|
2428
|
+
export const useArgoSync = createGitOpsMutation<ArgoSyncVars>({
|
|
2328
2429
|
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/sync`,
|
|
2430
|
+
getBody: (v) => ({
|
|
2431
|
+
resources: v.resources,
|
|
2432
|
+
revision: v.revision,
|
|
2433
|
+
prune: v.prune,
|
|
2434
|
+
dryRun: v.dryRun,
|
|
2435
|
+
force: v.force,
|
|
2436
|
+
applyOnly: v.applyOnly,
|
|
2437
|
+
syncOptions: v.syncOptions,
|
|
2438
|
+
}),
|
|
2329
2439
|
errorMessage: 'Failed to trigger sync',
|
|
2330
2440
|
successMessage: 'Sync initiated',
|
|
2331
2441
|
getInvalidateKeys: argoInvalidateKeys,
|
|
2332
2442
|
})
|
|
2333
2443
|
|
|
2444
|
+
export const useArgoRollback = createGitOpsMutation<ArgoRollbackVars>({
|
|
2445
|
+
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/rollback`,
|
|
2446
|
+
getBody: (v) => ({ id: v.id, prune: v.prune, dryRun: v.dryRun }),
|
|
2447
|
+
errorMessage: 'Failed to roll back application',
|
|
2448
|
+
successMessage: 'Rollback initiated',
|
|
2449
|
+
getInvalidateKeys: argoInvalidateKeys,
|
|
2450
|
+
})
|
|
2451
|
+
|
|
2334
2452
|
export const useArgoTerminate = createGitOpsMutation<ArgoAppVars>({
|
|
2335
2453
|
getPath: (v) => `/argo/applications/${v.namespace}/${v.name}/terminate`,
|
|
2336
2454
|
errorMessage: 'Failed to terminate sync',
|
|
@@ -2373,8 +2491,13 @@ export function useArgoRefresh() {
|
|
|
2373
2491
|
successMessage: 'Application refreshed',
|
|
2374
2492
|
},
|
|
2375
2493
|
onSuccess: (_, variables) => {
|
|
2376
|
-
|
|
2377
|
-
|
|
2494
|
+
// Match the standard Argo invalidation set so the GitOps detail page
|
|
2495
|
+
// (insights strip, resource tree) refetches after Refresh / Hard
|
|
2496
|
+
// Refresh — without these two extra keys the user clicks Refresh and
|
|
2497
|
+
// sees stale insight/tree data until the next staleTime tick.
|
|
2498
|
+
argoInvalidateKeys(variables).forEach((key) =>
|
|
2499
|
+
queryClient.invalidateQueries({ queryKey: key })
|
|
2500
|
+
)
|
|
2378
2501
|
},
|
|
2379
2502
|
})
|
|
2380
2503
|
}
|
|
@@ -2481,6 +2604,17 @@ export function useNamespaceScope() {
|
|
|
2481
2604
|
|
|
2482
2605
|
const NAMESPACE_SWITCH_TIMEOUT = 5000
|
|
2483
2606
|
|
|
2607
|
+
export function debugNamespaceLog(label: string, payload?: Record<string, unknown>) {
|
|
2608
|
+
if (typeof window === 'undefined') return
|
|
2609
|
+
const enabled = window.localStorage.getItem('radar:debug:namespaces')
|
|
2610
|
+
if (enabled !== '1' && enabled !== 'true') return
|
|
2611
|
+
console.log(`[namespace-debug] ${label}`, {
|
|
2612
|
+
t: Math.round(performance.now()),
|
|
2613
|
+
href: window.location.href,
|
|
2614
|
+
...payload,
|
|
2615
|
+
})
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2484
2618
|
export function useSetActiveNamespace() {
|
|
2485
2619
|
const queryClient = useQueryClient()
|
|
2486
2620
|
return useMutation<NamespaceScope, Error, { namespaces: string[] }>({
|
|
@@ -2493,8 +2627,10 @@ export function useSetActiveNamespace() {
|
|
|
2493
2627
|
errorMessage: 'Failed to update namespace selection',
|
|
2494
2628
|
},
|
|
2495
2629
|
mutationFn: async ({ namespaces }) => {
|
|
2630
|
+
debugNamespaceLog('mutation:start', { namespaces })
|
|
2496
2631
|
const controller = new AbortController()
|
|
2497
2632
|
const timeoutId = setTimeout(() => controller.abort(), NAMESPACE_SWITCH_TIMEOUT)
|
|
2633
|
+
const startedAt = performance.now()
|
|
2498
2634
|
try {
|
|
2499
2635
|
const response = await apiFetch(`${getApiBase()}/cluster/namespace`, {
|
|
2500
2636
|
method: 'POST',
|
|
@@ -2503,6 +2639,11 @@ export function useSetActiveNamespace() {
|
|
|
2503
2639
|
signal: controller.signal,
|
|
2504
2640
|
})
|
|
2505
2641
|
clearTimeout(timeoutId)
|
|
2642
|
+
debugNamespaceLog('mutation:response', {
|
|
2643
|
+
namespaces,
|
|
2644
|
+
status: response.status,
|
|
2645
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2646
|
+
})
|
|
2506
2647
|
if (!response.ok) {
|
|
2507
2648
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
2508
2649
|
throw new Error(error.error || `HTTP ${response.status}`)
|
|
@@ -2510,23 +2651,32 @@ export function useSetActiveNamespace() {
|
|
|
2510
2651
|
return response.json()
|
|
2511
2652
|
} catch (error) {
|
|
2512
2653
|
clearTimeout(timeoutId)
|
|
2654
|
+
debugNamespaceLog('mutation:error', {
|
|
2655
|
+
namespaces,
|
|
2656
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2657
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2658
|
+
})
|
|
2513
2659
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
2514
2660
|
throw new Error('Namespace switch timed out. The cluster may be unreachable.')
|
|
2515
2661
|
}
|
|
2516
2662
|
throw error
|
|
2517
2663
|
}
|
|
2518
2664
|
},
|
|
2519
|
-
onSuccess: () => {
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2665
|
+
onSuccess: (scope) => {
|
|
2666
|
+
debugNamespaceLog('mutation:success-before-scope-cache-write', {
|
|
2667
|
+
actives: scope.actives,
|
|
2668
|
+
mode: scope.mode,
|
|
2669
|
+
accessibleCount: scope.accessibleNamespaces.length,
|
|
2670
|
+
})
|
|
2671
|
+
queryClient.setQueryData<NamespaceScope>(['namespace-scope'], scope)
|
|
2672
|
+
debugNamespaceLog('mutation:success-after-scope-cache-write')
|
|
2524
2673
|
},
|
|
2525
2674
|
onError: () => {
|
|
2526
2675
|
// A failed switch can leave the server's stored pick out of sync
|
|
2527
2676
|
// with the cached scope (network timeout after the server wrote;
|
|
2528
2677
|
// partial mutation). Refetch so the displayed picker matches what
|
|
2529
2678
|
// the server actually persisted instead of what we assumed.
|
|
2679
|
+
debugNamespaceLog('mutation:on-error-invalidate-scope')
|
|
2530
2680
|
queryClient.invalidateQueries({ queryKey: ['namespace-scope'] })
|
|
2531
2681
|
},
|
|
2532
2682
|
})
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { DockProvider, useDock, useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal, useOpenLocalTerminal } from './DockContext'
|
|
1
|
+
export { DockProvider, useDock, useDockReservedHeight, useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal, useOpenLocalTerminal } from './DockContext'
|
|
2
2
|
export { BottomDock } from './BottomDock'
|