@rebasepro/studio 0.23.0 → 0.24.0

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 (57) hide show
  1. package/dist/{ApiExplorer-pYMZflL3.js → ApiExplorer-CCtczj3F.js} +15 -28
  2. package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
  3. package/dist/ApiKeysView-B66TdzhH.js +1738 -0
  4. package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
  5. package/dist/BackupsView-DmSMzAbT.js +368 -0
  6. package/dist/BackupsView-DmSMzAbT.js.map +1 -0
  7. package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
  8. package/dist/BranchesView-DP1a87E-.js.map +1 -0
  9. package/dist/{CronJobsView-7hjgSYId.js → CronJobsView-TDErHTuK.js} +126 -60
  10. package/dist/CronJobsView-TDErHTuK.js.map +1 -0
  11. package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
  12. package/dist/JSEditor-B59pwg4c.js.map +1 -0
  13. package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
  14. package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
  15. package/dist/{RLSEditor-Djh43lBE.js → RLSEditor-BlLLV6xs.js} +237 -77
  16. package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
  17. package/dist/{SQLEditor-TBw_K6CW.js → SQLEditor-1An7w0j6.js} +369 -294
  18. package/dist/SQLEditor-1An7w0j6.js.map +1 -0
  19. package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
  20. package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
  21. package/dist/{StorageView-CMG1DOVX.js → StorageView-CbsHuCBf.js} +355 -225
  22. package/dist/StorageView-CbsHuCBf.js.map +1 -0
  23. package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
  24. package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
  25. package/dist/components/ApiKeys/scope-words.d.ts +37 -0
  26. package/dist/components/ApiKeys/scopes.d.ts +113 -0
  27. package/dist/components/AuthSimulationSelector.d.ts +5 -1
  28. package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
  29. package/dist/components/RLSEditor/policyListing.d.ts +44 -0
  30. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
  31. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
  32. package/dist/components/load-failure.d.ts +1 -1
  33. package/dist/components/useRunAsUsers.d.ts +25 -0
  34. package/dist/index.es.js +11 -11
  35. package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
  36. package/dist/load-failure-Bd8lFIO7.js.map +1 -0
  37. package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
  38. package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
  39. package/dist/utils/sql_utils.d.ts +90 -49
  40. package/package.json +9 -9
  41. package/dist/ApiExplorer-pYMZflL3.js.map +0 -1
  42. package/dist/ApiKeysView-qLSs47tw.js +0 -1239
  43. package/dist/ApiKeysView-qLSs47tw.js.map +0 -1
  44. package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
  45. package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
  46. package/dist/BackupsView-DYTAD6aW.js +0 -253
  47. package/dist/BackupsView-DYTAD6aW.js.map +0 -1
  48. package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
  49. package/dist/CronJobsView-7hjgSYId.js.map +0 -1
  50. package/dist/JSEditor-qiuwmcvk.js.map +0 -1
  51. package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
  52. package/dist/RLSEditor-Djh43lBE.js.map +0 -1
  53. package/dist/SQLEditor-TBw_K6CW.js.map +0 -1
  54. package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
  55. package/dist/StorageView-CMG1DOVX.js.map +0 -1
  56. package/dist/components/ApiKeys/permissions.d.ts +0 -66
  57. package/dist/load-failure-C6OMJSGC.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { t as AuthSimulationSelector } from "./AuthSimulationSelector-BewmhMBw.js";
1
+ import { n as AuthSimulationSelector, t as useRunAsUsers } from "./useRunAsUsers-CLaAtXt6.js";
2
2
  import { ErrorView, IconForView, useApiConfig, useModeController, useRebaseClient, useRebaseContext, useSnackbarController, useStudioCollectionRegistry, useStudioSidePanelController, useTranslation } from "@rebasepro/app";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  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";
@@ -6,7 +6,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { ALL_WHERE_FILTER_OPS } from "@rebasepro/types";
7
7
  import { toSnakeCase } from "@rebasepro/utils";
8
8
  import { Highlight, themes } from "prism-react-renderer";
