@skyhook-io/k8s-ui 1.6.1 → 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 (59) hide show
  1. package/package.json +6 -1
  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/resources/ResourcesView.tsx +338 -57
  25. package/src/components/resources/index.ts +1 -0
  26. package/src/components/resources/renderers/CompositeRenderer.tsx +233 -0
  27. package/src/components/resources/renderers/CompositionRenderer.tsx +218 -0
  28. package/src/components/resources/renderers/CrossplanePackageRenderer.tsx +145 -0
  29. package/src/components/resources/renderers/CrossplaneProviderConfigRenderer.tsx +71 -0
  30. package/src/components/resources/renderers/HPARenderer.tsx +6 -1
  31. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +131 -0
  32. package/src/components/resources/renderers/NamespaceRenderer.tsx +223 -0
  33. package/src/components/resources/renderers/PVCRenderer.tsx +6 -1
  34. package/src/components/resources/renderers/PodRenderer.tsx +207 -2
  35. package/src/components/resources/renderers/RoleBindingRenderer.tsx +132 -20
  36. package/src/components/resources/renderers/RoleRenderer.tsx +148 -18
  37. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +456 -3
  38. package/src/components/resources/renderers/WorkloadRenderer.tsx +189 -2
  39. package/src/components/resources/renderers/XRDRenderer.tsx +153 -0
  40. package/src/components/resources/renderers/crossplane-cells.tsx +146 -0
  41. package/src/components/resources/renderers/flux-cells.tsx +12 -0
  42. package/src/components/resources/renderers/index.ts +8 -0
  43. package/src/components/resources/resource-utils-crossplane.test.ts +609 -0
  44. package/src/components/resources/resource-utils-crossplane.ts +325 -0
  45. package/src/components/resources/resource-utils-flux.ts +14 -0
  46. package/src/components/resources/resource-utils.ts +1 -0
  47. package/src/components/resources/resources-column-filter.test.ts +74 -0
  48. package/src/components/shared/ResourceActionsBar.tsx +77 -0
  49. package/src/components/shared/ResourceRendererDispatch.tsx +138 -9
  50. package/src/components/ui/YamlEditor.tsx +28 -15
  51. package/src/components/workload/ResourceDetailDrawer.tsx +1 -1
  52. package/src/index.ts +3 -0
  53. package/src/types/index.ts +1 -0
  54. package/src/types/rbac.ts +99 -0
  55. package/src/utils/api-resources.ts +9 -1
  56. package/src/utils/index.ts +1 -0
  57. package/src/utils/rbac-badges.ts +61 -0
  58. package/src/utils/rbac-blast-radius.test.ts +137 -0
  59. package/src/utils/rbac-blast-radius.ts +98 -0
@@ -1,7 +1,15 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { Server, ExternalLink, Scale, Minus, Plus, Loader2 } from 'lucide-react'
2
+ import { Server, ExternalLink, Scale, Minus, Plus, Loader2, Shield } from 'lucide-react'
3
+ import { clsx } from 'clsx'
3
4
  import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
5
  import { DialogPortal } from '../../ui/DialogPortal'
6
+ import type { RBACSubjectResponse, RBACPolicyRule } from '../../../types'
7
+ import { detectBlastRadius, rulePermissivenessScore } from '../../../utils/rbac-blast-radius'
8
+ import {
9
+ rbacVerbBadgeClass,
10
+ rbacResourceBadgeClass,
11
+ rbacApiGroupBadgeClass,
12
+ } from '../../../utils/rbac-badges'
5
13
 
