@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.
- package/README.md +1 -1
- package/dist/{JSEditor-DSucz6wV.js → JSEditor-Sgmw1SHC.js} +28 -32
- package/dist/JSEditor-Sgmw1SHC.js.map +1 -0
- package/dist/{RLSEditor-BM64laoW.js → RLSEditor-DPhky8XW.js} +8 -18
- package/dist/RLSEditor-DPhky8XW.js.map +1 -0
- package/dist/{SQLEditor-CuAhR-zr.js → SQLEditor-CgR2hlUI.js} +5 -5
- package/dist/SQLEditor-CgR2hlUI.js.map +1 -0
- package/dist/{SchemaVisualizer-OibKoD3g.js → SchemaVisualizer-WK4ZZR5X.js} +9 -9
- package/dist/SchemaVisualizer-WK4ZZR5X.js.map +1 -0
- package/dist/{StorageView-CvrnHmDG.js → StorageView-BMhD29YO.js} +33 -5
- package/dist/StorageView-BMhD29YO.js.map +1 -0
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +110 -82
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +174 -133
- package/dist/index.umd.js.map +1 -1
- package/dist/utils/pgColumnToProperty.d.ts +3 -3
- package/dist/utils/sql_utils.d.ts +3 -3
- package/package.json +17 -17
- package/src/components/JSEditor/JSEditor.tsx +7 -7
- package/src/components/JSEditor/JSMonacoEditor.tsx +23 -27
- package/src/components/RLSEditor/RLSEditor.tsx +18 -31
- package/src/components/SQLEditor/SQLEditor.tsx +4 -4
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +7 -7
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +11 -11
- package/src/components/StorageView/StorageView.tsx +43 -4
- package/src/components/StudioHomePage.tsx +139 -84
- package/src/index.ts +1 -1
- package/src/utils/pgColumnToProperty.ts +4 -4
- package/src/utils/sql_utils.test.ts +6 -6
- package/src/utils/sql_utils.ts +3 -3
- package/dist/JSEditor-DSucz6wV.js.map +0 -1
- package/dist/RLSEditor-BM64laoW.js.map +0 -1
- package/dist/SQLEditor-CuAhR-zr.js.map +0 -1
- package/dist/SchemaVisualizer-OibKoD3g.js.map +0 -1
- package/dist/StorageView-CvrnHmDG.js.map +0 -1
- 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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
"
|
|
190
|
-
"
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
</
|
|
227
|
-
|
|
228
|
-
</
|
|
229
|
-
|
|
230
|
-
</
|
|
231
|
-
</
|
|
232
|
-
)
|
|
233
|
-
|
|
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=
|
|
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=
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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<
|
|
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 {
|
|
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:
|
|
84
|
+
const mockCollections: CollectionConfig[] = [
|
|
85
85
|
{
|
|
86
86
|
slug: "users",
|
|
87
87
|
name: "Users",
|
|
88
88
|
table: "users",
|
|
89
89
|
properties: {}
|
|
90
|
-
} as
|
|
90
|
+
} as CollectionConfig,
|
|
91
91
|
{
|
|
92
92
|
slug: "roles",
|
|
93
93
|
name: "Roles",
|
|
94
94
|
table: "roles",
|
|
95
95
|
properties: {}
|
|
96
|
-
} as
|
|
96
|
+
} as CollectionConfig,
|
|
97
97
|
{
|
|
98
98
|
slug: "blogPosts",
|
|
99
99
|
name: "Blog Posts",
|
|
100
100
|
table: "blog_posts",
|
|
101
101
|
properties: {}
|
|
102
|
-
} as
|
|
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
|
|
108
|
+
} as CollectionConfig
|
|
109
109
|
];
|
|
110
110
|
|
|
111
111
|
describe("determineTableAndPK", () => {
|
package/src/utils/sql_utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseFirst } from "pgsql-ast-parser";
|
|
2
2
|
import { TableInfo } from "../components/SQLEditor/SQLEditor";
|
|
3
|
-
import {
|
|
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:
|
|
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:
|
|
80
|
+
collections: CollectionConfig[],
|
|
81
81
|
resultColumns?: string[]
|
|
82
82
|
): ResolvedQueryCollection[] {
|
|
83
83
|
const tables = extractTablesFromQuery(sqlString);
|