@datalayer/core 1.1.50 → 1.1.56

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.
Files changed (77) hide show
  1. package/README.md +19 -42
  2. package/lib/client/auth/AuthenticationManager.js +6 -4
  3. package/lib/client/auth/strategies.js +2 -0
  4. package/lib/components/animation/AnimatedText.d.ts +98 -0
  5. package/lib/components/animation/AnimatedText.js +220 -0
  6. package/lib/components/animation/index.d.ts +1 -0
  7. package/lib/components/animation/index.js +5 -0
  8. package/lib/components/auth/{LoginToken.d.ts → SigInAPIKey.d.ts} +3 -3
  9. package/lib/components/auth/{LoginToken.js → SigInAPIKey.js} +5 -5
  10. package/lib/components/auth/{Login.d.ts → SignIn.d.ts} +3 -3
  11. package/lib/components/auth/{Login.js → SignIn.js} +12 -12
  12. package/lib/components/auth/{Login.stories.d.ts → SignIn.stories.d.ts} +2 -2
  13. package/lib/components/auth/{Login.stories.js → SignIn.stories.js} +4 -4
  14. package/lib/components/auth/index.d.ts +2 -2
  15. package/lib/components/auth/index.js +2 -2
  16. package/lib/components/avatars/UserAvatar.d.ts +3 -1
  17. package/lib/components/avatars/UserAvatar.js +16 -3
  18. package/lib/components/avatars/UserProfileAvatar.js +2 -6
  19. package/lib/components/collaboration/LiveEditorCollaborators.d.ts +31 -0
  20. package/lib/components/collaboration/LiveEditorCollaborators.js +107 -0
  21. package/lib/components/collaboration/index.d.ts +1 -0
  22. package/lib/components/collaboration/index.js +9 -0
  23. package/lib/components/display/DatalayerBox.d.ts +3 -1
  24. package/lib/components/display/DatalayerBox.js +7 -7
  25. package/lib/components/display/JupyterDialog.d.ts +2 -0
  26. package/lib/components/display/JupyterDialog.js +10 -4
  27. package/lib/components/iam/ExternalTokenSilentLogin.js +2 -2
  28. package/lib/components/index.d.ts +2 -0
  29. package/lib/components/index.js +2 -0
  30. package/lib/components/labels/StatusLabels.d.ts +42 -0
  31. package/lib/components/labels/StatusLabels.js +61 -0
  32. package/lib/components/labels/index.d.ts +1 -0
  33. package/lib/components/labels/index.js +1 -0
  34. package/lib/components/principal/Principal.d.ts +2 -0
  35. package/lib/components/principal/Principal.js +11 -1
  36. package/lib/components/principal/PrincipalAppearance.d.ts +136 -0
  37. package/lib/components/principal/PrincipalAppearance.js +203 -0
  38. package/lib/components/principal/PrincipalAvatar.d.ts +5 -2
  39. package/lib/components/principal/PrincipalAvatar.js +16 -3
  40. package/lib/components/principal/PrincipalBadge.js +7 -0
  41. package/lib/components/principal/PrincipalDetailsOverlay.d.ts +2 -1
  42. package/lib/components/principal/PrincipalDetailsOverlay.js +2 -2
  43. package/lib/components/principal/index.d.ts +1 -0
  44. package/lib/components/principal/index.js +1 -0
  45. package/lib/components/progress/ConsumptionBar.d.ts +9 -0
  46. package/lib/components/progress/ConsumptionBar.js +10 -2
  47. package/lib/components/sharing/ShareAccessComponent.js +6 -1
  48. package/lib/config/Configuration.d.ts +17 -8
  49. package/lib/config/Configuration.js +12 -5
  50. package/lib/hooks/useCache.d.ts +2 -0
  51. package/lib/hooks/useCache.js +35 -11
  52. package/lib/hooks/usePrincipalCacheStore.d.ts +2 -0
  53. package/lib/hooks/usePrincipalCacheStore.js +2 -0
  54. package/lib/models/IAM.d.ts +10 -0
  55. package/lib/models/Organization.d.ts +2 -0
  56. package/lib/models/Organization.js +2 -0
  57. package/lib/models/Profile.d.ts +2 -0
  58. package/lib/models/Team.d.ts +2 -0
  59. package/lib/models/Team.js +2 -0
  60. package/lib/models/User.d.ts +4 -0
  61. package/lib/models/User.js +4 -0
  62. package/lib/models/UserDTO.d.ts +6 -0
  63. package/lib/models/UserDTO.js +8 -0
  64. package/lib/state/substates/CoreState.js +3 -2
  65. package/lib/state/substates/IAMState.js +6 -0
  66. package/lib/state/substates/LayoutState.js +62 -7
  67. package/lib/state/substates/OrganizationState.d.ts +6 -0
  68. package/lib/state/substates/OrganizationState.js +5 -0
  69. package/lib/state/substates/ProfileState.d.ts +34 -0
  70. package/lib/state/substates/ProfileState.js +68 -0
  71. package/lib/state/substates/TeamState.d.ts +6 -0
  72. package/lib/state/substates/TeamState.js +3 -0
  73. package/lib/state/substates/index.d.ts +1 -0
  74. package/lib/state/substates/index.js +1 -0
  75. package/lib/views/iam/SignInSimple.d.ts +1 -1
  76. package/lib/views/iam/SignInSimple.js +3 -4
  77. package/package.json +2 -2
