@rebasepro/studio 0.7.0 → 0.9.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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/{JSEditor-DSucz6wV.js → JSEditor-Sgmw1SHC.js} +28 -32
  3. package/dist/JSEditor-Sgmw1SHC.js.map +1 -0
  4. package/dist/{RLSEditor-BM64laoW.js → RLSEditor-DPhky8XW.js} +8 -18
  5. package/dist/RLSEditor-DPhky8XW.js.map +1 -0
  6. package/dist/{SQLEditor-CuAhR-zr.js → SQLEditor-CgR2hlUI.js} +5 -5
  7. package/dist/SQLEditor-CgR2hlUI.js.map +1 -0
  8. package/dist/{SchemaVisualizer-OibKoD3g.js → SchemaVisualizer-WK4ZZR5X.js} +9 -9
  9. package/dist/SchemaVisualizer-WK4ZZR5X.js.map +1 -0
  10. package/dist/{StorageView-CvrnHmDG.js → StorageView-BMhD29YO.js} +33 -5
  11. package/dist/StorageView-BMhD29YO.js.map +1 -0
  12. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +2 -2
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.es.js +110 -82
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/index.umd.js +174 -133
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/utils/pgColumnToProperty.d.ts +3 -3
  19. package/dist/utils/sql_utils.d.ts +3 -3
  20. package/package.json +17 -17
  21. package/src/components/JSEditor/JSEditor.tsx +7 -7
  22. package/src/components/JSEditor/JSMonacoEditor.tsx +23 -27
  23. package/src/components/RLSEditor/RLSEditor.tsx +18 -31
  24. package/src/components/SQLEditor/SQLEditor.tsx +4 -4
  25. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +7 -7
  26. package/src/components/SchemaVisualizer/useSchemaGraph.ts +11 -11
  27. package/src/components/StorageView/StorageView.tsx +43 -4
  28. package/src/components/StudioHomePage.tsx +139 -84
  29. package/src/index.ts +1 -1
  30. package/src/utils/pgColumnToProperty.ts +4 -4
  31. package/src/utils/sql_utils.test.ts +6 -6
  32. package/src/utils/sql_utils.ts +3 -3
  33. package/dist/JSEditor-DSucz6wV.js.map +0 -1
  34. package/dist/RLSEditor-BM64laoW.js.map +0 -1
  35. package/dist/SQLEditor-CuAhR-zr.js.map +0 -1
  36. package/dist/SchemaVisualizer-OibKoD3g.js.map +0 -1
  37. package/dist/StorageView-CvrnHmDG.js.map +0 -1
  38. package/src/vite-env.d.ts +0 -1
@@ -1,7 +1,6 @@
1
1
  import type { HomePageSection, PluginGenericProps } from "@rebasepro/types";
2
- import React, { useEffect } from "react";
3
- import { Card, cls, Container, Typography } from "@rebasepro/ui";
4
- import { ArrowRightIcon, iconSize } from "@rebasepro/ui";
2
+ import React, { useEffect, useMemo, useState } from "react";
3
+ import { Card, cls, Container, ExpandablePanel, Typography } from "@rebasepro/ui";
5
4
  import { IconForView, useRebaseContext, useRestoreScroll, useSlot } from "@rebasepro/core";
6
5
  import { useNavigate } from "react-router-dom";
7
6
  import { useStudioBreadcrumbs, SchemaDriftBanner } from "@rebasepro/core";
