@datalayer/core 1.1.48 → 1.1.50
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 +1 -1
- package/lib/components/auth/Login.js +46 -16
- package/lib/components/auth/Login.stories.js +5 -5
- package/lib/components/auth/LoginToken.d.ts +4 -0
- package/lib/components/auth/LoginToken.js +4 -4
- package/lib/components/avatars/UserAvatar.d.ts +5 -1
- package/lib/components/avatars/UserAvatar.js +4 -4
- package/lib/components/billing/BillingEntitySelect.js +10 -2
- package/lib/components/display/VisuallyHidden.d.ts +1 -1
- package/lib/components/iam/ExternalTokenSilentLogin.js +6 -2
- package/lib/hooks/useJupyterLabTheme.d.ts +1 -1
- package/lib/hooks/useToast.d.ts +1 -1
- package/lib/hooks/useToast.js +30 -15
- package/lib/state/substates/CoreState.js +2 -2
- package/lib/utils/Jwt.d.ts +0 -6
- package/lib/utils/Jwt.js +8 -1
- package/lib/views/iam/SignInSimple.d.ts +57 -1
- package/lib/views/iam/SignInSimple.js +222 -72
- package/lib/views/profile/UserBadge.js +6 -3
- package/package.json +2 -2
- package/lib/components/tokens/index.d.ts +0 -1
- package/lib/components/tokens/index.js +0 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sponsors/datalayer)
|
|
4
4
|
|
|
5
|
-
# ☰ Datalayer Core
|
|
5
|
+
# ☰ 〇 Datalayer Core
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Python and Typescript libraries for Datalayer</strong>
|
|
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { useEffect, useState } from 'react';
|
|
7
7
|
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
8
8
|
import { EyeIcon, EyeClosedIcon, MarkGithubIcon } from '@primer/octicons-react';
|
|
9
|
-
import {
|
|
9
|
+
import { GoogleIcon } from '@datalayer/icons-react';
|
|
10
|
+
import { Button, FormControl, Heading, Link, PageLayout, Spinner, TextInput, } from '@primer/react';
|
|
10
11
|
import { Box } from '@datalayer/primer-addons';
|
|
11
12
|
import { asUser, IAMProvidersSpecs } from '../../models/index.js';
|
|
12
13
|
import { useIAMStore } from '../../state/index.js';
|
|
@@ -14,8 +15,6 @@ import { CenteredSpinner } from '../display/index.js';
|
|
|
14
15
|
import { isInsideJupyterLab, validateLength } from '../../utils/index.js';
|
|
15
16
|
import { useNavigate, useCache, useToast, useIAM } from '../../hooks/index.js';
|
|
16
17
|
import { LoginToken } from './LoginToken.js';
|
|
17
|
-
/** Inline Google "G" icon for the login button. */
|
|
18
|
-
const GoogleIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 48 48", children: [_jsx("path", { fill: "#EA4335", d: "M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" }), _jsx("path", { fill: "#4285F4", d: "M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" }), _jsx("path", { fill: "#FBBC05", d: "M10.53 28.59a14.5 14.5 0 0 1 0-9.18l-7.98-6.19a24.1 24.1 0 0 0 0 21.56l7.98-6.19z" }), _jsx("path", { fill: "#34A853", d: "M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" })] }));
|
|
19
18
|
export const Login = (props) => {
|
|
20
19
|
const { heading, homeRoute, loginRoute, showEmailLogin = true, showGitHubLogin = true, showGoogleLogin = true, showTokenLogin = true, } = props;
|
|
21
20
|
const { useLogin, useOAuth2AuthorizationURL } = useCache({ loginRoute });
|
|
@@ -36,19 +35,20 @@ export const Login = (props) => {
|
|
|
36
35
|
password: undefined,
|
|
37
36
|
});
|
|
38
37
|
const [passwordVisibility, setPasswordVisibility] = useState(false);
|
|
38
|
+
const [pendingOAuthProvider, setPendingOAuthProvider] = useState(null);
|
|
39
|
+
const buildCallbackURI = (iamProvider) => isInsideJupyterLab()
|
|
40
|
+
? URLExt.join(PageConfig.getBaseUrl(), iamProvider.oauth2CallbackServerRoute)
|
|
41
|
+
: location.protocol +
|
|
42
|
+
'//' +
|
|
43
|
+
location.hostname +
|
|
44
|
+
':' +
|
|
45
|
+
location.port +
|
|
46
|
+
iamProvider.oauth2CallbackUIRoute;
|
|
39
47
|
useEffect(() => {
|
|
40
48
|
const initIAMProvider = (iamProvider) => {
|
|
41
|
-
const callbackURI = isInsideJupyterLab()
|
|
42
|
-
? URLExt.join(PageConfig.getBaseUrl(), iamProvider.oauth2CallbackServerRoute)
|
|
43
|
-
: location.protocol +
|
|
44
|
-
'//' +
|
|
45
|
-
location.hostname +
|
|
46
|
-
':' +
|
|
47
|
-
location.port +
|
|
48
|
-
iamProvider.oauth2CallbackUIRoute;
|
|
49
49
|
const queryArgs = {
|
|
50
50
|
provider: iamProvider.name,
|
|
51
|
-
callback_uri:
|
|
51
|
+
callback_uri: buildCallbackURI(iamProvider),
|
|
52
52
|
};
|
|
53
53
|
/*
|
|
54
54
|
const xsrfTokenMatch = document.cookie.match('\\b_xsrf=([^;]*)\\b');
|
|
@@ -142,6 +142,32 @@ export const Login = (props) => {
|
|
|
142
142
|
},
|
|
143
143
|
});
|
|
144
144
|
};
|
|
145
|
+
const startOAuthSignIn = (providerSpec) => {
|
|
146
|
+
if (getOAuth2URLMutation.isPending) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
setPendingOAuthProvider(providerSpec.name);
|
|
150
|
+
const queryArgs = {
|
|
151
|
+
provider: providerSpec.name,
|
|
152
|
+
callback_uri: buildCallbackURI(providerSpec),
|
|
153
|
+
};
|
|
154
|
+
getOAuth2URLMutation.mutate(queryArgs, {
|
|
155
|
+
onSuccess: authUrl => {
|
|
156
|
+
if (authUrl) {
|
|
157
|
+
window.location.assign(authUrl);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
setPendingOAuthProvider(null);
|
|
161
|
+
enqueueToast('Unable to start social sign-in.', { variant: 'warning' });
|
|
162
|
+
},
|
|
163
|
+
onError: () => {
|
|
164
|
+
setPendingOAuthProvider(null);
|
|
165
|
+
enqueueToast('Unable to start social sign-in.', { variant: 'error' });
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
const socialAuthDisabled = getOAuth2URLMutation.isPending;
|
|
170
|
+
const socialButtonsDisabled = socialAuthDisabled || loading;
|
|
145
171
|
useEffect(() => {
|
|
146
172
|
setValidationResult({
|
|
147
173
|
...validationResult,
|
|
@@ -157,9 +183,9 @@ export const Login = (props) => {
|
|
|
157
183
|
: 'Your password may not be empty.',
|
|
158
184
|
});
|
|
159
185
|
}, [formValues]);
|
|
160
|
-
return (_jsxs(PageLayout, { containerWidth: "medium", padding: "normal", style: { overflow: 'visible', minHeight: 'calc(100vh - 45px)' }, children: [_jsx(PageLayout.Header, { children: _jsx(Heading, { children: heading || '
|
|
186
|
+
return (_jsxs(PageLayout, { containerWidth: "medium", padding: "normal", style: { overflow: 'visible', minHeight: 'calc(100vh - 45px)' }, children: [_jsx(PageLayout.Header, { children: _jsx(Heading, { children: heading || 'Sign in to Datalayer' }) }), _jsx(PageLayout.Content, { children: loadingWithToken < 0 ? (_jsx(_Fragment, { children: _jsxs(Box, { display: "flex", children: [showEmailLogin && (_jsxs(Box, { sx: { label: { marginTop: 2 }, paddingRight: '10%' }, children: [_jsx(Box, { mt: 5, children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "Your username" }), _jsx(TextInput, { autoFocus: true, placeholder: "Your username", value: formValues.handle, disabled: loading, onChange: handleHandleChange, onKeyDown: handleKeyDown }), validationResult.handle && (_jsx(FormControl.Validation, { variant: validationResult.handle ? 'error' : 'success', children: validationResult.handle }))] }) }), _jsx(Box, { children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "Your password" }), _jsx(TextInput, { placeholder: "Your password", type: passwordVisibility ? 'text' : 'password', value: formValues.password, disabled: loading, onChange: handlePasswordChange, onKeyDown: handleKeyDown, trailingAction: _jsx(TextInput.Action, { onClick: () => {
|
|
161
187
|
setPasswordVisibility(!passwordVisibility);
|
|
162
|
-
}, icon: passwordVisibility ? EyeClosedIcon : EyeIcon, "aria-label": passwordVisibility
|
|
188
|
+
}, disabled: loading, icon: passwordVisibility ? EyeClosedIcon : EyeIcon, "aria-label": passwordVisibility
|
|
163
189
|
? 'Hide password'
|
|
164
190
|
: 'Show password', sx: { color: 'var(--fgColor-muted)' } }), sx: { overflow: 'visible' } }), validationResult.password && (_jsx(FormControl.Validation, { variant: validationResult.password ? 'error' : 'success', children: validationResult.password }))] }) }), _jsxs(Box, { mt: 5, children: [_jsx(Button, { variant: "primary", disabled: loading ||
|
|
165
191
|
validationResult.handle !== '' ||
|
|
@@ -168,7 +194,11 @@ export const Login = (props) => {
|
|
|
168
194
|
: heading
|
|
169
195
|
? 'Login with Datalayer'
|
|
170
196
|
: 'Login' }), _jsx(Box, { pt: 6 }), _jsx(Link, { href: "https://datalayer.ai/password", target: "_blank", children: "Forgot password?" })] })] })), _jsx(Box, { children: _jsxs(Box, { display: "flex", flexDirection: "column", sx: { margin: 'auto' }, children: [showGitHubLogin &&
|
|
171
|
-
iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name] && (_jsx(Button, { leadingVisual:
|
|
172
|
-
|
|
197
|
+
iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name] && (_jsx(Button, { leadingVisual: pendingOAuthProvider === IAMProvidersSpecs.GitHub.name
|
|
198
|
+
? () => _jsx(Spinner, { size: "small" })
|
|
199
|
+
: MarkGithubIcon, onClick: () => startOAuthSignIn(IAMProvidersSpecs.GitHub), disabled: socialButtonsDisabled, style: { margin: '10px 0' }, children: "Login with GitHub" })), showGoogleLogin &&
|
|
200
|
+
iamProvidersAuthorizationURL[IAMProvidersSpecs.Google.name] && (_jsx(Button, { leadingVisual: pendingOAuthProvider === IAMProvidersSpecs.Google.name
|
|
201
|
+
? () => _jsx(Spinner, { size: "small" })
|
|
202
|
+
: GoogleIcon, onClick: () => startOAuthSignIn(IAMProvidersSpecs.Google), disabled: socialButtonsDisabled, style: { margin: '10px 0' }, children: "Login with Google" })), showTokenLogin && (_jsx(LoginToken, { homeRoute: homeRoute, disabled: loading, style: { margin: '10px 0' } }))] }) })] }) })) : loadingWithToken ? (_jsx(CenteredSpinner, { message: "Checking authentication\u2026" })) : (_jsx(_Fragment, {})) })] }));
|
|
173
203
|
};
|
|
174
204
|
export default Login;
|
|
@@ -28,7 +28,7 @@ export default meta;
|
|
|
28
28
|
*/
|
|
29
29
|
export const Default = {
|
|
30
30
|
args: {
|
|
31
|
-
heading: '
|
|
31
|
+
heading: 'Sign in to Datalayer',
|
|
32
32
|
homeRoute: '/home',
|
|
33
33
|
loginRoute: '/login',
|
|
34
34
|
showEmailLogin: true,
|
|
@@ -41,7 +41,7 @@ export const Default = {
|
|
|
41
41
|
*/
|
|
42
42
|
export const EmailOnly = {
|
|
43
43
|
args: {
|
|
44
|
-
heading: '
|
|
44
|
+
heading: 'Sign in to Datalayer',
|
|
45
45
|
homeRoute: '/home',
|
|
46
46
|
loginRoute: '/login',
|
|
47
47
|
showEmailLogin: true,
|
|
@@ -54,7 +54,7 @@ export const EmailOnly = {
|
|
|
54
54
|
*/
|
|
55
55
|
export const OAuthOnly = {
|
|
56
56
|
args: {
|
|
57
|
-
heading: '
|
|
57
|
+
heading: 'Sign in to Datalayer',
|
|
58
58
|
homeRoute: '/home',
|
|
59
59
|
loginRoute: '/login',
|
|
60
60
|
showEmailLogin: false,
|
|
@@ -67,7 +67,7 @@ export const OAuthOnly = {
|
|
|
67
67
|
*/
|
|
68
68
|
export const TokenOnly = {
|
|
69
69
|
args: {
|
|
70
|
-
heading: '
|
|
70
|
+
heading: 'Sign in to Datalayer',
|
|
71
71
|
homeRoute: '/home',
|
|
72
72
|
loginRoute: '/login',
|
|
73
73
|
showEmailLogin: false,
|
|
@@ -93,7 +93,7 @@ export const CustomHeading = {
|
|
|
93
93
|
*/
|
|
94
94
|
export const WithPasswordReset = {
|
|
95
95
|
args: {
|
|
96
|
-
heading: '
|
|
96
|
+
heading: 'Sign in to Datalayer',
|
|
97
97
|
homeRoute: '/home',
|
|
98
98
|
loginRoute: '/login',
|
|
99
99
|
passwordRoute: '/password-reset',
|
|
@@ -3,6 +3,10 @@ export interface ILoginTokenProps {
|
|
|
3
3
|
* Home page route to navigate to after successful login
|
|
4
4
|
*/
|
|
5
5
|
homeRoute: string;
|
|
6
|
+
/**
|
|
7
|
+
* Disable token login controls (e.g., while another login flow is submitting)
|
|
8
|
+
*/
|
|
9
|
+
disabled?: boolean;
|
|
6
10
|
/**
|
|
7
11
|
* CSS style object
|
|
8
12
|
*/
|
|
@@ -15,7 +15,7 @@ import { useIAMStore } from '../../state/index.js';
|
|
|
15
15
|
* Provides a simple button that expands to show a token input form
|
|
16
16
|
*/
|
|
17
17
|
export const LoginToken = (props) => {
|
|
18
|
-
const { homeRoute, style } = props;
|
|
18
|
+
const { homeRoute, disabled = false, style } = props;
|
|
19
19
|
const [showForm, setShowForm] = useState(false);
|
|
20
20
|
const [token, setToken] = useState('');
|
|
21
21
|
const [loading, setLoading] = useState(false);
|
|
@@ -45,9 +45,9 @@ export const LoginToken = (props) => {
|
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
if (!showForm) {
|
|
48
|
-
return (_jsx(Button, { onClick: () => setShowForm(true), style: style, children: "Login with Token" }));
|
|
48
|
+
return (_jsx(Button, { onClick: () => setShowForm(true), style: style, disabled: disabled, children: "Login with Token" }));
|
|
49
49
|
}
|
|
50
|
-
return (_jsxs(Box, { style: style, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Your Authentication Token" }), _jsx(TextInput, { block: true, placeholder: "Paste your authentication token here", value: token, onChange: e => setToken(e.target.value), onKeyDown: e => {
|
|
50
|
+
return (_jsxs(Box, { style: style, children: [_jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Your Authentication Token" }), _jsx(TextInput, { block: true, placeholder: "Paste your authentication token here", value: token, disabled: disabled || loading, onChange: e => setToken(e.target.value), onKeyDown: e => {
|
|
51
51
|
if (e.key === 'Enter') {
|
|
52
52
|
handleLogin();
|
|
53
53
|
}
|
|
@@ -55,7 +55,7 @@ export const LoginToken = (props) => {
|
|
|
55
55
|
setShowForm(false);
|
|
56
56
|
setToken('');
|
|
57
57
|
}
|
|
58
|
-
} }), _jsx(FormControl.Caption, { children: "Enter your Datalayer authentication token to log in" })] }), _jsxs(Box, { mt: 3, display: "flex", justifyContent: "flex-start", children: [_jsx(Button, { variant: "primary", onClick: handleLogin, disabled: loading || !token.trim(), sx: { mr: 2 }, children: loading ? 'Authenticating...' : 'Login' }), _jsx(Button, { variant: "default", onClick: () => {
|
|
58
|
+
} }), _jsx(FormControl.Caption, { children: "Enter your Datalayer authentication token to log in" })] }), _jsxs(Box, { mt: 3, display: "flex", justifyContent: "flex-start", children: [_jsx(Button, { variant: "primary", onClick: handleLogin, disabled: disabled || loading || !token.trim(), sx: { mr: 2 }, children: loading ? 'Authenticating...' : 'Login' }), _jsx(Button, { variant: "default", disabled: disabled || loading, onClick: () => {
|
|
59
59
|
setShowForm(false);
|
|
60
60
|
setToken('');
|
|
61
61
|
}, children: "Cancel" })] })] }));
|
|
@@ -11,6 +11,10 @@ export type UserAvatarProps = {
|
|
|
11
11
|
square?: boolean;
|
|
12
12
|
/** Fallback icon size. Defaults to ~48% of `size`. */
|
|
13
13
|
iconSize?: number;
|
|
14
|
+
/** Optional background color override for the default (non-photo) avatar. */
|
|
15
|
+
fallbackBackground?: string;
|
|
16
|
+
/** Optional icon foreground color override for the default avatar. */
|
|
17
|
+
fallbackForeground?: string;
|
|
14
18
|
};
|
|
15
|
-
export declare const UserAvatar: ({ avatarUrl, size, square, iconSize, }: UserAvatarProps) => JSX.Element;
|
|
19
|
+
export declare const UserAvatar: ({ avatarUrl, size, square, iconSize, fallbackBackground, fallbackForeground, }: UserAvatarProps) => JSX.Element;
|
|
16
20
|
export default UserAvatar;
|
|
@@ -27,7 +27,7 @@ export function hasRealAvatar(url) {
|
|
|
27
27
|
}
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
|
-
export const UserAvatar = ({ avatarUrl, size = 100, square = true, iconSize, }) => {
|
|
30
|
+
export const UserAvatar = ({ avatarUrl, size = 100, square = true, iconSize, fallbackBackground, fallbackForeground, }) => {
|
|
31
31
|
const palette = useColorPalette();
|
|
32
32
|
if (hasRealAvatar(avatarUrl)) {
|
|
33
33
|
return _jsx(DLAvatar, { square: square, src: avatarUrl, size: size });
|
|
@@ -37,11 +37,11 @@ export const UserAvatar = ({ avatarUrl, size = 100, square = true, iconSize, })
|
|
|
37
37
|
width: size,
|
|
38
38
|
height: size,
|
|
39
39
|
borderRadius: square ? 2 : '50%',
|
|
40
|
-
bg: 'accent.subtle',
|
|
40
|
+
bg: fallbackBackground || 'accent.subtle',
|
|
41
41
|
display: 'flex',
|
|
42
42
|
alignItems: 'center',
|
|
43
43
|
justifyContent: 'center',
|
|
44
|
-
'--datalayer-icon-fg': palette.primary,
|
|
45
|
-
}, children: _jsx(AlienIcon, { size: resolvedIconSize, themed: true,
|
|
44
|
+
'--datalayer-icon-fg': fallbackForeground || palette.primary,
|
|
45
|
+
}, children: _jsx(AlienIcon, { size: resolvedIconSize, themed: true, colormode: true }) }));
|
|
46
46
|
};
|
|
47
47
|
export default UserAvatar;
|
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
* resolved account via `onSelectedAccountChange`.
|
|
18
18
|
*/
|
|
19
19
|
import { useCallback, useEffect, useMemo, useRef, Fragment } from 'react';
|
|
20
|
-
import { ActionList, ActionMenu, Button,
|
|
20
|
+
import { ActionList, ActionMenu, Button, FormControl, Label, Spinner, Text, } from '@primer/react';
|
|
21
21
|
import { OrganizationIcon, PeopleIcon, PersonIcon, } from '@primer/octicons-react';
|
|
22
22
|
import { Box } from '@datalayer/primer-addons';
|
|
23
23
|
import { useCache } from '../../hooks/useCache.js';
|
|
@@ -324,7 +324,15 @@ export function BillingEntitySelect({ value, onChange, onSelectedAccountChange,
|
|
|
324
324
|
? 'Organization'
|
|
325
325
|
: selectedAccount.accountType === 'team'
|
|
326
326
|
? 'Team'
|
|
327
|
-
: 'User', children: selectedAccount.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : selectedAccount.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), _jsx(Label, { size: "small", variant: selectedAccount.isPaidPlan ? 'success' : 'attention', children: formatBillingEntityPlanLabel(selectedAccount.planName) })] })] })) : !hasEligibleAccount ? (emptyMessage) : ('Select a billing entity') }) }), _jsxs(ActionMenu.Overlay, { width: "large", children: [_jsx(Box, { sx: { p: 2 }, children: _jsx(
|
|
327
|
+
: 'User', children: selectedAccount.accountType === 'organization' ? (_jsx(OrganizationIcon, { size: 12 })) : selectedAccount.accountType === 'team' ? (_jsx(PeopleIcon, { size: 12 })) : (_jsx(PersonIcon, { size: 12 })) }), _jsx(Label, { size: "small", variant: selectedAccount.isPaidPlan ? 'success' : 'attention', children: formatBillingEntityPlanLabel(selectedAccount.planName) })] })] })) : !hasEligibleAccount ? (emptyMessage) : ('Select a billing entity') }) }), _jsxs(ActionMenu.Overlay, { width: "large", children: [_jsx(Box, { sx: { p: 2 }, children: _jsx(Box, { sx: {
|
|
328
|
+
borderRadius: 2,
|
|
329
|
+
border: '1px solid',
|
|
330
|
+
borderColor: 'border.default',
|
|
331
|
+
bg: 'canvas.subtle',
|
|
332
|
+
color: 'fg.muted',
|
|
333
|
+
px: 3,
|
|
334
|
+
py: 2,
|
|
335
|
+
}, children: _jsx(Text, { sx: { fontSize: 1, lineHeight: 1.5 }, children: flashMessage }) }) }), _jsx(ActionList, { selectionVariant: "single", children: isLoading ? (_jsx(ActionList.Item, { disabled: true, children: _jsxs(Box, { sx: {
|
|
328
336
|
display: 'inline-flex',
|
|
329
337
|
alignItems: 'center',
|
|
330
338
|
gap: 2,
|
|
@@ -2,4 +2,4 @@ import { SxProp } from '@primer/react';
|
|
|
2
2
|
export interface IVisuallyHiddenProps {
|
|
3
3
|
isVisible?: boolean;
|
|
4
4
|
}
|
|
5
|
-
export declare const VisuallyHidden: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components").
|
|
5
|
+
export declare const VisuallyHidden: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, IVisuallyHiddenProps & SxProp>> & string;
|
|
@@ -3,10 +3,12 @@ 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 { useEffect } from 'react';
|
|
6
|
+
import { useContext, useEffect } from 'react';
|
|
7
|
+
import { QueryClient, QueryClientContext, QueryClientProvider, } from '@tanstack/react-query';
|
|
7
8
|
import { CenteredSpinner } from '../../components/display/index.js';
|
|
8
9
|
import { useIAMStore } from '../../state/index.js';
|
|
9
10
|
import { useToast, useIAM } from '../../hooks/index.js';
|
|
11
|
+
const externalTokenQueryClient = new QueryClient();
|
|
10
12
|
const ExternalTokenSilentLoginRoute = (props) => {
|
|
11
13
|
const { message } = props;
|
|
12
14
|
const { loginAndNavigate } = useIAM();
|
|
@@ -29,7 +31,9 @@ const ExternalTokenSilentLoginRoute = (props) => {
|
|
|
29
31
|
return _jsx(CenteredSpinner, { size: "small", message: message });
|
|
30
32
|
};
|
|
31
33
|
export const ExternalTokenSilentLogin = (props) => {
|
|
34
|
+
const queryClient = useContext(QueryClientContext);
|
|
32
35
|
// No navigation provider needed anymore - auto-detection works without it
|
|
33
|
-
|
|
36
|
+
const content = _jsx(ExternalTokenSilentLoginRoute, { ...props });
|
|
37
|
+
return queryClient ? (content) : (_jsx(QueryClientProvider, { client: externalTokenQueryClient, children: content }));
|
|
34
38
|
};
|
|
35
39
|
export default ExternalTokenSilentLogin;
|
package/lib/hooks/useToast.d.ts
CHANGED
package/lib/hooks/useToast.js
CHANGED
|
@@ -5,10 +5,25 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { toast } from 'react-toastify';
|
|
8
|
-
import { Notification } from '@jupyterlab/apputils';
|
|
9
8
|
import { Button } from '@primer/react';
|
|
10
|
-
import { JupyterReactTheme } from '@datalayer/jupyter-react';
|
|
11
9
|
import { isInsideJupyterLab } from '../utils/index.js';
|
|
10
|
+
// `@datalayer/jupyter-react` and `@jupyterlab/apputils` are heavy (they pull
|
|
11
|
+
// JupyterLab / CodeMirror / lumino) and are only needed when running INSIDE
|
|
12
|
+
// JupyterLab. They are lazily loaded here so the standalone bundle (landing app)
|
|
13
|
+
// never puts JupyterLab on its critical path.
|
|
14
|
+
const JupyterReactTheme = React.lazy(() => import('@datalayer/jupyter-react').then(m => ({
|
|
15
|
+
default: m.JupyterReactTheme,
|
|
16
|
+
})));
|
|
17
|
+
// Runtime handle to the JupyterLab Notification API. Populated only when running
|
|
18
|
+
// inside JupyterLab (where @jupyterlab/apputils is already a shared singleton),
|
|
19
|
+
// so standalone builds never eagerly import it. Callers fall back to
|
|
20
|
+
// react-toastify until (and unless) this resolves.
|
|
21
|
+
let NotificationApi;
|
|
22
|
+
if (isInsideJupyterLab()) {
|
|
23
|
+
void import('@jupyterlab/apputils').then(m => {
|
|
24
|
+
NotificationApi = m.Notification;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
12
27
|
const TOAST_POSITION = 'bottom-right';
|
|
13
28
|
const THEME_STORAGE_KEY = 'datalayer-theme';
|
|
14
29
|
function resolveToastTheme() {
|
|
@@ -62,7 +77,7 @@ function ToastButton({ action, closeToast }) {
|
|
|
62
77
|
*/
|
|
63
78
|
function createContent(message, closeHandler, actions) {
|
|
64
79
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "jp-toast-message", children: message.split('\n').map((part, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx("br", {}) : null, part] }, `part-${index}`))) }), (actions?.length ?? 0) > 0 && (_jsxs("div", { className: "jp-toast-buttonBar", children: [_jsx("div", { className: "jp-toast-spacer" }), actions.map((action, idx) => {
|
|
65
|
-
return (_jsx(JupyterReactTheme, { children: _jsx(ToastButton, { action: action, closeToast: closeHandler }, 'button-' + idx)
|
|
80
|
+
return (_jsx(React.Suspense, { fallback: null, children: _jsx(JupyterReactTheme, { children: _jsx(ToastButton, { action: action, closeToast: closeHandler }) }) }, 'button-' + idx));
|
|
66
81
|
})] }))] }));
|
|
67
82
|
}
|
|
68
83
|
export const useToast = () => {
|
|
@@ -73,8 +88,8 @@ export const useToast = () => {
|
|
|
73
88
|
const theme = resolveToastTheme();
|
|
74
89
|
switch (options.variant) {
|
|
75
90
|
case 'info': {
|
|
76
|
-
return insideJupyterLab
|
|
77
|
-
?
|
|
91
|
+
return insideJupyterLab && NotificationApi
|
|
92
|
+
? NotificationApi.info(message, {
|
|
78
93
|
autoClose: autoClose ?? 5000,
|
|
79
94
|
actions,
|
|
80
95
|
})
|
|
@@ -84,8 +99,8 @@ export const useToast = () => {
|
|
|
84
99
|
}, actions), { autoClose, position: TOAST_POSITION, theme });
|
|
85
100
|
}
|
|
86
101
|
case 'success': {
|
|
87
|
-
return insideJupyterLab
|
|
88
|
-
?
|
|
102
|
+
return insideJupyterLab && NotificationApi
|
|
103
|
+
? NotificationApi.success(message, {
|
|
89
104
|
autoClose: autoClose ?? 5000,
|
|
90
105
|
actions,
|
|
91
106
|
})
|
|
@@ -95,8 +110,8 @@ export const useToast = () => {
|
|
|
95
110
|
}, actions), { autoClose, position: TOAST_POSITION, theme });
|
|
96
111
|
}
|
|
97
112
|
case 'warning': {
|
|
98
|
-
return insideJupyterLab
|
|
99
|
-
?
|
|
113
|
+
return insideJupyterLab && NotificationApi
|
|
114
|
+
? NotificationApi.warning(message, {
|
|
100
115
|
autoClose: autoClose ?? false,
|
|
101
116
|
actions,
|
|
102
117
|
})
|
|
@@ -110,8 +125,8 @@ export const useToast = () => {
|
|
|
110
125
|
});
|
|
111
126
|
}
|
|
112
127
|
case 'error': {
|
|
113
|
-
return insideJupyterLab
|
|
114
|
-
?
|
|
128
|
+
return insideJupyterLab && NotificationApi
|
|
129
|
+
? NotificationApi.error(message, {
|
|
115
130
|
autoClose: autoClose ?? false,
|
|
116
131
|
actions,
|
|
117
132
|
})
|
|
@@ -128,8 +143,8 @@ export const useToast = () => {
|
|
|
128
143
|
};
|
|
129
144
|
const trackAsyncTask = (promise, options) => {
|
|
130
145
|
const theme = resolveToastTheme();
|
|
131
|
-
return insideJupyterLab
|
|
132
|
-
?
|
|
146
|
+
return insideJupyterLab && NotificationApi
|
|
147
|
+
? NotificationApi.promise(promise, options)
|
|
133
148
|
: toast.promise(promise, {
|
|
134
149
|
error: { render: options.error.message, ...options.error.options },
|
|
135
150
|
pending: options.pending.message,
|
|
@@ -158,8 +173,8 @@ export const useToast = () => {
|
|
|
158
173
|
* @param id Toast id
|
|
159
174
|
*/
|
|
160
175
|
dismiss: (id) => {
|
|
161
|
-
if (insideJupyterLab) {
|
|
162
|
-
|
|
176
|
+
if (insideJupyterLab && NotificationApi) {
|
|
177
|
+
NotificationApi.dismiss(id);
|
|
163
178
|
}
|
|
164
179
|
else {
|
|
165
180
|
toast.dismiss(id);
|
|
@@ -34,8 +34,8 @@ let initialConfiguration = {
|
|
|
34
34
|
supportUrl: 'https://prod1.datalayer.run',
|
|
35
35
|
launcher: {
|
|
36
36
|
category: 'Datalayer',
|
|
37
|
-
name: 'Datalayer
|
|
38
|
-
icon:
|
|
37
|
+
name: 'Datalayer',
|
|
38
|
+
icon: null,
|
|
39
39
|
rank: 1,
|
|
40
40
|
},
|
|
41
41
|
brand: {
|
package/lib/utils/Jwt.d.ts
CHANGED
package/lib/utils/Jwt.js
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight JWT payload utilities.
|
|
7
|
+
*
|
|
8
|
+
* Decodes a JWT without verification (client-side display only).
|
|
9
|
+
* Never use for security-critical checks.
|
|
10
|
+
*/
|
|
11
|
+
import { asDisplayName } from './Name.js';
|
|
5
12
|
// ── Utilities ─────────────────────────────────────────────────────
|
|
6
13
|
/**
|
|
7
14
|
* Decode the payload of a JWT without verifying the signature.
|
|
@@ -39,6 +46,6 @@ export function getDatalayerJwtUser(token) {
|
|
|
39
46
|
export function getDatalayerDisplayName(user, fallback = '') {
|
|
40
47
|
if (!user)
|
|
41
48
|
return fallback;
|
|
42
|
-
const full =
|
|
49
|
+
const full = asDisplayName(user.firstName ?? '', user.lastName ?? '').trim();
|
|
43
50
|
return full || user.handle || fallback;
|
|
44
51
|
}
|
|
@@ -13,7 +13,7 @@ export interface SignInSimpleProps {
|
|
|
13
13
|
* Called after a successful login with the JWT and the user handle.
|
|
14
14
|
* Typically used to store credentials in a Zustand / context store.
|
|
15
15
|
*/
|
|
16
|
-
onSignIn
|
|
16
|
+
onSignIn?: (token: string, handle: string) => void;
|
|
17
17
|
/**
|
|
18
18
|
* Called when the user authenticates with an API key.
|
|
19
19
|
* If not provided the "Sign In with an API Key" button is hidden.
|
|
@@ -74,6 +74,62 @@ export interface SignInSimpleProps {
|
|
|
74
74
|
* - `null`/empty disables forwarding any callback navigation target.
|
|
75
75
|
*/
|
|
76
76
|
socialSignInNavigationTarget?: string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Disable all interactive controls (useful when embedded in documentation).
|
|
79
|
+
*/
|
|
80
|
+
asDoc?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Hide header title and description area.
|
|
83
|
+
*/
|
|
84
|
+
hideHero?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Optional contextual heading shown above auth actions.
|
|
87
|
+
*/
|
|
88
|
+
calloutTitle?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Optional contextual description shown below the callout title.
|
|
91
|
+
*/
|
|
92
|
+
calloutDescription?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Label for password-auth toggle button.
|
|
95
|
+
*/
|
|
96
|
+
passwordToggleLabel?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Sign-up section heading text.
|
|
99
|
+
*/
|
|
100
|
+
signUpTitle?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Sign-up section descriptive text.
|
|
103
|
+
*/
|
|
104
|
+
signUpDescription?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Sign-up button label.
|
|
107
|
+
*/
|
|
108
|
+
signUpLabel?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Sign-up URL opened when clicking the sign-up button.
|
|
111
|
+
*/
|
|
112
|
+
signUpHref?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Forgot-password URL for password form.
|
|
115
|
+
*/
|
|
116
|
+
forgotPasswordHref?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Forgot-password link label.
|
|
119
|
+
*/
|
|
120
|
+
forgotPasswordLabel?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Show forgot-password link in password form.
|
|
123
|
+
*/
|
|
124
|
+
showForgotPassword?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Top margin for each social button.
|
|
127
|
+
*/
|
|
128
|
+
socialButtonMarginTop?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Maximum width (px) for primary action buttons.
|
|
131
|
+
*/
|
|
132
|
+
actionButtonMaxWidth?: number;
|
|
77
133
|
}
|
|
78
134
|
export declare const SignInSimple: React.FC<SignInSimpleProps>;
|
|
79
135
|
export default SignInSimple;
|
|
@@ -14,12 +14,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
14
14
|
*/
|
|
15
15
|
import { useState, useCallback, useRef, useMemo, useEffect, } from 'react';
|
|
16
16
|
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { createGlobalStyle } from 'styled-components';
|
|
18
|
+
import { Box, Button, FormControl, Heading, Link, Spinner, Text, Textarea, TextInput, } from '@primer/react';
|
|
19
|
+
import { EyeIcon, EyeClosedIcon, KeyIcon, MailIcon, SignInIcon, TelescopeIcon, } from '@primer/octicons-react';
|
|
20
|
+
import { GithubMarkIcon, GoogleIcon, LinkedInGreyIcon, } from '@datalayer/icons-react';
|
|
20
21
|
import { useToast } from '../../hooks/index.js';
|
|
21
22
|
import { isInsideJupyterLab } from '../../utils/Jupyter.js';
|
|
22
|
-
const GoogleIcon = () => (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", preserveAspectRatio: "xMidYMid", viewBox: "0 0 256 262", height: "16px", fill: "currentColor", "aria-hidden": "true", children: [_jsx("path", { fill: "currentColor", d: "M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" }), _jsx("path", { fill: "currentColor", d: "M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" }), _jsx("path", { fill: "currentColor", d: "M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" }), _jsx("path", { fill: "currentColor", d: "M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" })] }));
|
|
23
23
|
const OAUTH2_PROVIDERS = {
|
|
24
24
|
github: {
|
|
25
25
|
name: 'github',
|
|
@@ -68,10 +68,44 @@ const getErrorMessage = (error, fallback = 'Unable to start social sign-in.') =>
|
|
|
68
68
|
}
|
|
69
69
|
return fallback;
|
|
70
70
|
};
|
|
71
|
+
// Repaint the browser autofill highlight (Chrome's blue/yellow) with theme
|
|
72
|
+
// tokens so autofilled/disabled inputs keep the Primer surface colors. A
|
|
73
|
+
// scoped global style is used because Primer's TextInput inner <input> is not
|
|
74
|
+
// reliably reachable via the wrapper `sx` for vendor-prefixed autofill pseudos.
|
|
75
|
+
const SignInInputGlobalStyle = createGlobalStyle `
|
|
76
|
+
.signin-input-theme-scope input:-webkit-autofill,
|
|
77
|
+
.signin-input-theme-scope input:-webkit-autofill:hover,
|
|
78
|
+
.signin-input-theme-scope input:-webkit-autofill:focus,
|
|
79
|
+
.signin-input-theme-scope input:-webkit-autofill:active,
|
|
80
|
+
.signin-input-theme-scope input:-webkit-autofill:disabled,
|
|
81
|
+
.signin-input-theme-scope textarea:-webkit-autofill,
|
|
82
|
+
.signin-input-theme-scope textarea:-webkit-autofill:hover,
|
|
83
|
+
.signin-input-theme-scope textarea:-webkit-autofill:focus,
|
|
84
|
+
.signin-input-theme-scope textarea:-webkit-autofill:active,
|
|
85
|
+
.signin-input-theme-scope textarea:-webkit-autofill:disabled {
|
|
86
|
+
-webkit-text-fill-color: var(--fgColor-default) !important;
|
|
87
|
+
caret-color: var(--fgColor-default) !important;
|
|
88
|
+
-webkit-box-shadow: 0 0 0 1000px var(--bgColor-default) inset !important;
|
|
89
|
+
box-shadow: 0 0 0 1000px var(--bgColor-default) inset !important;
|
|
90
|
+
background-color: var(--bgColor-default) !important;
|
|
91
|
+
transition: background-color 9999s ease-in-out 0s !important;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.signin-input-theme-scope input:disabled,
|
|
95
|
+
.signin-input-theme-scope textarea:disabled {
|
|
96
|
+
-webkit-text-fill-color: var(--fgColor-muted) !important;
|
|
97
|
+
-webkit-box-shadow: 0 0 0 1000px var(--bgColor-default) inset !important;
|
|
98
|
+
box-shadow: 0 0 0 1000px var(--bgColor-default) inset !important;
|
|
99
|
+
background-color: var(--bgColor-default) !important;
|
|
100
|
+
opacity: 1 !important;
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
71
103
|
// ── Component ────────────────────────────────────────────────────────
|
|
72
|
-
export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, name, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', icon, leadingIcon = _jsx(TelescopeIcon, { size: 24 }), github = false, google = false, linkedin = false, apiKey = false, signUp = true, socialSignInNavigationTarget, }) => {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
104
|
+
export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp, name, title = 'Datalayer OTEL', description = 'Sign in to access the observability dashboard.', icon, leadingIcon = _jsx(TelescopeIcon, { size: 24 }), github = false, google = false, linkedin = false, apiKey = false, signUp = true, socialSignInNavigationTarget, asDoc = false, hideHero = false, calloutTitle, calloutDescription, passwordToggleLabel = 'Sign in with a password', signUpTitle = "Don't have an account?", signUpDescription = 'Create a free Datalayer account with your email address.', signUpLabel = 'Sign up with email', signUpHref = 'https://datalayer.ai/signup', forgotPasswordHref = '/password', forgotPasswordLabel = 'Forgot password?', showForgotPassword = true, socialButtonMarginTop = 3, actionButtonMaxWidth = 320, }) => {
|
|
105
|
+
const compactDocMode = asDoc && !hideHero;
|
|
106
|
+
const headingText = name ??
|
|
107
|
+
(asDoc && title === 'Datalayer OTEL' ? 'Datalayer Sign In' : title);
|
|
108
|
+
const headingIcon = icon ?? (asDoc ? _jsx(SignInIcon, { size: 24 }) : leadingIcon);
|
|
75
109
|
const loginUrl = useMemo(() => {
|
|
76
110
|
if (loginUrlProp)
|
|
77
111
|
return loginUrlProp;
|
|
@@ -80,9 +114,14 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
80
114
|
}, [loginUrlProp]);
|
|
81
115
|
const [handle, setHandle] = useState('');
|
|
82
116
|
const [password, setPassword] = useState('');
|
|
117
|
+
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
|
83
118
|
const [showPassword, setShowPassword] = useState(false);
|
|
84
119
|
const [loading, setLoading] = useState(false);
|
|
85
120
|
const [socialLoading, setSocialLoading] = useState(false);
|
|
121
|
+
const [pendingSocialProvider, setPendingSocialProvider] = useState(null);
|
|
122
|
+
const [mfaUserUid, setMfaUserUid] = useState(null);
|
|
123
|
+
const [mfaCode, setMfaCode] = useState('');
|
|
124
|
+
const [mfaToken, setMfaToken] = useState(null);
|
|
86
125
|
const [error, setError] = useState(null);
|
|
87
126
|
const { enqueueToast } = useToast();
|
|
88
127
|
// API Key dialog state
|
|
@@ -107,6 +146,7 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
107
146
|
return;
|
|
108
147
|
}
|
|
109
148
|
setError(null);
|
|
149
|
+
setPendingSocialProvider(providerSpec.name);
|
|
110
150
|
setSocialLoading(true);
|
|
111
151
|
try {
|
|
112
152
|
const iamUrl = getIAMUrlFromDocumentConfig();
|
|
@@ -143,6 +183,7 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
143
183
|
const message = getErrorMessage(err);
|
|
144
184
|
setError(message);
|
|
145
185
|
enqueueToast(message, { variant: 'error' });
|
|
186
|
+
setPendingSocialProvider(null);
|
|
146
187
|
setSocialLoading(false);
|
|
147
188
|
}
|
|
148
189
|
}, [
|
|
@@ -168,10 +209,21 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
168
209
|
closeApiKeyDialog();
|
|
169
210
|
}, [apiKeyValue, onApiKeySignIn, closeApiKeyDialog]);
|
|
170
211
|
const handleSignUp = useCallback(() => {
|
|
171
|
-
window.
|
|
172
|
-
}, []);
|
|
212
|
+
window.location.assign(signUpHref);
|
|
213
|
+
}, [signUpHref]);
|
|
214
|
+
const handleForgotPassword = useCallback(() => {
|
|
215
|
+
if (!forgotPasswordHref) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (forgotPasswordHref.startsWith('http://') ||
|
|
219
|
+
forgotPasswordHref.startsWith('https://')) {
|
|
220
|
+
window.open(forgotPasswordHref, '_blank', 'noopener,noreferrer');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
window.location.assign(forgotPasswordHref);
|
|
224
|
+
}, [forgotPasswordHref]);
|
|
173
225
|
const submit = useCallback(async () => {
|
|
174
|
-
if (!handle || !password || loading)
|
|
226
|
+
if (asDoc || !handle || !password || loading)
|
|
175
227
|
return;
|
|
176
228
|
setLoading(true);
|
|
177
229
|
setError(null);
|
|
@@ -186,7 +238,17 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
186
238
|
}
|
|
187
239
|
const data = await resp.json();
|
|
188
240
|
if (data.success && data.token) {
|
|
189
|
-
|
|
241
|
+
const maybeUserUid = String(data?.user?.id || data?.user?.uid || '').trim();
|
|
242
|
+
const hasMfa = Boolean(data?.user?.mfaUrl);
|
|
243
|
+
if (hasMfa && maybeUserUid) {
|
|
244
|
+
setMfaUserUid(maybeUserUid);
|
|
245
|
+
setMfaToken(data.token);
|
|
246
|
+
setError(null);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (onSignIn) {
|
|
250
|
+
onSignIn(data.token, handle);
|
|
251
|
+
}
|
|
190
252
|
}
|
|
191
253
|
else {
|
|
192
254
|
setError(data.message || 'Invalid username or password.');
|
|
@@ -198,71 +260,159 @@ export const SignInSimple = ({ onSignIn, onApiKeySignIn, loginUrl: loginUrlProp,
|
|
|
198
260
|
finally {
|
|
199
261
|
setLoading(false);
|
|
200
262
|
}
|
|
201
|
-
}, [handle, password, loading, loginUrl, onSignIn]);
|
|
263
|
+
}, [asDoc, handle, password, loading, loginUrl, onSignIn]);
|
|
264
|
+
const submitMfa = useCallback(async () => {
|
|
265
|
+
if (asDoc || loading || !mfaUserUid || !mfaCode.trim() || !mfaToken) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
setLoading(true);
|
|
269
|
+
setError(null);
|
|
270
|
+
try {
|
|
271
|
+
const loginUrlObject = new URL(loginUrl, window.location.origin);
|
|
272
|
+
const iamBaseUrl = loginUrlObject.pathname.endsWith('/api/iam/v1/login')
|
|
273
|
+
? loginUrlObject.href.replace(/\/api\/iam\/v1\/login$/, '')
|
|
274
|
+
: `${loginUrlObject.origin}`;
|
|
275
|
+
const response = await fetch(`${iamBaseUrl}/api/iam/v1/users/${mfaUserUid}/mfa/validate`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: { 'Content-Type': 'application/json' },
|
|
278
|
+
body: JSON.stringify({ userUid: mfaUserUid, code: mfaCode.trim() }),
|
|
279
|
+
});
|
|
280
|
+
const payload = await response.json();
|
|
281
|
+
if (!response.ok || !payload?.success) {
|
|
282
|
+
throw new Error(payload?.message || 'Failed to validate your MFA code.');
|
|
283
|
+
}
|
|
284
|
+
if (onSignIn) {
|
|
285
|
+
onSignIn(mfaToken, handle);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
290
|
+
setError(message);
|
|
291
|
+
enqueueToast(message, { variant: 'error' });
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
setLoading(false);
|
|
295
|
+
}
|
|
296
|
+
}, [
|
|
297
|
+
asDoc,
|
|
298
|
+
enqueueToast,
|
|
299
|
+
handle,
|
|
300
|
+
loading,
|
|
301
|
+
loginUrl,
|
|
302
|
+
mfaCode,
|
|
303
|
+
mfaToken,
|
|
304
|
+
mfaUserUid,
|
|
305
|
+
onSignIn,
|
|
306
|
+
]);
|
|
202
307
|
const handleKeyDown = useCallback((e) => {
|
|
203
|
-
if (e.key === 'Enter')
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
308
|
+
if (e.key === 'Enter') {
|
|
309
|
+
if (mfaUserUid) {
|
|
310
|
+
submitMfa();
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
submit();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}, [mfaUserUid, submit, submitMfa]);
|
|
317
|
+
const socialDisabled = socialLoading || loading || asDoc;
|
|
318
|
+
return (_jsxs(Box, { className: "signin-input-theme-scope", sx: {
|
|
207
319
|
display: 'flex',
|
|
208
|
-
alignItems: 'center',
|
|
320
|
+
alignItems: hideHero || compactDocMode ? 'flex-start' : 'center',
|
|
209
321
|
justifyContent: 'center',
|
|
210
|
-
|
|
211
|
-
bg: 'canvas.default',
|
|
322
|
+
minHeight: hideHero || compactDocMode ? 'auto' : '100vh',
|
|
323
|
+
bg: hideHero ? 'transparent' : 'canvas.default',
|
|
212
324
|
color: 'fg.default',
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}, sx: {
|
|
253
|
-
|
|
325
|
+
py: hideHero ? 0 : compactDocMode ? 2 : 4,
|
|
326
|
+
}, children: [_jsx(SignInInputGlobalStyle, {}), _jsxs(Box, { sx: {
|
|
327
|
+
width: '100%',
|
|
328
|
+
maxWidth: 440,
|
|
329
|
+
p: 0,
|
|
330
|
+
}, children: [!hideHero && (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
|
|
331
|
+
display: 'flex',
|
|
332
|
+
alignItems: 'center',
|
|
333
|
+
gap: 2,
|
|
334
|
+
mb: compactDocMode ? 2 : 3,
|
|
335
|
+
justifyContent: 'center',
|
|
336
|
+
}, children: [headingIcon, _jsx(Heading, { sx: { fontSize: 3 }, children: headingText })] }), _jsx(Text, { as: "p", sx: {
|
|
337
|
+
fontSize: 1,
|
|
338
|
+
color: 'fg.muted',
|
|
339
|
+
mb: compactDocMode ? 2 : 3,
|
|
340
|
+
textAlign: 'center',
|
|
341
|
+
}, children: description })] })), (calloutTitle || calloutDescription) && (_jsxs(Box, { sx: { mb: 3, textAlign: 'center' }, children: [calloutTitle && (_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 1 }, children: calloutTitle })), calloutDescription && (_jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: calloutDescription }))] })), !mfaUserUid && (github || google || linkedin) && (_jsxs(_Fragment, { children: [github && (_jsx(Button, { block: true, size: "large", leadingVisual: pendingSocialProvider === 'github'
|
|
342
|
+
? () => _jsx(Spinner, { size: "small" })
|
|
343
|
+
: GithubMarkIcon, disabled: socialDisabled, onClick: () => startOAuthSignIn(OAUTH2_PROVIDERS.github), sx: {
|
|
344
|
+
mt: socialButtonMarginTop,
|
|
345
|
+
maxWidth: actionButtonMaxWidth,
|
|
346
|
+
mx: 'auto',
|
|
347
|
+
}, children: "Sign in with GitHub" })), google && (_jsx(Button, { block: true, size: "large", leadingVisual: pendingSocialProvider === 'google'
|
|
348
|
+
? () => _jsx(Spinner, { size: "small" })
|
|
349
|
+
: GoogleIcon, disabled: socialDisabled, onClick: () => startOAuthSignIn(OAUTH2_PROVIDERS.google), sx: {
|
|
350
|
+
mt: socialButtonMarginTop,
|
|
351
|
+
maxWidth: actionButtonMaxWidth,
|
|
352
|
+
mx: 'auto',
|
|
353
|
+
}, children: "Sign in with Google" })), linkedin && (_jsx(Button, { block: true, size: "large", leadingVisual: pendingSocialProvider === 'linkedin'
|
|
354
|
+
? () => _jsx(Spinner, { size: "small" })
|
|
355
|
+
: LinkedInGreyIcon, disabled: socialDisabled, onClick: () => startOAuthSignIn(OAUTH2_PROVIDERS.linkedin), sx: {
|
|
356
|
+
mt: socialButtonMarginTop,
|
|
357
|
+
maxWidth: actionButtonMaxWidth,
|
|
358
|
+
mx: 'auto',
|
|
359
|
+
}, children: "Sign in with LinkedIn" }))] })), !mfaUserUid && !showPasswordForm && (_jsx(Box, { sx: { mt: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(Button, { leadingVisual: KeyIcon, size: "large", block: true, disabled: asDoc || loading || socialLoading, onClick: () => setShowPasswordForm(true), sx: { maxWidth: actionButtonMaxWidth, mx: 'auto' }, children: passwordToggleLabel }) })), !mfaUserUid && showPasswordForm && (_jsxs(Box, { sx: { mt: 4 }, children: [_jsxs(FormControl, { required: true, sx: { mb: 3 }, children: [_jsx(FormControl.Label, { children: "Username" }), _jsx(TextInput, { autoFocus: true, block: true, placeholder: "Your username", value: handle, disabled: loading || asDoc, onChange: e => setHandle(e.target.value), onKeyDown: handleKeyDown })] }), _jsxs(FormControl, { required: true, sx: { mb: 3 }, children: [_jsx(FormControl.Label, { children: "Password" }), _jsx(TextInput, { block: true, placeholder: "Your password", type: showPassword ? 'text' : 'password', value: password, disabled: loading || asDoc, onChange: e => setPassword(e.target.value), onKeyDown: handleKeyDown, trailingAction: _jsx(TextInput.Action, { onClick: () => setShowPassword(!showPassword), disabled: loading || asDoc, icon: showPassword ? EyeClosedIcon : EyeIcon, "aria-label": showPassword ? 'Hide password' : 'Show password', sx: { color: 'var(--fgColor-muted)' } }) })] }), _jsxs(Box, { sx: {
|
|
360
|
+
display: 'flex',
|
|
361
|
+
alignItems: 'center',
|
|
362
|
+
justifyContent: 'space-between',
|
|
363
|
+
mt: 3,
|
|
364
|
+
}, children: [_jsx(Button, { variant: "primary", disabled: loading || asDoc || !handle || !password, onClick: submit, children: loading ? 'Signing in…' : 'Sign in' }), showForgotPassword && (_jsx(Link, { as: "button", sx: {
|
|
365
|
+
fontSize: 1,
|
|
366
|
+
cursor: asDoc ? 'not-allowed' : 'pointer',
|
|
367
|
+
background: 'none',
|
|
368
|
+
border: 'none',
|
|
369
|
+
opacity: asDoc ? 0.6 : 1,
|
|
370
|
+
}, onClick: () => {
|
|
371
|
+
if (asDoc || loading) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
handleForgotPassword();
|
|
375
|
+
}, children: forgotPasswordLabel }))] })] })), mfaUserUid && (_jsxs(_Fragment, { children: [_jsxs(FormControl, { required: true, sx: { mb: 3, mt: 2 }, children: [_jsx(FormControl.Label, { children: "MFA Code" }), _jsx(TextInput, { autoFocus: true, block: true, placeholder: "Your MFA code", value: mfaCode, disabled: loading || asDoc, onChange: e => setMfaCode(e.target.value), onKeyDown: handleKeyDown })] }), _jsx(Button, { variant: "primary", block: true, disabled: loading || asDoc || !mfaCode.trim(), onClick: submitMfa, sx: { maxWidth: actionButtonMaxWidth, mx: 'auto' }, children: loading ? 'Validating…' : 'Validate MFA Code' })] })), error && (_jsx(Text, { sx: { color: 'danger.fg', fontSize: 1, mt: 3, display: 'block' }, children: error })), signUp && (_jsxs(Box, { sx: {
|
|
376
|
+
mt: 4,
|
|
377
|
+
pt: 4,
|
|
378
|
+
textAlign: 'center',
|
|
379
|
+
borderTop: '1px solid',
|
|
380
|
+
borderColor: 'border.muted',
|
|
381
|
+
}, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: signUpTitle }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mb: 3 }, children: signUpDescription }), _jsx(Box, { sx: { display: 'flex', justifyContent: 'center' }, children: _jsx(Button, { variant: "primary", size: "large", leadingVisual: MailIcon, disabled: asDoc || loading, onClick: handleSignUp, children: signUpLabel }) })] })), apiKey && onApiKeySignIn && (_jsxs(_Fragment, { children: [_jsxs(Box, { sx: {
|
|
382
|
+
display: 'flex',
|
|
383
|
+
alignItems: 'center',
|
|
384
|
+
gap: 2,
|
|
385
|
+
my: 3,
|
|
386
|
+
}, children: [_jsx(Box, { sx: { flex: 1, height: '1px', bg: 'border.default' } }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: "or" }), _jsx(Box, { sx: { flex: 1, height: '1px', bg: 'border.default' } })] }), _jsx(Button, { block: true, leadingVisual: KeyIcon, disabled: asDoc || loading || socialLoading, onClick: () => setShowApiKeyDialog(true), sx: { maxWidth: actionButtonMaxWidth, mx: 'auto' }, children: "Sign In with an API Key" }), showApiKeyDialog && (_jsx(Box, { role: "dialog", "aria-modal": "true", "aria-labelledby": "signin-api-key-title", onKeyDown: event => {
|
|
387
|
+
if (event.key === 'Escape') {
|
|
388
|
+
closeApiKeyDialog();
|
|
389
|
+
}
|
|
390
|
+
}, onClick: closeApiKeyDialog, sx: {
|
|
391
|
+
position: 'fixed',
|
|
392
|
+
inset: 0,
|
|
393
|
+
zIndex: 200,
|
|
254
394
|
p: 3,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
395
|
+
bg: 'canvas.backdrop',
|
|
396
|
+
display: 'flex',
|
|
397
|
+
alignItems: 'center',
|
|
398
|
+
justifyContent: 'center',
|
|
399
|
+
}, children: _jsxs(Box, { as: "form", onClick: event => event.stopPropagation(), onSubmit: event => {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
handleApiKeyAuthenticate();
|
|
402
|
+
}, sx: {
|
|
403
|
+
width: 'min(560px, 100%)',
|
|
404
|
+
p: 3,
|
|
405
|
+
borderRadius: 2,
|
|
406
|
+
border: '1px solid',
|
|
407
|
+
borderColor: 'border.default',
|
|
408
|
+
bg: 'canvas.overlay',
|
|
409
|
+
color: 'fg.default',
|
|
410
|
+
boxShadow: 'shadow.large',
|
|
411
|
+
}, children: [_jsx(Heading, { id: "signin-api-key-title", sx: { fontSize: 2, mb: 2 }, children: "Enter your API Key" }), _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "API Key" }), _jsx(Textarea, { block: true, required: true, autoFocus: true, placeholder: "Paste your API key here", value: apiKeyValue, onInput: (e) => setApiKeyValue(e.target.value), ref: apiKeyRef })] }), _jsxs(Box, { sx: {
|
|
412
|
+
mt: 3,
|
|
413
|
+
display: 'flex',
|
|
414
|
+
justifyContent: 'flex-end',
|
|
415
|
+
gap: 2,
|
|
416
|
+
}, children: [_jsx(Button, { type: "button", onClick: closeApiKeyDialog, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: !apiKeyValue.trim(), children: "Authenticate" })] })] }) }))] }))] })] }));
|
|
267
417
|
};
|
|
268
418
|
export default SignInSimple;
|
|
@@ -36,6 +36,9 @@ export const UserBadge = ({ token, variant = 'full', showExpandToggle = true, ex
|
|
|
36
36
|
const user = getDatalayerJwtUser(token);
|
|
37
37
|
const displayName = getDatalayerDisplayName(user, user?.handle ?? '');
|
|
38
38
|
const claims = parseJwtPayload(token);
|
|
39
|
+
const popoverDisplayName = claims?.user
|
|
40
|
+
? getDatalayerDisplayName(claims.user, claims.user.handle ?? '')
|
|
41
|
+
: displayName;
|
|
39
42
|
// Colour the trigger label based on token expiry:
|
|
40
43
|
// - red when already expired,
|
|
41
44
|
// - yellow when expiring within ``expirationWarningMs``,
|
|
@@ -84,14 +87,14 @@ export const UserBadge = ({ token, variant = 'full', showExpandToggle = true, ex
|
|
|
84
87
|
}, children: [_jsxs(Box, { sx: {
|
|
85
88
|
px: 3,
|
|
86
89
|
py: 2,
|
|
87
|
-
bg: 'canvas.
|
|
90
|
+
bg: 'canvas.inset',
|
|
88
91
|
borderBottom: '1px solid',
|
|
89
92
|
borderColor: 'border.default',
|
|
90
93
|
display: 'flex',
|
|
91
94
|
alignItems: 'center',
|
|
92
95
|
justifyContent: 'space-between',
|
|
93
96
|
gap: 2,
|
|
94
|
-
}, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children:
|
|
97
|
+
}, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 1 }, children: popoverDisplayName || 'User' }), variant === 'small' && showExpandToggle && (_jsx(Box, { as: "button", type: "button", onClick: () => setExpanded(v => !v), "aria-label": expanded ? 'Hide full JWT claims' : 'Show full JWT claims', "aria-expanded": expanded, title: expanded ? 'Hide full JWT claims' : 'Show full JWT claims', sx: {
|
|
95
98
|
appearance: 'none',
|
|
96
99
|
border: '1px solid',
|
|
97
100
|
borderColor: 'border.default',
|
|
@@ -120,7 +123,7 @@ export const UserBadge = ({ token, variant = 'full', showExpandToggle = true, ex
|
|
|
120
123
|
gridTemplateColumns: '90px 1fr',
|
|
121
124
|
gap: 1,
|
|
122
125
|
fontSize: 0,
|
|
123
|
-
}, children: [claims.user.email && (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: "Email" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.email })] })), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Handle" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.handle }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "UID" }), _jsx(Text, { sx: { fontFamily: 'mono', wordBreak: 'break-all' }, children: claims.user.uid }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Issued" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: new Date(claims.iat * 1000).toISOString() }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Expires" }), _jsxs(Box, { sx: {
|
|
126
|
+
}, children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: "First name" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.firstName }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Last name" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.lastName }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Display name" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: getDatalayerDisplayName(claims.user, claims.user.handle) }), claims.user.email && (_jsxs(_Fragment, { children: [_jsx(Text, { sx: { color: 'fg.muted' }, children: "Email" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.email })] })), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Handle" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: claims.user.handle }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "UID" }), _jsx(Text, { sx: { fontFamily: 'mono', wordBreak: 'break-all' }, children: claims.user.uid }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Issued" }), _jsx(Text, { sx: { fontFamily: 'mono' }, children: new Date(claims.iat * 1000).toISOString() }), _jsx(Text, { sx: { color: 'fg.muted' }, children: "Expires" }), _jsxs(Box, { sx: {
|
|
124
127
|
display: 'flex',
|
|
125
128
|
alignItems: 'center',
|
|
126
129
|
gap: 2,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
".",
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
"playwright": "^1.53.2",
|
|
225
225
|
"prettier": "^3.6.2",
|
|
226
226
|
"react-router-dom": "^6.22.3",
|
|
227
|
-
"rimraf": "^6.
|
|
227
|
+
"rimraf": "^6.1.3",
|
|
228
228
|
"storybook": "^9.1.1",
|
|
229
229
|
"typedoc-plugin-markdown": "^4.0.0",
|
|
230
230
|
"typescript": "^5.8.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|