@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.
@@ -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
- ...TONES[tone],
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(PersonIcon, {}) }), _jsxs(Box, { as: "span", sx: {
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',
@@ -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 datasource" })] }), _jsx(DatasourcesTable, { datasourcesListRoute: datasourcesListRoute, principalUid: principalUid, principalKind: principalKind, showInlineLoadingIndicator: showInlineLoadingIndicator })] }) }) }));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.1.59",
3
+ "version": "1.1.60",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",