@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.
Files changed (79) hide show
  1. package/package.json +7 -2
  2. package/src/components/charts/AreaChart.tsx +371 -0
  3. package/src/components/charts/MetricsSummary.tsx +49 -0
  4. package/src/components/charts/SeriesLegend.tsx +27 -0
  5. package/src/components/charts/colors.ts +49 -0
  6. package/src/components/charts/format.ts +41 -0
  7. package/src/components/charts/index.ts +14 -0
  8. package/src/components/charts/saturation.test.ts +60 -0
  9. package/src/components/charts/saturation.ts +32 -0
  10. package/src/components/charts/types.ts +36 -0
  11. package/src/components/compare/CompareResourcePicker.tsx +192 -0
  12. package/src/components/compare/CompareTray.tsx +130 -0
  13. package/src/components/compare/ResourceCompareView.tsx +272 -0
  14. package/src/components/compare/index.ts +14 -0
  15. package/src/components/compare/normalize.test.ts +193 -0
  16. package/src/components/compare/normalize.ts +69 -0
  17. package/src/components/compare/picks.test.ts +97 -0
  18. package/src/components/compare/picks.ts +35 -0
  19. package/src/components/compare/sort.test.ts +78 -0
  20. package/src/components/compare/sort.ts +26 -0
  21. package/src/components/compare/types.ts +43 -0
  22. package/src/components/compare/url.test.ts +61 -0
  23. package/src/components/compare/url.ts +26 -0
  24. package/src/components/gitops/GitOpsDetailLayout.tsx +621 -0
  25. package/src/components/gitops/GitOpsGraphFilterRail.tsx +216 -0
  26. package/src/components/gitops/GitOpsTableView.tsx +1441 -0
  27. package/src/components/gitops/RollbackDialog.tsx +117 -0
  28. package/src/components/gitops/SyncOptionsDialog.tsx +160 -0
  29. package/src/components/gitops/detail-helpers.test.ts +97 -0
  30. package/src/components/gitops/detail-helpers.ts +112 -0
  31. package/src/components/gitops/index.ts +52 -1
  32. package/src/components/gitops/short-cluster-name.test.ts +36 -0
  33. package/src/components/gitops/tree/index.ts +2 -0
  34. package/src/components/gitops/tree/merge.test.ts +240 -0
  35. package/src/components/gitops/tree/merge.ts +160 -0
  36. package/src/components/resources/ResourcesSidebar.tsx +42 -15
  37. package/src/components/resources/ResourcesView.tsx +403 -69
  38. package/src/components/resources/index.ts +1 -0
  39. package/src/components/resources/renderers/CompositeRenderer.tsx +233 -0
  40. package/src/components/resources/renderers/CompositionRenderer.tsx +218 -0
  41. package/src/components/resources/renderers/CrossplanePackageRenderer.tsx +145 -0
  42. package/src/components/resources/renderers/CrossplaneProviderConfigRenderer.tsx +71 -0
  43. package/src/components/resources/renderers/HPARenderer.tsx +6 -1
  44. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +131 -0
  45. package/src/components/resources/renderers/NamespaceRenderer.tsx +223 -0
  46. package/src/components/resources/renderers/PVCRenderer.tsx +6 -1
  47. package/src/components/resources/renderers/PodRenderer.tsx +207 -2
  48. package/src/components/resources/renderers/RoleBindingRenderer.tsx +132 -20
  49. package/src/components/resources/renderers/RoleRenderer.tsx +148 -18
  50. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +456 -3
  51. package/src/components/resources/renderers/WorkloadRenderer.tsx +189 -2
  52. package/src/components/resources/renderers/XRDRenderer.tsx +153 -0
  53. package/src/components/resources/renderers/crossplane-cells.tsx +146 -0
  54. package/src/components/resources/renderers/flux-cells.tsx +12 -0
  55. package/src/components/resources/renderers/index.ts +8 -0
  56. package/src/components/resources/resource-utils-crossplane.test.ts +609 -0
  57. package/src/components/resources/resource-utils-crossplane.ts +325 -0
  58. package/src/components/resources/resource-utils-flux.ts +14 -0
  59. package/src/components/resources/resource-utils.ts +1 -0
  60. package/src/components/resources/resources-column-filter.test.ts +74 -0
  61. package/src/components/shared/ResourceActionsBar.tsx +77 -0
  62. package/src/components/shared/ResourceRendererDispatch.tsx +138 -9
  63. package/src/components/ui/YamlEditor.tsx +28 -15
  64. package/src/components/ui/drawer-components.tsx +8 -20
  65. package/src/components/workload/ResourceDetailDrawer.tsx +1 -1
  66. package/src/components/workload/WorkloadView.tsx +2 -2
  67. package/src/index.ts +3 -0
  68. package/src/types/core.ts +31 -6
  69. package/src/types/index.ts +1 -0
  70. package/src/types/rbac.ts +99 -0
  71. package/src/utils/api-resources.ts +9 -1
  72. package/src/utils/badge-colors.ts +25 -0
  73. package/src/utils/gitops-owner.test.ts +70 -111
  74. package/src/utils/gitops-owner.ts +37 -74
  75. package/src/utils/helm-status.test.ts +50 -0
  76. package/src/utils/index.ts +1 -0
  77. package/src/utils/rbac-badges.ts +61 -0
  78. package/src/utils/rbac-blast-radius.test.ts +137 -0
  79. 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
