@rebasepro/studio 0.2.3 → 0.2.5
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-BmcdhAX0.js → ApiExplorer-CGHEF1uL.js} +4 -4
- package/dist/ApiExplorer-CGHEF1uL.js.map +1 -0
- package/dist/{CronJobsView-CNfz0etw.js → CronJobsView-3PM_qR8v.js} +20 -3
- package/dist/CronJobsView-3PM_qR8v.js.map +1 -0
- package/dist/{JSEditor-Ch8z8lJ4.js → JSEditor-BCSoElPg.js} +26 -36
- package/dist/JSEditor-BCSoElPg.js.map +1 -0
- package/dist/LogsExplorer-_4sZadKn.js +162 -0
- package/dist/LogsExplorer-_4sZadKn.js.map +1 -0
- package/dist/{SQLEditor-BELYJQRP.js → SQLEditor-BC0IOUQu.js} +4 -4
- package/dist/SQLEditor-BC0IOUQu.js.map +1 -0
- package/dist/common/src/collections/default-collections.d.ts +9 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/core/src/components/LoginView/LoginView.d.ts +25 -1
- package/dist/core/src/components/common/types.d.ts +10 -7
- package/dist/core/src/components/common/useDebouncedData.d.ts +1 -1
- package/dist/core/src/core/RebaseProps.d.ts +13 -2
- package/dist/core/src/core/RebaseRouter.d.ts +1 -1
- package/dist/core/src/hooks/data/useCollectionFetch.d.ts +12 -1
- package/dist/core/src/hooks/index.d.ts +0 -1
- package/dist/core/src/util/entity_cache.d.ts +0 -5
- package/dist/core/src/util/index.d.ts +0 -2
- package/dist/core/src/util/useStorageUploadController.d.ts +2 -2
- package/dist/formex/src/utils.d.ts +2 -2
- package/dist/index.es.js +23 -5
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +228 -44
- package/dist/index.umd.js.map +1 -1
- package/dist/studio/src/components/ApiExplorer/parseSpec.d.ts +1 -1
- package/dist/studio/src/components/ApiExplorer/types.d.ts +3 -3
- package/dist/studio/src/components/LogsExplorer/LogsExplorer.d.ts +1 -0
- package/dist/types/src/controllers/auth.d.ts +4 -26
- package/dist/types/src/controllers/client.d.ts +25 -43
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +23 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +5 -60
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +22 -57
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/dist/ui/src/components/Button.d.ts +2 -2
- package/dist/ui/src/components/ErrorBoundary.d.ts +25 -3
- package/dist/ui/src/components/VirtualTable/VirtualTable.d.ts +1 -1
- package/dist/ui/src/components/VirtualTable/VirtualTableCell.d.ts +6 -6
- package/dist/ui/src/components/VirtualTable/VirtualTableHeader.d.ts +8 -8
- package/dist/ui/src/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/ui/src/components/VirtualTable/VirtualTableProps.d.ts +11 -11
- package/dist/ui/src/components/VirtualTable/VirtualTableRow.d.ts +1 -1
- package/dist/ui/src/components/VirtualTable/types.d.ts +9 -9
- package/dist/ui/src/hooks/useDebounceCallback.d.ts +1 -1
- package/dist/ui/src/util/debounce.d.ts +1 -1
- package/package.json +8 -8
- package/src/components/ApiExplorer/ApiExplorer.tsx +2 -2
- package/src/components/ApiExplorer/EndpointDetail.tsx +1 -1
- package/src/components/ApiExplorer/TryItPanel.tsx +5 -5
- package/src/components/ApiExplorer/parseSpec.ts +3 -3
- package/src/components/ApiExplorer/types.ts +3 -3
- package/src/components/CronJobs/CronJobsView.tsx +27 -2
- package/src/components/JSEditor/JSEditor.tsx +21 -18
- package/src/components/JSEditor/JSMonacoEditor.tsx +6 -19
- package/src/components/LogsExplorer/LogsExplorer.tsx +224 -0
- package/src/components/RebaseStudio.tsx +10 -1
- package/src/components/SQLEditor/SQLEditor.tsx +28 -7
- package/src/components/StudioHomePage.tsx +2 -1
- package/src/utils/parseSpec.test.ts +274 -0
- package/src/utils/pgColumnToProperty.test.ts +1 -0
- package/src/utils/pgColumnToProperty.ts +35 -4
- package/dist/ApiExplorer-BmcdhAX0.js.map +0 -1
- package/dist/CronJobsView-CNfz0etw.js.map +0 -1
- package/dist/JSEditor-Ch8z8lJ4.js.map +0 -1
- package/dist/SQLEditor-BELYJQRP.js.map +0 -1
- package/dist/core/src/hooks/useValidateAuthenticator.d.ts +0 -21
- package/dist/core/src/util/icon_synonyms.d.ts +0 -1
- package/dist/core/src/util/useTraceUpdate.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -22
|
@@ -47,12 +47,12 @@ import { AuthSimulationSelector } from "../AuthSimulationSelector";
|
|
|
47
47
|
|
|
48
48
|
interface ConsoleEntry {
|
|
49
49
|
type: "log" | "warn" | "error" | "info";
|
|
50
|
-
args:
|
|
50
|
+
args: unknown[];
|
|
51
51
|
timestamp: number;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
interface ExecutionResult {
|
|
55
|
-
value:
|
|
55
|
+
value: unknown;
|
|
56
56
|
console: ConsoleEntry[];
|
|
57
57
|
duration: number;
|
|
58
58
|
error?: string;
|
|
@@ -101,7 +101,7 @@ function saveToStorage<T>(key: string, value: T) {
|
|
|
101
101
|
} catch { /* quota */ }
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function formatJSON(value:
|
|
104
|
+
function formatJSON(value: unknown): string {
|
|
105
105
|
try {
|
|
106
106
|
return JSON.stringify(value, null, 2);
|
|
107
107
|
} catch {
|
|
@@ -125,7 +125,7 @@ interface MatchedJSCollection {
|
|
|
125
125
|
*/
|
|
126
126
|
function detectCollectionsInResult(
|
|
127
127
|
code: string,
|
|
128
|
-
resultValue:
|
|
128
|
+
resultValue: unknown,
|
|
129
129
|
collections: EntityCollection[]
|
|
130
130
|
): MatchedJSCollection[] {
|
|
131
131
|
if (!resultValue || !collections?.length) return [];
|
|
@@ -151,9 +151,10 @@ function detectCollectionsInResult(
|
|
|
151
151
|
if (mentionedSlugs.size === 0) return [];
|
|
152
152
|
|
|
153
153
|
// Check if result has rows with an "id" field
|
|
154
|
-
let rows:
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
let rows: Record<string, unknown>[] = [];
|
|
155
|
+
const rv = resultValue as Record<string, unknown>;
|
|
156
|
+
if (rv?.data && Array.isArray(rv.data)) {
|
|
157
|
+
rows = rv.data;
|
|
157
158
|
} else if (Array.isArray(resultValue)) {
|
|
158
159
|
rows = resultValue;
|
|
159
160
|
}
|
|
@@ -405,7 +406,7 @@ isScoped: true };
|
|
|
405
406
|
info: console.info
|
|
406
407
|
};
|
|
407
408
|
|
|
408
|
-
const captureConsole = (type: ConsoleEntry["type"]) => (...args:
|
|
409
|
+
const captureConsole = (type: ConsoleEntry["type"]) => (...args: unknown[]) => {
|
|
409
410
|
consoleEntries.push({ type,
|
|
410
411
|
args,
|
|
411
412
|
timestamp: Date.now() });
|
|
@@ -451,7 +452,8 @@ timestamp: Date.now() });
|
|
|
451
452
|
});
|
|
452
453
|
|
|
453
454
|
// Auto-detect best view
|
|
454
|
-
|
|
455
|
+
const resultObj = value as Record<string, unknown> | undefined;
|
|
456
|
+
if (resultObj?.data && Array.isArray(resultObj.data)) {
|
|
455
457
|
setResultView("table");
|
|
456
458
|
} else if (consoleEntries.length > 0 && value === undefined) {
|
|
457
459
|
setResultView("console");
|
|
@@ -522,11 +524,12 @@ message: "Snippet saved" });
|
|
|
522
524
|
if (!result?.value) return { columns: [] as VirtualTableColumn[],
|
|
523
525
|
data: [] as Record<string, unknown>[] };
|
|
524
526
|
|
|
525
|
-
let rows:
|
|
526
|
-
|
|
527
|
-
|
|
527
|
+
let rows: Record<string, unknown>[] = [];
|
|
528
|
+
const val = result.value as Record<string, unknown>;
|
|
529
|
+
if (val?.data && Array.isArray(val.data)) {
|
|
530
|
+
rows = (val.data as Record<string, unknown>[]).map((entity) => ({
|
|
528
531
|
id: entity.id,
|
|
529
|
-
...entity.values,
|
|
532
|
+
...(entity.values as Record<string, unknown> ?? {}),
|
|
530
533
|
...(entity.values ? {} : entity)
|
|
531
534
|
}));
|
|
532
535
|
} else if (Array.isArray(result.value)) {
|
|
@@ -564,13 +567,13 @@ data: rows };
|
|
|
564
567
|
);
|
|
565
568
|
}, [result, activeTab?.code, collectionRegistry?.collections]);
|
|
566
569
|
|
|
567
|
-
const getRowEntityActions = useCallback((rowData:
|
|
570
|
+
const getRowEntityActions = useCallback((rowData: Record<string, unknown>): { collection: MatchedJSCollection; entityId: string | number }[] => {
|
|
568
571
|
if (!rowData || matchedCollections.length === 0) return [];
|
|
569
572
|
return matchedCollections
|
|
570
573
|
.filter(mc => rowData[mc.pkColumn] != null)
|
|
571
574
|
.map(mc => ({
|
|
572
575
|
collection: mc,
|
|
573
|
-
entityId: rowData[mc.pkColumn]
|
|
576
|
+
entityId: rowData[mc.pkColumn] as string | number
|
|
574
577
|
}));
|
|
575
578
|
}, [matchedCollections]);
|
|
576
579
|
|
|
@@ -685,7 +688,7 @@ message: t("studio_sql_markdown_copy_failed") });
|
|
|
685
688
|
</IconButton>
|
|
686
689
|
</Tooltip>
|
|
687
690
|
|
|
688
|
-
{result?.value && (
|
|
691
|
+
{result?.value != null && (
|
|
689
692
|
<Tooltip title="Export result as JSON">
|
|
690
693
|
<IconButton size="small" onClick={exportResult}>
|
|
691
694
|
<DownloadIcon size={iconSize.smallest}/>
|
|
@@ -843,7 +846,7 @@ resizable: false }, ...tableData.columns]
|
|
|
843
846
|
cellRenderer={({ rowData, column, rowIndex }: CellRendererParams<Record<string, unknown>>) => {
|
|
844
847
|
// Entity action column
|
|
845
848
|
if (column.key === "__entity_action__") {
|
|
846
|
-
const rowActions = getRowEntityActions(rowData);
|
|
849
|
+
const rowActions = getRowEntityActions(rowData ?? {});
|
|
847
850
|
if (rowActions.length === 0) return <div className="h-full w-full"/>;
|
|
848
851
|
if (rowActions.length === 1) {
|
|
849
852
|
const ra = rowActions[0];
|
|
@@ -1037,7 +1040,7 @@ id: String(ra.entityId) })}
|
|
|
1037
1040
|
|
|
1038
1041
|
// ─── JSON Syntax Highlighting ────────────────────────────────────────
|
|
1039
1042
|
|
|
1040
|
-
function JSONHighlight({ value }: { value:
|
|
1043
|
+
function JSONHighlight({ value }: { value: unknown }) {
|
|
1041
1044
|
const json = formatJSON(value);
|
|
1042
1045
|
const { mode } = useModeController();
|
|
1043
1046
|
|
|
@@ -111,7 +111,7 @@ interface RebaseAuth {
|
|
|
111
111
|
changePassword(oldPassword: string, newPassword: string): Promise<{ success: boolean; message: string }>;
|
|
112
112
|
sendVerificationEmail(): Promise<{ success: boolean; message: string }>;
|
|
113
113
|
verifyEmail(token: string): Promise<{ success: boolean; message: string }>;
|
|
114
|
-
getSessions(): Promise<
|
|
114
|
+
getSessions(): Promise<RebaseSession[]>;
|
|
115
115
|
revokeSession(sessionId: string): Promise<{ success: boolean }>;
|
|
116
116
|
revokeAllSessions(): Promise<{ success: boolean }>;
|
|
117
117
|
getSession(): RebaseSession | null;
|
|
@@ -129,25 +129,12 @@ interface AdminUser {
|
|
|
129
129
|
updatedAt: string;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
interface RebaseRole {
|
|
133
|
-
id: string;
|
|
134
|
-
name: string;
|
|
135
|
-
isAdmin: boolean;
|
|
136
|
-
defaultPermissions: Record<string, any> | null;
|
|
137
|
-
config: Record<string, any> | null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
132
|
interface RebaseAdmin {
|
|
141
133
|
listUsers(): Promise<{ users: AdminUser[] }>;
|
|
142
134
|
getUser(userId: string): Promise<{ user: AdminUser }>;
|
|
143
135
|
createUser(data: { email: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
|
|
144
136
|
updateUser(userId: string, data: { email?: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
|
|
145
137
|
deleteUser(userId: string): Promise<{ success: boolean }>;
|
|
146
|
-
listRoles(): Promise<{ roles: RebaseRole[] }>;
|
|
147
|
-
getRole(roleId: string): Promise<{ role: RebaseRole }>;
|
|
148
|
-
createRole(data: { id: string; name: string; isAdmin?: boolean; defaultPermissions?: any; config?: any }): Promise<{ role: RebaseRole }>;
|
|
149
|
-
updateRole(roleId: string, data: { name?: string; isAdmin?: boolean; defaultPermissions?: any; config?: any }): Promise<{ role: RebaseRole }>;
|
|
150
|
-
deleteRole(roleId: string): Promise<{ success: boolean }>;
|
|
151
138
|
bootstrap(): Promise<{ success: boolean; message: string; user: { uid: string; roles: string[] } }>;
|
|
152
139
|
}
|
|
153
140
|
|
|
@@ -155,7 +142,7 @@ interface UploadFileProps {
|
|
|
155
142
|
file: FileIcon;
|
|
156
143
|
fileName?: string;
|
|
157
144
|
path?: string;
|
|
158
|
-
metadata?: Record<string,
|
|
145
|
+
metadata?: Record<string, unknown>;
|
|
159
146
|
bucket?: string;
|
|
160
147
|
}
|
|
161
148
|
|
|
@@ -168,7 +155,7 @@ interface UploadFileResult {
|
|
|
168
155
|
interface DownloadConfig {
|
|
169
156
|
url: string | null;
|
|
170
157
|
fileNotFound?: boolean;
|
|
171
|
-
metadata?:
|
|
158
|
+
metadata?: Record<string, unknown>;
|
|
172
159
|
}
|
|
173
160
|
|
|
174
161
|
interface StorageSource {
|
|
@@ -176,7 +163,7 @@ interface StorageSource {
|
|
|
176
163
|
getSignedUrl(pathOrUrl: string, bucket?: string): Promise<DownloadConfig>;
|
|
177
164
|
getObject(path: string, bucket?: string): Promise<FileIcon | null>;
|
|
178
165
|
deleteObject(path: string, bucket?: string): Promise<void>;
|
|
179
|
-
listObjects(path: string, options?: { bucket?: string; maxResults?: number; pageToken?: string }): Promise<
|
|
166
|
+
listObjects(path: string, options?: { bucket?: string; maxResults?: number; pageToken?: string }): Promise<unknown>;
|
|
180
167
|
}
|
|
181
168
|
|
|
182
169
|
type RebaseData = {
|
|
@@ -220,9 +207,9 @@ interface RebaseClient {
|
|
|
220
207
|
/** Storage operations */
|
|
221
208
|
storage?: StorageSource;
|
|
222
209
|
/** Call a custom server-side endpoint */
|
|
223
|
-
call<T =
|
|
210
|
+
call<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
|
|
224
211
|
/** Direct collection access (shorthand) */
|
|
225
|
-
[collectionSlug: string]:
|
|
212
|
+
[collectionSlug: string]: unknown;
|
|
226
213
|
}
|
|
227
214
|
|
|
228
215
|
/** The pre-configured client instance. Already authenticated with the current user session. */
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { Select, SelectItem, TextField, Checkbox, Label } from "@rebasepro/ui";
|
|
3
|
+
|
|
4
|
+
interface LogEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
level: "debug" | "info" | "warn" | "error";
|
|
8
|
+
source: "api" | "auth" | "storage" | "realtime" | "system";
|
|
9
|
+
message: string;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LEVEL_COLORS: Record<string, string> = {
|
|
14
|
+
debug: "#6c7086",
|
|
15
|
+
info: "#89b4fa",
|
|
16
|
+
warn: "#f9e2af",
|
|
17
|
+
error: "#f38ba8"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SOURCE_COLORS: Record<string, string> = {
|
|
21
|
+
api: "#74c7ec",
|
|
22
|
+
auth: "#cba6f7",
|
|
23
|
+
storage: "#a6e3a1",
|
|
24
|
+
realtime: "#fab387",
|
|
25
|
+
system: "#6c7086"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function LogsExplorer() {
|
|
29
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
30
|
+
const [level, setLevel] = useState<string>("");
|
|
31
|
+
const [source, setSource] = useState<string>("");
|
|
32
|
+
const [search, setSearch] = useState("");
|
|
33
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
34
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const fetchLogs = useCallback(async () => {
|
|
36
|
+
try {
|
|
37
|
+
const params = new URLSearchParams();
|
|
38
|
+
if (level) params.set("level", level);
|
|
39
|
+
if (source) params.set("source", source);
|
|
40
|
+
if (search) params.set("search", search);
|
|
41
|
+
params.set("limit", "200");
|
|
42
|
+
|
|
43
|
+
const resp = await fetch(`/api/logs?${params}`);
|
|
44
|
+
if (resp.ok) {
|
|
45
|
+
const data: { entries?: LogEntry[] } = await resp.json();
|
|
46
|
+
setLogs(data.entries || []);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
/* ignore poll failures */
|
|
50
|
+
}
|
|
51
|
+
}, [level, source, search]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
|
|
57
|
+
fetchLogs();
|
|
58
|
+
|
|
59
|
+
const scheduleNext = () => {
|
|
60
|
+
if (cancelled) return;
|
|
61
|
+
timeoutId = setTimeout(async () => {
|
|
62
|
+
if (document.visibilityState === "visible") {
|
|
63
|
+
await fetchLogs();
|
|
64
|
+
}
|
|
65
|
+
scheduleNext();
|
|
66
|
+
}, 3000);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
scheduleNext();
|
|
70
|
+
|
|
71
|
+
const handleVisibility = () => {
|
|
72
|
+
if (document.visibilityState === "visible") {
|
|
73
|
+
fetchLogs();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
cancelled = true;
|
|
80
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
81
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
82
|
+
};
|
|
83
|
+
}, [fetchLogs]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (autoScroll && containerRef.current) {
|
|
87
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
88
|
+
}
|
|
89
|
+
}, [logs, autoScroll]);
|
|
90
|
+
|
|
91
|
+
const selectStyle: React.CSSProperties = {
|
|
92
|
+
background: "#313244",
|
|
93
|
+
color: "#cdd6f4",
|
|
94
|
+
border: "1px solid #45475a",
|
|
95
|
+
borderRadius: 4,
|
|
96
|
+
padding: "4px 8px"
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div style={{
|
|
101
|
+
display: "flex",
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
height: "calc(100vh - 64px)",
|
|
104
|
+
background: "#1e1e2e",
|
|
105
|
+
color: "#cdd6f4"
|
|
106
|
+
}}>
|
|
107
|
+
{/* Toolbar */}
|
|
108
|
+
<div style={{
|
|
109
|
+
display: "flex",
|
|
110
|
+
gap: 8,
|
|
111
|
+
padding: "8px 16px",
|
|
112
|
+
borderBottom: "1px solid #313244",
|
|
113
|
+
alignItems: "center",
|
|
114
|
+
flexWrap: "wrap"
|
|
115
|
+
}}>
|
|
116
|
+
<Select
|
|
117
|
+
value={level}
|
|
118
|
+
onValueChange={setLevel}
|
|
119
|
+
size="small"
|
|
120
|
+
placeholder="All Levels"
|
|
121
|
+
>
|
|
122
|
+
<SelectItem value="">All Levels</SelectItem>
|
|
123
|
+
<SelectItem value="debug">Debug</SelectItem>
|
|
124
|
+
<SelectItem value="info">Info</SelectItem>
|
|
125
|
+
<SelectItem value="warn">Warn</SelectItem>
|
|
126
|
+
<SelectItem value="error">Error</SelectItem>
|
|
127
|
+
</Select>
|
|
128
|
+
<Select
|
|
129
|
+
value={source}
|
|
130
|
+
onValueChange={setSource}
|
|
131
|
+
size="small"
|
|
132
|
+
placeholder="All Sources"
|
|
133
|
+
>
|
|
134
|
+
<SelectItem value="">All Sources</SelectItem>
|
|
135
|
+
<SelectItem value="api">API</SelectItem>
|
|
136
|
+
<SelectItem value="auth">Auth</SelectItem>
|
|
137
|
+
<SelectItem value="storage">Storage</SelectItem>
|
|
138
|
+
<SelectItem value="realtime">Realtime</SelectItem>
|
|
139
|
+
<SelectItem value="system">System</SelectItem>
|
|
140
|
+
</Select>
|
|
141
|
+
<TextField
|
|
142
|
+
size="small"
|
|
143
|
+
placeholder="Search logs..."
|
|
144
|
+
value={search}
|
|
145
|
+
onChange={e => setSearch(e.target.value)}
|
|
146
|
+
className="flex-1 min-w-[200px]"
|
|
147
|
+
/>
|
|
148
|
+
<div className="flex items-center gap-1.5 cursor-pointer">
|
|
149
|
+
<Checkbox
|
|
150
|
+
id="auto-scroll"
|
|
151
|
+
checked={autoScroll}
|
|
152
|
+
onCheckedChange={setAutoScroll}
|
|
153
|
+
size="small"
|
|
154
|
+
padding={false}
|
|
155
|
+
/>
|
|
156
|
+
<Label
|
|
157
|
+
htmlFor="auto-scroll"
|
|
158
|
+
className="text-xs select-none cursor-pointer"
|
|
159
|
+
>
|
|
160
|
+
Auto-scroll
|
|
161
|
+
</Label>
|
|
162
|
+
</div>
|
|
163
|
+
<span style={{ fontSize: 12, color: "#6c7086" }}>
|
|
164
|
+
{logs.length} entries
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
{/* Log entries */}
|
|
168
|
+
<div
|
|
169
|
+
ref={containerRef}
|
|
170
|
+
style={{
|
|
171
|
+
flex: 1,
|
|
172
|
+
overflow: "auto",
|
|
173
|
+
fontFamily: "monospace",
|
|
174
|
+
fontSize: 12,
|
|
175
|
+
padding: "8px 0"
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{logs.map(log => (
|
|
179
|
+
<div
|
|
180
|
+
key={log.id}
|
|
181
|
+
style={{
|
|
182
|
+
padding: "2px 16px",
|
|
183
|
+
display: "flex",
|
|
184
|
+
gap: 8,
|
|
185
|
+
borderBottom: "1px solid #181825"
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
<span style={{ color: "#6c7086", flexShrink: 0 }}>
|
|
189
|
+
{new Date(log.timestamp).toLocaleTimeString()}
|
|
190
|
+
</span>
|
|
191
|
+
<span style={{
|
|
192
|
+
color: LEVEL_COLORS[log.level] || "#cdd6f4",
|
|
193
|
+
width: 40,
|
|
194
|
+
flexShrink: 0,
|
|
195
|
+
textTransform: "uppercase",
|
|
196
|
+
fontWeight: 600
|
|
197
|
+
}}>
|
|
198
|
+
{log.level}
|
|
199
|
+
</span>
|
|
200
|
+
<span style={{
|
|
201
|
+
color: SOURCE_COLORS[log.source] || "#cdd6f4",
|
|
202
|
+
width: 64,
|
|
203
|
+
flexShrink: 0
|
|
204
|
+
}}>
|
|
205
|
+
[{log.source}]
|
|
206
|
+
</span>
|
|
207
|
+
<span style={{ color: "#cdd6f4", flex: 1 }}>
|
|
208
|
+
{log.message}
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
212
|
+
{logs.length === 0 && (
|
|
213
|
+
<div style={{
|
|
214
|
+
padding: 32,
|
|
215
|
+
textAlign: "center",
|
|
216
|
+
color: "#6c7086"
|
|
217
|
+
}}>
|
|
218
|
+
No log entries yet. Logs will appear here as requests come in.
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -13,6 +13,7 @@ const CronJobsView = lazy(() => import("./CronJobs/CronJobsView").then(m => ({ d
|
|
|
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
15
|
const ApiExplorer = lazy(() => import("./ApiExplorer/ApiExplorer").then(m => ({ default: m.ApiExplorer })));
|
|
16
|
+
const LogsExplorer = lazy(() => import("./LogsExplorer/LogsExplorer").then(m => ({ default: m.LogsExplorer })));
|
|
16
17
|
|
|
17
18
|
import { StudioHomePage } from "./StudioHomePage";
|
|
18
19
|
|
|
@@ -33,7 +34,7 @@ export function RebaseStudio({ tools, homePage }: RebaseStudioConfig) {
|
|
|
33
34
|
|
|
34
35
|
const devViews: AppView[] = useMemo(() => {
|
|
35
36
|
const views: AppView[] = [];
|
|
36
|
-
const activeTools = tools ?? ["sql", "js", "rls", "storage", "cron", "schema-visualizer", "branches", "api"];
|
|
37
|
+
const activeTools = tools ?? ["sql", "js", "rls", "storage", "cron", "schema-visualizer", "branches", "api", "logs"];
|
|
37
38
|
const suspense = (el: React.ReactNode) => <Suspense fallback={<CircularProgressCenter/>}>{el}</Suspense>;
|
|
38
39
|
|
|
39
40
|
if (activeTools.includes("sql")) {
|
|
@@ -99,6 +100,14 @@ group: "API",
|
|
|
99
100
|
icon: "BookOpen",
|
|
100
101
|
description: "Interactive API documentation and testing",
|
|
101
102
|
view: suspense(<ApiExplorer/>) });
|
|
103
|
+
}
|
|
104
|
+
if (activeTools.includes("logs")) {
|
|
105
|
+
views.push({ slug: "logs",
|
|
106
|
+
name: "Logs Explorer",
|
|
107
|
+
group: "Database",
|
|
108
|
+
icon: "Activity",
|
|
109
|
+
description: "Real-time system and query logs",
|
|
110
|
+
view: suspense(<LogsExplorer/>) });
|
|
102
111
|
}
|
|
103
112
|
// Note: "schema" tool is auto-injected by RebaseShell when collectionEditor is enabled.
|
|
104
113
|
// It is NOT registered here anymore.
|
|
@@ -66,10 +66,31 @@ const QueryLoadingView = () => {
|
|
|
66
66
|
|
|
67
67
|
useEffect(() => {
|
|
68
68
|
const start = Date.now();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
|
|
72
|
+
const tick = () => {
|
|
73
|
+
if (cancelled) return;
|
|
74
|
+
if (document.visibilityState === "visible") {
|
|
75
|
+
setElapsed(Date.now() - start);
|
|
76
|
+
}
|
|
77
|
+
timeoutId = setTimeout(tick, 100);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
tick();
|
|
81
|
+
|
|
82
|
+
const handleVisibility = () => {
|
|
83
|
+
if (document.visibilityState === "visible") {
|
|
84
|
+
setElapsed(Date.now() - start);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
92
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
93
|
+
};
|
|
73
94
|
}, []);
|
|
74
95
|
|
|
75
96
|
return (
|
|
@@ -1054,7 +1075,7 @@ resizable: false }, ...dataColumns]
|
|
|
1054
1075
|
cellRenderer={({ rowData, column, rowIndex }) => {
|
|
1055
1076
|
// Dedicated collection action column
|
|
1056
1077
|
if (column.key === "__cms_action__") {
|
|
1057
|
-
const rowActions = getRowEntityActions(rowData);
|
|
1078
|
+
const rowActions = getRowEntityActions(rowData ?? {});
|
|
1058
1079
|
if (rowActions.length === 0) {
|
|
1059
1080
|
return <div className="h-full w-full"/>;
|
|
1060
1081
|
}
|
|
@@ -1128,7 +1149,7 @@ id: String(ra.entityId) })}
|
|
|
1128
1149
|
return (
|
|
1129
1150
|
<FixedEditorOverlay
|
|
1130
1151
|
displayValue={displayValue}
|
|
1131
|
-
onSave={(val) => handleCellSave(val, rowData, column.key, rowIndex)}
|
|
1152
|
+
onSave={(val) => handleCellSave(val, rowData ?? {}, column.key, rowIndex)}
|
|
1132
1153
|
onCancel={() => setEditingCell(null)}
|
|
1133
1154
|
/>
|
|
1134
1155
|
);
|
|
@@ -1137,7 +1158,7 @@ id: String(ra.entityId) })}
|
|
|
1137
1158
|
return (
|
|
1138
1159
|
<div
|
|
1139
1160
|
className="px-4 py-1.5 h-full flex items-center whitespace-nowrap text-[13px] text-text-primary dark:text-text-primary-dark font-mono cursor-text group/cell"
|
|
1140
|
-
onDoubleClick={() => handleDoubleClick(rowIndex, column.key, displayValue, rowData)}
|
|
1161
|
+
onDoubleClick={() => handleDoubleClick(rowIndex, column.key, displayValue, rowData ?? {})}
|
|
1141
1162
|
>
|
|
1142
1163
|
<div className="truncate flex-grow" title={displayValue}>
|
|
1143
1164
|
{displayValue === "" ? <span className="text-text-disabled dark:text-text-disabled-dark italic text-[11px]">NULL</span> : displayValue}
|
|
@@ -34,7 +34,8 @@ const SECTIONS: StudioSection[] = [
|
|
|
34
34
|
{ path: "/schema-visualizer", name: "Schema Visualizer", description: "Interactive ERD showing tables, columns, and relationships", icon: "Network" },
|
|
35
35
|
{ path: "/sql", name: "SQL Console", description: "Execute raw SQL queries directly against your database", icon: "terminal" },
|
|
36
36
|
{ path: "/branches", name: "Branches", description: "Create and manage isolated database copies for development", icon: "GitBranch" },
|
|
37
|
-
{ path: "/rls", name: "RLS Policies", description: "Configure Row Level Security for fine-grained data access", icon: "ShieldCheck" }
|
|
37
|
+
{ path: "/rls", name: "RLS Policies", description: "Configure Row Level Security for fine-grained data access", icon: "ShieldCheck" },
|
|
38
|
+
{ path: "/logs", name: "Logs Explorer", description: "Real-time system, query, and authentication logs", icon: "Activity" }
|
|
38
39
|
]
|
|
39
40
|
},
|
|
40
41
|
{
|