9
- import { createRebaseClient } from "@rebasepro/client";
9
+ import { createMemoryStorage, createRebaseClient } from "@rebasepro/client";
10
10
  import Editor from "@monaco-editor/react";
11
11
  //#region src/components/JSEditor/JSMonacoEditor.tsx
12
12
  /**
@@ -138,8 +138,27 @@ interface AdminUser {
138
138
  updatedAt: string;
139
139
  }
140
140
 
141
+ interface ListUsersOptions {
142
+ search?: string;
143
+ /** Page size. The server's default is 25. */
144
+ limit?: number;
145
+ offset?: number;
146
+ orderBy?: string;
147
+ orderDir?: "asc" | "desc";
148
+ }
149
+
150
+ /** One page of users, and \`total\`: how many match in all. */
151
+ interface UsersPage {
152
+ users: AdminUser[];
153
+ total: number;
154
+ limit: number;
155
+ offset: number;
156
+ }
157
+
141
158
  interface RebaseCMS {
142
- listUsers(): Promise<{ users: AdminUser[] }>;
159
+ /** One page of users (25 unless \`limit\` says otherwise) and the total. */
160
+ listUsers(options?: ListUsersOptions): Promise<UsersPage>;
161
+ listUsersPaginated(options?: ListUsersOptions): Promise<UsersPage>;
143
162
  getUser(userId: string): Promise<{ user: AdminUser }>;
144
163
  createUser(data: { email: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
145
164
  updateUser(userId: string, data: { email?: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
@@ -200,8 +219,8 @@ type RebaseData = {
200
219
  * // Auth
201
220
  * const session = client.auth.getSession();
202
221
  *
203
- * // Admin
204
- * const { users } = await client.admin.listUsers();
222
+ * // Admin: one page of users, and how many there are
223
+ * const { users, total } = await client.admin.listUsers({ limit: 50 });
205
224
  *
206
225
  * // Custom endpoint
207
226
  * const result = await client.call("/my-endpoint", { myData: 123 });
@@ -221,12 +240,16 @@ interface RebaseClient {
221
240
  [collectionSlug: string]: unknown;
222
241
  }
223
242
 
224
- /** The pre-configured client instance. Already authenticated with the current user session. */
243
+ /**
244
+ * The client for this run: your own, one that runs every request as the user
245
+ * picked in "Run as" (it holds no session, so \`client.auth.getSession()\` is null),
246
+ * or an unauthenticated one under "No Auth".
247
+ */
225
248
  declare const client: RebaseClient;
226
249
 
227
250
  /** Execution context with user and collection information. */
228
251
  interface JSEditorContext {
229
- /** The user the script is running as. */
252
+ /** The user the script runs as: the one picked in "Run as", or you. \`null\` under "No Auth". */
230
253
  user: {
231
254
  uid: string;
232
255
  displayName: string | null;
@@ -359,15 +382,20 @@ var QUICK_REFERENCE = [
359
382
  code: "const result = await client.data.collection(\"COLLECTION\")\n .where(\"field\", \">\", value)\n .orderBy(\"field\", \"desc\")\n .limit(10)\n .find();\nreturn result;",
360
383
  description: "Chained query builder"
361
384
  },
385
+ {
386
+ label: "Who am I",
387
+ code: "return context.user;",
388
+ description: "The user this script runs as, or null under No Auth"
389
+ },
362
390
  {
363
391
  label: "Auth: Current session",
364
392
  code: "const session = client.auth.getSession();\nreturn session;",
365
- description: "Get current auth session"
393
+ description: "The session this client holds: yours, or none under Run as and No Auth"
366
394
  },
367
395
  {
368
396
  label: "Admin: List users",
369
- code: "const { users } = await client.admin.listUsers();\nreturn users;",
370
- description: "List all registered users"
397
+ 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 };",
398
+ description: "A page of users, and how many there are in all"
371
399
  },
372
400
  {
373
401
  label: "Custom endpoint",
@@ -618,6 +646,7 @@ function CollectionItem({ collection, onInsertCode }) {
618
646
  /* @__PURE__ */ jsx(Tooltip, {
619
647
  title: "Insert find() snippet",
620
648
  children: /* @__PURE__ */ jsx(IconButton, {
649
+ "aria-label": "Insert find() snippet",
621
650
  size: "smallest",
622
651
  className: "opacity-0 group-hover:opacity-100 text-text-disabled hover:text-primary transition-all",
623
652
  onClick: (e) => {
@@ -690,12 +719,12 @@ var STORAGE_PREFIX = "rebase_js_";
690
719
  * new user did be a 404 — a placeholder presented as working code. `slug` is
691
720
  * the project's first collection when one is known.
692
721
  */
693
- var makeDefaultCode = (slug) => `// Available: client (RebaseClient)
722
+ var makeDefaultCode = (slug) => `// Available: client (RebaseClient), context
694
723
  // Press Cmd+Enter (Ctrl+Enter) to run
695
724
  //
696
725
  // Examples:
697
- // const users = await client.admin.listUsers();
698
- // const session = client.auth.getSession();
726
+ // const { users, total } = await client.admin.listUsers({ limit: 50, offset: 0 }); // one page of total
727
+ // const me = context.user; // who this script runs as ("Run as"), null under No Auth
699
728
  ${slug ? `
700
729
  const result = await client.data.collection(${JSON.stringify(slug)}).find({ limit: 10 });
701
730
  return result;
@@ -704,9 +733,24 @@ return result;
704
733
  // const result = await client.data.collection("posts").find({ limit: 10 });
705
734
  // return result;
706
735
 
707
- return client.auth.getSession();
736
+ return context.user;
708
737
  `}`;
709
738
  var DEFAULT_CODE = makeDefaultCode();
739
+ /**
740
+ * The auth of a client built for one run: its own, in memory, and empty.
741
+ *
742
+ * The SDK's default auth restores the session the app saved in `localStorage`,
743
+ * sends its token on every request and keeps refreshing it. A run under "No
744
+ * Auth" or as another user must do none of that: it would go out as the
745
+ * signed-in user, and `client.auth` would describe the wrong person.
746
+ */
747
+ function runOnlyAuth() {
748
+ return {
749
+ storage: createMemoryStorage(),
750
+ persistSession: false,
751
+ autoRefresh: false
752
+ };
753
+ }
710
754
  function loadFromStorage(key, fallback) {
711
755
  try {
712
756
  const raw = localStorage.getItem(STORAGE_PREFIX + key);
@@ -727,14 +771,22 @@ function formatJSON(value) {
727
771
  return String(value);
728
772
  }
729
773
  }
774
+ /** The script without its comments: a collection named in one is not where rows came from. */
775
+ function withoutComments(code) {
776
+ return code.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
777
+ }
730
778
  /**
731
- * Given the raw SDK result, try to detect which collections are present.
779
+ * Given the raw SDK result, try to detect which collection its rows belong to.
732
780
  * 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.
781
+ * The heuristic: the executed code, outside its comments, names exactly one
782
+ * collection — `collection("<slug>")` or `client.data.<slug>` — and the result has
783
+ * rows with an "id" column. A script naming two could have returned either's
784
+ * rows, and an action on a row would open the other collection's row with the
785
+ * same id, so it gets none.
735
786
  */
736
- function detectCollectionsInResult(code, resultValue, collections) {
787
+ function detectCollectionsInResult(executedCode, resultValue, collections) {
737
788
  if (!resultValue || !collections?.length) return [];
789
+ const code = withoutComments(executedCode);
738
790
  const mentionedSlugs = /* @__PURE__ */ new Set();
739
791
  const collectionCallRegex = /\.collection\(["']([^"']+)["']\)/g;
740
792
  let m;
@@ -748,7 +800,7 @@ function detectCollectionsInResult(code, resultValue, collections) {
748
800
  "update",
749
801
  "delete"
750
802
  ].includes(m[1])) mentionedSlugs.add(m[1]);
751
- if (mentionedSlugs.size === 0) return [];
803
+ if (mentionedSlugs.size !== 1) return [];
752
804
  let rows = [];
753
805
  const rv = resultValue;
754
806
  if (rv?.data && Array.isArray(rv.data)) rows = rv.data;
@@ -839,27 +891,7 @@ function JSEditor() {
839
891
  } : tab);
840
892
  });
841
893
  }, [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]);
894
+ const { users, currentUser: currentSelectableUser, loading: usersLoading, onSearchTextChange: onUserSearchTextChange, totalCount: userTotalCount } = useRunAsUsers();
863
895
  useEffect(() => {
864
896
  saveToStorage("tabs", tabs);
865
897
  }, [tabs]);
@@ -918,29 +950,32 @@ function JSEditor() {
918
950
  const apiUrl = apiConfig?.apiUrl;
919
951
  const getAuthToken = apiConfig?.getAuthToken;
920
952
  if (!apiUrl) throw new Error("API URL not configured. Make sure apiUrl is set.");
921
- if (authMode === "none") return {
922
- client: createRebaseClient({
953
+ if (authMode === "none") {
954
+ const client = createRebaseClient({
923
955
  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.");
956
+ auth: runOnlyAuth()
957
+ });
930
958
  return {
931
- client: rebaseClient,
932
- isScoped: false
959
+ client,
960
+ release: () => client.close()
933
961
  };
934
962
  }
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.");
963
+ if (!selectedUser || currentUser && selectedUser.uid === currentUser.uid) {
964
+ if (!rebaseClient) throw new Error("Application client is not initialized.");
965
+ return { client: rebaseClient };
966
+ }
967
+ const token = getAuthToken ? await getAuthToken() ?? void 0 : void 0;
968
+ if (!getAuthToken || !token) throw new Error("No auth token available. Please sign in first.");
969
+ const client = createRebaseClient({
970
+ baseUrl: apiUrl,
971
+ token,
972
+ impersonate: selectedUser.uid,
973
+ auth: runOnlyAuth()
974
+ });
975
+ client.setAuthTokenGetter(getAuthToken);
938
976
  return {
939
- client: createRebaseClient({
940
- baseUrl: apiUrl,
941
- token
942
- }),
943
- isScoped: true
977
+ client,
978
+ release: () => client.close()
944
979
  };
945
980
  }, [
946
981
  apiConfig,
@@ -959,7 +994,8 @@ function JSEditor() {
959
994
  });
960
995
  const consoleEntries = [];
961
996
  const startTime = performance.now();
962
- let scopedClientToCleanUp = null;
997
+ let release;
998
+ const ranAsSignedInUser = authMode !== "none" && (!selectedUser || selectedUser.uid === currentUser?.uid);
963
999
  const originalConsole = {
964
1000
  log: console.log,
965
1001
  warn: console.warn,
@@ -979,10 +1015,11 @@ function JSEditor() {
979
1015
  console.warn = captureConsole("warn");
980
1016
  console.error = captureConsole("error");
981
1017
  console.info = captureConsole("info");
982
- const { client, isScoped } = await buildClient();
983
- if (isScoped) scopedClientToCleanUp = client;
1018
+ const built = await buildClient();
1019
+ release = built.release;
1020
+ const client = built.client;
984
1021
  const context = {
985
- user: selectedUser ?? (currentUser ? {
1022
+ user: authMode === "none" ? null : selectedUser ?? (currentUser ? {
986
1023
  uid: currentUser.uid,
987
1024
  displayName: currentUser.displayName,
988
1025
  email: currentUser.email,
@@ -997,7 +1034,9 @@ function JSEditor() {
997
1034
  value,
998
1035
  console: consoleEntries,
999
1036
  duration,
1000
- timestamp: Date.now()
1037
+ timestamp: Date.now(),
1038
+ code,
1039
+ ranAsSignedInUser
1001
1040
  });
1002
1041
  const resultObj = value;
1003
1042
  if (resultObj?.data && Array.isArray(resultObj.data)) setResultView("table");
@@ -1010,7 +1049,9 @@ function JSEditor() {
1010
1049
  console: consoleEntries,
1011
1050
  duration,
1012
1051
  error: err instanceof Error ? err.message : String(err),
1013
- timestamp: Date.now()
1052
+ timestamp: Date.now(),
1053
+ code,
1054
+ ranAsSignedInUser
1014
1055
  });
1015
1056
  setResultView("json");
1016
1057
  } finally {
@@ -1019,11 +1060,12 @@ function JSEditor() {
1019
1060
  console.error = originalConsole.error;
1020
1061
  console.info = originalConsole.info;
1021
1062
  setIsRunning(false);
1022
- if (scopedClientToCleanUp) scopedClientToCleanUp.ws?.disconnect();
1063
+ release?.();
1023
1064
  }
1024
1065
  }, [
1025
1066
  activeTab?.code,
1026
1067
  buildClient,
1068
+ authMode,
1027
1069
  selectedUser,
1028
1070
  currentUser,
1029
1071
  collectionInfos
@@ -1092,13 +1134,9 @@ function JSEditor() {
1092
1134
  };
1093
1135
  }, [result]);
1094
1136
  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
- ]);
1137
+ if (!result?.value || result.error || !result.ranAsSignedInUser) return [];
1138
+ return detectCollectionsInResult(result.code, result.value, collectionRegistry?.collections ?? []);
1139
+ }, [result, collectionRegistry?.collections]);
1102
1140
  const getRowEntityActions = useCallback((rowData) => {
1103
1141
  if (!rowData || matchedCollections.length === 0) return [];
1104
1142
  return matchedCollections.filter((mc) => rowData[mc.pkColumn] != null).map((mc) => ({
@@ -1216,6 +1254,7 @@ function JSEditor() {
1216
1254
  /* @__PURE__ */ jsx(Tooltip, {
1217
1255
  title: "Save as snippet",
1218
1256
  children: /* @__PURE__ */ jsx(IconButton, {
1257
+ "aria-label": "Save as snippet",
1219
1258
  size: "small",
1220
1259
  onClick: () => {
1221
1260
  setSnippetName("");
@@ -1228,6 +1267,7 @@ function JSEditor() {
1228
1267
  result?.value != null && /* @__PURE__ */ jsx(Tooltip, {
1229
1268
  title: "Export result as JSON",
1230
1269
  children: /* @__PURE__ */ jsx(IconButton, {
1270
+ "aria-label": "Export result as JSON",
1231
1271
  size: "small",
1232
1272
  onClick: exportResult,
1233
1273
  children: /* @__PURE__ */ jsx(DownloadIcon, { size: iconSize.smallest })
@@ -1265,7 +1305,9 @@ function JSEditor() {
1265
1305
  selectedUser,
1266
1306
  setSelectedUser,
1267
1307
  users,
1268
- loading: false,
1308
+ loading: usersLoading,
1309
+ onUserSearchTextChange,
1310
+ userTotalCount,
1269
1311
  currentUser: currentSelectableUser
1270
1312
  })
1271
1313
  }), /* @__PURE__ */ jsx("div", {
@@ -1429,6 +1471,10 @@ function JSEditor() {
1429
1471
  id: String(ra.entityId)
1430
1472
  }),
1431
1473
  children: /* @__PURE__ */ jsx(IconButton, {
1474
+ "aria-label": t("studio_sql_edit_entity", {
1475
+ name: ra.collection.collection.name,
1476
+ id: String(ra.entityId)
1477
+ }),
1432
1478
  size: "small",
1433
1479
  className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300",
1434
1480
  onClick: (e) => {
@@ -1627,4 +1673,4 @@ function JSONHighlight({ value }) {
1627
1673
  //#endregion
1628
1674
  export { JSEditor };
1629
1675
 
1630
- //# sourceMappingURL=JSEditor-qiuwmcvk.js.map
1676
+ //# sourceMappingURL=JSEditor-B59pwg4c.js.map