@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.
- package/README.md +27 -0
- package/lib/api/DatalayerApi.d.ts +5 -1
- package/lib/api/DatalayerApi.js +55 -8
- package/lib/api/iam/oauth2.d.ts +3 -3
- package/lib/api/iam/oauth2.js +2 -2
- package/lib/api/iam/profile.d.ts +9 -1
- package/lib/api/iam/profile.js +15 -0
- package/lib/api/runtimes/checkpoints.js +1 -1
- package/lib/api/runtimes/snapshots.d.ts +7 -7
- package/lib/api/runtimes/snapshots.js +9 -9
- package/lib/client/auth/storage.js +103 -12
- package/lib/client/index.d.ts +7 -7
- package/lib/client/index.js +1 -1
- package/lib/client/mixins/RuntimesMixin.d.ts +6 -6
- package/lib/client/mixins/RuntimesMixin.js +5 -5
- package/lib/client/mixins/SpacerMixin.js +22 -1
- package/lib/collaboration/DatalayerCollaborationProvider.d.ts +4 -3
- package/lib/components/avatars/DLAvatar.d.ts +24 -0
- package/lib/components/avatars/DLAvatar.js +26 -0
- package/lib/components/avatars/UserProfileAvatar.js +4 -4
- package/lib/components/avatars/index.d.ts +1 -0
- package/lib/components/avatars/index.js +1 -0
- package/lib/components/billing/BillableAccountSelect.d.ts +35 -0
- package/lib/components/billing/BillableAccountSelect.js +340 -0
- package/lib/components/billing/index.d.ts +1 -0
- package/lib/components/billing/index.js +5 -0
- package/lib/components/checkout/StripeCheckout.d.ts +24 -3
- package/lib/components/checkout/StripeCheckout.js +1116 -55
- package/lib/components/display/DatalayerBox.js +6 -1
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +2 -0
- package/lib/components/principal/Principal.d.ts +37 -0
- package/lib/components/principal/Principal.js +54 -0
- package/lib/components/principal/PrincipalAvatar.d.ts +10 -0
- package/lib/components/principal/PrincipalAvatar.js +55 -0
- package/lib/components/principal/PrincipalBadge.d.ts +20 -0
- package/lib/components/principal/PrincipalBadge.js +208 -0
- package/lib/components/principal/PrincipalBanner.d.ts +7 -0
- package/lib/components/principal/PrincipalBanner.js +84 -0
- package/lib/components/principal/PrincipalDetailsOverlay.d.ts +23 -0
- package/lib/components/principal/PrincipalDetailsOverlay.js +86 -0
- package/lib/components/principal/PrincipalSwitcherMenu.d.ts +14 -0
- package/lib/components/principal/PrincipalSwitcherMenu.js +287 -0
- package/lib/components/principal/index.d.ts +7 -0
- package/lib/components/principal/index.js +10 -0
- package/lib/components/runtimes/RuntimeLauncherDialog.d.ts +2 -2
- package/lib/components/sharing/ShareAccessComponent.d.ts +18 -0
- package/lib/components/sharing/ShareAccessComponent.js +1368 -0
- package/lib/components/sharing/SharingEditor.d.ts +20 -0
- package/lib/components/sharing/SharingEditor.js +65 -0
- package/lib/components/sharing/index.d.ts +9 -0
- package/lib/components/sharing/index.js +12 -0
- package/lib/components/snapshots/{RuntimeSnapshotMenu.d.ts → SandboxSnapshotMenu.d.ts} +4 -4
- package/lib/components/snapshots/{RuntimeSnapshotMenu.js → SandboxSnapshotMenu.js} +33 -33
- package/lib/components/snapshots/index.d.ts +1 -1
- package/lib/components/snapshots/index.js +1 -1
- package/lib/components/storage/ContentsBrowser.js +2 -2
- package/lib/components/tokens/SpaceVariantToken.d.ts +1 -1
- package/lib/components/tokens/SpaceVariantToken.js +7 -4
- package/lib/components/users/PeerIndicator.js +5 -4
- package/lib/hooks/index.d.ts +5 -0
- package/lib/hooks/index.js +5 -0
- package/lib/hooks/useBillableAccountStore.d.ts +31 -0
- package/lib/hooks/useBillableAccountStore.js +53 -0
- package/lib/hooks/useCache.d.ts +125 -18
- package/lib/hooks/useCache.js +893 -120
- package/lib/hooks/useDatalayer.js +31 -3
- package/lib/hooks/useIAM.js +1 -8
- package/lib/hooks/usePrincipalStore.d.ts +41 -0
- package/lib/hooks/usePrincipalStore.js +101 -0
- package/lib/hooks/useProjects.d.ts +44 -0
- package/lib/hooks/useProjects.js +26 -2
- package/lib/hooks/useSelectedBillableAccount.d.ts +14 -0
- package/lib/hooks/useSelectedBillableAccount.js +36 -0
- package/lib/hooks/useSelectedPrincipal.d.ts +17 -0
- package/lib/hooks/useSelectedPrincipal.js +29 -0
- package/lib/hooks/useUsageRefreshStore.d.ts +8 -0
- package/lib/hooks/useUsageRefreshStore.js +16 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/mocks/components/FlashMock.js +8 -2
- package/lib/models/{RuntimeSnapshot.d.ts → CodeSandboxSnapshot.d.ts} +5 -5
- package/lib/models/{RuntimeSnapshot.js → CodeSandboxSnapshot.js} +1 -1
- package/lib/models/{RuntimeSnapshotDTO.d.ts → CodeSandboxSnapshotDTO.d.ts} +30 -30
- package/lib/models/{RuntimeSnapshotDTO.js → CodeSandboxSnapshotDTO.js} +6 -6
- package/lib/models/Environment.d.ts +4 -0
- package/lib/models/IAM.d.ts +66 -0
- package/lib/models/Organization.d.ts +1 -0
- package/lib/models/Organization.js +1 -0
- package/lib/models/Page.d.ts +2 -2
- package/lib/models/Page.js +2 -2
- package/lib/models/Profile.d.ts +1 -1
- package/lib/models/RuntimeDTO.d.ts +2 -2
- package/lib/models/Space.d.ts +1 -0
- package/lib/models/Space.js +29 -5
- package/lib/models/SpaceDTO.d.ts +4 -0
- package/lib/models/SpaceDTO.js +5 -0
- package/lib/models/Usage.d.ts +15 -0
- package/lib/models/Usage.js +9 -0
- package/lib/models/User.d.ts +21 -0
- package/lib/models/User.js +6 -2
- package/lib/models/index.d.ts +2 -2
- package/lib/models/index.js +2 -2
- package/lib/otel/auth.js +0 -4
- package/lib/services/index.d.ts +1 -1
- package/lib/services/index.js +1 -1
- package/lib/state/substates/IAMState.js +0 -7
- package/lib/state/substates/RuntimesState.d.ts +9 -9
- package/lib/state/substates/RuntimesState.js +3 -3
- package/lib/stateful/runtimes/actions.d.ts +17 -17
- package/lib/stateful/runtimes/actions.js +35 -55
- package/lib/stateful/runtimes/apis.d.ts +3 -3
- package/lib/stateful/runtimes/snapshots.d.ts +2 -2
- package/lib/stateful/runtimes/snapshots.js +7 -7
- package/lib/utils/Format.d.ts +6 -0
- package/lib/utils/Format.js +17 -0
- package/lib/utils/Handles.d.ts +3 -0
- package/lib/utils/Handles.js +53 -0
- package/lib/utils/Snapshot.d.ts +1 -1
- package/lib/utils/Snapshot.js +1 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/views/datasources/DatasourceDetail.d.ts +7 -1
- package/lib/views/datasources/DatasourceDetail.js +61 -23
- package/lib/views/datasources/DatasourceNew.d.ts +8 -1
- package/lib/views/datasources/DatasourceNew.js +23 -10
- package/lib/views/datasources/Datasources.d.ts +5 -1
- package/lib/views/datasources/Datasources.js +38 -30
- package/lib/views/iam-tokens/IAMTokenNew.d.ts +3 -1
- package/lib/views/iam-tokens/IAMTokenNew.js +2 -2
- package/lib/views/iam-tokens/IAMTokens.d.ts +3 -1
- package/lib/views/iam-tokens/IAMTokens.js +2 -2
- package/lib/views/otel/OtelHeader.d.ts +5 -0
- package/lib/views/otel/OtelHeader.js +3 -3
- package/lib/views/profile/UserBadge.d.ts +7 -2
- package/lib/views/profile/UserBadge.js +11 -2
- package/lib/views/secrets/Secrets.js +21 -21
- 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
|
|
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:
|
|
52
|
+
connect(sharedModel: SharedModel, documentId: string, options?: ConnectOptions): Promise<void>;
|
|
52
53
|
disconnect(): void;
|
|
53
54
|
getProvider(): WebsocketProvider | null;
|
|
54
|
-
getSharedModel():
|
|
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 {
|
|
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 {
|
|
9
|
+
import { DLAvatar } from './DLAvatar.js';
|
|
10
10
|
const Profile = ({ user, size = 100, onClick }) => {
|
|
11
|
-
return (_jsx(Box, { style: { width: size }, children: _jsx(
|
|
11
|
+
return (_jsx(Box, { style: { width: size }, children: _jsx(DLAvatar
|
|
12
12
|
// square
|
|
13
13
|
, {
|
|
14
14
|
// square
|
|
15
|
-
src:
|
|
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 }));
|
|
@@ -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';
|
|
@@ -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;
|