@datalayer/core 1.1.43 → 1.1.46
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/BillingEntitySelect.d.ts +35 -0
- package/lib/components/billing/{BillableAccountSelect.js → BillingEntitySelect.js} +39 -41
- package/lib/components/billing/index.d.ts +1 -1
- package/lib/components/billing/index.js +1 -1
- package/lib/components/checkout/StripeCheckout.js +5 -5
- package/lib/components/display/VisuallyHidden.d.ts +1 -1
- package/lib/components/display/index.d.ts +0 -1
- package/lib/components/display/index.js +0 -1
- package/lib/components/principal/Principal.js +4 -2
- package/lib/components/principal/PrincipalBadge.js +21 -0
- package/lib/components/principal/PrincipalDetailsOverlay.js +72 -37
- package/lib/components/principal/PrincipalSwitcherMenu.d.ts +3 -3
- package/lib/components/principal/PrincipalSwitcherMenu.js +130 -111
- package/lib/config/Configuration.d.ts +18 -0
- package/lib/hooks/index.d.ts +2 -2
- package/lib/hooks/index.js +2 -2
- package/lib/hooks/useBillingEntityStore.d.ts +31 -0
- package/lib/hooks/useBillingEntityStore.js +53 -0
- package/lib/hooks/useCache.d.ts +34 -33
- package/lib/hooks/useCache.js +32 -16
- package/lib/hooks/useSelectedBillingEntity.d.ts +14 -0
- package/lib/hooks/{useSelectedBillableAccount.js → useSelectedBillingEntity.js} +14 -14
- package/lib/hooks/useSelectedPrincipal.d.ts +1 -1
- package/lib/hooks/useSelectedPrincipal.js +1 -1
- package/lib/otel/client/OtelClient.js +2 -3
- package/lib/otel/hooks/index.js +6 -6
- package/lib/otel/views/OtelLive.js +2 -2
- package/lib/state/substates/CoreState.d.ts +11 -0
- package/lib/state/substates/CoreState.js +18 -0
- package/package.json +4 -4
- package/lib/components/billing/BillableAccountSelect.d.ts +0 -35
- package/lib/components/display/LiveRelativeTime.d.ts +0 -1
- package/lib/components/display/LiveRelativeTime.js +0 -10
- package/lib/hooks/useBillableAccountStore.d.ts +0 -31
- package/lib/hooks/useBillableAccountStore.js +0 -53
- package/lib/hooks/useSelectedBillableAccount.d.ts +0 -14
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type BillingEntityType = 'user' | 'organization' | 'team';
|
|
2
|
+
export type BillingEntity = {
|
|
3
|
+
accountUid: string;
|
|
4
|
+
accountType: BillingEntityType;
|
|
5
|
+
accountHandle: string;
|
|
6
|
+
accountName: string;
|
|
7
|
+
planName: string;
|
|
8
|
+
isEligible: boolean;
|
|
9
|
+
isPaidPlan: boolean;
|
|
10
|
+
sourceOrganizationUid?: string;
|
|
11
|
+
sourceOrganizationHandle?: string;
|
|
12
|
+
teamHandle?: string;
|
|
13
|
+
};
|
|
14
|
+
export type BillingEntitySelectProps = {
|
|
15
|
+
value: string;
|
|
16
|
+
onChange: (accountUid: string) => void;
|
|
17
|
+
onSelectedAccountChange?: (account: BillingEntity | undefined) => void;
|
|
18
|
+
onAccountsResolved?: (state: {
|
|
19
|
+
accounts: BillingEntity[];
|
|
20
|
+
eligibleAccounts: BillingEntity[];
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
hasEligibleAccount: boolean;
|
|
23
|
+
}) => void;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
label?: string;
|
|
26
|
+
caption?: string;
|
|
27
|
+
emptyMessage?: string;
|
|
28
|
+
flashMessage?: string;
|
|
29
|
+
width?: string | number;
|
|
30
|
+
preferOrganizationDefault?: boolean;
|
|
31
|
+
};
|
|
32
|
+
export declare function resolveBillingPlanTier(value: unknown): 'free' | 'pro';
|
|
33
|
+
export declare function formatBillingEntityPlanLabel(planName: string): string;
|
|
34
|
+
export declare function BillingEntitySelect({ value, onChange, onSelectedAccountChange, onAccountsResolved, disabled, label, caption, emptyMessage, flashMessage, width, preferOrganizationDefault, }: BillingEntitySelectProps): JSX.Element;
|
|
35
|
+
export default BillingEntitySelect;
|
|
@@ -8,8 +8,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* Distributed under the terms of the Modified BSD License.
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* BillingEntitySelect — self-contained dropdown that lets the user pick a
|
|
12
|
+
* billing entity (personal, organization, or eligible team) for runs that
|
|
13
13
|
* consume wallet credits.
|
|
14
14
|
*
|
|
15
15
|
* Encapsulates eligibility merge logic and account-detail fetching. Callers
|
|
@@ -25,24 +25,24 @@ import { useSelectedPrincipal } from '../../hooks/useSelectedPrincipal.js';
|
|
|
25
25
|
import { useIAMStore } from '../../state/index.js';
|
|
26
26
|
const PLAN_FREE_TERMS = ['free', 'starter'];
|
|
27
27
|
const PLAN_PRO_TERMS = ['pro', 'paid', 'team', 'enterprise', 'business'];
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
function
|
|
28
|
+
const BILLING_ENTITY_COOKIE = 'datalayer-billing-entity-uid';
|
|
29
|
+
const BILLING_ENTITY_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
30
|
+
function readBillingEntityCookie() {
|
|
31
31
|
if (typeof document === 'undefined')
|
|
32
32
|
return null;
|
|
33
|
-
const escaped =
|
|
33
|
+
const escaped = BILLING_ENTITY_COOKIE.replace(/[.$?*|{}()[\]\\/+^]/g, '\\$&');
|
|
34
34
|
const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
|
|
35
35
|
return match ? decodeURIComponent(match[1]) : null;
|
|
36
36
|
}
|
|
37
|
-
function
|
|
37
|
+
function writeBillingEntityCookie(value) {
|
|
38
38
|
if (typeof document === 'undefined')
|
|
39
39
|
return;
|
|
40
40
|
document.cookie =
|
|
41
|
-
`${
|
|
42
|
-
` path=/; max-age=${
|
|
41
|
+
`${BILLING_ENTITY_COOKIE}=${encodeURIComponent(value)};` +
|
|
42
|
+
` path=/; max-age=${BILLING_ENTITY_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
43
43
|
}
|
|
44
44
|
const planContains = (value, terms) => terms.some(term => value.includes(term));
|
|
45
|
-
export function
|
|
45
|
+
export function resolveBillingPlanTier(value) {
|
|
46
46
|
const normalized = String(value ?? '').toLowerCase();
|
|
47
47
|
if (!normalized || normalized === 'unknown')
|
|
48
48
|
return 'free';
|
|
@@ -52,12 +52,10 @@ export function resolveBillablePlanTier(value) {
|
|
|
52
52
|
return 'pro';
|
|
53
53
|
return 'free';
|
|
54
54
|
}
|
|
55
|
-
export function
|
|
56
|
-
return
|
|
57
|
-
? 'Team Plan'
|
|
58
|
-
: 'Free Plan';
|
|
55
|
+
export function formatBillingEntityPlanLabel(planName) {
|
|
56
|
+
return resolveBillingPlanTier(planName) === 'pro' ? 'Team Plan' : 'Free Plan';
|
|
59
57
|
}
|
|
60
|
-
export function
|
|
58
|
+
export function BillingEntitySelect({ value, onChange, onSelectedAccountChange, onAccountsResolved, disabled = false, label = 'Run under', caption = 'Personal, organization, and eligible team accounts can be selected for billing assignment. For team billing, runtime runs are attributed to the parent organization while credits are consumed from the selected team wallet.', emptyMessage = 'No billing entitys available', flashMessage = 'Runs and credits are charged to the selected billing entity. Wallet credits of that account are consumed; LLM token usage is tracked for visibility only. Accounts without an eligible plan or wallet balance are disabled.', width = 'min(100%, 520px)', preferOrganizationDefault = false, }) {
|
|
61
59
|
const { user } = useIAMStore();
|
|
62
60
|
const { useEligibleSubscriptionAccounts, useSubscriptionAccountsDetails, useUserOrganizations, } = useCache();
|
|
63
61
|
const { selectedPrincipalKind, selectedPrincipalUid } = useSelectedPrincipal();
|
|
@@ -178,7 +176,7 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
178
176
|
accountName: String(details?.account_name || account.accountName || account.accountUid),
|
|
179
177
|
planName,
|
|
180
178
|
isEligible,
|
|
181
|
-
isPaidPlan:
|
|
179
|
+
isPaidPlan: resolveBillingPlanTier(planName || 'free') === 'pro',
|
|
182
180
|
sourceOrganizationUid,
|
|
183
181
|
sourceOrganizationHandle,
|
|
184
182
|
teamHandle: accountType === 'team' ? accountHandle : undefined,
|
|
@@ -190,21 +188,21 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
190
188
|
eligibleAccountByUid,
|
|
191
189
|
detailsByUid,
|
|
192
190
|
]);
|
|
193
|
-
const
|
|
194
|
-
const hasEligibleAccount =
|
|
191
|
+
const eligibleBilling = useMemo(() => accounts.filter(a => a.isEligible), [accounts]);
|
|
192
|
+
const hasEligibleAccount = eligibleBilling.length > 0;
|
|
195
193
|
const isLoading = eligibleAccountsLoading || detailsLoading;
|
|
196
|
-
const
|
|
194
|
+
const storedBillingEntityUid = useMemo(() => readBillingEntityCookie(), []);
|
|
197
195
|
const preferredEligible = useMemo(() => {
|
|
198
|
-
const fromCookie =
|
|
199
|
-
?
|
|
196
|
+
const fromCookie = storedBillingEntityUid
|
|
197
|
+
? eligibleBilling.find(account => account.accountUid === storedBillingEntityUid)
|
|
200
198
|
: undefined;
|
|
201
199
|
if (fromCookie)
|
|
202
200
|
return fromCookie;
|
|
203
|
-
const personalEligible =
|
|
201
|
+
const personalEligible = eligibleBilling.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
204
202
|
if (personalEligible)
|
|
205
203
|
return personalEligible;
|
|
206
204
|
const byPrincipal = selectedPrincipalUid
|
|
207
|
-
?
|
|
205
|
+
? eligibleBilling.find(account => {
|
|
208
206
|
if (account.accountUid !== selectedPrincipalUid)
|
|
209
207
|
return false;
|
|
210
208
|
if (selectedPrincipalKind === 'organization')
|
|
@@ -216,20 +214,20 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
216
214
|
: undefined;
|
|
217
215
|
if (byPrincipal)
|
|
218
216
|
return byPrincipal;
|
|
219
|
-
const firstOrg =
|
|
217
|
+
const firstOrg = eligibleBilling.find(a => a.accountType === 'organization');
|
|
220
218
|
if (preferOrganizationDefault && firstOrg)
|
|
221
219
|
return firstOrg;
|
|
222
|
-
return firstOrg ||
|
|
220
|
+
return firstOrg || eligibleBilling[0];
|
|
223
221
|
}, [
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
storedBillingEntityUid,
|
|
223
|
+
eligibleBilling,
|
|
226
224
|
personalAccountUid,
|
|
227
225
|
preferOrganizationDefault,
|
|
228
226
|
selectedPrincipalKind,
|
|
229
227
|
selectedPrincipalUid,
|
|
230
228
|
]);
|
|
231
229
|
const handleAccountSelect = useCallback((accountUid) => {
|
|
232
|
-
|
|
230
|
+
writeBillingEntityCookie(accountUid);
|
|
233
231
|
onChange(accountUid);
|
|
234
232
|
}, [onChange]);
|
|
235
233
|
// Apply persisted selection (cookie) once accounts are resolved. Runs once
|
|
@@ -241,25 +239,25 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
241
239
|
return;
|
|
242
240
|
if (initialSelectionAppliedRef.current)
|
|
243
241
|
return;
|
|
244
|
-
if (
|
|
242
|
+
if (eligibleBilling.length === 0 && accounts.length === 0)
|
|
245
243
|
return;
|
|
246
244
|
initialSelectionAppliedRef.current = true;
|
|
247
|
-
const storedAccount =
|
|
248
|
-
?
|
|
245
|
+
const storedAccount = storedBillingEntityUid
|
|
246
|
+
? eligibleBilling.find(a => a.accountUid === storedBillingEntityUid)
|
|
249
247
|
: undefined;
|
|
250
|
-
const personalAccount =
|
|
248
|
+
const personalAccount = eligibleBilling.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
251
249
|
const target = storedAccount || personalAccount || preferredEligible;
|
|
252
250
|
if (!target)
|
|
253
251
|
return;
|
|
254
252
|
if (target.accountUid !== value) {
|
|
255
|
-
|
|
253
|
+
writeBillingEntityCookie(target.accountUid);
|
|
256
254
|
onChange(target.accountUid);
|
|
257
255
|
}
|
|
258
256
|
}, [
|
|
259
257
|
isLoading,
|
|
260
258
|
accounts,
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
eligibleBilling,
|
|
260
|
+
storedBillingEntityUid,
|
|
263
261
|
personalAccountUid,
|
|
264
262
|
preferredEligible,
|
|
265
263
|
value,
|
|
@@ -278,7 +276,7 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
278
276
|
}
|
|
279
277
|
const current = accounts.find(a => a.accountUid === value);
|
|
280
278
|
if (!current || !current.isEligible) {
|
|
281
|
-
|
|
279
|
+
writeBillingEntityCookie(preferredEligible.accountUid);
|
|
282
280
|
onChange(preferredEligible.accountUid);
|
|
283
281
|
}
|
|
284
282
|
}, [isLoading, preferredEligible, accounts, value, onChange]);
|
|
@@ -289,13 +287,13 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
289
287
|
useEffect(() => {
|
|
290
288
|
onAccountsResolved?.({
|
|
291
289
|
accounts,
|
|
292
|
-
eligibleAccounts:
|
|
290
|
+
eligibleAccounts: eligibleBilling,
|
|
293
291
|
isLoading,
|
|
294
292
|
hasEligibleAccount,
|
|
295
293
|
});
|
|
296
294
|
}, [
|
|
297
295
|
accounts,
|
|
298
|
-
|
|
296
|
+
eligibleBilling,
|
|
299
297
|
isLoading,
|
|
300
298
|
hasEligibleAccount,
|
|
301
299
|
onAccountsResolved,
|
|
@@ -326,7 +324,7 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
326
324
|
? 'Organization'
|
|
327
325
|
: selectedAccount.accountType === 'team'
|
|
328
326
|
? 'Team'
|
|
329
|
-
: 'User', children: selectedAccount.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : selectedAccount.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), _jsx(Label, { size: "small", variant: selectedAccount.isPaidPlan ? 'success' : 'attention', children:
|
|
327
|
+
: 'User', children: selectedAccount.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : selectedAccount.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), _jsx(Label, { size: "small", variant: selectedAccount.isPaidPlan ? 'success' : 'attention', children: formatBillingEntityPlanLabel(selectedAccount.planName) })] })] })) : !hasEligibleAccount ? (emptyMessage) : ('Select a billing entity') }) }), _jsxs(ActionMenu.Overlay, { width: "large", children: [_jsx(Box, { sx: { p: 2 }, children: _jsx(Flash, { variant: "default", children: flashMessage }) }), _jsx(ActionList, { selectionVariant: "single", children: isLoading ? (_jsx(ActionList.Item, { disabled: true, children: _jsxs(Box, { sx: {
|
|
330
328
|
display: 'inline-flex',
|
|
331
329
|
alignItems: 'center',
|
|
332
330
|
gap: 2,
|
|
@@ -369,7 +367,7 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
369
367
|
: account.accountType === 'team'
|
|
370
368
|
? 'Team'
|
|
371
369
|
: 'User', children: account.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : account.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), !account.isEligible &&
|
|
372
|
-
account.accountType === 'team' && (_jsx(Label, { size: "small", variant: "attention", children: "No credits" })), _jsx(Label, { size: "small", variant: account.isPaidPlan ? 'success' : 'attention', children:
|
|
370
|
+
account.accountType === 'team' && (_jsx(Label, { size: "small", variant: "attention", children: "No credits" })), _jsx(Label, { size: "small", variant: account.isPaidPlan ? 'success' : 'attention', children: formatBillingEntityPlanLabel(account.planName) })] })] }), _jsx(ActionList.Description, { variant: "block", children: account.isEligible
|
|
373
371
|
? 'Eligible'
|
|
374
372
|
: account.accountType === 'team'
|
|
375
373
|
? 'Not eligible — no team credits allocated'
|
|
@@ -377,4 +375,4 @@ export function BillableAccountSelect({ value, onChange, onSelectedAccountChange
|
|
|
377
375
|
});
|
|
378
376
|
})()) })] })] }) }), _jsx(FormControl.Caption, { children: caption })] }));
|
|
379
377
|
}
|
|
380
|
-
export default
|
|
378
|
+
export default BillingEntitySelect;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './BillingEntitySelect';
|
|
@@ -492,7 +492,7 @@ export function StripeCheckout({ checkoutPortal, appearance, accountUid, showSta
|
|
|
492
492
|
? Math.max(remainingCredits, pendingTopUpTarget.targetWalletBalance)
|
|
493
493
|
: remainingCredits;
|
|
494
494
|
const isRunsOverQuota = runsTotal > 0 && usedRuns > runsTotal;
|
|
495
|
-
const
|
|
495
|
+
const hasBillingPlan = useMemo(() => {
|
|
496
496
|
const normalizedPlan = String(currentSubscriptionPlan).toLowerCase();
|
|
497
497
|
const freeLike = normalizedPlan.includes('free') ||
|
|
498
498
|
normalizedPlan.includes('trial') ||
|
|
@@ -502,7 +502,7 @@ export function StripeCheckout({ checkoutPortal, appearance, accountUid, showSta
|
|
|
502
502
|
}, [currentSubscriptionPlan]);
|
|
503
503
|
const isPaidSubscription = useMemo(() => {
|
|
504
504
|
const normalizedStatus = String(subscriptionStatus).toLowerCase();
|
|
505
|
-
if (!
|
|
505
|
+
if (!hasBillingPlan) {
|
|
506
506
|
return false;
|
|
507
507
|
}
|
|
508
508
|
if (normalizedStatus.includes('incomplete') ||
|
|
@@ -513,9 +513,9 @@ export function StripeCheckout({ checkoutPortal, appearance, accountUid, showSta
|
|
|
513
513
|
return false;
|
|
514
514
|
}
|
|
515
515
|
return ['active', 'trialing', 'past_due', 'paid'].includes(normalizedStatus);
|
|
516
|
-
}, [
|
|
516
|
+
}, [hasBillingPlan, subscriptionStatus]);
|
|
517
517
|
const canCancelSubscription = useMemo(() => {
|
|
518
|
-
if (!
|
|
518
|
+
if (!hasBillingPlan) {
|
|
519
519
|
return false;
|
|
520
520
|
}
|
|
521
521
|
const status = String(subscriptionStatus).toLowerCase();
|
|
@@ -526,7 +526,7 @@ export function StripeCheckout({ checkoutPortal, appearance, accountUid, showSta
|
|
|
526
526
|
status.includes('free') ||
|
|
527
527
|
status === 'unknown';
|
|
528
528
|
return !nonCancelable;
|
|
529
|
-
}, [
|
|
529
|
+
}, [hasBillingPlan, subscriptionStatus, isCancellationScheduled]);
|
|
530
530
|
const isCancelActionPending = cancelSubscriptionMutation.isPending || isConfirmingCancel;
|
|
531
531
|
const isResumeActionPending = resumeSubscriptionMutation.isPending || isResumingTransition;
|
|
532
532
|
const showResumeAction = isCancellationScheduled && !isCancelActionPending;
|
|
@@ -2,4 +2,4 @@ import { SxProp } from '@primer/react';
|
|
|
2
2
|
export interface IVisuallyHiddenProps {
|
|
3
3
|
isVisible?: boolean;
|
|
4
4
|
}
|
|
5
|
-
export declare const VisuallyHidden: import("styled-components").
|
|
5
|
+
export declare const VisuallyHidden: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").FastOmit<import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "sx" | "isVisible"> & IVisuallyHiddenProps & SxProp, never> & Partial<Pick<import("styled-components").FastOmit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "sx" | "isVisible"> & IVisuallyHiddenProps & SxProp, never>>> & string;
|
|
@@ -5,7 +5,6 @@ export * from './DatalayerBox';
|
|
|
5
5
|
export * from './DisplayHandle';
|
|
6
6
|
export * from './HorizontalCenter';
|
|
7
7
|
export * from './JupyterDialog';
|
|
8
|
-
export * from './LiveRelativeTime';
|
|
9
8
|
export * from './Markdown';
|
|
10
9
|
export * from './NavLink';
|
|
11
10
|
export * from './NotebookSkeleton';
|
|
@@ -9,7 +9,6 @@ export * from './DatalayerBox.js';
|
|
|
9
9
|
export * from './DisplayHandle.js';
|
|
10
10
|
export * from './HorizontalCenter.js';
|
|
11
11
|
export * from './JupyterDialog.js';
|
|
12
|
-
export * from './LiveRelativeTime.js';
|
|
13
12
|
export * from './Markdown.js';
|
|
14
13
|
export * from './NavLink.js';
|
|
15
14
|
export * from './NotebookSkeleton.js';
|
|
@@ -33,7 +33,7 @@ const GENERIC_PLACEHOLDER_DISPLAY_NAMES = new Set([
|
|
|
33
33
|
'personal',
|
|
34
34
|
'team',
|
|
35
35
|
'organization',
|
|
36
|
-
'
|
|
36
|
+
'billing entity',
|
|
37
37
|
]);
|
|
38
38
|
/** Truncate a placeholder string to `maxLength`, appending an ellipsis. */
|
|
39
39
|
function truncatePlaceholder(value, maxLength) {
|
|
@@ -165,7 +165,9 @@ export const Principal = ({ principal, isAdmin = false, avatarSize = 20, gap = 2
|
|
|
165
165
|
handle: (!providedHandleIsUidPlaceholder ? principal.handle : undefined) ||
|
|
166
166
|
String(hydratedEntity?.handle || '').trim() ||
|
|
167
167
|
undefined,
|
|
168
|
-
accountHandle: (!providedAccountHandleIsUidPlaceholder
|
|
168
|
+
accountHandle: (!providedAccountHandleIsUidPlaceholder
|
|
169
|
+
? principal.accountHandle
|
|
170
|
+
: undefined) ||
|
|
169
171
|
String(hydratedEntity?.handle || '').trim() ||
|
|
170
172
|
undefined,
|
|
171
173
|
name: principal.name ||
|
|
@@ -16,6 +16,27 @@ function normalizeUserOrigin(originRaw) {
|
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
18
|
const lower = value.toLowerCase();
|
|
19
|
+
const extPrefix = 'urn:dla:iam:ext::';
|
|
20
|
+
if (lower.startsWith(extPrefix)) {
|
|
21
|
+
const suffix = value.slice(extPrefix.length);
|
|
22
|
+
const provider = (suffix.split(':')[0] || '').trim().toLowerCase();
|
|
23
|
+
if (provider === 'github') {
|
|
24
|
+
return 'GitHub';
|
|
25
|
+
}
|
|
26
|
+
if (provider === 'google') {
|
|
27
|
+
return 'Google';
|
|
28
|
+
}
|
|
29
|
+
if (provider === 'linkedin') {
|
|
30
|
+
return 'LinkedIn';
|
|
31
|
+
}
|
|
32
|
+
if (provider === 'microsoft') {
|
|
33
|
+
return 'Microsoft';
|
|
34
|
+
}
|
|
35
|
+
if (provider) {
|
|
36
|
+
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
37
|
+
}
|
|
38
|
+
return 'External';
|
|
39
|
+
}
|
|
19
40
|
if (lower === 'github') {
|
|
20
41
|
return 'GitHub';
|
|
21
42
|
}
|
|
@@ -3,12 +3,47 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the Modified BSD License.
|
|
5
5
|
*/
|
|
6
|
-
import { ActionMenu, Box, Button, Label, Text } from '@primer/react';
|
|
6
|
+
import { ActionMenu, Box, Button, Label, Text, ThemeProvider, } from '@primer/react';
|
|
7
7
|
import { useNavigate } from '../../hooks/index.js';
|
|
8
8
|
import { PrincipalAvatar } from './PrincipalAvatar.js';
|
|
9
|
+
import { displayHandleText } from '../display/DisplayHandle.js';
|
|
9
10
|
function normalize(value) {
|
|
10
11
|
return (value || '').trim();
|
|
11
12
|
}
|
|
13
|
+
function normalizeOriginLabel(origin) {
|
|
14
|
+
const normalized = normalize(origin);
|
|
15
|
+
if (!normalized) {
|
|
16
|
+
return 'Datalayer';
|
|
17
|
+
}
|
|
18
|
+
const lower = normalized.toLowerCase();
|
|
19
|
+
if (lower === 'google')
|
|
20
|
+
return 'Google';
|
|
21
|
+
if (lower === 'github')
|
|
22
|
+
return 'GitHub';
|
|
23
|
+
if (lower === 'linkedin')
|
|
24
|
+
return 'LinkedIn';
|
|
25
|
+
if (lower === 'microsoft')
|
|
26
|
+
return 'Microsoft';
|
|
27
|
+
if (lower === 'datalayer')
|
|
28
|
+
return 'Datalayer';
|
|
29
|
+
const extPrefix = 'urn:dla:iam:ext::';
|
|
30
|
+
if (lower.startsWith(extPrefix)) {
|
|
31
|
+
const suffix = normalized.slice(extPrefix.length);
|
|
32
|
+
const provider = (suffix.split(':')[0] || '').trim().toLowerCase();
|
|
33
|
+
if (provider === 'google')
|
|
34
|
+
return 'Google';
|
|
35
|
+
if (provider === 'github')
|
|
36
|
+
return 'GitHub';
|
|
37
|
+
if (provider === 'linkedin')
|
|
38
|
+
return 'LinkedIn';
|
|
39
|
+
if (provider === 'microsoft')
|
|
40
|
+
return 'Microsoft';
|
|
41
|
+
if (provider) {
|
|
42
|
+
return provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
12
47
|
export function buildPrincipalProfilePath({ kind, uid, handle, accountHandle, isAdmin, }) {
|
|
13
48
|
const normalizedUid = normalize(uid);
|
|
14
49
|
const normalizedHandle = normalize(handle);
|
|
@@ -55,7 +90,7 @@ export function PrincipalDetailsOverlay({ kind, uid, displayName, name, descript
|
|
|
55
90
|
const normalizedUid = normalize(uid);
|
|
56
91
|
const normalizedName = normalize(name);
|
|
57
92
|
const normalizedDescription = normalize(description);
|
|
58
|
-
const normalizedOrigin =
|
|
93
|
+
const normalizedOrigin = normalizeOriginLabel(origin);
|
|
59
94
|
const resolvedHandle = normalizedHandle && normalizedHandle !== normalizedUid
|
|
60
95
|
? normalizedHandle
|
|
61
96
|
: '';
|
|
@@ -66,41 +101,41 @@ export function PrincipalDetailsOverlay({ kind, uid, displayName, name, descript
|
|
|
66
101
|
accountHandle,
|
|
67
102
|
isAdmin,
|
|
68
103
|
});
|
|
69
|
-
return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Box, { as: "button", type: "button", sx: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
textDecoration: 'underline',
|
|
73
|
-
background: 'transparent',
|
|
74
|
-
border: 0,
|
|
75
|
-
padding: 0,
|
|
76
|
-
margin: 0,
|
|
77
|
-
cursor: 'pointer',
|
|
78
|
-
':hover': {
|
|
104
|
+
return (_jsx(ThemeProvider, { children: _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Box, { as: "button", type: "button", sx: {
|
|
105
|
+
fontWeight: 'semibold',
|
|
106
|
+
color: 'accent.fg',
|
|
79
107
|
textDecoration: 'underline',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
background: 'transparent',
|
|
109
|
+
border: 0,
|
|
110
|
+
padding: 0,
|
|
111
|
+
margin: 0,
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
':hover': {
|
|
114
|
+
textDecoration: 'underline',
|
|
115
|
+
},
|
|
116
|
+
}, children: normalizedDisplayName }) }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsxs(Box, { sx: { display: 'grid', gap: 3, p: 4, minWidth: 420 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(PrincipalAvatar, { kind: kind, avatarUrl: avatarUrl, alt: normalizedDisplayName, size: 40 }), _jsxs(Box, { sx: { display: 'grid', gap: 0.5 }, children: [_jsx(Text, { sx: { fontWeight: 'semibold' }, children: normalizedDisplayName }), resolvedHandle && kind !== 'team' ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: displayHandleText(resolvedHandle) })) : null] })] }), _jsxs(Box, { sx: {
|
|
117
|
+
display: 'grid',
|
|
118
|
+
gridTemplateColumns: '110px 1fr',
|
|
119
|
+
gap: 1,
|
|
120
|
+
alignItems: 'baseline',
|
|
121
|
+
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Kind" }), _jsx(Text, { sx: { fontSize: 1 }, children: kind }), resolvedHandle && kind !== 'team' ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Handle" }), _jsx(Text, { sx: { fontSize: 1 }, children: displayHandleText(resolvedHandle) })] })) : null, normalizedUid ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "UID" }), _jsx(Text, { sx: { fontSize: 1 }, children: normalizedUid })] })) : null, kind === 'personal' ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "First name" }), _jsx(Text, { sx: { fontSize: 1 }, children: firstName || 'N/A' }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Last name" }), _jsx(Text, { sx: { fontSize: 1 }, children: lastName || 'N/A' }), email ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Email" }), _jsx(Text, { sx: { fontSize: 1 }, children: email })] })) : null, _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Origin" }), _jsx(Label, { size: "small", variant: "secondary", title: normalizedOrigin, sx: {
|
|
122
|
+
justifySelf: 'start',
|
|
123
|
+
width: 'fit-content',
|
|
124
|
+
maxWidth: '100%',
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
textOverflow: 'ellipsis',
|
|
127
|
+
whiteSpace: 'nowrap',
|
|
128
|
+
}, children: normalizedOrigin })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Name" }), _jsx(Text, { sx: { fontSize: 1 }, children: normalizedName || normalizedDisplayName }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Description" }), _jsx(Text, { sx: { fontSize: 1 }, children: normalizedDescription || 'N/A' }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Origin" }), _jsx(Label, { size: "small", variant: "secondary", title: normalizedOrigin, sx: {
|
|
129
|
+
justifySelf: 'start',
|
|
130
|
+
width: 'fit-content',
|
|
131
|
+
maxWidth: '100%',
|
|
132
|
+
overflow: 'hidden',
|
|
133
|
+
textOverflow: 'ellipsis',
|
|
134
|
+
whiteSpace: 'nowrap',
|
|
135
|
+
}, children: normalizedOrigin })] }))] }), _jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end' }, children: _jsx(Button, { size: "small", onClick: () => {
|
|
136
|
+
if (targetPath) {
|
|
137
|
+
navigate(targetPath);
|
|
138
|
+
}
|
|
139
|
+
}, disabled: !targetPath, children: "View Profile" }) })] }) })] }) }));
|
|
105
140
|
}
|
|
106
141
|
export default PrincipalDetailsOverlay;
|
|
@@ -5,10 +5,10 @@ export type PrincipalSwitcherMenuProps = {
|
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* PrincipalSwitcherMenu — the *only* component allowed to write to the
|
|
8
|
-
* principal store and the
|
|
8
|
+
* principal store and the billing entity store. It keeps both stores in
|
|
9
9
|
* sync per the rule:
|
|
10
|
-
* - selecting a user/org principal →
|
|
11
|
-
* - selecting a team principal →
|
|
10
|
+
* - selecting a user/org principal → billing entity = same user/org
|
|
11
|
+
* - selecting a team principal → billing entity = the team's parent org
|
|
12
12
|
*/
|
|
13
13
|
export declare function PrincipalSwitcherMenu({ maxLabelChars, fullWidth, showClosedBorder, }: PrincipalSwitcherMenuProps): JSX.Element;
|
|
14
14
|
export default PrincipalSwitcherMenu;
|