@datalayer/core 1.1.43 → 1.1.45

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.
Files changed (30) hide show
  1. package/lib/components/billing/BillingEntitySelect.d.ts +35 -0
  2. package/lib/components/billing/{BillableAccountSelect.js → BillingEntitySelect.js} +39 -41
  3. package/lib/components/billing/index.d.ts +1 -1
  4. package/lib/components/billing/index.js +1 -1
  5. package/lib/components/checkout/StripeCheckout.js +5 -5
  6. package/lib/components/display/VisuallyHidden.d.ts +1 -1
  7. package/lib/components/display/index.d.ts +0 -1
  8. package/lib/components/display/index.js +0 -1
  9. package/lib/components/principal/Principal.js +4 -2
  10. package/lib/components/principal/PrincipalBadge.js +21 -0
  11. package/lib/components/principal/PrincipalDetailsOverlay.js +72 -37
  12. package/lib/components/principal/PrincipalSwitcherMenu.d.ts +3 -3
  13. package/lib/components/principal/PrincipalSwitcherMenu.js +130 -111
  14. package/lib/hooks/index.d.ts +2 -2
  15. package/lib/hooks/index.js +2 -2
  16. package/lib/hooks/useBillingEntityStore.d.ts +31 -0
  17. package/lib/hooks/useBillingEntityStore.js +53 -0
  18. package/lib/hooks/useCache.d.ts +34 -33
  19. package/lib/hooks/useCache.js +32 -16
  20. package/lib/hooks/useSelectedBillingEntity.d.ts +14 -0
  21. package/lib/hooks/{useSelectedBillableAccount.js → useSelectedBillingEntity.js} +14 -14
  22. package/lib/hooks/useSelectedPrincipal.d.ts +1 -1
  23. package/lib/hooks/useSelectedPrincipal.js +1 -1
  24. package/package.json +4 -4
  25. package/lib/components/billing/BillableAccountSelect.d.ts +0 -35
  26. package/lib/components/display/LiveRelativeTime.d.ts +0 -1
  27. package/lib/components/display/LiveRelativeTime.js +0 -10
  28. package/lib/hooks/useBillableAccountStore.d.ts +0 -31
  29. package/lib/hooks/useBillableAccountStore.js +0 -53
  30. 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
