@datalayer/core 1.1.59 → 1.1.60
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/avatars/UserAvatar.d.ts +9 -1
- package/lib/components/avatars/UserAvatar.js +14 -2
- package/lib/components/collaboration/LiveEditorCollaborators.js +12 -0
- package/lib/components/labels/StatusLabels.d.ts +9 -0
- package/lib/components/labels/StatusLabels.js +30 -1
- package/lib/components/principal/PrincipalAvatar.d.ts +8 -1
- package/lib/components/principal/PrincipalAvatar.js +9 -2
- package/lib/components/principal/PrincipalSwitcherMenu.js +2 -1
- package/lib/hooks/useUser.js +42 -0
- package/lib/views/datasources/Datasources.js +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,15 @@ export type UserAvatarProps = {
|
|
|
16
16
|
fallbackBackground?: string;
|
|
17
17
|
/** Optional icon foreground color override for the default avatar. */
|
|
18
18
|
fallbackForeground?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a ring is drawn around the avatar.
|
|
21
|
+
*
|
|
22
|
+
* Off by default: an avatar sits on its own in most places, and a ring
|
|
23
|
+
* there is one more line for nothing. On, it gives the avatar an edge of
|
|
24
|
+
* its own against a background of the same colour — a profile page, a card.
|
|
25
|
+
*/
|
|
26
|
+
ring?: boolean;
|
|
19
27
|
className?: string;
|
|
20
28
|
};
|
|
21
|
-
export declare const UserAvatar: ({ avatarUrl, avatarIcon, size, square, iconSize, fallbackBackground, fallbackForeground, className, }: UserAvatarProps) => JSX.Element;
|
|
29
|
+
export declare const UserAvatar: ({ avatarUrl, avatarIcon, size, square, iconSize, fallbackBackground, fallbackForeground, ring, className, }: UserAvatarProps) => JSX.Element;
|
|
22
30
|
export default UserAvatar;
|
|
@@ -28,8 +28,18 @@ export function hasRealAvatar(url) {
|
|
|
28
28
|
}
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
|
-
export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, iconSize, fallbackBackground, fallbackForeground, className, }) => {
|
|
31
|
+
export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, iconSize, fallbackBackground, fallbackForeground, ring = false, className, }) => {
|
|
32
32
|
const palette = useColorPalette();
|
|
33
|
+
/*
|
|
34
|
+
* The ring, as a shadow rather than a border.
|
|
35
|
+
*
|
|
36
|
+
* A border would take from the size asked for — the avatar is laid out
|
|
37
|
+
* against others of the same measure — so it is drawn just outside the
|
|
38
|
+
* edge instead, following whatever shape the avatar has.
|
|
39
|
+
*/
|
|
40
|
+
const ringSx = ring
|
|
41
|
+
? { boxShadow: '0 0 0 1px var(--borderColor-default, currentColor)' }
|
|
42
|
+
: undefined;
|
|
33
43
|
const SelectedIcon = getPrincipalAvatarIcon(avatarIcon);
|
|
34
44
|
if (SelectedIcon) {
|
|
35
45
|
return (_jsx(Box, { className: className, sx: {
|
|
@@ -42,10 +52,11 @@ export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, i
|
|
|
42
52
|
justifyContent: 'center',
|
|
43
53
|
bg: fallbackBackground || 'accent.subtle',
|
|
44
54
|
'--datalayer-icon-fg': fallbackForeground || palette.primary,
|
|
55
|
+
...ringSx,
|
|
45
56
|
}, children: _jsx(SelectedIcon, { size: iconSize ?? Math.round(size * 0.62), themed: true, colormode: true }) }));
|
|
46
57
|
}
|
|
47
58
|
if (hasRealAvatar(avatarUrl)) {
|
|
48
|
-
return _jsx(DLAvatar, { className: className, square: square, src: avatarUrl, size: size });
|
|
59
|
+
return (_jsx(DLAvatar, { className: className, square: square, src: avatarUrl, size: size, sx: ringSx }));
|
|
49
60
|
}
|
|
50
61
|
const resolvedIconSize = iconSize ?? Math.round(size * 0.48);
|
|
51
62
|
return (_jsx(Box, { className: className, sx: {
|
|
@@ -57,6 +68,7 @@ export const UserAvatar = ({ avatarUrl, avatarIcon, size = 100, square = true, i
|
|
|
57
68
|
alignItems: 'center',
|
|
58
69
|
justifyContent: 'center',
|
|
59
70
|
'--datalayer-icon-fg': fallbackForeground || palette.primary,
|
|
71
|
+
...ringSx,
|
|
60
72
|
}, children: _jsx(AlienIcon, { size: resolvedIconSize, themed: true, colormode: true }) }));
|
|
61
73
|
};
|
|
62
74
|
export default UserAvatar;
|
|
@@ -35,10 +35,22 @@ function collaboratorOfState(clientId, state) {
|
|
|
35
35
|
if (!user) {
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
|
+
// Not a person: the presence of the collaboration service itself — the
|
|
39
|
+
// spacer joins its own rooms, wearing an `agent` field.
|
|
40
|
+
if (user.agent) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
38
43
|
const name = user.display_name || user.displayName || user.name || user.username || '';
|
|
39
44
|
if (!name) {
|
|
40
45
|
return undefined;
|
|
41
46
|
}
|
|
47
|
+
// Nobody either: the seed identity of a Jupyter Server that does not
|
|
48
|
+
// authenticate its users — "Anonymous Hegemone" and kin. A face for it
|
|
49
|
+
// would be one avatar too many, wearing the default fallback, next to the
|
|
50
|
+
// real identity written over it a moment later.
|
|
51
|
+
if (user.anonymous === true || /^anonymous(\s|$)/i.test(name)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
42
54
|
return {
|
|
43
55
|
id: String(user.uid ?? user.id ?? user.username ?? clientId),
|
|
44
56
|
name,
|
|
@@ -39,4 +39,13 @@ export declare function AdminLabel(props?: StatusLabelProps): JSX.Element;
|
|
|
39
39
|
* The one that is taken when none is chosen — the default space of an account.
|
|
40
40
|
*/
|
|
41
41
|
export declare function DefaultLabel(props?: StatusLabelProps): JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* Something announced and not there yet.
|
|
44
|
+
*
|
|
45
|
+
* A tone of its own, and deliberately not the accent an alpha wears: one says
|
|
46
|
+
* "you may try this, it may bite", the other "there is nothing to try". Read
|
|
47
|
+
* side by side in a navigation, two labels of one colour say the same thing
|
|
48
|
+
* about two entries that mean different things.
|
|
49
|
+
*/
|
|
50
|
+
export declare function SoonLabel(props?: StatusLabelProps): JSX.Element;
|
|
42
51
|
export default StatusLabel;
|
|
@@ -33,7 +33,24 @@ export function StatusLabel(props) {
|
|
|
33
33
|
return (_jsx(Label, { variant: "secondary", size: size, sx: {
|
|
34
34
|
textTransform: 'lowercase',
|
|
35
35
|
lineHeight: 1.2,
|
|
36
|
-
|
|
36
|
+
/*
|
|
37
|
+
* The tone, under a doubled selector.
|
|
38
|
+
*
|
|
39
|
+
* Primer ships `NavList` and `ActionList` as CSS modules, and the
|
|
40
|
+
* colour they set on the contents of an item ties with the one Emotion
|
|
41
|
+
* generates for `sx` — so the same label came out in the colour of the
|
|
42
|
+
* navigation in one place and in its own colour in another, depending
|
|
43
|
+
* on which stylesheet happened to come last. `&&` raises the
|
|
44
|
+
* specificity enough that the tone always lands, wherever the label is
|
|
45
|
+
* put.
|
|
46
|
+
*/
|
|
47
|
+
'&&': {
|
|
48
|
+
...TONES[tone],
|
|
49
|
+
// The navigation of Primer colours the contents of its items, and
|
|
50
|
+
// its stylesheet is loaded after ours: a doubled selector was not
|
|
51
|
+
// enough on its own, so the colour of the tone is stated as final.
|
|
52
|
+
color: `${TONES[tone].color} !important`,
|
|
53
|
+
},
|
|
37
54
|
...sx,
|
|
38
55
|
}, children: children }));
|
|
39
56
|
}
|
|
@@ -58,4 +75,16 @@ export function DefaultLabel(props = {}) {
|
|
|
58
75
|
const { children = 'default', ...rest } = props;
|
|
59
76
|
return (_jsx(StatusLabel, { tone: "accent", ...rest, children: children }));
|
|
60
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Something announced and not there yet.
|
|
80
|
+
*
|
|
81
|
+
* A tone of its own, and deliberately not the accent an alpha wears: one says
|
|
82
|
+
* "you may try this, it may bite", the other "there is nothing to try". Read
|
|
83
|
+
* side by side in a navigation, two labels of one colour say the same thing
|
|
84
|
+
* about two entries that mean different things.
|
|
85
|
+
*/
|
|
86
|
+
export function SoonLabel(props = {}) {
|
|
87
|
+
const { children = 'soon', ...rest } = props;
|
|
88
|
+
return (_jsx(StatusLabel, { tone: "neutral", ...rest, children: children }));
|
|
89
|
+
}
|
|
61
90
|
export default StatusLabel;
|
|
@@ -7,7 +7,14 @@ export type PrincipalAvatarProps = {
|
|
|
7
7
|
alt?: string;
|
|
8
8
|
size?: number;
|
|
9
9
|
square?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a ring is drawn around the avatar.
|
|
12
|
+
*
|
|
13
|
+
* Off by default, and the same option `UserAvatar` carries — a public
|
|
14
|
+
* profile wants the edge, a menu entry does not.
|
|
15
|
+
*/
|
|
16
|
+
ring?: boolean;
|
|
10
17
|
className?: string;
|
|
11
18
|
};
|
|
12
|
-
export declare function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size, square, className, }: PrincipalAvatarProps): JSX.Element;
|
|
19
|
+
export declare function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size, square, ring, className, }: PrincipalAvatarProps): JSX.Element;
|
|
13
20
|
export default PrincipalAvatar;
|
|
@@ -10,8 +10,13 @@ import { getPrincipalAvatarIcon } from './PrincipalAppearance.js';
|
|
|
10
10
|
function getFallbackIconSize(size) {
|
|
11
11
|
return Math.max(12, Math.round(size * 0.62));
|
|
12
12
|
}
|
|
13
|
-
export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, square = false, className, }) {
|
|
13
|
+
export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, square = false, ring = false, className, }) {
|
|
14
14
|
const SelectedIcon = getPrincipalAvatarIcon(avatarIcon);
|
|
15
|
+
// Outside the edge rather than a border, so the avatar keeps the size it
|
|
16
|
+
// was asked for — see `UserAvatar`, which draws its ring the same way.
|
|
17
|
+
const ringSx = ring
|
|
18
|
+
? { boxShadow: '0 0 0 1px var(--borderColor-default, currentColor)' }
|
|
19
|
+
: undefined;
|
|
15
20
|
if (SelectedIcon) {
|
|
16
21
|
return (_jsx(Box, { className: className, sx: {
|
|
17
22
|
width: size,
|
|
@@ -21,10 +26,11 @@ export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, s
|
|
|
21
26
|
justifyContent: 'center',
|
|
22
27
|
overflow: 'hidden',
|
|
23
28
|
borderRadius: square ? 2 : '50%',
|
|
29
|
+
...ringSx,
|
|
24
30
|
}, "aria-label": alt || `${kind} avatar`, children: _jsx(SelectedIcon, { size: size, themed: true, colormode: true }) }));
|
|
25
31
|
}
|
|
26
32
|
if (kind === 'personal') {
|
|
27
|
-
return (_jsx(UserAvatar, { avatarUrl: avatarUrl, avatarIcon: avatarIcon, size: size, square: square, iconSize: getFallbackIconSize(size), className: className }));
|
|
33
|
+
return (_jsx(UserAvatar, { avatarUrl: avatarUrl, avatarIcon: avatarIcon, size: size, square: square, iconSize: getFallbackIconSize(size), ring: ring, className: className }));
|
|
28
34
|
}
|
|
29
35
|
const iconSize = getFallbackIconSize(size);
|
|
30
36
|
const borderRadius = square ? 2 : '50%';
|
|
@@ -39,6 +45,7 @@ export function PrincipalAvatar({ kind, avatarUrl, avatarIcon, alt, size = 20, s
|
|
|
39
45
|
borderRadius,
|
|
40
46
|
border: '1px solid',
|
|
41
47
|
borderColor: 'border.default',
|
|
48
|
+
...ringSx,
|
|
42
49
|
}, "aria-label": alt || (kind === 'team' ? 'Team' : 'Organization'), children: _jsx(Icon, { size: iconSize }) }));
|
|
43
50
|
}
|
|
44
51
|
export default PrincipalAvatar;
|
|
@@ -9,6 +9,7 @@ import { OrganizationIcon, PeopleIcon, PersonIcon, TriangleDownIcon, } from '@pr
|
|
|
9
9
|
import { useCache, useAuthorization } from '../../hooks/index.js';
|
|
10
10
|
import { useCoreStore } from '../../state/index.js';
|
|
11
11
|
import { useIAMStore } from '../../state/substates/index.js';
|
|
12
|
+
import { UserAvatar } from '../avatars/index.js';
|
|
12
13
|
import { memberships as fetchMemberships } from '../../api/iam/profile.js';
|
|
13
14
|
import { usePrincipalStore } from '../../hooks/usePrincipalStore.js';
|
|
14
15
|
import { useBillingEntityStore } from '../../hooks/useBillingEntityStore.js';
|
|
@@ -284,7 +285,7 @@ export function PrincipalSwitcherMenu({ maxLabelChars = 48, fullWidth = true, sh
|
|
|
284
285
|
if (personalUid && personalHandle) {
|
|
285
286
|
selectUser(personalUid, personalHandle);
|
|
286
287
|
}
|
|
287
|
-
}, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(
|
|
288
|
+
}, children: [_jsx(ActionList.LeadingVisual, { children: _jsx(UserAvatar, { avatarUrl: user?.avatarUrl, avatarIcon: user?.avatarIcon, size: 16, square: false, ring: true }) }), _jsxs(Box, { as: "span", sx: {
|
|
288
289
|
display: 'block',
|
|
289
290
|
minWidth: 0,
|
|
290
291
|
overflow: 'hidden',
|
package/lib/hooks/useUser.js
CHANGED
|
@@ -15,6 +15,28 @@ const isOnPublicPath = () => {
|
|
|
15
15
|
}
|
|
16
16
|
return isPublicPath(window.location?.pathname || '');
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Whether these views are running inside JupyterLab.
|
|
20
|
+
*
|
|
21
|
+
* Read from the configuration JupyterLab writes into its own page, so nothing
|
|
22
|
+
* has to be imported from it: this module is used by the web application as
|
|
23
|
+
* well, which has no such configuration.
|
|
24
|
+
*/
|
|
25
|
+
const isInsideJupyterLab = () => {
|
|
26
|
+
if (typeof document === 'undefined') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const config = document.getElementById('jupyter-config-data');
|
|
30
|
+
if (!config) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(config.textContent || '{}').appName === 'JupyterLab';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
18
40
|
export const useUser = (role) => {
|
|
19
41
|
const { user } = useIAMStore();
|
|
20
42
|
if (role) {
|
|
@@ -22,6 +44,10 @@ export const useUser = (role) => {
|
|
|
22
44
|
if (isOnPublicPath()) {
|
|
23
45
|
return user;
|
|
24
46
|
}
|
|
47
|
+
if (isInsideJupyterLab()) {
|
|
48
|
+
// Same reason as below: the router of these views answers this.
|
|
49
|
+
return user;
|
|
50
|
+
}
|
|
25
51
|
console.warn(`User should have role ${role.handle} - Forcing navigation to login page...`);
|
|
26
52
|
window.location.href = LOGIN_HREF;
|
|
27
53
|
throw new Error(`User should have role ${role.handle}`);
|
|
@@ -31,6 +57,22 @@ export const useUser = (role) => {
|
|
|
31
57
|
if (isOnPublicPath()) {
|
|
32
58
|
return user;
|
|
33
59
|
}
|
|
60
|
+
/*
|
|
61
|
+
* Inside JupyterLab, nobody leaves the page.
|
|
62
|
+
*
|
|
63
|
+
* `/login` is a page of the web application, at the root of the origin —
|
|
64
|
+
* and the origin of a JupyterLab is the Jupyter Server, which serves
|
|
65
|
+
* something else entirely there. Signing out of a view thus threw the user
|
|
66
|
+
* out of JupyterLab and onto a page of the server, which is not even a
|
|
67
|
+
* sign-in form.
|
|
68
|
+
*
|
|
69
|
+
* The views of Datalayer carry their own sign-in route under the base of
|
|
70
|
+
* JupyterLab, and their router shows it as soon as there is no user — so
|
|
71
|
+
* the way to reach it is to let that render, not to navigate away from it.
|
|
72
|
+
*/
|
|
73
|
+
if (isInsideJupyterLab()) {
|
|
74
|
+
return user;
|
|
75
|
+
}
|
|
34
76
|
console.warn('No user found... Forcing navigation to /login');
|
|
35
77
|
window.location.href = LOGIN_HREF;
|
|
36
78
|
}
|
|
@@ -63,6 +63,6 @@ export const Datasources = ({ newDatasourceRoute = '/new/datasource', datasource
|
|
|
63
63
|
gap: 3,
|
|
64
64
|
flexWrap: 'wrap',
|
|
65
65
|
mb: 4,
|
|
66
|
-
}, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Datasources" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Configure external data providers available to your workspaces and agents." })] }), _jsx(Button, { size: "small", variant: "primary", leadingVisual: DatabaseIcon, onClick: e => navigate(newDatasourceRoute, e), children: "New
|
|
66
|
+
}, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h2", sx: { fontSize: 3, mb: 1 }, children: "Datasources" }), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Configure external data providers available to your workspaces and agents." })] }), _jsx(Button, { size: "small", variant: "primary", leadingVisual: DatabaseIcon, onClick: e => navigate(newDatasourceRoute, e), children: "New Datasource" })] }), _jsx(DatasourcesTable, { datasourcesListRoute: datasourcesListRoute, principalUid: principalUid, principalKind: principalKind, showInlineLoadingIndicator: showInlineLoadingIndicator })] }) }) }));
|
|
67
67
|
};
|
|
68
68
|
export default Datasources;
|