@datalayer/core 1.0.20 → 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.
Files changed (138) hide show
  1. package/README.md +27 -0
  2. package/lib/api/DatalayerApi.d.ts +5 -1
  3. package/lib/api/DatalayerApi.js +55 -8
  4. package/lib/api/iam/oauth2.d.ts +3 -3
  5. package/lib/api/iam/oauth2.js +2 -2
  6. package/lib/api/iam/profile.d.ts +9 -1
  7. package/lib/api/iam/profile.js +15 -0
  8. package/lib/api/runtimes/checkpoints.js +1 -1
  9. package/lib/api/runtimes/snapshots.d.ts +7 -7
  10. package/lib/api/runtimes/snapshots.js +9 -9
  11. package/lib/client/auth/storage.js +103 -12
  12. package/lib/client/index.d.ts +7 -7
  13. package/lib/client/index.js +1 -1
  14. package/lib/client/mixins/RuntimesMixin.d.ts +6 -6
  15. package/lib/client/mixins/RuntimesMixin.js +5 -5
  16. package/lib/client/mixins/SpacerMixin.js +22 -1
  17. package/lib/collaboration/DatalayerCollaborationProvider.d.ts +4 -3
  18. package/lib/components/avatars/DLAvatar.d.ts +24 -0
  19. package/lib/components/avatars/DLAvatar.js +26 -0
  20. package/lib/components/avatars/UserProfileAvatar.js +4 -4
  21. package/lib/components/avatars/index.d.ts +1 -0
  22. package/lib/components/avatars/index.js +1 -0
  23. package/lib/components/billing/BillableAccountSelect.d.ts +35 -0
  24. package/lib/components/billing/BillableAccountSelect.js +340 -0
  25. package/lib/components/billing/index.d.ts +1 -0
  26. package/lib/components/billing/index.js +5 -0
  27. package/lib/components/checkout/StripeCheckout.d.ts +24 -3
  28. package/lib/components/checkout/StripeCheckout.js +1116 -55
  29. package/lib/components/display/DatalayerBox.js +6 -1
  30. package/lib/components/index.d.ts +2 -0
  31. package/lib/components/index.js +2 -0
  32. package/lib/components/principal/Principal.d.ts +37 -0
  33. package/lib/components/principal/Principal.js +54 -0
  34. package/lib/components/principal/PrincipalAvatar.d.ts +10 -0
  35. package/lib/components/principal/PrincipalAvatar.js +55 -0
  36. package/lib/components/principal/PrincipalBadge.d.ts +20 -0
  37. package/lib/components/principal/PrincipalBadge.js +208 -0
  38. package/lib/components/principal/PrincipalBanner.d.ts +7 -0
  39. package/lib/components/principal/PrincipalBanner.js +84 -0
  40. package/lib/components/principal/PrincipalDetailsOverlay.d.ts +23 -0
  41. package/lib/components/principal/PrincipalDetailsOverlay.js +86 -0
  42. package/lib/components/principal/PrincipalSwitcherMenu.d.ts +14 -0
  43. package/lib/components/principal/PrincipalSwitcherMenu.js +287 -0
  44. package/lib/components/principal/index.d.ts +7 -0
  45. package/lib/components/principal/index.js +10 -0
  46. package/lib/components/runtimes/RuntimeLauncherDialog.d.ts +2 -2
  47. package/lib/components/sharing/ShareAccessComponent.d.ts +18 -0
  48. package/lib/components/sharing/ShareAccessComponent.js +1368 -0
  49. package/lib/components/sharing/SharingEditor.d.ts +20 -0
  50. package/lib/components/sharing/SharingEditor.js +65 -0
  51. package/lib/components/sharing/index.d.ts +9 -0
  52. package/lib/components/sharing/index.js +12 -0
  53. package/lib/components/snapshots/{RuntimeSnapshotMenu.d.ts → SandboxSnapshotMenu.d.ts} +4 -4
  54. package/lib/components/snapshots/{RuntimeSnapshotMenu.js → SandboxSnapshotMenu.js} +33 -33
  55. package/lib/components/snapshots/index.d.ts +1 -1
  56. package/lib/components/snapshots/index.js +1 -1
  57. package/lib/components/storage/ContentsBrowser.js +2 -2
  58. package/lib/components/tokens/SpaceVariantToken.d.ts +1 -1
  59. package/lib/components/tokens/SpaceVariantToken.js +7 -4
  60. package/lib/components/users/PeerIndicator.js +5 -4
  61. package/lib/hooks/index.d.ts +5 -0
  62. package/lib/hooks/index.js +5 -0
  63. package/lib/hooks/useBillableAccountStore.d.ts +31 -0
  64. package/lib/hooks/useBillableAccountStore.js +53 -0
  65. package/lib/hooks/useCache.d.ts +125 -18
  66. package/lib/hooks/useCache.js +893 -120
  67. package/lib/hooks/useDatalayer.js +31 -3
  68. package/lib/hooks/useIAM.js +1 -8
  69. package/lib/hooks/usePrincipalStore.d.ts +41 -0
  70. package/lib/hooks/usePrincipalStore.js +101 -0
  71. package/lib/hooks/useProjects.d.ts +44 -0
  72. package/lib/hooks/useProjects.js +26 -2
  73. package/lib/hooks/useSelectedBillableAccount.d.ts +14 -0
  74. package/lib/hooks/useSelectedBillableAccount.js +36 -0
  75. package/lib/hooks/useSelectedPrincipal.d.ts +17 -0
  76. package/lib/hooks/useSelectedPrincipal.js +29 -0
  77. package/lib/hooks/useUsageRefreshStore.d.ts +8 -0
  78. package/lib/hooks/useUsageRefreshStore.js +16 -0
  79. package/lib/index.d.ts +2 -2
  80. package/lib/index.js +1 -1
  81. package/lib/mocks/components/FlashMock.js +8 -2
  82. package/lib/models/{RuntimeSnapshot.d.ts → CodeSandboxSnapshot.d.ts} +5 -5
  83. package/lib/models/{RuntimeSnapshot.js → CodeSandboxSnapshot.js} +1 -1
  84. package/lib/models/{RuntimeSnapshotDTO.d.ts → CodeSandboxSnapshotDTO.d.ts} +30 -30
  85. package/lib/models/{RuntimeSnapshotDTO.js → CodeSandboxSnapshotDTO.js} +6 -6
  86. package/lib/models/Environment.d.ts +4 -0
  87. package/lib/models/IAM.d.ts +66 -0
  88. package/lib/models/Organization.d.ts +1 -0
  89. package/lib/models/Organization.js +1 -0
  90. package/lib/models/Page.d.ts +2 -2
  91. package/lib/models/Page.js +2 -2
  92. package/lib/models/Profile.d.ts +1 -1
  93. package/lib/models/RuntimeDTO.d.ts +2 -2
  94. package/lib/models/Space.d.ts +1 -0
  95. package/lib/models/Space.js +29 -5
  96. package/lib/models/SpaceDTO.d.ts +4 -0
  97. package/lib/models/SpaceDTO.js +5 -0
  98. package/lib/models/Usage.d.ts +15 -0
  99. package/lib/models/Usage.js +9 -0
  100. package/lib/models/User.d.ts +21 -0
  101. package/lib/models/User.js +6 -2
  102. package/lib/models/index.d.ts +2 -2
  103. package/lib/models/index.js +2 -2
  104. package/lib/otel/auth.js +0 -4
  105. package/lib/services/index.d.ts +1 -1
  106. package/lib/services/index.js +1 -1
  107. package/lib/state/substates/IAMState.js +0 -7
  108. package/lib/state/substates/RuntimesState.d.ts +9 -9
  109. package/lib/state/substates/RuntimesState.js +3 -3
  110. package/lib/stateful/runtimes/actions.d.ts +17 -17
  111. package/lib/stateful/runtimes/actions.js +35 -55
  112. package/lib/stateful/runtimes/apis.d.ts +3 -3
  113. package/lib/stateful/runtimes/snapshots.d.ts +2 -2
  114. package/lib/stateful/runtimes/snapshots.js +7 -7
  115. package/lib/utils/Format.d.ts +6 -0
  116. package/lib/utils/Format.js +17 -0
  117. package/lib/utils/Handles.d.ts +3 -0
  118. package/lib/utils/Handles.js +53 -0
  119. package/lib/utils/Snapshot.d.ts +1 -1
  120. package/lib/utils/Snapshot.js +1 -1
  121. package/lib/utils/index.d.ts +1 -0
  122. package/lib/utils/index.js +1 -0
  123. package/lib/views/datasources/DatasourceDetail.d.ts +7 -1
  124. package/lib/views/datasources/DatasourceDetail.js +61 -23
  125. package/lib/views/datasources/DatasourceNew.d.ts +8 -1
  126. package/lib/views/datasources/DatasourceNew.js +23 -10
  127. package/lib/views/datasources/Datasources.d.ts +5 -1
  128. package/lib/views/datasources/Datasources.js +38 -30
  129. package/lib/views/iam-tokens/IAMTokenNew.d.ts +3 -1
  130. package/lib/views/iam-tokens/IAMTokenNew.js +2 -2
  131. package/lib/views/iam-tokens/IAMTokens.d.ts +3 -1
  132. package/lib/views/iam-tokens/IAMTokens.js +2 -2
  133. package/lib/views/otel/OtelHeader.d.ts +5 -0
  134. package/lib/views/otel/OtelHeader.js +3 -3
  135. package/lib/views/profile/UserBadge.d.ts +7 -2
  136. package/lib/views/profile/UserBadge.js +11 -2
  137. package/lib/views/secrets/Secrets.js +21 -21
  138. package/package.json +7 -8
