@rebasepro/studio 0.9.1-canary.fd3754b → 0.10.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/{ApiKeysView-Dbe5cXkS.js → ApiKeysView-vQKqt2wl.js} +44 -2
- package/dist/ApiKeysView-vQKqt2wl.js.map +1 -0
- package/dist/{JSEditor-Etgw9-Cw.js → JSEditor-BeHHuxhA.js} +3 -3
- package/dist/{JSEditor-Etgw9-Cw.js.map → JSEditor-BeHHuxhA.js.map} +1 -1
- package/dist/LogsExplorer-BCXoGREr.js +298 -0
- package/dist/LogsExplorer-BCXoGREr.js.map +1 -0
- package/dist/{RLSEditor-Cgv_kUVW.js → RLSEditor-rLlMcZPG.js} +42 -28
- package/dist/RLSEditor-rLlMcZPG.js.map +1 -0
- package/dist/{SQLEditor-ZZsy1Mk6.js → SQLEditor-_dCqVAN_.js} +3 -3
- package/dist/SQLEditor-_dCqVAN_.js.map +1 -0
- package/dist/{SchemaVisualizer-D01DzU-f.js → SchemaVisualizer-C_ErehfH.js} +2 -2
- package/dist/{SchemaVisualizer-D01DzU-f.js.map → SchemaVisualizer-C_ErehfH.js.map} +1 -1
- package/dist/components/RLSEditor/PolicyEditor.d.ts +1 -1
- package/dist/components/SQLEditor/SQLEditor.d.ts +2 -10
- package/dist/components/SQLEditor/SQLEditorSidebar.d.ts +1 -1
- package/dist/components/SQLEditor/SchemaBrowser.d.ts +1 -1
- package/dist/components/SQLEditor/sql_editor_types.d.ts +16 -0
- package/dist/index.es.js +58 -105
- package/dist/index.es.js.map +1 -1
- package/dist/utils/sql_utils.d.ts +1 -1
- package/package.json +13 -12
- package/src/components/ApiKeys/ApiKeysView.tsx +41 -1
- package/src/components/JSEditor/JSEditor.tsx +3 -3
- package/src/components/LogsExplorer/LogsExplorer.tsx +168 -46
- package/src/components/RLSEditor/PolicyEditor.tsx +15 -13
- package/src/components/RLSEditor/RLSEditor.tsx +48 -18
- package/src/components/SQLEditor/SQLEditor.tsx +3 -11
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +1 -1
- package/src/components/SQLEditor/SchemaBrowser.tsx +3 -3
- package/src/components/SQLEditor/sql_editor_types.ts +18 -0
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +2 -1
- package/src/components/StudioHomePage.tsx +61 -86
- package/src/utils/sql_utils.test.ts +1 -1
- package/src/utils/sql_utils.ts +1 -1
- package/dist/ApiKeysView-Dbe5cXkS.js.map +0 -1
- package/dist/LogsExplorer-J4xfsuv3.js +0 -206
- package/dist/LogsExplorer-J4xfsuv3.js.map +0 -1
- package/dist/RLSEditor-Cgv_kUVW.js.map +0 -1
- package/dist/SQLEditor-ZZsy1Mk6.js.map +0 -1
- package/dist/index.umd.js +0 -11709
- package/dist/index.umd.js.map +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { useApiConfig } from "@rebasepro/app";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { ArrowDownToLineIcon, Checkbox, Label, Select, SelectItem, TextField, Typography, cls, defaultBorderMixin } from "@rebasepro/ui";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/LogsExplorer/LogsExplorer.tsx
|
|
6
|
+
var LEVEL_COLORS = {
|
|
7
|
+
debug: "text-surface-500",
|
|
8
|
+
info: "text-blue-600 dark:text-blue-500",
|
|
9
|
+
warn: "text-amber-600 dark:text-amber-500",
|
|
10
|
+
error: "text-red-600 dark:text-red-500"
|
|
11
|
+
};
|
|
12
|
+
var SOURCE_COLORS = {
|
|
13
|
+
api: "text-sky-600 dark:text-sky-400",
|
|
14
|
+
auth: "text-purple-600 dark:text-purple-400",
|
|
15
|
+
storage: "text-green-600 dark:text-green-500",
|
|
16
|
+
realtime: "text-orange-600 dark:text-orange-400",
|
|
17
|
+
system: "text-surface-600 dark:text-surface-400"
|
|
18
|
+
};
|
|
19
|
+
var idNum = (entry) => Number(entry.id.slice(4));
|
|
20
|
+
var sameLogs = (a, b) => a.length === b.length && a[0]?.id === b[0]?.id && a[a.length - 1]?.id === b[b.length - 1]?.id;
|
|
21
|
+
var STICK_THRESHOLD = 40;
|
|
22
|
+
function LogsExplorer() {
|
|
23
|
+
const [logs, setLogs] = useState([]);
|
|
24
|
+
const [level, setLevel] = useState("all");
|
|
25
|
+
const [source, setSource] = useState("all");
|
|
26
|
+
const [searchInput, setSearchInput] = useState("");
|
|
27
|
+
const [search, setSearch] = useState("");
|
|
28
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
29
|
+
const [error, setError] = useState(null);
|
|
30
|
+
const [newCount, setNewCount] = useState(0);
|
|
31
|
+
const [atBottom, setAtBottom] = useState(true);
|
|
32
|
+
const containerRef = useRef(null);
|
|
33
|
+
const stickRef = useRef(true);
|
|
34
|
+
const lastMaxIdRef = useRef(null);
|
|
35
|
+
const apiConfig = useApiConfig();
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const t = setTimeout(() => setSearch(searchInput), 300);
|
|
38
|
+
return () => clearTimeout(t);
|
|
39
|
+
}, [searchInput]);
|
|
40
|
+
const fetchLogs = useCallback(async () => {
|
|
41
|
+
if (!apiConfig?.apiUrl) {
|
|
42
|
+
setError("No API URL configured — cannot load logs.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const params = new URLSearchParams();
|
|
47
|
+
if (level && level !== "all") params.set("level", level);
|
|
48
|
+
if (source && source !== "all") params.set("source", source);
|
|
49
|
+
if (search) params.set("search", search);
|
|
50
|
+
params.set("limit", "200");
|
|
51
|
+
const headers = {};
|
|
52
|
+
const token = apiConfig.getAuthToken ? await apiConfig.getAuthToken() : null;
|
|
53
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
54
|
+
const resp = await fetch(`${apiConfig.apiUrl}/api/logs?${params}`, { headers });
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
setError(resp.status === 401 || resp.status === 403 ? "Not authorised to read logs — an admin role is required." : `Could not load logs (HTTP ${resp.status}).`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const entries = ((await resp.json()).entries || []).slice().reverse();
|
|
60
|
+
const prevMax = lastMaxIdRef.current;
|
|
61
|
+
if (prevMax != null && !stickRef.current) {
|
|
62
|
+
const fresh = entries.filter((e) => idNum(e) > prevMax).length;
|
|
63
|
+
if (fresh > 0) setNewCount((c) => c + fresh);
|
|
64
|
+
}
|
|
65
|
+
if (entries.length > 0) lastMaxIdRef.current = idNum(entries[entries.length - 1]);
|
|
66
|
+
setLogs((prev) => sameLogs(prev, entries) ? prev : entries);
|
|
67
|
+
setError(null);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
setError(e instanceof Error ? e.message : "Could not load logs.");
|
|
70
|
+
}
|
|
71
|
+
}, [
|
|
72
|
+
level,
|
|
73
|
+
source,
|
|
74
|
+
search,
|
|
75
|
+
apiConfig
|
|
76
|
+
]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
lastMaxIdRef.current = null;
|
|
79
|
+
setNewCount(0);
|
|
80
|
+
}, [
|
|
81
|
+
level,
|
|
82
|
+
source,
|
|
83
|
+
search
|
|
84
|
+
]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
let timeoutId = null;
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
fetchLogs();
|
|
89
|
+
const scheduleNext = () => {
|
|
90
|
+
if (cancelled) return;
|
|
91
|
+
timeoutId = setTimeout(async () => {
|
|
92
|
+
if (document.visibilityState === "visible") await fetchLogs();
|
|
93
|
+
scheduleNext();
|
|
94
|
+
}, 3e3);
|
|
95
|
+
};
|
|
96
|
+
scheduleNext();
|
|
97
|
+
const handleVisibility = () => {
|
|
98
|
+
if (document.visibilityState === "visible") fetchLogs();
|
|
99
|
+
};
|
|
100
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
101
|
+
return () => {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
104
|
+
document.removeEventListener("visibilitychange", handleVisibility);
|
|
105
|
+
};
|
|
106
|
+
}, [fetchLogs]);
|
|
107
|
+
const scrollToBottom = useCallback(() => {
|
|
108
|
+
const el = containerRef.current;
|
|
109
|
+
if (!el) return;
|
|
110
|
+
el.scrollTop = el.scrollHeight;
|
|
111
|
+
stickRef.current = true;
|
|
112
|
+
setAtBottom(true);
|
|
113
|
+
setNewCount(0);
|
|
114
|
+
}, []);
|
|
115
|
+
const handleScroll = useCallback(() => {
|
|
116
|
+
const el = containerRef.current;
|
|
117
|
+
if (!el) return;
|
|
118
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;
|
|
119
|
+
stickRef.current = nearBottom;
|
|
120
|
+
setAtBottom(nearBottom);
|
|
121
|
+
if (nearBottom) setNewCount(0);
|
|
122
|
+
}, []);
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
if (autoScroll && stickRef.current) {
|
|
125
|
+
const el = containerRef.current;
|
|
126
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
127
|
+
}
|
|
128
|
+
}, [logs, autoScroll]);
|
|
129
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: "flex flex-col h-[calc(100vh-64px)] w-full bg-surface-50 dark:bg-surface-800",
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ jsxs("div", {
|
|
133
|
+
className: cls("flex gap-2 p-3 border-b items-center flex-wrap shrink-0", defaultBorderMixin),
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ jsxs(Select, {
|
|
136
|
+
value: level,
|
|
137
|
+
onValueChange: setLevel,
|
|
138
|
+
size: "small",
|
|
139
|
+
placeholder: "All Levels",
|
|
140
|
+
children: [
|
|
141
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
142
|
+
value: "all",
|
|
143
|
+
children: "All Levels"
|
|
144
|
+
}),
|
|
145
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
146
|
+
value: "debug",
|
|
147
|
+
children: "Debug"
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
150
|
+
value: "info",
|
|
151
|
+
children: "Info"
|
|
152
|
+
}),
|
|
153
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
154
|
+
value: "warn",
|
|
155
|
+
children: "Warn"
|
|
156
|
+
}),
|
|
157
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
158
|
+
value: "error",
|
|
159
|
+
children: "Error"
|
|
160
|
+
})
|
|
161
|
+
]
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ jsxs(Select, {
|
|
164
|
+
value: source,
|
|
165
|
+
onValueChange: setSource,
|
|
166
|
+
size: "small",
|
|
167
|
+
placeholder: "All Sources",
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
170
|
+
value: "all",
|
|
171
|
+
children: "All Sources"
|
|
172
|
+
}),
|
|
173
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
174
|
+
value: "api",
|
|
175
|
+
children: "API"
|
|
176
|
+
}),
|
|
177
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
178
|
+
value: "auth",
|
|
179
|
+
children: "Auth"
|
|
180
|
+
}),
|
|
181
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
182
|
+
value: "storage",
|
|
183
|
+
children: "Storage"
|
|
184
|
+
}),
|
|
185
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
186
|
+
value: "realtime",
|
|
187
|
+
children: "Realtime"
|
|
188
|
+
}),
|
|
189
|
+
/* @__PURE__ */ jsx(SelectItem, {
|
|
190
|
+
value: "system",
|
|
191
|
+
children: "System"
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
}),
|
|
195
|
+
/* @__PURE__ */ jsx(TextField, {
|
|
196
|
+
size: "small",
|
|
197
|
+
placeholder: "Search logs...",
|
|
198
|
+
value: searchInput,
|
|
199
|
+
onChange: (e) => setSearchInput(e.target.value),
|
|
200
|
+
className: "flex-1 min-w-[200px]"
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ jsxs("div", {
|
|
203
|
+
className: "flex items-center gap-1.5 cursor-pointer ml-2",
|
|
204
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
205
|
+
id: "auto-scroll",
|
|
206
|
+
checked: autoScroll,
|
|
207
|
+
onCheckedChange: (checked) => {
|
|
208
|
+
setAutoScroll(checked);
|
|
209
|
+
if (checked) scrollToBottom();
|
|
210
|
+
},
|
|
211
|
+
size: "small",
|
|
212
|
+
padding: false
|
|
213
|
+
}), /* @__PURE__ */ jsx(Label, {
|
|
214
|
+
htmlFor: "auto-scroll",
|
|
215
|
+
className: "text-xs select-none cursor-pointer text-surface-600 dark:text-surface-400",
|
|
216
|
+
children: "Auto-scroll"
|
|
217
|
+
})]
|
|
218
|
+
}),
|
|
219
|
+
/* @__PURE__ */ jsx("div", {
|
|
220
|
+
className: "ml-auto pl-4",
|
|
221
|
+
children: /* @__PURE__ */ jsxs(Typography, {
|
|
222
|
+
variant: "caption",
|
|
223
|
+
color: "secondary",
|
|
224
|
+
children: [logs.length, " entries"]
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
}),
|
|
229
|
+
error && logs.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
230
|
+
className: cls("px-4 py-1.5 border-b bg-amber-50 dark:bg-amber-950/20 shrink-0", defaultBorderMixin),
|
|
231
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
232
|
+
variant: "caption",
|
|
233
|
+
className: "text-amber-700 dark:text-amber-400",
|
|
234
|
+
children: error
|
|
235
|
+
})
|
|
236
|
+
}),
|
|
237
|
+
/* @__PURE__ */ jsxs("div", {
|
|
238
|
+
className: "relative flex-1 min-h-0",
|
|
239
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
240
|
+
ref: containerRef,
|
|
241
|
+
onScroll: handleScroll,
|
|
242
|
+
className: "h-full overflow-auto py-2",
|
|
243
|
+
children: [logs.map((log) => /* @__PURE__ */ jsxs("div", {
|
|
244
|
+
className: cls("flex gap-4 px-4 py-[6px] border-b hover:bg-surface-100 dark:hover:bg-surface-900 transition-colors", defaultBorderMixin),
|
|
245
|
+
children: [
|
|
246
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
247
|
+
variant: "body2",
|
|
248
|
+
color: "secondary",
|
|
249
|
+
className: "w-[72px] shrink-0 font-mono",
|
|
250
|
+
children: new Date(log.timestamp).toLocaleTimeString()
|
|
251
|
+
}),
|
|
252
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
253
|
+
variant: "body2",
|
|
254
|
+
className: cls("w-[48px] shrink-0 uppercase font-semibold font-mono", LEVEL_COLORS[log.level] || "text-surface-500"),
|
|
255
|
+
children: log.level
|
|
256
|
+
}),
|
|
257
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
258
|
+
variant: "body2",
|
|
259
|
+
className: cls("w-[80px] shrink-0 font-mono", SOURCE_COLORS[log.source] || "text-surface-500"),
|
|
260
|
+
children: [
|
|
261
|
+
"[",
|
|
262
|
+
log.source,
|
|
263
|
+
"]"
|
|
264
|
+
]
|
|
265
|
+
}),
|
|
266
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
267
|
+
variant: "body2",
|
|
268
|
+
className: "flex-1 font-mono break-all whitespace-pre-wrap text-surface-900 dark:text-surface-100",
|
|
269
|
+
children: log.message
|
|
270
|
+
})
|
|
271
|
+
]
|
|
272
|
+
}, log.id)), logs.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
273
|
+
className: "p-8 text-center",
|
|
274
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
275
|
+
variant: "body2",
|
|
276
|
+
className: error ? "text-red-600 dark:text-red-500" : void 0,
|
|
277
|
+
color: error ? void 0 : "secondary",
|
|
278
|
+
children: error ?? "No log entries yet. Logs will appear here as requests come in."
|
|
279
|
+
})
|
|
280
|
+
})]
|
|
281
|
+
}), autoScroll && !atBottom && newCount > 0 && /* @__PURE__ */ jsxs("button", {
|
|
282
|
+
onClick: scrollToBottom,
|
|
283
|
+
className: "absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 rounded-full bg-primary text-white text-xs font-medium pl-2.5 pr-3 py-1.5 shadow-md hover:bg-primary-dark transition-colors",
|
|
284
|
+
children: [
|
|
285
|
+
/* @__PURE__ */ jsx(ArrowDownToLineIcon, { size: 14 }),
|
|
286
|
+
newCount,
|
|
287
|
+
" new ",
|
|
288
|
+
newCount === 1 ? "entry" : "entries"
|
|
289
|
+
]
|
|
290
|
+
})]
|
|
291
|
+
})
|
|
292
|
+
]
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
export { LogsExplorer };
|
|
297
|
+
|
|
298
|
+
//# sourceMappingURL=LogsExplorer-BCXoGREr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LogsExplorer-BCXoGREr.js","names":[],"sources":["../src/components/LogsExplorer/LogsExplorer.tsx"],"sourcesContent":["import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { ArrowDownToLineIcon, Checkbox, cls, defaultBorderMixin, Label, Select, SelectItem, TextField, Typography } from \"@rebasepro/ui\";\nimport { useApiConfig } from \"@rebasepro/app\";\n\ninterface LogEntry {\n id: string;\n timestamp: string;\n level: \"debug\" | \"info\" | \"warn\" | \"error\";\n source: \"api\" | \"auth\" | \"storage\" | \"realtime\" | \"system\";\n message: string;\n metadata?: Record<string, unknown>;\n}\n\nconst LEVEL_COLORS: Record<string, string> = {\n debug: \"text-surface-500\",\n info: \"text-blue-600 dark:text-blue-500\",\n warn: \"text-amber-600 dark:text-amber-500\",\n error: \"text-red-600 dark:text-red-500\"\n};\n\nconst SOURCE_COLORS: Record<string, string> = {\n api: \"text-sky-600 dark:text-sky-400\",\n auth: \"text-purple-600 dark:text-purple-400\",\n storage: \"text-green-600 dark:text-green-500\",\n realtime: \"text-orange-600 dark:text-orange-400\",\n system: \"text-surface-600 dark:text-surface-400\"\n};\n\n// Ids are `log_<n>` with a monotonic counter, so numeric comparison tells\n// old entries from new ones across polls.\nconst idNum = (entry: LogEntry): number => Number(entry.id.slice(\"log_\".length));\n\n// The window is contiguous and ordered, so identical ends mean identical content.\nconst sameLogs = (a: LogEntry[], b: LogEntry[]): boolean =>\n a.length === b.length &&\n a[0]?.id === b[0]?.id &&\n a[a.length - 1]?.id === b[b.length - 1]?.id;\n\n// How close to the bottom edge still counts as \"at the bottom\".\nconst STICK_THRESHOLD = 40;\n\nexport function LogsExplorer() {\n const [logs, setLogs] = useState<LogEntry[]>([]);\n const [level, setLevel] = useState<string>(\"all\");\n const [source, setSource] = useState<string>(\"all\");\n const [searchInput, setSearchInput] = useState(\"\");\n const [search, setSearch] = useState(\"\");\n const [autoScroll, setAutoScroll] = useState(true);\n const [error, setError] = useState<string | null>(null);\n // Entries that arrived while the user was scrolled away from the bottom.\n const [newCount, setNewCount] = useState(0);\n const [atBottom, setAtBottom] = useState(true);\n const containerRef = useRef<HTMLDivElement>(null);\n // Whether the view is stuck to the bottom right now. A ref, not state:\n // the scroll handler and the fetch loop both read it synchronously.\n const stickRef = useRef(true);\n const lastMaxIdRef = useRef<number | null>(null);\n const apiConfig = useApiConfig();\n\n // Debounce the search box so typing doesn't refetch per keystroke.\n useEffect(() => {\n const t = setTimeout(() => setSearch(searchInput), 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const fetchLogs = useCallback(async () => {\n if (!apiConfig?.apiUrl) {\n setError(\"No API URL configured — cannot load logs.\");\n return;\n }\n try {\n const params = new URLSearchParams();\n if (level && level !== \"all\") params.set(\"level\", level);\n if (source && source !== \"all\") params.set(\"source\", source);\n if (search) params.set(\"search\", search);\n params.set(\"limit\", \"200\");\n\n // Logs are admin-only, so the request must carry the auth token. The\n // URL is absolute: a relative one would resolve against the frontend\n // origin, which serves index.html rather than the API.\n const headers: Record<string, string> = {};\n const token = apiConfig.getAuthToken ? await apiConfig.getAuthToken() : null;\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\n\n const resp = await fetch(`${apiConfig.apiUrl}/api/logs?${params}`, { headers });\n if (!resp.ok) {\n setError(resp.status === 401 || resp.status === 403\n ? \"Not authorised to read logs — an admin role is required.\"\n : `Could not load logs (HTTP ${resp.status}).`);\n return;\n }\n const data: { entries?: LogEntry[] } = await resp.json();\n // The API returns newest-first; the view tails like a terminal, so\n // flip to chronological order (newest at the bottom).\n const entries = (data.entries || []).slice().reverse();\n\n const prevMax = lastMaxIdRef.current;\n if (prevMax != null && !stickRef.current) {\n const fresh = entries.filter(e => idNum(e) > prevMax).length;\n if (fresh > 0) setNewCount(c => c + fresh);\n }\n if (entries.length > 0) lastMaxIdRef.current = idNum(entries[entries.length - 1]);\n\n // Unchanged window → keep the previous array so React leaves the\n // DOM alone (hover states and text selection survive the poll).\n setLogs(prev => sameLogs(prev, entries) ? prev : entries);\n setError(null);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Could not load logs.\");\n }\n }, [level, source, search, apiConfig]);\n\n // A filter change replaces the window wholesale — \"new entries since last\n // poll\" stops meaning anything, so the counter starts over.\n useEffect(() => {\n lastMaxIdRef.current = null;\n setNewCount(0);\n }, [level, source, search]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let cancelled = false;\n\n fetchLogs();\n\n const scheduleNext = () => {\n if (cancelled) return;\n timeoutId = setTimeout(async () => {\n if (document.visibilityState === \"visible\") {\n await fetchLogs();\n }\n scheduleNext();\n }, 3000);\n };\n\n scheduleNext();\n\n const handleVisibility = () => {\n if (document.visibilityState === \"visible\") {\n fetchLogs();\n }\n };\n document.addEventListener(\"visibilitychange\", handleVisibility);\n\n return () => {\n cancelled = true;\n if (timeoutId) clearTimeout(timeoutId);\n document.removeEventListener(\"visibilitychange\", handleVisibility);\n };\n }, [fetchLogs]);\n\n const scrollToBottom = useCallback(() => {\n const el = containerRef.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n stickRef.current = true;\n setAtBottom(true);\n setNewCount(0);\n }, []);\n\n // Stick to the bottom only while the user is already there. Scrolling up\n // disengages; scrolling back down re-engages. Our own scrollTop writes\n // always land at the bottom, so they can never disengage it.\n const handleScroll = useCallback(() => {\n const el = containerRef.current;\n if (!el) return;\n const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;\n stickRef.current = nearBottom;\n setAtBottom(nearBottom);\n if (nearBottom) setNewCount(0);\n }, []);\n\n useLayoutEffect(() => {\n if (autoScroll && stickRef.current) {\n const el = containerRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }\n }, [logs, autoScroll]);\n\n return (\n <div className=\"flex flex-col h-[calc(100vh-64px)] w-full bg-surface-50 dark:bg-surface-800\">\n {/* Toolbar */}\n <div className={cls(\n \"flex gap-2 p-3 border-b items-center flex-wrap shrink-0\",\n defaultBorderMixin\n )}>\n <Select\n value={level}\n onValueChange={setLevel}\n size=\"small\"\n placeholder=\"All Levels\"\n >\n <SelectItem value=\"all\">All Levels</SelectItem>\n <SelectItem value=\"debug\">Debug</SelectItem>\n <SelectItem value=\"info\">Info</SelectItem>\n <SelectItem value=\"warn\">Warn</SelectItem>\n <SelectItem value=\"error\">Error</SelectItem>\n </Select>\n <Select\n value={source}\n onValueChange={setSource}\n size=\"small\"\n placeholder=\"All Sources\"\n >\n <SelectItem value=\"all\">All Sources</SelectItem>\n <SelectItem value=\"api\">API</SelectItem>\n <SelectItem value=\"auth\">Auth</SelectItem>\n <SelectItem value=\"storage\">Storage</SelectItem>\n <SelectItem value=\"realtime\">Realtime</SelectItem>\n <SelectItem value=\"system\">System</SelectItem>\n </Select>\n <TextField\n size=\"small\"\n placeholder=\"Search logs...\"\n value={searchInput}\n onChange={e => setSearchInput(e.target.value)}\n className=\"flex-1 min-w-[200px]\"\n />\n <div className=\"flex items-center gap-1.5 cursor-pointer ml-2\">\n <Checkbox\n id=\"auto-scroll\"\n checked={autoScroll}\n onCheckedChange={(checked: boolean) => {\n setAutoScroll(checked);\n if (checked) scrollToBottom();\n }}\n size=\"small\"\n padding={false}\n />\n <Label\n htmlFor=\"auto-scroll\"\n className=\"text-xs select-none cursor-pointer text-surface-600 dark:text-surface-400\"\n >\n Auto-scroll\n </Label>\n </div>\n <div className=\"ml-auto pl-4\">\n <Typography variant=\"caption\" color=\"secondary\">\n {logs.length} entries\n </Typography>\n </div>\n </div>\n\n {/* A failing poll must stay visible even while stale logs are on\n screen — otherwise the view quietly freezes. */}\n {error && logs.length > 0 && (\n <div className={cls(\n \"px-4 py-1.5 border-b bg-amber-50 dark:bg-amber-950/20 shrink-0\",\n defaultBorderMixin\n )}>\n <Typography variant=\"caption\" className=\"text-amber-700 dark:text-amber-400\">\n {error}\n </Typography>\n </div>\n )}\n\n {/* Log entries */}\n <div className=\"relative flex-1 min-h-0\">\n <div\n ref={containerRef}\n onScroll={handleScroll}\n className=\"h-full overflow-auto py-2\"\n >\n {logs.map(log => (\n <div\n key={log.id}\n className={cls(\n \"flex gap-4 px-4 py-[6px] border-b hover:bg-surface-100 dark:hover:bg-surface-900 transition-colors\",\n defaultBorderMixin\n )}\n >\n <Typography variant=\"body2\" color=\"secondary\" className=\"w-[72px] shrink-0 font-mono\">\n {new Date(log.timestamp).toLocaleTimeString()}\n </Typography>\n <Typography variant=\"body2\" className={cls(\"w-[48px] shrink-0 uppercase font-semibold font-mono\", LEVEL_COLORS[log.level] || \"text-surface-500\")}>\n {log.level}\n </Typography>\n <Typography variant=\"body2\" className={cls(\"w-[80px] shrink-0 font-mono\", SOURCE_COLORS[log.source] || \"text-surface-500\")}>\n [{log.source}]\n </Typography>\n <Typography variant=\"body2\" className=\"flex-1 font-mono break-all whitespace-pre-wrap text-surface-900 dark:text-surface-100\">\n {log.message}\n </Typography>\n </div>\n ))}\n {logs.length === 0 && (\n <div className=\"p-8 text-center\">\n <Typography\n variant=\"body2\"\n className={error ? \"text-red-600 dark:text-red-500\" : undefined}\n color={error ? undefined : \"secondary\"}\n >\n {error ?? \"No log entries yet. Logs will appear here as requests come in.\"}\n </Typography>\n </div>\n )}\n </div>\n\n {autoScroll && !atBottom && newCount > 0 && (\n <button\n onClick={scrollToBottom}\n className=\"absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 rounded-full bg-primary text-white text-xs font-medium pl-2.5 pr-3 py-1.5 shadow-md hover:bg-primary-dark transition-colors\"\n >\n <ArrowDownToLineIcon size={14}/>\n {newCount} new {newCount === 1 ? \"entry\" : \"entries\"}\n </button>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
4
4
|
import { Alert, AlertTriangleIcon, Button, Chip, CircularProgress, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, HelpCircleIcon, IconButton, KeyIcon, Link2Icon, LockIcon, MultiSelect, MultiSelectItem, Paper, RefreshCwIcon, ResizablePanels, Select, SelectItem, ShieldIcon, Tab, Tabs, TextField, Tooltip, Trash2Icon, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
7
|
-
import {
|
|
7
|
+
import { getPolicyNamesForRule, getPolicyNamesForRules, getPolicyOperations } from "@rebasepro/utils";
|
|
8
|
+
import { JUNCTION_TABLES_SQL, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, getJunctionSecurityRules, resolveJunctionSpecs } from "@rebasepro/common";
|
|
8
9
|
//#region src/components/RLSEditor/PolicyEditor.tsx
|
|
9
10
|
var COMMAND_OPTIONS = [
|
|
10
11
|
"ALL",
|
|
@@ -56,34 +57,34 @@ var POLICY_PRESETS = [
|
|
|
56
57
|
{
|
|
57
58
|
id: "user_select_own",
|
|
58
59
|
label: "Users can read their own rows",
|
|
59
|
-
description: "Users can only read rows where the
|
|
60
|
+
description: "Users can only read rows where the uid matches their auth.uid()",
|
|
60
61
|
policyname: "Users can select their own data",
|
|
61
62
|
cmd: "SELECT",
|
|
62
63
|
permissive: "PERMISSIVE",
|
|
63
64
|
roles: ["authenticated"],
|
|
64
|
-
qual: "auth.uid() =
|
|
65
|
+
qual: "auth.uid() = uid",
|
|
65
66
|
with_check: ""
|
|
66
67
|
},
|
|
67
68
|
{
|
|
68
69
|
id: "user_update_own",
|
|
69
70
|
label: "Users can update their own rows",
|
|
70
|
-
description: "Users can only update rows where the
|
|
71
|
+
description: "Users can only update rows where the uid matches their auth.uid()",
|
|
71
72
|
policyname: "Users can update their own data",
|
|
72
73
|
cmd: "UPDATE",
|
|
73
74
|
permissive: "PERMISSIVE",
|
|
74
75
|
roles: ["authenticated"],
|
|
75
|
-
qual: "auth.uid() =
|
|
76
|
-
with_check: "auth.uid() =
|
|
76
|
+
qual: "auth.uid() = uid",
|
|
77
|
+
with_check: "auth.uid() = uid"
|
|
77
78
|
},
|
|
78
79
|
{
|
|
79
80
|
id: "user_delete_own",
|
|
80
81
|
label: "Users can delete their own rows",
|
|
81
|
-
description: "Users can only delete rows where the
|
|
82
|
+
description: "Users can only delete rows where the uid matches their auth.uid()",
|
|
82
83
|
policyname: "Users can delete their own data",
|
|
83
84
|
cmd: "DELETE",
|
|
84
85
|
permissive: "PERMISSIVE",
|
|
85
86
|
roles: ["authenticated"],
|
|
86
|
-
qual: "auth.uid() =
|
|
87
|
+
qual: "auth.uid() = uid",
|
|
87
88
|
with_check: ""
|
|
88
89
|
}
|
|
89
90
|
];
|
|
@@ -138,9 +139,10 @@ var PolicyEditor = ({ policy, schema, table, onSave, onCancel }) => {
|
|
|
138
139
|
});
|
|
139
140
|
};
|
|
140
141
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
141
|
-
/* @__PURE__ */ jsxs(
|
|
142
|
-
className: "flex justify-between items-center w-full",
|
|
142
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
143
143
|
variant: "h6",
|
|
144
|
+
gutterBottom: true,
|
|
145
|
+
className: "mt-8 mx-8 flex justify-between items-center w-full",
|
|
144
146
|
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: policy ? t("studio_policy_edit") : t("studio_policy_create") }), /* @__PURE__ */ jsxs("div", {
|
|
145
147
|
className: "text-sm font-normal text-text-secondary dark:text-text-secondary-dark tracking-wide mt-1",
|
|
146
148
|
children: [
|
|
@@ -455,7 +457,7 @@ var PolicyEditor = ({ policy, schema, table, onSave, onCancel }) => {
|
|
|
455
457
|
}),
|
|
456
458
|
/* @__PURE__ */ jsx("div", {
|
|
457
459
|
className: cls("bg-surface-100 dark:bg-surface-950 px-3 py-2 rounded-md font-mono text-sm my-2", defaultBorderMixin),
|
|
458
|
-
children: "Example: auth.uid() =
|
|
460
|
+
children: "Example: auth.uid() = uid"
|
|
459
461
|
}),
|
|
460
462
|
/* @__PURE__ */ jsx(Typography, {
|
|
461
463
|
variant: "caption",
|
|
@@ -479,7 +481,7 @@ var PolicyEditor = ({ policy, schema, table, onSave, onCancel }) => {
|
|
|
479
481
|
}),
|
|
480
482
|
/* @__PURE__ */ jsx("div", {
|
|
481
483
|
className: cls("bg-surface-100 dark:bg-surface-950 px-3 py-2 rounded-md font-mono text-sm my-2", defaultBorderMixin),
|
|
482
|
-
children: "Example: auth.uid() =
|
|
484
|
+
children: "Example: auth.uid() = uid"
|
|
483
485
|
}),
|
|
484
486
|
/* @__PURE__ */ jsx(Typography, {
|
|
485
487
|
variant: "caption",
|
|
@@ -511,7 +513,7 @@ var PolicyEditor = ({ policy, schema, table, onSave, onCancel }) => {
|
|
|
511
513
|
className: "block mt-0.5",
|
|
512
514
|
children: ["Returns the current user's ID as text. Example: ", /* @__PURE__ */ jsx("code", {
|
|
513
515
|
className: "bg-surface-100 dark:bg-surface-950 px-1 py-0.5 rounded text-[11px]",
|
|
514
|
-
children: "auth.uid() =
|
|
516
|
+
children: "auth.uid() = uid"
|
|
515
517
|
})]
|
|
516
518
|
})] }),
|
|
517
519
|
/* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("code", {
|
|
@@ -838,21 +840,33 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
838
840
|
};
|
|
839
841
|
});
|
|
840
842
|
if (activeCollection && isPostgresCollectionConfig(activeCollection) && activeCollection.securityRules) activeCollection.securityRules.forEach((rule) => {
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
843
|
+
const ops = getPolicyOperations(rule);
|
|
844
|
+
getPolicyNamesForRule(rule, activeTableData.tableName).forEach((policyName, opIdx) => {
|
|
845
|
+
policiesMap[policyName] = {
|
|
846
|
+
policyname: policyName,
|
|
847
|
+
tablename: activeTableData.tableName,
|
|
848
|
+
permissive: (rule.mode || "permissive").toUpperCase(),
|
|
849
|
+
cmd: (ops[opIdx] ?? rule.operation ?? "ALL").toUpperCase(),
|
|
850
|
+
roles: [...rule.roles ?? ["public"]],
|
|
851
|
+
qual: rule.using || null,
|
|
852
|
+
with_check: rule.withCheck || null,
|
|
853
|
+
status: policiesMap[policyName] ? "both" : "code_only"
|
|
854
|
+
};
|
|
855
|
+
});
|
|
853
856
|
});
|
|
857
|
+
if (!activeCollection) try {
|
|
858
|
+
const spec = resolveJunctionSpecs(collectionRegistry.collections ?? []).get(activeTableData.tableName);
|
|
859
|
+
if (spec) {
|
|
860
|
+
const generatedNames = getPolicyNamesForRules(getJunctionSecurityRules(spec), spec.table);
|
|
861
|
+
for (const p of Object.values(policiesMap)) if (generatedNames.has(p.policyname)) p.status = "both";
|
|
862
|
+
}
|
|
863
|
+
} catch {}
|
|
854
864
|
return Object.values(policiesMap).sort((a, b) => a.policyname.localeCompare(b.policyname));
|
|
855
|
-
}, [
|
|
865
|
+
}, [
|
|
866
|
+
activeTableData,
|
|
867
|
+
activeCollection,
|
|
868
|
+
collectionRegistry.collections
|
|
869
|
+
]);
|
|
856
870
|
const rlsStats = useMemo(() => {
|
|
857
871
|
return {
|
|
858
872
|
total: tables.length,
|
|
@@ -1321,7 +1335,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1321
1335
|
}), /* @__PURE__ */ jsx(Typography, {
|
|
1322
1336
|
variant: "caption",
|
|
1323
1337
|
className: "opacity-80",
|
|
1324
|
-
children: "This is an auto-generated junction table for a many-to-many relation.
|
|
1338
|
+
children: "This is an auto-generated junction table for a many-to-many relation. Rebase derives its policies: rows are readable when both related rows are, and writable following the declaring collection's update rules (plus the server/admin baseline). You can still add RLS policies directly to broaden access."
|
|
1325
1339
|
})] })]
|
|
1326
1340
|
})
|
|
1327
1341
|
}),
|
|
@@ -1543,4 +1557,4 @@ var RLSEditor = ({ apiUrl = "" }) => {
|
|
|
1543
1557
|
//#endregion
|
|
1544
1558
|
export { RLSEditor };
|
|
1545
1559
|
|
|
1546
|
-
//# sourceMappingURL=RLSEditor-
|
|
1560
|
+
//# sourceMappingURL=RLSEditor-rLlMcZPG.js.map
|