@rebasepro/studio 0.9.0 → 0.9.1-canary.0de22e0

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 (79) hide show
  1. package/README.md +3 -3
  2. package/dist/{ApiExplorer-CdIwR9Ga.js → ApiExplorer-B9M9-uy_.js} +3 -3
  3. package/dist/ApiExplorer-B9M9-uy_.js.map +1 -0
  4. package/dist/{ApiKeysView-D-_FSlNL.js → ApiKeysView-vQKqt2wl.js} +45 -3
  5. package/dist/ApiKeysView-vQKqt2wl.js.map +1 -0
  6. package/dist/{AuthSimulationSelector-iEZ-Or_1.js → AuthSimulationSelector-Bougd521.js} +2 -2
  7. package/dist/AuthSimulationSelector-Bougd521.js.map +1 -0
  8. package/dist/BackupsView-DNS6LdVg.js +227 -0
  9. package/dist/BackupsView-DNS6LdVg.js.map +1 -0
  10. package/dist/{BranchesView-Dlg78EQ8.js → BranchesView-DH0qnU9J.js} +2 -2
  11. package/dist/BranchesView-DH0qnU9J.js.map +1 -0
  12. package/dist/{CronJobsView-4gdtJvoe.js → CronJobsView-B59i3Mis.js} +2 -2
  13. package/dist/CronJobsView-B59i3Mis.js.map +1 -0
  14. package/dist/{JSEditor-Sgmw1SHC.js → JSEditor-BeHHuxhA.js} +5 -5
  15. package/dist/JSEditor-BeHHuxhA.js.map +1 -0
  16. package/dist/LogsExplorer-BCXoGREr.js +298 -0
  17. package/dist/LogsExplorer-BCXoGREr.js.map +1 -0
  18. package/dist/{MonacoEditor-COZqrIJ1.js → MonacoEditor-D0_QljcX.js} +2 -2
  19. package/dist/MonacoEditor-D0_QljcX.js.map +1 -0
  20. package/dist/{RLSEditor-DPhky8XW.js → RLSEditor-D6adwB6e.js} +34 -20
  21. package/dist/RLSEditor-D6adwB6e.js.map +1 -0
  22. package/dist/{SQLEditor-CgR2hlUI.js → SQLEditor-_dCqVAN_.js} +5 -5
  23. package/dist/SQLEditor-_dCqVAN_.js.map +1 -0
  24. package/dist/{SchemaVisualizer-WK4ZZR5X.js → SchemaVisualizer-C_ErehfH.js} +3 -3
  25. package/dist/SchemaVisualizer-C_ErehfH.js.map +1 -0
  26. package/dist/{StorageView-BMhD29YO.js → StorageView-UvyM2xdW.js} +2 -2
  27. package/dist/StorageView-UvyM2xdW.js.map +1 -0
  28. package/dist/components/AuthSimulationSelector.d.ts +1 -1
  29. package/dist/components/Backups/BackupsView.d.ts +2 -0
  30. package/dist/components/RLSEditor/PolicyEditor.d.ts +1 -1
  31. package/dist/components/SQLEditor/SQLEditor.d.ts +2 -10
  32. package/dist/components/SQLEditor/SQLEditorSidebar.d.ts +1 -1
  33. package/dist/components/SQLEditor/SchemaBrowser.d.ts +1 -1
  34. package/dist/components/SQLEditor/sql_editor_types.d.ts +16 -0
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.es.js +73 -110
  37. package/dist/index.es.js.map +1 -1
  38. package/dist/utils/sql_utils.d.ts +1 -1
  39. package/package.json +13 -12
  40. package/src/components/ApiExplorer/ApiExplorer.tsx +1 -1
  41. package/src/components/ApiExplorer/TryItPanel.tsx +1 -1
  42. package/src/components/ApiKeys/ApiKeysView.tsx +43 -3
  43. package/src/components/AuthSimulationSelector.tsx +1 -1
  44. package/src/components/Backups/BackupsView.tsx +185 -0
  45. package/src/components/Branches/BranchesView.tsx +1 -1
  46. package/src/components/CronJobs/CronJobsView.tsx +1 -1
  47. package/src/components/JSEditor/JSEditor.tsx +6 -6
  48. package/src/components/JSEditor/JSMonacoEditor.tsx +1 -1
  49. package/src/components/LogsExplorer/LogsExplorer.tsx +168 -46
  50. package/src/components/RLSEditor/PolicyEditor.tsx +6 -4
  51. package/src/components/RLSEditor/RLSEditor.tsx +50 -20
  52. package/src/components/RebaseStudio.tsx +12 -3
  53. package/src/components/SQLEditor/MonacoEditor.tsx +1 -1
  54. package/src/components/SQLEditor/SQLEditor.tsx +6 -14
  55. package/src/components/SQLEditor/SQLEditorSidebar.tsx +2 -2
  56. package/src/components/SQLEditor/SchemaBrowser.tsx +4 -4
  57. package/src/components/SQLEditor/sql_editor_types.ts +18 -0
  58. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +3 -2
  59. package/src/components/SchemaVisualizer/TableNode.tsx +1 -1
  60. package/src/components/StorageView/StorageView.tsx +1 -1
  61. package/src/components/StudioHomePage.tsx +62 -87
  62. package/src/index.ts +3 -3
  63. package/src/utils/sql_utils.test.ts +1 -1
  64. package/src/utils/sql_utils.ts +1 -1
  65. package/dist/ApiExplorer-CdIwR9Ga.js.map +0 -1
  66. package/dist/ApiKeysView-D-_FSlNL.js.map +0 -1
  67. package/dist/AuthSimulationSelector-iEZ-Or_1.js.map +0 -1
  68. package/dist/BranchesView-Dlg78EQ8.js.map +0 -1
  69. package/dist/CronJobsView-4gdtJvoe.js.map +0 -1
  70. package/dist/JSEditor-Sgmw1SHC.js.map +0 -1
  71. package/dist/LogsExplorer-J4xfsuv3.js +0 -206
  72. package/dist/LogsExplorer-J4xfsuv3.js.map +0 -1
  73. package/dist/MonacoEditor-COZqrIJ1.js.map +0 -1
  74. package/dist/RLSEditor-DPhky8XW.js.map +0 -1
  75. package/dist/SQLEditor-CgR2hlUI.js.map +0 -1
  76. package/dist/SchemaVisualizer-WK4ZZR5X.js.map +0 -1
  77. package/dist/StorageView-BMhD29YO.js.map +0 -1
  78. package/dist/index.umd.js +0 -11477
  79. package/dist/index.umd.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
 