6
14
  interface WorkloadRendererProps {
7
15
  kind: string
@@ -11,6 +19,14 @@ interface WorkloadRendererProps {
11
19
  onScale?: (replicas: number) => Promise<void>
12
20
  isScalePending?: boolean
13
21
  onRequestRefresh?: () => void
22
+ /**
23
+ * RBAC reverse-lookup for the workload's pod-template ServiceAccount.
24
+ * Undefined means the host didn't wire the fetch (Permissions section is
25
+ * omitted). Null means the fetch failed.
26
+ */
27
+ rbacData?: RBACSubjectResponse | null
28
+ rbacLoading?: boolean
29
+ rbacError?: Error | null
14
30
  }
15
31
 
16
32
  // Check if the workload is actively progressing (scaling, rolling update)
@@ -86,7 +102,7 @@ function getWorkloadProgress(status: any, spec: any, kind: string): string | nul
86
102
  return null
87
103
  }
88
104
 
89
- export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, onRequestRefresh }: WorkloadRendererProps) {
105
+ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, onRequestRefresh, rbacData, rbacLoading, rbacError }: WorkloadRendererProps) {
90
106
  const status = data.status || {}
91
107
  const spec = data.spec || {}
92
108
  const metadata = data.metadata || {}
@@ -304,6 +320,177 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
304
320
  </Section>
305
321
 
306
322
  <ConditionsSection conditions={status.conditions} />
323
+
324
+ {/* Permissions — same shape as PodPermissionsSection but framed for a
325
+ * workload (Pods this workload spawns inherit the SA). Placed below
326
+ * the diagnostic-signal sections because it answers an incident/audit
327
+ * question, not a daily-browsing one. Only renders when the host
328
+ * wired the RBAC fetch. */}
329
+ {rbacData !== undefined && (
330
+ <WorkloadPermissionsSection
331
+ saName={spec.template?.spec?.serviceAccountName || 'default'}
332
+ namespace={metadata.namespace || ''}
333
+ rbacData={rbacData}
334
+ loading={!!rbacLoading}
335
+ error={rbacError ?? null}
336
+ onNavigate={onNavigate}
337
+ />
338
+ )}
307
339
  </>
308
340
  )
309
341
  }
