@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
|
@@ -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'
|