2
- import { useStudioCollectionRegistry } from "@rebasepro/core";
2
+ import { useStudioCollectionRegistry } from "@rebasepro/app";
3
3
  import React, { useState, useEffect, useCallback, useMemo } from "react";
4
4
  import {
5
5
  Alert,
@@ -25,9 +25,11 @@ import {
25
25
  Trash2Icon,
26
26
  Typography
27
27
  } from "@rebasepro/ui";
28
- import { useRebaseContext, useSnackbarController, ErrorView, useTranslation } from "@rebasepro/core";
28
+ import { useRebaseContext, useSnackbarController, ErrorView, useTranslation } from "@rebasepro/app";
29
29
  import { isPostgresCollectionConfig } from "@rebasepro/types";
30
30
  import { REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_PREFIXES, JUNCTION_TABLES_SQL } from "@rebasepro/common";
31
+ import { getPolicyNamesForRule, getPolicyNamesForRules, getPolicyOperations } from "@rebasepro/utils";
32
+ import { resolveJunctionSpecs, getJunctionSecurityRules } from "@rebasepro/common";
31
33
  import { PolicyEditor } from "./PolicyEditor";
32
34
 
33
35
  type TableCategory = "collection" | "junction" | "internal" | "other";
@@ -387,28 +389,54 @@ export const RLSEditor = ({ apiUrl = "" }: { apiUrl?: string }) => {
387
389
  status: "live" };
388
390
  });
389
391
 
