@rebasepro/studio 0.23.0 → 0.24.1-canary.g56af4a4

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 (68) hide show
  1. package/dist/{ApiExplorer-pYMZflL3.js → ApiExplorer-Bzt7Ye49.js} +18 -29
  2. package/dist/ApiExplorer-Bzt7Ye49.js.map +1 -0
  3. package/dist/ApiKeysView-Bqf6XYR1.js +1739 -0
  4. package/dist/ApiKeysView-Bqf6XYR1.js.map +1 -0
  5. package/dist/BackupsView-BhOHB6iP.js +354 -0
  6. package/dist/BackupsView-BhOHB6iP.js.map +1 -0
  7. package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-BHfwGTYc.js} +16 -10
  8. package/dist/BranchesView-BHfwGTYc.js.map +1 -0
  9. package/dist/{CronJobsView-7hjgSYId.js → CronJobsView-CMNK7eWO.js} +175 -116
  10. package/dist/CronJobsView-CMNK7eWO.js.map +1 -0
  11. package/dist/{JSEditor-qiuwmcvk.js → JSEditor-DIO2Mw8q.js} +230 -165
  12. package/dist/JSEditor-DIO2Mw8q.js.map +1 -0
  13. package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-C-bwDFIl.js} +66 -44
  14. package/dist/LogsExplorer-C-bwDFIl.js.map +1 -0
  15. package/dist/{RLSEditor-Djh43lBE.js → RLSEditor-jFD_d06f.js} +246 -84
  16. package/dist/RLSEditor-jFD_d06f.js.map +1 -0
  17. package/dist/{SQLEditor-TBw_K6CW.js → SQLEditor-D5Ci4pyW.js} +502 -600
  18. package/dist/SQLEditor-D5Ci4pyW.js.map +1 -0
  19. package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-D-rAqwul.js} +496 -227
  20. package/dist/SchemaVisualizer-D-rAqwul.js.map +1 -0
  21. package/dist/{StorageView-CMG1DOVX.js → StorageView-CPkDKbsX.js} +838 -451
  22. package/dist/StorageView-CPkDKbsX.js.map +1 -0
  23. package/dist/assets/dagre-layout.worker-BFnhAzuR.js.map +1 -0
  24. package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
  25. package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
  26. package/dist/components/ApiKeys/scope-words.d.ts +37 -0
  27. package/dist/components/ApiKeys/scopes.d.ts +113 -0
  28. package/dist/components/AuthSimulationSelector.d.ts +5 -1
  29. package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
  30. package/dist/components/RLSEditor/policyListing.d.ts +44 -0
  31. package/dist/components/SQLEditor/SQLEditor.d.ts +11 -0
  32. package/dist/components/SchemaVisualizer/dagre-layout.d.ts +32 -0
  33. package/dist/components/SchemaVisualizer/dagre-layout.worker.d.ts +13 -0
  34. package/dist/components/SchemaVisualizer/layout-runner.d.ts +23 -0
  35. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +21 -11
  36. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +44 -1
  37. package/dist/components/StorageView/useVisibleFileMetadata.d.ts +46 -0
  38. package/dist/components/load-failure.d.ts +1 -1
  39. package/dist/components/useRunAsUsers.d.ts +25 -0
  40. package/dist/dagre-layout.worker-j9NbH9Gb.js +27 -0
  41. package/dist/dagre-layout.worker-j9NbH9Gb.js.map +1 -0
  42. package/dist/index.es.js +16 -14
  43. package/dist/index.es.js.map +1 -1
  44. package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
  45. package/dist/load-failure-Bd8lFIO7.js.map +1 -0
  46. package/dist/sql_utils-BZD31xE3.js +478 -0
  47. package/dist/sql_utils-BZD31xE3.js.map +1 -0
  48. package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
  49. package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
  50. package/dist/utils/sql_utils.d.ts +150 -49
  51. package/package.json +9 -9
  52. package/dist/ApiExplorer-pYMZflL3.js.map +0 -1
  53. package/dist/ApiKeysView-qLSs47tw.js +0 -1239
  54. package/dist/ApiKeysView-qLSs47tw.js.map +0 -1
  55. package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
  56. package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
  57. package/dist/BackupsView-DYTAD6aW.js +0 -253
  58. package/dist/BackupsView-DYTAD6aW.js.map +0 -1
  59. package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
  60. package/dist/CronJobsView-7hjgSYId.js.map +0 -1
  61. package/dist/JSEditor-qiuwmcvk.js.map +0 -1
  62. package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
  63. package/dist/RLSEditor-Djh43lBE.js.map +0 -1
  64. package/dist/SQLEditor-TBw_K6CW.js.map +0 -1
  65. package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
  66. package/dist/StorageView-CMG1DOVX.js.map +0 -1
  67. package/dist/components/ApiKeys/permissions.d.ts +0 -66
  68. package/dist/load-failure-C6OMJSGC.js.map +0 -1
