@skyhook-io/radar-app 1.16.2 → 1.16.3
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 +46 -5
- package/src/api/client.ts +53 -47
- package/src/api/cnpg.ts +95 -0
- package/src/api/timelineSource.ts +1 -1
- package/src/components/NoClusterAccessBanner.tsx +68 -0
- 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/helm/ChartBrowser.tsx +6 -7
- 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/TrackChartSourceDialog.tsx +10 -2
- package/src/components/helm/ValuesViewer.tsx +1 -1
- package/src/components/resources/ResourceDetailDrawer.tsx +6 -0
- package/src/components/resources/ResourcesView.tsx +6 -38
- package/src/components/timeline/TimelineList.test.ts +47 -29
- package/src/components/timeline/TimelineList.tsx +57 -11
- package/src/components/timeline/TimelineView.tsx +9 -2
- package/src/components/timeline/TimelineView.urlparams.test.ts +6 -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.tsx +30 -4
- package/src/contexts/CapabilitiesContext.tsx +9 -1
- package/src/hooks/useResourceCounts.ts +49 -0
- package/src/components/helm/RoleGatedPanel.tsx +0 -47
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from 'lucide-react'
|
|
20
20
|
import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
|
|
21
21
|
import {
|
|
22
|
-
useCloudRole,
|
|
23
22
|
useHelmHooksDiff,
|
|
24
23
|
useHelmManifestDiff,
|
|
25
24
|
useHelmNotesDiff,
|
|
@@ -30,7 +29,6 @@ import {
|
|
|
30
29
|
import type { HelmHook, HelmRevision, HooksDiff, ResourceDiff } from '../../types'
|
|
31
30
|
import { getHelmStatusColor, getKindBadgeColor, SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
32
31
|
import { formatDate } from './helm-utils'
|
|
33
|
-
import { RoleGatedPanel } from './RoleGatedPanel'
|
|
34
32
|
import { Tooltip } from '../ui/Tooltip'
|
|
35
33
|
import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
|
|
36
34
|
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
@@ -53,8 +51,6 @@ export function HelmCompareRoute() {
|
|
|
53
51
|
const navigate = useNavigate()
|
|
54
52
|
const location = useLocation()
|
|
55
53
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
56
|
-
const { canAtLeast } = useCloudRole()
|
|
57
|
-
const canViewSensitive = canAtLeast('member')
|
|
58
54
|
|
|
59
55
|
const releaseRef = parseReleaseParam(searchParams.get('release'))
|
|
60
56
|
const storageNamespace = searchParams.get('releaseStorage') || undefined
|
|
@@ -74,16 +70,15 @@ export function HelmCompareRoute() {
|
|
|
74
70
|
const revision1 = defaultLeftRevision
|
|
75
71
|
const revision2 = defaultRightRevision
|
|
76
72
|
const pairReady = Boolean(helmNamespace && releaseName && revision1 > 0 && revision2 > 0 && revision1 !== revision2)
|
|
77
|
-
const diffEnabled = canViewSensitive && pairReady
|
|
78
73
|
|
|
79
74
|
const left = revisions.find((r) => r.revision === revision1)
|
|
80
75
|
const right = revisions.find((r) => r.revision === revision2)
|
|
81
76
|
|
|
82
|
-
const manifestDiff = useHelmManifestDiff(helmNamespace, releaseName, revision1, revision2,
|
|
83
|
-
const valuesDiff = useHelmValuesDiff(helmNamespace, releaseName, revision1, revision2, false,
|
|
84
|
-
const notesDiff = useHelmNotesDiff(helmNamespace, releaseName, revision1, revision2,
|
|
85
|
-
const hooksDiff = useHelmHooksDiff(helmNamespace, releaseName, revision1, revision2,
|
|
86
|
-
const resourceDiff = useHelmResourceDiff(helmNamespace, releaseName, revision1, revision2,
|
|
77
|
+
const manifestDiff = useHelmManifestDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
|
|
78
|
+
const valuesDiff = useHelmValuesDiff(helmNamespace, releaseName, revision1, revision2, false, pairReady)
|
|
79
|
+
const notesDiff = useHelmNotesDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
|
|
80
|
+
const hooksDiff = useHelmHooksDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
|
|
81
|
+
const resourceDiff = useHelmResourceDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
|
|
87
82
|
|
|
88
83
|
const updateRevision = useCallback(
|
|
89
84
|
(key: 'revision1' | 'revision2', value: number) => {
|
|
@@ -214,120 +209,118 @@ export function HelmCompareRoute() {
|
|
|
214
209
|
</div>
|
|
215
210
|
</header>
|
|
216
211
|
|
|
217
|
-
<
|
|
218
|
-
<div className=
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
<div className="
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)}
|
|
212
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
213
|
+
<div className={clsx('grid w-full grid-cols-1 gap-4 px-4 py-4', pairReady && 'xl:grid-cols-[220px_minmax(0,1fr)]')}>
|
|
214
|
+
{pairReady && (
|
|
215
|
+
<nav className="hidden xl:block">
|
|
216
|
+
<div className="sticky top-4 rounded-xl border border-theme-border bg-theme-surface p-2 shadow-theme-sm">
|
|
217
|
+
<div className="px-2 pb-2 text-[11px] font-medium uppercase text-theme-text-tertiary">Compare</div>
|
|
218
|
+
{[
|
|
219
|
+
['summary', 'Summary'],
|
|
220
|
+
['manifest', 'Manifest'],
|
|
221
|
+
['resources', 'Resources'],
|
|
222
|
+
['values', 'Values'],
|
|
223
|
+
['hooks', 'Hooks'],
|
|
224
|
+
['notes', 'Notes'],
|
|
225
|
+
].map(([id, label]) => (
|
|
226
|
+
<a
|
|
227
|
+
key={id}
|
|
228
|
+
href={`#${id}`}
|
|
229
|
+
onClick={(event) => scrollCompareSection(event, id)}
|
|
230
|
+
className="block rounded-md px-2 py-1.5 text-xs text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary"
|
|
231
|
+
>
|
|
232
|
+
{label}
|
|
233
|
+
</a>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
</nav>
|
|
237
|
+
)}
|
|
244
238
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
</div>
|
|
239
|
+
<main className="min-w-0 space-y-4">
|
|
240
|
+
{!pairReady ? (
|
|
241
|
+
<div className="card-inner-lg flex items-start gap-3">
|
|
242
|
+
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-500" />
|
|
243
|
+
<div>
|
|
244
|
+
<div className="text-sm font-medium text-theme-text-primary">Pick two different revisions to compare</div>
|
|
245
|
+
<div className="mt-1 text-sm text-theme-text-secondary">Use the revision selectors above to choose a source and target revision.</div>
|
|
253
246
|
</div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
right={right}
|
|
260
|
-
revision1={revision1}
|
|
261
|
-
revision2={revision2}
|
|
262
|
-
manifestDiff={manifestDiff.data?.diff}
|
|
263
|
-
manifestLoading={manifestDiff.isLoading}
|
|
264
|
-
manifestError={manifestDiff.error}
|
|
265
|
-
valuesDiff={valuesDiff.data?.diff}
|
|
266
|
-
valuesLoading={valuesDiff.isLoading}
|
|
267
|
-
valuesError={valuesDiff.error}
|
|
268
|
-
notesDiff={notesDiff.data?.diff}
|
|
269
|
-
notesLoading={notesDiff.isLoading}
|
|
270
|
-
notesError={notesDiff.error}
|
|
271
|
-
hooksDiff={hooksDiff.data}
|
|
272
|
-
hooksLoading={hooksDiff.isLoading}
|
|
273
|
-
hooksError={hooksDiff.error}
|
|
274
|
-
resourceDiff={resourceDiff.data}
|
|
275
|
-
resourceLoading={resourceDiff.isLoading}
|
|
276
|
-
resourceError={resourceDiff.error}
|
|
277
|
-
/>
|
|
278
|
-
</section>
|
|
279
|
-
|
|
280
|
-
<DiffSection
|
|
281
|
-
id="manifest"
|
|
282
|
-
icon={Code}
|
|
283
|
-
title="Rendered manifest diff"
|
|
284
|
-
description="Rendered Kubernetes YAML is the ground truth for what Helm would apply between these revisions."
|
|
285
|
-
diff={manifestDiff.data?.diff || ''}
|
|
286
|
-
isLoading={manifestDiff.isLoading}
|
|
287
|
-
error={manifestDiff.error}
|
|
288
|
-
emptyLabel="No rendered manifest changes found."
|
|
289
|
-
/>
|
|
290
|
-
|
|
291
|
-
<ResourceInventoryDiffSection
|
|
292
|
-
diff={resourceDiff.data}
|
|
293
|
-
isLoading={resourceDiff.isLoading}
|
|
294
|
-
error={resourceDiff.error}
|
|
247
|
+
</div>
|
|
248
|
+
) : (
|
|
249
|
+
<>
|
|
250
|
+
<section id="summary" className="scroll-mt-4 space-y-4">
|
|
251
|
+
<CompareSummary
|
|
295
252
|
left={left}
|
|
296
253
|
right={right}
|
|
297
254
|
revision1={revision1}
|
|
298
255
|
revision2={revision2}
|
|
256
|
+
manifestDiff={manifestDiff.data?.diff}
|
|
257
|
+
manifestLoading={manifestDiff.isLoading}
|
|
258
|
+
manifestError={manifestDiff.error}
|
|
259
|
+
valuesDiff={valuesDiff.data?.diff}
|
|
260
|
+
valuesLoading={valuesDiff.isLoading}
|
|
261
|
+
valuesError={valuesDiff.error}
|
|
262
|
+
notesDiff={notesDiff.data?.diff}
|
|
263
|
+
notesLoading={notesDiff.isLoading}
|
|
264
|
+
notesError={notesDiff.error}
|
|
265
|
+
hooksDiff={hooksDiff.data}
|
|
266
|
+
hooksLoading={hooksDiff.isLoading}
|
|
267
|
+
hooksError={hooksDiff.error}
|
|
268
|
+
resourceDiff={resourceDiff.data}
|
|
269
|
+
resourceLoading={resourceDiff.isLoading}
|
|
270
|
+
resourceError={resourceDiff.error}
|
|
299
271
|
/>
|
|
272
|
+
</section>
|
|
273
|
+
|
|
274
|
+
<DiffSection
|
|
275
|
+
id="manifest"
|
|
276
|
+
icon={Code}
|
|
277
|
+
title="Rendered manifest diff"
|
|
278
|
+
description="Rendered Kubernetes YAML is the ground truth for what Helm would apply between these revisions."
|
|
279
|
+
diff={manifestDiff.data?.diff || ''}
|
|
280
|
+
isLoading={manifestDiff.isLoading}
|
|
281
|
+
error={manifestDiff.error}
|
|
282
|
+
emptyLabel="No rendered manifest changes found."
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
<ResourceInventoryDiffSection
|
|
286
|
+
diff={resourceDiff.data}
|
|
287
|
+
isLoading={resourceDiff.isLoading}
|
|
288
|
+
error={resourceDiff.error}
|
|
289
|
+
left={left}
|
|
290
|
+
right={right}
|
|
291
|
+
revision1={revision1}
|
|
292
|
+
revision2={revision2}
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
<DiffSection
|
|
296
|
+
id="values"
|
|
297
|
+
icon={Settings}
|
|
298
|
+
title="User-supplied values diff"
|
|
299
|
+
description="Only values explicitly supplied to the release are compared here; computed chart defaults can still affect the rendered manifest."
|
|
300
|
+
diff={valuesDiff.data?.diff || ''}
|
|
301
|
+
isLoading={valuesDiff.isLoading}
|
|
302
|
+
error={valuesDiff.error}
|
|
303
|
+
emptyLabel="No user-supplied value changes found."
|
|
304
|
+
/>
|
|
305
|
+
|
|
306
|
+
<HooksDiffSection diff={hooksDiff.data} isLoading={hooksDiff.isLoading} error={hooksDiff.error} />
|
|
307
|
+
|
|
308
|
+
<DiffSection
|
|
309
|
+
id="notes"
|
|
310
|
+
icon={FileText}
|
|
311
|
+
title="Release notes diff"
|
|
312
|
+
description="NOTES.txt output can reveal chart-level instructions that changed without changing live Kubernetes objects."
|
|
313
|
+
diff={notesDiff.data?.diff || ''}
|
|
314
|
+
isLoading={notesDiff.isLoading}
|
|
315
|
+
error={notesDiff.error}
|
|
316
|
+
emptyLabel="No release notes changes found."
|
|
317
|
+
/>
|
|
300
318
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
title="User-supplied values diff"
|
|
305
|
-
description="Only values explicitly supplied to the release are compared here; computed chart defaults can still affect the rendered manifest."
|
|
306
|
-
diff={valuesDiff.data?.diff || ''}
|
|
307
|
-
isLoading={valuesDiff.isLoading}
|
|
308
|
-
error={valuesDiff.error}
|
|
309
|
-
emptyLabel="No user-supplied value changes found."
|
|
310
|
-
/>
|
|
311
|
-
|
|
312
|
-
<HooksDiffSection diff={hooksDiff.data} isLoading={hooksDiff.isLoading} error={hooksDiff.error} />
|
|
313
|
-
|
|
314
|
-
<DiffSection
|
|
315
|
-
id="notes"
|
|
316
|
-
icon={FileText}
|
|
317
|
-
title="Release notes diff"
|
|
318
|
-
description="NOTES.txt output can reveal chart-level instructions that changed without changing live Kubernetes objects."
|
|
319
|
-
diff={notesDiff.data?.diff || ''}
|
|
320
|
-
isLoading={notesDiff.isLoading}
|
|
321
|
-
error={notesDiff.error}
|
|
322
|
-
emptyLabel="No release notes changes found."
|
|
323
|
-
/>
|
|
324
|
-
|
|
325
|
-
</>
|
|
326
|
-
)}
|
|
327
|
-
</main>
|
|
328
|
-
</div>
|
|
319
|
+
</>
|
|
320
|
+
)}
|
|
321
|
+
</main>
|
|
329
322
|
</div>
|
|
330
|
-
</
|
|
323
|
+
</div>
|
|
331
324
|
</div>
|
|
332
325
|
)
|
|
333
326
|
}
|
|
@@ -18,8 +18,7 @@ import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation, Hel
|
|
|
18
18
|
import { apiVersionToGroup, kindToPluralWithGroup, type NavigateToResource } from '../../utils/navigation'
|
|
19
19
|
import { formatDate } from './helm-utils'
|
|
20
20
|
import { getHelmStatusColor, getKindBadgeColor, getResourceStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
21
|
-
import { useCanHelmAct
|
|
22
|
-
import { RoleGatedPanel } from './RoleGatedPanel'
|
|
21
|
+
import { useCanHelmAct } from '../../api/client'
|
|
23
22
|
import { RevisionHistory } from './RevisionHistory'
|
|
24
23
|
import { ManifestViewer } from './ManifestViewer'
|
|
25
24
|
import { ValuesViewer } from './ValuesViewer'
|
|
@@ -129,14 +128,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
129
128
|
const resizeStartWidth = useRef(DEFAULT_WIDTH)
|
|
130
129
|
const targetVersionRef = useRef('')
|
|
131
130
|
const editedUpgradeYamlRef = useRef('')
|
|
132
|
-
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
133
|
-
// Cloud viewers can't view release manifests / values / diffs
|
|
134
|
-
// (backend gate at requireCloudRole('member')). Skip the queries
|
|
135
|
-
// when the role would 403 — saves a round-trip and avoids a
|
|
136
|
-
// transient error state under the role-gated panel.
|
|
137
|
-
const { canAtLeast } = useCloudRole()
|
|
138
|
-
const canViewSensitive = canAtLeast('member')
|
|
139
131
|
const helmNamespace = release.storageNamespace || release.namespace
|
|
132
|
+
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct(helmNamespace)
|
|
140
133
|
|
|
141
134
|
const { data: releaseDetail, isLoading, error: releaseError, refetch: refetchRelease } = useHelmRelease(
|
|
142
135
|
helmNamespace,
|
|
@@ -149,7 +142,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
149
142
|
helmNamespace,
|
|
150
143
|
release.name,
|
|
151
144
|
selectedRevision,
|
|
152
|
-
canViewSensitive,
|
|
153
145
|
)
|
|
154
146
|
|
|
155
147
|
// Fetch values
|
|
@@ -157,7 +149,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
157
149
|
helmNamespace,
|
|
158
150
|
release.name,
|
|
159
151
|
showAllValues,
|
|
160
|
-
|
|
152
|
+
true,
|
|
161
153
|
selectedRevision,
|
|
162
154
|
)
|
|
163
155
|
const {
|
|
@@ -168,7 +160,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
168
160
|
helmNamespace,
|
|
169
161
|
release.name,
|
|
170
162
|
false,
|
|
171
|
-
|
|
163
|
+
showUpgradeConfirm && adjustValues,
|
|
172
164
|
)
|
|
173
165
|
|
|
174
166
|
// Lazy check for upgrade availability
|
|
@@ -714,7 +706,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
714
706
|
releaseNamespace={releaseDetail.namespace}
|
|
715
707
|
managedByFluxHelmRelease={releaseDetail.managedByFluxHelmRelease}
|
|
716
708
|
hookDiagnostics={releaseDetail.hookDiagnostics}
|
|
717
|
-
canCompare={canViewSensitive}
|
|
718
709
|
onCompare={handleCompareRevisions}
|
|
719
710
|
onNavigateToResource={onNavigateToResource}
|
|
720
711
|
/>
|
|
@@ -732,32 +723,28 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
732
723
|
/>
|
|
733
724
|
)}
|
|
734
725
|
{activeTab === 'manifest' && (
|
|
735
|
-
<
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
/>
|
|
743
|
-
</RoleGatedPanel>
|
|
726
|
+
<ManifestViewer
|
|
727
|
+
manifest={manifest || ''}
|
|
728
|
+
isLoading={manifestLoading}
|
|
729
|
+
revision={selectedRevision}
|
|
730
|
+
onCopy={(text) => copyToClipboard(text, 'manifest')}
|
|
731
|
+
copied={copied === 'manifest'}
|
|
732
|
+
/>
|
|
744
733
|
)}
|
|
745
734
|
{activeTab === 'values' && (
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
/>
|
|
760
|
-
</RoleGatedPanel>
|
|
735
|
+
<ValuesViewer
|
|
736
|
+
values={values}
|
|
737
|
+
isLoading={valuesLoading}
|
|
738
|
+
showAllValues={showAllValues}
|
|
739
|
+
onToggleAllValues={setShowAllValues}
|
|
740
|
+
onCopy={(text) => copyToClipboard(text, 'values')}
|
|
741
|
+
copied={copied === 'values'}
|
|
742
|
+
namespace={helmNamespace}
|
|
743
|
+
name={release.name}
|
|
744
|
+
revision={selectedRevision}
|
|
745
|
+
currentRevision={releaseDetail.revision}
|
|
746
|
+
onApplySuccess={() => refetch()}
|
|
747
|
+
/>
|
|
761
748
|
)}
|
|
762
749
|
{activeTab === 'resources' && (
|
|
763
750
|
<OwnedResources
|
|
@@ -899,7 +886,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
899
886
|
)}
|
|
900
887
|
</div>
|
|
901
888
|
)}
|
|
902
|
-
{upgradeProgress.length === 0 && canHelmWrite &&
|
|
889
|
+
{upgradeProgress.length === 0 && canHelmWrite && (
|
|
903
890
|
<div className="mt-3 border-t border-theme-border pt-3">
|
|
904
891
|
<Tooltip content="Edit the user-supplied Helm values that Radar will pass to this upgrade.">
|
|
905
892
|
<button
|
|
@@ -1067,7 +1054,6 @@ function HelmOperationBanner({
|
|
|
1067
1054
|
releaseNamespace,
|
|
1068
1055
|
managedByFluxHelmRelease,
|
|
1069
1056
|
hookDiagnostics,
|
|
1070
|
-
canCompare,
|
|
1071
1057
|
onCompare,
|
|
1072
1058
|
onNavigateToResource,
|
|
1073
1059
|
}: {
|
|
@@ -1076,7 +1062,6 @@ function HelmOperationBanner({
|
|
|
1076
1062
|
releaseNamespace: string
|
|
1077
1063
|
managedByFluxHelmRelease?: string
|
|
1078
1064
|
hookDiagnostics?: HookDiagnostic[]
|
|
1079
|
-
canCompare?: boolean
|
|
1080
1065
|
onCompare?: (rev1: number, rev2: number) => void
|
|
1081
1066
|
onNavigateToResource?: NavigateToResource
|
|
1082
1067
|
}) {
|
|
@@ -1147,7 +1132,6 @@ function HelmOperationBanner({
|
|
|
1147
1132
|
<OperationInsightSignals
|
|
1148
1133
|
insight={operationInsight}
|
|
1149
1134
|
releaseNamespace={releaseNamespace}
|
|
1150
|
-
canCompare={Boolean(canCompare)}
|
|
1151
1135
|
onCompare={onCompare}
|
|
1152
1136
|
onNavigateToResource={onNavigateToResource}
|
|
1153
1137
|
/>
|
|
@@ -1166,13 +1150,11 @@ function HelmOperationBanner({
|
|
|
1166
1150
|
function OperationInsightSignals({
|
|
1167
1151
|
insight,
|
|
1168
1152
|
releaseNamespace,
|
|
1169
|
-
canCompare,
|
|
1170
1153
|
onCompare,
|
|
1171
1154
|
onNavigateToResource,
|
|
1172
1155
|
}: {
|
|
1173
1156
|
insight?: HelmOperationInsight
|
|
1174
1157
|
releaseNamespace: string
|
|
1175
|
-
canCompare: boolean
|
|
1176
1158
|
onCompare?: (rev1: number, rev2: number) => void
|
|
1177
1159
|
onNavigateToResource?: NavigateToResource
|
|
1178
1160
|
}) {
|
|
@@ -1181,7 +1163,7 @@ function OperationInsightSignals({
|
|
|
1181
1163
|
const relatedCount = primaryResource
|
|
1182
1164
|
? Math.max(0, (insight?.signalCount ?? 0) - 1)
|
|
1183
1165
|
: insight?.relatedResources?.length ?? 0
|
|
1184
|
-
const showCompare = Boolean(compare &&
|
|
1166
|
+
const showCompare = Boolean(compare && onCompare)
|
|
1185
1167
|
if (!primaryResource && !showCompare) {
|
|
1186
1168
|
return null
|
|
1187
1169
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { X, Package, ChevronRight, ChevronLeft, Play, Loader2, AlertTriangle, CheckCircle, User, BookOpen, Link as LinkIcon, Star, BadgeCheck, Shield, Globe, Building2, Plus, Minus, Terminal } from 'lucide-react'
|
|
3
|
-
import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { PaneLoader, Input, useDebouncedValue } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import yaml from 'yaml'
|
|
6
6
|
import { createPatch } from 'diff'
|
|
@@ -66,7 +66,6 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
66
66
|
const progressEndRef = useRef<HTMLDivElement>(null)
|
|
67
67
|
|
|
68
68
|
const queryClient = useQueryClient()
|
|
69
|
-
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
70
69
|
|
|
71
70
|
// Choose the right data based on source
|
|
72
71
|
const isLocal = source === 'local'
|
|
@@ -209,6 +208,10 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
209
208
|
)
|
|
210
209
|
const releaseNameError = releaseNameValidation.valid ? null : releaseNameValidation.error
|
|
211
210
|
const namespaceError = namespaceValidation.valid ? null : namespaceValidation.error
|
|
211
|
+
// Checked in the namespace being installed into, once it's a valid name.
|
|
212
|
+
// Debounced: each new namespace is a capabilities round-trip of several SARs.
|
|
213
|
+
const helmActNamespace = useDebouncedValue(namespaceValidation.valid ? namespace.trim() : undefined, 300)
|
|
214
|
+
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct(helmActNamespace)
|
|
212
215
|
const canProceedFromInfo = releaseNameValidation.valid && namespaceValidation.valid
|
|
213
216
|
const canInstall = canProceedFromInfo && !yamlError
|
|
214
217
|
|
|
@@ -2,7 +2,7 @@ import { useState, useId } from 'react'
|
|
|
2
2
|
import { DialogPortal } from '@skyhook-io/k8s-ui/components/ui/DialogPortal'
|
|
3
3
|
import { X, Plus, Trash2, Link2, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
|
-
import { useHelmOCISources, useAddOCISource, useRemoveOCISource, useClusterInfo, useCapabilities } from '../../api/client'
|
|
5
|
+
import { useHelmOCISources, useAddOCISource, useRemoveOCISource, useClusterInfo, useCapabilities, useCloudRole } from '../../api/client'
|
|
6
6
|
import { OperatorManagedNotice } from '../settings/OperatorManagedNotice'
|
|
7
7
|
import { Input } from '@skyhook-io/k8s-ui'
|
|
8
8
|
import type { UpgradeInfo } from '../../types'
|
|
@@ -58,7 +58,10 @@ export function TrackChartSourceDialog({ open, onClose, chartName, sourceIssue,
|
|
|
58
58
|
const removeSource = useRemoveOCISource()
|
|
59
59
|
const { data: capabilities } = useCapabilities()
|
|
60
60
|
const operatorManaged = capabilities?.configManagement === 'operator'
|
|
61
|
-
|
|
61
|
+
// Chart sources are shared configuration that every user's upgrades resolve
|
|
62
|
+
// against, so in Radar Cloud only owners change them (the server enforces it).
|
|
63
|
+
const ownerOnly = !useCloudRole().canAtLeast('owner')
|
|
64
|
+
const canEdit = capabilities != null && !operatorManaged && !ownerOnly
|
|
62
65
|
|
|
63
66
|
// In-cluster Radar has no `helm registry login` store (the pod's HELM_CONFIG_HOME
|
|
64
67
|
// points at an empty /tmp), so private registries can't authenticate — only
|
|
@@ -102,6 +105,11 @@ export function TrackChartSourceDialog({ open, onClose, chartName, sourceIssue,
|
|
|
102
105
|
|
|
103
106
|
<div className="p-4 space-y-4">
|
|
104
107
|
{operatorManaged && <OperatorManagedNotice helmValue="helm.ociSources" />}
|
|
108
|
+
{!operatorManaged && ownerOnly && (
|
|
109
|
+
<p className="text-sm text-theme-text-secondary">
|
|
110
|
+
Only organization owners can add or remove chart sources.
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
105
113
|
{sourceIssueCopy && (
|
|
106
114
|
<div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-sm">
|
|
107
115
|
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-theme-text-secondary" />
|
|
@@ -46,7 +46,7 @@ export function ValuesViewer({
|
|
|
46
46
|
|
|
47
47
|
const previewMutation = useHelmPreviewValues()
|
|
48
48
|
const applyMutation = useHelmApplyValues()
|
|
49
|
-
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
49
|
+
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct(namespace)
|
|
50
50
|
const isHistoricalRevision = typeof revision === 'number' && typeof currentRevision === 'number' && revision !== currentRevision
|
|
51
51
|
|
|
52
52
|
const canEdit = Boolean(namespace && name) && canHelmWrite && !isHistoricalRevision
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ResourceDetailDrawer as BaseResourceDetailDrawer } from '@skyhook-io/k8s-ui'
|
|
2
2
|
import type { SelectedResource } from '../../types'
|
|
3
3
|
import { WorkloadView } from '../workload/WorkloadView'
|
|
4
|
+
import { CNPGDrawerTrailBack } from '../cnpg/CNPGDrawerTrail'
|
|
4
5
|
|
|
5
6
|
interface ResourceDetailDrawerProps {
|
|
6
7
|
resource: SelectedResource
|
|
@@ -31,6 +32,9 @@ export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
|
|
|
31
32
|
return (
|
|
32
33
|
<BaseResourceDetailDrawer {...props}>
|
|
33
34
|
{({ resource, expanded, active, initialTab, onClose, onExpand, onExpandIntent, onCancelExpandIntent, onBack, onNavigateToResource, onCollapseToDrawer }) => (
|
|
35
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
36
|
+
{!expanded && <CNPGDrawerTrailBack resource={resource} />}
|
|
37
|
+
<div className="min-h-0 flex-1">
|
|
34
38
|
<WorkloadView
|
|
35
39
|
kind={resource.kind}
|
|
36
40
|
namespace={resource.namespace}
|
|
@@ -47,6 +51,8 @@ export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
|
|
|
47
51
|
onNavigateToResource={onNavigateToResource}
|
|
48
52
|
onCollapseToDrawer={onCollapseToDrawer}
|
|
49
53
|
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
50
56
|
)}
|
|
51
57
|
</BaseResourceDetailDrawer>
|
|
52
58
|
)
|
|
@@ -10,6 +10,8 @@ import { useAPIResources } from '../../api/apiResources'
|
|
|
10
10
|
import { useConnection } from '../../context/ConnectionContext'
|
|
11
11
|
import { initNavigationMap, getSecretStoreProviderType } from '@skyhook-io/k8s-ui'
|
|
12
12
|
import { usePinnedKinds } from '../../hooks/useFavorites'
|
|
13
|
+
import { useResourceCounts } from '../../hooks/useResourceCounts'
|
|
14
|
+
import { useCNPGSidebarWorkspace } from '../cnpg/useCNPGSidebarWorkspace'
|
|
13
15
|
import { useOpenLogs, useOpenWorkloadLogs } from '../dock'
|
|
14
16
|
import {
|
|
15
17
|
canBulkRestartKind,
|
|
@@ -26,13 +28,6 @@ import { apiVersionToGroup, kindToPluralWithGroup, type NavigateToResource } fro
|
|
|
26
28
|
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
27
29
|
import { getSkeletonYaml } from '../../utils/skeleton-yaml'
|
|
28
30
|
|
|
29
|
-
interface ResourceCountsResponse {
|
|
30
|
-
counts: Record<string, number>
|
|
31
|
-
forbidden?: string[]
|
|
32
|
-
reasons?: Record<string, string>
|
|
33
|
-
unavailable?: string[]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
interface ResourcesViewProps {
|
|
37
32
|
namespaces: string[]
|
|
38
33
|
selectedResource?: SelectedResource | null
|
|
@@ -114,6 +109,8 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
114
109
|
if (apiResources) initNavigationMap(apiResources)
|
|
115
110
|
}, [apiResources])
|
|
116
111
|
|
|
112
|
+
const cnpgSidebarWorkspace = useCNPGSidebarWorkspace({ apiResources, namespaces })
|
|
113
|
+
|
|
117
114
|
// Track the selected kind from the k8s-ui component
|
|
118
115
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(null)
|
|
119
116
|
const workloadWrites = namespaces.length === 0
|
|
@@ -126,37 +123,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
126
123
|
|
|
127
124
|
// Lightweight resource counts for sidebar badges (~2KB instead of ~608MB)
|
|
128
125
|
const namespacesParam = namespaces.join(',')
|
|
129
|
-
const { data: countsData, isError: countsIsError } =
|
|
130
|
-
queryKey: ['resource-counts', namespacesParam],
|
|
131
|
-
queryFn: async () => {
|
|
132
|
-
const params = new URLSearchParams()
|
|
133
|
-
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
134
|
-
const startedAt = performance.now()
|
|
135
|
-
debugNamespaceLog('resources:counts-fetch-start', { namespaces, params: params.toString() })
|
|
136
|
-
try {
|
|
137
|
-
return await fetchJSON<ResourceCountsResponse>(`/resource-counts?${params}`)
|
|
138
|
-
} finally {
|
|
139
|
-
debugNamespaceLog('resources:counts-fetch-end', {
|
|
140
|
-
namespaces,
|
|
141
|
-
params: params.toString(),
|
|
142
|
-
durationMs: Math.round(performance.now() - startedAt),
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
staleTime: 10000,
|
|
147
|
-
// SSE invalidation isn't running while connecting, and mid-sync counts
|
|
148
|
-
// are what unlatch guarded kinds as their informers finish — poll fast
|
|
149
|
-
// during the shell, settle to the safety net once connected.
|
|
150
|
-
refetchInterval: connection.state === 'connecting' ? 3000 : 60000,
|
|
151
|
-
// During the first seconds of the progressive shell the endpoint 503s
|
|
152
|
-
// (cluster_connecting) until the mid-sync cache handle exists; keep the
|
|
153
|
-
// query pending rather than parking it in error state, which would
|
|
154
|
-
// unlatch the large-list guard at the connected flip.
|
|
155
|
-
retry: (failureCount: number, error: Error) =>
|
|
156
|
-
isStillLoadingError(error) ? true : failureCount < 3,
|
|
157
|
-
retryDelay: (failureCount: number, error: Error) =>
|
|
158
|
-
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
159
|
-
})
|
|
126
|
+
const { data: countsData, isError: countsIsError } = useResourceCounts(namespaces)
|
|
160
127
|
|
|
161
128
|
// Determine if selected kind is a CRD (only CRDs should send ?group= to backend)
|
|
162
129
|
const isSelectedCrd = useMemo(() => {
|
|
@@ -440,6 +407,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
440
407
|
connectionState={connection.state === 'connecting' && connection.syncStatus ? 'syncing' : connection.state}
|
|
441
408
|
largeListGuard={largeListGuard}
|
|
442
409
|
onSelectedKindChange={setSelectedKind}
|
|
410
|
+
sidebarCategoryWorkspaces={cnpgSidebarWorkspace}
|
|
443
411
|
topPodMetrics={topPodMetrics}
|
|
444
412
|
topNodeMetrics={topNodeMetrics}
|
|
445
413
|
certExpiry={certExpiry}
|