390
- // Merge code-based policies
392
+ // Merge code-based policies.
393
+ //
394
+ // A rule without an explicit `name` still produces policies — Postgres gets
395
+ // `<table>_<op>_<hash>`, one per operation. Skipping those rules left their
396
+ // live policies looking like hand-written SQL ("DB Only"), so derive the
397
+ // names the generator would emit and match on those.
391
398
  if (activeCollection && isPostgresCollectionConfig(activeCollection) && activeCollection.securityRules) {
392
399
  activeCollection.securityRules.forEach((rule) => {
393
- const ruleName = rule.name;
394
- if (!ruleName) return;
395
-
396
- policiesMap[ruleName] = {
397
- policyname: ruleName,
398
- tablename: activeTableData.tableName,
399
- permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
400
- cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
401
- roles: [...(rule.roles ?? ["public"])],
402
- qual: rule.using || null,
403
- with_check: rule.withCheck || null,
404
- // "both" = defined in code and live in Postgres (potentially edited)
405
- status: policiesMap[ruleName] ? "both" : "code_only"
406
- };
400
+ const ops = getPolicyOperations(rule);
401
+ const policyNames = getPolicyNamesForRule(rule, activeTableData.tableName);
402
+
403
+ policyNames.forEach((policyName, opIdx) => {
404
+ policiesMap[policyName] = {
405
+ policyname: policyName,
406
+ tablename: activeTableData.tableName,
407
+ permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
408
+ cmd: (ops[opIdx] ?? rule.operation ?? "ALL").toUpperCase() as PostgresPolicy["cmd"],
409
+ roles: [...(rule.roles ?? ["public"])],
410
+ qual: rule.using || null,
411
+ with_check: rule.withCheck || null,
412
+ // "both" = defined in code and live in Postgres (potentially edited)
413
+ status: policiesMap[policyName] ? "both" : "code_only"
414
+ };
415
+ });
407
416
  });
408
417
  }
409
418
 
419
+ // Junction tables have no collection, but their policies are generated
420
+ // too — derived from the endpoints' relations. Recognise them so they
421
+ // don't show as hand-written SQL ("DB Only"). If the registry's
422
+ // collections don't carry resolvable relations, they simply stay "live".
423
+ if (!activeCollection) {
424
+ try {
425
+ const registryCollections = (collectionRegistry.collections ?? []) as unknown as Parameters<typeof resolveJunctionSpecs>[0];
426
+ const spec = resolveJunctionSpecs(registryCollections).get(activeTableData.tableName);
427
+ if (spec) {
428
+ const generatedNames = getPolicyNamesForRules(getJunctionSecurityRules(spec), spec.table);
429
+ for (const p of Object.values(policiesMap)) {
430
+ if (generatedNames.has(p.policyname)) p.status = "both";
431
+ }
432
+ }
433
+ } catch {
434
+ /* serialized configs without relation closures — leave as live */
435
+ }
436
+ }
437
+
410
438
  return Object.values(policiesMap).sort((a, b) => a.policyname.localeCompare(b.policyname));
411
- }, [activeTableData, activeCollection]);
439
+ }, [activeTableData, activeCollection, collectionRegistry.collections]);
412
440
 
413
441
  // Stats for the info tab
414
442
  const rlsStats = useMemo(() => {
@@ -816,8 +844,10 @@ message: e instanceof Error ? e.message : String(e) });
816
844
  </Typography>
817
845
  <Typography variant="caption" className="opacity-80">
818
846
  This is an auto-generated junction table for a many-to-many relation.
819
- Its access is typically managed through the related collections&apos; policies.
820
- You can still add RLS policies directly if needed.
847
+ Rebase derives its policies: rows are readable when both related rows
848
+ are, and writable following the declaring collection&apos;s update rules
849
+ (plus the server/admin baseline). You can still add RLS policies
850
+ directly to broaden access.
821
851
  </Typography>
822
852
  </div>
823
853
  </div>
@@ -1,5 +1,5 @@
1
1
  import React, { lazy, Suspense, useLayoutEffect, useMemo } from "react";
2
- import { useRebaseRegistryDispatch } from "@rebasepro/core";
2
+ import { useRebaseRegistryDispatch } from "@rebasepro/app";
3
3
  import type { RebaseStudioConfig, AppView } from "@rebasepro/types";
