@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.
Files changed (87) hide show
  1. package/dist/{ApiExplorer-BmcdhAX0.js → ApiExplorer-CGHEF1uL.js} +4 -4
  2. package/dist/ApiExplorer-CGHEF1uL.js.map +1 -0
  3. package/dist/{CronJobsView-CNfz0etw.js → CronJobsView-3PM_qR8v.js} +20 -3
  4. package/dist/CronJobsView-3PM_qR8v.js.map +1 -0
  5. package/dist/{JSEditor-Ch8z8lJ4.js → JSEditor-BCSoElPg.js} +26 -36
  6. package/dist/JSEditor-BCSoElPg.js.map +1 -0
  7. package/dist/LogsExplorer-_4sZadKn.js +162 -0
  8. package/dist/LogsExplorer-_4sZadKn.js.map +1 -0
  9. package/dist/{SQLEditor-BELYJQRP.js → SQLEditor-BC0IOUQu.js} +4 -4
  10. package/dist/SQLEditor-BC0IOUQu.js.map +1 -0
  11. package/dist/common/src/collections/default-collections.d.ts +9 -0
  12. package/dist/common/src/collections/index.d.ts +1 -0
  13. package/dist/common/src/util/permissions.d.ts +1 -0
  14. package/dist/core/src/components/LoginView/LoginView.d.ts +25 -1
  15. package/dist/core/src/components/common/types.d.ts +10 -7
  16. package/dist/core/src/components/common/useDebouncedData.d.ts +1 -1
  17. package/dist/core/src/core/RebaseProps.d.ts +13 -2
  18. package/dist/core/src/core/RebaseRouter.d.ts +1 -1
  19. package/dist/core/src/hooks/data/useCollectionFetch.d.ts +12 -1
  20. package/dist/core/src/hooks/index.d.ts +0 -1
  21. package/dist/core/src/util/entity_cache.d.ts +0 -5
  22. package/dist/core/src/util/index.d.ts +0 -2
  23. package/dist/core/src/util/useStorageUploadController.d.ts +2 -2
  24. package/dist/formex/src/utils.d.ts +2 -2
  25. package/dist/index.es.js +23 -5
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/index.umd.js +228 -44
  28. package/dist/index.umd.js.map +1 -1
  29. package/dist/studio/src/components/ApiExplorer/parseSpec.d.ts +1 -1
  30. package/dist/studio/src/components/ApiExplorer/types.d.ts +3 -3
  31. package/dist/studio/src/components/LogsExplorer/LogsExplorer.d.ts +1 -0
  32. package/dist/types/src/controllers/auth.d.ts +4 -26
  33. package/dist/types/src/controllers/client.d.ts +25 -43
  34. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  35. package/dist/types/src/controllers/data.d.ts +4 -0
  36. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  37. package/dist/types/src/controllers/registry.d.ts +5 -4
  38. package/dist/types/src/rebase_context.d.ts +1 -1
  39. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  40. package/dist/types/src/types/backend.d.ts +2 -2
  41. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  42. package/dist/types/src/types/collections.d.ts +0 -4
  43. package/dist/types/src/types/component_ref.d.ts +1 -1
  44. package/dist/types/src/types/cron.d.ts +1 -1
  45. package/dist/types/src/types/entity_views.d.ts +1 -0
  46. package/dist/types/src/types/export_import.d.ts +1 -1
  47. package/dist/types/src/types/formex.d.ts +2 -2
  48. package/dist/types/src/types/properties.d.ts +9 -7
  49. package/dist/types/src/types/translations.d.ts +28 -12
  50. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  51. package/dist/types/src/users/index.d.ts +0 -1
  52. package/dist/types/src/users/user.d.ts +0 -1
  53. package/dist/ui/src/components/Button.d.ts +2 -2
  54. package/dist/ui/src/components/ErrorBoundary.d.ts +25 -3
  55. package/dist/ui/src/components/VirtualTable/VirtualTable.d.ts +1 -1
  56. package/dist/ui/src/components/VirtualTable/VirtualTableCell.d.ts +6 -6
  57. package/dist/ui/src/components/VirtualTable/VirtualTableHeader.d.ts +8 -8
  58. package/dist/ui/src/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  59. package/dist/ui/src/components/VirtualTable/VirtualTableProps.d.ts +11 -11
  60. package/dist/ui/src/components/VirtualTable/VirtualTableRow.d.ts +1 -1
  61. package/dist/ui/src/components/VirtualTable/types.d.ts +9 -9
  62. package/dist/ui/src/hooks/useDebounceCallback.d.ts +1 -1
  63. package/dist/ui/src/util/debounce.d.ts +1 -1
  64. package/package.json +8 -8
  65. package/src/components/ApiExplorer/ApiExplorer.tsx +2 -2
  66. package/src/components/ApiExplorer/EndpointDetail.tsx +1 -1
  67. package/src/components/ApiExplorer/TryItPanel.tsx +5 -5
  68. package/src/components/ApiExplorer/parseSpec.ts +3 -3
  69. package/src/components/ApiExplorer/types.ts +3 -3
  70. package/src/components/CronJobs/CronJobsView.tsx +27 -2
  71. package/src/components/JSEditor/JSEditor.tsx +21 -18
  72. package/src/components/JSEditor/JSMonacoEditor.tsx +6 -19
  73. package/src/components/LogsExplorer/LogsExplorer.tsx +224 -0
  74. package/src/components/RebaseStudio.tsx +10 -1
  75. package/src/components/SQLEditor/SQLEditor.tsx +28 -7
  76. package/src/components/StudioHomePage.tsx +2 -1
  77. package/src/utils/parseSpec.test.ts +274 -0
  78. package/src/utils/pgColumnToProperty.test.ts +1 -0
  79. package/src/utils/pgColumnToProperty.ts +35 -4
  80. package/dist/ApiExplorer-BmcdhAX0.js.map +0 -1
  81. package/dist/CronJobsView-CNfz0etw.js.map +0 -1
  82. package/dist/JSEditor-Ch8z8lJ4.js.map +0 -1
  83. package/dist/SQLEditor-BELYJQRP.js.map +0 -1
  84. package/dist/core/src/hooks/useValidateAuthenticator.d.ts +0 -21
  85. package/dist/core/src/util/icon_synonyms.d.ts +0 -1
  86. package/dist/core/src/util/useTraceUpdate.d.ts +0 -2
  87. 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: any[];
50
+ args: unknown[];
51
51
  timestamp: number;
52
52
  }
53
53
 
54
54
  interface ExecutionResult {
55
- value: any;
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: any): string {
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: any,
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: any[] = [];
155
- if (resultValue?.data && Array.isArray(resultValue.data)) {
156
- rows = resultValue.data;
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: any[]) => {
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
- if (value?.data && Array.isArray(value.data)) {
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: any[] = [];
526
- if (result.value?.data && Array.isArray(result.value.data)) {
527
- rows = result.value.data.map((entity: any) => ({
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: any): { collection: MatchedJSCollection; entityId: string | number }[] => {
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: any }) {
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<any[]>;
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, any>;
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?: any;
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<any>;
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 = any>(endpoint: string, payload?: any): Promise<T>;
210
+ call<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
224
211
  /** Direct collection access (shorthand) */
225
- [collectionSlug: string]: any;
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
- const interval = setInterval(() => {
70
- setElapsed(Date.now() - start);
71
- }, 100);
72
- return () => clearInterval(interval);
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
  {