@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
@@ -2,6 +2,7 @@ export * from './resource-utils'
2
2
  export * from './resource-utils-argo'
3
3
  export * from './resource-utils-certmanager'
4
4
  export * from './resource-utils-cnpg'
5
+ export * from './resource-utils-crossplane'
5
6
  export * from './resource-utils-eso'
6
7
  export * from './resource-utils-flux'
7
8
  export * from './resource-utils-istio'
@@ -0,0 +1,233 @@
1
+ import { clsx } from 'clsx'
2
+ import { Layers, GitBranch, Box, ChevronRight, Pause } from 'lucide-react'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { Tooltip } from '../../ui/Tooltip'
5
+ import {
6
+ getCrossplaneStatus,
7
+ getCrossplaneStatusReason,
8
+ getCompositionRef,
9
+ getCompositionRevisionRef,
10
+ getCompositionUpdatePolicy,
11
+ getCrossplaneResourceRefs,
12
+ getBoundXRRef,
13
+ isClaim,
14
+ isCrossplanePaused,
15
+ type CrossplaneResourceRef,
16
+ } from '../resource-utils-crossplane'
17
+ import { kindToPlural } from '../../../utils/navigation'
18
+ import type { StatusBadge } from '../resource-utils'
19
+
20
+ /**
21
+ * Status of a composed resource, fetched by the host wrapper. The package
22
+ * renderer is pure — the host fans out per-ref queries (React Query) and
23
+ * passes the result down here.
24
+ *
25
+ * `missing` and `error` are split because they read differently to operators:
26
+ * `missing` (404) means the composed resource genuinely doesn't exist yet
27
+ * (reconciliation hasn't created it), which is a normal state for a
28
+ * freshly-applied Composite. Any other failure (401/403/500/network) means
29
+ * Radar can't tell the operator what they want to know, and "missing" would
30
+ * mislead them into thinking the resource is absent.
31
+ */
32
+ export interface ComposedRefStatus {
33
+ ref: CrossplaneResourceRef
34
+ /** undefined means we haven't fetched yet (or the host doesn't support it) */
35
+ status?: StatusBadge
36
+ /** true while the fetch is in flight */
37
+ loading?: boolean
38
+ /** 404 — composed resource not created yet (normal during reconciliation) */
39
+ missing?: boolean
40
+ /** any non-404 fetch failure (auth, network, server error); message in errorMessage */
41
+ error?: boolean
42
+ errorMessage?: string
43
+ }
44
+
45
+ interface CompositeRendererProps {
46
+ data: any
47
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
48
+ /** Per-composed-ref status, indexed by `${apiVersion}/${kind}/${namespace}/${name}`.
49
+ * apiVersion is part of the key because two providers can ship CRDs with
50
+ * identical kind plurals across different groups — without it, the
51
+ * status of a Bucket from s3.aws.upbound.io would overwrite the status
52
+ * of an unrelated Bucket from another provider. Populated by the host
53
+ * wrapper. When absent, the renderer shows refs without status badges. */
54
+ composedRefStatuses?: Map<string, ComposedRefStatus>
55
+ }
56
+
57
+ function makeRefKey(ref: CrossplaneResourceRef): string {
58
+ return `${ref.apiVersion}/${ref.kind}/${ref.namespace ?? ''}/${ref.name}`
59
+ }
60
+
61
+ // Crossplane refs carry full apiVersion ("s3.aws.upbound.io/v1beta1"); the
62
+ // renderer needs the group ("s3.aws.upbound.io") to disambiguate plurals
63
+ // across providers.
64
+ function groupFromApiVersion(apiVersion: string | undefined): string {
65
+ if (!apiVersion) return ''
66
+ const slash = apiVersion.indexOf('/')
67
+ return slash < 0 ? '' : apiVersion.slice(0, slash)
68
+ }
69
+
70
+ export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: CompositeRendererProps) {
71
+ const status = getCrossplaneStatus(data)
72
+ const reason = getCrossplaneStatusReason(data)
73
+ const compositionRef = getCompositionRef(data)
74
+ const compositionRevisionRef = getCompositionRevisionRef(data)
75
+ const compositionUpdatePolicy = getCompositionUpdatePolicy(data)
76
+ const resourceRefs = getCrossplaneResourceRefs(data)
77
+ const claim = isClaim(data)
78
+ const boundXR = claim ? getBoundXRRef(data) : null
79
+ const paused = isCrossplanePaused(data)
80
+ const alertVariant = status.level === 'unhealthy' || status.level === 'alert' ? status.level : null
81
+
82
+ return (
83
+ <>
84
+ {paused && (
85
+ <AlertBanner
86
+ variant="warning"
87
+ icon={Pause}
88
+ title="Reconciliation paused"
89
+ message="The crossplane.io/paused annotation is set. Composed resources will not be reconciled until the annotation is removed."
90
+ />
91
+ )}
92
+
93
+ {alertVariant && reason && (
94
+ <AlertBanner
95
+ variant={alertVariant === 'unhealthy' ? 'error' : 'warning'}
96
+ title={status.text}
97
+ message={reason}
98
+ />
99
+ )}
100
+
101
+ <Section title={claim ? 'Claim' : 'Composite Resource'} icon={Layers} defaultExpanded>
102
+ <PropertyList>
103
+ {compositionRef && (
104
+ <Property
105
+ label="Composition"
106
+ value={
107
+ <ResourceLink
108
+ name={compositionRef.name}
109
+ kind="compositions"
110
+ namespace=""
111
+ onNavigate={onNavigate}
112
+ />
113
+ }
114
+ />
115
+ )}
116
+ {compositionRevisionRef && (
117
+ <Property
118
+ label="Composition Revision"
119
+ value={
120
+ <ResourceLink
121
+ name={compositionRevisionRef.name}
122
+ kind="compositionrevisions"
123
+ namespace=""
124
+ onNavigate={onNavigate}
125
+ />
126
+ }
127
+ />
128
+ )}
129
+ {compositionUpdatePolicy && (
130
+ <Property label="Update Policy" value={compositionUpdatePolicy} />
131
+ )}
132
+ {boundXR && (
133
+ <Property
134
+ label="Bound Composite"
135
+ value={
136
+ <ResourceLink
137
+ name={boundXR.name}
138
+ kind={kindToPlural(boundXR.kind)}
139
+ namespace={boundXR.namespace || ''}
140
+ group={groupFromApiVersion(boundXR.apiVersion) || undefined}
141
+ onNavigate={onNavigate}
142
+ />
143
+ }
144
+ />
145
+ )}
146
+ </PropertyList>
147
+ </Section>
148
+
149
+ <Section
150
+ title={`Composed Resources${resourceRefs.length > 0 ? ` (${resourceRefs.length})` : ''}`}
151
+ icon={GitBranch}
152
+ defaultExpanded
153
+ >
154
+ {resourceRefs.length === 0 ? (
155
+ <div className="text-sm text-theme-text-tertiary py-2">
156
+ No composed resources yet — the composition has not produced any managed resources.
157
+ </div>
158
+ ) : (
159
+ <div className="space-y-1">
160
+ {resourceRefs.map((ref) => (
161
+ <ComposedRefRow
162
+ key={makeRefKey(ref)}
163
+ ref_={ref}
164
+ statusEntry={composedRefStatuses?.get(makeRefKey(ref))}
165
+ onNavigate={onNavigate}
166
+ />
167
+ ))}
168
+ </div>
169
+ )}
170
+ </Section>
171
+
172
+ <ConditionsSection conditions={data?.status?.conditions} />
173
+ </>
174
+ )
175
+ }
176
+
177
+ function ComposedRefRow({
178
+ ref_,
179
+ statusEntry,
180
+ onNavigate,
181
+ }: {
182
+ ref_: CrossplaneResourceRef
183
+ statusEntry?: ComposedRefStatus
184
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
185
+ }) {
186
+ const kindPlural = kindToPlural(ref_.kind)
187
+ const group = groupFromApiVersion(ref_.apiVersion)
188
+ return (
189
+ <div className="flex items-center gap-2 py-1 px-2 rounded hover:bg-theme-hover text-sm">
190
+ <Box className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
191
+ <span className="text-theme-text-tertiary font-mono shrink-0 w-32 truncate">{ref_.kind}</span>
192
+ <span className="flex-1 min-w-0 truncate">
193
+ <ResourceLink
194
+ name={ref_.name}
195
+ kind={kindPlural}
196
+ namespace={ref_.namespace || ''}
197
+ group={group || undefined}
198
+ onNavigate={onNavigate}
199
+ />
200
+ </span>
201
+ {ref_.namespace && (
202
+ <span className="text-xs text-theme-text-tertiary truncate w-32 text-right">{ref_.namespace}</span>
203
+ )}
204
+ <StatusCellInline statusEntry={statusEntry} />
205
+ <ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
206
+ </div>
207
+ )
208
+ }
209
+
210
+ function StatusCellInline({ statusEntry }: { statusEntry?: ComposedRefStatus }) {
211
+ if (!statusEntry || statusEntry.loading) {
212
+ return <span className="badge-sm bg-theme-elevated text-theme-text-tertiary w-20 text-center">…</span>
213
+ }
214
+ if (statusEntry.missing) {
215
+ return <span className="badge-sm bg-theme-elevated text-theme-text-tertiary w-20 text-center">missing</span>
216
+ }
217
+ if (statusEntry.error) {
218
+ const tip = statusEntry.errorMessage || 'Could not fetch composed resource status'
219
+ return (
220
+ <Tooltip content={tip}>
221
+ <span className="badge-sm status-unhealthy w-20 text-center">error</span>
222
+ </Tooltip>
223
+ )
224
+ }
225
+ if (!statusEntry.status) {
226
+ return <span className="badge-sm bg-theme-elevated text-theme-text-tertiary w-20 text-center">-</span>
227
+ }
228
+ return (
229
+ <span className={clsx('badge-sm w-24 text-center truncate', statusEntry.status.color)}>
230
+ {statusEntry.status.text}
231
+ </span>
232
+ )
233
+ }
@@ -0,0 +1,218 @@
1
+ import { clsx } from 'clsx'
2
+ import { Layers, GitBranch, FileText, Boxes, ScrollText } from 'lucide-react'
3
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { CodeViewer } from '../../ui/CodeViewer'
5
+ import { kindToPlural } from '../../../utils/navigation'
6
+
7
+ interface CompositionRendererProps {
8
+ data: any
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
10
+ }
11
+
12
+ function extractApiGroup(apiVersion?: string): string {
13
+ if (!apiVersion) return ''
14
+ const slash = apiVersion.indexOf('/')
15
+ return slash < 0 ? '' : apiVersion.slice(0, slash)
16
+ }
17
+
18
+ /** Used by both CompositionRenderer and CompositionRevisionRenderer — same shape. */
19
+ function CompositionBody({ data, onNavigate, revision }: CompositionRendererProps & { revision?: number | null }) {
20
+ const spec = data?.spec ?? {}
21
+ const compositeTypeRef = spec.compositeTypeRef ?? {}
22
+ const xrdGroup = extractApiGroup(compositeTypeRef.apiVersion)
23
+ const xrdKind = compositeTypeRef.kind
24
+ const mode = spec.mode || 'Resources'
25
+ const pipeline: any[] = spec.pipeline ?? []
26
+ const resources: any[] = spec.resources ?? []
27
+ const writeConnNs = spec.writeConnectionSecretsToNamespace
28
+ const isPipeline = mode === 'Pipeline'
29
+ const hasNoSteps = isPipeline && pipeline.length === 0
30
+ const hasNoResources = !isPipeline && resources.length === 0
31
+
32
+ return (
33
+ <>
34
+ {hasNoSteps && (
35
+ <AlertBanner
36
+ variant="warning"
37
+ title="Empty pipeline"
38
+ message="This Composition runs in Pipeline mode but has no functions configured. It will produce no composed resources."
39
+ />
40
+ )}
41
+ {hasNoResources && (
42
+ <AlertBanner
43
+ variant="warning"
44
+ title="No composed resources"
45
+ message="This Composition has no resource templates and is not in Pipeline mode."
46
+ />
47
+ )}
48
+
49
+ <Section title="Composition" icon={Layers} defaultExpanded>
50
+ <PropertyList>
51
+ {revision != null && <Property label="Revision" value={`#${revision}`} />}
52
+ <Property label="Mode" value={
53
+ <span className={clsx('badge-sm', mode === 'Pipeline' ? 'status-violet' : 'status-neutral')}>
54
+ {mode}
55
+ </span>
56
+ } />
57
+ {xrdKind && (
58
+ <Property
59
+ label="Composite Kind"
60
+ value={<span className="font-mono text-theme-text-secondary">{xrdKind}</span>}
61
+ />
62
+ )}
63
+ {compositeTypeRef.apiVersion && (
64
+ <Property
65
+ label="API Version"
66
+ value={<span className="font-mono text-theme-text-tertiary text-xs">{compositeTypeRef.apiVersion}</span>}
67
+ />
68
+ )}
69
+ {writeConnNs && <Property label="Connection Secret Namespace" value={writeConnNs} />}
70
+ </PropertyList>
71
+ {xrdKind && (
72
+ <div className="mt-2 text-xs text-theme-text-secondary">
73
+ Backed by XRD:{' '}
74
+ <ResourceLink
75
+ name={`${kindToPlural(xrdKind)}.${xrdGroup}`}
76
+ kind="compositeresourcedefinitions"
77
+ namespace=""
78
+ onNavigate={onNavigate}
79
+ label={<span className="font-mono">{kindToPlural(xrdKind)}.{xrdGroup}</span>}
80
+ />
81
+ </div>
82
+ )}
83
+ </Section>
84
+
85
+ {isPipeline && pipeline.length > 0 && (
86
+ <Section title={`Pipeline (${pipeline.length} step${pipeline.length === 1 ? '' : 's'})`} icon={GitBranch} defaultExpanded>
87
+ <div className="space-y-3">
88
+ {pipeline.map((step: any, i: number) => {
89
+ const fnName = step?.functionRef?.name
90
+ const inputKind = step?.input?.kind
91
+ const inputApiVersion = step?.input?.apiVersion
92
+ return (
93
+ <div key={i} className="card-inner-lg">
94
+ <div className="flex items-center gap-2 mb-2">
95
+ <span className="badge-sm status-neutral font-mono">#{i + 1}</span>
96
+ <span className="font-medium text-theme-text-primary">{step.step || '(unnamed step)'}</span>
97
+ </div>
98
+ <PropertyList>
99
+ {fnName && (
100
+ <Property
101
+ label="Function"
102
+ value={
103
+ <ResourceLink
104
+ name={fnName}
105
+ kind="functions"
106
+ namespace=""
107
+ onNavigate={onNavigate}
108
+ />
109
+ }
110
+ />
111
+ )}
112
+ {inputKind && (
113
+ <Property
114
+ label="Input Kind"
115
+ value={<span className="font-mono">{inputKind}</span>}
116
+ />
117
+ )}
118
+ {inputApiVersion && (
119
+ <Property
120
+ label="Input API"
121
+ value={<span className="font-mono text-xs text-theme-text-tertiary">{inputApiVersion}</span>}
122
+ />
123
+ )}
124
+ </PropertyList>
125
+ {step.input && (
126
+ <details className="mt-2">
127
+ <summary className="text-xs text-theme-text-tertiary cursor-pointer hover:text-theme-text-secondary">
128
+ Show input
129
+ </summary>
130
+ <div className="mt-1">
131
+ <CodeViewer code={JSON.stringify(step.input, null, 2)} language="json" maxHeight="200px" />
132
+ </div>
133
+ </details>
134
+ )}
135
+ </div>
136
+ )
137
+ })}
138
+ </div>
139
+ </Section>
140
+ )}
141
+
142
+ {!isPipeline && resources.length > 0 && (
143
+ <Section title={`Resources (${resources.length})`} icon={Boxes} defaultExpanded>
144
+ <div className="space-y-1">
145
+ {resources.map((res: any, i: number) => {
146
+ const baseKind = res?.base?.kind
147
+ const baseApiVersion = res?.base?.apiVersion
148
+ const patchCount = (res?.patches || []).length
149
+ return (
150
+ <div key={i} className="card-inner text-sm flex items-center gap-2 flex-wrap">
151
+ <span className="badge-sm status-neutral">{baseKind || 'Unknown'}</span>
152
+ <span className="font-mono text-theme-text-secondary break-all">{res.name || `resource-${i}`}</span>
153
+ {baseApiVersion && (
154
+ <span className="text-theme-text-tertiary text-xs">{baseApiVersion}</span>
155
+ )}
156
+ {patchCount > 0 && (
157
+ <span className="ml-auto text-theme-text-tertiary text-xs">
158
+ {patchCount} patch{patchCount === 1 ? '' : 'es'}
159
+ </span>
160
+ )}
161
+ </div>
162
+ )
163
+ })}
164
+ </div>
165
+ </Section>
166
+ )}
167
+
168
+ {spec.publishConnectionDetailsWithStoreConfigRef?.name && (
169
+ <Section title="Connection Store" icon={FileText} defaultExpanded={false}>
170
+ <PropertyList>
171
+ <Property label="StoreConfig" value={spec.publishConnectionDetailsWithStoreConfigRef.name} />
172
+ </PropertyList>
173
+ </Section>
174
+ )}
175
+ </>
176
+ )
177
+ }
178
+
179
+ export function CompositionRenderer({ data, onNavigate }: CompositionRendererProps) {
180
+ return <CompositionBody data={data} onNavigate={onNavigate} />
181
+ }
182
+
183
+ interface CompositionRevisionRendererProps {
184
+ data: any
185
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
186
+ }
187
+
188
+ export function CompositionRevisionRenderer({ data, onNavigate }: CompositionRevisionRendererProps) {
189
+ const revision: number | null = data?.spec?.revision ?? null
190
+ const sourceCompositionName = data?.metadata?.labels?.['crossplane.io/composition-name']
191
+ // Crossplane labels active vs inactive revisions via spec.revision being the
192
+ // highest one; the package controller doesn't write a flag. Use ownerRefs as
193
+ // a stronger signal — the source Composition's UID controls the revision.
194
+
195
+ return (
196
+ <>
197
+ {sourceCompositionName && (
198
+ <Section title="Source Composition" icon={ScrollText} defaultExpanded>
199
+ <PropertyList>
200
+ <Property
201
+ label="Composition"
202
+ value={
203
+ <ResourceLink
204
+ name={sourceCompositionName}
205
+ kind="compositions"
206
+ namespace=""
207
+ onNavigate={onNavigate}
208
+ />
209
+ }
210
+ />
211
+ {revision != null && <Property label="Revision" value={`#${revision}`} />}
212
+ </PropertyList>
213
+ </Section>
214
+ )}
215
+ <CompositionBody data={data} onNavigate={onNavigate} revision={revision} />
216
+ </>
217
+ )
218
+ }
@@ -0,0 +1,145 @@
1
+ import { Package, Settings, ScrollText, Layers } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import {
4
+ getProviderStatus,
5
+ getProviderPackage,
6
+ getProviderPackagePullPolicy,
7
+ getProviderCurrentRevision,
8
+ } from '../resource-utils-crossplane'
9
+
10
+ /**
11
+ * Shared renderer for Crossplane package kinds — Provider, Function, Configuration.
12
+ *
13
+ * All three share the same `pkg.crossplane.io` package machinery (spec.package,
14
+ * pull policy, revision history, currentRevision in status, Healthy/Installed
15
+ * conditions). The only material difference is that a Configuration's
16
+ * `status.objectRefs` lists the XRDs/Compositions/Functions it installed —
17
+ * surfaced as an extra section.
18
+ */
19
+ interface CrossplanePackageRendererProps {
20
+ data: any
21
+ /** Human label for the section header (e.g. "Provider", "Function", "Configuration"). */
22
+ kindLabel: string
23
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
24
+ }
25
+
26
+ export function CrossplanePackageRenderer({ data, kindLabel, onNavigate }: CrossplanePackageRendererProps) {
27
+ const status = getProviderStatus(data)
28
+ const pkg = getProviderPackage(data)
29
+ const pullPolicy = getProviderPackagePullPolicy(data)
30
+ const currentRevision = getProviderCurrentRevision(data)
31
+ const conditions = data?.status?.conditions || []
32
+
33
+ const installed = conditions.find((c: any) => c.type === 'Installed')
34
+ const healthy = conditions.find((c: any) => c.type === 'Healthy')
35
+ const objectRefs: any[] = data?.status?.objectRefs || []
36
+ const dependsOn: any[] = data?.spec?.dependsOn || []
37
+
38
+ return (
39
+ <>
40
+ {installed?.status === 'False' && (
41
+ <AlertBanner
42
+ variant="error"
43
+ title={`${kindLabel} install failed`}
44
+ message={installed.message || installed.reason || 'Package failed to install.'}
45
+ />
46
+ )}
47
+ {installed?.status === 'True' && healthy?.status === 'False' && (
48
+ <AlertBanner
49
+ variant="warning"
50
+ title={`${kindLabel} unhealthy`}
51
+ message={healthy.message || healthy.reason || `${kindLabel} package is installed but its controller is not healthy.`}
52
+ />
53
+ )}
54
+
55
+ <Section title={kindLabel} icon={Package} defaultExpanded>
56
+ <PropertyList>
57
+ <Property label="Package" value={<span className="font-mono break-all">{pkg}</span>} />
58
+ {pullPolicy && <Property label="Pull Policy" value={pullPolicy} />}
59
+ {data?.spec?.revisionActivationPolicy && (
60
+ <Property label="Revision Activation" value={data.spec.revisionActivationPolicy} />
61
+ )}
62
+ {typeof data?.spec?.revisionHistoryLimit === 'number' && (
63
+ <Property label="Revision History Limit" value={String(data.spec.revisionHistoryLimit)} />
64
+ )}
65
+ {data?.spec?.skipDependencyResolution !== undefined && (
66
+ <Property label="Skip Dependency Resolution" value={data.spec.skipDependencyResolution ? 'Yes' : 'No'} />
67
+ )}
68
+ <Property label="Status" value={status.text} />
69
+ </PropertyList>
70
+ </Section>
71
+
72
+ {(currentRevision || data?.status?.currentIdentifier) && (
73
+ <Section title="Revision" icon={ScrollText} defaultExpanded>
74
+ <PropertyList>
75
+ {currentRevision && (
76
+ <Property label="Current Revision" value={<span className="font-mono break-all">{currentRevision}</span>} />
77
+ )}
78
+ {data?.status?.currentIdentifier && (
79
+ <Property
80
+ label="Current Identifier"
81
+ value={<span className="font-mono break-all">{data.status.currentIdentifier}</span>}
82
+ />
83
+ )}
84
+ </PropertyList>
85
+ </Section>
86
+ )}
87
+
88
+ {data?.spec?.runtimeConfigRef?.name && (
89
+ <Section title="Runtime Config" icon={Settings} defaultExpanded>
90
+ <PropertyList>
91
+ <Property
92
+ label="Name"
93
+ value={
94
+ <ResourceLink
95
+ name={data.spec.runtimeConfigRef.name}
96
+ kind="deploymentruntimeconfigs"
97
+ namespace=""
98
+ onNavigate={onNavigate}
99
+ />
100
+ }
101
+ />
102
+ </PropertyList>
103
+ </Section>
104
+ )}
105
+
106
+ {dependsOn.length > 0 && (
107
+ <Section title={`Dependencies (${dependsOn.length})`} icon={Layers} defaultExpanded>
108
+ <div className="space-y-1">
109
+ {dependsOn.map((dep: any, i: number) => {
110
+ const ref = dep.provider || dep.function || dep.configuration
111
+ const depKind = dep.provider ? 'Provider' : dep.function ? 'Function' : dep.configuration ? 'Configuration' : 'Unknown'
112
+ return (
113
+ <div key={i} className="card-inner text-sm flex items-center gap-2 flex-wrap">
114
+ <span className="badge-sm status-neutral">{depKind}</span>
115
+ <span className="font-mono text-theme-text-secondary break-all">{ref || '-'}</span>
116
+ {dep.version && (
117
+ <span className="text-theme-text-tertiary text-xs">version: {dep.version}</span>
118
+ )}
119
+ </div>
120
+ )
121
+ })}
122
+ </div>
123
+ </Section>
124
+ )}
125
+
126
+ {objectRefs.length > 0 && (
127
+ <Section title={`Installed Resources (${objectRefs.length})`} icon={Layers} defaultExpanded>
128
+ <div className="space-y-1">
129
+ {objectRefs.map((ref: any, i: number) => (
130
+ <div key={i} className="card-inner text-sm flex items-center gap-2 flex-wrap">
131
+ <span className="badge-sm status-neutral">{ref.kind || 'Unknown'}</span>
132
+ <span className="font-mono text-theme-text-secondary break-all">{ref.name}</span>
133
+ {ref.apiVersion && (
134
+ <span className="text-theme-text-tertiary text-xs">{ref.apiVersion}</span>
135
+ )}
136
+ </div>
137
+ ))}
138
+ </div>
139
+ </Section>
140
+ )}
141
+
142
+ <ConditionsSection conditions={conditions} />
143
+ </>
144
+ )
145
+ }
@@ -0,0 +1,71 @@
1
+ import { KeyRound, Settings } from 'lucide-react'
2
+ import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { CodeViewer } from '../../ui/CodeViewer'
4
+ import { getProviderConfigStatus, getProviderConfigCredentialsSource } from '../resource-utils-crossplane'
5
+
6
+ interface CrossplaneProviderConfigRendererProps {
7
+ data: any
8
+ onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
9
+ }
10
+
11
+ function extractApiGroup(apiVersion?: string): string {
12
+ if (!apiVersion) return ''
13
+ const slash = apiVersion.indexOf('/')
14
+ return slash < 0 ? '' : apiVersion.slice(0, slash)
15
+ }
16
+
17
+ export function CrossplaneProviderConfigRenderer({ data, onNavigate }: CrossplaneProviderConfigRendererProps) {
18
+ const status = getProviderConfigStatus(data)
19
+ const credentialsSource = getProviderConfigCredentialsSource(data)
20
+ const apiGroup = extractApiGroup(data?.apiVersion)
21
+ const secretRef = data?.spec?.credentials?.secretRef
22
+ const namespace = data?.metadata?.namespace || ''
23
+ const users = typeof data?.status?.users === 'number' ? data.status.users : null
24
+
25
+ return (
26
+ <>
27
+ {credentialsSource === 'None' && (
28
+ <AlertBanner
29
+ variant="info"
30
+ title="No credentials configured"
31
+ message="This ProviderConfig has credentials.source set to None — only useful for providers that auto-discover credentials (e.g. in-cluster K8s, IRSA)."
32
+ />
33
+ )}
34
+
35
+ <Section title="ProviderConfig" icon={Settings} defaultExpanded>
36
+ <PropertyList>
37
+ <Property label="API Group" value={apiGroup || '-'} />
38
+ <Property label="Credentials Source" value={credentialsSource} />
39
+ {users !== null && <Property label="In Use By" value={`${users} resource${users === 1 ? '' : 's'}`} />}
40
+ <Property label="Status" value={status.text} />
41
+ </PropertyList>
42
+ </Section>
43
+
44
+ {secretRef && (
45
+ <Section title="Credentials Secret" icon={KeyRound} defaultExpanded>
46
+ <PropertyList>
47
+ <Property
48
+ label="Secret"
49
+ value={
50
+ <ResourceLink
51
+ name={secretRef.name}
52
+ kind="secrets"
53
+ namespace={secretRef.namespace || namespace}
54
+ onNavigate={onNavigate}
55
+ />
56
+ }
57
+ />
58
+ {secretRef.namespace && <Property label="Namespace" value={secretRef.namespace} />}
59
+ {secretRef.key && <Property label="Key" value={<span className="font-mono">{secretRef.key}</span>} />}
60
+ </PropertyList>
61
+ </Section>
62
+ )}
63
+
64
+ {data?.spec && (
65
+ <Section title="Spec" icon={Settings} defaultExpanded={false}>
66
+ <CodeViewer code={JSON.stringify(data.spec, null, 2)} language="json" />
67
+ </Section>
68
+ )}
69
+ </>
70
+ )
71
+ }