@skyhook-io/k8s-ui 1.6.0 → 1.6.2
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 +7 -2
- package/src/components/charts/AreaChart.tsx +371 -0
- package/src/components/charts/MetricsSummary.tsx +49 -0
- package/src/components/charts/SeriesLegend.tsx +27 -0
- package/src/components/charts/colors.ts +49 -0
- package/src/components/charts/format.ts +41 -0
- package/src/components/charts/index.ts +14 -0
- package/src/components/charts/saturation.test.ts +60 -0
- package/src/components/charts/saturation.ts +32 -0
- package/src/components/charts/types.ts +36 -0
- package/src/components/compare/CompareResourcePicker.tsx +192 -0
- package/src/components/compare/CompareTray.tsx +130 -0
- package/src/components/compare/ResourceCompareView.tsx +272 -0
- package/src/components/compare/index.ts +14 -0
- package/src/components/compare/normalize.test.ts +193 -0
- package/src/components/compare/normalize.ts +69 -0
- package/src/components/compare/picks.test.ts +97 -0
- package/src/components/compare/picks.ts +35 -0
- package/src/components/compare/sort.test.ts +78 -0
- package/src/components/compare/sort.ts +26 -0
- package/src/components/compare/types.ts +43 -0
- package/src/components/compare/url.test.ts +61 -0
- package/src/components/compare/url.ts +26 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +621 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +216 -0
- package/src/components/gitops/GitOpsTableView.tsx +1441 -0
- package/src/components/gitops/RollbackDialog.tsx +117 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +160 -0
- package/src/components/gitops/detail-helpers.test.ts +97 -0
- package/src/components/gitops/detail-helpers.ts +112 -0
- package/src/components/gitops/index.ts +52 -1
- package/src/components/gitops/short-cluster-name.test.ts +36 -0
- package/src/components/gitops/tree/index.ts +2 -0
- package/src/components/gitops/tree/merge.test.ts +240 -0
- package/src/components/gitops/tree/merge.ts +160 -0
- package/src/components/resources/ResourcesSidebar.tsx +42 -15
- package/src/components/resources/ResourcesView.tsx +403 -69
- package/src/components/resources/index.ts +1 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +233 -0
- package/src/components/resources/renderers/CompositionRenderer.tsx +218 -0
- package/src/components/resources/renderers/CrossplanePackageRenderer.tsx +145 -0
- package/src/components/resources/renderers/CrossplaneProviderConfigRenderer.tsx +71 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +131 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +223 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +6 -1
- package/src/components/resources/renderers/PodRenderer.tsx +207 -2
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +132 -20
- package/src/components/resources/renderers/RoleRenderer.tsx +148 -18
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +456 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +189 -2
- package/src/components/resources/renderers/XRDRenderer.tsx +153 -0
- package/src/components/resources/renderers/crossplane-cells.tsx +146 -0
- package/src/components/resources/renderers/flux-cells.tsx +12 -0
- package/src/components/resources/renderers/index.ts +8 -0
- package/src/components/resources/resource-utils-crossplane.test.ts +609 -0
- package/src/components/resources/resource-utils-crossplane.ts +325 -0
- package/src/components/resources/resource-utils-flux.ts +14 -0
- package/src/components/resources/resource-utils.ts +1 -0
- package/src/components/resources/resources-column-filter.test.ts +74 -0
- package/src/components/shared/ResourceActionsBar.tsx +77 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +138 -9
- package/src/components/ui/YamlEditor.tsx +28 -15
- package/src/components/ui/drawer-components.tsx +8 -20
- package/src/components/workload/ResourceDetailDrawer.tsx +1 -1
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/index.ts +3 -0
- package/src/types/core.ts +31 -6
- package/src/types/index.ts +1 -0
- package/src/types/rbac.ts +99 -0
- package/src/utils/api-resources.ts +9 -1
- package/src/utils/badge-colors.ts +25 -0
- package/src/utils/gitops-owner.test.ts +70 -111
- package/src/utils/gitops-owner.ts +37 -74
- package/src/utils/helm-status.test.ts +50 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/rbac-badges.ts +61 -0
- package/src/utils/rbac-blast-radius.test.ts +137 -0
- package/src/utils/rbac-blast-radius.ts +98 -0
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
ClusterNetworkPolicyRenderer,
|
|
106
106
|
PodDisruptionBudgetRenderer,
|
|
107
107
|
ServiceAccountRenderer,
|
|
108
|
+
NamespaceRenderer,
|
|
108
109
|
RoleRenderer,
|
|
109
110
|
RoleBindingRenderer,
|
|
110
111
|
WebhookConfigRenderer,
|
|
@@ -199,7 +200,23 @@ import {
|
|
|
199
200
|
AzureManagedControlPlaneRenderer,
|
|
200
201
|
AzureManagedMachinePoolRenderer,
|
|
201
202
|
AzureMachineRenderer,
|
|
203
|
+
ManagedResourceRenderer,
|
|
204
|
+
CompositeRenderer,
|
|
205
|
+
CrossplanePackageRenderer,
|
|
206
|
+
CrossplaneProviderConfigRenderer,
|
|
207
|
+
CompositionRenderer,
|
|
208
|
+
CompositionRevisionRenderer,
|
|
209
|
+
XRDRenderer,
|
|
202
210
|
} from '../resources/renderers'
|
|
211
|
+
import type { ComposedRefStatus } from '../resources/renderers/CompositeRenderer'
|
|
212
|
+
import {
|
|
213
|
+
getCrossplaneStatus,
|
|
214
|
+
getProviderStatus,
|
|
215
|
+
getProviderConfigStatus,
|
|
216
|
+
isManagedResource,
|
|
217
|
+
isComposite,
|
|
218
|
+
isClaim,
|
|
219
|
+
} from '../resources/resource-utils-crossplane'
|
|
203
220
|
import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo, ResolvedEnvFrom, TimelineEvent } from '../../types'
|
|
204
221
|
import type { CopyHandler } from '../ui/drawer-components'
|
|
205
222
|
import { AlertBanner } from '../ui/drawer-components'
|
|
@@ -229,6 +246,51 @@ export interface RendererOverrides {
|
|
|
229
246
|
kind: string; data: any
|
|
230
247
|
onNavigate?: (ref: ResourceRef) => void
|
|
231
248
|
}>
|
|
249
|
+
// Optional override for Crossplane Composite / Claim — host wraps the
|
|
250
|
+
// package renderer to fan out per-composed-ref status fetches via React Query.
|
|
251
|
+
CompositeRenderer?: React.ComponentType<{
|
|
252
|
+
data: any
|
|
253
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
254
|
+
composedRefStatuses?: Map<string, ComposedRefStatus>
|
|
255
|
+
}>
|
|
256
|
+
// ServiceAccount reverse-lookup: the host fetches /api/rbac/subject/... and
|
|
257
|
+
// feeds the result into the base renderer via this wrapper.
|
|
258
|
+
ServiceAccountRenderer?: React.ComponentType<{
|
|
259
|
+
data: any
|
|
260
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
261
|
+
}>
|
|
262
|
+
// Role / ClusterRole reverse-lookup: host fetches /api/rbac/role/... so the
|
|
263
|
+
// detail page can show "who is bound to this role".
|
|
264
|
+
RoleRenderer?: React.ComponentType<{
|
|
265
|
+
data: any
|
|
266
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
267
|
+
}>
|
|
268
|
+
// RoleBinding inline rules preview: host fetches the referenced Role/
|
|
269
|
+
// ClusterRole's rules so the binding view can show what's granted without
|
|
270
|
+
// a navigation step.
|
|
271
|
+
RoleBindingRenderer?: React.ComponentType<{
|
|
272
|
+
data: any
|
|
273
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
274
|
+
}>
|
|
275
|
+
// Namespace RBAC summary: host fetches /api/rbac/namespace/{ns} so the
|
|
276
|
+
// namespace page can show bindings configured here without falling
|
|
277
|
+
// through to GenericRenderer.
|
|
278
|
+
NamespaceRenderer?: React.ComponentType<{
|
|
279
|
+
data: any
|
|
280
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
281
|
+
}>
|
|
282
|
+
// HPA: host wraps the base renderer to add Prometheus-backed replicas /
|
|
283
|
+
// metric charts below the static spec data.
|
|
284
|
+
HPARenderer?: React.ComponentType<{
|
|
285
|
+
data: any
|
|
286
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
287
|
+
}>
|
|
288
|
+
// PVC: host wraps the base renderer to add a kubelet-derived usage gauge
|
|
289
|
+
// when Prometheus is scraping kubelet endpoints.
|
|
290
|
+
PVCRenderer?: React.ComponentType<{
|
|
291
|
+
data: any
|
|
292
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
293
|
+
}>
|
|
232
294
|
}
|
|
233
295
|
|
|
234
296
|
// Known resource types with specific renderers (module-level to avoid re-allocation)
|
|
@@ -243,7 +305,7 @@ const KNOWN_KINDS = new Set([
|
|
|
243
305
|
'networkpolicies', 'networkpolicy',
|
|
244
306
|
'ciliumnetworkpolicies', 'ciliumnetworkpolicy', 'ciliumclusterwidenetworkpolicies', 'ciliumclusterwidenetworkpolicy',
|
|
245
307
|
'clusternetworkpolicies', 'clusternetworkpolicy',
|
|
246
|
-
'poddisruptionbudgets', 'serviceaccounts',
|
|
308
|
+
'poddisruptionbudgets', 'serviceaccounts', 'namespaces',
|
|
247
309
|
'roles', 'clusterroles', 'rolebindings', 'clusterrolebindings',
|
|
248
310
|
'events', 'gitrepositories', 'ocirepositories', 'helmrepositories',
|
|
249
311
|
'kustomizations', 'helmreleases', 'alerts', 'applications',
|
|
@@ -282,6 +344,12 @@ const KNOWN_KINDS = new Set([
|
|
|
282
344
|
// Azure CAPI Infrastructure Provider
|
|
283
345
|
'azuremanagedcontrolplanes', 'azuremanagedmachinepools', 'azuremachines',
|
|
284
346
|
'azuremachinetemplates', 'azuremanagedclusters',
|
|
347
|
+
// Crossplane core (Managed Resources, Composites, and Claims are detected
|
|
348
|
+
// dynamically by spec shape — their plurals are unbounded so they're handled
|
|
349
|
+
// via fall-through, not enumerated in KNOWN_KINDS).
|
|
350
|
+
'providers', 'providerconfigs',
|
|
351
|
+
'compositeresourcedefinitions', 'compositions', 'compositionrevisions',
|
|
352
|
+
'functions', 'configurations',
|
|
285
353
|
])
|
|
286
354
|
|
|
287
355
|
// ============================================================================
|
|
@@ -355,12 +423,47 @@ export function ResourceRendererDispatch({
|
|
|
355
423
|
}: ResourceRendererDispatchProps) {
|
|
356
424
|
const kind = resource.kind.toLowerCase()
|
|
357
425
|
|
|
358
|
-
|
|
426
|
+
// Crossplane Managed Resources / Composites / Claims are detected by spec
|
|
427
|
+
// shape because their plurals are unbounded (one CRD kind per provider
|
|
428
|
+
// service). These flags suppress the GenericRenderer fall-through and route
|
|
429
|
+
// to the right Crossplane renderer below.
|
|
430
|
+
const isCrossplaneMR = isManagedResource(data)
|
|
431
|
+
const isCrossplaneClaim = !isCrossplaneMR && isClaim(data)
|
|
432
|
+
const isCrossplaneXR = !isCrossplaneMR && !isCrossplaneClaim && isComposite(data)
|
|
433
|
+
|
|
434
|
+
// Crossplane plurals that collide with foreign CRDs — `configurations`
|
|
435
|
+
// overlaps Knative serving.knative.dev/Configuration, `functions` could
|
|
436
|
+
// collide with OpenFaaS, `compositions` is in this list because its
|
|
437
|
+
// render line is apiVersion-gated. We add these to KNOWN_KINDS so the
|
|
438
|
+
// Crossplane renderer wins on apiVersion match, but a foreign CR with
|
|
439
|
+
// the same plural needs to fall through to GenericRenderer — otherwise
|
|
440
|
+
// it renders blank (no Crossplane match + isKnownKind suppresses
|
|
441
|
+
// generic). Only kinds whose render lines are apiVersion-gated belong
|
|
442
|
+
// here; `providerconfigs`/`compositionrevisions`/`compositeresource-
|
|
443
|
+
// definitions` are Crossplane-specific plurals with no realistic
|
|
444
|
+
// collision and unguarded render lines, so including them would risk
|
|
445
|
+
// double-render for foreign CRDs we'll never see.
|
|
446
|
+
const isCollisionGatedKind =
|
|
447
|
+
kind === 'providers' || kind === 'functions' || kind === 'configurations' || kind === 'compositions'
|
|
448
|
+
const crossplaneApiVersionMatched = isCollisionGatedKind && (
|
|
449
|
+
data?.apiVersion?.startsWith('pkg.crossplane.io/')
|
|
450
|
+
|| data?.apiVersion?.startsWith('apiextensions.crossplane.io/')
|
|
451
|
+
)
|
|
452
|
+
const crossplaneCollisionFallthrough = isCollisionGatedKind && !crossplaneApiVersionMatched
|
|
453
|
+
|
|
454
|
+
const isKnownKind = KNOWN_KINDS.has(kind) || isCrossplaneMR || isCrossplaneClaim || isCrossplaneXR
|
|
359
455
|
|
|
360
456
|
const PodComp = rendererOverrides?.PodRenderer ?? PodRenderer
|
|
361
457
|
const WorkloadComp = rendererOverrides?.WorkloadRenderer ?? WorkloadRenderer
|
|
362
458
|
const NodeComp = rendererOverrides?.NodeRenderer ?? NodeRenderer
|
|
363
459
|
const ServiceComp = rendererOverrides?.ServiceRenderer ?? ServiceRenderer
|
|
460
|
+
const CompositeComp = rendererOverrides?.CompositeRenderer ?? CompositeRenderer
|
|
461
|
+
const ServiceAccountComp = rendererOverrides?.ServiceAccountRenderer ?? ServiceAccountRenderer
|
|
462
|
+
const RoleComp = rendererOverrides?.RoleRenderer ?? RoleRenderer
|
|
463
|
+
const RoleBindingComp = rendererOverrides?.RoleBindingRenderer ?? RoleBindingRenderer
|
|
464
|
+
const NamespaceComp = rendererOverrides?.NamespaceRenderer ?? NamespaceRenderer
|
|
465
|
+
const HPAComp = rendererOverrides?.HPARenderer ?? HPARenderer
|
|
466
|
+
const PVCComp = rendererOverrides?.PVCRenderer ?? PVCRenderer
|
|
364
467
|
|
|
365
468
|
const sidebarContent = showCommonSections && (
|
|
366
469
|
<>
|
|
@@ -385,9 +488,9 @@ export function ResourceRendererDispatch({
|
|
|
385
488
|
{kind === 'secrets' && <SecretRenderer data={data} certificateInfo={certificateInfo} resourceData={data} onSaveSecretValue={onSaveSecretValue} isSaving={isSavingSecret} />}
|
|
386
489
|
{kind === 'jobs' && <JobRenderer data={data} />}
|
|
387
490
|
{kind === 'cronjobs' && <CronJobRenderer data={data} onNavigate={onNavigate} />}
|
|
388
|
-
{(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <
|
|
491
|
+
{(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <HPAComp data={data} onNavigate={onNavigate} />}
|
|
389
492
|
{kind === 'nodes' && <NodeComp data={data} relationships={relationships} />}
|
|
390
|
-
{kind === 'persistentvolumeclaims' && <
|
|
493
|
+
{kind === 'persistentvolumeclaims' && <PVCComp data={data} onNavigate={onNavigate} />}
|
|
391
494
|
{kind === 'rollouts' && <RolloutRenderer data={data} />}
|
|
392
495
|
{kind === 'certificates' && !data?.apiVersion?.includes('networking.internal.knative.dev') && <CertificateRenderer data={data} />}
|
|
393
496
|
{kind === 'workflows' && <WorkflowRenderer data={data} />}
|
|
@@ -410,9 +513,10 @@ export function ResourceRendererDispatch({
|
|
|
410
513
|
{(kind === 'ciliumnetworkpolicies' || kind === 'ciliumnetworkpolicy' || kind === 'ciliumclusterwidenetworkpolicies' || kind === 'ciliumclusterwidenetworkpolicy') && <CiliumNetworkPolicyRenderer data={data} />}
|
|
411
514
|
{(kind === 'clusternetworkpolicies' || kind === 'clusternetworkpolicy') && <ClusterNetworkPolicyRenderer data={data} />}
|
|
412
515
|
{kind === 'poddisruptionbudgets' && <PodDisruptionBudgetRenderer data={data} />}
|
|
413
|
-
{kind === 'serviceaccounts' && <
|
|
414
|
-
{
|
|
415
|
-
{(kind === '
|
|
516
|
+
{kind === 'serviceaccounts' && <ServiceAccountComp data={data} onNavigate={onNavigate} />}
|
|
517
|
+
{kind === 'namespaces' && <NamespaceComp data={data} onNavigate={onNavigate} />}
|
|
518
|
+
{(kind === 'roles' || kind === 'clusterroles') && <RoleComp data={data} onNavigate={onNavigate} />}
|
|
519
|
+
{(kind === 'rolebindings' || kind === 'clusterrolebindings') && <RoleBindingComp data={data} onNavigate={onNavigate} />}
|
|
416
520
|
{kind === 'events' && <EventRenderer data={data} onNavigate={onNavigate} />}
|
|
417
521
|
{kind === 'gitrepositories' && <GitRepositoryRenderer data={data} />}
|
|
418
522
|
{kind === 'ocirepositories' && <OCIRepositoryRenderer data={data} />}
|
|
@@ -525,8 +629,23 @@ export function ResourceRendererDispatch({
|
|
|
525
629
|
{/* Contour */}
|
|
526
630
|
{kind === 'httpproxies' && <ContourHTTPProxyRenderer data={data} onNavigate={onNavigate} />}
|
|
527
631
|
|
|
528
|
-
{/*
|
|
529
|
-
|
|
632
|
+
{/* Crossplane — kind-dispatched for the static package/config kinds, spec-shape
|
|
633
|
+
detected for MR/XR/Claim (their plurals are unbounded). */}
|
|
634
|
+
{kind === 'providers' && data?.apiVersion?.startsWith('pkg.crossplane.io/') && <CrossplanePackageRenderer data={data} kindLabel="Provider" onNavigate={onNavigate} />}
|
|
635
|
+
{kind === 'functions' && data?.apiVersion?.startsWith('pkg.crossplane.io/') && <CrossplanePackageRenderer data={data} kindLabel="Function" onNavigate={onNavigate} />}
|
|
636
|
+
{kind === 'configurations' && data?.apiVersion?.startsWith('pkg.crossplane.io/') && <CrossplanePackageRenderer data={data} kindLabel="Configuration" onNavigate={onNavigate} />}
|
|
637
|
+
{kind === 'providerconfigs' && <CrossplaneProviderConfigRenderer data={data} onNavigate={onNavigate} />}
|
|
638
|
+
{kind === 'compositeresourcedefinitions' && <XRDRenderer data={data} onNavigate={onNavigate} />}
|
|
639
|
+
{kind === 'compositions' && data?.apiVersion?.startsWith('apiextensions.crossplane.io/') && <CompositionRenderer data={data} onNavigate={onNavigate} />}
|
|
640
|
+
{kind === 'compositionrevisions' && <CompositionRevisionRenderer data={data} onNavigate={onNavigate} />}
|
|
641
|
+
{isCrossplaneMR && <ManagedResourceRenderer data={data} onNavigate={onNavigate} />}
|
|
642
|
+
{(isCrossplaneXR || isCrossplaneClaim) && <CompositeComp data={data} onNavigate={onNavigate} />}
|
|
643
|
+
|
|
644
|
+
{/* Generic renderer for CRDs and unknown resource types — also fires
|
|
645
|
+
for known-plural collisions where no apiVersion-gated renderer
|
|
646
|
+
matched (e.g. a Knative Configuration sharing the `configurations`
|
|
647
|
+
plural with Crossplane Configuration). */}
|
|
648
|
+
{(!isKnownKind || crossplaneCollisionFallthrough) && <GenericRenderer data={data} />}
|
|
530
649
|
|
|
531
650
|
{/* Common sections - can be disabled when parent handles them separately */}
|
|
532
651
|
{showCommonSections && (
|
|
@@ -662,6 +781,16 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
662
781
|
if (k === 'peerauthentications') return getPeerAuthenticationStatus(data)
|
|
663
782
|
if (k === 'authorizationpolicies') return getAuthorizationPolicyStatus(data)
|
|
664
783
|
|
|
784
|
+
// Crossplane — Provider/Function/Configuration share package conditions;
|
|
785
|
+
// ProviderConfig has its own; MR/XR/Claim detected by spec shape.
|
|
786
|
+
if ((k === 'providers' || k === 'functions' || k === 'configurations') && data?.apiVersion?.startsWith('pkg.crossplane.io/')) {
|
|
787
|
+
return getProviderStatus(data)
|
|
788
|
+
}
|
|
789
|
+
if (k === 'providerconfigs') return getProviderConfigStatus(data)
|
|
790
|
+
if (isManagedResource(data) || isComposite(data) || isClaim(data)) {
|
|
791
|
+
return getCrossplaneStatus(data)
|
|
792
|
+
}
|
|
793
|
+
|
|
665
794
|
// Contour HTTPProxy
|
|
666
795
|
if (k === 'httpproxies') {
|
|
667
796
|
const s = getHTTPProxyStatus(data)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useCallback, useEffect } from 'react'
|
|
2
|
-
import Editor, { OnMount, OnChange, type Monaco } from '@monaco-editor/react'
|
|
2
|
+
import Editor, { DiffEditor, OnMount, OnChange, type Monaco } from '@monaco-editor/react'
|
|
3
|
+
import { PaneLoader } from './PaneLoader'
|
|
3
4
|
import type { editor } from 'monaco-editor'
|
|
4
5
|
|
|
5
6
|
interface YamlEditorProps {
|
|
@@ -240,40 +241,52 @@ export function YamlEditor({
|
|
|
240
241
|
)
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
// Diff editor for showing changes before apply
|
|
244
244
|
interface YamlDiffEditorProps {
|
|
245
245
|
original: string
|
|
246
246
|
modified: string
|
|
247
247
|
height?: string | number
|
|
248
|
+
/** When true, render unified (single column) instead of side-by-side. */
|
|
249
|
+
unified?: boolean
|
|
250
|
+
/** When true, only diff regions stay rendered — unchanged sections collapse. */
|
|
251
|
+
hideUnchanged?: boolean
|
|
252
|
+
/** Caller-controlled theme. Defaults to dark to match YamlEditor. */
|
|
253
|
+
theme?: 'vs-dark' | 'vs'
|
|
254
|
+
/** When true, drops the rounded border so the editor reaches its container's edges. */
|
|
255
|
+
bleed?: boolean
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
export function YamlDiffEditor({
|
|
251
|
-
original
|
|
259
|
+
original,
|
|
252
260
|
modified,
|
|
253
261
|
height = '100%',
|
|
262
|
+
unified = false,
|
|
263
|
+
hideUnchanged = false,
|
|
264
|
+
theme = 'vs-dark',
|
|
265
|
+
bleed = false,
|
|
254
266
|
}: YamlDiffEditorProps) {
|
|
255
|
-
// TODO: Use Monaco DiffEditor to show actual diff
|
|
256
|
-
void _original
|
|
257
267
|
return (
|
|
258
|
-
<div className=
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
<div className={bleed ? 'overflow-hidden' : 'rounded-lg overflow-hidden border border-theme-border'} style={{ height }}>
|
|
269
|
+
<DiffEditor
|
|
270
|
+
original={original}
|
|
271
|
+
modified={modified}
|
|
272
|
+
language="yaml"
|
|
273
|
+
theme={theme}
|
|
263
274
|
options={{
|
|
264
275
|
readOnly: true,
|
|
276
|
+
renderSideBySide: !unified,
|
|
277
|
+
hideUnchangedRegions: { enabled: hideUnchanged },
|
|
265
278
|
minimap: { enabled: false },
|
|
266
279
|
lineNumbers: 'on',
|
|
267
280
|
scrollBeyondLastLine: false,
|
|
281
|
+
wordWrap: 'on',
|
|
268
282
|
fontSize: 13,
|
|
269
283
|
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
270
284
|
padding: { top: 12, bottom: 12 },
|
|
285
|
+
renderOverviewRuler: true,
|
|
286
|
+
ignoreTrimWhitespace: false,
|
|
287
|
+
automaticLayout: true,
|
|
271
288
|
}}
|
|
272
|
-
loading={
|
|
273
|
-
<div className="flex items-center justify-center h-full bg-theme-surface text-theme-text-secondary">
|
|
274
|
-
Loading diff...
|
|
275
|
-
</div>
|
|
276
|
-
}
|
|
289
|
+
loading={<PaneLoader label="Loading resources…" className="h-full" />}
|
|
277
290
|
/>
|
|
278
291
|
</div>
|
|
279
292
|
)
|
|
@@ -724,7 +724,8 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
724
724
|
(relationships.configRefs && relationships.configRefs.length > 0) ||
|
|
725
725
|
(relationships.consumers && relationships.consumers.length > 0) ||
|
|
726
726
|
(relationships.scalers && relationships.scalers.length > 0) ||
|
|
727
|
-
(relationships.
|
|
727
|
+
(relationships.pdbs && relationships.pdbs.length > 0) ||
|
|
728
|
+
(relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
|
|
728
729
|
relationships.scaleTarget
|
|
729
730
|
|
|
730
731
|
if (!hasRelationships) return null
|
|
@@ -765,25 +766,12 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
765
766
|
{relationships.scalers && relationships.scalers.length > 0 && (
|
|
766
767
|
<RelationshipGroup label="Autoscaler" refs={dedupeRefs(relationships.scalers)} onNavigate={onNavigate} />
|
|
767
768
|
)}
|
|
768
|
-
{relationships.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
<>
|
|
775
|
-
{pdbs.length > 0 && (
|
|
776
|
-
<RelationshipGroup label="Disruption Budget" refs={dedupeRefs(pdbs)} onNavigate={onNavigate} />
|
|
777
|
-
)}
|
|
778
|
-
{netpols.length > 0 && (
|
|
779
|
-
<RelationshipGroup label="Network Policies" refs={dedupeRefs(netpols)} onNavigate={onNavigate} />
|
|
780
|
-
)}
|
|
781
|
-
{other.length > 0 && (
|
|
782
|
-
<RelationshipGroup label="Policies" refs={dedupeRefs(other)} onNavigate={onNavigate} />
|
|
783
|
-
)}
|
|
784
|
-
</>
|
|
785
|
-
)
|
|
786
|
-
})()}
|
|
769
|
+
{relationships.pdbs && relationships.pdbs.length > 0 && (
|
|
770
|
+
<RelationshipGroup label="Disruption Budget" refs={dedupeRefs(relationships.pdbs)} onNavigate={onNavigate} />
|
|
771
|
+
)}
|
|
772
|
+
{relationships.networkPolicies && relationships.networkPolicies.length > 0 && (
|
|
773
|
+
<RelationshipGroup label="Network Policies" refs={dedupeRefs(relationships.networkPolicies)} onNavigate={onNavigate} />
|
|
774
|
+
)}
|
|
787
775
|
{relationships.scaleTarget && (
|
|
788
776
|
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
789
777
|
)}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom } from '../../types'
|
|
24
24
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
25
25
|
import { refToSelectedResource, pluralToKind } from '../../utils/navigation'
|
|
26
|
-
import {
|
|
26
|
+
import { gitOpsOwnerFromRelationships, type GitOpsOwnerRef } from '../../utils/gitops-owner'
|
|
27
27
|
import { gitOpsRouteForResource } from '../../utils/gitops-route'
|
|
28
28
|
import { isChangeEvent, isHistoricalEvent } from '../../types'
|
|
29
29
|
import { getKindBadgeColor, getHealthBadgeColor } from '../../utils/badge-colors'
|
|
@@ -311,7 +311,7 @@ export function WorkloadView({
|
|
|
311
311
|
|
|
312
312
|
// Metadata
|
|
313
313
|
const metadata = useMemo(() => extractMetadata(kind, resource), [kind, resource])
|
|
314
|
-
const gitopsOwner = useMemo(() =>
|
|
314
|
+
const gitopsOwner = useMemo(() => gitOpsOwnerFromRelationships(relationships), [relationships])
|
|
315
315
|
// When the resource itself is a portal GitOps CR (Application, Kustomization,
|
|
316
316
|
// HelmRelease, etc.), surface a link to its dedicated GitOps detail page —
|
|
317
317
|
// the drawer's renderer is thorough but the tab has the tree + insights +
|
package/src/index.ts
CHANGED
|
@@ -42,3 +42,6 @@ export * from './components/audit'
|
|
|
42
42
|
|
|
43
43
|
// Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
|
|
44
44
|
export * from './components/cluster-switcher'
|
|
45
|
+
|
|
46
|
+
// Compare (ResourceCompareView, CompareResourcePicker, normalize utilities)
|
|
47
|
+
export * from './components/compare'
|
package/src/types/core.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Topology types matching the Go backend
|
|
2
2
|
|
|
3
|
-
// Per-resource-type RBAC permissions
|
|
3
|
+
// Per-resource-type RBAC permissions. Field names must match the JSON keys
|
|
4
|
+
// produced by ResourcePermissions in internal/k8s/capabilities.go — there
|
|
5
|
+
// is no automated check across the Go/TS boundary.
|
|
4
6
|
export interface ResourcePermissions {
|
|
5
7
|
pods: boolean
|
|
6
8
|
services: boolean
|
|
@@ -12,20 +14,39 @@ export interface ResourcePermissions {
|
|
|
12
14
|
configMaps: boolean
|
|
13
15
|
secrets: boolean
|
|
14
16
|
events: boolean
|
|
15
|
-
|
|
17
|
+
persistentVolumeClaims: boolean
|
|
16
18
|
nodes: boolean
|
|
17
19
|
namespaces: boolean
|
|
18
20
|
jobs: boolean
|
|
19
21
|
cronJobs: boolean
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
horizontalPodAutoscalers: boolean
|
|
23
|
+
persistentVolumes: boolean
|
|
24
|
+
storageClasses: boolean
|
|
25
|
+
podDisruptionBudgets: boolean
|
|
26
|
+
networkPolicies: boolean
|
|
27
|
+
serviceAccounts: boolean
|
|
23
28
|
roles: boolean
|
|
24
29
|
clusterRoles: boolean
|
|
25
30
|
roleBindings: boolean
|
|
26
31
|
clusterRoleBindings: boolean
|
|
32
|
+
limitRanges: boolean
|
|
33
|
+
gateways: boolean
|
|
34
|
+
httpRoutes: boolean
|
|
35
|
+
verticalPodAutoscalers: boolean
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
// Keys in ResourcePermissions that represent optional CRDs Radar can monitor
|
|
39
|
+
// when they're installed in the cluster. A `false` here means "CRD not
|
|
40
|
+
// installed (or RBAC denied)" — NOT "Radar is missing data the user expects",
|
|
41
|
+
// so banners about RBAC restrictions should ignore these keys.
|
|
42
|
+
//
|
|
43
|
+
// Keep in sync with dynamicCapabilityKinds in internal/k8s/capabilities_alignment_test.go.
|
|
44
|
+
export const OPTIONAL_RESOURCE_KINDS: ReadonlyArray<keyof ResourcePermissions> = [
|
|
45
|
+
'gateways',
|
|
46
|
+
'httpRoutes',
|
|
47
|
+
'verticalPodAutoscalers',
|
|
48
|
+
]
|
|
49
|
+
|
|
29
50
|
// Feature capabilities based on RBAC permissions
|
|
30
51
|
export interface Capabilities {
|
|
31
52
|
exec: boolean // Terminal feature (pods/exec)
|
|
@@ -394,6 +415,7 @@ export interface ResourceRef {
|
|
|
394
415
|
export interface Relationships {
|
|
395
416
|
owner?: ResourceRef
|
|
396
417
|
deployment?: ResourceRef // Grandparent Deployment (for Pods owned by ReplicaSets)
|
|
418
|
+
managedBy?: ResourceRef[] // Topmost meaningful manager(s): GitOps controller (ArgoCD Application / Flux Kustomization / Flux HelmRelease), Helm release, or the topmost K8s owner. Synthesized server-side; replaces client-side detectGitOpsOwner.
|
|
397
419
|
children?: ResourceRef[]
|
|
398
420
|
services?: ResourceRef[]
|
|
399
421
|
ingresses?: ResourceRef[]
|
|
@@ -403,8 +425,11 @@ export interface Relationships {
|
|
|
403
425
|
consumers?: ResourceRef[]
|
|
404
426
|
scalers?: ResourceRef[]
|
|
405
427
|
scaleTarget?: ResourceRef
|
|
406
|
-
|
|
428
|
+
pdbs?: ResourceRef[] // PodDisruptionBudgets protecting this workload
|
|
429
|
+
networkPolicies?: ResourceRef[] // NetworkPolicy / CiliumNetworkPolicy / ClusterNetworkPolicy variants selecting this workload
|
|
407
430
|
pods?: ResourceRef[]
|
|
431
|
+
serviceAccount?: ResourceRef // For Pods: derived from pod.spec.serviceAccountName
|
|
432
|
+
node?: ResourceRef // For scheduled Pods: derived from pod.spec.nodeName
|
|
408
433
|
}
|
|
409
434
|
|
|
410
435
|
// Parsed X.509 certificate metadata (from backend cert parsing)
|
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Wire types for the /api/rbac/* endpoints, kept in the shared package so
|
|
2
|
+
// both the host (which fetches) and the renderers (which display) agree on
|
|
3
|
+
// the shape. Mirrors the Go types in internal/server/rbac_handlers.go and
|
|
4
|
+
// pkg/rbac/index.go.
|
|
5
|
+
|
|
6
|
+
export interface RBACSubject {
|
|
7
|
+
kind: string // "ServiceAccount" | "User" | "Group"
|
|
8
|
+
namespace: string // empty for User/Group
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RBACRoleRef {
|
|
13
|
+
kind: string // "Role" | "ClusterRole"
|
|
14
|
+
namespace: string // empty for ClusterRole
|
|
15
|
+
name: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RBACBindingRef {
|
|
19
|
+
kind: string // "RoleBinding" | "ClusterRoleBinding"
|
|
20
|
+
namespace: string // empty for ClusterRoleBinding
|
|
21
|
+
name: string
|
|
22
|
+
roleRef: RBACRoleRef
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RBACPolicyRule {
|
|
26
|
+
verbs?: string[]
|
|
27
|
+
apiGroups?: string[]
|
|
28
|
+
resources?: string[]
|
|
29
|
+
resourceNames?: string[]
|
|
30
|
+
nonResourceURLs?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RBACBindingRules {
|
|
34
|
+
binding: RBACBindingRef
|
|
35
|
+
role: RBACRoleRef
|
|
36
|
+
rules: RBACPolicyRule[]
|
|
37
|
+
/** Populated when a RoleBinding references a ClusterRole — rules apply
|
|
38
|
+
* only in this namespace. */
|
|
39
|
+
scopeNamespace?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RBACInheritedGroup {
|
|
43
|
+
groupName: string // e.g. "system:authenticated"
|
|
44
|
+
bindings: RBACBindingRules[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RBACSubjectResponse {
|
|
48
|
+
subject: RBACSubject
|
|
49
|
+
direct: RBACBindingRules[]
|
|
50
|
+
inheritedFromGroups: RBACInheritedGroup[]
|
|
51
|
+
flat: RBACPolicyRule[]
|
|
52
|
+
truncated: boolean
|
|
53
|
+
/** Pods whose spec.serviceAccountName references this SA. Populated only
|
|
54
|
+
* for ServiceAccount subjects; undefined or empty for User/Group. */
|
|
55
|
+
usedByPods?: RBACPodRef[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RBACPodRef {
|
|
59
|
+
namespace: string
|
|
60
|
+
name: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RBACNamespaceResponse {
|
|
64
|
+
namespace: string
|
|
65
|
+
roleBindings: RBACBindingWithSubjects[]
|
|
66
|
+
/** ClusterRoleBindings with at least one subject in this namespace. */
|
|
67
|
+
clusterRoleBindingsWithLocalSubject: RBACBindingWithSubjects[]
|
|
68
|
+
serviceAccountCount: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RBACBindingWithSubjects {
|
|
72
|
+
binding: RBACBindingRef
|
|
73
|
+
subjects: RBACSubject[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RBACRoleResponse {
|
|
77
|
+
role: RBACRoleRef
|
|
78
|
+
bindings: RBACBindingWithSubjects[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RBACResourceRule {
|
|
82
|
+
verbs: string[]
|
|
83
|
+
apiGroups?: string[]
|
|
84
|
+
resources?: string[]
|
|
85
|
+
resourceNames?: string[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface RBACNonResourceRule {
|
|
89
|
+
verbs: string[]
|
|
90
|
+
nonResourceURLs?: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface RBACWhoamiResponse {
|
|
94
|
+
namespace: string
|
|
95
|
+
resourceRules: RBACResourceRule[]
|
|
96
|
+
nonResourceRules: RBACNonResourceRule[]
|
|
97
|
+
incomplete: boolean
|
|
98
|
+
evaluationError?: string
|
|
99
|
+
}
|
|
@@ -124,8 +124,11 @@ export function formatGroupName(group: string): string {
|
|
|
124
124
|
'gateway.networking.k8s.io': 'Gateway API',
|
|
125
125
|
'traefik.io': 'Traefik',
|
|
126
126
|
'traefik.containo.us': 'Traefik',
|
|
127
|
+
'crossplane.io': 'Crossplane',
|
|
127
128
|
'pkg.crossplane.io': 'Crossplane',
|
|
128
129
|
'apiextensions.crossplane.io': 'Crossplane',
|
|
130
|
+
'helm.crossplane.io': 'Crossplane',
|
|
131
|
+
'kubernetes.crossplane.io': 'Crossplane',
|
|
129
132
|
'source.toolkit.fluxcd.io': 'Flux',
|
|
130
133
|
'helm.toolkit.fluxcd.io': 'Flux',
|
|
131
134
|
'kustomize.toolkit.fluxcd.io': 'Flux',
|
|
@@ -167,7 +170,12 @@ export function formatGroupName(group: string): string {
|
|
|
167
170
|
'kubeflow.org': 'Kubeflow',
|
|
168
171
|
'snapshot.storage.k8s.io': 'Snapshots',
|
|
169
172
|
}
|
|
170
|
-
|
|
173
|
+
if (knownGroups[group]) return knownGroups[group]
|
|
174
|
+
// Suffix rules — for unbounded provider group sets (Crossplane providers ship
|
|
175
|
+
// their own per-service groups like s3.aws.upbound.io, compute.gcp.upbound.io).
|
|
176
|
+
if (group.endsWith('.upbound.io')) return 'Crossplane'
|
|
177
|
+
if (group.endsWith('.crossplane.io')) return 'Crossplane'
|
|
178
|
+
return group
|
|
171
179
|
}
|
|
172
180
|
|
|
173
181
|
export function shortenGroupName(group: string): string {
|
|
@@ -286,6 +286,31 @@ export function getHelmStatusColor(status: string): string {
|
|
|
286
286
|
return HELM_STATUS_COLORS[statusLower] || 'bg-theme-hover/50 text-theme-text-secondary'
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Helm release statuses where the row UI should signpost the user
|
|
291
|
+
* toward the drawer (history / rollback / logs).
|
|
292
|
+
*
|
|
293
|
+
* Currently `failed` only. The `pending-*` statuses (install /
|
|
294
|
+
* upgrade / rollback) are excluded deliberately: they're Helm's
|
|
295
|
+
* normal in-flight states during every routine operation. Treating
|
|
296
|
+
* them as "actionable" would briefly attach an alarming chevron +
|
|
297
|
+
* tooltip to every install while it ran — indistinguishable from
|
|
298
|
+
* the genuinely-stuck case (controller crashed mid-flight). Until
|
|
299
|
+
* we have release age available client-side to disambiguate
|
|
300
|
+
* "in-flight" from "stuck > N min", we give up the stuck-detect
|
|
301
|
+
* signpost rather than wrongly alarm the common case.
|
|
302
|
+
*
|
|
303
|
+
* @see https://github.com/helm/helm/blob/dev-v3/pkg/release/status.go
|
|
304
|
+
*/
|
|
305
|
+
const ACTIONABLE_HELM_STATUSES: ReadonlySet<string> = new Set([
|
|
306
|
+
'failed',
|
|
307
|
+
])
|
|
308
|
+
|
|
309
|
+
export function isHelmReleaseActionable(status: string | null | undefined): boolean {
|
|
310
|
+
if (!status) return false
|
|
311
|
+
return ACTIONABLE_HELM_STATUSES.has(status.toLowerCase())
|
|
312
|
+
}
|
|
313
|
+
|
|
289
314
|
// =============================================================================
|
|
290
315
|
// VULNERABILITY SEVERITY COLORS - for Trivy and other security scanners
|
|
291
316
|
// =============================================================================
|