- const isKnownKind = KNOWN_KINDS.has(kind)
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') && <HPARenderer data={data} onNavigate={onNavigate} />}
491
+ {(kind === 'hpas' || kind === 'horizontalpodautoscalers') && <HPAComp data={data} onNavigate={onNavigate} />}
389
492
  {kind === 'nodes' && <NodeComp data={data} relationships={relationships} />}
390
- {kind === 'persistentvolumeclaims' && <PVCRenderer data={data} onNavigate={onNavigate} />}
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' && <ServiceAccountRenderer data={data} />}
414
- {(kind === 'roles' || kind === 'clusterroles') && <RoleRenderer data={data} />}
415
- {(kind === 'rolebindings' || kind === 'clusterrolebindings') && <RoleBindingRenderer data={data} onNavigate={onNavigate} />}
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
- {/* Generic renderer for CRDs and unknown resource types */}
529
- {!isKnownKind && <GenericRenderer data={data} />}
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: _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="rounded-lg overflow-hidden border border-theme-border" style={{ height }}>
259
- <Editor
260
- defaultLanguage="yaml"
261
- value={modified}
262
- theme="vs-dark"
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.policies && relationships.policies.length > 0) ||
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.policies && relationships.policies.length > 0 && (() => {
769
- const policyKinds = new Set(['NetworkPolicy', 'CiliumNetworkPolicy', 'CiliumClusterwideNetworkPolicy', 'ClusterNetworkPolicy'])
770
- const pdbs = relationships.policies.filter(r => r.kind === 'PodDisruptionBudget')
771
- const netpols = relationships.policies.filter(r => policyKinds.has(r.kind))
772
- const other = relationships.policies.filter(r => r.kind !== 'PodDisruptionBudget' && !policyKinds.has(r.kind))
773
- return (
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
  )}
@@ -37,7 +37,7 @@ interface ResourceDetailDrawerProps {
37
37
  }) => ReactNode
38
38
  }
39
39
 
40
- const MIN_WIDTH = 400
40
+ const MIN_WIDTH = 520
41
41
  const MAX_WIDTH_PERCENT = 0.7
42
42
  const DEFAULT_WIDTH = 550
43
43
  const WIDE_WIDTH = 750
@@ -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 { detectGitOpsOwner, type GitOpsOwnerRef } from '../../utils/gitops-owner'
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(() => detectGitOpsOwner(resource), [resource])
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 (matches backend k8s.ResourcePermissions)
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
- pvcs: boolean
17
+ persistentVolumeClaims: boolean
16
18
  nodes: boolean
17
19
  namespaces: boolean
18
20
  jobs: boolean
19
21
  cronJobs: boolean
20
- hpas: boolean
21
- gateways: boolean
22
- httpRoutes: boolean
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
- policies?: ResourceRef[]
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)
@@ -2,3 +2,4 @@ export * from './core'
2
2
  export * from './gitops'
3
3
  export * from './gitops-tree'
4
4
  export * from './gitops-insights'
5
+ export * from './rbac'
@@ -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
- return knownGroups[group] || group
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
  // =============================================================================