@@ -473,8 +473,29 @@ export function SpacerMixin(Base) {
473
473
  // Use getMySpaces and filter by variant, because the
474
474
  // /spaces/types/project endpoint returns empty results.
475
475
  const allSpaces = await this.getMySpaces();
476
+ const isProjectSpace = (space) => {
477
+ const raw = space.rawData();
478
+ const variant = raw?.variant_s ?? space.variant;
479
+ if (variant !== 'project') {
480
+ return false;
481
+ }
482
+ const items = Array.isArray(raw?.items)
483
+ ? raw.items
484
+ : raw?.items && typeof raw.items === 'object'
485
+ ? [raw.items]
486
+ : [];
487
+ if (items.length === 0) {
488
+ return true;
489
+ }
490
+ const hasNotebook = items.some((item) => (item?.type_s ?? item?.type) === 'notebook');
491
+ const hasDocument = items.some((item) => {
492
+ const type = item?.type_s ?? item?.type;
493
+ return type === 'document' || type === 'lexical';
494
+ });
495
+ return hasNotebook && hasDocument;
496
+ };
476
497
  return allSpaces
477
- .filter(s => s.variant === 'project')
498
+ .filter(s => isProjectSpace(s))
478
499
  .map(s => new ProjectDTO(s.rawData()));
479
500
  }