@@ -270,6 +270,8 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
270
270
  email: string;
271
271
  firstName: string;
272
272
  lastName: string;
273
+ avatarIcon?: string;
274
+ banner?: string;
273
275
  }, unknown>;
274
276
  useWhoami: () => import("@tanstack/react-query").UseQueryResult<any, Error>;
275
277
  useChangePassword: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
@@ -39,7 +39,7 @@
39
39
  import { useQuery, useQueries, useMutation, useQueryClient, } from '@tanstack/react-query';
40
40
  import { BOOTSTRAP_USER_ONBOARDING, asContact, asDatasource, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, } from '../models/index.js';
41
41
  import { asPage, asSpace } from './cacheConverters.js';
42
- import { useCoreStore, useIAMStore } from '../state/index.js';
42
+ import { useCoreStore, useIAMStore, profileStore } from '../state/index.js';
43
43
  import { asDisplayName, namesAsInitials, asArray } from '../utils/index.js';
44
44
  import { useDatalayer } from './useDatalayer.js';
45
45
  import { useAuthorization } from './useAuthorization.js';
@@ -977,7 +977,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
977
977
  token,
978
978
  });
979
979
  if (resp.me) {
980
- return toUser(resp.me);
980
+ const me = toUser(resp.me);
981
+ // The full profile — banner and avatar included — feeds the
982
+ // profile store every surface reads.
983
+ profileStore.getState().setProfileFromUser(me);
984
+ return me;
981
985
  }
982
986
  return null;
983
987
  },
@@ -989,15 +993,18 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
989
993
  */
990
994
  const useUpdateMe = () => {
991
995
  return useMutation({
992
- mutationFn: async ({ email, firstName, lastName, }) => {
996
+ mutationFn: async ({ email, firstName, lastName, avatarIcon, banner, }) => {
993
997
  return requestDatalayer({
994
998
  url: `${configuration.iamUrl}/api/iam/v1/me`,
995
999
  method: 'PUT',
996
- body: { email, firstName, lastName },
1000
+ body: { email, firstName, lastName, avatarIcon, banner },
997
1001
  });
998
1002
  },
999
1003
  onSuccess: () => {
1000
1004
  queryClient.invalidateQueries({ queryKey: queryKeys.auth.me() });
1005
+ // Every cached view of the user — the public profile page, the
1006
+ // admin list and detail, the search seeds — is now out of date.
1007
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.all() });
1001
1008
  },
1002
1009
  });
1003
1010
  };
@@ -1105,11 +1112,16 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1105
1112
  first_name_t: profile.first_name,
1106
1113
  last_name_t: profile.last_name,
1107
1114
  avatar_url_s: profile.avatar_url,
1115
+ avatar_icon_s: profile.avatar_icon_s,
1116
+ banner_s: profile.banner_s,
1108
1117
  origin_s: profile.origin,
1109
1118
  });
