@datalayer/core 1.0.21 → 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/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/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/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/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,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';
|
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;
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
import { Box, useColorPalette } from '@datalayer/primer-addons';
|
|
7
|
+
import { OrganizationIcon, PeopleIcon } from '@primer/octicons-react';
|
|
8
|
+
import { AlienIcon } from '@datalayer/icons-react';
|
|
9
|
+
import { DLAvatar } from '../avatars/index.js';
|
|
10
|
+
function hasRealAvatar(url) {
|
|
11
|
+
if (!url) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (url.startsWith('https://www.gravatar.com/avatar')) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
function getFallbackIconSize(size) {
|
|
20
|
+
return Math.max(12, Math.round(size * 0.62));
|
|
21
|
+
}
|
|
22
|
+
export function PrincipalAvatar({ kind, avatarUrl, alt, size = 20, square = false, }) {
|
|
23
|
+
const palette = useColorPalette();
|
|
24
|
+
if (kind === 'user' && hasRealAvatar(avatarUrl)) {
|
|
25
|
+
return (_jsx(DLAvatar, { src: avatarUrl, alt: alt || 'User', size: size, square: square }));
|
|
26
|
+
}
|
|
27
|
+
const iconSize = getFallbackIconSize(size);
|
|
28
|
+
const borderRadius = square ? 2 : '50%';
|
|
29
|
+
if (kind === 'user') {
|
|
30
|
+
return (_jsx(Box, { sx: {
|
|
31
|
+
width: size,
|
|
32
|
+
height: size,
|
|
33
|
+
display: 'inline-flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
justifyContent: 'center',
|
|
36
|
+
bg: 'accent.subtle',
|
|
37
|
+
borderRadius,
|
|
38
|
+
overflow: 'hidden',
|
|
39
|
+
'--datalayer-icon-fg': palette.primary,
|
|
40
|
+
}, "aria-label": alt || 'User', children: _jsx(AlienIcon, { size: iconSize }) }));
|
|
41
|
+
}
|
|
42
|
+
const Icon = kind === 'team' ? PeopleIcon : OrganizationIcon;
|
|
43
|
+
return (_jsx(Box, { sx: {
|
|
44
|
+
width: size,
|
|
45
|
+
height: size,
|
|
46
|
+
display: 'inline-flex',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'center',
|
|
49
|
+
bg: 'canvas.subtle',
|
|
50
|
+
borderRadius,
|
|
51
|
+
border: '1px solid',
|
|
52
|
+
borderColor: 'border.default',
|
|
53
|
+
}, "aria-label": alt || (kind === 'team' ? 'Team' : 'Organization'), children: _jsx(Icon, { size: iconSize }) }));
|
|
54
|
+
}
|
|
55
|
+
export default PrincipalAvatar;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type PrincipalDescriptor } from './Principal';
|
|
2
|
+
export type PrincipalBadgeInput = Omit<PrincipalDescriptor, 'displayName'> & {
|
|
3
|
+
displayName?: string;
|
|
4
|
+
};
|
|
5
|
+
type PrincipalBadgeProps = {
|
|
6
|
+
principal?: PrincipalBadgeInput;
|
|
7
|
+
showPrincipalLabel?: boolean;
|
|
8
|
+
showApplyingToText?: boolean;
|
|
9
|
+
showOriginLabel?: boolean;
|
|
10
|
+
principalLabel?: string;
|
|
11
|
+
isAdmin?: boolean;
|
|
12
|
+
sx?: any;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* PrincipalBadge — small inline pill that displays a resolved principal
|
|
16
|
+
* (user / organization / team). Falls back to the currently selected
|
|
17
|
+
* principal when no explicit `principal` prop is supplied.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PrincipalBadge: ({ principal: providedPrincipal, showPrincipalLabel, showApplyingToText, showOriginLabel, principalLabel, isAdmin, sx, }?: PrincipalBadgeProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export default PrincipalBadge;
|