@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,41 +1,67 @@
|
|
|
1
|
-
import { Shield, Users } from 'lucide-react'
|
|
1
|
+
import { Shield, Users, Eye } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ResourceLink, AlertBanner } from '../../ui/drawer-components'
|
|
4
|
-
import {
|
|
4
|
+
import type { ResourceRef, RBACPolicyRule } from '../../../types'
|
|
5
|
+
import {
|
|
6
|
+
rbacVerbBadgeClass,
|
|
7
|
+
rbacResourceBadgeClass,
|
|
8
|
+
rbacApiGroupBadgeClass,
|
|
9
|
+
rbacKindBadgeClass,
|
|
10
|
+
} from '../../../utils/rbac-badges'
|
|
11
|
+
import { BADGE_SEVERITY_COLORS } from '../../ui/Badge'
|
|
5
12
|
|
|
6
13
|
interface RoleBindingRendererProps {
|
|
7
14
|
data: any
|
|
8
|
-
onNavigate?: (ref:
|
|
15
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
16
|
+
/** Rules from the referenced Role/ClusterRole. Undefined means the host
|
|
17
|
+
* hasn't wired the fetch (inline rules preview is omitted). Null means
|
|
18
|
+
* the fetch failed; the section shows a tactful note instead of silence. */
|
|
19
|
+
roleRules?: RBACPolicyRule[] | null
|
|
20
|
+
roleRulesLoading?: boolean
|
|
9
21
|
}
|
|
10
22
|
|
|
23
|
+
// Wide groups whose membership effectively widens a binding beyond a named
|
|
24
|
+
// subject. Kept in sync with pkg/rbac/builtins.go (WideGroups). Mismatch
|
|
25
|
+
// here = misleading or missing warning, not a security flaw — but cross-
|
|
26
|
+
// check if you add to one side, add to the other.
|
|
27
|
+
const WIDE_GROUP_DESCRIPTIONS: Record<string, string> = {
|
|
28
|
+
'system:authenticated':
|
|
29
|
+
'every authenticated entity in the cluster, including external OIDC users',
|
|
30
|
+
'system:unauthenticated':
|
|
31
|
+
'every unauthenticated (anonymous) request',
|
|
32
|
+
'system:masters':
|
|
33
|
+
'the system:masters super-user group, which bypasses authorization',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findWideGroupSubject(subjects: any[]): { name: string; description: string } | null {
|
|
37
|
+
for (const s of subjects) {
|
|
38
|
+
if (s.kind !== 'Group') continue
|
|
39
|
+
const d = WIDE_GROUP_DESCRIPTIONS[s.name]
|
|
40
|
+
if (d) return { name: s.name, description: d }
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Subject-kind colors (ServiceAccount/User/Group). Reuse theme-aware palette
|
|
46
|
+
// classes for proper light/dark contrast.
|
|
11
47
|
function getSubjectKindBadgeClass(kind: string): string {
|
|
12
48
|
switch (kind) {
|
|
13
49
|
case 'ServiceAccount':
|
|
14
|
-
return
|
|
50
|
+
return BADGE_SEVERITY_COLORS.success
|
|
15
51
|
case 'User':
|
|
16
|
-
return
|
|
52
|
+
return BADGE_SEVERITY_COLORS.info
|
|
17
53
|
case 'Group':
|
|
18
|
-
return '
|
|
19
|
-
default:
|
|
20
|
-
return BADGE_INACTIVE
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getRoleRefKindBadgeClass(kind: string): string {
|
|
25
|
-
switch (kind) {
|
|
26
|
-
case 'Role':
|
|
27
|
-
return 'bg-blue-500/20 text-blue-400'
|
|
28
|
-
case 'ClusterRole':
|
|
29
|
-
return 'bg-purple-500/20 text-purple-400'
|
|
54
|
+
return rbacKindBadgeClass('ClusterRole') // purple — distinct from User
|
|
30
55
|
default:
|
|
31
|
-
return
|
|
56
|
+
return BADGE_SEVERITY_COLORS.neutral
|
|
32
57
|
}
|
|
33
58
|
}
|
|
34
59
|
|
|
35
|
-
export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererProps) {
|
|
60
|
+
export function RoleBindingRenderer({ data, onNavigate, roleRules, roleRulesLoading }: RoleBindingRendererProps) {
|
|
36
61
|
const roleRef = data.roleRef || {}
|
|
37
62
|
const subjects: any[] = data.subjects || []
|
|
38
63
|
const isClusterRoleBinding = data.kind === 'ClusterRoleBinding'
|
|
64
|
+
const wideGroup = findWideGroupSubject(subjects)
|
|
39
65
|
|
|
40
66
|
return (
|
|
41
67
|
<>
|
|
@@ -47,6 +73,19 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
|
|
|
47
73
|
/>
|
|
48
74
|
)}
|
|
49
75
|
|
|
76
|
+
{/* Wide-group warning — describe the blast radius, don't just label it. */}
|
|
77
|
+
{wideGroup && (
|
|
78
|
+
<AlertBanner variant="warning" title={`Wide grant: ${wideGroup.name}`}>
|
|
79
|
+
<div className="text-xs">
|
|
80
|
+
This binding grants{' '}
|
|
81
|
+
<span className="text-theme-text-secondary font-medium">
|
|
82
|
+
{roleRef.kind || 'a role'}{roleRef.name ? ` "${roleRef.name}"` : ''}
|
|
83
|
+
</span>{' '}
|
|
84
|
+
to {wideGroup.description}.
|
|
85
|
+
</div>
|
|
86
|
+
</AlertBanner>
|
|
87
|
+
)}
|
|
88
|
+
|
|
50
89
|
{isClusterRoleBinding && (
|
|
51
90
|
<div className="p-2 bg-blue-500/10 border border-blue-500/30 rounded text-xs text-blue-300/80 flex items-start gap-2">
|
|
52
91
|
<span>ClusterRoleBinding grants permissions across all namespaces.</span>
|
|
@@ -59,7 +98,7 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
|
|
|
59
98
|
label="Kind"
|
|
60
99
|
value={
|
|
61
100
|
roleRef.kind ? (
|
|
62
|
-
<span className={clsx('badge',
|
|
101
|
+
<span className={clsx('badge', rbacKindBadgeClass(roleRef.kind))}>
|
|
63
102
|
{roleRef.kind}
|
|
64
103
|
</span>
|
|
65
104
|
) : undefined
|
|
@@ -72,6 +111,16 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
|
|
|
72
111
|
</PropertyList>
|
|
73
112
|
</Section>
|
|
74
113
|
|
|
114
|
+
{/* Inline rules preview — saves a click when operators are validating
|
|
115
|
+
* "what does this binding actually grant". Only when host wired fetch. */}
|
|
116
|
+
{roleRules !== undefined && (
|
|
117
|
+
<RulesPreviewSection
|
|
118
|
+
rules={roleRules}
|
|
119
|
+
loading={!!roleRulesLoading}
|
|
120
|
+
roleName={roleRef.name}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
|
|
75
124
|
<Section title={`Subjects (${subjects.length})`} icon={Users} defaultExpanded>
|
|
76
125
|
<div className="space-y-2">
|
|
77
126
|
{subjects.map((subject: any, i: number) => (
|
|
@@ -85,6 +134,9 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
|
|
|
85
134
|
) : (
|
|
86
135
|
<span className="text-theme-text-primary font-medium">{subject.name}</span>
|
|
87
136
|
)}
|
|
137
|
+
{subject.kind === 'Group' && WIDE_GROUP_DESCRIPTIONS[subject.name] && (
|
|
138
|
+
<span className={clsx('badge text-xs', BADGE_SEVERITY_COLORS.alert)}>wide</span>
|
|
139
|
+
)}
|
|
88
140
|
</div>
|
|
89
141
|
<div className="text-xs text-theme-text-tertiary mt-1">
|
|
90
142
|
Namespace: {subject.kind === 'ServiceAccount' ? (subject.namespace || 'default') : '-'}
|
|
@@ -96,3 +148,63 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
|
|
|
96
148
|
</>
|
|
97
149
|
)
|
|
98
150
|
}
|
|
151
|
+
|
|
152
|
+
function RulesPreviewSection({
|
|
153
|
+
rules,
|
|
154
|
+
loading,
|
|
155
|
+
roleName,
|
|
156
|
+
}: {
|
|
157
|
+
rules: RBACPolicyRule[] | null
|
|
158
|
+
loading: boolean
|
|
159
|
+
roleName?: string
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
<Section title="Rules Granted" icon={Eye} defaultExpanded={false}>
|
|
163
|
+
{loading ? (
|
|
164
|
+
<div className="text-sm text-theme-text-tertiary">Loading rules…</div>
|
|
165
|
+
) : !rules ? (
|
|
166
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
167
|
+
Could not resolve referenced role
|
|
168
|
+
{roleName ? ` "${roleName}"` : ''} — it may not exist (orphan binding)
|
|
169
|
+
or be unavailable.
|
|
170
|
+
</div>
|
|
171
|
+
) : rules.length === 0 ? (
|
|
172
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
173
|
+
The referenced role has no rules.
|
|
174
|
+
</div>
|
|
175
|
+
) : (
|
|
176
|
+
<div className="space-y-1">
|
|
177
|
+
{rules.map((r, i) => {
|
|
178
|
+
const resources = r.resources ?? []
|
|
179
|
+
const nonResourceURLs = r.nonResourceURLs ?? []
|
|
180
|
+
// Non-resource rules (e.g. system:discovery grants `get` on
|
|
181
|
+
// /api, /healthz) have no resources/apiGroups - render the
|
|
182
|
+
// URLs in place so the line doesn't read as a bare "X on"
|
|
183
|
+
// with nothing after it.
|
|
184
|
+
const isNonResource = resources.length === 0 && nonResourceURLs.length > 0
|
|
185
|
+
return (
|
|
186
|
+
<div key={i} className="flex items-center gap-1 flex-wrap text-xs">
|
|
187
|
+
{(r.verbs ?? []).map((v) => (
|
|
188
|
+
<span key={v} className={clsx('badge', rbacVerbBadgeClass(v))}>{v}</span>
|
|
189
|
+
))}
|
|
190
|
+
<span className="text-theme-text-secondary">on</span>
|
|
191
|
+
{isNonResource ? (
|
|
192
|
+
nonResourceURLs.map((u) => (
|
|
193
|
+
<span key={u} className="badge text-xs font-mono bg-theme-elevated text-theme-text-secondary">{u}</span>
|
|
194
|
+
))
|
|
195
|
+
) : (
|
|
196
|
+
resources.map((res) => (
|
|
197
|
+
<span key={res} className={clsx('badge', rbacResourceBadgeClass)}>{res}</span>
|
|
198
|
+
))
|
|
199
|
+
)}
|
|
200
|
+
{!isNonResource && (r.apiGroups ?? []).filter((g) => g !== '').map((g) => (
|
|
201
|
+
<span key={g} className={clsx('badge', rbacApiGroupBadgeClass)}>{g}</span>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
})}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</Section>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import { Shield, Info } from 'lucide-react'
|
|
1
|
+
import { Shield, Info, Users } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import type { RBACRoleResponse, RBACSubject, ResourceRef } from '../../../types'
|
|
5
|
+
import { rbacKindBadgeClass, rbacVerbBadgeClass } from '../../../utils/rbac-badges'
|
|
4
6
|
|
|
5
7
|
interface RoleRendererProps {
|
|
6
8
|
data: any
|
|
9
|
+
/** Reverse-lookup data from /api/rbac/role/{kind}/{namespace}/{name}.
|
|
10
|
+
* Undefined when the host hasn't wired the fetch (Bindings section is
|
|
11
|
+
* omitted). Null when the fetch failed (renders an inline error). */
|
|
12
|
+
rbacRoleData?: RBACRoleResponse | null
|
|
13
|
+
rbacRoleLoading?: boolean
|
|
14
|
+
rbacRoleError?: Error | null
|
|
15
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
7
16
|
}
|
|
8
17
|
|
|
9
|
-
const READ_VERBS = new Set(['get', 'list', 'watch'])
|
|
10
|
-
const WRITE_VERBS = new Set(['create', 'update', 'patch'])
|
|
11
|
-
const DELETE_VERBS = new Set(['delete', 'deletecollection'])
|
|
12
18
|
|
|
13
|
-
function
|
|
14
|
-
if (verb === '*') return 'bg-red-500/20 text-red-400'
|
|
15
|
-
if (READ_VERBS.has(verb)) return 'bg-green-500/20 text-green-400'
|
|
16
|
-
if (WRITE_VERBS.has(verb)) return 'bg-yellow-500/20 text-yellow-400'
|
|
17
|
-
if (DELETE_VERBS.has(verb)) return 'bg-red-500/20 text-red-400'
|
|
18
|
-
return 'bg-blue-500/20 text-blue-400'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function RoleRenderer({ data }: RoleRendererProps) {
|
|
19
|
+
export function RoleRenderer({ data, rbacRoleData, rbacRoleLoading, rbacRoleError, onNavigate }: RoleRendererProps) {
|
|
22
20
|
const rules = data.rules || []
|
|
23
21
|
const aggregationRule = data.aggregationRule
|
|
24
22
|
|
|
@@ -36,6 +34,16 @@ export function RoleRenderer({ data }: RoleRendererProps) {
|
|
|
36
34
|
/>
|
|
37
35
|
)}
|
|
38
36
|
|
|
37
|
+
{/* Bindings reverse-lookup — only when host wired the fetch. */}
|
|
38
|
+
{rbacRoleData !== undefined && (
|
|
39
|
+
<RoleBindingsSection
|
|
40
|
+
rbacRoleData={rbacRoleData}
|
|
41
|
+
loading={!!rbacRoleLoading}
|
|
42
|
+
error={rbacRoleError ?? null}
|
|
43
|
+
onNavigate={onNavigate}
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
|
|
39
47
|
{/* Overview */}
|
|
40
48
|
<Section title="Overview" icon={Shield}>
|
|
41
49
|
<PropertyList>
|
|
@@ -107,10 +115,7 @@ export function RoleRenderer({ data }: RoleRendererProps) {
|
|
|
107
115
|
{rule.verbs.map((verb: string) => (
|
|
108
116
|
<span
|
|
109
117
|
key={verb}
|
|
110
|
-
className={clsx(
|
|
111
|
-
'badge',
|
|
112
|
-
getVerbColor(verb)
|
|
113
|
-
)}
|
|
118
|
+
className={clsx('badge', rbacVerbBadgeClass(verb))}
|
|
114
119
|
>
|
|
115
120
|
{verb}
|
|
116
121
|
</span>
|
|
@@ -190,3 +195,128 @@ export function RoleRenderer({ data }: RoleRendererProps) {
|
|
|
190
195
|
</>
|
|
191
196
|
)
|
|
192
197
|
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// ROLE BINDINGS REVERSE-LOOKUP SECTION
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
interface RoleBindingsSectionProps {
|
|
204
|
+
rbacRoleData: RBACRoleResponse | null
|
|
205
|
+
loading: boolean
|
|
206
|
+
error: Error | null
|
|
207
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function RoleBindingsSection({ rbacRoleData, loading, error, onNavigate }: RoleBindingsSectionProps) {
|
|
211
|
+
if (loading) {
|
|
212
|
+
return (
|
|
213
|
+
<Section title="Bindings" icon={Users}>
|
|
214
|
+
<div className="text-sm text-theme-text-tertiary">Loading bindings…</div>
|
|
215
|
+
</Section>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
if (error) {
|
|
219
|
+
return (
|
|
220
|
+
<Section title="Bindings" icon={Users}>
|
|
221
|
+
<div className="text-sm text-red-400">
|
|
222
|
+
Could not load bindings: {error.message}
|
|
223
|
+
</div>
|
|
224
|
+
</Section>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
if (!rbacRoleData) return null
|
|
228
|
+
|
|
229
|
+
const bindings = rbacRoleData.bindings ?? []
|
|
230
|
+
return (
|
|
231
|
+
<Section title={`Bindings (${bindings.length})`} icon={Users} defaultExpanded>
|
|
232
|
+
{bindings.length === 0 ? (
|
|
233
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
234
|
+
No bindings reference this role. It grants no effective permissions.
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<div className="space-y-2">
|
|
238
|
+
{bindings.map((b) => {
|
|
239
|
+
const bindingKindPlural =
|
|
240
|
+
b.binding.kind === 'RoleBinding' ? 'rolebindings' : 'clusterrolebindings'
|
|
241
|
+
return (
|
|
242
|
+
<div key={b.binding.kind + '/' + b.binding.namespace + '/' + b.binding.name} className="card-inner">
|
|
243
|
+
{/* items-center + uniform text-xs so the badge, link, and
|
|
244
|
+
* "granted to N subjects" footnote share a baseline. */}
|
|
245
|
+
<div className="flex items-center gap-2 flex-wrap text-xs mb-2">
|
|
246
|
+
<span className={clsx('badge', rbacKindBadgeClass(b.binding.kind))}>
|
|
247
|
+
{b.binding.kind}
|
|
248
|
+
</span>
|
|
249
|
+
<ResourceLink
|
|
250
|
+
kind={bindingKindPlural}
|
|
251
|
+
namespace={b.binding.namespace}
|
|
252
|
+
name={b.binding.name}
|
|
253
|
+
onNavigate={onNavigate}
|
|
254
|
+
/>
|
|
255
|
+
<span className="text-theme-text-secondary">
|
|
256
|
+
granted to {b.subjects.length} subject{b.subjects.length === 1 ? '' : 's'}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
<RoleBindingSubjectsList subjects={b.subjects} onNavigate={onNavigate} />
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
})}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</Section>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function RoleBindingSubjectsList({
|
|
270
|
+
subjects,
|
|
271
|
+
onNavigate,
|
|
272
|
+
}: {
|
|
273
|
+
subjects: RBACSubject[]
|
|
274
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
275
|
+
}) {
|
|
276
|
+
const inline = subjects.slice(0, 3)
|
|
277
|
+
const hidden = subjects.length - inline.length
|
|
278
|
+
return (
|
|
279
|
+
<div className="flex flex-wrap gap-1 items-center text-xs">
|
|
280
|
+
{inline.map((s, i) => (
|
|
281
|
+
<RoleBindingSubjectChip key={i} subject={s} onNavigate={onNavigate} />
|
|
282
|
+
))}
|
|
283
|
+
{hidden > 0 && (
|
|
284
|
+
<span className="text-theme-text-secondary">+{hidden} more</span>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function RoleBindingSubjectChip({
|
|
291
|
+
subject,
|
|
292
|
+
onNavigate,
|
|
293
|
+
}: {
|
|
294
|
+
subject: RBACSubject
|
|
295
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
296
|
+
}) {
|
|
297
|
+
// ServiceAccounts have a detail page; Users and Groups don't (those are
|
|
298
|
+
// external identities — there's no "User" resource in Kubernetes).
|
|
299
|
+
if (subject.kind === 'ServiceAccount') {
|
|
300
|
+
return (
|
|
301
|
+
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-theme-elevated border border-theme-border">
|
|
302
|
+
<span className="text-theme-text-secondary">sa:</span>
|
|
303
|
+
<ResourceLink
|
|
304
|
+
kind="serviceaccounts"
|
|
305
|
+
namespace={subject.namespace}
|
|
306
|
+
name={subject.name}
|
|
307
|
+
onNavigate={onNavigate}
|
|
308
|
+
/>
|
|
309
|
+
{subject.namespace && <span className="text-theme-text-secondary">({subject.namespace})</span>}
|
|
310
|
+
</span>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
return (
|
|
314
|
+
<span
|
|
315
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-theme-elevated border border-theme-border text-theme-text-primary"
|
|
316
|
+
title={`${subject.kind} — no detail page (external identity)`}
|
|
317
|
+
>
|
|
318
|
+
<span className="text-theme-text-secondary">{subject.kind.toLowerCase()}:</span>
|
|
319
|
+
{subject.name}
|
|
320
|
+
</span>
|
|
321
|
+
)
|
|
322
|
+
}
|