@rebasepro/studio 0.0.1-canary.eae7889 → 0.1.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/dist/{ApiExplorer-gMJt5JrS.js → ApiExplorer-DHVmWYfK.js} +13 -13
- package/dist/ApiExplorer-DHVmWYfK.js.map +1 -0
- package/dist/AuthSimulationSelector-CM488Eei.js +106 -0
- package/dist/AuthSimulationSelector-CM488Eei.js.map +1 -0
- package/dist/{JSEditor-D8nVp3Lp.js → JSEditor-CSHA0t_O.js} +2 -2
- package/dist/{JSEditor-D8nVp3Lp.js.map → JSEditor-CSHA0t_O.js.map} +1 -1
- package/dist/{RLSEditor-DBH09u9v.js → RLSEditor-BzDjqo6w.js} +46 -5
- package/dist/RLSEditor-BzDjqo6w.js.map +1 -0
- package/dist/{SQLEditor-CkVx9vgr.js → SQLEditor-Cr9Kg_Qg.js} +63 -75
- package/dist/SQLEditor-Cr9Kg_Qg.js.map +1 -0
- package/dist/{SchemaVisualizer-BgD5Zb77.js → SchemaVisualizer-BGpmzyXT.js} +5 -5
- package/dist/SchemaVisualizer-BGpmzyXT.js.map +1 -0
- package/dist/StorageView-BYoslzBR.js +870 -0
- package/dist/StorageView-BYoslzBR.js.map +1 -0
- package/dist/core/src/components/BootstrapAdminBanner.d.ts +4 -0
- package/dist/core/src/components/LoginView/LoginView.d.ts +22 -0
- package/dist/core/src/components/common/useDataTableController.d.ts +3 -3
- package/dist/core/src/components/index.d.ts +1 -0
- package/dist/core/src/hooks/data/useRelationSelector.d.ts +2 -2
- package/dist/core/src/hooks/index.d.ts +1 -0
- package/dist/core/src/hooks/useCollapsedGroups.d.ts +16 -1
- package/dist/core/src/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/index.es.js +79 -71
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +821 -834
- package/dist/index.umd.js.map +1 -1
- package/dist/types/src/controllers/auth.d.ts +8 -2
- package/dist/types/src/controllers/client.d.ts +13 -0
- package/dist/types/src/controllers/collection_registry.d.ts +2 -1
- package/dist/types/src/controllers/data_driver.d.ts +36 -1
- package/dist/types/src/controllers/navigation.d.ts +18 -6
- package/dist/types/src/controllers/registry.d.ts +9 -1
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -0
- package/dist/types/src/rebase_context.d.ts +17 -0
- package/dist/types/src/types/backend_hooks.d.ts +187 -0
- package/dist/types/src/types/collections.d.ts +31 -11
- package/dist/types/src/types/component_ref.d.ts +47 -0
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +6 -7
- package/dist/types/src/types/formex.d.ts +40 -0
- package/dist/types/src/types/index.d.ts +3 -0
- package/dist/types/src/types/plugins.d.ts +6 -3
- package/dist/types/src/types/properties.d.ts +72 -88
- package/dist/types/src/types/slots.d.ts +20 -10
- package/dist/types/src/types/translations.d.ts +6 -0
- package/dist/ui/src/components/FileUpload.d.ts +1 -1
- package/dist/ui/src/components/SearchBar.d.ts +5 -1
- package/dist/ui/src/styles.d.ts +2 -2
- package/package.json +10 -10
- package/src/components/ApiExplorer/ApiExplorer.tsx +7 -5
- package/src/components/ApiExplorer/TryItPanel.tsx +29 -53
- package/src/components/AuthSimulationSelector.tsx +13 -17
- package/src/components/RLSEditor/RLSEditor.tsx +82 -3
- package/src/components/SQLEditor/SQLEditor.tsx +16 -18
- package/src/components/SQLEditor/SchemaBrowser.tsx +6 -8
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +20 -22
- package/src/components/StorageView/StorageView.tsx +719 -304
- package/src/components/StudioHomePage.tsx +4 -1
- package/dist/ApiExplorer-gMJt5JrS.js.map +0 -1
- package/dist/AuthSimulationSelector-BF4rkRGp.js +0 -118
- package/dist/AuthSimulationSelector-BF4rkRGp.js.map +0 -1
- package/dist/RLSEditor-DBH09u9v.js.map +0 -1
- package/dist/SQLEditor-CkVx9vgr.js.map +0 -1
- package/dist/SchemaVisualizer-BgD5Zb77.js.map +0 -1
- package/dist/StorageView-CTqGFhY9.js +0 -907
- package/dist/StorageView-CTqGFhY9.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
2
|
-
import { Typography, Button, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
2
|
+
import { Typography, Button, TextField, IconButton, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
3
3
|
import { LoaderIcon, SendIcon, PlusIcon, XIcon } from "lucide-react";
|
|
4
4
|
import { useRebaseContext, UserSelectPopover, SelectableUser } from "@rebasepro/core";
|
|
5
5
|
import { AuthSimulationSelector } from "../AuthSimulationSelector";
|
|
@@ -233,19 +233,15 @@ time: elapsed });
|
|
|
233
233
|
>
|
|
234
234
|
Request Body
|
|
235
235
|
</Typography>
|
|
236
|
-
<
|
|
236
|
+
<TextField
|
|
237
|
+
multiline
|
|
238
|
+
minRows={10}
|
|
237
239
|
value={body}
|
|
238
240
|
onChange={(e) => { setBody(e.target.value); setValidationError(null); }}
|
|
239
|
-
rows={10}
|
|
240
241
|
spellCheck={false}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
validationError ? "border-red-500 focus:ring-red-500/30" : cls("border focus:ring-primary/30 dark:focus:ring-primary-dark/30", defaultBorderMixin),
|
|
245
|
-
"text-text-primary dark:text-text-primary-dark",
|
|
246
|
-
"focus:outline-none focus:ring-2",
|
|
247
|
-
"transition-all"
|
|
248
|
-
)}
|
|
242
|
+
error={!!validationError}
|
|
243
|
+
className="w-full"
|
|
244
|
+
inputClassName="font-mono text-xs p-3 resize-y"
|
|
249
245
|
/>
|
|
250
246
|
{validationError && (
|
|
251
247
|
<Typography variant="caption" className="text-red-500 mt-1 block text-xs">
|
|
@@ -352,21 +348,13 @@ function ParamSection({
|
|
|
352
348
|
<code className="text-xs font-mono font-semibold">{p.name}</code>
|
|
353
349
|
{p.required && <span className="text-red-500 ml-0.5 text-xs">*</span>}
|
|
354
350
|
</label>
|
|
355
|
-
<
|
|
356
|
-
|
|
351
|
+
<TextField
|
|
352
|
+
size="small"
|
|
357
353
|
placeholder={p.description ?? p.name}
|
|
358
354
|
value={values[p.name] ?? ""}
|
|
359
355
|
onChange={(e) => onChange(p.name, e.target.value)}
|
|
360
|
-
className=
|
|
361
|
-
|
|
362
|
-
"bg-surface-50 dark:bg-surface-900",
|
|
363
|
-
"border",
|
|
364
|
-
defaultBorderMixin,
|
|
365
|
-
"text-text-primary dark:text-text-primary-dark",
|
|
366
|
-
"placeholder:text-text-secondary/50",
|
|
367
|
-
"focus:outline-none focus:ring-2 focus:ring-primary/30",
|
|
368
|
-
"transition-all"
|
|
369
|
-
)}
|
|
356
|
+
className="flex-1"
|
|
357
|
+
inputClassName="font-mono text-xs"
|
|
370
358
|
/>
|
|
371
359
|
</div>
|
|
372
360
|
))}
|
|
@@ -397,55 +385,43 @@ function CustomKeyValueSection({
|
|
|
397
385
|
>
|
|
398
386
|
{title}
|
|
399
387
|
</Typography>
|
|
400
|
-
<
|
|
388
|
+
<Button
|
|
389
|
+
variant="text"
|
|
390
|
+
size="small"
|
|
391
|
+
color="primary"
|
|
401
392
|
onClick={onAdd}
|
|
402
|
-
className="text-xs
|
|
393
|
+
className="text-xs p-0 min-h-0"
|
|
403
394
|
>
|
|
404
395
|
<PlusIcon size={iconSize.small} className="mr-1" /> Add Header
|
|
405
|
-
</
|
|
396
|
+
</Button>
|
|
406
397
|
</div>
|
|
407
398
|
<div className="space-y-2">
|
|
408
399
|
{values.map((v, i) => (
|
|
409
400
|
<div key={i} className="flex items-center gap-2">
|
|
410
|
-
<
|
|
411
|
-
|
|
401
|
+
<TextField
|
|
402
|
+
size="small"
|
|
412
403
|
placeholder="Header name"
|
|
413
404
|
value={v.key}
|
|
414
405
|
onChange={(e) => onChange(i, e.target.value, v.value)}
|
|
415
|
-
className=
|
|
416
|
-
|
|
417
|
-
"bg-surface-50 dark:bg-surface-900",
|
|
418
|
-
"border",
|
|
419
|
-
defaultBorderMixin,
|
|
420
|
-
"text-text-primary dark:text-text-primary-dark",
|
|
421
|
-
"placeholder:text-text-secondary/50",
|
|
422
|
-
"focus:outline-none focus:ring-2 focus:ring-primary/30",
|
|
423
|
-
"transition-all"
|
|
424
|
-
)}
|
|
406
|
+
className="w-1/3"
|
|
407
|
+
inputClassName="font-mono text-xs"
|
|
425
408
|
/>
|
|
426
|
-
<
|
|
427
|
-
|
|
409
|
+
<TextField
|
|
410
|
+
size="small"
|
|
428
411
|
placeholder="Value"
|
|
429
412
|
value={v.value}
|
|
430
413
|
onChange={(e) => onChange(i, v.key, e.target.value)}
|
|
431
|
-
className=
|
|
432
|
-
|
|
433
|
-
"bg-surface-50 dark:bg-surface-900",
|
|
434
|
-
"border",
|
|
435
|
-
defaultBorderMixin,
|
|
436
|
-
"text-text-primary dark:text-text-primary-dark",
|
|
437
|
-
"placeholder:text-text-secondary/50",
|
|
438
|
-
"focus:outline-none focus:ring-2 focus:ring-primary/30",
|
|
439
|
-
"transition-all"
|
|
440
|
-
)}
|
|
414
|
+
className="flex-1"
|
|
415
|
+
inputClassName="font-mono text-xs"
|
|
441
416
|
/>
|
|
442
|
-
<
|
|
417
|
+
<IconButton
|
|
418
|
+
size="small"
|
|
443
419
|
onClick={() => onRemove(i)}
|
|
444
|
-
className="
|
|
420
|
+
className="text-text-secondary hover:text-red-500 shrink-0"
|
|
445
421
|
title="Remove"
|
|
446
422
|
>
|
|
447
423
|
<XIcon size={iconSize.small} />
|
|
448
|
-
</
|
|
424
|
+
</IconButton>
|
|
449
425
|
</div>
|
|
450
426
|
))}
|
|
451
427
|
{values.length === 0 && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Typography, cls, iconSize } from "@rebasepro/ui";
|
|
2
|
+
import { Typography, cls, iconSize, Button } from "@rebasepro/ui";
|
|
3
3
|
import { KeyRoundIcon } from "lucide-react";
|
|
4
4
|
import { UserSelectPopover, SelectableUser } from "@rebasepro/core";
|
|
5
5
|
|
|
@@ -31,28 +31,24 @@ export function AuthSimulationSelector({
|
|
|
31
31
|
>
|
|
32
32
|
Auth:
|
|
33
33
|
</Typography>
|
|
34
|
-
<
|
|
34
|
+
<Button
|
|
35
|
+
size="small"
|
|
36
|
+
variant={authMode === "jwt" ? "outlined" : "outlined"}
|
|
37
|
+
color={authMode === "jwt" ? "primary" : "neutral"}
|
|
35
38
|
onClick={() => setAuthMode("jwt")}
|
|
36
|
-
className=
|
|
37
|
-
"px-3 py-1 text-xs rounded-full border transition-all",
|
|
38
|
-
authMode === "jwt"
|
|
39
|
-
? "bg-primary/15 text-primary dark:text-primary-dark border-primary/30 font-medium"
|
|
40
|
-
: "border-surface-300 dark:border-surface-600 text-text-secondary dark:text-text-secondary-dark hover:border-primary/30"
|
|
41
|
-
)}
|
|
39
|
+
className="rounded-full !px-3 !py-1 !text-xs"
|
|
42
40
|
>
|
|
43
41
|
JWT Token
|
|
44
|
-
</
|
|
45
|
-
<
|
|
42
|
+
</Button>
|
|
43
|
+
<Button
|
|
44
|
+
size="small"
|
|
45
|
+
variant={authMode === "none" ? "outlined" : "outlined"}
|
|
46
|
+
color={authMode === "none" ? "error" : "neutral"}
|
|
46
47
|
onClick={() => setAuthMode("none")}
|
|
47
|
-
className=
|
|
48
|
-
"px-3 py-1 text-xs rounded-full border transition-all",
|
|
49
|
-
authMode === "none"
|
|
50
|
-
? "bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/30 font-medium"
|
|
51
|
-
: "border-surface-300 dark:border-surface-600 text-text-secondary dark:text-text-secondary-dark hover:border-red-500/30"
|
|
52
|
-
)}
|
|
48
|
+
className="rounded-full !px-3 !py-1 !text-xs"
|
|
53
49
|
>
|
|
54
50
|
No Auth
|
|
55
|
-
</
|
|
51
|
+
</Button>
|
|
56
52
|
|
|
57
53
|
{authMode === "jwt" && (
|
|
58
54
|
<>
|
|
@@ -7,6 +7,18 @@ import { useRebaseContext, useSnackbarController, ErrorView, useTranslation } fr
|
|
|
7
7
|
import { isPostgresCollection } from "@rebasepro/types";
|
|
8
8
|
import { PolicyEditor } from "./PolicyEditor";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Validates and double-quotes a SQL identifier to prevent injection.
|
|
12
|
+
* Only allows safe Postgres identifiers (letters, digits, underscores).
|
|
13
|
+
* Throws if the identifier contains unsafe characters.
|
|
14
|
+
*/
|
|
15
|
+
function sanitizeSqlIdentifier(name: string): string {
|
|
16
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
17
|
+
throw new Error(`Invalid SQL identifier: "${name}". Only letters, digits, and underscores are allowed.`);
|
|
18
|
+
}
|
|
19
|
+
return `"${name}"`;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
export interface PostgresPolicy {
|
|
11
23
|
policyname: string;
|
|
12
24
|
tablename: string;
|
|
@@ -402,6 +414,35 @@ totalPolicies };
|
|
|
402
414
|
<Typography variant="body2" className="font-mono text-[13px] font-medium">{rlsStats.totalPolicies}</Typography>
|
|
403
415
|
</div>
|
|
404
416
|
</div>
|
|
417
|
+
|
|
418
|
+
{/* Security health indicators */}
|
|
419
|
+
{rlsStats.total - rlsStats.enabled > 0 && (
|
|
420
|
+
<div className={cls("p-2.5 rounded border border-yellow-200 dark:border-yellow-900/50 bg-yellow-50 dark:bg-yellow-900/20 flex items-start gap-2", defaultBorderMixin)}>
|
|
421
|
+
<AlertTriangleIcon size={14} className="text-yellow-600 dark:text-yellow-500 mt-0.5 shrink-0"/>
|
|
422
|
+
<div>
|
|
423
|
+
<Typography variant="caption" className="text-yellow-800 dark:text-yellow-400 text-[11px] font-semibold block">
|
|
424
|
+
{rlsStats.total - rlsStats.enabled} table{rlsStats.total - rlsStats.enabled > 1 ? "s" : ""} without RLS
|
|
425
|
+
</Typography>
|
|
426
|
+
<Typography variant="caption" className="text-yellow-700 dark:text-yellow-600 text-[10px] block mt-0.5">
|
|
427
|
+
These tables have no row-level access control. If auth enforcement is disabled, data may be publicly accessible.
|
|
428
|
+
</Typography>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
|
|
433
|
+
{rlsStats.enabled > 0 && rlsStats.enabled - rlsStats.withPolicies > 0 && (
|
|
434
|
+
<div className={cls("p-2.5 rounded border border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20 flex items-start gap-2", defaultBorderMixin)}>
|
|
435
|
+
<ShieldIcon size={14} className="text-blue-600 dark:text-blue-400 mt-0.5 shrink-0"/>
|
|
436
|
+
<div>
|
|
437
|
+
<Typography variant="caption" className="text-blue-800 dark:text-blue-300 text-[11px] font-semibold block">
|
|
438
|
+
{rlsStats.enabled - rlsStats.withPolicies} table{rlsStats.enabled - rlsStats.withPolicies > 1 ? "s" : ""} with RLS but no policies
|
|
439
|
+
</Typography>
|
|
440
|
+
<Typography variant="caption" className="text-blue-700 dark:text-blue-500 text-[10px] block mt-0.5">
|
|
441
|
+
RLS is enabled but no permissive policies exist. All access is denied by default (Postgres deny-all).
|
|
442
|
+
</Typography>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
405
446
|
</div>
|
|
406
447
|
</div>
|
|
407
448
|
)}
|
|
@@ -437,7 +478,7 @@ totalPolicies };
|
|
|
437
478
|
const action = activeTableData.rlsEnabled ? "DISABLE" : "ENABLE";
|
|
438
479
|
if (!confirm(`Are you sure you want to ${action.toLowerCase()} Row Level Security on "${table}"?`)) return;
|
|
439
480
|
try {
|
|
440
|
-
await databaseAdmin!.executeSql!(`ALTER TABLE
|
|
481
|
+
await databaseAdmin!.executeSql!(`ALTER TABLE ${sanitizeSqlIdentifier(table)} ${action} ROW LEVEL SECURITY`);
|
|
441
482
|
snackbarController.open({ type: "success",
|
|
442
483
|
message: `RLS ${action.toLowerCase()}d on ${table}` });
|
|
443
484
|
fetchRLSData();
|
|
@@ -476,7 +517,11 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
476
517
|
</div>
|
|
477
518
|
</div>
|
|
478
519
|
|
|
479
|
-
{
|
|
520
|
+
{isLoading && !activeTableData ? (
|
|
521
|
+
<div className="flex-grow flex items-center justify-center h-full">
|
|
522
|
+
<CircularProgress size="small"/>
|
|
523
|
+
</div>
|
|
524
|
+
) : error ? (
|
|
480
525
|
<div className="p-6 h-full flex items-center justify-center">
|
|
481
526
|
<ErrorView title={t("studio_rls_error")} error={error} onRetry={fetchRLSData}/>
|
|
482
527
|
</div>
|
|
@@ -660,7 +705,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
660
705
|
const table = activeTableData!.tableName;
|
|
661
706
|
if (!confirm(`Drop policy "${policy.policyname}" from table "${table}"?`)) return;
|
|
662
707
|
try {
|
|
663
|
-
await databaseAdmin!.executeSql!(`DROP POLICY
|
|
708
|
+
await databaseAdmin!.executeSql!(`DROP POLICY ${sanitizeSqlIdentifier(policy.policyname)} ON ${sanitizeSqlIdentifier(table)}`);
|
|
664
709
|
snackbarController.open({ type: "success",
|
|
665
710
|
message: `Policy "${policy.policyname}" dropped` });
|
|
666
711
|
fetchRLSData();
|
|
@@ -680,6 +725,40 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
680
725
|
</div>
|
|
681
726
|
)}
|
|
682
727
|
|
|
728
|
+
{activeTableData && mergedPolicies.length === 0 && activeTableData.rlsEnabled && (
|
|
729
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
730
|
+
<ShieldIcon size={40} className="text-surface-300 dark:text-surface-600 mb-4"/>
|
|
731
|
+
<Typography variant="subtitle2" className="text-text-secondary dark:text-text-secondary-dark mb-2">
|
|
732
|
+
No policies defined
|
|
733
|
+
</Typography>
|
|
734
|
+
<Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark max-w-sm mb-4">
|
|
735
|
+
RLS is enabled on this table but no policies exist. All access is denied by default (Postgres deny-all). Create a policy to allow specific access.
|
|
736
|
+
</Typography>
|
|
737
|
+
{activeCollection && (
|
|
738
|
+
<Button
|
|
739
|
+
size="small"
|
|
740
|
+
variant="filled"
|
|
741
|
+
color="primary"
|
|
742
|
+
onClick={() => setEditingPolicy("new")}
|
|
743
|
+
>
|
|
744
|
+
{t("studio_rls_create_policy")}
|
|
745
|
+
</Button>
|
|
746
|
+
)}
|
|
747
|
+
</div>
|
|
748
|
+
)}
|
|
749
|
+
|
|
750
|
+
{activeTableData && mergedPolicies.length === 0 && !activeTableData.rlsEnabled && activeCollection && (
|
|
751
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
752
|
+
<AlertTriangleIcon size={40} className="text-yellow-400 dark:text-yellow-600 mb-4"/>
|
|
753
|
+
<Typography variant="subtitle2" className="text-text-secondary dark:text-text-secondary-dark mb-2">
|
|
754
|
+
No access control
|
|
755
|
+
</Typography>
|
|
756
|
+
<Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark max-w-sm">
|
|
757
|
+
This table has neither RLS nor policies. Enable RLS and create policies to restrict row-level access.
|
|
758
|
+
</Typography>
|
|
759
|
+
</div>
|
|
760
|
+
)}
|
|
761
|
+
|
|
683
762
|
</div>
|
|
684
763
|
</div>
|
|
685
764
|
</div>
|
|
@@ -3,7 +3,7 @@ import { IconForView } from "@rebasepro/core";
|
|
|
3
3
|
import { useStudioCollectionRegistry, useStudioSideEntityController } from "@rebasepro/core";
|
|
4
4
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
|
-
import { Button, Paper, Typography, CircularProgress, cls, IconButton, InputLabel, Dialog, DialogContent, DialogActions, DialogTitle, TextField, Tooltip, Alert, Tabs, Tab, defaultBorderMixin, Select, SelectItem, Menu, MenuItem, ResizablePanels, Chip, VirtualTable, VirtualTableColumn , iconSize } from "@rebasepro/ui";
|
|
6
|
+
import { Button, Paper, Typography, CircularProgress, cls, IconButton, InputLabel, Dialog, DialogContent, DialogActions, DialogTitle, TextField, Tooltip, Alert, Tabs, Tab, defaultBorderMixin, Select, SelectItem, Menu, MenuItem, ResizablePanels, Chip, VirtualTable, VirtualTableColumn , iconSize, Checkbox, TextareaAutosize } from "@rebasepro/ui";
|
|
7
7
|
import { DatabaseIcon, TerminalIcon, XIcon, PlusIcon, PencilIcon, MoreVerticalIcon, MenuIcon, PlayIcon } from "lucide-react";
|
|
8
8
|
// VirtualTableInput is conditionally loaded from CMS when available
|
|
9
9
|
let VirtualTableInput: React.ComponentType<any> | null = null;
|
|
@@ -122,21 +122,15 @@ const FixedEditorOverlay = ({
|
|
|
122
122
|
maxWidth: Math.min(400, windowSize.width - left - 10)
|
|
123
123
|
}}
|
|
124
124
|
>
|
|
125
|
-
<
|
|
125
|
+
<TextareaAutosize
|
|
126
126
|
className="w-full h-full bg-transparent outline-none border-none ring-0 font-mono text-[13px] text-text-primary dark:text-text-primary-dark px-4 py-1.5 resize-none overflow-y-auto"
|
|
127
127
|
defaultValue={displayValue}
|
|
128
128
|
autoFocus
|
|
129
|
-
style={{ minHeight: '32px' }}
|
|
129
|
+
style={{ minHeight: '32px', maxHeight: resolvedMaxHeight }}
|
|
130
130
|
onFocus={(e) => {
|
|
131
131
|
const val = e.target.value;
|
|
132
132
|
e.target.value = "";
|
|
133
133
|
e.target.value = val;
|
|
134
|
-
e.target.style.height = 'auto';
|
|
135
|
-
e.target.style.height = `${Math.min(e.target.scrollHeight, resolvedMaxHeight)}px`;
|
|
136
|
-
}}
|
|
137
|
-
onChange={(e) => {
|
|
138
|
-
e.target.style.height = 'auto';
|
|
139
|
-
e.target.style.height = `${Math.min(e.target.scrollHeight, resolvedMaxHeight)}px`;
|
|
140
134
|
}}
|
|
141
135
|
onBlur={(e) => {
|
|
142
136
|
onSave(e.target.value || null);
|
|
@@ -1232,7 +1226,7 @@ id: String(ra.entityId) })}
|
|
|
1232
1226
|
</div>
|
|
1233
1227
|
<div className="flex shrink-0 items-center justify-end pr-2 gap-1.5">
|
|
1234
1228
|
<Tooltip title={t("studio_sql_format_sql")}>
|
|
1235
|
-
<IconButton size="small" onClick={handlePrettify}
|
|
1229
|
+
<IconButton size="small" onClick={handlePrettify}>
|
|
1236
1230
|
<MenuIcon size={iconSize.smallest}/>
|
|
1237
1231
|
</IconButton>
|
|
1238
1232
|
</Tooltip>
|
|
@@ -1248,15 +1242,19 @@ id: String(ra.entityId) })}
|
|
|
1248
1242
|
|
|
1249
1243
|
<div className="h-4 w-px bg-surface-200 dark:bg-surface-950 mx-1"></div>
|
|
1250
1244
|
|
|
1251
|
-
<div className="flex items-center space-x-2 px-2
|
|
1245
|
+
<div className="flex items-center space-x-2 px-2" onClick={(e) => {
|
|
1246
|
+
setAutoLimit(!autoLimit);
|
|
1247
|
+
e.stopPropagation();
|
|
1248
|
+
}}>
|
|
1252
1249
|
<Typography variant="caption" className="text-[11px] text-text-secondary cursor-pointer select-none">{t("studio_sql_limit_1000")}</Typography>
|
|
1253
|
-
<
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1250
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
1251
|
+
<Checkbox
|
|
1252
|
+
checked={autoLimit}
|
|
1253
|
+
onCheckedChange={setAutoLimit}
|
|
1254
|
+
size="smallest"
|
|
1255
|
+
padding={false}
|
|
1256
|
+
/>
|
|
1257
|
+
</div>
|
|
1260
1258
|
</div>
|
|
1261
1259
|
|
|
1262
1260
|
<div className="h-4 w-px bg-surface-200 dark:bg-surface-950 mx-1"></div>
|
|
@@ -67,28 +67,26 @@ export const SchemaBrowser = ({
|
|
|
67
67
|
<svg className="w-3.5 h-3.5 mr-1 shrink-0 text-text-disabled dark:text-text-disabled-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
68
68
|
<Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark text-xs truncate flex-1 min-w-0">{table.tableName}</Typography>
|
|
69
69
|
|
|
70
|
-
<div className="flex opacity-0 group-hover:opacity-100 focus-within:opacity-100 absolute right-1 items-center bg-surface-100 dark:bg-surface-
|
|
70
|
+
<div className="flex opacity-0 group-hover:opacity-100 focus-within:opacity-100 absolute right-1 items-center bg-surface-100 dark:bg-surface-800 px-1 gap-1 rounded transition-opacity">
|
|
71
71
|
<IconButton
|
|
72
|
-
size="
|
|
73
|
-
className="transition-colors text-text-secondary hover:text-text-primary pointer-events-auto"
|
|
72
|
+
size="small"
|
|
74
73
|
onClick={(e) => {
|
|
75
74
|
e.stopPropagation();
|
|
76
75
|
navigator.clipboard.writeText(table.tableName);
|
|
77
76
|
}}
|
|
78
77
|
title="CopyIcon table name"
|
|
79
78
|
>
|
|
80
|
-
<CopyIcon size={iconSize.
|
|
79
|
+
<CopyIcon size={iconSize.small}/>
|
|
81
80
|
</IconButton>
|
|
82
81
|
|
|
83
82
|
<Menu
|
|
84
83
|
trigger={
|
|
85
84
|
<IconButton
|
|
86
|
-
size={"
|
|
87
|
-
className="transition-colors text-text-secondary hover:text-text-primary pointer-events-auto"
|
|
85
|
+
size={"small"}
|
|
88
86
|
onClick={(e) => e.stopPropagation()}
|
|
89
87
|
title="Generate SQL templates"
|
|
90
88
|
>
|
|
91
|
-
<MoreVerticalIcon size={iconSize.
|
|
89
|
+
<MoreVerticalIcon size={iconSize.small}/>
|
|
92
90
|
</IconButton>
|
|
93
91
|
}
|
|
94
92
|
>
|
|
@@ -133,7 +131,7 @@ export const SchemaBrowser = ({
|
|
|
133
131
|
<Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark text-[9px] truncate mr-1 uppercase shrink-0" title={col.dataType}>{col.dataType}</Typography>
|
|
134
132
|
<IconButton
|
|
135
133
|
size="smallest"
|
|
136
|
-
className="opacity-0 group-hover:opacity-100 absolute right-1 bg-surface-50 dark:bg-surface-
|
|
134
|
+
className="opacity-0 group-hover:opacity-100 absolute right-1 bg-surface-50 dark:bg-surface-800 transition-colors pointer-events-auto"
|
|
137
135
|
onClick={(e) => {
|
|
138
136
|
e.stopPropagation();
|
|
139
137
|
navigator.clipboard.writeText(col.name);
|
|
@@ -13,13 +13,16 @@ import {
|
|
|
13
13
|
import type { Node, Edge, NodeChange, EdgeChange } from "@xyflow/react";
|
|
14
14
|
import "@xyflow/react/dist/style.css";
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
Button,
|
|
17
|
+
SearchBar,
|
|
17
18
|
TextField,
|
|
19
|
+
Tooltip,
|
|
20
|
+
Alert,
|
|
21
|
+
Typography,
|
|
18
22
|
cls,
|
|
19
23
|
defaultBorderMixin,
|
|
20
24
|
CircularProgress,
|
|
21
25
|
ResizablePanels,
|
|
22
|
-
Tooltip,
|
|
23
26
|
IconButton
|
|
24
27
|
} from "@rebasepro/ui";
|
|
25
28
|
import {
|
|
@@ -230,12 +233,11 @@ duration: 400 }
|
|
|
230
233
|
|
|
231
234
|
{/* Search */}
|
|
232
235
|
<div className="px-2 py-1.5 border-b border-surface-200/40 dark:border-surface-700/40">
|
|
233
|
-
<
|
|
236
|
+
<SearchBar
|
|
234
237
|
size="smallest"
|
|
235
238
|
placeholder="Filter tables…"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
inputClassName="text-xs"
|
|
239
|
+
onTextSearch={(val) => setSearchQuery(val ?? "")}
|
|
240
|
+
innerClassName="text-xs"
|
|
239
241
|
/>
|
|
240
242
|
</div>
|
|
241
243
|
|
|
@@ -443,30 +445,26 @@ duration: 400 }
|
|
|
443
445
|
{/* Direction toggle */}
|
|
444
446
|
<div className="flex items-center bg-surface-100 dark:bg-surface-950 rounded-md border border-surface-200/40 dark:border-surface-700/40">
|
|
445
447
|
<Tooltip title="Left to right layout">
|
|
446
|
-
<
|
|
448
|
+
<Button
|
|
449
|
+
size="small"
|
|
450
|
+
variant={direction === "LR" ? "filled" : "text"}
|
|
451
|
+
color={direction === "LR" ? "primary" : "neutral"}
|
|
447
452
|
onClick={() => setDirection("LR")}
|
|
448
|
-
className=
|
|
449
|
-
"px-2 py-1 text-[10px] font-mono rounded-l-md transition-colors",
|
|
450
|
-
direction === "LR"
|
|
451
|
-
? "bg-primary text-white"
|
|
452
|
-
: "text-text-secondary dark:text-text-secondary-dark hover:bg-surface-200 dark:hover:bg-surface-700"
|
|
453
|
-
)}
|
|
453
|
+
className="!rounded-r-none !px-2 !py-1 !text-[10px] !font-mono"
|
|
454
454
|
>
|
|
455
455
|
LR
|
|
456
|
-
</
|
|
456
|
+
</Button>
|
|
457
457
|
</Tooltip>
|
|
458
458
|
<Tooltip title="Top to bottom layout">
|
|
459
|
-
<
|
|
459
|
+
<Button
|
|
460
|
+
size="small"
|
|
461
|
+
variant={direction === "TB" ? "filled" : "text"}
|
|
462
|
+
color={direction === "TB" ? "primary" : "neutral"}
|
|
460
463
|
onClick={() => setDirection("TB")}
|
|
461
|
-
className=
|
|
462
|
-
"px-2 py-1 text-[10px] font-mono rounded-r-md transition-colors",
|
|
463
|
-
direction === "TB"
|
|
464
|
-
? "bg-primary text-white"
|
|
465
|
-
: "text-text-secondary dark:text-text-secondary-dark hover:bg-surface-200 dark:hover:bg-surface-700"
|
|
466
|
-
)}
|
|
464
|
+
className="!rounded-l-none !px-2 !py-1 !text-[10px] !font-mono"
|
|
467
465
|
>
|
|
468
466
|
TB
|
|
469
|
-
</
|
|
467
|
+
</Button>
|
|
470
468
|
</Tooltip>
|
|
471
469
|
</div>
|
|
472
470
|
|