4
4
  import { CircularProgressCenter } from "@rebasepro/ui";
5
5
 
@@ -12,6 +12,7 @@ const StorageView = lazy(() => import("./StorageView/StorageView").then(m => ({
12
12
  const CronJobsView = lazy(() => import("./CronJobs/CronJobsView").then(m => ({ default: m.CronJobsView })));
13
13
  const SchemaVisualizer = lazy(() => import("./SchemaVisualizer/SchemaVisualizer").then(m => ({ default: m.SchemaVisualizer })));
14
14
  const BranchesView = lazy(() => import("./Branches/BranchesView").then(m => ({ default: m.BranchesView })));
15
+ const BackupsView = lazy(() => import("./Backups/BackupsView").then(m => ({ default: m.BackupsView })));
15
16
  const ApiExplorer = lazy(() => import("./ApiExplorer/ApiExplorer").then(m => ({ default: m.ApiExplorer })));
16
17
  const LogsExplorer = lazy(() => import("./LogsExplorer/LogsExplorer").then(m => ({ default: m.LogsExplorer })));
17
18
  const ApiKeysView = lazy(() => import("./ApiKeys/ApiKeysView").then(m => ({ default: m.ApiKeysView })));
@@ -23,7 +24,7 @@ import { StudioHomePage } from "./StudioHomePage";
23
24
  * Renders nothing — purely registers config into the RebaseRegistry.
24
25
  *
25
26
  * The "schema" tool (collection editor view) is now a built-in CMS feature.
26
- * When `<RebaseCMS collectionEditor={...}>` is used, the schema view is
27
+ * When `<RebaseAdmin collectionEditor={...}>` is used, the schema view is
27
28
  * automatically injected into Studio — no manual wiring needed.
28
29
  */
29
30
  const DEFAULT_HOME_PAGE = <StudioHomePage/>;
@@ -35,7 +36,7 @@ export function RebaseStudio({ tools, homePage }: RebaseStudioConfig) {
35
36
 
36
37
  const devViews: AppView[] = useMemo(() => {
37
38
  const views: AppView[] = [];
38
- const activeTools = tools ?? ["sql", "js", "rls", "storage", "cron", "schema-visualizer", "branches", "api", "logs", "api-keys"];
39
+ const activeTools = tools ?? ["sql", "js", "rls", "storage", "cron", "schema-visualizer", "branches", "backups", "api", "logs", "api-keys"];
39
40
  const suspense = (el: React.ReactNode) => <Suspense fallback={<CircularProgressCenter/>}>{el}</Suspense>;
40
41
 
41
42
  if (activeTools.includes("sql")) {
@@ -93,6 +94,14 @@ group: "Database",
93
94
  icon: "GitBranch",
94
95
  description: "Create and manage database branches",
95
96
  view: suspense(<BranchesView/>) });
97
+ }
98
+ if (activeTools.includes("backups")) {
99
+ views.push({ slug: "backups",
100
+ name: "Backups",
101
+ group: "Database",
102
+ icon: "Database",
103
+ description: "Download database backups",
104
+ view: suspense(<BackupsView/>) });
96
105
  }
97
106
  if (activeTools.includes("api")) {
98
107
  views.push({ slug: "api",
@@ -2,7 +2,7 @@ import React, { useRef } from "react";
2
2
  import Editor, { Monaco, OnMount } from "@monaco-editor/react";
3
3
  import type { editor, Position, IRange } from "monaco-editor";
4
4
  import { cls, defaultBorderMixin } from "@rebasepro/ui";
5
- import { useModeController } from "@rebasepro/core";
5
+ import { useModeController } from "@rebasepro/app";
6
6
 
7
7
  export type MonacoEditorProps = {
8
8
  value: string;
@@ -1,6 +1,6 @@
1
1
 
2
- import { IconForView } from "@rebasepro/core";
3
- import { useStudioCollectionRegistry, useStudioSidePanelController } from "@rebasepro/core";
2
+ import { IconForView } from "@rebasepro/app";
3
+ import { useStudioCollectionRegistry, useStudioSidePanelController } from "@rebasepro/app";
4
4
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
5
5
  import { createPortal } from "react-dom";
6
6
  import {
@@ -42,24 +42,16 @@ import {
42
42
  XIcon
43
43
  } from "@rebasepro/ui";
44
44
 
45
- import { useRebaseContext, useSnackbarController, ConfirmationDialog, ErrorView, useTranslation } from "@rebasepro/core";
45
+ import { useRebaseContext, useSnackbarController, ConfirmationDialog, ErrorView, useTranslation } from "@rebasepro/app";
46
46
  import { MonacoEditor } from "./MonacoEditor";
47
47
  import { SQLEditorSidebar, Snippet } from "./SQLEditorSidebar";
48
48
  import { parseFirst } from "pgsql-ast-parser";
49
49
  import { determineTableAndPK, resolveQueryCollections, ResolvedQueryCollection } from "../../utils/sql_utils";
50
50
  import { ExplainVisualizer } from "./ExplainVisualizer";
51
51
 
52
- export interface SQLEditorColumnInfo {
53
- name: string;
54
- dataType: string;
55
- isPrimaryKey: boolean;
56
- }
57
-
58
- export interface TableInfo {
59
- schemaName: string;
60
- tableName: string;
61
- columns: SQLEditorColumnInfo[];
62
- }
52
+ import type { SQLEditorColumnInfo, TableInfo } from "./sql_editor_types";
53
+
54
+ export type { SQLEditorColumnInfo, TableInfo };
63
55
 
64
56
  const QueryLoadingView = () => {
65
57
  const [elapsed, setElapsed] = useState(0);
@@ -1,9 +1,9 @@
1
1
 
2
2
  import React, { useState } from "react";
3
3
  import { cls, defaultBorderMixin, IconButton, iconSize, Tab, Tabs, Trash2Icon, Typography } from "@rebasepro/ui";
4
- import { useTranslation } from "@rebasepro/core";
4
+ import { useTranslation } from "@rebasepro/app";
5
5
  import { SchemaBrowser } from "./SchemaBrowser";
6
- import { TableInfo } from "./SQLEditor";
6
+ import type { TableInfo } from "./sql_editor_types";
7
7
 
8
8
  export interface Snippet {
9
9
  id: string;
@@ -14,8 +14,8 @@ import {
14
14
  RefreshCwIcon,
15
15
  Typography
16
16
  } from "@rebasepro/ui";
17
- import { TableInfo } from "./SQLEditor";
18
- import { ErrorView, useTranslation } from "@rebasepro/core";
17
+ import type { TableInfo } from "./sql_editor_types";
18
+ import { ErrorView, useTranslation } from "@rebasepro/app";
19
19
 
20
20
  export const SchemaBrowser = ({
21
21
  onTableClick,
@@ -86,7 +86,7 @@ export const SchemaBrowser = ({
86
86
  e.stopPropagation();
87
87
  navigator.clipboard.writeText(table.tableName);
88
88
  }}
89
- title="CopyIcon table name"
89
+ title="Copy table name"
90
90
  >
91
91
  <CopyIcon size={iconSize.small}/>
92
92
  </IconButton>
@@ -148,7 +148,7 @@ export const SchemaBrowser = ({
148
148
  e.stopPropagation();
149
149
  navigator.clipboard.writeText(col.name);
150
150
  }}
151
- title="CopyIcon column name"
151
+ title="Copy column name"
152
152
  >
153
153
  <CopyIcon size={iconSize.smallest}/>
154
154
  </IconButton>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shapes describing the introspected schema the SQL editor works against.
3
+ * They live here rather than in SQLEditor.tsx because the sidebar, the schema
4
+ * browser and utils/sql_utils all need them, and importing them from the
5
+ * component would close a cycle back through it.
6
+ */
7
+
8
+ export interface SQLEditorColumnInfo {
9
+ name: string;
10
+ dataType: string;
11
+ isPrimaryKey: boolean;
12
+ }
13
+
14
+ export interface TableInfo {
15
+ schemaName: string;
16
+ tableName: string;
17
+ columns: SQLEditorColumnInfo[];
18
+ }
@@ -28,7 +28,7 @@ import {
28
28
  import {
29
29
  useStudioCollectionRegistry,
30
30
  IconForView
31
- } from "@rebasepro/core";
31
+ } from "@rebasepro/app";
32
32
  import type { CollectionConfig } from "@rebasepro/types";
33
33
  import { isPostgresCollectionConfig } from "@rebasepro/types";
34
34
  import { useSchemaGraph } from "./useSchemaGraph";
@@ -543,7 +543,8 @@ duration: 400 }
543
543
  />
544
544
  <Controls
545
545
  showInteractive={false}
546
- className="!bg-white dark:!bg-surface-900 !border !border-surface-200/40 dark:!border-surface-700/40 !shadow-sm !rounded-lg"
546
+ // mb clears the legend overlay pinned at bottom-left
547
+ className="!mb-16 !bg-white dark:!bg-surface-900 !border !border-surface-200/40 dark:!border-surface-700/40 !shadow-sm !rounded-lg dark:[--xy-controls-button-background-color:var(--color-surface-900)] dark:[--xy-controls-button-background-color-hover:var(--color-surface-800)] dark:[--xy-controls-button-color:var(--color-surface-300)] dark:[--xy-controls-button-color-hover:var(--color-surface-50)] dark:[--xy-controls-button-border-color:var(--color-surface-700)]"
547
548
  />
548
549
  <MiniMap
549
550
  nodeStrokeColor={(n) => {
@@ -2,7 +2,7 @@ import React, { memo, useMemo } from "react";
2
2
  import { Handle, Position } from "@xyflow/react";
3
3
  import type { NodeProps } from "@xyflow/react";
4
4
  import { Typography, Chip, Tooltip, cls } from "@rebasepro/ui";
5
- import { IconForView } from "@rebasepro/core";
5
+ import { IconForView } from "@rebasepro/app";
6
6
  import type { TableNodeData, ColumnInfo } from "./useSchemaGraph";
7
7
  import { getColumnRowY, getHeaderHeight } from "./schema-visualizer.utils";
8
8
 
@@ -41,7 +41,7 @@ import {
41
41
  VideoIcon,
42
42
  XIcon
43
43
  } from "@rebasepro/ui";
44
- import { useStorageSource, useStorageSources, useSnackbarController, ErrorView, useApiConfig } from "@rebasepro/core";
44
+ import { useStorageSource, useStorageSources, useSnackbarController, ErrorView, useApiConfig } from "@rebasepro/app";
45
45
  import { DEFAULT_STORAGE_SOURCE_KEY, type StorageListResult } from "@rebasepro/types";
46
46
  import { useSearchParams } from "react-router-dom";
47
47
  import { useDropzone } from "react-dropzone";
@@ -1,19 +1,19 @@
1
- import type { HomePageSection, PluginGenericProps } from "@rebasepro/types";
1
+ import type { AppView, HomePageSection, PluginGenericProps } from "@rebasepro/types";
2
2
  import React, { useEffect, useMemo, useState } from "react";
3
3
  import { Card, cls, Container, ExpandablePanel, Typography } from "@rebasepro/ui";
4
- import { IconForView, useRebaseContext, useRestoreScroll, useSlot } from "@rebasepro/core";
4
+ import { IconForView, useRebaseContext, useRebaseRegistry, useRestoreScroll, useSlot } from "@rebasepro/app";
5
5
  import { useNavigate } from "react-router-dom";
6
- import { useStudioBreadcrumbs, SchemaDriftBanner } from "@rebasepro/core";
6
+ import { useStudioBreadcrumbs, SchemaDriftBanner } from "@rebasepro/app";
7
7
 
8
8
  /* ═══════════════════════════════════════════════════════════════
9
- Static studio tool definitions
9
+ Studio tool sections, derived from the registered Studio views
10
10
  ═══════════════════════════════════════════════════════════════ */
11
11
 
12
12
  interface StudioTool {
13
13
  path: string;
14
14
  name: string;
15
15
  description: string;
16
- icon: string;
16
+ icon?: string | React.ReactNode;
17
17
  }
18
18
 
19
19
  interface StudioSection {
@@ -21,85 +21,44 @@ interface StudioSection {
21
21
  tools: StudioTool[];
22
22
  }
23
23
 
24
- const SECTIONS: StudioSection[] = [
25
- {
26
- label: "Database",
27
- tools: [
28
- { path: "/schema",
29
- name: "Collections",
30
- description: "Define and manage your data model and collection schemas",
31
- icon: "LayoutList" },
32
- { path: "/schema-visualizer",
33
- name: "Schema Visualizer",
34
- description: "Interactive ERD showing tables, columns, and relationships",
35
- icon: "Network" },
36
- { path: "/sql",
37
- name: "SQL Console",
38
- description: "Execute raw SQL queries directly against your database",
39
- icon: "terminal" },
40
- { path: "/branches",
41
- name: "Branches",
42
- description: "Create and manage isolated database copies for development",
43
- icon: "GitBranch" },
44
- { path: "/rls",
45
- name: "RLS Policies",
46
- description: "Configure Row Level Security for fine-grained data access",
47
- icon: "ShieldCheck" },
48
- { path: "/logs",
49
- name: "Logs Explorer",
50
- description: "Real-time system, query, and authentication logs",
51
- icon: "Activity" }
52
- ]
53
- },
54
- {
55
- label: "Compute",
56
- tools: [
57
- { path: "/js",
58
- name: "JS Console",
59
- description: "Run JavaScript with the Rebase SDK in a live sandbox",
60
- icon: "code" },
61
- { path: "/cron",
62
- name: "Cron Jobs",
63
- description: "Monitor and manage scheduled background tasks",
64
- icon: "Clock" }
65
- ]
66
- },
67
- {
68
- label: "API",
69
- tools: [
70
- { path: "/api",
71
- name: "API Explorer",
72
- description: "Interactive API documentation with live request testing",
73
- icon: "BookOpen" }
74
- ]
75
- },
76
- {
77
- label: "Storage",
78
- tools: [
79
- { path: "/storage",
80
- name: "Storage",
81
- description: "Browse, upload, and manage files in your storage bucket",
82
- icon: "HardDrive" }
83
- ]
84
- },
85
- {
86
- label: "Access Control",
87
- tools: [
88
- { path: "/users",
89
- name: "Users",
90
- description: "Manage developers and assign roles in your workspace",
91
- icon: "group" },
92
- { path: "/roles",
93
- name: "Roles",
94
- description: "Create and configure fine-grained access permissions",
95
- icon: "ShieldEllipsis" },
96
- { path: "/api-keys",
97
- name: "API Keys",
98
- description: "Create and manage scoped keys for machine-to-machine access",
99
- icon: "KeyRound" }
100
- ]
24
+ /** The view metadata the cards need — not the rendered `view` itself. */
25
+ type StudioViewMeta = Pick<AppView, "slug" | "name" | "group" | "description" | "icon" | "hideFromNavigation">;
26
+
27
+ /** Group order for the home page; unknown groups are appended in view order. */
28
+ const GROUP_ORDER = ["Database", "Compute", "Storage", "API", "Access Control"];
29
+
30
+ const UNGROUPED_LABEL = "Tools";
31
+
32
+ /**
33
+ * Build the home page sections from the Studio views that are actually
34
+ * registered, so a card exists if and only if its route does. A hand-written
35
+ * list drifts: it used to advertise Users and Roles pages that 404, while
36
+ * omitting the Backups view that does exist.
37
+ */
38
+ function buildSections(views: StudioViewMeta[]): StudioSection[] {
39
+ const byGroup = new Map<string, StudioTool[]>();
40
+
41
+ for (const view of views) {
42
+ if (view.hideFromNavigation) continue;
43
+ const label = view.group ?? UNGROUPED_LABEL;
44
+ const tools = byGroup.get(label) ?? [];
45
+ tools.push({
46
+ path: `/${view.slug}`,
47
+ name: view.name,
48
+ description: view.description ?? "",
49
+ icon: view.icon
50
+ });
51
+ byGroup.set(label, tools);
101
52
  }
102
- ];
53
+
54
+ const ordered = [
55
+ ...GROUP_ORDER.filter(g => byGroup.has(g)),
56
+ ...[...byGroup.keys()].filter(g => !GROUP_ORDER.includes(g))
57
+ ];
58
+
59
+ return ordered.map(label => ({ label,
60
+ tools: byGroup.get(label)! }));
61
+ }
103
62
 
104
63
  /* ═══════════════════════════════════════════════════════════════ */
105
64
 
@@ -151,6 +110,7 @@ export function StudioHomePage({
151
110
  const context = useRebaseContext();
152
111
  const breadcrumbs = useStudioBreadcrumbs();
153
112
  const navigate = useNavigate();
113
+ const registry = useRebaseRegistry();
154
114
 
155
115
  useEffect(() => {
156
116
  breadcrumbs.set({ breadcrumbs: [] });
@@ -162,10 +122,25 @@ export function StudioHomePage({
162
122
 
163
123
  const pluginActions = useSlot("home.actions", sectionProps);
164
124
 
165
- const filteredSections = useMemo(
166
- () => SECTIONS.filter(s => !hiddenGroups?.includes(s.label)),
167
- [hiddenGroups]
168
- );
125
+ // The collection editor ("schema") is not part of `devViews` — RebaseNavigation
126
+ // injects it when the CMS enables a collection editor. Mirror that condition so
127
+ // the card tracks the route.
128
+ const schemaEnabled = Boolean(registry.studioConfig && registry.cmsConfig?.collectionEditor);
129
+
130
+ const filteredSections = useMemo(() => {
131
+ const views: StudioViewMeta[] = [];
132
+ if (schemaEnabled) {
133
+ views.push({
134
+ slug: "schema",
135
+ name: "Collections",
136
+ group: "Database",
137
+ icon: "LayoutList",
138
+ description: "Define and manage your data model and collection schemas"
139
+ });
140
+ }
141
+ views.push(...(registry.studioConfig?.devViews ?? []));
142
+ return buildSections(views).filter(s => !hiddenGroups?.includes(s.label));
143
+ }, [registry.studioConfig?.devViews, schemaEnabled, hiddenGroups]);
169
144
 
170
145
  const groupNames = useMemo(
171
146
  () => filteredSections.map(s => s.label),
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // ─── Bridge ─────────────────────────────────────────────────────────
2
- // Re-export the Studio Bridge from @rebasepro/core.
2
+ // Re-export the Studio Bridge from @rebasepro/app.
3
3
  // The bridge lives in core so both studio and CMS can access it
4
4
  // without circular dependencies.
5
5
  export {
@@ -10,12 +10,12 @@ export {
10
10
  useStudioUrlController,
11
11
  useStudioNavigationState,
12
12
  useStudioBreadcrumbs
13
- } from "@rebasepro/core";
13
+ } from "@rebasepro/app";
14
14
  export type {
15
15
  StudioBridge,
16
16
  BreadcrumbEntry,
17
17
  BreadcrumbsController
18
- } from "@rebasepro/core";
18
+ } from "@rebasepro/app";
19
19
 
20
20
  // ─── Studio Entry Point ─────────────────────────────────────────────
21
21
  // Only export the lightweight orchestrator & home page.
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { describe, it, expect } from "@jest/globals";
3
3
  import { determineTableAndPK, extractTablesFromQuery, resolveQueryCollections } from "./sql_utils";
4
- import { TableInfo } from "../components/SQLEditor/SQLEditor";
4
+ import type { TableInfo } from "../components/SQLEditor/sql_editor_types";
5
5
  import { CollectionConfig } from "@rebasepro/types";
6
6
 
7
7
  const mockSchemas: Record<string, TableInfo[]> = {
@@ -1,5 +1,5 @@
1
1
  import { parseFirst } from "pgsql-ast-parser";
2
- import { TableInfo } from "../components/SQLEditor/SQLEditor";
2
+ import type { TableInfo } from "../components/SQLEditor/sql_editor_types";
3
3
  import { CollectionConfig } from "@rebasepro/types";
4
4
 
5
5
  /**