480
501
  /**
@@ -1,4 +1,3 @@
1
- import { YNotebook } from '@jupyter/ydoc';
2
1
  import { WebsocketProvider } from 'y-websocket';
3
2
  import type { ICollaborationProvider, ICollaborationProviderEvents } from '@datalayer/jupyter-react';
4
3
  export declare enum CollaborationStatus {
@@ -7,6 +6,8 @@ export declare enum CollaborationStatus {
7
6
  Connected = "connected",
8
7
  Error = "error"
9
8
  }
9
+ type SharedModel = Parameters<ICollaborationProvider['connect']>[0];
10
+ type ConnectOptions = Parameters<ICollaborationProvider['connect']>[2];
10
11
  /**
11
12
  * Configuration for Datalayer collaboration provider
12
13
  */
@@ -48,10 +49,10 @@ export declare class DatalayerCollaborationProvider implements ICollaborationPro
48
49
  get isDisposed(): boolean;
49
50
  get events(): ICollaborationProviderEvents;
50
51
  private setStatus;
51
- connect(sharedModel: YNotebook, documentId: string, options?: Record<string, any>): Promise<void>;
52
+ connect(sharedModel: SharedModel, documentId: string, options?: ConnectOptions): Promise<void>;
52
53
  disconnect(): void;
53
54
  getProvider(): WebsocketProvider | null;
54
- getSharedModel(): YNotebook | null;
55
+ getSharedModel(): SharedModel | null;
55
56
  handleConnectionClose(event: CloseEvent): void;
56
57
  handleSync(isSynced: boolean): void;
57
58
  dispose(): void;