- * BillableAccountSelect — self-contained dropdown that lets the user pick a
12
- * billable account (personal, organization, or eligible team) for runs that
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 BILLABLE_ACCOUNT_COOKIE = 'datalayer-billable-account-uid';
29
- const BILLABLE_ACCOUNT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
30
- function readBillableAccountCookie() {
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 = BILLABLE_ACCOUNT_COOKIE.replace(/[.$?*|{}()[\]\\/+^]/g, '\\$&');
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 writeBillableAccountCookie(value) {
37
+ function writeBillingEntityCookie(value) {
38
38
  if (typeof document === 'undefined')
39
39
  return;
40
40
  document.cookie =
41
- `${BILLABLE_ACCOUNT_COOKIE}=${encodeURIComponent(value)};` +
42
- ` path=/; max-age=${BILLABLE_ACCOUNT_COOKIE_MAX_AGE}; SameSite=Lax`;
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 resolveBillablePlanTier(value) {
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 formatBillableAccountPlanLabel(planName) {
56
- return resolveBillablePlanTier(planName) === 'pro'
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 BillableAccountSelect({ value, onChange, onSelectedAccountChange, onAccountsResolved, disabled = false, label = 'Run under', caption = 'Personal, organization, and eligible team accounts can be selected for billable assignment. For team billing, runtime runs are attributed to the parent organization while credits are consumed from the selected team wallet.', emptyMessage = 'No billable accounts available', flashMessage = 'Runs and credits are charged to the selected billable account. 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, }) {
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: resolveBillablePlanTier(planName || 'free') === 'pro',
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 eligibleBillable = useMemo(() => accounts.filter(a => a.isEligible), [accounts]);
194
- const hasEligibleAccount = eligibleBillable.length > 0;
191
+ const eligibleBilling = useMemo(() => accounts.filter(a => a.isEligible), [accounts]);
192
+ const hasEligibleAccount = eligibleBilling.length > 0;
195
193
  const isLoading = eligibleAccountsLoading || detailsLoading;
196
- const storedBillableAccountUid = useMemo(() => readBillableAccountCookie(), []);
194
+ const storedBillingEntityUid = useMemo(() => readBillingEntityCookie(), []);
197
195
  const preferredEligible = useMemo(() => {
198
- const fromCookie = storedBillableAccountUid
199
- ? eligibleBillable.find(account => account.accountUid === storedBillableAccountUid)
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 = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
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
- ? eligibleBillable.find(account => {
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 = eligibleBillable.find(a => a.accountType === 'organization');
217
+ const firstOrg = eligibleBilling.find(a => a.accountType === 'organization');
220
218
  if (preferOrganizationDefault && firstOrg)
221
219
  return firstOrg;
222
- return firstOrg || eligibleBillable[0];
220
+ return firstOrg || eligibleBilling[0];
223
221
  }, [
224
- storedBillableAccountUid,
225
- eligibleBillable,
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
- writeBillableAccountCookie(accountUid);
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 (eligibleBillable.length === 0 && accounts.length === 0)
242
+ if (eligibleBilling.length === 0 && accounts.length === 0)
245
243
  return;
246
244
  initialSelectionAppliedRef.current = true;
247
- const storedAccount = storedBillableAccountUid
248
- ? eligibleBillable.find(a => a.accountUid === storedBillableAccountUid)
245
+ const storedAccount = storedBillingEntityUid
246
+ ? eligibleBilling.find(a => a.accountUid === storedBillingEntityUid)
249
247
  : undefined;
250
- const personalAccount = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
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
- writeBillableAccountCookie(target.accountUid);
253
+ writeBillingEntityCookie(target.accountUid);
256
254
  onChange(target.accountUid);
257
255
  }
258
256
  }, [
259
257
  isLoading,
260
258
  accounts,
261
- eligibleBillable,
262
- storedBillableAccountUid,
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
- writeBillableAccountCookie(preferredEligible.accountUid);
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: eligibleBillable,
290
+ eligibleAccounts: eligibleBilling,
293
291
  isLoading,
294
292
  hasEligibleAccount,
295
293
  });
296
294
  }, [
297
295
  accounts,
298
- eligibleBillable,
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: formatBillableAccountPlanLabel(selectedAccount.planName) })] })] })) : !hasEligibleAccount ? (emptyMessage) : ('Select a billable account') }) }), _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: {
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: formatBillableAccountPlanLabel(account.planName) })] })] }), _jsx(ActionList.Description, { variant: "block", children: account.isEligible
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 BillableAccountSelect;
378
+ export default BillingEntitySelect;
@@ -1 +1 @@
1
- export * from './BillableAccountSelect';
1
+ export * from './BillingEntitySelect';
@@ -6,4 +6,4 @@
6
6
  * Copyright (c) 2023-2026 Datalayer, Inc.
7
7
  * Distributed under the terms of the Modified BSD License.
8
8
  */
9
- export * from './BillableAccountSelect.js';
9
+ export * from './BillingEntitySelect.js';
@@ -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 hasBillablePlan = useMemo(() => {
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 (!hasBillablePlan) {
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
- }, [hasBillablePlan, subscriptionStatus]);
516
+ }, [hasBillingPlan, subscriptionStatus]);
517
517
  const canCancelSubscription = useMemo(() => {
518
- if (!hasBillablePlan) {
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
- }, [hasBillablePlan, subscriptionStatus, isCancellationScheduled]);
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").StyledComponent<"span", any, IVisuallyHiddenProps & SxProp, never>;
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
- 'billable account',
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 ? principal.accountHandle : undefined) ||
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 = normalize(origin) || 'Datalayer';
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
- fontWeight: 'semibold',
71
- color: 'accent.fg',
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
- }, 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 ? (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["@", resolvedHandle] })) : null] })] }), _jsxs(Box, { sx: {
82
- display: 'grid',
83
- gridTemplateColumns: '110px 1fr',
84
- gap: 1,
85
- alignItems: 'baseline',
86
- }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Kind" }), _jsx(Text, { sx: { fontSize: 1 }, children: kind }), resolvedHandle ? (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "Handle" }), _jsxs(Text, { sx: { fontSize: 1 }, children: ["@", 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: {
87
- justifySelf: 'start',
88
- width: 'fit-content',
89
- maxWidth: '100%',
90
- overflow: 'hidden',
91
- textOverflow: 'ellipsis',
92
- whiteSpace: 'nowrap',
93
- }, 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: {
94
- justifySelf: 'start',
95
- width: 'fit-content',
96
- maxWidth: '100%',
97
- overflow: 'hidden',
98
- textOverflow: 'ellipsis',
99
- whiteSpace: 'nowrap',
100
- }, children: normalizedOrigin })] }))] }), _jsx(Box, { sx: { display: 'flex', justifyContent: 'flex-end' }, children: _jsx(Button, { size: "small", onClick: () => {
101
- if (targetPath) {
102
- navigate(targetPath);
103
- }
104
- }, disabled: !targetPath, children: "View Profile" }) })] }) })] }));
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 billable account store. It keeps both stores in
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 → billable account = same user/org
11
- * - selecting a team principal → billable account = the team's parent org
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;