@@ -19,16 +18,12 @@ interface StudioTool {
19
18
 
20
19
  interface StudioSection {
21
20
  label: string;
22
- dotColor: string;
23
- iconColor: string;
24
21
  tools: StudioTool[];
25
22
  }
26
23
 
27
24
  const SECTIONS: StudioSection[] = [
28
25
  {
29
26
  label: "Database",
30
- dotColor: "bg-emerald-400",
31
- iconColor: "text-emerald-400",
32
27
  tools: [
33
28
  { path: "/schema",
34
29
  name: "Collections",
@@ -58,8 +53,6 @@ icon: "Activity" }
58
53
  },
59
54
  {
60
55
  label: "Compute",
61
- dotColor: "bg-blue-400",
62
- iconColor: "text-blue-400",
63
56
  tools: [
64
57
  { path: "/js",
65
58
  name: "JS Console",
@@ -73,8 +66,6 @@ icon: "Clock" }
73
66
  },
74
67
  {
75
68
  label: "API",
76
- dotColor: "bg-violet-400",
77
- iconColor: "text-violet-400",
78
69
  tools: [
79
70
  { path: "/api",
80
71
  name: "API Explorer",
@@ -84,8 +75,6 @@ icon: "BookOpen" }
84
75
  },
85
76
  {
86
77
  label: "Storage",
87
- dotColor: "bg-amber-400",
88
- iconColor: "text-amber-400",
89
78
  tools: [
90
79
  { path: "/storage",
91
80
  name: "Storage",
@@ -95,8 +84,6 @@ icon: "HardDrive" }
95
84
  },
96
85
  {
97
86
  label: "Access Control",
98
- dotColor: "bg-rose-400",
99
- iconColor: "text-rose-400",
100
87
  tools: [
101
88
  { path: "/users",
102
89
  name: "Users",
@@ -116,6 +103,38 @@ icon: "KeyRound" }
116
103
 
117
104
  /* ═══════════════════════════════════════════════════════════════ */
118
105
 
106
+ const COLLAPSED_STORAGE_KEY = "rebase-studio-home-collapsed";
107
+
108
+ function useStudioCollapsedGroups(groupNames: string[]) {
109
+ const [collapsed, setCollapsed] = useState<Set<string>>(() => {
110
+ try {
111
+ const stored = localStorage.getItem(COLLAPSED_STORAGE_KEY);
112
+ return stored ? new Set(JSON.parse(stored)) : new Set<string>();
113
+ } catch {
114
+ return new Set<string>();
115
+ }
116
+ });
117
+
118
+ const isGroupCollapsed = (name: string) => collapsed.has(name);
119
+
120
+ const toggleGroupCollapsed = (name: string) => {
121
+ setCollapsed(prev => {
122
+ const next = new Set(prev);
123
+ if (next.has(name)) {
124
+ next.delete(name);
125
+ } else {
126
+ next.add(name);
127
+ }
128
+ try {
129
+ localStorage.setItem(COLLAPSED_STORAGE_KEY, JSON.stringify([...next]));
130
+ } catch { /* noop */ }
131
+ return next;
132
+ });
133
+ };
134
+
135
+ return { isGroupCollapsed, toggleGroupCollapsed };
136
+ }
137
+
119
138
  export function StudioHomePage({
120
139
  additionalActions,
121
140
  additionalChildrenStart,
@@ -143,8 +162,20 @@ export function StudioHomePage({
143
162
 
144
163
  const pluginActions = useSlot("home.actions", sectionProps);
145
164
 
165
+ const filteredSections = useMemo(
166
+ () => SECTIONS.filter(s => !hiddenGroups?.includes(s.label)),
167
+ [hiddenGroups]
168
+ );
169
+
170
+ const groupNames = useMemo(
171
+ () => filteredSections.map(s => s.label),
172
+ [filteredSections]
173
+ );
174
+
175
+ const { isGroupCollapsed, toggleGroupCollapsed } = useStudioCollapsedGroups(groupNames);
176
+
146
177
  return (
147
- <div ref={containerRef} className="py-2 overflow-auto h-full w-full">
178
+ <div ref={containerRef} className="py-2 overflow-auto h-full w-full bg-surface-50 dark:bg-surface-800">
148
179
  <Container maxWidth="6xl">
149
180
  <div className="mb-4 flex flex-col gap-2">
150
181
  <SchemaDriftBanner />
@@ -160,77 +191,95 @@ export function StudioHomePage({
160
191
  {additionalChildrenStart}
161
192
 
162
193
  {/* ── Tool sections ── */}
163
- <div className="flex flex-col gap-8 pt-2">
164
- {SECTIONS.map((section) => (
165
- <section key={section.label} aria-label={section.label}>
166
- {/* Section header */}
167
- <Typography
168
- variant="caption"
169
- component="h2"
170
- color="secondary"
171
- className="py-2 font-medium uppercase text-sm text-surface-600 dark:text-surface-400"
172
- >
173
- {section.label}
174
- </Typography>
175
-
176
- {/* Card grid */}
177
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-2">
178
- {section.tools.map((tool) => (
179
- <Card
180
- key={tool.path}
181
- onClick={() => {
182
- navigate(tool.path);
183
- context.analyticsController?.onAnalyticsEvent?.(
184
- "home_navigate_to_view",
185
- { path: tool.path }
186
- );
187
- }}
194
+ {filteredSections.map((section) => {
195
+ const sectionCollapsed = isGroupCollapsed(section.label);
196
+
197
+ return (
198
+ <div key={section.label} className="my-10">
199
+ <ExpandablePanel
200
+ invisible
201
+ expanded={!sectionCollapsed}
202
+ onExpandedChange={(open) => {
203
+ if (open !== !sectionCollapsed) {
204
+ toggleGroupCollapsed(section.label);
205
+ }
206
+ }}
207
+ className="mt-6"
208
+ titleClassName={cls(
209
+ "min-h-0 p-0 border-none",
210
+ "rounded flex items-center justify-between w-full",
211
+ "hover:bg-transparent",
212
+ "cursor-pointer select-none",
213
+ sectionCollapsed && "bg-surface-100 dark:bg-surface-900/50"
214
+ )}
215
+ innerClassName="mt-4 pt-0"
216
+ title={
217
+ <Typography
218
+ variant="caption"
219
+ component="h2"
220
+ color="secondary"
188
221
  className={cls(
189
- "h-full px-4 py-2.5 cursor-pointer transition-all duration-200 ease-in-out",
190
- "hover:-translate-y-0.5 hover:shadow-md hover:shadow-primary/5"
222
+ "px-4 py-1 rounded",
223
+ "font-medium text-[10px] uppercase tracking-[0.08em] text-primary/50 dark:text-primary/70"
191
224
  )}
192
225
  >
193
- <div className="flex flex-col items-start h-full">
194
- <div className="grow w-full">
195
- {/* Icon */}
196
- <div className={cls("h-6 flex items-center", section.iconColor)}>
197
- <IconForView
198
- collectionOrView={{ slug: tool.path,
199
- name: tool.name,
200
- icon: tool.icon }}
201
- size="small"
202
- />
203
- </div>
226
+ {section.label}
227
+ </Typography>
228
+ }
229
+ >
230
+ <div className="mt-4 pt-0">
231
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
232
+ {section.tools.map((tool) => (
233
+ <Card
234
+ key={tool.path}
235
+ onClick={() => {
236
+ navigate(tool.path);
237
+ context.analyticsController?.onAnalyticsEvent?.(
238
+ "home_navigate_to_view",
239
+ { path: tool.path }
240
+ );
241
+ }}
242
+ className={cls(
243
+ "group h-full p-4 cursor-pointer transition-colors duration-150 ease-in-out",
244
+ "hover:bg-primary/5 dark:hover:bg-primary/5"
245
+ )}
246
+ >
247
+ <div className="flex flex-col h-full">
248
+ {/* Header: icon + title */}
249
+ <div className="flex items-center w-full justify-between mb-1">
250
+ <div className="flex items-center gap-3">
251
+ <div className="flex items-center justify-center w-5 h-5 text-surface-400 dark:text-surface-500 transition-colors duration-150 group-hover:text-primary dark:group-hover:text-primary">
252
+ <IconForView
253
+ collectionOrView={{ slug: tool.path, name: tool.name, icon: tool.icon }}
254
+ size="small"
255
+ />
256
+ </div>
257
+ <Typography variant="subtitle1" component="h2">
258
+ {tool.name}
259
+ </Typography>
260
+ </div>
261
+ </div>
204
262
 
205
- {/* Title */}
206
- <Typography
207
- gutterBottom
208
- variant="subtitle1"
209
- className="mt-1 font-semibold"
210
- component="h2"
211
- >
212
- {tool.name}
213
- </Typography>
214
-
215
- {/* Description */}
216
- <Typography variant="caption" color="secondary" component="div">
217
- {tool.description}
218
- </Typography>
219
- </div>
220
-
221
- {/* Arrow */}
222
- <div style={{ alignSelf: "flex-end" }}>
223
- <div className="p-2">
224
- <ArrowRightIcon className="text-primary" size={iconSize.small}/>
263
+ {/* Description indented to align with title */}
264
+ <div className="pl-8">
265
+ {tool.description && (
266
+ <Typography variant="caption" color="secondary" component="div">
267
+ {tool.description}
268
+ </Typography>
269
+ )}
270
+ </div>
271
+
272
+ {/* Spacer */}
273
+ <div className="grow"/>
225
274
  </div>
226
- </div>
227
- </div>
228
- </Card>
229
- ))}
230
- </div>
231
- </section>
232
- ))}
233
- </div>
275
+ </Card>
276
+ ))}
277
+ </div>
278
+ </div>
279
+ </ExpandablePanel>
280
+ </div>
281
+ );
282
+ })}
234
283
 
235
284
  {/* ── SDK Quick Start ── */}
236
285
  <div className="mt-10 mb-6">
@@ -239,7 +288,10 @@ icon: tool.icon }}
239
288
  variant="caption"
240
289
  component="h2"
241
290
  color="secondary"
242
- className="py-2 font-medium uppercase text-sm text-surface-600 dark:text-surface-400"
291
+ className={cls(
292
+ "px-4 py-1 rounded",
293
+ "font-medium text-[10px] uppercase tracking-[0.08em] text-primary/50 dark:text-primary/70"
294
+ )}
243
295
  >
244
296
  Quick Start
245
297
  </Typography>
@@ -285,7 +337,10 @@ icon: tool.icon }}
285
337
  variant="caption"
286
338
  component="h2"
287
339
  color="secondary"
288
- className="p-4 py-2 rounded font-medium uppercase text-sm text-surface-600 dark:text-surface-400"
340
+ className={cls(
341
+ "px-4 py-1 rounded",
342
+ "font-medium text-[10px] uppercase tracking-[0.08em] text-primary/50 dark:text-primary/70"
343
+ )}
289
344
  >
290
345
  {s.title}
291
346
  </Typography>
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@ export {
6
6
  StudioBridgeProvider,
7
7
  StudioBridgeContext,
8
8
  useStudioCollectionRegistry,
9
- useStudioSideEntityController,
9
+ useStudioSidePanelController,
10
10
  useStudioUrlController,
11
11
  useStudioNavigationState,
12
12
  useStudioBreadcrumbs
@@ -1,4 +1,4 @@
1
- import type { EntityCollection, Property, StringProperty, NumberProperty, ArrayProperty, TableColumnInfo, TableMetadata, SecurityOperation, SecurityRule } from "@rebasepro/types";
1
+ import type { CollectionConfig, PostgresProperties, Property, StringProperty, NumberProperty, ArrayProperty, TableColumnInfo, TableMetadata, SecurityOperation, SecurityRule } from "@rebasepro/types";
2
2
 
3
3
  /**
4
4
  * Maps a PostgreSQL column data type to a Rebase property type.
@@ -194,13 +194,13 @@ label: v.replace(/_/g, " ").replace(/\b\w/g, (c: string) => c.toUpperCase()) }))
194
194
  }
195
195
 
196
196
  /**
197
- * Builds a partial EntityCollection from PostgreSQL table metadata.
197
+ * Builds a partial CollectionConfig from PostgreSQL table metadata.
198
198
  * This is used when creating a new collection from an existing database table.
199
199
  */
200
200
  export function buildCollectionFromTableMetadata(
201
201
  tableName: string,
202
202
  metadata: TableMetadata
203
- ): Partial<EntityCollection> {
203
+ ): Partial<CollectionConfig> {
204
204
  const properties: Record<string, Property> = {};
205
205
  const propertiesOrder: string[] = [];
206
206
  const relations: Array<{
@@ -301,7 +301,7 @@ export function buildCollectionFromTableMetadata(
301
301
  name: prettifiedName,
302
302
  slug: tableName,
303
303
  table: tableName,
304
- properties,
304
+ properties: properties as PostgresProperties,
305
305
  propertiesOrder,
306
306
  ...(relations.length > 0 ? { relations } : {}),
307
307
  ...(securityRules.length > 0 ? { securityRules } : {})
@@ -2,7 +2,7 @@
2
2
  import { describe, it, expect } from "@jest/globals";
3
3
  import { determineTableAndPK, extractTablesFromQuery, resolveQueryCollections } from "./sql_utils";
4
4
  import { TableInfo } from "../components/SQLEditor/SQLEditor";
5
- import { EntityCollection } from "@rebasepro/types";
5
+ import { CollectionConfig } from "@rebasepro/types";
6
6
 
7
7
  const mockSchemas: Record<string, TableInfo[]> = {
8
8
  "public": [
@@ -81,31 +81,31 @@ isPrimaryKey: false }
81
81
  ]
82
82
  };
83
83
 
84
- const mockCollections: EntityCollection[] = [
84
+ const mockCollections: CollectionConfig[] = [
85
85
  {
86
86
  slug: "users",
87
87
  name: "Users",
88
88
  table: "users",
89
89
  properties: {}
90
- } as EntityCollection,
90
+ } as CollectionConfig,
91
91
  {
92
92
  slug: "roles",
93
93
  name: "Roles",
94
94
  table: "roles",
95
95
  properties: {}
96
- } as EntityCollection,
96
+ } as CollectionConfig,
97
97
  {
98
98
  slug: "blogPosts",
99
99
  name: "Blog Posts",
100
100
  table: "blog_posts",
101
101
  properties: {}
102
- } as EntityCollection,
102
+ } as CollectionConfig,
103
103
  {
104
104
  slug: "blog-entries", // slug with hyphen, no table → falls back to snake_case "blog_entries"
105
105
  name: "Blog Entries",
106
106
  table: "",
107
107
  properties: {}
108
- } as EntityCollection
108
+ } as CollectionConfig
109
109
  ];
110
110
 
111
111
  describe("determineTableAndPK", () => {
@@ -1,6 +1,6 @@
1
1
  import { parseFirst } from "pgsql-ast-parser";
2
2
  import { TableInfo } from "../components/SQLEditor/SQLEditor";
3
- import { EntityCollection } from "@rebasepro/types";
3
+ import { CollectionConfig } from "@rebasepro/types";
4
4
 
5
5
  /**
6
6
  * A table extracted from a SQL query's FROM/JOIN clauses.
@@ -19,7 +19,7 @@ export interface ResolvedQueryCollection {
19
19
  /** SQL alias if present (e.g. "bp") */
20
20
  tableAlias?: string;
21
21
  /** The matched collection */
22
- collection: EntityCollection;
22
+ collection: CollectionConfig;
23
23
  /** Columns from this table that are present in the result set */
24
24
  columns: string[];
25
25
  /** The result column name that holds the primary key for this table (e.g. "id", "author_id") */
@@ -77,7 +77,7 @@ import { toSnakeCase } from "@rebasepro/utils";
77
77
  export function resolveQueryCollections(
78
78
  sqlString: string,
79
79
  schemas: Record<string, TableInfo[]>,
80
- collections: EntityCollection[],
80
+ collections: CollectionConfig[],
81
81
  resultColumns?: string[]
82
82
  ): ResolvedQueryCollection[] {
83
83
  const tables = extractTablesFromQuery(sqlString);