1119
+ // Stale seeding (updatedAt: 0): the public profile is a partial
1120
+ // shape, and a fresh-marked seed would overwrite a richer detail
1121
+ // entry and be served as the whole truth for the staleTime.
1110
1122
  if (mappedUser) {
1111
- queryClient.setQueryData(queryKeys.users.detail(mappedUser.id), mappedUser);
1112
- queryClient.setQueryData(queryKeys.users.byHandle(mappedUser.handle), mappedUser);
1123
+ queryClient.setQueryData(queryKeys.users.detail(mappedUser.id), mappedUser, { updatedAt: 0 });
1124
+ queryClient.setQueryData(queryKeys.users.byHandle(mappedUser.handle), mappedUser, { updatedAt: 0 });
1113
1125
  }
1114
1126
  }
1115
1127
  return profile;
@@ -1135,10 +1147,16 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1135
1147
  if (resp.success && resp.users) {
1136
1148
  const users = resp.users.map((u) => {
1137
1149
  const user = toUser(u);
1138
- // Pre-populate individual caches
1150
+ // Pre-populate individual caches so a detail view renders
1151
+ // instantly — but as STALE data (updatedAt: 0): a search answer
1152
+ // can be a partial shape (a member's search has no email or
1153
+ // settings), and a fresh-marked partial would be served as the
1154
+ // whole truth for the staleTime. Stale renders and revalidates.
1139
1155
  if (user) {
1140
- queryClient.setQueryData(queryKeys.users.detail(user.id), user);
1141
- queryClient.setQueryData(queryKeys.users.byHandle(user.handle), user);
1156
+ queryClient.setQueryData(queryKeys.users.detail(user.id), user, {
1157
+ updatedAt: 0,
1158
+ });
1159
+ queryClient.setQueryData(queryKeys.users.byHandle(user.handle), user, { updatedAt: 0 });
1142
1160
  }
1143
1161
  return user;
1144
1162
  });
@@ -1173,9 +1191,11 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1173
1191
  const users = resp.users
1174
1192
  .map((u) => {
1175
1193
  const user = toUser(u);
1194
+ // Stale seeding, as in useSearchUsers: the bulk answer may be
1195
+ // a partial shape, so it renders but revalidates.
1176
1196
  if (user) {
1177
- queryClient.setQueryData(queryKeys.users.detail(user.id), user);
1178
- queryClient.setQueryData(queryKeys.users.byHandle(user.handle), user);
1197
+ queryClient.setQueryData(queryKeys.users.detail(user.id), user, { updatedAt: 0 });
1198
+ queryClient.setQueryData(queryKeys.users.byHandle(user.handle), user, { updatedAt: 0 });
1179
1199
  }
1180
1200
  return user;
1181
1201
  })
@@ -1369,6 +1389,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1369
1389
  body: {
1370
1390
  name: organization.name,
1371
1391
  description: organization.description,
1392
+ avatarIcon: organization.avatarIcon,
1393
+ banner: organization.banner,
1372
1394
  },
1373
1395
  });
1374
1396
  },
@@ -1479,6 +1501,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1479
1501
  body: {
1480
1502
  name: team.name,
1481
1503
  description: team.description,
1504
+ avatarIcon: team.avatarIcon,
1505
+ banner: team.banner,
1482
1506
  },
1483
1507
  });
1484
1508
  },