@@ -0,0 +1,24 @@
1
+ import { type AvatarProps } from '@primer/react';
2
+ /**
3
+ * Props for {@link DLAvatar}.
4
+ *
5
+ * Same as Primer's `AvatarProps` but `src` accepts the raw avatar URL
6
+ * (e.g. `user.avatar_url_s`) and is run through {@link getAvatarURL}
7
+ * so Gravatar fallbacks and the default avatar are handled consistently.
8
+ */
9
+ export type DLAvatarProps = Omit<AvatarProps, 'src'> & {
10
+ src?: string;
11
+ };
12
+ /**
13
+ * Datalayer Avatar wrapper.
14
+ *
15
+ * Centralizes two concerns for every avatar rendered in our apps:
16
+ *
17
+ * - URL normalization via {@link getAvatarURL} (handles Gravatar query
18
+ * parameters and the default avatar fallback).
19
+ * - `referrerPolicy="no-referrer"` so that third-party CDNs such as
20
+ * `lh3.googleusercontent.com` (Google), LinkedIn media, etc. do not
21
+ * deny the request based on the `Referer` header.
22
+ */
23
+ export declare const DLAvatar: ({ src, ...rest }: DLAvatarProps) => import("react/jsx-runtime").JSX.Element;
24
+ export default DLAvatar;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /*
7
+ * Copyright (c) 2023-2026 Datalayer, Inc.
8
+ * Distributed under the terms of the Modified BSD License.
9
+ */
10
+ import { Avatar } from '@primer/react';
11
+ import { getAvatarURL } from '../../utils/index.js';
12
+ /**
13
+ * Datalayer Avatar wrapper.
14
+ *
15
+ * Centralizes two concerns for every avatar rendered in our apps:
16
+ *
17
+ * - URL normalization via {@link getAvatarURL} (handles Gravatar query
18
+ * parameters and the default avatar fallback).
19
+ * - `referrerPolicy="no-referrer"` so that third-party CDNs such as
20
+ * `lh3.googleusercontent.com` (Google), LinkedIn media, etc. do not
21
+ * deny the request based on the `Referer` header.
22
+ */
23
+ export const DLAvatar = ({ src, ...rest }) => {
24
+ return (_jsx(Avatar, { src: getAvatarURL(src), referrerPolicy: "no-referrer", ...rest }));
25
+ };
26
+ export default DLAvatar;
@@ -3,16 +3,16 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  * Copyright (c) 2023-2025 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
- import { Avatar, Link } from '@primer/react';
6
+ import { Link } from '@primer/react';
7
7
  import { Box } from '@datalayer/primer-addons';
8
8
  import { AvatarSkeleton } from '../../components/display/index.js';
9
- import { getAvatarURL } from '../../utils/index.js';
9
+ import { DLAvatar } from './DLAvatar.js';
10
10
  const Profile = ({ user, size = 100, onClick }) => {
11
- return (_jsx(Box, { style: { width: size }, children: _jsx(Avatar
11
+ return (_jsx(Box, { style: { width: size }, children: _jsx(DLAvatar
12
12
  // square
13
13
  , {
14
14
  // square
15
- src: getAvatarURL(user?.avatarUrl), size: size, onClick: onClick }) }));
15
+ src: user?.avatarUrl, size: size, onClick: onClick }) }));
16
16
  };