342
+
343
+ // ============================================================================
344
+ // WORKLOAD PERMISSIONS SECTION
345
+ // ============================================================================
346
+ // Mirror of PodPermissionsSection but framed at the workload level: "Pods
347
+ // this workload spawns inherit these permissions". A compromise of any
348
+ // replica gives the attacker the same SA. Detection criteria match Pod's —
349
+ // verb wildcards, escalation verbs (escalate/bind/impersonate), cluster-
350
+ // admin, cluster-wide create pods. Resource-only wildcards do NOT trigger
351
+ // (would fire on every authenticated SA via inherited `view`).
352
+
353
+ // Blast-radius detection and scoring is shared with Pod / ServiceAccount
354
+ // renderers — see utils/rbac-blast-radius.ts.
355
+
356
+ interface WorkloadPermissionsSectionProps {
357
+ saName: string
358
+ namespace: string
359
+ rbacData: RBACSubjectResponse | null
360
+ loading: boolean
361
+ error: Error | null
362
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
363
+ }
364
+
365
+ function WorkloadPermissionsSection({
366
+ saName,
367
+ namespace,
368
+ rbacData,
369
+ loading,
370
+ error,
371
+ onNavigate,
372
+ }: WorkloadPermissionsSectionProps) {
373
+ const title = `Permissions via ServiceAccount: ${saName}`
374
+ if (loading) {
375
+ return (
376
+ <Section title={title} icon={Shield}>
377
+ <div className="text-sm text-theme-text-secondary">Loading RBAC graph…</div>
378
+ </Section>
379
+ )
380
+ }
381
+ if (error) {
382
+ return (
383
+ <Section title={title} icon={Shield}>
384
+ <div className="text-sm text-red-400">Could not load permissions: {error.message}</div>
385
+ </Section>
386
+ )
387
+ }
388
+ if (!rbacData) return null
389
+
390
+ const blast = detectBlastRadius(rbacData)
391
+ const sorted = [...(rbacData.flat ?? [])].sort(
392
+ (a, b) => rulePermissivenessScore(b) - rulePermissivenessScore(a),
393
+ )
394
+ const preview = sorted.slice(0, 5)
395
+ const more = Math.max(0, sorted.length - preview.length)
396
+ const directCount = rbacData.direct?.length ?? 0
397
+ const inheritedCount = (rbacData.inheritedFromGroups ?? []).reduce((n, g) => n + g.bindings.length, 0)
398
+ const ruleCount = rbacData.flat?.length ?? 0
399
+
400
+ // Default collapsed unless the Pod's permissions are genuinely risky —
401
+ // workload-detail pages are most-often opened for "is this rolling out
402
+ // OK / why is it crashing", not "audit the SA". Auto-expanding only on
403
+ // blast-radius hits keeps the noisy case quiet without burying real
404
+ // alarms.
405
+ const hasBlastRadius = blast.length > 0
406
+ return (
407
+ <Section title={title} icon={Shield} defaultExpanded={hasBlastRadius}>
408
+ {blast.length > 0 && (
409
+ <AlertBanner variant="warning" title="Blast radius">
410
+ <div className="text-xs">
411
+ Every Pod this workload spawns inherits the ServiceAccount's
412
+ permissions. Compromising any replica gives an attacker:
413
+ </div>
414
+ <ul className="mt-1.5 text-xs space-y-1">
415
+ {blast.map((r, i) => (
416
+ <li key={i}>
417
+ <span className="text-theme-text-secondary">
418
+ {r.binding.binding.kind} <span className="font-medium">{r.binding.binding.name}</span>
419
+ </span>{' '}
420
+ <span className="text-theme-text-tertiary">{r.reason}</span>
421
+ </li>
422
+ ))}
423
+ </ul>
424
+ </AlertBanner>
425
+ )}
426
+
427
+ <div className="text-xs text-theme-text-tertiary mb-3">
428
+ {directCount} direct binding{directCount === 1 ? '' : 's'} ·{' '}
429
+ {inheritedCount} inherited via group{inheritedCount === 1 ? '' : 's'} ·{' '}
430
+ {ruleCount} distinct rule{ruleCount === 1 ? '' : 's'}
431
+ {rbacData.truncated && <span className="text-orange-400"> (truncated)</span>}
432
+ </div>
433
+
434
+ {preview.length === 0 ? (
435
+ <div className="text-sm text-theme-text-secondary">
436
+ This ServiceAccount has no effective permissions in the cluster.
437
+ </div>
438
+ ) : (
439
+ <div className="space-y-1">
440
+ {preview.map((r, i) => (
441
+ <WorkloadRulePreviewLine key={i} rule={r} />
442
+ ))}
443
+ {more > 0 && (
444
+ <div className="text-xs text-theme-text-tertiary">
445
+ +{more} more rule{more === 1 ? '' : 's'} — open the ServiceAccount
446
+ for full provenance.
447
+ </div>
448
+ )}
449
+ </div>
450
+ )}
451
+
452
+ <div className="mt-3 text-xs">
453
+ <ResourceLink
454
+ name={saName}
455
+ kind="serviceaccounts"
456
+ namespace={namespace}
457
+ label="View full permissions →"
458
+ onNavigate={onNavigate}
459
+ />
460
+ </div>
461
+ </Section>
462
+ )
463
+ }
464
+
465
+ function WorkloadRulePreviewLine({ rule }: { rule: RBACPolicyRule }) {
466
+ const verbs = rule.verbs ?? []
467
+ const resources = rule.resources ?? []
468
+ const nonResourceURLs = rule.nonResourceURLs ?? []
469
+ const groups = rule.apiGroups ?? []
470
+ const isNonResource = resources.length === 0 && nonResourceURLs.length > 0
471
+ return (
472
+ <div className="flex items-center gap-1 flex-wrap text-xs">
473
+ {verbs.map((v) => (
474
+ <span key={v} className={clsx('badge', rbacVerbBadgeClass(v))}>{v}</span>
475
+ ))}
476
+ <span className="text-theme-text-secondary">on</span>
477
+ {isNonResource ? (
478
+ nonResourceURLs.map((u) => (
479
+ <span key={u} className="badge font-mono bg-theme-elevated text-theme-text-secondary">{u}</span>
480
+ ))
481
+ ) : (
482
+ resources.map((r) => (
483
+ <span key={r} className={clsx('badge', rbacResourceBadgeClass)}>{r === '*' ? '*' : r}</span>
484
+ ))
485
+ )}
486
+ {!isNonResource && groups.length > 0 && groups.some((g) => g !== '') && (
487
+ <>
488
+ <span className="text-theme-text-secondary">in</span>
489
+ {groups.map((g) => (
490
+ <span key={g} className={clsx('badge', rbacApiGroupBadgeClass)}>{g === '' ? 'core' : g}</span>
491
+ ))}
492
+ </>
493
+ )}
494
+ </div>
495
+ )
496
+ }
@@ -0,0 +1,153 @@
1
+ import { clsx } from 'clsx'
2
+ import { FileCode2, Layers, KeyRound, Settings } from 'lucide-react'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+
5
+ interface XRDRendererProps {
6
+ data: any
7
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
8
+ }
9
+
10
+ export function XRDRenderer({ data, onNavigate }: XRDRendererProps) {
11
+ const spec = data?.spec ?? {}
12
+ const conditions: any[] = data?.status?.conditions ?? []
13
+ const established = conditions.find((c: any) => c.type === 'Established')
14
+ const offered = conditions.find((c: any) => c.type === 'Offered')
15
+ const group = spec.group ?? ''
16
+ const names = spec.names ?? {}
17
+ const claimNames = spec.claimNames
18
+ const versions: any[] = spec.versions ?? []
19
+ const scope = spec.scope // v2 only
20
+ const connectionSecretKeys: string[] = spec.connectionSecretKeys ?? []
21
+ const defaultCompositionRef = spec.defaultCompositionRef
22
+ const enforcedCompositionRef = spec.enforcedCompositionRef
23
+ const defaultCompositeDeletePolicy = spec.defaultCompositeDeletePolicy
24
+
25
+ return (
26
+ <>
27
+ {established?.status === 'False' && (
28
+ <AlertBanner
29
+ variant="error"
30
+ title="XRD Not Established"
31
+ message={established.message || established.reason || 'The XRD has not produced a CRD. Check schema validity.'}
32
+ />
33
+ )}
34
+ {offered?.status === 'False' && claimNames && (
35
+ <AlertBanner
36
+ variant="warning"
37
+ title="Claim Not Offered"
38
+ message={offered.message || offered.reason || 'Claim CRD has not been produced from the claimNames spec.'}
39
+ />
40
+ )}
41
+
42
+ <Section title="Generated CR" icon={FileCode2} defaultExpanded>
43
+ <PropertyList>
44
+ {names.kind && (
45
+ <Property label="Kind" value={<span className="font-mono">{names.kind}</span>} />
46
+ )}
47
+ {names.plural && (
48
+ <Property label="Plural" value={<span className="font-mono">{names.plural}</span>} />
49
+ )}
50
+ {names.singular && (
51
+ <Property label="Singular" value={<span className="font-mono">{names.singular}</span>} />
52
+ )}
53
+ {names.listKind && (
54
+ <Property label="List Kind" value={<span className="font-mono">{names.listKind}</span>} />
55
+ )}
56
+ <Property label="Group" value={<span className="font-mono break-all">{group}</span>} />
57
+ {scope && (
58
+ <Property
59
+ label="Scope"
60
+ value={
61
+ <span className={clsx('badge-sm', scope === 'Cluster' ? 'status-violet' : 'status-neutral')}>
62
+ {scope}
63
+ </span>
64
+ }
65
+ />
66
+ )}
67
+ </PropertyList>
68
+ </Section>
69
+
70
+ {claimNames && (
71
+ <Section title="Claim (v1)" icon={Layers} defaultExpanded>
72
+ <PropertyList>
73
+ {claimNames.kind && (
74
+ <Property label="Kind" value={<span className="font-mono">{claimNames.kind}</span>} />
75
+ )}
76
+ {claimNames.plural && (
77
+ <Property label="Plural" value={<span className="font-mono">{claimNames.plural}</span>} />
78
+ )}
79
+ {claimNames.singular && (
80
+ <Property label="Singular" value={<span className="font-mono">{claimNames.singular}</span>} />
81
+ )}
82
+ </PropertyList>
83
+ </Section>
84
+ )}
85
+
86
+ {versions.length > 0 && (
87
+ <Section title={`Versions (${versions.length})`} icon={FileCode2} defaultExpanded>
88
+ <div className="space-y-1">
89
+ {versions.map((v: any, i: number) => (
90
+ <div key={i} className="card-inner text-sm flex items-center gap-2 flex-wrap">
91
+ <span className="font-mono text-theme-text-primary">{v.name}</span>
92
+ {v.served && <span className="badge-sm status-healthy">served</span>}
93
+ {v.referenceable && <span className="badge-sm status-neutral">referenceable</span>}
94
+ {v.deprecated && <span className="badge-sm status-alert">deprecated</span>}
95
+ {v.deprecationWarning && (
96
+ <span className="text-theme-text-tertiary text-xs break-all">{v.deprecationWarning}</span>
97
+ )}
98
+ </div>
99
+ ))}
100
+ </div>
101
+ </Section>
102
+ )}
103
+
104
+ {(defaultCompositionRef?.name || enforcedCompositionRef?.name) && (
105
+ <Section title="Composition" icon={Settings} defaultExpanded>
106
+ <PropertyList>
107
+ {defaultCompositionRef?.name && (
108
+ <Property
109
+ label="Default"
110
+ value={
111
+ <ResourceLink
112
+ name={defaultCompositionRef.name}
113
+ kind="compositions"
114
+ namespace=""
115
+ onNavigate={onNavigate}
116
+ />
117
+ }
118
+ />
119
+ )}
120
+ {enforcedCompositionRef?.name && (
121
+ <Property
122
+ label="Enforced"
123
+ value={
124
+ <ResourceLink
125
+ name={enforcedCompositionRef.name}
126
+ kind="compositions"
127
+ namespace=""
128
+ onNavigate={onNavigate}
129
+ />
130
+ }
131
+ />
132
+ )}
133
+ {defaultCompositeDeletePolicy && (
134
+ <Property label="Default Delete Policy" value={defaultCompositeDeletePolicy} />
135
+ )}
136
+ </PropertyList>
137
+ </Section>
138
+ )}
139
+
140
+ {connectionSecretKeys.length > 0 && (
141
+ <Section title={`Connection Secret Keys (${connectionSecretKeys.length})`} icon={KeyRound} defaultExpanded={false}>
142
+ <div className="flex flex-wrap gap-1">
143
+ {connectionSecretKeys.map((k: string) => (
144
+ <span key={k} className="badge-sm status-neutral font-mono">{k}</span>
145
+ ))}
146
+ </div>
147
+ </Section>
148
+ )}
149
+
150
+ <ConditionsSection conditions={conditions} />
151
+ </>
152
+ )
153
+ }
@@ -0,0 +1,146 @@
1
+ // Crossplane cell components for ResourcesView table
2
+
3
+ import { clsx } from 'clsx'
4
+ import { Tooltip } from '../../ui/Tooltip'
5
+ import {
6
+ getCrossplaneStatus,
7
+ getCrossplaneStatusReason,
8
+ getProviderStatus,
9
+ getProviderConfigStatus,
10
+ getProviderConfigCredentialsSource,
11
+ getProviderPackage,
12
+ getProviderCurrentRevision,
13
+ getProviderConfigRef,
14
+ getExternalName,
15
+ getCompositionRef,
16
+ getCrossplaneResourceRefs,
17
+ getCompositionCompositeTypeRef,
18
+ getCompositionMode,
19
+ getCompositionFunctionCount,
20
+ getXRDClaimNames,
21
+ getXRDKind,
22
+ } from '../resource-utils-crossplane'
23
+
24
+ function StatusBadge({ resource, getStatus }: { resource: any; getStatus: (r: any) => { text: string; color: string } }) {
25
+ const status = getStatus(resource)
26
+ const reason = getCrossplaneStatusReason(resource)
27
+ const badge = (
28
+ <span className={clsx('badge', status.color)}>
29
+ {status.text}
30
+ </span>
31
+ )
32
+ return reason ? <Tooltip content={reason}>{badge}</Tooltip> : badge
33
+ }
34
+
35
+ export function ManagedResourceCell({ resource, column }: { resource: any; column: string }) {
36
+ switch (column) {
37
+ case 'status':
38
+ return <StatusBadge resource={resource} getStatus={getCrossplaneStatus} />
39
+ case 'external': {
40
+ const name = getExternalName(resource)
41
+ if (!name) return <span className="text-sm text-theme-text-tertiary">-</span>
42
+ return (
43
+ <Tooltip content={name}>
44
+ <span className="text-sm text-theme-text-secondary font-mono truncate block">{name}</span>
45
+ </Tooltip>
46
+ )
47
+ }
48
+ case 'provider': {
49
+ const ref = getProviderConfigRef(resource)
50
+ if (!ref) return <span className="text-sm text-theme-text-tertiary">-</span>
51
+ return <span className="text-sm text-theme-text-secondary">{ref.name}</span>
52
+ }
53
+ case 'kind':
54
+ return <span className="text-sm text-theme-text-secondary">{resource.kind || '-'}</span>
55
+ default:
56
+ return <span className="text-sm text-theme-text-tertiary">-</span>
57
+ }
58
+ }
59
+
60
+ export function CompositeResourceCell({ resource, column }: { resource: any; column: string }) {
61
+ switch (column) {
62
+ case 'status':
63
+ return <StatusBadge resource={resource} getStatus={getCrossplaneStatus} />
64
+ case 'composition': {
65
+ const ref = getCompositionRef(resource)
66
+ if (!ref) return <span className="text-sm text-theme-text-tertiary">-</span>
67
+ return (
68
+ <Tooltip content={ref.name}>
69
+ <span className="text-sm text-theme-text-secondary truncate block">{ref.name}</span>
70
+ </Tooltip>
71
+ )
72
+ }
73
+ case 'composed': {
74
+ const refs = getCrossplaneResourceRefs(resource)
75
+ return <span className="text-sm text-theme-text-secondary">{refs.length > 0 ? refs.length : '-'}</span>
76
+ }
77
+ default:
78
+ return <span className="text-sm text-theme-text-tertiary">-</span>
79
+ }
80
+ }
81
+
82
+ export function CrossplaneProviderCell({ resource, column }: { resource: any; column: string }) {
83
+ switch (column) {
84
+ case 'status':
85
+ return <StatusBadge resource={resource} getStatus={getProviderStatus} />
86
+ case 'package': {
87
+ const pkg = getProviderPackage(resource)
88
+ return (
89
+ <Tooltip content={pkg}>
90
+ <span className="text-sm text-theme-text-secondary font-mono truncate block">{pkg}</span>
91
+ </Tooltip>
92
+ )
93
+ }
94
+ case 'revision': {
95
+ const rev = getProviderCurrentRevision(resource)
96
+ if (!rev) return <span className="text-sm text-theme-text-tertiary">-</span>
97
+ return (
98
+ <Tooltip content={rev}>
99
+ <span className="text-sm text-theme-text-tertiary font-mono truncate block">{rev}</span>
100
+ </Tooltip>
101
+ )
102
+ }
103
+ default:
104
+ return <span className="text-sm text-theme-text-tertiary">-</span>
105
+ }
106
+ }
107
+
108
+ export function CrossplaneProviderConfigCell({ resource, column }: { resource: any; column: string }) {
109
+ switch (column) {
110
+ case 'status':
111
+ return <StatusBadge resource={resource} getStatus={getProviderConfigStatus} />
112
+ case 'credentials':
113
+ return <span className="text-sm text-theme-text-secondary">{getProviderConfigCredentialsSource(resource)}</span>
114
+ default:
115
+ return <span className="text-sm text-theme-text-tertiary">-</span>
116
+ }
117
+ }
118
+
119
+ export function XRDCell({ resource, column }: { resource: any; column: string }) {
120
+ switch (column) {
121
+ case 'kind':
122
+ return <span className="text-sm text-theme-text-secondary font-mono">{getXRDKind(resource)}</span>
123
+ case 'claim': {
124
+ const cn = getXRDClaimNames(resource)
125
+ if (!cn.kind) return <span className="text-sm text-theme-text-tertiary">-</span>
126
+ return <span className="text-sm text-theme-text-secondary font-mono">{cn.kind}</span>
127
+ }
128
+ default:
129
+ return <span className="text-sm text-theme-text-tertiary">-</span>
130
+ }
131
+ }
132
+
133
+ export function CompositionCell({ resource, column }: { resource: any; column: string }) {
134
+ switch (column) {
135
+ case 'composite':
136
+ return <span className="text-sm text-theme-text-secondary font-mono">{getCompositionCompositeTypeRef(resource)}</span>
137
+ case 'mode':
138
+ return <span className="text-sm text-theme-text-secondary">{getCompositionMode(resource)}</span>
139
+ case 'functions': {
140
+ const count = getCompositionFunctionCount(resource)
141
+ return <span className="text-sm text-theme-text-secondary">{count > 0 ? count : '-'}</span>
142
+ }
143
+ default:
144
+ return <span className="text-sm text-theme-text-tertiary">-</span>
145
+ }
146
+ }
@@ -22,6 +22,7 @@ import {
22
22
  getFluxHelmReleaseVersion,
23
23
  getFluxHelmReleaseStatus,
24
24
  getFluxHelmReleaseRevision,
25
+ getFluxHelmReleaseMessage,
25
26
  getFluxAlertProvider,
26
27
  getFluxAlertEventCount,
27
28
  getFluxAlertStatus,
@@ -202,6 +203,17 @@ export function FluxHelmReleaseCell({ resource, column }: { resource: any; colum
202
203
  </span>
203
204
  )
204
205
  }
206
+ case 'message': {
207
+ const message = getFluxHelmReleaseMessage(resource)
208
+ if (!message) {
209
+ return <span className="text-sm text-theme-text-tertiary">-</span>
210
+ }
211
+ return (
212
+ <Tooltip content={message}>
213
+ <span className="text-sm text-theme-text-secondary truncate block">{message}</span>
214
+ </Tooltip>
215
+ )
216
+ }
205
217
  case 'revision': {
206
218
  const revision = getFluxHelmReleaseRevision(resource)
207
219
  return (
@@ -9,6 +9,13 @@ export * from './ClusterComplianceReportRenderer'
9
9
  export * from './ClusterExternalSecretRenderer'
10
10
  export * from './ClusterIssuerRenderer'
11
11
  export * from './cnpg-cells'
12
+ export * from './crossplane-cells'
13
+ export * from './ManagedResourceRenderer'
14
+ export * from './CompositeRenderer'
15
+ export * from './CrossplanePackageRenderer'
16
+ export * from './CrossplaneProviderConfigRenderer'
17
+ export * from './CompositionRenderer'
18
+ export * from './XRDRenderer'
12
19
  export * from './CNPGBackupRenderer'
13
20
  export * from './CNPGClusterRenderer'
14
21
  export * from './CNPGPoolerRenderer'
@@ -86,6 +93,7 @@ export * from './SealedSecretRenderer'
86
93
  export * from './SecretRenderer'
87
94
  export * from './SecretStoreRenderer'
88
95
  export * from './ServiceAccountRenderer'
96
+ export * from './NamespaceRenderer'
89
97
  export * from './ServiceRenderer'
90
98
  export * from './ServiceMonitorRenderer'
91
99
  export * from './SimpleRouteRenderer'