@datalayer/core 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/billing/BillableAccountSelect.d.ts +35 -0
- package/lib/components/billing/BillableAccountSelect.js +376 -0
- package/lib/components/billing/index.d.ts +1 -0
- package/lib/components/billing/index.js +5 -0
- package/lib/components/display/JupyterDialog.d.ts +8 -1
- package/lib/components/display/JupyterDialog.js +38 -5
- 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 +4 -0
- package/lib/components/runtimes/RuntimeLauncherDialog.js +36 -11
- package/lib/components/runtimes/RuntimePickerCell.js +1 -1
- package/lib/components/runtimes/RuntimePickerNotebook.js +89 -24
- package/lib/components/runtimes/RuntimeReservationControl.js +13 -4
- package/lib/components/sharing/ShareAccessComponent.d.ts +18 -0
- package/lib/components/sharing/ShareAccessComponent.js +1368 -0
- package/lib/components/sharing/SharingEditor.d.ts +20 -0
- package/lib/components/sharing/SharingEditor.js +65 -0
- package/lib/components/sharing/index.d.ts +9 -0
- package/lib/components/sharing/index.js +12 -0
- package/lib/hooks/index.d.ts +5 -0
- package/lib/hooks/index.js +5 -0
- package/lib/hooks/useBillableAccountStore.d.ts +31 -0
- package/lib/hooks/useBillableAccountStore.js +53 -0
- package/lib/hooks/usePrincipalStore.d.ts +41 -0
- package/lib/hooks/usePrincipalStore.js +101 -0
- package/lib/hooks/useSelectedBillableAccount.d.ts +14 -0
- package/lib/hooks/useSelectedBillableAccount.js +36 -0
- package/lib/hooks/useSelectedPrincipal.d.ts +17 -0
- package/lib/hooks/useSelectedPrincipal.js +29 -0
- package/lib/hooks/useUsageRefreshStore.d.ts +8 -0
- package/lib/hooks/useUsageRefreshStore.js +16 -0
- package/lib/stateful/runtimes/actions.js +7 -1
- package/lib/utils/Handles.d.ts +3 -0
- package/lib/utils/Handles.js +53 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/package.json +1 -3
|
@@ -0,0 +1,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,376 @@
|
|
|
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, useRef, 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 personalEligible = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
200
|
+
if (personalEligible)
|
|
201
|
+
return personalEligible;
|
|
202
|
+
const byPrincipal = selectedPrincipalUid
|
|
203
|
+
? eligibleBillable.find(account => {
|
|
204
|
+
if (account.accountUid !== selectedPrincipalUid)
|
|
205
|
+
return false;
|
|
206
|
+
if (selectedPrincipalKind === 'organization')
|
|
207
|
+
return account.accountType === 'organization';
|
|
208
|
+
if (selectedPrincipalKind === 'team')
|
|
209
|
+
return account.accountType === 'team';
|
|
210
|
+
return account.accountType === 'user';
|
|
211
|
+
})
|
|
212
|
+
: undefined;
|
|
213
|
+
if (byPrincipal)
|
|
214
|
+
return byPrincipal;
|
|
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
|
+
// Apply persisted selection (cookie) once accounts are resolved. Runs once
|
|
232
|
+
// per mount so users can still change selection afterwards; falls back to
|
|
233
|
+
// personal user account when the stored uid is unknown/ineligible.
|
|
234
|
+
const initialSelectionAppliedRef = useRef(false);
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (isLoading)
|
|
237
|
+
return;
|
|
238
|
+
if (initialSelectionAppliedRef.current)
|
|
239
|
+
return;
|
|
240
|
+
if (eligibleBillable.length === 0 && accounts.length === 0)
|
|
241
|
+
return;
|
|
242
|
+
initialSelectionAppliedRef.current = true;
|
|
243
|
+
const storedAccount = storedBillableAccountUid
|
|
244
|
+
? eligibleBillable.find(a => a.accountUid === storedBillableAccountUid)
|
|
245
|
+
: undefined;
|
|
246
|
+
const personalAccount = eligibleBillable.find(a => a.accountType === 'user' && a.accountUid === personalAccountUid);
|
|
247
|
+
const target = storedAccount || personalAccount || preferredEligible;
|
|
248
|
+
if (!target)
|
|
249
|
+
return;
|
|
250
|
+
if (target.accountUid !== value) {
|
|
251
|
+
writeBillableAccountCookie(target.accountUid);
|
|
252
|
+
onChange(target.accountUid);
|
|
253
|
+
}
|
|
254
|
+
}, [
|
|
255
|
+
isLoading,
|
|
256
|
+
accounts,
|
|
257
|
+
eligibleBillable,
|
|
258
|
+
storedBillableAccountUid,
|
|
259
|
+
personalAccountUid,
|
|
260
|
+
preferredEligible,
|
|
261
|
+
value,
|
|
262
|
+
onChange,
|
|
263
|
+
]);
|
|
264
|
+
// Auto-select a sensible default when current value is empty/ineligible.
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (isLoading)
|
|
267
|
+
return;
|
|
268
|
+
if (!initialSelectionAppliedRef.current)
|
|
269
|
+
return;
|
|
270
|
+
if (!preferredEligible) {
|
|
271
|
+
if (value)
|
|
272
|
+
onChange('');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const current = accounts.find(a => a.accountUid === value);
|
|
276
|
+
if (!current || !current.isEligible) {
|
|
277
|
+
writeBillableAccountCookie(preferredEligible.accountUid);
|
|
278
|
+
onChange(preferredEligible.accountUid);
|
|
279
|
+
}
|
|
280
|
+
}, [isLoading, preferredEligible, accounts, value, onChange]);
|
|
281
|
+
const selectedAccount = useMemo(() => accounts.find(a => a.accountUid === value), [accounts, value]);
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
onSelectedAccountChange?.(selectedAccount);
|
|
284
|
+
}, [selectedAccount, onSelectedAccountChange]);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
onAccountsResolved?.({
|
|
287
|
+
accounts,
|
|
288
|
+
eligibleAccounts: eligibleBillable,
|
|
289
|
+
isLoading,
|
|
290
|
+
hasEligibleAccount,
|
|
291
|
+
});
|
|
292
|
+
}, [
|
|
293
|
+
accounts,
|
|
294
|
+
eligibleBillable,
|
|
295
|
+
isLoading,
|
|
296
|
+
hasEligibleAccount,
|
|
297
|
+
onAccountsResolved,
|
|
298
|
+
]);
|
|
299
|
+
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: {
|
|
300
|
+
display: 'inline-flex',
|
|
301
|
+
alignItems: 'center',
|
|
302
|
+
gap: 2,
|
|
303
|
+
}, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1 }, children: "Loading plan status..." })] })) : selectedAccount ? (_jsxs(Box, { sx: {
|
|
304
|
+
display: 'flex',
|
|
305
|
+
alignItems: 'center',
|
|
306
|
+
justifyContent: 'space-between',
|
|
307
|
+
width: '100%',
|
|
308
|
+
minWidth: 0,
|
|
309
|
+
gap: 2,
|
|
310
|
+
}, children: [_jsxs(Text, { sx: {
|
|
311
|
+
minWidth: 0,
|
|
312
|
+
overflow: 'hidden',
|
|
313
|
+
textOverflow: 'ellipsis',
|
|
314
|
+
whiteSpace: 'nowrap',
|
|
315
|
+
}, children: ["@", selectedAccount.accountName] }), _jsxs(Box, { sx: {
|
|
316
|
+
display: 'flex',
|
|
317
|
+
alignItems: 'center',
|
|
318
|
+
justifyContent: 'flex-end',
|
|
319
|
+
gap: 1,
|
|
320
|
+
flexShrink: 0,
|
|
321
|
+
}, children: [_jsx(Label, { size: "small", variant: "secondary", title: selectedAccount.accountType === 'organization'
|
|
322
|
+
? 'Organization'
|
|
323
|
+
: selectedAccount.accountType === 'team'
|
|
324
|
+
? 'Team'
|
|
325
|
+
: '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: {
|
|
326
|
+
display: 'inline-flex',
|
|
327
|
+
alignItems: 'center',
|
|
328
|
+
gap: 2,
|
|
329
|
+
}, children: [_jsx(Spinner, { size: "small" }), _jsx(Text, { sx: { fontSize: 1 }, children: "Loading plan status..." })] }) })) : !hasEligibleAccount ? (_jsx(ActionList.Item, { disabled: true, children: emptyMessage })) : ((() => {
|
|
330
|
+
const typeOrder = {
|
|
331
|
+
user: 0,
|
|
332
|
+
organization: 1,
|
|
333
|
+
team: 2,
|
|
334
|
+
};
|
|
335
|
+
const sorted = [...accounts].sort((a, b) => (typeOrder[a.accountType] ?? 99) -
|
|
336
|
+
(typeOrder[b.accountType] ?? 99));
|
|
337
|
+
return sorted.map((account, idx) => {
|
|
338
|
+
const prevType = idx > 0 ? sorted[idx - 1].accountType : undefined;
|
|
339
|
+
const showDivider = prevType !== undefined &&
|
|
340
|
+
prevType !== account.accountType;
|
|
341
|
+
return (_jsxs(Fragment, { children: [showDivider && _jsx(ActionList.Divider, {}), _jsxs(ActionList.Item, { selected: account.accountUid === value, disabled: !account.isEligible, onSelect: () => {
|
|
342
|
+
if (account.isEligible) {
|
|
343
|
+
handleAccountSelect(account.accountUid);
|
|
344
|
+
}
|
|
345
|
+
}, children: [_jsxs(Box, { sx: {
|
|
346
|
+
display: 'flex',
|
|
347
|
+
alignItems: 'center',
|
|
348
|
+
justifyContent: 'space-between',
|
|
349
|
+
width: '100%',
|
|
350
|
+
minWidth: 0,
|
|
351
|
+
gap: 2,
|
|
352
|
+
}, children: [_jsxs(Text, { sx: {
|
|
353
|
+
minWidth: 0,
|
|
354
|
+
overflow: 'hidden',
|
|
355
|
+
textOverflow: 'ellipsis',
|
|
356
|
+
whiteSpace: 'nowrap',
|
|
357
|
+
}, children: ["@", account.accountName] }), _jsxs(Box, { sx: {
|
|
358
|
+
display: 'flex',
|
|
359
|
+
alignItems: 'center',
|
|
360
|
+
justifyContent: 'flex-end',
|
|
361
|
+
gap: 1,
|
|
362
|
+
flexShrink: 0,
|
|
363
|
+
}, children: [_jsx(Label, { size: "small", variant: "secondary", title: account.accountType === 'organization'
|
|
364
|
+
? 'Organization'
|
|
365
|
+
: account.accountType === 'team'
|
|
366
|
+
? 'Team'
|
|
367
|
+
: 'User', children: account.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : account.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), !account.isEligible &&
|
|
368
|
+
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
|
|
369
|
+
? 'Eligible'
|
|
370
|
+
: account.accountType === 'team'
|
|
371
|
+
? 'Not eligible — no team credits allocated'
|
|
372
|
+
: 'Not eligible — activate a plan or add credits to use this account' })] })] }, account.accountUid));
|
|
373
|
+
});
|
|
374
|
+
})()) })] })] }) }), _jsx(FormControl.Caption, { children: caption })] }));
|
|
375
|
+
}
|
|
376
|
+
export default BillableAccountSelect;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './BillableAccountSelect';
|
|
@@ -33,6 +33,11 @@ export interface IDialogWrapperOptions<T> {
|
|
|
33
33
|
* The top level text for the dialog.
|
|
34
34
|
*/
|
|
35
35
|
title: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional async hook called before an accept button closes the dialog.
|
|
38
|
+
* Return false to keep the dialog open.
|
|
39
|
+
*/
|
|
40
|
+
onWillAccept: (result: Dialog.IResult<T>) => Promise<boolean | void> | boolean | void;
|
|
36
41
|
}
|
|
37
42
|
/**
|
|
38
43
|
* A primer dialog mimicking the JupyterLab dialog interface
|
|
@@ -45,6 +50,8 @@ export declare class JupyterDialog<T> extends ReactWidget {
|
|
|
45
50
|
protected buttons: Dialog.IButton[];
|
|
46
51
|
protected host: HTMLElement;
|
|
47
52
|
protected dialogTitle?: string;
|
|
53
|
+
protected onWillAccept?: (result: Dialog.IResult<T>) => Promise<boolean | void> | boolean | void;
|
|
54
|
+
private _pendingButtonIndex;
|
|
48
55
|
private _closing;
|
|
49
56
|
private _result;
|
|
50
57
|
/**
|
|
@@ -60,7 +67,7 @@ export declare class JupyterDialog<T> extends ReactWidget {
|
|
|
60
67
|
* @returns a promise that resolves with the result of the dialog.
|
|
61
68
|
*/
|
|
62
69
|
launch(): Promise<Dialog.IResult<T>>;
|
|
63
|
-
protected handleButton: (idx: number) => void
|
|
70
|
+
protected handleButton: (idx: number) => Promise<void>;
|
|
64
71
|
protected setButton: (button: Dialog.IButton) => void;
|
|
65
72
|
protected setChecked: (c: boolean) => void;
|
|
66
73
|
protected setValue: (v: T | Error) => void;
|
|
@@ -9,7 +9,7 @@ import { ReactWidget } from '@jupyterlab/ui-components';
|
|
|
9
9
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
10
10
|
import { Widget } from '@lumino/widgets';
|
|
11
11
|
import { FocusKeys } from '@primer/behaviors';
|
|
12
|
-
import { Checkbox, FormControl, useFocusZone } from '@primer/react';
|
|
12
|
+
import { Checkbox, FormControl, Spinner, useFocusZone } from '@primer/react';
|
|
13
13
|
import { Dialog as PrimerDialog, } from '@primer/react/experimental';
|
|
14
14
|
import { JupyterReactTheme } from '@datalayer/jupyter-react';
|
|
15
15
|
function DialogFooter(props) {
|
|
@@ -33,6 +33,8 @@ export class JupyterDialog extends ReactWidget {
|
|
|
33
33
|
buttons;
|
|
34
34
|
host;
|
|
35
35
|
dialogTitle;
|
|
36
|
+
onWillAccept;
|
|
37
|
+
_pendingButtonIndex = null;
|
|
36
38
|
_closing = new PromiseDelegate();
|
|
37
39
|
_result = {
|
|
38
40
|
button: null,
|
|
@@ -52,6 +54,7 @@ export class JupyterDialog extends ReactWidget {
|
|
|
52
54
|
Dialog.okButton(),
|
|
53
55
|
];
|
|
54
56
|
this.dialogTitle = options.title;
|
|
57
|
+
this.onWillAccept = options.onWillAccept;
|
|
55
58
|
}
|
|
56
59
|
_renderBody = (props) => (_jsx(PrimerDialog.Body, { children: createElement(this.body, {
|
|
57
60
|
...props,
|
|
@@ -61,7 +64,11 @@ export class JupyterDialog extends ReactWidget {
|
|
|
61
64
|
render() {
|
|
62
65
|
// TODO title color is enforced for JupyterLab.
|
|
63
66
|
// This may be fixed in the jupyter-react theme (Primer generates h1 for the dialog title).
|
|
64
|
-
return (_jsx(JupyterReactTheme, { children: _jsx(PrimerDialog, { title: _jsx("span", { style: { color: 'var(--fgColor-default)' }, children: this.dialogTitle }), onClose:
|
|
67
|
+
return (_jsx(JupyterReactTheme, { children: _jsx(PrimerDialog, { title: _jsx("span", { style: { color: 'var(--fgColor-default)' }, children: this.dialogTitle }), onClose: () => {
|
|
68
|
+
if (this._pendingButtonIndex === null) {
|
|
69
|
+
this.close();
|
|
70
|
+
}
|
|
71
|
+
}, renderBody: this._renderBody, renderFooter: this._renderFooter, footerButtons: this.buttons.map((but, idx) => {
|
|
65
72
|
const footerButton = {
|
|
66
73
|
buttonType: but.displayType === 'default'
|
|
67
74
|
? but.accept
|
|
@@ -71,8 +78,9 @@ export class JupyterDialog extends ReactWidget {
|
|
|
71
78
|
onClick: () => {
|
|
72
79
|
this.handleButton(idx);
|
|
73
80
|
},
|
|
74
|
-
content: but.label,
|
|
81
|
+
content: this._pendingButtonIndex === idx ? (_jsx(Spinner, { size: "small" })) : (but.label),
|
|
75
82
|
'aria-label': but.ariaLabel,
|
|
83
|
+
disabled: this._pendingButtonIndex !== null,
|
|
76
84
|
autoFocus: but.accept,
|
|
77
85
|
};
|
|
78
86
|
return footerButton;
|
|
@@ -88,8 +96,30 @@ export class JupyterDialog extends ReactWidget {
|
|
|
88
96
|
await this._closing.promise;
|
|
89
97
|
return this._result;
|
|
90
98
|
}
|
|
91
|
-
handleButton = (idx) => {
|
|
92
|
-
|
|
99
|
+
handleButton = async (idx) => {
|
|
100
|
+
if (this._pendingButtonIndex !== null) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const button = this.buttons[idx];
|
|
104
|
+
this.setButton(button);
|
|
105
|
+
if (button.accept && this.onWillAccept) {
|
|
106
|
+
this._pendingButtonIndex = idx;
|
|
107
|
+
this.update();
|
|
108
|
+
try {
|
|
109
|
+
const shouldClose = await this.onWillAccept(this._result);
|
|
110
|
+
if (shouldClose === false) {
|
|
111
|
+
this._pendingButtonIndex = null;
|
|
112
|
+
this.update();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this._pendingButtonIndex = null;
|
|
118
|
+
this.update();
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
this._pendingButtonIndex = null;
|
|
93
123
|
this.close();
|
|
94
124
|
};
|
|
95
125
|
setButton = (button) => {
|
|
@@ -107,6 +137,9 @@ export class JupyterDialog extends ReactWidget {
|
|
|
107
137
|
}
|
|
108
138
|
};
|
|
109
139
|
close = () => {
|
|
140
|
+
if (this._pendingButtonIndex !== null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
110
143
|
Widget.detach(this);
|
|
111
144
|
this._closing.resolve();
|
|
112
145
|
};
|
package/lib/components/index.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Principal – common, tunable display for an actor (user / team /
|
|
3
|
+
* organization). Combines a {@link PrincipalAvatar} with a
|
|
4
|
+
* {@link PrincipalDetailsOverlay} so all spots that need to show
|
|
5
|
+
* "avatar + clickable name with details overlay" can share a single
|
|
6
|
+
* component.
|
|
7
|
+
*/
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { PrincipalAvatarKind } from './PrincipalAvatar';
|
|
10
|
+
type PrincipalKind = PrincipalAvatarKind;
|
|
11
|
+
/**
|
|
12
|
+
* Normalised actor descriptor used by all caching resolvers. Views are
|
|
13
|
+
* expected to produce one of these out of their raw API data so the
|
|
14
|
+
* common component can render consistently.
|
|
15
|
+
*/
|
|
16
|
+
export type PrincipalDescriptor = {
|
|
17
|
+
kind: PrincipalKind;
|
|
18
|
+
uid?: string;
|
|
19
|
+
displayName: string;
|
|
20
|
+
handle?: string;
|
|
21
|
+
accountHandle?: string;
|
|
22
|
+
firstName?: string;
|
|
23
|
+
lastName?: string;
|
|
24
|
+
email?: string;
|
|
25
|
+
origin?: string;
|
|
26
|
+
avatarUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
export type PrincipalProps = {
|
|
29
|
+
principal: PrincipalDescriptor;
|
|
30
|
+
isAdmin?: boolean;
|
|
31
|
+
avatarSize?: number;
|
|
32
|
+
gap?: number;
|
|
33
|
+
square?: boolean;
|
|
34
|
+
sx?: any;
|
|
35
|
+
};
|
|
36
|
+
export declare const Principal: React.FC<PrincipalProps>;
|
|
37
|
+
export default Principal;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from '@datalayer/primer-addons';
|
|
3
|
+
import { useCache } from '../../hooks/index.js';
|
|
4
|
+
import { PrincipalAvatar } from './PrincipalAvatar.js';
|
|
5
|
+
import { PrincipalDetailsOverlay } from './PrincipalDetailsOverlay.js';
|
|
6
|
+
export const Principal = ({ principal, isAdmin = false, avatarSize = 20, gap = 2, square = false, sx, }) => {
|
|
7
|
+
const { useUser, useOrganization } = useCache();
|
|
8
|
+
const hydratedUserQuery = useUser(principal.kind === 'user' ? String(principal.uid || '') : '');
|
|
9
|
+
const hydratedOrgQuery = useOrganization(principal.kind === 'organization' ? String(principal.uid || '') : '');
|
|
10
|
+
const hydratedEntity = principal.kind === 'user'
|
|
11
|
+
? hydratedUserQuery.data
|
|
12
|
+
: principal.kind === 'organization'
|
|
13
|
+
? hydratedOrgQuery.data
|
|
14
|
+
: undefined;
|
|
15
|
+
const hydratedDisplayName = principal.kind === 'user'
|
|
16
|
+
? String(hydratedEntity?.displayName ||
|
|
17
|
+
[
|
|
18
|
+
hydratedEntity?.firstName,
|
|
19
|
+
hydratedEntity?.lastName,
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(' ') ||
|
|
23
|
+
'').trim()
|
|
24
|
+
: String(hydratedEntity?.displayName ||
|
|
25
|
+
hydratedEntity?.name ||
|
|
26
|
+
'').trim();
|
|
27
|
+
const resolvedPrincipal = {
|
|
28
|
+
...principal,
|
|
29
|
+
displayName: hydratedDisplayName ||
|
|
30
|
+
principal.displayName ||
|
|
31
|
+
principal.handle ||
|
|
32
|
+
principal.uid ||
|
|
33
|
+
'Unknown',
|
|
34
|
+
handle: principal.handle ||
|
|
35
|
+
String(hydratedEntity?.handle || '').trim() ||
|
|
36
|
+
undefined,
|
|
37
|
+
accountHandle: principal.accountHandle ||
|
|
38
|
+
String(hydratedEntity?.handle || '').trim() ||
|
|
39
|
+
undefined,
|
|
40
|
+
avatarUrl: principal.avatarUrl || hydratedEntity?.avatarUrl || undefined,
|
|
41
|
+
firstName: principal.firstName || hydratedEntity?.firstName || undefined,
|
|
42
|
+
lastName: principal.lastName || hydratedEntity?.lastName || undefined,
|
|
43
|
+
email: principal.email || hydratedEntity?.email || undefined,
|
|
44
|
+
origin: principal.origin || hydratedEntity?.origin || undefined,
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs(Box, { sx: {
|
|
47
|
+
display: 'inline-flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
gap,
|
|
50
|
+
minWidth: 0,
|
|
51
|
+
...sx,
|
|
52
|
+
}, children: [_jsx(PrincipalAvatar, { kind: resolvedPrincipal.kind, avatarUrl: resolvedPrincipal.avatarUrl, alt: resolvedPrincipal.displayName, size: avatarSize, square: square }), _jsx(PrincipalDetailsOverlay, { kind: resolvedPrincipal.kind, uid: resolvedPrincipal.uid, displayName: resolvedPrincipal.displayName, handle: resolvedPrincipal.handle, accountHandle: resolvedPrincipal.accountHandle, firstName: resolvedPrincipal.firstName, lastName: resolvedPrincipal.lastName, email: resolvedPrincipal.email, origin: resolvedPrincipal.origin, avatarUrl: resolvedPrincipal.avatarUrl, isAdmin: isAdmin })] }));
|
|
53
|
+
};
|
|
54
|
+
export default Principal;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type PrincipalAvatarKind = 'user' | 'team' | 'organization';
|
|
2
|
+
export type PrincipalAvatarProps = {
|
|
3
|
+
kind: PrincipalAvatarKind;
|
|
4
|
+
avatarUrl?: string;
|
|
5
|
+
alt?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
square?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function PrincipalAvatar({ kind, avatarUrl, alt, size, square, }: PrincipalAvatarProps): JSX.Element;
|
|
10
|
+
export default PrincipalAvatar;
|