17
17
  export const UserProfileAvatar = ({ onClick, user, size = 100, }) => {
18
18
  return user ? (onClick ? (_jsx(Link, { href: "javascript: return false;", onClick: onClick, children: _jsx(Profile, { user: user, size: size, onClick: onClick }) })) : (_jsx(Profile, { user: user, size: size, onClick: onClick }))) : (_jsx(AvatarSkeleton, { size: size }));
@@ -1,2 +1,3 @@
1
1
  export * from './BoringAvatar';
2
+ export * from './DLAvatar';
2
3
  export * from './UserProfileAvatar';
@@ -3,4 +3,5 @@
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
5
  export * from './BoringAvatar.js';
6
+ export * from './DLAvatar.js';
6
7
  export * from './UserProfileAvatar.js';
@@ -0,0 +1,35 @@
1
+ export type BillableAccountType = 'user' | 'organization' | 'team';
2
+ export type BillableAccount = {
3
+ accountUid: string;
4
+ accountType: BillableAccountType;
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 BillableAccountSelectProps = {
15
+ value: string;
16
+ onChange: (accountUid: string) => void;
17
+ onSelectedAccountChange?: (account: BillableAccount | undefined) => void;
18
+ onAccountsResolved?: (state: {
19
+ accounts: BillableAccount[];
20
+ eligibleAccounts: BillableAccount[];
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 resolveBillablePlanTier(value: unknown): 'free' | 'pro';
33
+ export declare function formatBillableAccountPlanLabel(planName: string): string;
34
+ export declare function BillableAccountSelect({ value, onChange, onSelectedAccountChange, onAccountsResolved, disabled, label, caption, emptyMessage, flashMessage, width, preferOrganizationDefault, }: BillableAccountSelectProps): JSX.Element;
35
+ export default BillableAccountSelect;
@@ -0,0 +1,340 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * BillableAccountSelect — self-contained dropdown that lets the user pick a
8
+ * billable account (personal, organization, or eligible team) for runs that
9
+ * consume wallet credits.
10
+ *
11
+ * Encapsulates eligibility merge logic and account-detail fetching. Callers
12
+ * only need to provide a value/onChange pair and optionally observe the full
13
+ * resolved account via `onSelectedAccountChange`.
14
+ */
15
+ import { useCallback, useEffect, useMemo, Fragment } from 'react';
16
+ import { ActionList, ActionMenu, Button, Flash, FormControl, Label, Spinner, Text, } from '@primer/react';
17
+ import { OrganizationIcon, PeopleIcon, PersonIcon, } from '@primer/octicons-react';
18
+ import { Box } from '@datalayer/primer-addons';
19
+ import { useCache } from '../../hooks/useCache.js';
20
+ import { useSelectedPrincipal } from '../../hooks/useSelectedPrincipal.js';
21
+ import { useIAMStore } from '../../state/index.js';
22
+ const PLAN_FREE_TERMS = ['free', 'starter'];
23
+ const PLAN_PRO_TERMS = ['pro', 'paid', 'team', 'enterprise', 'business'];
24
+ const BILLABLE_ACCOUNT_COOKIE = 'datalayer-billable-account-uid';
25
+ const BILLABLE_ACCOUNT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
26
+ function readBillableAccountCookie() {
27
+ if (typeof document === 'undefined')
28
+ return null;
29
+ const escaped = BILLABLE_ACCOUNT_COOKIE.replace(/[.$?*|{}()[\]\\/+^]/g, '\\$&');
30
+ const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
31
+ return match ? decodeURIComponent(match[1]) : null;
32
+ }
33
+ function writeBillableAccountCookie(value) {
34
+ if (typeof document === 'undefined')
35
+ return;
36
+ document.cookie =
37
+ `${BILLABLE_ACCOUNT_COOKIE}=${encodeURIComponent(value)};` +
38
+ ` path=/; max-age=${BILLABLE_ACCOUNT_COOKIE_MAX_AGE}; SameSite=Lax`;
39
+ }
40
+ const planContains = (value, terms) => terms.some(term => value.includes(term));
41
+ export function resolveBillablePlanTier(value) {
42
+ const normalized = String(value ?? '').toLowerCase();
43
+ if (!normalized || normalized === 'unknown')
44
+ return 'free';
45
+ if (planContains(normalized, PLAN_FREE_TERMS))
46
+ return 'free';
47
+ if (planContains(normalized, PLAN_PRO_TERMS))
48
+ return 'pro';
49
+ return 'free';
50
+ }
51
+ export function formatBillableAccountPlanLabel(planName) {
52
+ return resolveBillablePlanTier(planName) === 'pro'
53
+ ? 'Team Plan'
54
+ : 'Free Plan';
55
+ }
56
+ 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, }) {
57
+ const { user } = useIAMStore();
58
+ const { useEligibleSubscriptionAccounts, useSubscriptionAccountsDetails, useUserOrganizations, } = useCache();
59
+ const { selectedPrincipalKind, selectedPrincipalUid } = useSelectedPrincipal();
60
+ const userOrganizationsQuery = useUserOrganizations();
61
+ const { data: eligibleAccountsRaw, isLoading: eligibleAccountsLoading } = useEligibleSubscriptionAccounts({
62
+ refetchInterval: 10_000,
63
+ refetchOnMount: true,
64
+ refetchOnWindowFocus: true,
65
+ staleTime: 0,
66
+ });
67
+ const eligibleAccounts = useMemo(() => (eligibleAccountsRaw || []).map((entry) => ({
68
+ accountUid: String(entry.account_uid || ''),
69
+ accountType: String(entry.account_type || 'user'),
70
+ accountHandle: String(entry.account_handle || '').trim(),
71
+ accountName: String(entry.account_name || '').trim() ||
72
+ String(entry.account_handle || '').trim() ||
73
+ String(entry.account_uid || '').trim(),
74
+ planName: String(entry?.subscription?.plan_name || entry?.plan?.plan_name || '').trim(),
75
+ })), [eligibleAccountsRaw]);
76
+ const personalAccountUid = String(user?.id || '');
77
+ const allContextAccounts = useMemo(() => {
78
+ const accountMap = new Map();
79
+ if (personalAccountUid) {
80
+ accountMap.set(personalAccountUid, {
81
+ accountUid: personalAccountUid,
82
+ accountType: 'user',
83
+ accountHandle: String(user?.handle || '').trim(),
84
+ accountName: String(user?.handle || '').trim() || personalAccountUid,
85
+ planName: '',
86
+ });
87
+ }
88
+ for (const organization of (userOrganizationsQuery.data || [])) {
89
+ const orgUid = String(organization?.uid || organization?.id || '').trim();
90
+ if (!orgUid)
91
+ continue;
92
+ accountMap.set(orgUid, {
93
+ accountUid: orgUid,
94
+ accountType: 'organization',
95
+ accountHandle: String(organization?.handle || '').trim(),
96
+ accountName: String(organization?.handle || '').trim() ||
97
+ String(organization?.name || '').trim() ||
98
+ orgUid,
99
+ planName: String(organization?.subscription?.plan_name ||
100
+ organization?.plan_name ||
101
+ '').trim(),
102
+ });
103
+ }
104
+ return Array.from(accountMap.values());
105
+ }, [personalAccountUid, user, userOrganizationsQuery.data]);
106
+ const eligibleAccountByUid = useMemo(() => new Map(eligibleAccounts.map(a => [a.accountUid, a])), [eligibleAccounts]);
107
+ const candidateUids = useMemo(() => {
108
+ const values = new Set();
109
+ for (const a of allContextAccounts)
110
+ values.add(a.accountUid);
111
+ for (const a of eligibleAccounts)
112
+ values.add(a.accountUid);
113
+ if (value)
114
+ values.add(value);
115
+ return Array.from(values).filter(Boolean);
116
+ }, [allContextAccounts, eligibleAccounts, value]);
117
+ const { data: detailsRaw, isLoading: detailsLoading } = useSubscriptionAccountsDetails(candidateUids, {
118
+ refetchInterval: 10_000,
119
+ refetchOnMount: true,
120
+ refetchOnWindowFocus: true,
121
+ staleTime: 0,
122
+ });
123
+ const detailsByUid = useMemo(() => new Map((detailsRaw || []).map((entry) => [
124
+ String(entry.account_uid || ''),
125
+ entry,
126
+ ])), [detailsRaw]);
127
+ const accounts = useMemo(() => {
128
+ const accountMap = new Map();
129
+ for (const a of allContextAccounts)
130
+ accountMap.set(a.accountUid, a);
131
+ for (const a of eligibleAccounts)
132
+ accountMap.set(a.accountUid, a);
133
+ const merged = Array.from(accountMap.values());
134
+ const mergedByUid = new Map(merged.map(a => [a.accountUid, a]));
135
+ return merged.map(account => {
136
+ const eligible = eligibleAccountByUid.get(account.accountUid);
137
+ const details = detailsByUid.get(account.accountUid);
138
+ const accountType = String(details?.account_type || account.accountType || 'user');
139
+ const accountHandle = String(details?.account_handle || account.accountHandle || '');
140
+ const planName = String(details?.subscription?.plan_name ||
141
+ eligible?.planName ||
142
+ account.planName ||
143
+ '').trim();
144
+ const walletBalance = Number(accountType === 'team'
145
+ ? (details?.wallet_balance ?? 0)
146
+ : (details?.subscription?.wallet_balance ??
147
+ details?.wallet_balance ??
148
+ 0));
149
+ const hasPositiveWallet = Number.isFinite(walletBalance) && walletBalance > 0;
150
+ const isEligible = accountType === 'team'
151
+ ? hasPositiveWallet
152
+ : typeof details?.is_eligible === 'boolean'
153
+ ? details.is_eligible ||
154
+ (accountType === 'user' && hasPositiveWallet)
155
+ : Boolean(eligible);
156
+ const sourceOrganizationUid = accountType === 'team'
157
+ ? String(details?.plan_source_account_uid || '').trim() || undefined
158
+ : undefined;
159
+ const sourceOrgDetails = sourceOrganizationUid
160
+ ? detailsByUid.get(sourceOrganizationUid)
161
+ : undefined;
162
+ const sourceOrgMerged = sourceOrganizationUid
163
+ ? mergedByUid.get(sourceOrganizationUid)
164
+ : undefined;
165
+ const sourceOrganizationHandle = sourceOrganizationUid
166
+ ? String(sourceOrgDetails?.account_handle ||
167
+ sourceOrgMerged?.accountHandle ||
168
+ '').trim() || undefined
169
+ : undefined;
170
+ return {
171
+ accountUid: account.accountUid,
172
+ accountType,
173
+ accountHandle,
174
+ accountName: String(details?.account_name || account.accountName || account.accountUid),
175
+ planName,
176
+ isEligible,
177
+ isPaidPlan: resolveBillablePlanTier(planName || 'free') === 'pro',
178
+ sourceOrganizationUid,
179
+ sourceOrganizationHandle,
180
+ teamHandle: accountType === 'team' ? accountHandle : undefined,
181
+ };
182
+ });
183
+ }, [
184
+ allContextAccounts,
185
+ eligibleAccounts,
186
+ eligibleAccountByUid,
187
+ detailsByUid,
188
+ ]);
189
+ const eligibleBillable = useMemo(() => accounts.filter(a => a.isEligible), [accounts]);
190
+ const hasEligibleAccount = eligibleBillable.length > 0;
191
+ const isLoading = eligibleAccountsLoading || detailsLoading;
192
+ const storedBillableAccountUid = useMemo(() => readBillableAccountCookie(), []);
193
+ const preferredEligible = useMemo(() => {
194
+ const fromCookie = storedBillableAccountUid
195
+ ? eligibleBillable.find(account => account.accountUid === storedBillableAccountUid)
196
+ : undefined;
197
+ if (fromCookie)
198
+ return fromCookie;
199
+ const byPrincipal = selectedPrincipalUid
200
+ ? eligibleBillable.find(account => {
201
+ if (account.accountUid !== selectedPrincipalUid)
202
+ return false;
203
+ if (selectedPrincipalKind === 'organization')
204
+ return account.accountType === 'organization';
205
+ if (selectedPrincipalKind === 'team')
206
+ return account.accountType === 'team';
207
+ return account.accountType === 'user';
208
+ })
209
+ : undefined;
210
+ if (byPrincipal)
211
+ return byPrincipal;
212
+ const personalEligible = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
213
+ if (personalEligible)
214
+ return personalEligible;
215
+ const firstOrg = eligibleBillable.find(a => a.accountType === 'organization');
216
+ if (preferOrganizationDefault && firstOrg)
217
+ return firstOrg;
218
+ return firstOrg || eligibleBillable[0];
219
+ }, [
220
+ storedBillableAccountUid,
221
+ eligibleBillable,
222
+ personalAccountUid,
223
+ preferOrganizationDefault,
224
+ selectedPrincipalKind,
225
+ selectedPrincipalUid,
226
+ ]);
227
+ const handleAccountSelect = useCallback((accountUid) => {
228
+ writeBillableAccountCookie(accountUid);
229
+ onChange(accountUid);
230
+ }, [onChange]);
231
+ // Auto-select a sensible default when current value is empty/ineligible.
232
+ useEffect(() => {
233
+ if (isLoading)
234
+ return;
235
+ if (!preferredEligible) {
236
+ if (value)
237
+ onChange('');
238
+ return;
239
+ }
240
+ const current = accounts.find(a => a.accountUid === value);
241
+ if (!current || !current.isEligible) {
242
+ onChange(preferredEligible.accountUid);
243
+ }
244
+ }, [isLoading, preferredEligible, accounts, value, onChange]);
245
+ const selectedAccount = useMemo(() => accounts.find(a => a.accountUid === value), [accounts, value]);
246
+ useEffect(() => {
247
+ onSelectedAccountChange?.(selectedAccount);
248
+ }, [selectedAccount, onSelectedAccountChange]);
249
+ useEffect(() => {
250
+ onAccountsResolved?.({
251
+ accounts,
252
+ eligibleAccounts: eligibleBillable,
253
+ isLoading,
254
+ hasEligibleAccount,
255
+ });
256
+ }, [
257
+ accounts,
258
+ eligibleBillable,
259
+ isLoading,
260
+ hasEligibleAccount,
261
+ onAccountsResolved,
262
+ ]);
263
+ return (_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: label }), _jsx(Box, { sx: { display: 'flex', justifyContent: 'center' }, children: _jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Button, { variant: "default", disabled: disabled || !hasEligibleAccount, sx: { width, justifyContent: 'space-between' }, children: isLoading ? (_jsxs(Box, { sx: {
264
+ display: 'inline-flex',
265
+ alignItems: 'center',
266
+ gap: 2,
267
+ }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1 }, children: "Loading plan status..." })] })) : selectedAccount ? (_jsxs(Box, { sx: {
268
+ display: 'flex',
269
+ alignItems: 'center',
270
+ justifyContent: 'space-between',
271
+ width: '100%',
272
+ minWidth: 0,
273
+ gap: 2,
274
+ }, children: [_jsxs(Text, { sx: {
275
+ minWidth: 0,
276
+ overflow: 'hidden',
277
+ textOverflow: 'ellipsis',
278
+ whiteSpace: 'nowrap',
279
+ }, children: ["@", selectedAccount.accountName] }), _jsxs(Box, { sx: {
280
+ display: 'flex',
281
+ alignItems: 'center',
282
+ justifyContent: 'flex-end',
283
+ gap: 1,
284
+ flexShrink: 0,
285
+ }, children: [_jsx(Label, { size: "small", variant: "secondary", title: selectedAccount.accountType === 'organization'
286
+ ? 'Organization'
287
+ : selectedAccount.accountType === 'team'
288
+ ? 'Team'
289
+ : '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: {
290
+ display: 'inline-flex',
291
+ alignItems: 'center',
292
+ gap: 2,
293
+ }, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1 }, children: "Loading plan status..." })] }) })) : !hasEligibleAccount ? (_jsx(ActionList.Item, { disabled: true, children: emptyMessage })) : ((() => {
294
+ const typeOrder = {
295
+ user: 0,
296
+ organization: 1,
297
+ team: 2,
298
+ };
299
+ const sorted = [...accounts].sort((a, b) => (typeOrder[a.accountType] ?? 99) -
300
+ (typeOrder[b.accountType] ?? 99));
301
+ return sorted.map((account, idx) => {
302
+ const prevType = idx > 0 ? sorted[idx - 1].accountType : undefined;
303
+ const showDivider = prevType !== undefined &&
304
+ prevType !== account.accountType;
305
+ return (_jsxs(Fragment, { children: [showDivider && _jsx(ActionList.Divider, {}), _jsxs(ActionList.Item, { selected: account.accountUid === value, disabled: !account.isEligible, onSelect: () => {
306
+ if (account.isEligible) {
307
+ handleAccountSelect(account.accountUid);
308
+ }
309
+ }, children: [_jsxs(Box, { sx: {
310
+ display: 'flex',
311
+ alignItems: 'center',
312
+ justifyContent: 'space-between',
313
+ width: '100%',
314
+ minWidth: 0,
315
+ gap: 2,
316
+ }, children: [_jsxs(Text, { sx: {
317
+ minWidth: 0,
318
+ overflow: 'hidden',
319
+ textOverflow: 'ellipsis',
320
+ whiteSpace: 'nowrap',
321
+ }, children: ["@", account.accountName] }), _jsxs(Box, { sx: {
322
+ display: 'flex',
323
+ alignItems: 'center',
324
+ justifyContent: 'flex-end',
325
+ gap: 1,
326
+ flexShrink: 0,
327
+ }, children: [_jsx(Label, { size: "small", variant: "secondary", title: account.accountType === 'organization'
328
+ ? 'Organization'
329
+ : account.accountType === 'team'
330
+ ? 'Team'
331
+ : 'User', children: account.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : account.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), !account.isEligible &&
332
+ 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
333
+ ? 'Eligible'
334
+ : account.accountType === 'team'
335
+ ? 'Not eligible — no team credits allocated'
336
+ : 'Not eligible — activate a plan or add credits to use this account' })] })] }, account.accountUid));
337
+ });
338
+ })()) })] })] }) }), _jsx(FormControl.Caption, { children: caption })] }));
339
+ }
340
+ export default BillableAccountSelect;
@@ -0,0 +1 @@
1
+ export * from './BillableAccountSelect';
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from './BillableAccountSelect.js';
@@ -1,3 +1,4 @@
1
+ import type { StripeElementsOptions } from '@stripe/stripe-js';
1
2
  import type { ICheckoutPortal } from '../../models';