@@ -1,12 +1,13 @@
1
- import { t as AuthSimulationSelector } from "./AuthSimulationSelector-BewmhMBw.js";
1
+ import { n as AuthSimulationSelector, t as useRunAsUsers } from "./useRunAsUsers-CLaAtXt6.js";
2
+ import { f as toCsv } from "./sql_utils-BZD31xE3.js";
2
3
  import { ErrorView, IconForView, useApiConfig, useModeController, useRebaseClient, useRebaseContext, useSnackbarController, useStudioCollectionRegistry, useStudioSidePanelController, useTranslation } from "@rebasepro/app";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
5
  import { Button, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, IconButton, Menu, MenuItem, MoreVerticalIcon, PencilIcon, PlayIcon, PlusIcon, ResizablePanels, SaveIcon, Tab, Tabs, TerminalIcon, TextField, Tooltip, Trash2Icon, Typography, VirtualTable, XIcon, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
7
  import { ALL_WHERE_FILTER_OPS } from "@rebasepro/types";
7
8
  import { toSnakeCase } from "@rebasepro/utils";
8
9
  import { Highlight, themes } from "prism-react-renderer";
9
- import { createRebaseClient } from "@rebasepro/client";
10
+ import { createMemoryStorage, createRebaseClient } from "@rebasepro/client";
10
11
  import Editor from "@monaco-editor/react";
11
12
  //#region src/components/JSEditor/JSMonacoEditor.tsx
12
13
  /**
@@ -138,10 +139,29 @@ interface AdminUser {
138
139
  updatedAt: string;
139
140
  }
140
141
 
142
+ interface ListUsersOptions {
143
+ search?: string;
144
+ /** Page size. The server's default is 25. */
145
+ limit?: number;
146
+ offset?: number;
147
+ orderBy?: string;
148
+ orderDir?: "asc" | "desc";
149
+ }
150
+
151
+ /** One page of users, and \`total\`: how many match in all. */
152
+ interface UsersPage {
153
+ users: AdminUser[];
154
+ total: number;
155
+ limit: number;
156
+ offset: number;
157
+ }
158
+
141
159
  interface RebaseCMS {
142
- listUsers(): Promise<{ users: AdminUser[] }>;
160
+ /** One page of users (25 unless \`limit\` says otherwise) and the total. */
161
+ listUsers(options?: ListUsersOptions): Promise<UsersPage>;
162
+ listUsersPaginated(options?: ListUsersOptions): Promise<UsersPage>;
143
163
  getUser(userId: string): Promise<{ user: AdminUser }>;
144
- createUser(data: { email: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
164
+ createUser(data: { email: string; displayName?: string; password?: string; roles?: string[]; skipPasswordPolicy?: boolean }): Promise<{ user: AdminUser }>;
145
165
  updateUser(userId: string, data: { email?: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
146
166
  deleteUser(userId: string): Promise<{ success: boolean }>;
147
167
  bootstrap(): Promise<{ success: boolean; message: string; user: { uid: string; roles: string[] } }>;
@@ -200,8 +220,8 @@ type RebaseData = {
200
220
  * // Auth
201
221
  * const session = client.auth.getSession();
202
222
  *
203
- * // Admin
204
- * const { users } = await client.admin.listUsers();
223
+ * // Admin: one page of users, and how many there are
224
+ * const { users, total } = await client.admin.listUsers({ limit: 50 });
205
225
  *
206
226
  * // Custom endpoint
207
227
  * const result = await client.call("/my-endpoint", { myData: 123 });
@@ -221,12 +241,16 @@ interface RebaseClient {
221
241
  [collectionSlug: string]: unknown;
222
242
  }
223
243
 
224
- /** The pre-configured client instance. Already authenticated with the current user session. */
244
+ /**
245
+ * The client for this run: your own, one that runs every request as the user
246
+ * picked in "Run as" (it holds no session, so \`client.auth.getSession()\` is null),
247
+ * or an unauthenticated one under "No Auth".
248
+ */
225
249
  declare const client: RebaseClient;
226
250
 
227
251
  /** Execution context with user and collection information. */
228
252
  interface JSEditorContext {
229
- /** The user the script is running as. */
253
+ /** The user the script runs as: the one picked in "Run as", or you. \`null\` under "No Auth". */
230
254
  user: {
231
255
  uid: string;
232
256
  displayName: string | null;
@@ -359,15 +383,20 @@ var QUICK_REFERENCE = [
359
383
  code: "const result = await client.data.collection(\"COLLECTION\")\n .where(\"field\", \">\", value)\n .orderBy(\"field\", \"desc\")\n .limit(10)\n .find();\nreturn result;",
360
384
  description: "Chained query builder"
361
385
  },
386
+ {
387
+ label: "Who am I",
388
+ code: "return context.user;",
389
+ description: "The user this script runs as, or null under No Auth"
390
+ },
362
391
  {
363
392
  label: "Auth: Current session",
364
393
  code: "const session = client.auth.getSession();\nreturn session;",
365
- description: "Get current auth session"
394
+ description: "The session this client holds: yours, or none under Run as and No Auth"
366
395
  },
367
396
  {
368
397
  label: "Admin: List users",
369
- code: "const { users } = await client.admin.listUsers();\nreturn users;",
370
- description: "List all registered users"
398
+ code: "// One page: raise limit or page with offset while users.length < total\nconst { users, total } = await client.admin.listUsers({ limit: 50, offset: 0 });\nreturn { users, total };",
399
+ description: "A page of users, and how many there are in all"
371
400
  },
372
401
  {
373
402
  label: "Custom endpoint",
@@ -376,6 +405,7 @@ var QUICK_REFERENCE = [
376
405
  }
377
406
  ];
378
407
  var JSEditorSidebar = ({ collections, snippets, history, onSelectSnippet, onDeleteSnippet, onInsertCode }) => {
408
+ const { t } = useTranslation();
379
409
  const [activeTab, setActiveTab] = useState("collections");
380
410
  return /* @__PURE__ */ jsxs("div", {
381
411
  className: cls("flex flex-col h-full w-full bg-surface-card border-r", defaultBorderMixin),
@@ -391,7 +421,7 @@ var JSEditorSidebar = ({ collections, snippets, history, onSelectSnippet, onDele
391
421
  title: "Browse collections",
392
422
  children: /* @__PURE__ */ jsx("span", {
393
423
  className: "text-xs",
394
- children: "Collections"
424
+ children: t("studio_js_collections_tab")
395
425
  })
396
426
  })
397
427
  }),
@@ -430,7 +460,7 @@ var JSEditorSidebar = ({ collections, snippets, history, onSelectSnippet, onDele
430
460
  children: /* @__PURE__ */ jsx(Typography, {
431
461
  variant: "caption",
432
462
  className: "font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark",
433
- children: "Collections"
463
+ children: t("studio_js_collections_tab")
434
464
  })
435
465
  }), /* @__PURE__ */ jsx("div", {
436
466
  className: "flex-grow overflow-y-auto p-1 space-y-1 no-scrollbar",
@@ -618,6 +648,7 @@ function CollectionItem({ collection, onInsertCode }) {
618
648
  /* @__PURE__ */ jsx(Tooltip, {
619
649
  title: "Insert find() snippet",
620
650
  children: /* @__PURE__ */ jsx(IconButton, {
651
+ "aria-label": "Insert find() snippet",
621
652
  size: "smallest",
622
653
  className: "opacity-0 group-hover:opacity-100 text-text-disabled hover:text-primary transition-all",
623
654
  onClick: (e) => {
@@ -653,6 +684,7 @@ function CollectionItem({ collection, onInsertCode }) {
653
684
  })] });
654
685
  }
655
686
  function SnippetItem({ snippet, onSelect, onDelete }) {
687
+ const { t } = useTranslation();
656
688
  return /* @__PURE__ */ jsxs("div", {
657
689
  className: cls("group p-2 rounded border hover:border-hairline-strong bg-surface-card transition-all cursor-pointer relative", defaultBorderMixin),
658
690
  onClick: () => onSelect(snippet.code),
@@ -668,6 +700,7 @@ function SnippetItem({ snippet, onSelect, onDelete }) {
668
700
  children: snippet.code
669
701
  }),
670
702
  /* @__PURE__ */ jsx(IconButton, {
703
+ "aria-label": t("delete"),
671
704
  size: "smallest",
672
705
  className: "absolute right-1 top-1 opacity-0 group-hover:opacity-100 text-text-disabled hover:text-red-500 transition-opacity",
673
706
  onClick: (e) => {
@@ -690,12 +723,12 @@ var STORAGE_PREFIX = "rebase_js_";
690
723
  * new user did be a 404 — a placeholder presented as working code. `slug` is
691
724
  * the project's first collection when one is known.
692
725
  */
693
- var makeDefaultCode = (slug) => `// Available: client (RebaseClient)
726
+ var makeDefaultCode = (slug) => `// Available: client (RebaseClient), context
694
727
  // Press Cmd+Enter (Ctrl+Enter) to run
695
728
  //
696
729
  // Examples:
697
- // const users = await client.admin.listUsers();
698
- // const session = client.auth.getSession();
730
+ // const { users, total } = await client.admin.listUsers({ limit: 50, offset: 0 }); // one page of total
731
+ // const me = context.user; // who this script runs as ("Run as"), null under No Auth
699
732
  ${slug ? `
700
733
  const result = await client.data.collection(${JSON.stringify(slug)}).find({ limit: 10 });
701
734
  return result;
@@ -704,9 +737,24 @@ return result;
704
737
  // const result = await client.data.collection("posts").find({ limit: 10 });
705
738
  // return result;
706
739
 
707
- return client.auth.getSession();
740
+ return context.user;
708
741
  `}`;
709
742
  var DEFAULT_CODE = makeDefaultCode();
743
+ /**
744
+ * The auth of a client built for one run: its own, in memory, and empty.
745
+ *
746
+ * The SDK's default auth restores the session the app saved in `localStorage`,
747
+ * sends its token on every request and keeps refreshing it. A run under "No
748
+ * Auth" or as another user must do none of that: it would go out as the
749
+ * signed-in user, and `client.auth` would describe the wrong person.
750
+ */
751
+ function runOnlyAuth() {
752
+ return {
753
+ storage: createMemoryStorage(),
754
+ persistSession: false,
755
+ autoRefresh: false
756
+ };
757
+ }
710
758
  function loadFromStorage(key, fallback) {
711
759
  try {
712
760
  const raw = localStorage.getItem(STORAGE_PREFIX + key);
@@ -727,14 +775,22 @@ function formatJSON(value) {
727
775
  return String(value);
728
776
  }
729
777
  }
778
+ /** The script without its comments: a collection named in one is not where rows came from. */
779
+ function withoutComments(code) {
780
+ return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
781
+ }
730
782
  /**
731
- * Given the raw SDK result, try to detect which collections are present.
783
+ * Given the raw SDK result, try to detect which collection its rows belong to.
732
784
  * JS SDK results typically come back as `{ data: [{ id, values }] }` or plain arrays.
733
- * The heuristic: if the executed code contains `collection("<slug>")` or `client.data.<slug>`,
734
- * and the result has rows with an "id" column, we match those slugs.
785
+ * The heuristic: the executed code, outside its comments, names exactly one
786
+ * collection — `collection("<slug>")` or `client.data.<slug>` — and the result has
787
+ * rows with an "id" column. A script naming two could have returned either's
788
+ * rows, and an action on a row would open the other collection's row with the
789
+ * same id, so it gets none.
735
790
  */
736
- function detectCollectionsInResult(code, resultValue, collections) {
791
+ function detectCollectionsInResult(executedCode, resultValue, collections) {
737
792
  if (!resultValue || !collections?.length) return [];
793
+ const code = withoutComments(executedCode);
738
794
  const mentionedSlugs = /* @__PURE__ */ new Set();
739
795
  const collectionCallRegex = /\.collection\(["']([^"']+)["']\)/g;
740
796
  let m;
@@ -748,7 +804,7 @@ function detectCollectionsInResult(code, resultValue, collections) {
748
804
  "update",
749
805
  "delete"
750
806
  ].includes(m[1])) mentionedSlugs.add(m[1]);
751
- if (mentionedSlugs.size === 0) return [];
807
+ if (mentionedSlugs.size !== 1) return [];
752
808
  let rows = [];
753
809
  const rv = resultValue;
754
810
  if (rv?.data && Array.isArray(rv.data)) rows = rv.data;
@@ -839,27 +895,7 @@ function JSEditor() {
839
895
  } : tab);
840
896
  });
841
897
  }, [firstSlug]);
842
- const users = useMemo(() => {
843
- const managed = [];
844
- if (currentUser) managed.push({
845
- uid: currentUser.uid,
846
- displayName: currentUser.displayName,
847
- email: currentUser.email,
848
- photoURL: currentUser.photoURL,
849
- roles: currentUser.roles
850
- });
851
- return managed;
852
- }, [currentUser]);
853
- const currentSelectableUser = useMemo(() => {
854
- if (!currentUser) return null;
855
- return {
856
- uid: currentUser.uid,
857
- displayName: currentUser.displayName,
858
- email: currentUser.email,
859
- photoURL: currentUser.photoURL,
860
- roles: currentUser.roles
861
- };
862
- }, [currentUser]);
898
+ const { users, currentUser: currentSelectableUser, loading: usersLoading, onSearchTextChange: onUserSearchTextChange, totalCount: userTotalCount } = useRunAsUsers();
863
899
  useEffect(() => {
864
900
  saveToStorage("tabs", tabs);
865
901
  }, [tabs]);
@@ -918,29 +954,32 @@ function JSEditor() {
918
954
  const apiUrl = apiConfig?.apiUrl;
919
955
  const getAuthToken = apiConfig?.getAuthToken;
920
956
  if (!apiUrl) throw new Error("API URL not configured. Make sure apiUrl is set.");
921
- if (authMode === "none") return {
922
- client: createRebaseClient({
957
+ if (authMode === "none") {
958
+ const client = createRebaseClient({
923
959
  baseUrl: apiUrl,
924
- token: void 0
925
- }),
926
- isScoped: true
927
- };
928
- if (!selectedUser || currentUser && selectedUser.uid === currentUser.uid) {
929
- if (!rebaseClient) throw new Error("Application client is not initialized.");
960
+ auth: runOnlyAuth()
961
+ });
930
962
  return {
931
- client: rebaseClient,
932
- isScoped: false
963
+ client,
964
+ release: () => client.close()
933
965
  };
934
966
  }
935
- let token;
936
- if (getAuthToken) token = await getAuthToken() ?? void 0;
937
- if (!token) throw new Error("No auth token available. Please sign in first.");
967
+ if (!selectedUser || currentUser && selectedUser.uid === currentUser.uid) {
968
+ if (!rebaseClient) throw new Error("Application client is not initialized.");
969
+ return { client: rebaseClient };
970
+ }
971
+ const token = getAuthToken ? await getAuthToken() ?? void 0 : void 0;
972
+ if (!getAuthToken || !token) throw new Error("No auth token available. Please sign in first.");
973
+ const client = createRebaseClient({
974
+ baseUrl: apiUrl,
975
+ token,
976
+ impersonate: selectedUser.uid,
977
+ auth: runOnlyAuth()
978
+ });
979
+ client.setAuthTokenGetter(getAuthToken);
938
980
  return {
939
- client: createRebaseClient({
940
- baseUrl: apiUrl,
941
- token
942
- }),
943
- isScoped: true
981
+ client,
982
+ release: () => client.close()
944
983
  };
945
984
  }, [
946
985
  apiConfig,
@@ -959,7 +998,8 @@ function JSEditor() {
959
998
  });
960
999
  const consoleEntries = [];
961
1000
  const startTime = performance.now();
962
- let scopedClientToCleanUp = null;
1001
+ let release;
1002
+ const ranAsSignedInUser = authMode !== "none" && (!selectedUser || selectedUser.uid === currentUser?.uid);
963
1003
  const originalConsole = {
964
1004
  log: console.log,
965
1005
  warn: console.warn,
@@ -979,10 +1019,11 @@ function JSEditor() {
979
1019
  console.warn = captureConsole("warn");
980
1020
  console.error = captureConsole("error");
981
1021
  console.info = captureConsole("info");
982
- const { client, isScoped } = await buildClient();
983
- if (isScoped) scopedClientToCleanUp = client;
1022
+ const built = await buildClient();
1023
+ release = built.release;
1024
+ const client = built.client;
984
1025
  const context = {
985
- user: selectedUser ?? (currentUser ? {
1026
+ user: authMode === "none" ? null : selectedUser ?? (currentUser ? {
986
1027
  uid: currentUser.uid,
987
1028
  displayName: currentUser.displayName,
988
1029
  email: currentUser.email,
@@ -997,7 +1038,9 @@ function JSEditor() {
997
1038
  value,
998
1039
  console: consoleEntries,
999
1040
  duration,
1000
- timestamp: Date.now()
1041
+ timestamp: Date.now(),
1042
+ code,
1043
+ ranAsSignedInUser
1001
1044
  });
1002
1045
  const resultObj = value;
1003
1046
  if (resultObj?.data && Array.isArray(resultObj.data)) setResultView("table");
@@ -1010,7 +1053,9 @@ function JSEditor() {
1010
1053
  console: consoleEntries,
1011
1054
  duration,
1012
1055
  error: err instanceof Error ? err.message : String(err),
1013
- timestamp: Date.now()
1056
+ timestamp: Date.now(),
1057
+ code,
1058
+ ranAsSignedInUser
1014
1059
  });
1015
1060
  setResultView("json");
1016
1061
  } finally {
@@ -1019,11 +1064,12 @@ function JSEditor() {
1019
1064
  console.error = originalConsole.error;
1020
1065
  console.info = originalConsole.info;
1021
1066
  setIsRunning(false);
1022
- if (scopedClientToCleanUp) scopedClientToCleanUp.ws?.disconnect();
1067
+ release?.();
1023
1068
  }
1024
1069
  }, [
1025
1070
  activeTab?.code,
1026
1071
  buildClient,
1072
+ authMode,
1027
1073
  selectedUser,
1028
1074
  currentUser,
1029
1075
  collectionInfos
@@ -1092,13 +1138,9 @@ function JSEditor() {
1092
1138
  };
1093
1139
  }, [result]);
1094
1140
  const matchedCollections = useMemo(() => {
1095
- if (!result?.value || result.error) return [];
1096
- return detectCollectionsInResult(activeTab?.code ?? "", result.value, collectionRegistry?.collections ?? []);
1097
- }, [
1098
- result,
1099
- activeTab?.code,
1100
- collectionRegistry?.collections
1101
- ]);
1141
+ if (!result?.value || result.error || !result.ranAsSignedInUser) return [];
1142
+ return detectCollectionsInResult(result.code, result.value, collectionRegistry?.collections ?? []);
1143
+ }, [result, collectionRegistry?.collections]);
1102
1144
  const getRowEntityActions = useCallback((rowData) => {
1103
1145
  if (!rowData || matchedCollections.length === 0) return [];
1104
1146
  return matchedCollections.filter((mc) => rowData[mc.pkColumn] != null).map((mc) => ({
@@ -1106,13 +1148,97 @@ function JSEditor() {
1106
1148
  entityId: rowData[mc.pkColumn]
1107
1149
  }));
1108
1150
  }, [matchedCollections]);
1151
+ const resultColumns = useMemo(() => matchedCollections.length > 0 ? [{
1152
+ key: "__entity_action__",
1153
+ title: "",
1154
+ width: 36,
1155
+ sortable: false,
1156
+ resizable: false
1157
+ }, ...tableData.columns] : tableData.columns, [matchedCollections.length, tableData.columns]);
1158
+ const renderResultCell = useCallback(({ rowData, column }) => {
1159
+ if (column.key === "__entity_action__") {
1160
+ const rowActions = getRowEntityActions(rowData ?? {});
1161
+ if (rowActions.length === 0) return /* @__PURE__ */ jsx("div", { className: "h-full w-full" });
1162
+ if (rowActions.length === 1) {
1163
+ const ra = rowActions[0];
1164
+ return /* @__PURE__ */ jsx("div", {
1165
+ className: "h-full flex items-center justify-center",
1166
+ children: /* @__PURE__ */ jsx(Tooltip, {
1167
+ title: t("studio_sql_edit_entity", {
1168
+ name: ra.collection.collection.name,
1169
+ id: String(ra.entityId)
1170
+ }),
1171
+ children: /* @__PURE__ */ jsx(IconButton, {
1172
+ "aria-label": t("studio_sql_edit_entity", {
1173
+ name: ra.collection.collection.name,
1174
+ id: String(ra.entityId)
1175
+ }),
1176
+ size: "small",
1177
+ className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300",
1178
+ onClick: (e) => {
1179
+ e.stopPropagation();
1180
+ sidePanelController.open({
1181
+ path: ra.collection.collectionSlug,
1182
+ entityId: ra.entityId,
1183
+ collection: ra.collection.collection,
1184
+ updateUrl: false
1185
+ });
1186
+ },
1187
+ children: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.smallest })
1188
+ })
1189
+ })
1190
+ });
1191
+ }
1192
+ return /* @__PURE__ */ jsx("div", {
1193
+ className: "h-full flex items-center justify-center",
1194
+ children: /* @__PURE__ */ jsx(Menu, {
1195
+ trigger: /* @__PURE__ */ jsx(IconButton, {
1196
+ "aria-label": t("more_actions"),
1197
+ size: "small",
1198
+ className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300",
1199
+ onClick: (e) => e.stopPropagation(),
1200
+ children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: iconSize.smallest })
1201
+ }),
1202
+ children: rowActions.map((ra) => /* @__PURE__ */ jsx(MenuItem, {
1203
+ dense: true,
1204
+ onClick: () => {
1205
+ sidePanelController.open({
1206
+ path: ra.collection.collectionSlug,
1207
+ entityId: ra.entityId,
1208
+ collection: ra.collection.collection,
1209
+ updateUrl: false
1210
+ });
1211
+ },
1212
+ children: t("studio_sql_edit_entity", {
1213
+ name: ra.collection.collection.name,
1214
+ id: String(ra.entityId)
1215
+ })
1216
+ }, ra.collection.collectionSlug))
1217
+ })
1218
+ });
1219
+ }
1220
+ if (!rowData) return null;
1221
+ const val = rowData[column.key];
1222
+ const displayValue = typeof val === "object" && val !== null ? JSON.stringify(val) : String(val ?? "");
1223
+ return /* @__PURE__ */ jsx("div", {
1224
+ className: "px-4 py-1.5 h-full flex items-center whitespace-nowrap text-[13px] text-text-primary dark:text-text-primary-dark font-mono",
1225
+ children: /* @__PURE__ */ jsx("div", {
1226
+ className: "truncate flex-grow",
1227
+ title: displayValue,
1228
+ children: displayValue === "" ? /* @__PURE__ */ jsx("span", {
1229
+ className: "text-text-disabled dark:text-text-disabled-dark italic text-[11px]",
1230
+ children: "NULL"
1231
+ }) : displayValue
1232
+ })
1233
+ });
1234
+ }, [
1235
+ getRowEntityActions,
1236
+ sidePanelController,
1237
+ t
1238
+ ]);
1109
1239
  const handleExportCSV = useCallback(() => {
1110
1240
  if (tableData.data.length === 0) return;
1111
- const csv = [tableData.columns.map((c) => c.key).join(","), ...tableData.data.map((row) => tableData.columns.map((c) => {
1112
- const val = row[c.key];
1113
- const str = val === null || val === void 0 ? "" : String(val);
1114
- return str.includes(",") ? `"${str}"` : str;
1115
- }).join(","))].join("\n");
1241
+ const csv = toCsv(tableData.columns.map((c) => c.key), tableData.data);
1116
1242
  const blob = new Blob([csv], { type: "text/csv" });
1117
1243
  const url = URL.createObjectURL(blob);
1118
1244
  const a = document.createElement("a");
@@ -1154,6 +1280,7 @@ function JSEditor() {
1154
1280
  children: [/* @__PURE__ */ jsx("div", {
1155
1281
  className: "flex-grow overflow-hidden",
1156
1282
  children: /* @__PURE__ */ jsx(ResizablePanels, {
1283
+ dividerAriaLabel: t("resize_sidebar"),
1157
1284
  orientation: "horizontal",
1158
1285
  panelSizePercent: sidebarSize,
1159
1286
  onPanelSizeChange: setSidebarSize,
@@ -1193,6 +1320,7 @@ function JSEditor() {
1193
1320
  children: tab.name
1194
1321
  }),
1195
1322
  tabs.length > 1 && /* @__PURE__ */ jsx(IconButton, {
1323
+ "aria-label": t("close_tab"),
1196
1324
  size: "smallest",
1197
1325
  onClick: (e) => {
1198
1326
  e.stopPropagation();
@@ -1204,6 +1332,7 @@ function JSEditor() {
1204
1332
  ]
1205
1333
  }, tab.id))
1206
1334
  }), /* @__PURE__ */ jsx(IconButton, {
1335
+ "aria-label": t("new_tab"),
1207
1336
  size: "small",
1208
1337
  onClick: addTab,
1209
1338
  className: "ml-2 flex-shrink-0",
@@ -1216,6 +1345,7 @@ function JSEditor() {
1216
1345
  /* @__PURE__ */ jsx(Tooltip, {
1217
1346
  title: "Save as snippet",
1218
1347
  children: /* @__PURE__ */ jsx(IconButton, {
1348
+ "aria-label": "Save as snippet",
1219
1349
  size: "small",
1220
1350
  onClick: () => {
1221
1351
  setSnippetName("");
@@ -1228,6 +1358,7 @@ function JSEditor() {
1228
1358
  result?.value != null && /* @__PURE__ */ jsx(Tooltip, {
1229
1359
  title: "Export result as JSON",
1230
1360
  children: /* @__PURE__ */ jsx(IconButton, {
1361
+ "aria-label": "Export result as JSON",
1231
1362
  size: "small",
1232
1363
  onClick: exportResult,
1233
1364
  children: /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest })
@@ -1251,6 +1382,7 @@ function JSEditor() {
1251
1382
  }), /* @__PURE__ */ jsx("div", {
1252
1383
  className: "flex-grow overflow-hidden",
1253
1384
  children: /* @__PURE__ */ jsx(ResizablePanels, {
1385
+ dividerAriaLabel: t("resize_editor"),
1254
1386
  orientation: "vertical",
1255
1387
  panelSizePercent: editorHeight,
1256
1388
  onPanelSizeChange: setEditorHeight,
@@ -1265,7 +1397,9 @@ function JSEditor() {
1265
1397
  selectedUser,
1266
1398
  setSelectedUser,
1267
1399
  users,
1268
- loading: false,
1400
+ loading: usersLoading,
1401
+ onUserSearchTextChange,
1402
+ userTotalCount,
1269
1403
  currentUser: currentSelectableUser
1270
1404
  })
1271
1405
  }), /* @__PURE__ */ jsx("div", {
@@ -1406,87 +1540,10 @@ function JSEditor() {
1406
1540
  className: "flex-grow relative h-full min-h-0 min-w-0",
1407
1541
  children: /* @__PURE__ */ jsx(VirtualTable, {
1408
1542
  data: tableData.data,
1409
- columns: matchedCollections.length > 0 ? [{
1410
- key: "__entity_action__",
1411
- title: "",
1412
- width: 36,
1413
- sortable: false,
1414
- resizable: false
1415
- }, ...tableData.columns] : tableData.columns,
1543
+ columns: resultColumns,
1416
1544
  rowHeight: 32,
1417
1545
  headerHeight: 32,
1418
- cellRenderer: ({ rowData, column, rowIndex }) => {
1419
- if (column.key === "__entity_action__") {
1420
- const rowActions = getRowEntityActions(rowData ?? {});
1421
- if (rowActions.length === 0) return /* @__PURE__ */ jsx("div", { className: "h-full w-full" });
1422
- if (rowActions.length === 1) {
1423
- const ra = rowActions[0];
1424
- return /* @__PURE__ */ jsx("div", {
1425
- className: "h-full flex items-center justify-center",
1426
- children: /* @__PURE__ */ jsx(Tooltip, {
1427
- title: t("studio_sql_edit_entity", {
1428
- name: ra.collection.collection.name,
1429
- id: String(ra.entityId)
1430
- }),
1431
- children: /* @__PURE__ */ jsx(IconButton, {
1432
- size: "small",
1433
- className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300",
1434
- onClick: (e) => {
1435
- e.stopPropagation();
1436
- sidePanelController.open({
1437
- path: ra.collection.collectionSlug,
1438
- entityId: ra.entityId,
1439
- collection: ra.collection.collection,
1440
- updateUrl: false
1441
- });
1442
- },
1443
- children: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.smallest })
1444
- })
1445
- })
1446
- });
1447
- }
1448
- return /* @__PURE__ */ jsx("div", {
1449
- className: "h-full flex items-center justify-center",
1450
- children: /* @__PURE__ */ jsx(Menu, {
1451
- trigger: /* @__PURE__ */ jsx(IconButton, {
1452
- size: "small",
1453
- className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300",
1454
- onClick: (e) => e.stopPropagation(),
1455
- children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: iconSize.smallest })
1456
- }),
1457
- children: rowActions.map((ra) => /* @__PURE__ */ jsx(MenuItem, {
1458
- dense: true,
1459
- onClick: () => {
1460
- sidePanelController.open({
1461
- path: ra.collection.collectionSlug,
1462
- entityId: ra.entityId,
1463
- collection: ra.collection.collection,
1464
- updateUrl: false
1465
- });
1466
- },
1467
- children: t("studio_sql_edit_entity", {
1468
- name: ra.collection.collection.name,
1469
- id: String(ra.entityId)
1470
- })
1471
- }, ra.collection.collectionSlug))
1472
- })
1473
- });
1474
- }
1475
- if (!rowData) return null;
1476
- const val = rowData[column.key];
1477
- const displayValue = typeof val === "object" && val !== null ? JSON.stringify(val) : String(val ?? "");
1478
- return /* @__PURE__ */ jsx("div", {
1479
- className: "px-4 py-1.5 h-full flex items-center whitespace-nowrap text-[13px] text-text-primary dark:text-text-primary-dark font-mono",
1480
- children: /* @__PURE__ */ jsx("div", {
1481
- className: "truncate flex-grow",
1482
- title: displayValue,
1483
- children: displayValue === "" ? /* @__PURE__ */ jsx("span", {
1484
- className: "text-text-disabled dark:text-text-disabled-dark italic text-[11px]",
1485
- children: "NULL"
1486
- }) : displayValue
1487
- })
1488
- });
1489
- }
1546
+ cellRenderer: renderResultCell
1490
1547
  })
1491
1548
  })]
1492
1549
  }),
@@ -1513,7 +1570,7 @@ function JSEditor() {
1513
1570
  children: "Return Value"
1514
1571
  }), /* @__PURE__ */ jsx("pre", {
1515
1572
  className: "text-text-primary dark:text-text-primary-dark whitespace-pre-wrap break-words text-[12px]",
1516
- children: formatJSON(result.value)
1573
+ children: /* @__PURE__ */ jsx(FormattedJSON, { value: result.value })
1517
1574
  })]
1518
1575
  })]
1519
1576
  })
@@ -1605,7 +1662,11 @@ function JSEditor() {
1605
1662
  })]
1606
1663
  });
1607
1664
  }
1608
- function JSONHighlight({ value }) {
1665
+ /**
1666
+ * Memoised on the value: tokenising a large result is the costliest thing
1667
+ * this pane draws, and it used to run again for every keystroke in the editor.
1668
+ */
1669
+ var JSONHighlight = React.memo(function JSONHighlight({ value }) {
1609
1670
  const json = formatJSON(value);
1610
1671
  const { mode } = useModeController();
1611
1672
  return /* @__PURE__ */ jsx(Highlight, {
@@ -1623,8 +1684,12 @@ function JSONHighlight({ value }) {
1623
1684
  }, i))
1624
1685
  })
1625
1686
  });
1626
- }
1687
+ });
1688
+ /** A value as indented JSON, formatted again only when the value changes. */
1689
+ var FormattedJSON = React.memo(function FormattedJSON({ value }) {
1690
+ return /* @__PURE__ */ jsx(Fragment, { children: formatJSON(value) });
1691
+ });
1627
1692
  //#endregion
1628
1693
  export { JSEditor };
1629
1694
 
1630
- //# sourceMappingURL=JSEditor-qiuwmcvk.js.map
1695
+ //# sourceMappingURL=JSEditor-DIO2Mw8q.js.map