@datalayer/core 1.0.21 → 1.0.22
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/lib/components/billing/BillableAccountSelect.d.ts +35 -0
- package/lib/components/billing/BillableAccountSelect.js +340 -0
- package/lib/components/billing/index.d.ts +1 -0
- package/lib/components/billing/index.js +5 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +2 -0
- package/lib/components/principal/Principal.d.ts +37 -0
- package/lib/components/principal/Principal.js +54 -0
- package/lib/components/principal/PrincipalAvatar.d.ts +10 -0
- package/lib/components/principal/PrincipalAvatar.js +55 -0
- package/lib/components/principal/PrincipalBadge.d.ts +20 -0
- package/lib/components/principal/PrincipalBadge.js +208 -0
- package/lib/components/principal/PrincipalBanner.d.ts +7 -0
- package/lib/components/principal/PrincipalBanner.js +84 -0
- package/lib/components/principal/PrincipalDetailsOverlay.d.ts +23 -0
- package/lib/components/principal/PrincipalDetailsOverlay.js +86 -0
- package/lib/components/principal/PrincipalSwitcherMenu.d.ts +14 -0
- package/lib/components/principal/PrincipalSwitcherMenu.js +287 -0
- package/lib/components/principal/index.d.ts +7 -0
- package/lib/components/principal/index.js +10 -0
- package/lib/components/sharing/ShareAccessComponent.d.ts +18 -0
- package/lib/components/sharing/ShareAccessComponent.js +1368 -0
- package/lib/components/sharing/SharingEditor.d.ts +20 -0
- package/lib/components/sharing/SharingEditor.js +65 -0
- package/lib/components/sharing/index.d.ts +9 -0
- package/lib/components/sharing/index.js +12 -0
- package/lib/hooks/index.d.ts +5 -0
- package/lib/hooks/index.js +5 -0
- package/lib/hooks/useBillableAccountStore.d.ts +31 -0
- package/lib/hooks/useBillableAccountStore.js +53 -0
- package/lib/hooks/usePrincipalStore.d.ts +41 -0
- package/lib/hooks/usePrincipalStore.js +101 -0
- package/lib/hooks/useSelectedBillableAccount.d.ts +14 -0
- package/lib/hooks/useSelectedBillableAccount.js +36 -0
- package/lib/hooks/useSelectedPrincipal.d.ts +17 -0
- package/lib/hooks/useSelectedPrincipal.js +29 -0
- package/lib/hooks/useUsageRefreshStore.d.ts +8 -0
- package/lib/hooks/useUsageRefreshStore.js +16 -0
- package/lib/utils/Handles.d.ts +3 -0
- package/lib/utils/Handles.js +53 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/package.json +1 -3
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { KeyIcon, PersonIcon, OrganizationIcon, PeopleIcon, } from '@primer/octicons-react';
|
|
8
|
+
import { Box } from '@datalayer/primer-addons';
|
|
9
|
+
import { ActionList, ActionMenu, Button, Dialog, Label, Spinner, Text, TextInput, } from '@primer/react';
|
|
10
|
+
import { useToast } from '../../hooks/index.js';
|
|
11
|
+
import { useCoreStore, useIAMStore } from '../../state/index.js';
|
|
12
|
+
import { PrincipalAvatar } from '../principal/PrincipalAvatar.js';
|
|
13
|
+
import { PrincipalBadge } from '../principal/PrincipalBadge.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Constants.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
const ACCESS_LEVELS = ['view', 'update', 'execute'];
|
|
18
|
+
const DEFAULT_PRINCIPAL_KINDS = [
|
|
19
|
+
'user',
|
|
20
|
+
'team',
|
|
21
|
+
'organization',
|
|
22
|
+
];
|
|
23
|
+
const ACCESS_LEVEL_LABELS = {
|
|
24
|
+
view: 'Viewer',
|
|
25
|
+
update: 'Editor',
|
|
26
|
+
execute: 'Executor',
|
|
27
|
+
};
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// String / payload helpers.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
function pickFirstString(...values) {
|
|
32
|
+
for (const value of values) {
|
|
33
|
+
if (typeof value === 'string' && value.trim()) {
|
|
34
|
+
return value.trim();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
function normalizePrincipalKind(kindRaw) {
|
|
40
|
+
const kind = (kindRaw || '').trim().toLowerCase();
|
|
41
|
+
if (kind === 'team') {
|
|
42
|
+
return 'team';
|
|
43
|
+
}
|
|
44
|
+
if (kind === 'organization' || kind === 'org') {
|
|
45
|
+
return 'organization';
|
|
46
|
+
}
|
|
47
|
+
return 'user';
|
|
48
|
+
}
|
|
49
|
+
function toTitleCase(value) {
|
|
50
|
+
if (!value) {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
54
|
+
}
|
|
55
|
+
function normalizeUserOrigin(originRaw) {
|
|
56
|
+
const value = (originRaw || '').trim();
|
|
57
|
+
if (!value) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const lower = value.toLowerCase();
|
|
61
|
+
if (lower === 'datalayer') {
|
|
62
|
+
return 'Datalayer';
|
|
63
|
+
}
|
|
64
|
+
const extPrefix = 'urn:dla:iam:ext::';
|
|
65
|
+
if (lower.startsWith(extPrefix)) {
|
|
66
|
+
const suffix = value.slice(extPrefix.length);
|
|
67
|
+
const provider = suffix.split(':')[0]?.trim();
|
|
68
|
+
if (!provider) {
|
|
69
|
+
return 'External';
|
|
70
|
+
}
|
|
71
|
+
return toTitleCase(provider.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
return toTitleCase(lower);
|
|
74
|
+
}
|
|
75
|
+
function ensurePrincipalDisplayName(kind, ...candidates) {
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
78
|
+
return candidate.trim();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return kind === 'organization' ? 'Organization' : 'Principal';
|
|
82
|
+
}
|
|
83
|
+
function isSharingAuthorizationMessage(message) {
|
|
84
|
+
const normalized = (message || '').trim().toLowerCase();
|
|
85
|
+
return normalized.includes('not authorized');
|
|
86
|
+
}
|
|
87
|
+
function principalKey(kind, uid) {
|
|
88
|
+
return `${kind}:${uid.toLowerCase()}`;
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Owner extraction (preserves all current fallbacks).
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
function extractOwnerPrincipals(payload) {
|
|
94
|
+
const ownersFromSharing = Array.isArray(payload?.sharing?.owners)
|
|
95
|
+
? payload.sharing.owners
|
|
96
|
+
: [];
|
|
97
|
+
const ownersFromSpaceField = [
|
|
98
|
+
...(Array.isArray(payload?.space?.shared_owner_user_uids_ss)
|
|
99
|
+
? payload.space.shared_owner_user_uids_ss
|
|
100
|
+
: []),
|
|
101
|
+
...(Array.isArray(payload?.space?.shared_ower_user_uids_ss)
|
|
102
|
+
? payload.space.shared_ower_user_uids_ss
|
|
103
|
+
: []),
|
|
104
|
+
];
|
|
105
|
+
const ownerPayload = payload?.owner ||
|
|
106
|
+
payload?.data?.owner ||
|
|
107
|
+
payload?.item?.owner ||
|
|
108
|
+
payload?.space?.owner ||
|
|
109
|
+
payload?.notebook?.owner ||
|
|
110
|
+
payload?.lexical?.owner ||
|
|
111
|
+
payload?.document?.owner ||
|
|
112
|
+
payload?.cell?.owner ||
|
|
113
|
+
payload?.resource?.owner ||
|
|
114
|
+
payload?.sharing?.owner ||
|
|
115
|
+
{};
|
|
116
|
+
const ownerUid = pickFirstString(ownerPayload?.uid, ownerPayload?.owner_uid, ownerPayload?.ownerUid, ownerPayload?.id, payload?.owner_uid, payload?.ownerUid);
|
|
117
|
+
const ownerHandle = pickFirstString(ownerPayload?.handle_s, ownerPayload?.handle, ownerPayload?.owner_handle, ownerPayload?.ownerHandle, payload?.owner_handle, payload?.ownerHandle);
|
|
118
|
+
const kindFromOwnerPayload = normalizePrincipalKind(pickFirstString(ownerPayload?.kind, ownerPayload?.type, ownerPayload?.principal_kind, ownerPayload?.principalKind, payload?.owner_kind, payload?.ownerKind, payload?.owner_type, payload?.ownerType));
|
|
119
|
+
const accountHandle = pickFirstString(ownerPayload?.organization_handle_s, ownerPayload?.organizationHandle, ownerPayload?.organization_handle, payload?.space?.organization_handle_s, payload?.space?.organizationHandle, payload?.space?.organization_handle);
|
|
120
|
+
const firstName = pickFirstString(ownerPayload?.first_name_t, ownerPayload?.firstName);
|
|
121
|
+
const lastName = pickFirstString(ownerPayload?.last_name_t, ownerPayload?.lastName);
|
|
122
|
+
const fullName = `${firstName} ${lastName}`.trim();
|
|
123
|
+
const displayName = fullName ||
|
|
124
|
+
pickFirstString(ownerPayload?.display_name_t, ownerPayload?.display_name, ownerPayload?.name_t, ownerPayload?.name, ownerHandle, ownerUid);
|
|
125
|
+
const origin = normalizeUserOrigin(pickFirstString(ownerPayload?.origin, ownerPayload?.origin_s, ownerPayload?.origin_t));
|
|
126
|
+
const fallbackOwner = ownerUid || ownerHandle
|
|
127
|
+
? {
|
|
128
|
+
kind: kindFromOwnerPayload,
|
|
129
|
+
uid: ownerUid || ownerHandle,
|
|
130
|
+
handle: ownerHandle || accountHandle || ownerUid,
|
|
131
|
+
displayName,
|
|
132
|
+
avatarUrl: pickFirstString(ownerPayload?.avatar_url_s, ownerPayload?.avatarUrl, ownerPayload?.avatar_url, payload?.owner_avatar_url, payload?.owner_avatar_url_s, payload?.ownerAvatarUrl) || undefined,
|
|
133
|
+
origin,
|
|
134
|
+
accountHandle: accountHandle || undefined,
|
|
135
|
+
}
|
|
136
|
+
: null;
|
|
137
|
+
const ownersFromSharingMapped = ownersFromSharing
|
|
138
|
+
.map((entry) => {
|
|
139
|
+
if (typeof entry === 'string') {
|
|
140
|
+
const uid = entry.trim();
|
|
141
|
+
return uid
|
|
142
|
+
? { kind: 'user', uid, handle: uid, displayName: uid }
|
|
143
|
+
: null;
|
|
144
|
+
}
|
|
145
|
+
const uid = pickFirstString(entry?.uid, entry?.owner_uid, entry?.id);
|
|
146
|
+
if (!uid) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const handle = pickFirstString(entry?.handle_s, entry?.handle, uid);
|
|
150
|
+
const ownerKind = normalizePrincipalKind(pickFirstString(entry?.kind, entry?.type, entry?.principal_kind));
|
|
151
|
+
const ownerOrigin = normalizeUserOrigin(pickFirstString(entry?.origin, entry?.origin_s, entry?.origin_t));
|
|
152
|
+
const ownerDisplayName = pickFirstString(entry?.display_name_t, entry?.display_name, entry?.name_t, entry?.name, handle, uid) || uid;
|
|
153
|
+
const ownerAvatarUrl = pickFirstString(entry?.avatar_url_s, entry?.avatarUrl, entry?.avatar_url) || undefined;
|
|
154
|
+
return {
|
|
155
|
+
kind: ownerKind,
|
|
156
|
+
uid,
|
|
157
|
+
handle,
|
|
158
|
+
displayName: ownerDisplayName,
|
|
159
|
+
avatarUrl: ownerAvatarUrl,
|
|
160
|
+
origin: ownerOrigin,
|
|
161
|
+
accountHandle: pickFirstString(entry?.organization_handle_s, entry?.organization_handle, entry?.organizationHandle) || undefined,
|
|
162
|
+
};
|
|
163
|
+
})
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
const ownersFromSpaceMapped = ownersFromSpaceField
|
|
166
|
+
.map((uid) => {
|
|
167
|
+
if (typeof uid !== 'string' || !uid.trim()) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const normalizedUid = uid.trim();
|
|
171
|
+
return {
|
|
172
|
+
kind: 'user',
|
|
173
|
+
uid: normalizedUid,
|
|
174
|
+
handle: normalizedUid,
|
|
175
|
+
displayName: normalizedUid,
|
|
176
|
+
};
|
|
177
|
+
})
|
|
178
|
+
.filter(Boolean);
|
|
179
|
+
const allOwners = [
|
|
180
|
+
...ownersFromSharingMapped,
|
|
181
|
+
...ownersFromSpaceMapped,
|
|
182
|
+
...(fallbackOwner ? [fallbackOwner] : []),
|
|
183
|
+
];
|
|
184
|
+
const deduped = new Map();
|
|
185
|
+
allOwners.forEach(owner => {
|
|
186
|
+
const key = principalKey(owner.kind, owner.uid);
|
|
187
|
+
if (!deduped.has(key)) {
|
|
188
|
+
deduped.set(key, owner);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return Array.from(deduped.values());
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// AccessByLevel helpers.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
function emptyAccessByLevel() {
|
|
197
|
+
return {
|
|
198
|
+
view: { userUids: [], teamUids: [], organizationUids: [] },
|
|
199
|
+
update: { userUids: [], teamUids: [], organizationUids: [] },
|
|
200
|
+
execute: { userUids: [], teamUids: [], organizationUids: [] },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function bucketFor(kind) {
|
|
204
|
+
return kind === 'user'
|
|
205
|
+
? 'userUids'
|
|
206
|
+
: kind === 'team'
|
|
207
|
+
? 'teamUids'
|
|
208
|
+
: 'organizationUids';
|
|
209
|
+
}
|
|
210
|
+
function hasPrincipal(state, level, kind, uid) {
|
|
211
|
+
const lower = uid.toLowerCase();
|
|
212
|
+
return state[level][bucketFor(kind)].some(value => value.toLowerCase() === lower);
|
|
213
|
+
}
|
|
214
|
+
function withPrincipalAdded(state, level, kind, uid) {
|
|
215
|
+
if (hasPrincipal(state, level, kind, uid)) {
|
|
216
|
+
return state;
|
|
217
|
+
}
|
|
218
|
+
const bucket = bucketFor(kind);
|
|
219
|
+
return {
|
|
220
|
+
...state,
|
|
221
|
+
[level]: {
|
|
222
|
+
...state[level],
|
|
223
|
+
[bucket]: [...state[level][bucket], uid],
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function withPrincipalRemoved(state, kind, uid) {
|
|
228
|
+
const lower = uid.toLowerCase();
|
|
229
|
+
const bucket = bucketFor(kind);
|
|
230
|
+
const next = {
|
|
231
|
+
view: { ...state.view },
|
|
232
|
+
update: { ...state.update },
|
|
233
|
+
execute: { ...state.execute },
|
|
234
|
+
};
|
|
235
|
+
for (const level of ACCESS_LEVELS) {
|
|
236
|
+
next[level][bucket] = next[level][bucket].filter(value => value.toLowerCase() !== lower);
|
|
237
|
+
}
|
|
238
|
+
return next;
|
|
239
|
+
}
|
|
240
|
+
function buildAclEntries(state, principalKinds) {
|
|
241
|
+
const allowed = new Set(principalKinds);
|
|
242
|
+
const byPrincipal = new Map();
|
|
243
|
+
const upsert = (kind, uid, level) => {
|
|
244
|
+
if (!allowed.has(kind)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const key = principalKey(kind, uid);
|
|
248
|
+
const existing = byPrincipal.get(key);
|
|
249
|
+
if (!existing) {
|
|
250
|
+
byPrincipal.set(key, { kind, uid, levels: [level] });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!existing.levels.includes(level)) {
|
|
254
|
+
existing.levels.push(level);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
for (const level of ACCESS_LEVELS) {
|
|
258
|
+
state[level].userUids.forEach(uid => upsert('user', uid, level));
|
|
259
|
+
state[level].teamUids.forEach(uid => upsert('team', uid, level));
|
|
260
|
+
state[level].organizationUids.forEach(uid => upsert('organization', uid, level));
|
|
261
|
+
}
|
|
262
|
+
return Array.from(byPrincipal.values()).sort((a, b) => {
|
|
263
|
+
if (a.kind !== b.kind) {
|
|
264
|
+
return a.kind.localeCompare(b.kind);
|
|
265
|
+
}
|
|
266
|
+
return a.uid.localeCompare(b.uid);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function hydrateAccessFromSharing(sharing) {
|
|
270
|
+
const access = sharing.access || {};
|
|
271
|
+
const view = access.view || {};
|
|
272
|
+
const update = access.update || {};
|
|
273
|
+
const execute = access.execute || {};
|
|
274
|
+
return {
|
|
275
|
+
view: {
|
|
276
|
+
userUids: [...(view.userUids || [])],
|
|
277
|
+
teamUids: [...(view.teamUids || [])],
|
|
278
|
+
organizationUids: [...(view.organizationUids || [])],
|
|
279
|
+
},
|
|
280
|
+
update: {
|
|
281
|
+
userUids: [...(update.userUids || [])],
|
|
282
|
+
teamUids: [...(update.teamUids || [])],
|
|
283
|
+
organizationUids: [...(update.organizationUids || [])],
|
|
284
|
+
},
|
|
285
|
+
execute: {
|
|
286
|
+
userUids: [...(execute.userUids || [])],
|
|
287
|
+
teamUids: [...(execute.teamUids || [])],
|
|
288
|
+
organizationUids: [...(execute.organizationUids || [])],
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Avatar shimmer (used while a user row is being hydrated).
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
function AvatarShimmer({ size = 20 }) {
|
|
296
|
+
return (_jsx(Box, { sx: {
|
|
297
|
+
width: size,
|
|
298
|
+
height: size,
|
|
299
|
+
borderRadius: '50%',
|
|
300
|
+
backgroundImage: 'linear-gradient(90deg, var(--bgColor-muted, #d0d7de) 25%, var(--bgColor-default, #ffffff) 50%, var(--bgColor-muted, #d0d7de) 75%)',
|
|
301
|
+
backgroundSize: '220% 100%',
|
|
302
|
+
animation: 'avatarShimmerAcl 1.2s ease-in-out infinite',
|
|
303
|
+
'@keyframes avatarShimmerAcl': {
|
|
304
|
+
'0%': { backgroundPosition: '100% 0' },
|
|
305
|
+
'100%': { backgroundPosition: '-100% 0' },
|
|
306
|
+
},
|
|
307
|
+
} }));
|
|
308
|
+
}
|
|
309
|
+
function OwnerPrincipalRow({ ownerPrincipal, cache, showAvatarSkeleton = false, isPlatformAdmin, }) {
|
|
310
|
+
const entry = cache[principalKey(ownerPrincipal.kind, ownerPrincipal.uid)] || {};
|
|
311
|
+
const cachedHandle = entry.handle;
|
|
312
|
+
const safeCachedHandle = cachedHandle && cachedHandle !== ownerPrincipal.uid ? cachedHandle : '';
|
|
313
|
+
const safeOwnerHandle = ownerPrincipal.handle && ownerPrincipal.handle !== ownerPrincipal.uid
|
|
314
|
+
? ownerPrincipal.handle
|
|
315
|
+
: '';
|
|
316
|
+
const resolvedHandle = safeCachedHandle || safeOwnerHandle || ownerPrincipal.accountHandle;
|
|
317
|
+
const resolvedAccountHandle = entry.accountHandle || ownerPrincipal.accountHandle;
|
|
318
|
+
const resolvedDisplayName = ensurePrincipalDisplayName(ownerPrincipal.kind, ownerPrincipal.displayName, entry.displayName, resolvedHandle, ownerPrincipal.handle, ownerPrincipal.accountHandle, ownerPrincipal.uid);
|
|
319
|
+
const resolvedAvatarUrl = ownerPrincipal.avatarUrl || entry.avatarUrl;
|
|
320
|
+
const resolvedOrigin = ownerPrincipal.origin ||
|
|
321
|
+
entry.origin ||
|
|
322
|
+
(ownerPrincipal.kind === 'user' ? 'Datalayer' : undefined);
|
|
323
|
+
return (_jsx(Box, { sx: {
|
|
324
|
+
display: 'inline-flex',
|
|
325
|
+
alignItems: 'center',
|
|
326
|
+
gap: 1,
|
|
327
|
+
flexWrap: 'wrap',
|
|
328
|
+
}, children: showAvatarSkeleton ? (_jsxs(_Fragment, { children: [_jsx(AvatarShimmer, { size: 20 }), _jsx(Text, { sx: { fontWeight: 'semibold' }, children: resolvedDisplayName })] })) : (_jsx(PrincipalBadge, { principal: {
|
|
329
|
+
kind: ownerPrincipal.kind,
|
|
330
|
+
uid: ownerPrincipal.uid,
|
|
331
|
+
displayName: resolvedDisplayName,
|
|
332
|
+
handle: resolvedHandle,
|
|
333
|
+
accountHandle: resolvedAccountHandle,
|
|
334
|
+
avatarUrl: resolvedAvatarUrl,
|
|
335
|
+
origin: resolvedOrigin,
|
|
336
|
+
}, showPrincipalLabel: false, showApplyingToText: false, showOriginLabel: ownerPrincipal.kind === 'user', isAdmin: isPlatformAdmin, sx: { px: 0, py: 0, border: 'none', bg: 'transparent' } })) }));
|
|
337
|
+
}
|
|
338
|
+
function AccessPrincipalRow({ entry, cache, showAvatarSkeleton = false, isPlatformAdmin, }) {
|
|
339
|
+
const cached = cache[principalKey(entry.kind, entry.uid)] || {};
|
|
340
|
+
const cachedHandle = cached.handle;
|
|
341
|
+
const safeCachedHandle = cachedHandle && cachedHandle !== entry.uid ? cachedHandle : '';
|
|
342
|
+
const resolvedHandle = safeCachedHandle || cached.accountHandle;
|
|
343
|
+
const resolvedDisplayName = ensurePrincipalDisplayName(entry.kind, cached.displayName, resolvedHandle, entry.uid);
|
|
344
|
+
return (_jsx(Box, { sx: {
|
|
345
|
+
display: 'inline-flex',
|
|
346
|
+
alignItems: 'center',
|
|
347
|
+
gap: 1,
|
|
348
|
+
flexWrap: 'wrap',
|
|
349
|
+
}, children: showAvatarSkeleton ? (_jsxs(_Fragment, { children: [_jsx(AvatarShimmer, { size: 20 }), _jsx(Text, { children: resolvedDisplayName })] })) : (_jsx(PrincipalBadge, { principal: {
|
|
350
|
+
kind: entry.kind,
|
|
351
|
+
uid: entry.uid,
|
|
352
|
+
displayName: resolvedDisplayName,
|
|
353
|
+
handle: resolvedHandle,
|
|
354
|
+
accountHandle: cached.accountHandle,
|
|
355
|
+
avatarUrl: cached.avatarUrl,
|
|
356
|
+
origin: cached.origin || 'Datalayer',
|
|
357
|
+
}, showPrincipalLabel: false, showApplyingToText: false, showOriginLabel: entry.kind === 'user', isAdmin: isPlatformAdmin, sx: { px: 0, py: 0, border: 'none', bg: 'transparent' } })) }));
|
|
358
|
+
}
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// Main component.
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
export function ShareAccessComponent({ isOpen, requestUrl, resourceLabel, resourceName, resourceDescription: _resourceDescription, onSharingAccessRestrictedChange, defaultAccessLevel = 'view', principalKinds = DEFAULT_PRINCIPAL_KINDS, displayMode = 'dialog', onClose, }) {
|
|
363
|
+
void _resourceDescription;
|
|
364
|
+
const { token, user } = useIAMStore();
|
|
365
|
+
const { configuration } = useCoreStore();
|
|
366
|
+
const { enqueueToast } = useToast();
|
|
367
|
+
const isPlatformAdmin = Boolean(Array.isArray(user?.roles) && user.roles.includes('platform_admin'));
|
|
368
|
+
// ----- State -----
|
|
369
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
370
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
371
|
+
const [selectedAccessLevel, setSelectedAccessLevel] = useState(defaultAccessLevel);
|
|
372
|
+
const [access, setAccess] = useState(emptyAccessByLevel());
|
|
373
|
+
const [ownerPrincipals, setOwnerPrincipals] = useState([]);
|
|
374
|
+
const [shareablePrincipals, setShareablePrincipals] = useState([]);
|
|
375
|
+
const [isLoadingShareable, setIsLoadingShareable] = useState(false);
|
|
376
|
+
const [principalCache, setPrincipalCache] = useState({});
|
|
377
|
+
const [hydratingUserUids, setHydratingUserUids] = useState({});
|
|
378
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
379
|
+
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
|
|
380
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
381
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
382
|
+
const [isSearchOverlayOpen, setIsSearchOverlayOpen] = useState(false);
|
|
383
|
+
const [sharingAccessMessage, setSharingAccessMessage] = useState(null);
|
|
384
|
+
const [isSharingAccessConfirmed, setIsSharingAccessConfirmed] = useState(false);
|
|
385
|
+
// ----- Refs -----
|
|
386
|
+
const hasLoadedForOpenRef = useRef(false);
|
|
387
|
+
const hasHydratedSharingRef = useRef(false);
|
|
388
|
+
const lastSavedSharingRef = useRef(null);
|
|
389
|
+
const autoSaveTimerRef = useRef(null);
|
|
390
|
+
const activeSearchRequestRef = useRef(0);
|
|
391
|
+
const searchContainerRef = useRef(null);
|
|
392
|
+
const searchInputRef = useRef(null);
|
|
393
|
+
const userHydrationMissesRef = useRef(new Set());
|
|
394
|
+
const enqueueToastRef = useRef(enqueueToast);
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
enqueueToastRef.current = enqueueToast;
|
|
397
|
+
}, [enqueueToast]);
|
|
398
|
+
// ----- Notify caller about restricted access state -----
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
if (!onSharingAccessRestrictedChange) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const restricted = !isSharingAccessConfirmed || Boolean(sharingAccessMessage);
|
|
404
|
+
onSharingAccessRestrictedChange(restricted, sharingAccessMessage || undefined);
|
|
405
|
+
}, [
|
|
406
|
+
isSharingAccessConfirmed,
|
|
407
|
+
sharingAccessMessage,
|
|
408
|
+
onSharingAccessRestrictedChange,
|
|
409
|
+
]);
|
|
410
|
+
// ----- Derived -----
|
|
411
|
+
const canRequest = Boolean(requestUrl && token);
|
|
412
|
+
const canSearchPrincipals = Boolean(configuration?.iamRunUrl && token);
|
|
413
|
+
const iamRunUrl = configuration?.iamRunUrl;
|
|
414
|
+
const principalKindsSet = useMemo(() => new Set(principalKinds), [principalKinds]);
|
|
415
|
+
const principalKindsKey = useMemo(() => [...principalKinds].sort().join('|'), [principalKinds]);
|
|
416
|
+
const aclEntries = useMemo(() => buildAclEntries(access, principalKinds), [access, principalKinds]);
|
|
417
|
+
const normalizedSearch = searchQuery.trim();
|
|
418
|
+
const normalizedDebouncedSearch = debouncedSearchQuery.trim();
|
|
419
|
+
const canShowSearchResults = isSearchOverlayOpen && normalizedSearch.length > 0;
|
|
420
|
+
// ----- Cache mutators (single consolidated record) -----
|
|
421
|
+
const mergePrincipalCacheEntry = useCallback((kind, uid, patch) => {
|
|
422
|
+
if (!uid) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const key = principalKey(kind, uid);
|
|
426
|
+
setPrincipalCache(prev => {
|
|
427
|
+
const existing = prev[key] || {};
|
|
428
|
+
const merged = { ...existing };
|
|
429
|
+
let changed = false;
|
|
430
|
+
Object.keys(patch).forEach(field => {
|
|
431
|
+
const value = patch[field];
|
|
432
|
+
if (typeof value === 'string') {
|
|
433
|
+
const trimmed = value.trim();
|
|
434
|
+
if (trimmed && existing[field] !== trimmed) {
|
|
435
|
+
merged[field] = trimmed;
|
|
436
|
+
changed = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return changed ? { ...prev, [key]: merged } : prev;
|
|
441
|
+
});
|
|
442
|
+
}, []);
|
|
443
|
+
// ----- Reset on close / load on open -----
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
if (isOpen) {
|
|
446
|
+
setSelectedAccessLevel(defaultAccessLevel);
|
|
447
|
+
}
|
|
448
|
+
}, [isOpen, defaultAccessLevel]);
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!isOpen) {
|
|
451
|
+
hasLoadedForOpenRef.current = false;
|
|
452
|
+
hasHydratedSharingRef.current = false;
|
|
453
|
+
lastSavedSharingRef.current = null;
|
|
454
|
+
if (autoSaveTimerRef.current) {
|
|
455
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
456
|
+
autoSaveTimerRef.current = null;
|
|
457
|
+
}
|
|
458
|
+
setSearchQuery('');
|
|
459
|
+
setIsSearchOverlayOpen(false);
|
|
460
|
+
setSearchResults([]);
|
|
461
|
+
setIsSearching(false);
|
|
462
|
+
setSharingAccessMessage(null);
|
|
463
|
+
setIsSharingAccessConfirmed(false);
|
|
464
|
+
setPrincipalCache({});
|
|
465
|
+
setOwnerPrincipals([]);
|
|
466
|
+
setShareablePrincipals([]);
|
|
467
|
+
setHydratingUserUids({});
|
|
468
|
+
userHydrationMissesRef.current = new Set();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!canRequest || !requestUrl) {
|
|
472
|
+
setIsLoading(false);
|
|
473
|
+
setIsSharingAccessConfirmed(false);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (hasLoadedForOpenRef.current) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
hasLoadedForOpenRef.current = true;
|
|
480
|
+
let cancelled = false;
|
|
481
|
+
const run = async () => {
|
|
482
|
+
setIsLoading(true);
|
|
483
|
+
setIsSharingAccessConfirmed(false);
|
|
484
|
+
setSharingAccessMessage(null);
|
|
485
|
+
try {
|
|
486
|
+
const response = await fetch(requestUrl, {
|
|
487
|
+
method: 'GET',
|
|
488
|
+
headers: {
|
|
489
|
+
'Content-Type': 'application/json',
|
|
490
|
+
Authorization: `Bearer ${token}`,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
const payload = await response.json();
|
|
494
|
+
const message = payload?.detail ||
|
|
495
|
+
payload?.message ||
|
|
496
|
+
`Unable to load ${resourceLabel.toLowerCase()} sharing.`;
|
|
497
|
+
if (payload?.success === false) {
|
|
498
|
+
if (!cancelled && isSharingAuthorizationMessage(message)) {
|
|
499
|
+
setSharingAccessMessage(message);
|
|
500
|
+
setIsSharingAccessConfirmed(true);
|
|
501
|
+
setAccess(emptyAccessByLevel());
|
|
502
|
+
setOwnerPrincipals([]);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
throw new Error(message);
|
|
506
|
+
}
|
|
507
|
+
if (!response.ok) {
|
|
508
|
+
if (response.status === 403 ||
|
|
509
|
+
isSharingAuthorizationMessage(message)) {
|
|
510
|
+
if (!cancelled) {
|
|
511
|
+
setSharingAccessMessage(message);
|
|
512
|
+
setIsSharingAccessConfirmed(true);
|
|
513
|
+
setAccess(emptyAccessByLevel());
|
|
514
|
+
setOwnerPrincipals([]);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
throw new Error(message);
|
|
519
|
+
}
|
|
520
|
+
if (cancelled) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const sharing = (payload?.sharing || {});
|
|
524
|
+
const owners = extractOwnerPrincipals(payload);
|
|
525
|
+
const hydrated = hydrateAccessFromSharing(sharing);
|
|
526
|
+
setOwnerPrincipals(owners);
|
|
527
|
+
owners.forEach(owner => {
|
|
528
|
+
mergePrincipalCacheEntry(owner.kind, owner.uid, {
|
|
529
|
+
displayName: owner.displayName || owner.handle || owner.uid,
|
|
530
|
+
handle: owner.handle || owner.uid,
|
|
531
|
+
avatarUrl: owner.avatarUrl,
|
|
532
|
+
accountHandle: owner.accountHandle,
|
|
533
|
+
origin: owner.kind === 'user' ? owner.origin : undefined,
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
setAccess(hydrated);
|
|
537
|
+
lastSavedSharingRef.current = JSON.stringify(hydrated);
|
|
538
|
+
hasHydratedSharingRef.current = true;
|
|
539
|
+
setIsSharingAccessConfirmed(true);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (cancelled) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const message = error instanceof Error
|
|
546
|
+
? error.message
|
|
547
|
+
: `Unable to load ${resourceLabel.toLowerCase()} sharing.`;
|
|
548
|
+
enqueueToastRef.current(message, { variant: 'error' });
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
if (!cancelled) {
|
|
552
|
+
setIsLoading(false);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
void run();
|
|
557
|
+
return () => {
|
|
558
|
+
cancelled = true;
|
|
559
|
+
};
|
|
560
|
+
}, [
|
|
561
|
+
isOpen,
|
|
562
|
+
canRequest,
|
|
563
|
+
requestUrl,
|
|
564
|
+
token,
|
|
565
|
+
resourceLabel,
|
|
566
|
+
resourceName,
|
|
567
|
+
mergePrincipalCacheEntry,
|
|
568
|
+
]);
|
|
569
|
+
// ----- Fetch shareable principals on open -----
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (!isOpen || !canSearchPrincipals || !iamRunUrl || !token) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
let cancelled = false;
|
|
575
|
+
const run = async () => {
|
|
576
|
+
setIsLoadingShareable(true);
|
|
577
|
+
try {
|
|
578
|
+
const response = await fetch(`${iamRunUrl}/api/iam/v1/principals/shareable`, {
|
|
579
|
+
method: 'GET',
|
|
580
|
+
headers: {
|
|
581
|
+
'Content-Type': 'application/json',
|
|
582
|
+
Authorization: `Bearer ${token}`,
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
const payload = await response.json();
|
|
586
|
+
if (!response.ok || payload?.success === false) {
|
|
587
|
+
const message = payload?.detail ||
|
|
588
|
+
payload?.message ||
|
|
589
|
+
'Unable to load shareable principals.';
|
|
590
|
+
throw new Error(message);
|
|
591
|
+
}
|
|
592
|
+
if (cancelled) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const raw = Array.isArray(payload?.principals)
|
|
596
|
+
? payload.principals
|
|
597
|
+
: [];
|
|
598
|
+
const mapped = raw
|
|
599
|
+
.map((entry) => {
|
|
600
|
+
const uid = pickFirstString(entry?.uid);
|
|
601
|
+
const handle = pickFirstString(entry?.handle, entry?.handle_s);
|
|
602
|
+
if (!uid) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
const kind = normalizePrincipalKind(pickFirstString(entry?.kind));
|
|
606
|
+
return {
|
|
607
|
+
kind,
|
|
608
|
+
uid,
|
|
609
|
+
handle: handle || uid,
|
|
610
|
+
name: pickFirstString(entry?.name) || null,
|
|
611
|
+
email: pickFirstString(entry?.email) || null,
|
|
612
|
+
avatarUrl: pickFirstString(entry?.avatar_url, entry?.avatarUrl) || null,
|
|
613
|
+
organizationUid: pickFirstString(entry?.organization_uid, entry?.organizationUid) || null,
|
|
614
|
+
organizationHandle: pickFirstString(entry?.organization_handle, entry?.organizationHandle) || null,
|
|
615
|
+
};
|
|
616
|
+
})
|
|
617
|
+
.filter(Boolean);
|
|
618
|
+
setShareablePrincipals(mapped);
|
|
619
|
+
mapped.forEach(principal => {
|
|
620
|
+
mergePrincipalCacheEntry(principal.kind, principal.uid, {
|
|
621
|
+
displayName: principal.name || principal.handle,
|
|
622
|
+
handle: principal.handle,
|
|
623
|
+
avatarUrl: principal.avatarUrl || undefined,
|
|
624
|
+
accountHandle: principal.organizationHandle || undefined,
|
|
625
|
+
origin: principal.kind === 'user' ? 'Datalayer' : undefined,
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (cancelled) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const message = error instanceof Error
|
|
634
|
+
? error.message
|
|
635
|
+
: 'Unable to load shareable principals.';
|
|
636
|
+
enqueueToastRef.current(message, { variant: 'error' });
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
if (!cancelled) {
|
|
640
|
+
setIsLoadingShareable(false);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
void run();
|
|
645
|
+
return () => {
|
|
646
|
+
cancelled = true;
|
|
647
|
+
};
|
|
648
|
+
}, [isOpen, canSearchPrincipals, iamRunUrl, token, mergePrincipalCacheEntry]);
|
|
649
|
+
// ----- Debounce search query -----
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
const timeout = window.setTimeout(() => {
|
|
652
|
+
setDebouncedSearchQuery(searchQuery);
|
|
653
|
+
}, 350);
|
|
654
|
+
return () => {
|
|
655
|
+
window.clearTimeout(timeout);
|
|
656
|
+
};
|
|
657
|
+
}, [searchQuery]);
|
|
658
|
+
// ----- Run search against /principals/search -----
|
|
659
|
+
useEffect(() => {
|
|
660
|
+
if (!isOpen || !canSearchPrincipals || !iamRunUrl || !token) {
|
|
661
|
+
setSearchResults([]);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const query = normalizedDebouncedSearch;
|
|
665
|
+
if (!query || query.length < 2) {
|
|
666
|
+
setSearchResults([]);
|
|
667
|
+
setIsSearching(false);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
let cancelled = false;
|
|
671
|
+
const requestId = activeSearchRequestRef.current + 1;
|
|
672
|
+
activeSearchRequestRef.current = requestId;
|
|
673
|
+
const run = async () => {
|
|
674
|
+
setIsSearching(true);
|
|
675
|
+
try {
|
|
676
|
+
const controller = new AbortController();
|
|
677
|
+
const timeoutId = window.setTimeout(() => controller.abort(), 8000);
|
|
678
|
+
let response;
|
|
679
|
+
let payload;
|
|
680
|
+
try {
|
|
681
|
+
response = await fetch(`${iamRunUrl}/api/iam/v1/principals/search`, {
|
|
682
|
+
method: 'POST',
|
|
683
|
+
headers: {
|
|
684
|
+
'Content-Type': 'application/json',
|
|
685
|
+
Authorization: `Bearer ${token}`,
|
|
686
|
+
},
|
|
687
|
+
body: JSON.stringify({
|
|
688
|
+
query,
|
|
689
|
+
principalTypes: [...principalKinds],
|
|
690
|
+
}),
|
|
691
|
+
signal: controller.signal,
|
|
692
|
+
});
|
|
693
|
+
payload = await response.json();
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
window.clearTimeout(timeoutId);
|
|
697
|
+
}
|
|
698
|
+
if (requestId !== activeSearchRequestRef.current || cancelled) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (!response.ok || !payload?.success) {
|
|
702
|
+
const message = payload?.detail ||
|
|
703
|
+
payload?.message ||
|
|
704
|
+
'Unable to search principals.';
|
|
705
|
+
throw new Error(message);
|
|
706
|
+
}
|
|
707
|
+
const data = payload?.data && typeof payload.data === 'object'
|
|
708
|
+
? payload.data
|
|
709
|
+
: payload;
|
|
710
|
+
const users = Array.isArray(data?.users) ? data.users : [];
|
|
711
|
+
const teams = Array.isArray(data?.teams) ? data.teams : [];
|
|
712
|
+
const organizations = Array.isArray(data?.organizations)
|
|
713
|
+
? data.organizations
|
|
714
|
+
: [];
|
|
715
|
+
const mappedUsers = users
|
|
716
|
+
.map((entry) => {
|
|
717
|
+
const uid = pickFirstString(entry?.uid);
|
|
718
|
+
const handle = pickFirstString(entry?.handle_s, entry?.handle);
|
|
719
|
+
if (!uid) {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const firstName = pickFirstString(entry?.first_name_t, entry?.firstName);
|
|
723
|
+
const lastName = pickFirstString(entry?.last_name_t, entry?.lastName);
|
|
724
|
+
const displayName = `${firstName} ${lastName}`.trim() ||
|
|
725
|
+
pickFirstString(entry?.display_name_t, entry?.display_name, handle);
|
|
726
|
+
const origin = normalizeUserOrigin(pickFirstString(entry?.origin, entry?.origin_s, entry?.origin_t));
|
|
727
|
+
const avatarUrl = pickFirstString(entry?.avatar_url_s, entry?.avatarUrl, entry?.avatar_url);
|
|
728
|
+
return {
|
|
729
|
+
kind: 'user',
|
|
730
|
+
uid,
|
|
731
|
+
handle: handle || uid,
|
|
732
|
+
displayName: displayName || handle || uid,
|
|
733
|
+
avatarUrl: avatarUrl || undefined,
|
|
734
|
+
origin,
|
|
735
|
+
};
|
|
736
|
+
})
|
|
737
|
+
.filter(Boolean);
|
|
738
|
+
const mappedTeams = teams
|
|
739
|
+
.map((entry) => {
|
|
740
|
+
const uid = pickFirstString(entry?.uid);
|
|
741
|
+
const handle = pickFirstString(entry?.handle_s, entry?.handle);
|
|
742
|
+
if (!uid) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
kind: 'team',
|
|
747
|
+
uid,
|
|
748
|
+
handle: handle || uid,
|
|
749
|
+
displayName: pickFirstString(entry?.name_t, entry?.name) || handle || uid,
|
|
750
|
+
accountHandle: pickFirstString(entry?.organization_handle_s, entry?.organizationHandle, entry?.organization_handle) || undefined,
|
|
751
|
+
};
|
|
752
|
+
})
|
|
753
|
+
.filter(Boolean);
|
|
754
|
+
const mappedOrganizations = organizations
|
|
755
|
+
.map((entry) => {
|
|
756
|
+
const uid = pickFirstString(entry?.uid);
|
|
757
|
+
const handle = pickFirstString(entry?.handle_s, entry?.handle);
|
|
758
|
+
if (!uid) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
kind: 'organization',
|
|
763
|
+
uid,
|
|
764
|
+
handle: handle || uid,
|
|
765
|
+
displayName: pickFirstString(entry?.name_t, entry?.name) || handle || uid,
|
|
766
|
+
};
|
|
767
|
+
})
|
|
768
|
+
.filter(Boolean);
|
|
769
|
+
const filtered = [
|
|
770
|
+
...mappedUsers,
|
|
771
|
+
...mappedTeams,
|
|
772
|
+
...mappedOrganizations,
|
|
773
|
+
].filter(result => principalKindsSet.has(result.kind));
|
|
774
|
+
filtered.forEach(result => {
|
|
775
|
+
mergePrincipalCacheEntry(result.kind, result.uid, {
|
|
776
|
+
displayName: result.displayName || result.handle,
|
|
777
|
+
handle: result.handle,
|
|
778
|
+
avatarUrl: result.avatarUrl,
|
|
779
|
+
accountHandle: result.accountHandle,
|
|
780
|
+
origin: result.kind === 'user' ? result.origin : undefined,
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
setSearchResults(filtered);
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
if (cancelled || requestId !== activeSearchRequestRef.current) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
setSearchResults([]);
|
|
790
|
+
const message = error instanceof Error
|
|
791
|
+
? error.message
|
|
792
|
+
: 'Unable to search principals.';
|
|
793
|
+
enqueueToastRef.current(message, { variant: 'error' });
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
if (requestId === activeSearchRequestRef.current && !cancelled) {
|
|
797
|
+
setIsSearching(false);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
void run();
|
|
802
|
+
return () => {
|
|
803
|
+
cancelled = true;
|
|
804
|
+
};
|
|
805
|
+
}, [
|
|
806
|
+
isOpen,
|
|
807
|
+
canSearchPrincipals,
|
|
808
|
+
iamRunUrl,
|
|
809
|
+
token,
|
|
810
|
+
normalizedDebouncedSearch,
|
|
811
|
+
principalKindsKey,
|
|
812
|
+
principalKinds,
|
|
813
|
+
principalKindsSet,
|
|
814
|
+
mergePrincipalCacheEntry,
|
|
815
|
+
]);
|
|
816
|
+
// ----- Hydrate ACL user uids in bulk -----
|
|
817
|
+
useEffect(() => {
|
|
818
|
+
if (!isOpen || !canSearchPrincipals || !iamRunUrl || !token) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const userUids = Array.from(new Set([
|
|
822
|
+
...aclEntries
|
|
823
|
+
.filter(e => e.kind === 'user')
|
|
824
|
+
.map(e => e.uid)
|
|
825
|
+
.filter(Boolean),
|
|
826
|
+
...ownerPrincipals
|
|
827
|
+
.filter(o => o.kind === 'user')
|
|
828
|
+
.map(o => o.uid)
|
|
829
|
+
.filter(Boolean),
|
|
830
|
+
]));
|
|
831
|
+
const unknown = userUids.filter(uid => {
|
|
832
|
+
if (!uid || userHydrationMissesRef.current.has(uid)) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
const cached = principalCache[principalKey('user', uid)] || {};
|
|
836
|
+
return (!cached.displayName ||
|
|
837
|
+
!cached.avatarUrl ||
|
|
838
|
+
!cached.origin ||
|
|
839
|
+
!cached.handle);
|
|
840
|
+
});
|
|
841
|
+
if (unknown.length === 0) {
|
|
842
|
+
setHydratingUserUids({});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
let cancelled = false;
|
|
846
|
+
const run = async () => {
|
|
847
|
+
setHydratingUserUids(unknown.reduce((acc, uid) => {
|
|
848
|
+
acc[uid] = true;
|
|
849
|
+
return acc;
|
|
850
|
+
}, {}));
|
|
851
|
+
try {
|
|
852
|
+
const response = await fetch(`${iamRunUrl}/api/iam/v1/users/bulk`, {
|
|
853
|
+
method: 'POST',
|
|
854
|
+
headers: {
|
|
855
|
+
'Content-Type': 'application/json',
|
|
856
|
+
Authorization: `Bearer ${token}`,
|
|
857
|
+
},
|
|
858
|
+
body: JSON.stringify({ userIds: unknown }),
|
|
859
|
+
});
|
|
860
|
+
const payload = await response.json();
|
|
861
|
+
if (!response.ok || !payload?.success || cancelled) {
|
|
862
|
+
unknown.forEach(uid => userHydrationMissesRef.current.add(uid));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const data = payload?.data && typeof payload.data === 'object'
|
|
866
|
+
? payload.data
|
|
867
|
+
: payload;
|
|
868
|
+
const users = Array.isArray(data?.users) ? data.users : [];
|
|
869
|
+
const hydratedSet = new Set();
|
|
870
|
+
users.forEach((entry) => {
|
|
871
|
+
const uid = pickFirstString(entry?.uid);
|
|
872
|
+
if (!uid) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
hydratedSet.add(uid);
|
|
876
|
+
const handle = pickFirstString(entry?.handle_s, entry?.handle) || uid;
|
|
877
|
+
const firstName = pickFirstString(entry?.first_name_t, entry?.firstName);
|
|
878
|
+
const lastName = pickFirstString(entry?.last_name_t, entry?.lastName);
|
|
879
|
+
const displayName = `${firstName} ${lastName}`.trim() ||
|
|
880
|
+
pickFirstString(entry?.display_name_t, entry?.display_name) ||
|
|
881
|
+
handle;
|
|
882
|
+
const avatarUrl = pickFirstString(entry?.avatar_url_s, entry?.avatarUrl, entry?.avatar_url);
|
|
883
|
+
const origin = normalizeUserOrigin(pickFirstString(entry?.origin, entry?.origin_s, entry?.origin_t));
|
|
884
|
+
mergePrincipalCacheEntry('user', uid, {
|
|
885
|
+
displayName,
|
|
886
|
+
handle,
|
|
887
|
+
avatarUrl: avatarUrl || undefined,
|
|
888
|
+
origin,
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
unknown.forEach(uid => {
|
|
892
|
+
if (!hydratedSet.has(uid)) {
|
|
893
|
+
userHydrationMissesRef.current.add(uid);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
unknown.forEach(uid => userHydrationMissesRef.current.add(uid));
|
|
899
|
+
}
|
|
900
|
+
finally {
|
|
901
|
+
if (!cancelled) {
|
|
902
|
+
setHydratingUserUids({});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
void run();
|
|
907
|
+
return () => {
|
|
908
|
+
cancelled = true;
|
|
909
|
+
};
|
|
910
|
+
}, [
|
|
911
|
+
isOpen,
|
|
912
|
+
canSearchPrincipals,
|
|
913
|
+
iamRunUrl,
|
|
914
|
+
token,
|
|
915
|
+
aclEntries,
|
|
916
|
+
ownerPrincipals,
|
|
917
|
+
principalCache,
|
|
918
|
+
mergePrincipalCacheEntry,
|
|
919
|
+
]);
|
|
920
|
+
// ----- Hydrate ACL team uids individually -----
|
|
921
|
+
useEffect(() => {
|
|
922
|
+
if (!isOpen || !canSearchPrincipals || !iamRunUrl || !token) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const unknownTeams = aclEntries.filter(entry => {
|
|
926
|
+
if (entry.kind !== 'team') {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
const cached = principalCache[principalKey('team', entry.uid)] || {};
|
|
930
|
+
return !cached.displayName;
|
|
931
|
+
});
|
|
932
|
+
if (unknownTeams.length === 0) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
let cancelled = false;
|
|
936
|
+
void Promise.all(unknownTeams.map(async (entry) => {
|
|
937
|
+
try {
|
|
938
|
+
const response = await fetch(`${iamRunUrl}/api/iam/v1/teams/${encodeURIComponent(entry.uid)}`, {
|
|
939
|
+
method: 'GET',
|
|
940
|
+
headers: {
|
|
941
|
+
'Content-Type': 'application/json',
|
|
942
|
+
Authorization: `Bearer ${token}`,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
const payload = await response.json();
|
|
946
|
+
if (!response.ok || !payload?.success || cancelled) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const data = payload?.data && typeof payload.data === 'object'
|
|
950
|
+
? payload.data
|
|
951
|
+
: payload;
|
|
952
|
+
const obj = data?.team || data;
|
|
953
|
+
const name = pickFirstString(obj?.name_t, obj?.name);
|
|
954
|
+
const handle = pickFirstString(obj?.handle_s, obj?.handle);
|
|
955
|
+
const accountHandle = pickFirstString(obj?.organization_handle_s, obj?.organizationHandle, obj?.organization_handle);
|
|
956
|
+
mergePrincipalCacheEntry('team', entry.uid, {
|
|
957
|
+
displayName: name || handle,
|
|
958
|
+
handle,
|
|
959
|
+
accountHandle,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// Best effort.
|
|
964
|
+
}
|
|
965
|
+
}));
|
|
966
|
+
return () => {
|
|
967
|
+
cancelled = true;
|
|
968
|
+
};
|
|
969
|
+
}, [
|
|
970
|
+
isOpen,
|
|
971
|
+
canSearchPrincipals,
|
|
972
|
+
iamRunUrl,
|
|
973
|
+
token,
|
|
974
|
+
aclEntries,
|
|
975
|
+
principalCache,
|
|
976
|
+
mergePrincipalCacheEntry,
|
|
977
|
+
]);
|
|
978
|
+
// ----- Auto-save on access change after hydration -----
|
|
979
|
+
const saveAccess = useCallback(async (snapshot) => {
|
|
980
|
+
if (!canRequest || !requestUrl) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
setIsSaving(true);
|
|
984
|
+
try {
|
|
985
|
+
const body = {
|
|
986
|
+
access: {
|
|
987
|
+
view: {
|
|
988
|
+
userUids: snapshot.view.userUids,
|
|
989
|
+
teamUids: snapshot.view.teamUids,
|
|
990
|
+
organizationUids: snapshot.view.organizationUids,
|
|
991
|
+
},
|
|
992
|
+
update: {
|
|
993
|
+
userUids: snapshot.update.userUids,
|
|
994
|
+
teamUids: snapshot.update.teamUids,
|
|
995
|
+
organizationUids: snapshot.update.organizationUids,
|
|
996
|
+
},
|
|
997
|
+
execute: {
|
|
998
|
+
userUids: snapshot.execute.userUids,
|
|
999
|
+
teamUids: snapshot.execute.teamUids,
|
|
1000
|
+
organizationUids: snapshot.execute.organizationUids,
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
const response = await fetch(requestUrl, {
|
|
1005
|
+
method: 'PUT',
|
|
1006
|
+
headers: {
|
|
1007
|
+
'Content-Type': 'application/json',
|
|
1008
|
+
Authorization: `Bearer ${token}`,
|
|
1009
|
+
},
|
|
1010
|
+
body: JSON.stringify(body),
|
|
1011
|
+
});
|
|
1012
|
+
const payload = await response.json();
|
|
1013
|
+
const message = payload?.detail ||
|
|
1014
|
+
payload?.message ||
|
|
1015
|
+
`Unable to update ${resourceLabel.toLowerCase()} sharing.`;
|
|
1016
|
+
if (payload?.success === false) {
|
|
1017
|
+
if (isSharingAuthorizationMessage(message)) {
|
|
1018
|
+
setSharingAccessMessage(message);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
throw new Error(message);
|
|
1022
|
+
}
|
|
1023
|
+
if (!response.ok) {
|
|
1024
|
+
if (response.status === 403 ||
|
|
1025
|
+
isSharingAuthorizationMessage(message)) {
|
|
1026
|
+
setSharingAccessMessage(message);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
throw new Error(message);
|
|
1030
|
+
}
|
|
1031
|
+
enqueueToastRef.current(`${resourceLabel} sharing updated.`, {
|
|
1032
|
+
variant: 'success',
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
const message = error instanceof Error
|
|
1037
|
+
? error.message
|
|
1038
|
+
: `Unable to update ${resourceLabel.toLowerCase()} sharing.`;
|
|
1039
|
+
enqueueToastRef.current(message, { variant: 'error' });
|
|
1040
|
+
}
|
|
1041
|
+
finally {
|
|
1042
|
+
setIsSaving(false);
|
|
1043
|
+
}
|
|
1044
|
+
}, [canRequest, requestUrl, token, resourceLabel]);
|
|
1045
|
+
useEffect(() => {
|
|
1046
|
+
if (!hasHydratedSharingRef.current) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (!canRequest || !requestUrl) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (!isSharingAccessConfirmed || sharingAccessMessage) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const serialized = JSON.stringify(access);
|
|
1056
|
+
if (lastSavedSharingRef.current === serialized) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (autoSaveTimerRef.current) {
|
|
1060
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
1061
|
+
}
|
|
1062
|
+
autoSaveTimerRef.current = setTimeout(() => {
|
|
1063
|
+
autoSaveTimerRef.current = null;
|
|
1064
|
+
lastSavedSharingRef.current = serialized;
|
|
1065
|
+
void saveAccess(access);
|
|
1066
|
+
}, 400);
|
|
1067
|
+
return () => {
|
|
1068
|
+
if (autoSaveTimerRef.current) {
|
|
1069
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
1070
|
+
autoSaveTimerRef.current = null;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}, [
|
|
1074
|
+
access,
|
|
1075
|
+
canRequest,
|
|
1076
|
+
requestUrl,
|
|
1077
|
+
isSharingAccessConfirmed,
|
|
1078
|
+
sharingAccessMessage,
|
|
1079
|
+
saveAccess,
|
|
1080
|
+
]);
|
|
1081
|
+
// ----- Action handlers -----
|
|
1082
|
+
const addPrincipal = useCallback((kind, uid) => {
|
|
1083
|
+
if (!principalKindsSet.has(kind)) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
setAccess(prev => withPrincipalAdded(prev, selectedAccessLevel, kind, uid));
|
|
1087
|
+
}, [principalKindsSet, selectedAccessLevel]);
|
|
1088
|
+
const removePrincipal = useCallback((kind, uid) => {
|
|
1089
|
+
setAccess(prev => withPrincipalRemoved(prev, kind, uid));
|
|
1090
|
+
}, []);
|
|
1091
|
+
const handleSearchResultSelect = useCallback((result) => {
|
|
1092
|
+
addPrincipal(result.kind, result.uid);
|
|
1093
|
+
setSearchQuery('');
|
|
1094
|
+
setIsSearchOverlayOpen(false);
|
|
1095
|
+
setSearchResults([]);
|
|
1096
|
+
}, [addPrincipal]);
|
|
1097
|
+
// ----- Search overlay outside-click + escape -----
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
if (!isSearchOverlayOpen) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const handlePointer = (event) => {
|
|
1103
|
+
const target = event.target;
|
|
1104
|
+
if (target && searchContainerRef.current?.contains(target)) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
setIsSearchOverlayOpen(false);
|
|
1108
|
+
};
|
|
1109
|
+
const handleEscape = (event) => {
|
|
1110
|
+
if (event.key !== 'Escape') {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
event.preventDefault();
|
|
1114
|
+
setIsSearchOverlayOpen(false);
|
|
1115
|
+
searchInputRef.current?.focus();
|
|
1116
|
+
};
|
|
1117
|
+
document.addEventListener('mousedown', handlePointer);
|
|
1118
|
+
document.addEventListener('keydown', handleEscape);
|
|
1119
|
+
return () => {
|
|
1120
|
+
document.removeEventListener('mousedown', handlePointer);
|
|
1121
|
+
document.removeEventListener('keydown', handleEscape);
|
|
1122
|
+
};
|
|
1123
|
+
}, [isSearchOverlayOpen]);
|
|
1124
|
+
// ----- Shareable picker groupings -----
|
|
1125
|
+
const groupedShareable = useMemo(() => {
|
|
1126
|
+
const filtered = shareablePrincipals.filter(p => principalKindsSet.has(p.kind));
|
|
1127
|
+
const selfUid = pickFirstString(user?.uid);
|
|
1128
|
+
const self = filtered.filter(p => p.kind === 'user' && p.uid === selfUid);
|
|
1129
|
+
const otherUsers = filtered.filter(p => p.kind === 'user' && p.uid !== selfUid);
|
|
1130
|
+
const orgs = filtered.filter(p => p.kind === 'organization');
|
|
1131
|
+
const teams = filtered.filter(p => p.kind === 'team');
|
|
1132
|
+
return { self, otherUsers, orgs, teams };
|
|
1133
|
+
}, [shareablePrincipals, principalKindsSet, user?.uid]);
|
|
1134
|
+
if (!isOpen) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
const isReadOnly = !canRequest ||
|
|
1138
|
+
!isSharingAccessConfirmed ||
|
|
1139
|
+
isLoading ||
|
|
1140
|
+
Boolean(sharingAccessMessage);
|
|
1141
|
+
// ----- Sub-renderers (kept inline for locality) -----
|
|
1142
|
+
const renderShareablePrincipalRow = (principal) => {
|
|
1143
|
+
const alreadyAdded = hasPrincipal(access, selectedAccessLevel, principal.kind, principal.uid);
|
|
1144
|
+
const cached = principalCache[principalKey(principal.kind, principal.uid)] || {};
|
|
1145
|
+
const displayName = principal.name || cached.displayName || principal.handle || principal.uid;
|
|
1146
|
+
const Icon = principal.kind === 'user'
|
|
1147
|
+
? PersonIcon
|
|
1148
|
+
: principal.kind === 'organization'
|
|
1149
|
+
? OrganizationIcon
|
|
1150
|
+
: PeopleIcon;
|
|
1151
|
+
return (_jsxs(Box, { as: "button", type: "button", onClick: () => {
|
|
1152
|
+
if (!alreadyAdded) {
|
|
1153
|
+
addPrincipal(principal.kind, principal.uid);
|
|
1154
|
+
}
|
|
1155
|
+
}, disabled: alreadyAdded || isSaving || isReadOnly, sx: {
|
|
1156
|
+
all: 'unset',
|
|
1157
|
+
display: 'flex',
|
|
1158
|
+
alignItems: 'center',
|
|
1159
|
+
justifyContent: 'space-between',
|
|
1160
|
+
gap: 2,
|
|
1161
|
+
px: 2,
|
|
1162
|
+
py: 2,
|
|
1163
|
+
cursor: alreadyAdded || isSaving || isReadOnly ? 'not-allowed' : 'pointer',
|
|
1164
|
+
opacity: alreadyAdded ? 0.55 : 1,
|
|
1165
|
+
borderRadius: 2,
|
|
1166
|
+
borderWidth: 1,
|
|
1167
|
+
borderStyle: 'solid',
|
|
1168
|
+
borderColor: 'border.default',
|
|
1169
|
+
bg: 'canvas.default',
|
|
1170
|
+
':hover': {
|
|
1171
|
+
bg: alreadyAdded || isSaving || isReadOnly
|
|
1172
|
+
? 'canvas.default'
|
|
1173
|
+
: 'canvas.subtle',
|
|
1174
|
+
},
|
|
1175
|
+
}, children: [_jsxs(Box, { sx: {
|
|
1176
|
+
display: 'inline-flex',
|
|
1177
|
+
alignItems: 'center',
|
|
1178
|
+
gap: 2,
|
|
1179
|
+
minWidth: 0,
|
|
1180
|
+
}, children: [_jsx(PrincipalAvatar, { kind: principal.kind, avatarUrl: principal.avatarUrl || undefined, alt: displayName, size: 22 }), _jsxs(Box, { sx: { display: 'grid', minWidth: 0 }, children: [_jsxs(Box, { sx: {
|
|
1181
|
+
display: 'inline-flex',
|
|
1182
|
+
alignItems: 'center',
|
|
1183
|
+
gap: 1,
|
|
1184
|
+
flexWrap: 'wrap',
|
|
1185
|
+
}, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: displayName }), principal.kind === 'user' &&
|
|
1186
|
+
user?.uid &&
|
|
1187
|
+
principal.uid === user.uid && (_jsx(Label, { size: "small", variant: "accent", children: "You" }))] }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["@", principal.handle, principal.kind === 'team' && principal.organizationHandle && (_jsxs(Text, { as: "span", sx: { color: 'fg.muted' }, children: [' · ', "org @", principal.organizationHandle] }))] })] })] }), _jsxs(Box, { sx: { display: 'inline-flex', alignItems: 'center', gap: 1 }, children: [_jsx(Icon, { size: 14 }), alreadyAdded ? (_jsx(Label, { size: "small", variant: "success", children: "Added" })) : (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Add" }))] })] }, principalKey(principal.kind, principal.uid)));
|
|
1188
|
+
};
|
|
1189
|
+
const renderShareableGroup = (label, items) => {
|
|
1190
|
+
if (items.length === 0) {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
return (_jsxs(Box, { sx: { display: 'grid', gap: 1 }, children: [_jsx(Text, { sx: {
|
|
1194
|
+
fontSize: 0,
|
|
1195
|
+
color: 'fg.muted',
|
|
1196
|
+
textTransform: 'uppercase',
|
|
1197
|
+
letterSpacing: 0.4,
|
|
1198
|
+
}, children: label }), _jsx(Box, { sx: { display: 'grid', gap: 1 }, children: items.map(renderShareablePrincipalRow) })] }));
|
|
1199
|
+
};
|
|
1200
|
+
const content = (_jsxs(Box, { sx: { p: 3, display: 'flex', flexDirection: 'column', gap: 3 }, children: [sharingAccessMessage && (_jsxs(Box, { sx: {
|
|
1201
|
+
px: 3,
|
|
1202
|
+
py: 3,
|
|
1203
|
+
borderRadius: 2,
|
|
1204
|
+
borderWidth: 1,
|
|
1205
|
+
borderStyle: 'solid',
|
|
1206
|
+
borderColor: 'attention.muted',
|
|
1207
|
+
bg: 'attention.subtle',
|
|
1208
|
+
display: 'grid',
|
|
1209
|
+
gap: 1,
|
|
1210
|
+
}, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "Sharing access is restricted" }), _jsx(Text, { sx: { color: 'fg.muted' }, children: sharingAccessMessage })] })), _jsxs(Box, { sx: {
|
|
1211
|
+
opacity: isReadOnly ? 0.6 : 1,
|
|
1212
|
+
pointerEvents: isReadOnly ? 'none' : 'auto',
|
|
1213
|
+
display: 'flex',
|
|
1214
|
+
flexDirection: 'column',
|
|
1215
|
+
gap: 3,
|
|
1216
|
+
}, "aria-disabled": isReadOnly, children: [_jsxs(Box, { sx: {
|
|
1217
|
+
px: 3,
|
|
1218
|
+
py: 2,
|
|
1219
|
+
borderRadius: 2,
|
|
1220
|
+
borderWidth: 1,
|
|
1221
|
+
borderStyle: 'solid',
|
|
1222
|
+
borderColor: 'border.default',
|
|
1223
|
+
bg: 'canvas.subtle',
|
|
1224
|
+
display: 'flex',
|
|
1225
|
+
alignItems: 'flex-start',
|
|
1226
|
+
justifyContent: 'space-between',
|
|
1227
|
+
gap: 2,
|
|
1228
|
+
}, children: [_jsx(Box, { sx: { display: 'grid', gap: 1 }, children: _jsxs(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: ["Share ", resourceName || `this ${resourceLabel.toLowerCase()}`, ". Pick a principal below \u2014 they will be granted the selected access level."] }) }), _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsxs(Button, { variant: "default", size: "small", leadingVisual: KeyIcon, disabled: isSaving || isReadOnly, children: ["Access: ", ACCESS_LEVEL_LABELS[selectedAccessLevel]] }) }), _jsx(ActionMenu.Overlay, { width: "small", children: _jsx(ActionList, { selectionVariant: "single", children: ACCESS_LEVELS.map(level => (_jsx(ActionList.Item, { selected: selectedAccessLevel === level, onSelect: () => setSelectedAccessLevel(level), children: ACCESS_LEVEL_LABELS[level] }, level))) }) })] })] }), _jsxs(Box, { sx: {
|
|
1229
|
+
px: 3,
|
|
1230
|
+
py: 2,
|
|
1231
|
+
borderRadius: 2,
|
|
1232
|
+
borderWidth: 1,
|
|
1233
|
+
borderStyle: 'solid',
|
|
1234
|
+
borderColor: 'border.default',
|
|
1235
|
+
bg: 'canvas.default',
|
|
1236
|
+
display: 'grid',
|
|
1237
|
+
gap: 1,
|
|
1238
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Owner" }), ownerPrincipals.length > 0 ? (_jsx(Box, { sx: { display: 'grid' }, children: ownerPrincipals.map((ownerPrincipal, index) => (_jsx(Box, { sx: {
|
|
1239
|
+
py: 1,
|
|
1240
|
+
borderTopWidth: index === 0 ? 0 : 1,
|
|
1241
|
+
borderTopStyle: 'solid',
|
|
1242
|
+
borderColor: 'border.subtle',
|
|
1243
|
+
}, children: _jsx(OwnerPrincipalRow, { ownerPrincipal: ownerPrincipal, cache: principalCache, showAvatarSkeleton: ownerPrincipal.kind === 'user' &&
|
|
1244
|
+
Boolean(hydratingUserUids[ownerPrincipal.uid]), isPlatformAdmin: isPlatformAdmin }) }, principalKey(ownerPrincipal.kind, ownerPrincipal.uid)))) })) : (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Owner information is not available." }))] }), _jsxs(Box, { sx: {
|
|
1245
|
+
px: 3,
|
|
1246
|
+
py: 2,
|
|
1247
|
+
borderRadius: 2,
|
|
1248
|
+
borderWidth: 1,
|
|
1249
|
+
borderStyle: 'solid',
|
|
1250
|
+
borderColor: 'border.default',
|
|
1251
|
+
bg: 'canvas.default',
|
|
1252
|
+
display: 'grid',
|
|
1253
|
+
gap: 2,
|
|
1254
|
+
}, children: [_jsxs(Box, { sx: {
|
|
1255
|
+
display: 'flex',
|
|
1256
|
+
alignItems: 'center',
|
|
1257
|
+
justifyContent: 'space-between',
|
|
1258
|
+
gap: 2,
|
|
1259
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, fontWeight: 600 }, children: "Share with\u2026" }), isLoadingShareable && (_jsxs(Box, { sx: { display: 'inline-flex', alignItems: 'center', gap: 1 }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Loading\u2026" })] }))] }), !isLoadingShareable &&
|
|
1260
|
+
groupedShareable.self.length === 0 &&
|
|
1261
|
+
groupedShareable.otherUsers.length === 0 &&
|
|
1262
|
+
groupedShareable.orgs.length === 0 &&
|
|
1263
|
+
groupedShareable.teams.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "No principals available to share with." })) : (_jsxs(Box, { sx: { display: 'grid', gap: 2 }, children: [renderShareableGroup('You', groupedShareable.self), renderShareableGroup('Other users', groupedShareable.otherUsers), renderShareableGroup('Your organizations', groupedShareable.orgs), renderShareableGroup('Your teams', groupedShareable.teams)] }))] }), _jsxs(Box, { sx: {
|
|
1264
|
+
px: 3,
|
|
1265
|
+
py: 2,
|
|
1266
|
+
borderRadius: 2,
|
|
1267
|
+
borderWidth: 1,
|
|
1268
|
+
borderStyle: 'solid',
|
|
1269
|
+
borderColor: 'border.subtle',
|
|
1270
|
+
bg: 'canvas.subtle',
|
|
1271
|
+
display: 'grid',
|
|
1272
|
+
gap: 1,
|
|
1273
|
+
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Or search for any user, team, or organization" }), _jsxs(Box, { sx: { position: 'relative' }, ref: searchContainerRef, children: [_jsx(TextInput, { ref: searchInputRef, block: true, value: searchQuery, onChange: e => {
|
|
1274
|
+
const next = e.target.value;
|
|
1275
|
+
setSearchQuery(next);
|
|
1276
|
+
setIsSearchOverlayOpen(next.trim().length > 0);
|
|
1277
|
+
}, onFocus: () => {
|
|
1278
|
+
if (searchQuery.trim().length > 0) {
|
|
1279
|
+
setIsSearchOverlayOpen(true);
|
|
1280
|
+
}
|
|
1281
|
+
}, onKeyDown: e => {
|
|
1282
|
+
if (e.key === 'Escape') {
|
|
1283
|
+
e.preventDefault();
|
|
1284
|
+
setIsSearchOverlayOpen(false);
|
|
1285
|
+
}
|
|
1286
|
+
}, placeholder: "Search by handle, name, or email", "aria-label": "Search principals", disabled: isSaving }), canShowSearchResults && (_jsx(Box, { sx: {
|
|
1287
|
+
position: 'absolute',
|
|
1288
|
+
top: 'calc(100% + 8px)',
|
|
1289
|
+
left: 0,
|
|
1290
|
+
right: 0,
|
|
1291
|
+
zIndex: 100,
|
|
1292
|
+
borderWidth: 1,
|
|
1293
|
+
borderStyle: 'solid',
|
|
1294
|
+
borderColor: 'border.default',
|
|
1295
|
+
borderRadius: 2,
|
|
1296
|
+
maxHeight: '220px',
|
|
1297
|
+
overflowY: 'auto',
|
|
1298
|
+
bg: 'canvas.overlay',
|
|
1299
|
+
boxShadow: 'shadow.medium',
|
|
1300
|
+
}, children: isSearching ? (_jsxs(Box, { sx: {
|
|
1301
|
+
px: 3,
|
|
1302
|
+
py: 2,
|
|
1303
|
+
display: 'inline-flex',
|
|
1304
|
+
alignItems: 'center',
|
|
1305
|
+
gap: 2,
|
|
1306
|
+
}, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Searching\u2026" })] })) : searchResults.length === 0 ? (_jsx(Box, { sx: { px: 3, py: 2 }, children: _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "No principals found." }) })) : (_jsx(ActionList, { children: searchResults.map(result => (_jsxs(ActionList.Item, { onSelect: () => handleSearchResultSelect(result), children: [_jsx(ActionList.LeadingVisual, { children: _jsx(PrincipalAvatar, { kind: result.kind, avatarUrl: result.avatarUrl, alt: result.displayName, size: 18 }) }), _jsxs(Box, { sx: {
|
|
1307
|
+
display: 'inline-flex',
|
|
1308
|
+
alignItems: 'center',
|
|
1309
|
+
gap: 1,
|
|
1310
|
+
flexWrap: 'wrap',
|
|
1311
|
+
}, children: [_jsx(Text, { children: result.displayName }), result.kind === 'user' && (_jsx(Label, { size: "small", variant: "secondary", children: result.origin ||
|
|
1312
|
+
principalCache[principalKey('user', result.uid)]
|
|
1313
|
+
?.origin ||
|
|
1314
|
+
'Datalayer' }))] }), _jsxs(ActionList.Description, { variant: "block", children: ["@", result.handle] })] }, principalKey(result.kind, result.uid)))) })) }))] })] }), _jsxs(Box, { sx: {
|
|
1315
|
+
px: 3,
|
|
1316
|
+
py: 2,
|
|
1317
|
+
borderRadius: 2,
|
|
1318
|
+
borderWidth: 1,
|
|
1319
|
+
borderStyle: 'solid',
|
|
1320
|
+
borderColor: 'border.default',
|
|
1321
|
+
bg: 'canvas.default',
|
|
1322
|
+
display: 'grid',
|
|
1323
|
+
gap: 1,
|
|
1324
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Access Control List (ACL)" }), _jsx(Box, { sx: {
|
|
1325
|
+
borderWidth: 1,
|
|
1326
|
+
borderStyle: 'solid',
|
|
1327
|
+
borderColor: 'border.default',
|
|
1328
|
+
borderRadius: 2,
|
|
1329
|
+
maxHeight: '220px',
|
|
1330
|
+
overflowY: 'auto',
|
|
1331
|
+
bg: 'canvas.subtle',
|
|
1332
|
+
}, children: aclEntries.length === 0 ? (_jsx(Box, { sx: { px: 3, py: 2 }, children: _jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "No principals shared yet." }) })) : (_jsx(Box, { sx: { display: 'grid' }, children: aclEntries.map(entry => (_jsxs(Box, { sx: {
|
|
1333
|
+
px: 3,
|
|
1334
|
+
py: 2,
|
|
1335
|
+
display: 'flex',
|
|
1336
|
+
alignItems: 'center',
|
|
1337
|
+
justifyContent: 'space-between',
|
|
1338
|
+
gap: 2,
|
|
1339
|
+
borderTopWidth: 1,
|
|
1340
|
+
borderTopStyle: 'solid',
|
|
1341
|
+
borderColor: 'border.subtle',
|
|
1342
|
+
'&:first-of-type': { borderTop: 'none' },
|
|
1343
|
+
}, children: [_jsx(AccessPrincipalRow, { entry: entry, cache: principalCache, showAvatarSkeleton: entry.kind === 'user' &&
|
|
1344
|
+
Boolean(hydratingUserUids[entry.uid]), isPlatformAdmin: isPlatformAdmin }), _jsx(Box, { sx: {
|
|
1345
|
+
display: 'flex',
|
|
1346
|
+
flexWrap: 'wrap',
|
|
1347
|
+
gap: 1,
|
|
1348
|
+
justifyContent: 'flex-end',
|
|
1349
|
+
}, children: entry.levels.map(level => (_jsx(Label, { size: "small", variant: "secondary", children: ACCESS_LEVEL_LABELS[level] }, level))) }), _jsx(Button, { size: "small", variant: "invisible", onClick: () => removePrincipal(entry.kind, entry.uid), disabled: isSaving, children: "Remove" })] }, principalKey(entry.kind, entry.uid)))) })) })] })] }), _jsxs(Box, { sx: {
|
|
1350
|
+
display: 'flex',
|
|
1351
|
+
justifyContent: 'flex-end',
|
|
1352
|
+
alignItems: 'center',
|
|
1353
|
+
gap: 2,
|
|
1354
|
+
}, children: [isSaving && (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Saving\u2026" })] })), displayMode === 'dialog' && (_jsx(Button, { onClick: onClose, disabled: isSaving, children: "Close" }))] }), isLoading && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Loading current sharing settings\u2026" }))] }));
|
|
1355
|
+
if (displayMode === 'inline') {
|
|
1356
|
+
return (_jsx(Box, { sx: {
|
|
1357
|
+
borderWidth: 1,
|
|
1358
|
+
borderStyle: 'solid',
|
|
1359
|
+
borderColor: 'border.default',
|
|
1360
|
+
borderRadius: 2,
|
|
1361
|
+
bg: 'canvas.default',
|
|
1362
|
+
}, children: content }));
|
|
1363
|
+
}
|
|
1364
|
+
return (_jsx(Dialog, { title: `Share ${resourceLabel.toLowerCase()}`, onClose: onClose, width: "large", children: content }));
|
|
1365
|
+
}
|
|
1366
|
+
export default ShareAccessComponent;
|
|
1367
|
+
// Backward-compatible aliases (deprecated: use ShareAccessComponent).
|
|
1368
|
+
export const ShareAccessDialog = ShareAccessComponent;
|