2
3
  /**
3
4
  * Price item interface
@@ -23,10 +24,30 @@ export interface IPrice {
23
24
  * Computational credits to receive
24
25
  */
25
26
  credits: number;
27
+ /**
28
+ * Whether this price is the server-selected default option
29
+ */
30
+ default?: boolean;
26
31
  }
32
+ export interface ISubscriptionPlan {
33
+ id: string;
34
+ name: string;
35
+ amount: number;
36
+ currency: string;
37
+ interval?: string;
38
+ included_runs?: number;
39
+ }
40
+ export type StripeCheckoutProps = {
41
+ checkoutPortal: ICheckoutPortal | null;
42
+ appearance?: StripeElementsOptions['appearance'];
43
+ accountUid?: string;
44
+ showStatusUsageSummary?: boolean;
45
+ onCheckoutSuccess?: (event: {
46
+ checkoutType: 'topup' | 'subscription' | 'resume';
47
+ purchasedCredits?: number;
48
+ }) => void;
49
+ };
27
50
  /**
28
51
  * Stripe checkout.
29
52
  */
30
- export declare function StripeCheckout({ checkoutPortal, }: {
31
- checkoutPortal: ICheckoutPortal | null;
32
- }): import("react/jsx-runtime").JSX.Element;
53
+ export declare function StripeCheckout({ checkoutPortal, appearance, accountUid, showStatusUsageSummary, onCheckoutSuccess, }: StripeCheckoutProps): import("react/jsx-runtime").JSX.Element;