@skyhook-io/radar-app 1.16.2 → 1.17.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 +5 -5
- package/src/App.tsx +60 -12
- package/src/RadarApp.tsx +27 -17
- package/src/api/client.goneResource.test.ts +49 -0
- package/src/api/client.ts +168 -55
- package/src/api/cnpg.ts +95 -0
- package/src/api/diagnose.test.ts +40 -0
- package/src/api/diagnose.ts +12 -1
- package/src/api/radarFeatures.render.test.tsx +19 -1
- package/src/api/radarFeatures.ts +3 -0
- package/src/api/timelineSource.ts +1 -1
- package/src/api/traffic.sources.test.ts +55 -0
- package/src/api/traffic.ts +173 -14
- package/src/components/NoClusterAccessBanner.tsx +68 -0
- package/src/components/applications/ApplicationsView.tsx +3 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +18 -7
- package/src/components/capacity/CapacityView.test.tsx +31 -1
- package/src/components/capacity/shared.tsx +12 -21
- package/src/components/cnpg/CNPGClusterActivity.tsx +49 -0
- package/src/components/cnpg/CNPGClusterLogs.tsx +62 -0
- package/src/components/cnpg/CNPGDeclarations.tsx +311 -0
- package/src/components/cnpg/CNPGDetailPage.tsx +228 -0
- package/src/components/cnpg/CNPGDrawerTrail.tsx +37 -0
- package/src/components/cnpg/CNPGOperator.tsx +234 -0
- package/src/components/cnpg/CNPGOverview.tsx +307 -0
- package/src/components/cnpg/CNPGPooling.tsx +116 -0
- package/src/components/cnpg/CNPGProtection.tsx +320 -0
- package/src/components/cnpg/CNPGSummaryHost.tsx +143 -0
- package/src/components/cnpg/CNPGView.tsx +156 -0
- package/src/components/cnpg/paths.ts +13 -0
- package/src/components/cnpg/routes.test.ts +60 -0
- package/src/components/cnpg/routes.ts +112 -0
- package/src/components/cnpg/shared.tsx +290 -0
- package/src/components/cnpg/useCNPGSidebarWorkspace.ts +88 -0
- package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
- package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
- package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
- package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
- package/src/components/diagnose/ApplyDialog.tsx +26 -28
- package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
- package/src/components/diagnose/DiagnoseContext.tsx +136 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
- package/src/components/diagnose/InvestigationView.tsx +39 -2
- package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
- package/src/components/diagnose/agentCatalog.ts +2 -2
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
- package/src/components/dock/TrafficFlowListTab.tsx +27 -6
- package/src/components/execution/BatchExecutionView.tsx +11 -11
- package/src/components/helm/ChartBrowser.tsx +9 -10
- package/src/components/helm/HelmCompareRoute.tsx +108 -115
- package/src/components/helm/HelmReleaseDrawer.tsx +26 -44
- package/src/components/helm/InstallWizard.tsx +5 -2
- package/src/components/helm/RevisionHistory.tsx +3 -2
- package/src/components/helm/TrackChartSourceDialog.tsx +10 -2
- package/src/components/helm/ValuesViewer.tsx +1 -1
- package/src/components/home/ClusterHealthCard.tsx +7 -0
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/resources/ResourceDetailDrawer.tsx +6 -0
- package/src/components/resources/ResourcesView.tsx +6 -38
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
- package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
- package/src/components/settings/SettingsDialog.tsx +122 -19
- package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
- package/src/components/timeline/TimelineList.test.ts +47 -29
- package/src/components/timeline/TimelineList.tsx +60 -11
- package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
- package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
- package/src/components/timeline/TimelineView.tsx +16 -2
- package/src/components/timeline/TimelineView.urlparams.test.ts +6 -0
- package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
- package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
- package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
- package/src/components/traffic/TrafficGraph.tsx +109 -26
- package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
- package/src/components/traffic/TrafficView.tsx +582 -149
- package/src/components/traffic/trafficFilters.test.ts +398 -1
- package/src/components/traffic/trafficFilters.ts +408 -0
- package/src/components/whats-new/WhatsNew.test.ts +107 -28
- package/src/components/whats-new/WhatsNew.tsx +146 -86
- package/src/components/whats-new/WhatsNewDialog.test.tsx +132 -10
- package/src/components/whats-new/releaseNotes.ts +155 -24
- package/src/components/workload/WorkloadView.test.ts +2 -1
- package/src/components/workload/WorkloadView.tsx +165 -294
- package/src/context/TimelineCustomization.tsx +15 -0
- package/src/contexts/CapabilitiesContext.tsx +9 -1
- package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
- package/src/hooks/useGitOpsWriteGuard.ts +193 -0
- package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
- package/src/hooks/useResourceCounts.ts +49 -0
- package/src/components/helm/RoleGatedPanel.tsx +0 -47
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
getGitOpsResourceStatus,
|
|
4
|
+
gitOpsOwnerFromRelationships,
|
|
5
|
+
type GitOpsOwnerRef,
|
|
6
|
+
type GitOpsStatus,
|
|
7
|
+
type HelmOwnerRef,
|
|
8
|
+
} from '@skyhook-io/k8s-ui'
|
|
9
|
+
import type { Relationships, ResourceRef } from '../types'
|
|
10
|
+
import { kindToPluralWithGroup } from '../utils/navigation'
|
|
11
|
+
import { useResource, useResourceWithRelationships, useResources } from '../api/client'
|
|
12
|
+
|
|
13
|
+
export interface ResolvedGitOpsOwner {
|
|
14
|
+
/** GitOps owner, with an Argo Application's namespace filled in when the
|
|
15
|
+
* tracking label omitted it and exactly one Application matches. */
|
|
16
|
+
owner: GitOpsOwnerRef | null
|
|
17
|
+
/** Owner as reported by relationships, before namespace resolution. */
|
|
18
|
+
rawOwner: GitOpsOwnerRef | null
|
|
19
|
+
/** The owner CR (Application / Kustomization / HelmRelease). */
|
|
20
|
+
ownerObject: any
|
|
21
|
+
ownerPending: boolean
|
|
22
|
+
ownerVerified: boolean
|
|
23
|
+
/** The Argo CD tracking label named no namespace and Radar filled it in
|
|
24
|
+
* from a single Application of that name. */
|
|
25
|
+
ownerMatchedByName: boolean
|
|
26
|
+
ownerStatus: GitOpsStatus | null
|
|
27
|
+
/** The label/annotation that ties the object to its owner. */
|
|
28
|
+
ownerSource: string | null
|
|
29
|
+
helmOwner: HelmOwnerRef | null
|
|
30
|
+
helmSource: string | null
|
|
31
|
+
/** A parent (e.g. the Deployment of a ReplicaSet) consulted when the
|
|
32
|
+
* object carries no GitOps ownership itself. */
|
|
33
|
+
inheritedLookupRef: ResourceRef | null
|
|
34
|
+
lookupPending: boolean
|
|
35
|
+
lookupError: boolean
|
|
36
|
+
/** The parent was read but the server sent no relationships for it (its
|
|
37
|
+
* topology isn't built yet), so an inherited owner is unknown, not absent. */
|
|
38
|
+
relationshipsUnavailable: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolves who manages a resource: its own GitOps/Helm tracking metadata or,
|
|
42
|
+
// failing that, the metadata of the workload that owns it.
|
|
43
|
+
export function useResolvedGitOpsOwner({
|
|
44
|
+
kind,
|
|
45
|
+
group,
|
|
46
|
+
namespace,
|
|
47
|
+
name,
|
|
48
|
+
relationships,
|
|
49
|
+
resource,
|
|
50
|
+
enabled = true,
|
|
51
|
+
cacheScope,
|
|
52
|
+
}: {
|
|
53
|
+
kind: string
|
|
54
|
+
group?: string
|
|
55
|
+
namespace: string
|
|
56
|
+
name: string
|
|
57
|
+
relationships: Relationships | undefined
|
|
58
|
+
resource: any
|
|
59
|
+
enabled?: boolean
|
|
60
|
+
/** Keeps these reads apart from the shared resource cache (see useResource). */
|
|
61
|
+
cacheScope?: string
|
|
62
|
+
}): ResolvedGitOpsOwner {
|
|
63
|
+
const active = enabled && Boolean(relationships)
|
|
64
|
+
const relationshipGitopsOwner = useMemo(
|
|
65
|
+
() => (active ? gitOpsOwnerFromRelationships(relationships) : null),
|
|
66
|
+
[active, relationships],
|
|
67
|
+
)
|
|
68
|
+
const inheritedLookupRef = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
active
|
|
71
|
+
? findInheritedGitOpsLookupRef(relationships, relationshipGitopsOwner, { kind, namespace, name, group })
|
|
72
|
+
: null,
|
|
73
|
+
[active, relationships, relationshipGitopsOwner, kind, namespace, name, group],
|
|
74
|
+
)
|
|
75
|
+
const inherited = useResourceWithRelationships<any>(
|
|
76
|
+
inheritedLookupRef ? kindToPluralWithGroup(inheritedLookupRef.kind, inheritedLookupRef.group ?? '') : '',
|
|
77
|
+
inheritedLookupRef?.namespace ?? '',
|
|
78
|
+
inheritedLookupRef?.name ?? '',
|
|
79
|
+
inheritedLookupRef?.group,
|
|
80
|
+
{ cacheScope },
|
|
81
|
+
)
|
|
82
|
+
const inheritedGitopsOwner = useMemo(
|
|
83
|
+
() => (inheritedLookupRef ? gitOpsOwnerFromRelationships(inherited.data?.relationships) : null),
|
|
84
|
+
[inheritedLookupRef, inherited.data?.relationships],
|
|
85
|
+
)
|
|
86
|
+
const relationshipHelmOwner = useMemo(
|
|
87
|
+
() => (active ? nativeHelmOwnerFromRelationships(relationships, resource?.metadata?.namespace ?? namespace) : null),
|
|
88
|
+
[active, relationships, resource?.metadata?.namespace, namespace],
|
|
89
|
+
)
|
|
90
|
+
const inheritedHelmOwner = useMemo(
|
|
91
|
+
() =>
|
|
92
|
+
inheritedLookupRef
|
|
93
|
+
? nativeHelmOwnerFromRelationships(
|
|
94
|
+
inherited.data?.relationships,
|
|
95
|
+
inherited.data?.resource?.metadata?.namespace ?? namespace,
|
|
96
|
+
)
|
|
97
|
+
: null,
|
|
98
|
+
[inheritedLookupRef, inherited.data?.relationships, inherited.data?.resource?.metadata?.namespace, namespace],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const rawOwner = relationshipGitopsOwner ?? inheritedGitopsOwner
|
|
102
|
+
const gitOpsSourceResource = relationshipGitopsOwner ? resource : inherited.data?.resource
|
|
103
|
+
const helmOwner = relationshipHelmOwner ?? inheritedHelmOwner
|
|
104
|
+
const helmSourceResource = relationshipHelmOwner ? resource : inherited.data?.resource
|
|
105
|
+
|
|
106
|
+
const shouldResolveArgoNamespace = rawOwner?.tool === 'argocd' && !rawOwner.namespace
|
|
107
|
+
const argoQuery = useResources<any>('applications', undefined, 'argoproj.io', {
|
|
108
|
+
enabled: shouldResolveArgoNamespace,
|
|
109
|
+
cacheScope,
|
|
110
|
+
})
|
|
111
|
+
const argoApplications = argoQuery.data
|
|
112
|
+
const owner = useMemo(() => resolveGitOpsOwner(rawOwner, argoApplications), [rawOwner, argoApplications])
|
|
113
|
+
|
|
114
|
+
const ownerGroup = owner ? gitOpsOwnerGroup(owner) : ''
|
|
115
|
+
const shouldFetchOwner = Boolean(owner?.namespace)
|
|
116
|
+
const ownerQuery = useResource<any>(
|
|
117
|
+
shouldFetchOwner ? owner!.kind : '',
|
|
118
|
+
owner?.namespace ?? '',
|
|
119
|
+
owner?.name ?? '',
|
|
120
|
+
ownerGroup,
|
|
121
|
+
{ enabled: shouldFetchOwner, cacheScope },
|
|
122
|
+
)
|
|
123
|
+
const ownerObject = shouldFetchOwner ? ownerQuery.data : undefined
|
|
124
|
+
|
|
125
|
+
const ownerStatus = useMemo(() => deriveGitOpsOwnerStatus(owner, ownerObject), [owner, ownerObject])
|
|
126
|
+
const ownerSource = useMemo(
|
|
127
|
+
() => describeGitOpsOwnerSource(rawOwner, gitOpsSourceResource),
|
|
128
|
+
[rawOwner, gitOpsSourceResource],
|
|
129
|
+
)
|
|
130
|
+
const helmSource = useMemo(() => describeHelmOwnerSource(helmOwner, helmSourceResource), [helmOwner, helmSourceResource])
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
owner,
|
|
134
|
+
rawOwner,
|
|
135
|
+
ownerObject,
|
|
136
|
+
ownerPending: Boolean(shouldFetchOwner && ownerQuery.isLoading && !ownerQuery.data),
|
|
137
|
+
ownerVerified: Boolean(shouldFetchOwner && ownerQuery.data),
|
|
138
|
+
ownerMatchedByName: Boolean(shouldResolveArgoNamespace && owner?.namespace),
|
|
139
|
+
ownerStatus,
|
|
140
|
+
ownerSource,
|
|
141
|
+
helmOwner,
|
|
142
|
+
helmSource,
|
|
143
|
+
inheritedLookupRef,
|
|
144
|
+
lookupPending: Boolean((inheritedLookupRef && inherited.isPending) || (shouldResolveArgoNamespace && argoQuery.isPending)),
|
|
145
|
+
lookupError: Boolean(inheritedLookupRef && inherited.isError),
|
|
146
|
+
relationshipsUnavailable: Boolean(inheritedLookupRef && inherited.data && !inherited.data.relationships),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function gitOpsOwnerGroup(owner: GitOpsOwnerRef): string {
|
|
151
|
+
if (owner.tool === 'argocd') return 'argoproj.io'
|
|
152
|
+
if (owner.kind === 'kustomizations') return 'kustomize.toolkit.fluxcd.io'
|
|
153
|
+
return 'helm.toolkit.fluxcd.io'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The owner as the singular Kind + group the server's APIs take. */
|
|
157
|
+
export function gitOpsOwnerKindRef(owner: GitOpsOwnerRef): { kind: string; group: string } {
|
|
158
|
+
if (owner.tool === 'argocd') return { kind: 'Application', group: 'argoproj.io' }
|
|
159
|
+
if (owner.kind === 'kustomizations') return { kind: 'Kustomization', group: 'kustomize.toolkit.fluxcd.io' }
|
|
160
|
+
return { kind: 'HelmRelease', group: 'helm.toolkit.fluxcd.io' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveGitOpsOwner(owner: GitOpsOwnerRef | null, argoApplications: any[] | undefined): GitOpsOwnerRef | null {
|
|
164
|
+
if (!owner || owner.namespace || owner.tool !== 'argocd') return owner
|
|
165
|
+
const matches = (argoApplications ?? []).filter((app) => app?.metadata?.name === owner.name)
|
|
166
|
+
if (matches.length !== 1) return owner
|
|
167
|
+
const namespace = matches[0]?.metadata?.namespace
|
|
168
|
+
return namespace ? { ...owner, namespace } : owner
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function findInheritedGitOpsLookupRef(
|
|
172
|
+
relationships: Relationships | undefined,
|
|
173
|
+
directOwner: GitOpsOwnerRef | null,
|
|
174
|
+
current: ResourceRef,
|
|
175
|
+
): ResourceRef | null {
|
|
176
|
+
if (directOwner) return null
|
|
177
|
+
const inheritedManagerRefs = (relationships?.managedBy ?? []).filter(
|
|
178
|
+
(ref) => !gitOpsOwnerFromRelationships({ managedBy: [ref] }) && !isNativeHelmManager(ref),
|
|
179
|
+
)
|
|
180
|
+
const candidates = [relationships?.deployment, ...inheritedManagerRefs, relationships?.owner].filter(
|
|
181
|
+
Boolean,
|
|
182
|
+
) as ResourceRef[]
|
|
183
|
+
|
|
184
|
+
return candidates.find((ref) => !isCurrentResource(ref, current)) ?? null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function nativeHelmOwnerFromRelationships(
|
|
188
|
+
relationships: Relationships | undefined,
|
|
189
|
+
fallbackNamespace: string,
|
|
190
|
+
): HelmOwnerRef | null {
|
|
191
|
+
const ref = relationships?.managedBy?.[0]
|
|
192
|
+
if (!ref || !isNativeHelmManager(ref)) return null
|
|
193
|
+
return {
|
|
194
|
+
namespace: ref.namespace || fallbackNamespace,
|
|
195
|
+
name: ref.name,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isCurrentResource(ref: ResourceRef, current: ResourceRef): boolean {
|
|
200
|
+
return (
|
|
201
|
+
kindToPluralWithGroup(ref.kind, ref.group ?? '') === kindToPluralWithGroup(current.kind, current.group ?? '') &&
|
|
202
|
+
ref.namespace === current.namespace &&
|
|
203
|
+
ref.name === current.name &&
|
|
204
|
+
(ref.group ?? '') === (current.group ?? '')
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isNativeHelmManager(ref: ResourceRef): boolean {
|
|
209
|
+
return ref.kind === 'HelmRelease' && ref.group !== 'helm.toolkit.fluxcd.io'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function describeGitOpsOwnerSource(owner: GitOpsOwnerRef | null, resource: any): string | null {
|
|
213
|
+
if (!owner || !resource) return null
|
|
214
|
+
const labels = resource.metadata?.labels ?? {}
|
|
215
|
+
const annotations = resource.metadata?.annotations ?? {}
|
|
216
|
+
|
|
217
|
+
if (owner.tool === 'fluxcd') {
|
|
218
|
+
const nameKey = owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/name' : 'kustomize.toolkit.fluxcd.io/name'
|
|
219
|
+
const nsKey =
|
|
220
|
+
owner.kind === 'helmreleases' ? 'helm.toolkit.fluxcd.io/namespace' : 'kustomize.toolkit.fluxcd.io/namespace'
|
|
221
|
+
if (labels[nameKey] || labels[nsKey]) {
|
|
222
|
+
return `${nameKey}=${labels[nameKey] ?? ''}, ${nsKey}=${labels[nsKey] ?? ''}`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const trackingID = annotations['argocd.argoproj.io/tracking-id']
|
|
227
|
+
if (trackingID) return `argocd.argoproj.io/tracking-id=${trackingID}`
|
|
228
|
+
const argoInstance = labels['argocd.argoproj.io/instance']
|
|
229
|
+
if (argoInstance) return `argocd.argoproj.io/instance=${argoInstance}`
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function describeHelmOwnerSource(owner: HelmOwnerRef | null, resource: any): string | null {
|
|
234
|
+
if (!owner || !resource) return null
|
|
235
|
+
const annotations = resource.metadata?.annotations ?? {}
|
|
236
|
+
const releaseName = annotations['meta.helm.sh/release-name']
|
|
237
|
+
const releaseNamespace = annotations['meta.helm.sh/release-namespace']
|
|
238
|
+
if (releaseName || releaseNamespace) {
|
|
239
|
+
return `meta.helm.sh/release-name=${releaseName ?? ''}, meta.helm.sh/release-namespace=${releaseNamespace ?? ''}`
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function deriveGitOpsOwnerStatus(owner: GitOpsOwnerRef | null, resource: any): GitOpsStatus | null {
|
|
245
|
+
if (!owner || !resource || !hasGitOpsStatusPayload(owner, resource)) return null
|
|
246
|
+
return getGitOpsResourceStatus(owner.kind, resource)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function hasGitOpsStatusPayload(owner: GitOpsOwnerRef, resource: any): boolean {
|
|
250
|
+
if (owner.kind === 'applications') {
|
|
251
|
+
const status = resource.status ?? {}
|
|
252
|
+
return Boolean(status.sync?.status || status.health?.status || status.operationState?.phase)
|
|
253
|
+
}
|
|
254
|
+
if (resource.spec?.suspend === true) return true
|
|
255
|
+
return Array.isArray(resource.status?.conditions) && resource.status.conditions.length > 0
|
|
256
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { debugNamespaceLog, fetchJSON, isStillLoadingError } from '../api/client'
|
|
3
|
+
import { useConnection } from '../context/ConnectionContext'
|
|
4
|
+
|
|
5
|
+
export interface ResourceCountsResponse {
|
|
6
|
+
counts: Record<string, number>
|
|
7
|
+
forbidden?: string[]
|
|
8
|
+
reasons?: Record<string, string>
|
|
9
|
+
unavailable?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Lightweight per-kind counts for the Resources sidebar badges. Shared by the
|
|
13
|
+
// Resources view and any surface that renders the same sidebar standalone, so
|
|
14
|
+
// both read one cache entry.
|
|
15
|
+
export function useResourceCounts(namespaces: string[]) {
|
|
16
|
+
const { connection } = useConnection()
|
|
17
|
+
const namespacesParam = namespaces.join(',')
|
|
18
|
+
return useQuery({
|
|
19
|
+
queryKey: ['resource-counts', namespacesParam],
|
|
20
|
+
queryFn: async () => {
|
|
21
|
+
const params = new URLSearchParams()
|
|
22
|
+
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
23
|
+
const startedAt = performance.now()
|
|
24
|
+
debugNamespaceLog('resources:counts-fetch-start', { namespaces, params: params.toString() })
|
|
25
|
+
try {
|
|
26
|
+
return await fetchJSON<ResourceCountsResponse>(`/resource-counts?${params}`)
|
|
27
|
+
} finally {
|
|
28
|
+
debugNamespaceLog('resources:counts-fetch-end', {
|
|
29
|
+
namespaces,
|
|
30
|
+
params: params.toString(),
|
|
31
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
staleTime: 10000,
|
|
36
|
+
// SSE invalidation isn't running while connecting, and mid-sync counts
|
|
37
|
+
// are what unlatch guarded kinds as their informers finish — poll fast
|
|
38
|
+
// during the shell, settle to the safety net once connected.
|
|
39
|
+
refetchInterval: connection.state === 'connecting' ? 3000 : 60000,
|
|
40
|
+
// During the first seconds of the progressive shell the endpoint 503s
|
|
41
|
+
// (cluster_connecting) until the mid-sync cache handle exists; keep the
|
|
42
|
+
// query pending rather than parking it in error state, which would
|
|
43
|
+
// unlatch the large-list guard at the connected flip.
|
|
44
|
+
retry: (failureCount: number, error: Error) =>
|
|
45
|
+
isStillLoadingError(error) ? true : failureCount < 3,
|
|
46
|
+
retryDelay: (failureCount: number, error: Error) =>
|
|
47
|
+
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Lock } from 'lucide-react'
|
|
2
|
-
import { useCloudRole, type CloudRole } from '../../api/client'
|
|
3
|
-
|
|
4
|
-
interface RoleGatedPanelProps {
|
|
5
|
-
/** Minimum Cloud tier required to view the children. */
|
|
6
|
-
min: CloudRole
|
|
7
|
-
/** Human-readable name of the gated content, e.g. "release manifests". */
|
|
8
|
-
feature: string
|
|
9
|
-
children: React.ReactNode
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* RoleGatedPanel wraps tab-content surfaces that require a Cloud role
|
|
14
|
-
* (member+) to view. Renders the children when the gate passes; renders
|
|
15
|
-
* an explanatory empty state when the caller's role is insufficient.
|
|
16
|
-
*
|
|
17
|
-
* Why a per-tab gate rather than hiding tabs from the list: viewers
|
|
18
|
-
* should learn what the platform offers and what their tier can't do,
|
|
19
|
-
* not have features silently vanish. This mirrors how the action
|
|
20
|
-
* buttons (Uninstall, Upgrade, etc.) are rendered visible-but-disabled
|
|
21
|
-
* with a role-aware tooltip.
|
|
22
|
-
*
|
|
23
|
-
* Bypasses for non-Cloud users (OSS, OIDC, etc.) — `canAtLeast` returns
|
|
24
|
-
* true when no Cloud role is present. The backend gate has the same
|
|
25
|
-
* shape, so the frontend stays in lockstep.
|
|
26
|
-
*/
|
|
27
|
-
export function RoleGatedPanel({ min, feature, children }: RoleGatedPanelProps) {
|
|
28
|
-
const { role, canAtLeast } = useCloudRole()
|
|
29
|
-
if (canAtLeast(min)) {
|
|
30
|
-
return <>{children}</>
|
|
31
|
-
}
|
|
32
|
-
return (
|
|
33
|
-
<div className="flex flex-col items-center justify-center h-full py-12 px-6 text-center">
|
|
34
|
-
<div className="rounded-full bg-theme-surface p-3 mb-3">
|
|
35
|
-
<Lock className="w-5 h-5 text-theme-text-tertiary" aria-hidden />
|
|
36
|
-
</div>
|
|
37
|
-
<div className="text-sm font-medium text-theme-text-primary">
|
|
38
|
-
Your role can't view {feature}
|
|
39
|
-
</div>
|
|
40
|
-
<div className="mt-1 max-w-md text-xs text-theme-text-secondary">
|
|
41
|
-
You're signed in as <span className="inline-code">{role ?? 'viewer'}</span>.
|
|
42
|
-
This view requires <span className="inline-code">{min}</span> or higher.
|
|
43
|
-
Ask a {min} or owner if you need access.
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
)
|
|
47
|
-
}
|