@@ -19,6 +19,8 @@ export type CachedPrincipal = {
19
19
  email?: string;
20
20
  origin?: string;
21
21
  avatarUrl?: string;
22
+ avatarIcon?: string;
23
+ banner?: string;
22
24
  /** Team-specific: parent organization handle. */
23
25
  organizationHandle?: string;
24
26
  /** Team-specific: parent organization display name. */
@@ -18,6 +18,8 @@ const MERGEABLE_KEYS = [
18
18
  'email',
19
19
  'origin',
20
20
  'avatarUrl',
21
+ 'avatarIcon',
22
+ 'banner',
21
23
  'organizationHandle',
22
24
  'organizationName',
23
25
  'memberCount',
@@ -83,6 +83,8 @@ export interface UserMeResponse {
83
83
  last_name_t: string;
84
84
  /** Avatar URL for the user (may be empty string) */
85
85
  avatar_url_s: string;
86
+ avatar_icon_s?: string;
87
+ banner_s?: string;
86
88
  /** Array of roles assigned to the user */
87
89
  roles_ss: string[];
88
90
  /** Allow additional fields that may come from the server */
@@ -127,6 +129,8 @@ export interface ShareablePrincipal {
127
129
  description?: string | null;
128
130
  email?: string | null;
129
131
  avatar_url?: string | null;
132
+ avatar_icon?: string | null;
133
+ banner?: string | null;
130
134
  /** Parent organization UID — set when kind === 'team'. */
131
135
  organization_uid?: string | null;
132
136
  /** Parent organization handle — set when kind === 'team'. */
@@ -151,6 +155,10 @@ export interface PrincipalSearchUser {
151
155
  origin?: string;
152
156
  avatar_url_s?: string;
153
157
  avatarUrl?: string;
158
+ avatar_icon_s?: string;
159
+ avatarIcon?: string;
160
+ banner_s?: string;
161
+ banner?: string;
154
162
  [key: string]: any;
155
163
  }
156
164
  export interface PrincipalSearchTeam {
@@ -160,6 +168,8 @@ export interface PrincipalSearchTeam {
160
168
  description_t?: string;
161
169
  organization_uid?: string;
162
170
  organization_handle_s?: string;
171
+ avatar_icon_s?: string;
172
+ banner_s?: string;
163
173
  [key: string]: any;
164
174
  }
165
175
  export interface PrincipalSearchOrganization {
@@ -16,6 +16,8 @@ export type IBaseOrganization = {
16
16
  name: string;
17
17
  displayName?: string;
18
18
  description: string;
19
+ avatarIcon?: string;
20
+ banner?: string;
19
21
  public: boolean;
20
22
  creationDate: Date;
21
23
  members: IOrganizationMember[];
@@ -25,6 +25,8 @@ export function asOrganization(org) {
25
25
  name: org.name_t,
26
26
  displayName: org.display_name_t || org.name_t,
27
27
  description: org.description_t,
28
+ avatarIcon: org.avatar_icon_s ?? org.avatarIcon,
29
+ banner: org.banner_s ?? org.banner,
28
30
  public: org.public_b,
29
31
  members,
30
32
  teams: [],
@@ -32,6 +32,8 @@ export interface Profile {
32
32
  roles_ss?: string[];
33
33
  /** Avatar URL */
34
34
  avatar_url_s?: string;
35
+ avatar_icon_s?: string;
36
+ banner_s?: string;
35
37
  /** Onboarding state as JSON string */
36
38
  onboarding_s?: string;
37
39
  /** New password request timestamp */
@@ -8,6 +8,8 @@ export type IBaseTeam = {
8
8
  handle: string;
9
9
  name: string;
10
10
  description: string;
11
+ avatarIcon?: string;
12
+ banner?: string;
11
13
  public: boolean;
12
14
  creationDate: Date;
13
15
  lastUpdateDate?: Date;
@@ -18,6 +18,8 @@ export function asTeam(t, organizationId) {
18
18
  type: 'team',
19
19
  name: t.name_t,
20
20
  description: t.description_t,
21
+ avatarIcon: t.avatar_icon_s ?? t.avatarIcon,
22
+ banner: t.banner_s ?? t.banner,
21
23
  public: t.public_b,
22
24
  members,
23
25
  organization: {
@@ -25,6 +25,8 @@ export declare class User implements IUser {
25
25
  credits?: number;
26
26
  creditsCustomerId?: string;
27
27
  avatarUrl?: string;
28
+ avatarIcon?: string;
29
+ banner?: string;
28
30
  origin?: string;
29
31
  invites?: Array<IInvite>;
30
32
  iamProviders: Array<IIAMProviderLinked>;
@@ -63,6 +65,8 @@ export type IUser = IBaseUser & {
63
65
  credits?: number;
64
66
  creditsCustomerId?: string;
65
67
  avatarUrl?: string;
68
+ avatarIcon?: string;
69
+ banner?: string;
66
70
  origin?: string;
67
71
  invites?: Array<IInvite>;
68
72
  iamProviders: Array<IIAMProviderLinked>;
@@ -41,6 +41,8 @@ export class User {
41
41
  credits;
42
42
  creditsCustomerId;
43
43
  avatarUrl;
44
+ avatarIcon;
45
+ banner;
44
46
  origin;
45
47
  invites;
46
48
  iamProviders;
@@ -64,6 +66,8 @@ export class User {
64
66
  this.friendlyName =
65
67
  toFriendlyName(u.first_name_t, u.last_name_t) || u.handle_s;
66
68
  this.avatarUrl = u.avatar_url_s;
69
+ this.avatarIcon = u.avatar_icon_s ?? u.avatarIcon;
70
+ this.banner = u.banner_s ?? u.banner;
67
71
  this.origin = u.origin_s;
68
72
  this.joinDate = u.join_ts_dt ? new Date(u.join_ts_dt) : undefined;
69
73
  this.credits = u.credits_i ? Number(u.credits_i) : 0;
@@ -23,6 +23,8 @@ export interface UserData {
23
23
  last_name_t: string;
24
24
  /** Display name shown in the UI */
25
25
  avatar_url_s: string;
26
+ avatar_icon_s?: string;
27
+ banner_s?: string;
26
28
  /** Additional fields that may be present in the response */
27
29
  [key: string]: any;
28
30
  }
@@ -43,6 +45,8 @@ export interface UserJSON {
43
45
  handle: string;
44
46
  /** URL to the user's avatar image */
45
47
  avatarUrl: string;
48
+ avatarIcon?: string;
49
+ banner?: string;
46
50
  }
47
51
  /**
48
52
  * User model representing a Datalayer platform user.
@@ -65,6 +69,8 @@ export declare class UserDTO {
65
69
  get lastName(): string;
66
70
  get displayName(): string;
67
71
  get avatarUrl(): string;
72
+ get avatarIcon(): string | undefined;
73
+ get banner(): string | undefined;
68
74
  /**
69
75
  * Get user data in camelCase format.
70
76
  * Returns only the core fields that consumers need.
@@ -43,6 +43,12 @@ export class UserDTO {
43
43
  get avatarUrl() {
44
44
  return this._data.avatar_url_s;
45
45
  }
46
+ get avatarIcon() {
47
+ return this._data.avatar_icon_s;
48
+ }
49
+ get banner() {
50
+ return this._data.banner_s;
51
+ }
46
52
  // ========================================================================
47
53
  // Utility Methods
48
54
  // ========================================================================
@@ -63,6 +69,8 @@ export class UserDTO {
63
69
  email: this.email,
64
70
  handle: this.handle,
65
71
  avatarUrl: this.avatarUrl,
72
+ avatarIcon: this.avatarIcon,
73
+ banner: this.banner,
66
74
  };
67
75
  validateJSON(obj, 'User');
68
76
  return obj;
@@ -7,7 +7,6 @@ import { useStore } from 'zustand';
7
7
  import { configLogger } from '../../utils/Logger.js';
8
8
  let loadConfigurationFromServer = true;
9
9
  let initialConfiguration = {
10
- datalayerUrl: 'https://prod1.datalayer.run',
11
10
  token: '',
12
11
  credits: 0,
13
12
  cpuEnvironment: 'ai-agents-env"',
@@ -17,12 +16,14 @@ let initialConfiguration = {
17
16
  loadConfigurationFromServer: true,
18
17
  jupyterServerless: false,
19
18
  iamUrl: 'https://prod1.datalayer.run',
19
+ managerUrl: 'https://prod1.datalayer.run',
20
20
  runtimesUrl: 'https://r1.datalayer.run',
21
+ schedulerUrl: 'https://prod1.datalayer.run',
21
22
  libraryUrl: 'https://prod1.datalayer.run',
22
23
  spacerUrl: 'https://prod1.datalayer.run',
23
24
  aiAgentsUrl: 'https://prod1.datalayer.run',
24
25
  aiInferenceUrl: 'https://prod1.datalayer.run',
25
- mcpServersUrl: 'https://prod1.datalayer.run',
26
+ jupyterMcpServerUrl: 'https://mcp.datalayer.run/mcp',
26
27
  otelUrl: 'https://prod1.datalayer.run',
27
28
  // Defaults to prod so telemetry is consumed from the production OTEL service
28
29
  // even when runtimes export elsewhere (see `getOtelConsumeUrl` and the
@@ -10,6 +10,7 @@ import { getStoredToken, getStoredUser, loadRefreshTokenFromCookie, storeToken,
10
10
  import { requestDatalayerAPI, } from '../../api/DatalayerApi.js';
11
11
  import { getCookie, setCookie, deleteCookie } from '../../utils/index.js';
12
12
  import { coreStore } from './CoreState.js';
13
+ import { profileStore } from './ProfileState.js';
13
14
  /**
14
15
  * Limit to warn about low credits in milliseconds.
15
16
  */
@@ -66,6 +67,7 @@ export const iamStore = createStore((set, get) => {
66
67
  logout: () => {
67
68
  storeUser();
68
69
  storeToken();
70
+ profileStore.getState().clearProfile();
69
71
  set({
70
72
  credits: undefined,
71
73
  creditsReservations: [],
@@ -106,6 +108,7 @@ export const iamStore = createStore((set, get) => {
106
108
  ...user,
107
109
  },
108
110
  };
111
+ profileStore.getState().setProfileFromUser(updatedState.user);
109
112
  /*
110
113
  if (state.user?.email && !updatedState.user.email) {
111
114
  updatedState.user.email = state.user.email;
@@ -200,6 +203,9 @@ export const iamStore = createStore((set, get) => {
200
203
  setLogin: (user, token) => set((state) => {
201
204
  storeUser(user);
202
205
  storeToken(token);
206
+ // The profile store is what the surfaces (user menu, sidebars,
207
+ // profile views) read: every flow that learns the user feeds it.
208
+ profileStore.getState().setProfileFromUser(user);
203
209
  return {
204
210
  user,
205
211
  token,
@@ -4,6 +4,58 @@
4
4
  */
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { useStore } from 'zustand';
7
+ /**
8
+ * Name of the cookie holding the space last selected by the user.
9
+ *
10
+ * The selection has to survive a reload, as the whole user interface is scoped
11
+ * by it; it lives in a cookie next to the principal context written by
12
+ * `usePrincipalStore`.
13
+ */
14
+ const SPACE_CONTEXT_COOKIE = 'datalayer-space-context';
15
+ const SPACE_CONTEXT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
16
+ const readSpaceContextCookie = () => {
17
+ if (typeof document === 'undefined') {
18
+ return undefined;
19
+ }
20
+ const escaped = SPACE_CONTEXT_COOKIE.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
21
+ const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`));
22
+ if (!match?.[1]) {
23
+ return undefined;
24
+ }
25
+ try {
26
+ const parsed = JSON.parse(decodeURIComponent(match[1]));
27
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.id !== 'string') {
28
+ return undefined;
29
+ }
30
+ return {
31
+ id: parsed.id,
32
+ handle: typeof parsed.handle === 'string' ? parsed.handle : undefined,
33
+ name: typeof parsed.name === 'string' ? parsed.name : undefined,
34
+ };
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ };
40
+ const writeSpaceContextCookie = (space) => {
41
+ if (typeof document === 'undefined') {
42
+ return;
43
+ }
44
+ if (!space?.id) {
45
+ document.cookie =
46
+ `${SPACE_CONTEXT_COOKIE}=;` + ' path=/; max-age=0; SameSite=Lax';
47
+ return;
48
+ }
49
+ const value = encodeURIComponent(JSON.stringify({
50
+ id: space.id,
51
+ handle: space.handle,
52
+ name: space.name,
53
+ }));
54
+ document.cookie =
55
+ `${SPACE_CONTEXT_COOKIE}=${value};` +
56
+ ` path=/; max-age=${SPACE_CONTEXT_COOKIE_MAX_AGE}; SameSite=Lax`;
57
+ };
58
+ const initialSpaceContext = readSpaceContextCookie();
7
59
  export const layoutStore = createStore((set, get) => ({
8
60
  backdrop: undefined,
9
61
  banner: undefined,
@@ -15,7 +67,8 @@ export const layoutStore = createStore((set, get) => ({
15
67
  organization: undefined,
16
68
  rightPortal: undefined,
17
69
  screenCapture: undefined,
18
- space: undefined,
70
+ // Restored from the cookie; the full space is hydrated once loaded.
71
+ space: initialSpaceContext,
19
72
  team: undefined,
20
73
  hideBackdrop: () => set((state) => ({
21
74
  backdrop: { open: false, message: undefined },
@@ -65,13 +118,15 @@ export const layoutStore = createStore((set, get) => ({
65
118
  };
66
119
  }),
67
120
  updateLayoutSpace: (space) => set((state) => {
121
+ const next = space
122
+ ? {
123
+ ...state.space,
124
+ ...space,
125
+ }
126
+ : undefined;
127
+ writeSpaceContextCookie(next);
68
128
  return {
69
- space: space
70
- ? {
71
- ...state.space,
72
- ...space,
73
- }
74
- : undefined,
129
+ space: next,
75
130
  };
76
131
  }),
77
132
  setItem: (item) => set((state) => ({ item })),
@@ -4,6 +4,12 @@ export type IOrganizationState = {
4
4
  };
5
5
  export type OrganizationState = IOrganizationState & {
6
6
  updateOrganizations: (organizations: IAnyOrganization[]) => void;
7
+ /**
8
+ * Merge what just changed into the organization of the list — the way a
9
+ * settings save keeps every surface reading this store current (avatar,
10
+ * banner, name…) without refetching the list.
11
+ */
12
+ updateOrganization: (organization: Partial<IAnyOrganization> & Pick<IAnyOrganization, 'id'>) => void;
7
13
  };
8
14
  export declare const organizationStore: import("zustand").StoreApi<OrganizationState>;
9
15
  export declare function useOrganizationStore(): OrganizationState;
@@ -9,6 +9,11 @@ export const organizationStore = createStore((set, get) => ({
9
9
  updateOrganizations: (organizations) => set((state) => ({
10
10
  organizations,
11
11
  })),
12
+ updateOrganization: organization => set((state) => ({
13
+ organizations: state.organizations.map(known => known.id === organization.id
14
+ ? { ...known, ...organization }
15
+ : known),
16
+ })),
12
17
  }));
13
18
  export function useOrganizationStore(selector) {
14
19
  return useStore(organizationStore, selector);
@@ -0,0 +1,34 @@
1
+ import type { IUser } from '../../models';
2
+ /** The profile of the authenticated user, as the surfaces show it. */
3
+ export type IProfile = {
4
+ id?: string;
5
+ uid?: string;
6
+ handle?: string;
7
+ email?: string;
8
+ firstName?: string;
9
+ lastName?: string;
10
+ displayName?: string;
11
+ initials?: string;
12
+ avatarUrl?: string;
13
+ avatarIcon?: string;
14
+ banner?: string;
15
+ origin?: string;
16
+ roles?: string[];
17
+ };
18
+ export type ProfileState = {
19
+ profile?: IProfile;
20
+ /** Replace the profile wholesale. */
21
+ setProfile: (profile?: IProfile) => void;
22
+ /** Merge what a flow just learned into the profile. */
23
+ updateProfile: (partial: Partial<IProfile>) => void;
24
+ /** The IAM user as the profile — login and refresh feed through this. */
25
+ setProfileFromUser: (user?: IUser) => void;
26
+ /** Logout. */
27
+ clearProfile: () => void;
28
+ };
29
+ export declare const profileStore: import("zustand").StoreApi<ProfileState>;
30
+ export declare function useProfileStore(): ProfileState;
31
+ export declare function useProfileStore<T>(selector: (state: ProfileState) => T): T;
32
+ /** The profile, for surfaces that show it. */
33
+ export declare const useProfile: () => IProfile | undefined;
34
+ export default useProfileStore;
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * ProfileState — the one place the authenticated user's profile lives.
7
+ *
8
+ * The avatar and the banner used to be read from whatever carried them at
9
+ * each call site — the IAM user, a whoami response, a form snapshot — and
10
+ * the surfaces drifted: a banner updated on the profile page did not reach
11
+ * the user menu until the page reloaded. Every surface showing the profile
12
+ * (user menu, sidebars, profile views) reads this store instead, and every
13
+ * flow that learns something about the profile (login, `GET /me`, a profile
14
+ * update) writes it here.
15
+ *
16
+ * The store is fed automatically by {@link IAMState} on login, profile
17
+ * update and logout, and by the `useMe` query with the full profile — the
18
+ * consumers only ever read.
19
+ *
20
+ * @module state/substates/ProfileState
21
+ */
22
+ import { useStore } from 'zustand';
23
+ import { createStore } from 'zustand/vanilla';
24
+ const userToProfile = (user) => user
25
+ ? {
26
+ id: user.id,
27
+ uid: user.uid,
28
+ handle: user.handle,
29
+ email: user.email,
30
+ firstName: user.firstName,
31
+ lastName: user.lastName,
32
+ displayName: user.displayName,
33
+ initials: user.initials,
34
+ avatarUrl: user.avatarUrl,
35
+ avatarIcon: user.avatarIcon,
36
+ banner: user.banner,
37
+ origin: user.origin,
38
+ roles: user.roles,
39
+ }
40
+ : undefined;
41
+ export const profileStore = createStore(set => ({
42
+ profile: undefined,
43
+ setProfile: profile => set({ profile }),
44
+ updateProfile: partial => set(state => ({ profile: { ...state.profile, ...partial } })),
45
+ setProfileFromUser: user => set(state => {
46
+ const profile = userToProfile(user);
47
+ if (!profile) {
48
+ return state;
49
+ }
50
+ // A login refresh may know less than a profile update that already
51
+ // landed (the anonymous whoami has no banner): merge, newest wins,
52
+ // but never erase a field with undefined.
53
+ const merged = { ...state.profile };
54
+ for (const [key, value] of Object.entries(profile)) {
55
+ if (value !== undefined) {
56
+ merged[key] = value;
57
+ }
58
+ }
59
+ return { profile: merged };
60
+ }),
61
+ clearProfile: () => set({ profile: undefined }),
62
+ }));
63
+ export function useProfileStore(selector) {
64
+ return useStore(profileStore, selector);
65
+ }
66
+ /** The profile, for surfaces that show it. */
67
+ export const useProfile = () => useProfileStore(state => state.profile);
68
+ export default useProfileStore;
@@ -4,6 +4,12 @@ export type ITeamState = {
4
4
  };
5
5
  export type TeamState = ITeamState & {
6
6
  update: (teams: IBaseTeam[]) => void;
7
+ /**
8
+ * Merge what just changed into the team of the list — the way a settings
9
+ * save keeps every surface reading this store current (avatar, banner,
10
+ * name…) without refetching the list.
11
+ */
12
+ updateTeam: (team: Partial<IBaseTeam> & Pick<IBaseTeam, 'id'>) => void;
7
13
  };
8
14
  export declare const teamStore: import("zustand").StoreApi<TeamState>;
9
15
  export declare function useTeamStore(): TeamState;
@@ -10,6 +10,9 @@ export const teamStore = createStore((set, get) => ({
10
10
  update: (teams) => set((state) => ({
11
11
  teams,
12
12
  })),
13
+ updateTeam: team => set((state) => ({
14
+ teams: state.teams.map(known => known.id === team.id ? { ...known, ...team } : known),
15
+ })),
13
16
  }));
14
17
  export function useTeamStore(selector) {
15
18
  return useStore(teamStore, selector);
@@ -2,6 +2,7 @@ export * from './CoreState';
2
2
  export * from './DatasourceState';
3
3
  export * from './IAMState';
4
4
  export * from './LayoutState';
5
+ export * from './ProfileState';
5
6
  export * from './OrganizationState';
6
7
  export * from './SurveysState';
7
8
  export * from './TeamState';
@@ -6,6 +6,7 @@ export * from './CoreState.js';
6
6
  export * from './DatasourceState.js';
7
7
  export * from './IAMState.js';
8
8
  export * from './LayoutState.js';
9
+ export * from './ProfileState.js';
9
10
  export * from './OrganizationState.js';
10
11
  export * from './SurveysState.js';
11
12
  export * from './TeamState.js';
@@ -16,7 +16,7 @@ export interface SignInSimpleProps {
16
16
  onSignIn?: (token: string, handle: string) => void;
17
17
  /**
18
18
  * Called when the user authenticates with an API key.
19
- * If not provided the "Sign In with an API Key" button is hidden.
19
+ * If not provided the "Sign in with an API Key" button is hidden.
20
20
  */
21
21
  onApiKeySignIn?: (apiKey: string) => void;
22
22
  /**