@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.
- package/package.json +6 -1
- 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/resources/ResourcesView.tsx +338 -57
- 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/workload/ResourceDetailDrawer.tsx +1 -1
- package/src/index.ts +3 -0
- 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/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
|
@@ -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
|
+
}
|