@rebasepro/studio 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +12 -4
- package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
- package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
- package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
- package/dist/BackupsView-DVkKzaPR.js.map +1 -0
- package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
- package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
- package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
- package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
- package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
- package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
- package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
- package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
- package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
- package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
- package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
- package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
- package/dist/components/RLSEditor/saveRules.d.ts +38 -0
- package/dist/components/RebaseStudio.d.ts +1 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
- package/dist/components/load-failure-view.d.ts +17 -0
- package/dist/components/load-failure.d.ts +47 -0
- package/dist/index.es.js +56 -36
- package/dist/index.es.js.map +1 -1
- package/dist/load-failure-C6OMJSGC.js +31 -0
- package/dist/load-failure-C6OMJSGC.js.map +1 -0
- package/dist/load-failure-view-CBTB_h-h.js +48 -0
- package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
- package/package.json +36 -21
- package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
- package/dist/BackupsView-DNS6LdVg.js.map +0 -1
- package/dist/BranchesView-CM3B-ER3.js.map +0 -1
- package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
- package/dist/RLSEditor-CBX4uD66.js.map +0 -1
- package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
- package/dist/StorageView-BGDeC5GI.js.map +0 -1
- package/dist/components/StorageView/storage-failure.d.ts +0 -34
- package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
- package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
- package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
- package/src/components/ApiExplorer/parseSpec.ts +0 -104
- package/src/components/ApiExplorer/types.ts +0 -84
- package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
- package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
- package/src/components/ApiKeys/permissions.ts +0 -145
- package/src/components/AuthSimulationSelector.tsx +0 -72
- package/src/components/Backups/BackupsView.tsx +0 -185
- package/src/components/Branches/BranchesView.tsx +0 -386
- package/src/components/CronJobs/CronJobsView.tsx +0 -413
- package/src/components/JSEditor/JSEditor.tsx +0 -1100
- package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
- package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
- package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
- package/src/components/LogsExplorer/useLogTail.ts +0 -356
- package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
- package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
- package/src/components/RLSEditor/index.ts +0 -1
- package/src/components/RLSEditor/policy-presets.ts +0 -176
- package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
- package/src/components/RebaseStudio.tsx +0 -174
- package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
- package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
- package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
- package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
- package/src/components/SQLEditor/sql_editor_types.ts +0 -18
- package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
- package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
- package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
- package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
- package/src/components/StorageView/StorageView.tsx +0 -1588
- package/src/components/StorageView/storage-failure.ts +0 -63
- package/src/components/StudioHomePage.tsx +0 -430
- package/src/index.ts +0 -31
- package/src/utils/parseSpec.test.ts +0 -309
- package/src/utils/sql_utils.test.ts +0 -265
- package/src/utils/sql_utils.ts +0 -291
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useRef, useEffect } from "react";
|
|
3
|
-
import Editor, { Monaco, OnMount } from "@monaco-editor/react";
|
|
4
|
-
import type { editor } from "monaco-editor";
|
|
5
|
-
import { cls, defaultBorderMixin, FileIcon } from "@rebasepro/ui";
|
|
6
|
-
import { useModeController } from "@rebasepro/app";
|
|
7
|
-
import { ALL_WHERE_FILTER_OPS } from "@rebasepro/types";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Shape of `monaco.languages.typescript` at runtime.
|
|
11
|
-
* The bundled type stubs mark this API as deprecated, but it is fully functional.
|
|
12
|
-
*/
|
|
13
|
-
interface MonacoTypeScriptApi {
|
|
14
|
-
typescriptDefaults: {
|
|
15
|
-
setCompilerOptions(options: Record<string, unknown>): void;
|
|
16
|
-
setDiagnosticsOptions(options: Record<string, unknown>): void;
|
|
17
|
-
addExtraLib(content: string, filePath: string): void;
|
|
18
|
-
};
|
|
19
|
-
ScriptTarget: Record<string, number>;
|
|
20
|
-
ModuleKind: Record<string, number>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Ambient type definitions for the Rebase client SDK injected into Monaco.
|
|
25
|
-
*
|
|
26
|
-
* Hand-mirrored from `@rebasepro/client`, so it drifts: this block spent long
|
|
27
|
-
* enough declaring the ten Firestore-era filter operators and a
|
|
28
|
-
* `where?: Record<string, string>` that the editor was autocompleting a query
|
|
29
|
-
* shape the server rejects. The operator union is interpolated from
|
|
30
|
-
* {@link ALL_WHERE_FILTER_OPS} for that reason — it is the one part that cannot
|
|
31
|
-
* fall behind. The rest still has to be kept in step by hand.
|
|
32
|
-
*/
|
|
33
|
-
const REBASE_CLIENT_TYPES = `
|
|
34
|
-
// ─── Rebase Client SDK Type Definitions ─────────────────────────────
|
|
35
|
-
|
|
36
|
-
/** A flat database row with the id merged at the top level. */
|
|
37
|
-
|
|
38
|
-
interface FindParams {
|
|
39
|
-
limit?: number;
|
|
40
|
-
offset?: number;
|
|
41
|
-
page?: number;
|
|
42
|
-
/** \`{ status: ["==", "active"] }\` — a tuple per field, or an array of tuples. */
|
|
43
|
-
where?: Record<string, [WhereFilterOp, any] | [WhereFilterOp, any][]>;
|
|
44
|
-
logical?: LogicalCondition;
|
|
45
|
-
/** \`["created_at", "desc"]\` */
|
|
46
|
-
orderBy?: [string, "asc" | "desc"];
|
|
47
|
-
include?: string[];
|
|
48
|
-
searchString?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface FindResult<M extends Record<string, any> = any> {
|
|
52
|
-
data: M[];
|
|
53
|
-
meta: {
|
|
54
|
-
total: number;
|
|
55
|
-
limit: number;
|
|
56
|
-
offset: number;
|
|
57
|
-
hasMore: boolean;
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type WhereFilterOp = ${ALL_WHERE_FILTER_OPS.map(op => `"${op}"`).join(" | ")};
|
|
62
|
-
|
|
63
|
-
interface FilterCondition {
|
|
64
|
-
column: string;
|
|
65
|
-
operator: WhereFilterOp;
|
|
66
|
-
value: any;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface LogicalCondition {
|
|
70
|
-
type: "and" | "or";
|
|
71
|
-
conditions: (FilterCondition | LogicalCondition)[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface QueryBuilder<M extends Record<string, any> = any> {
|
|
75
|
-
where(column: keyof M & string, operator: WhereFilterOp, value: any): QueryBuilder<M>;
|
|
76
|
-
orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilder<M>;
|
|
77
|
-
limit(count: number): QueryBuilder<M>;
|
|
78
|
-
offset(count: number): QueryBuilder<M>;
|
|
79
|
-
search(searchString: string): QueryBuilder<M>;
|
|
80
|
-
find(): Promise<FindResult<M>>;
|
|
81
|
-
count(): Promise<number>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
interface CollectionClient<M extends Record<string, any> = any> {
|
|
85
|
-
find(params?: FindParams): Promise<FindResult<M>>;
|
|
86
|
-
findById(id: string | number): Promise<M | undefined>;
|
|
87
|
-
create(data: Partial<M>, id?: string | number): Promise<M>;
|
|
88
|
-
update(id: string | number, data: Partial<M>): Promise<M>;
|
|
89
|
-
delete(id: string | number): Promise<void>;
|
|
90
|
-
where(column: keyof M & string, operator: WhereFilterOp, value: any): QueryBuilder<M>;
|
|
91
|
-
orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilder<M>;
|
|
92
|
-
limit(count: number): QueryBuilder<M>;
|
|
93
|
-
offset(count: number): QueryBuilder<M>;
|
|
94
|
-
search(searchString: string): QueryBuilder<M>;
|
|
95
|
-
listen?(params: FindParams | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
|
|
96
|
-
listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
|
|
97
|
-
count?(params?: FindParams): Promise<number>;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface User {
|
|
101
|
-
uid: string;
|
|
102
|
-
email: string | null;
|
|
103
|
-
displayName: string | null;
|
|
104
|
-
photoURL: string | null;
|
|
105
|
-
providerId: string;
|
|
106
|
-
isAnonymous: boolean;
|
|
107
|
-
emailVerified?: boolean;
|
|
108
|
-
roles?: string[];
|
|
109
|
-
metadata?: Record<string, unknown>;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
interface RebaseSession {
|
|
113
|
-
accessToken: string;
|
|
114
|
-
refreshToken: string;
|
|
115
|
-
expiresAt: number;
|
|
116
|
-
user: User;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
type AuthChangeEvent = 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'USER_UPDATED';
|
|
120
|
-
|
|
121
|
-
interface RebaseAuth {
|
|
122
|
-
signInWithEmail(email: string, password: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;
|
|
123
|
-
signUp(email: string, password: string, displayName?: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;
|
|
124
|
-
signInWithGoogle(idToken: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;
|
|
125
|
-
signOut(): Promise<void>;
|
|
126
|
-
refreshSession(): Promise<RebaseSession>;
|
|
127
|
-
getUser(): Promise<User>;
|
|
128
|
-
updateUser(updates: { displayName?: string; photoURL?: string }): Promise<User>;
|
|
129
|
-
resetPasswordForEmail(email: string): Promise<{ success: boolean; message: string }>;
|
|
130
|
-
resetPassword(token: string, password: string): Promise<{ success: boolean; message: string }>;
|
|
131
|
-
changePassword(oldPassword: string, newPassword: string): Promise<{ success: boolean; message: string }>;
|
|
132
|
-
sendVerificationEmail(): Promise<{ success: boolean; message: string }>;
|
|
133
|
-
verifyEmail(token: string): Promise<{ success: boolean; message: string }>;
|
|
134
|
-
getSessions(): Promise<RebaseSession[]>;
|
|
135
|
-
revokeSession(sessionId: string): Promise<{ success: boolean }>;
|
|
136
|
-
revokeAllSessions(): Promise<{ success: boolean }>;
|
|
137
|
-
getSession(): RebaseSession | null;
|
|
138
|
-
onAuthStateChange(callback: (event: AuthChangeEvent, session: RebaseSession | null) => void): () => void;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
interface AdminUser {
|
|
142
|
-
uid: string;
|
|
143
|
-
email: string;
|
|
144
|
-
displayName: string | null;
|
|
145
|
-
photoURL: string | null;
|
|
146
|
-
provider: string;
|
|
147
|
-
roles: string[];
|
|
148
|
-
createdAt: string;
|
|
149
|
-
updatedAt: string;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
interface RebaseCMS {
|
|
153
|
-
listUsers(): Promise<{ users: AdminUser[] }>;
|
|
154
|
-
getUser(userId: string): Promise<{ user: AdminUser }>;
|
|
155
|
-
createUser(data: { email: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
|
|
156
|
-
updateUser(userId: string, data: { email?: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;
|
|
157
|
-
deleteUser(userId: string): Promise<{ success: boolean }>;
|
|
158
|
-
bootstrap(): Promise<{ success: boolean; message: string; user: { uid: string; roles: string[] } }>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
interface UploadFileProps {
|
|
162
|
-
file: FileIcon;
|
|
163
|
-
fileName?: string;
|
|
164
|
-
path?: string;
|
|
165
|
-
metadata?: Record<string, unknown>;
|
|
166
|
-
bucket?: string;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
interface UploadFileResult {
|
|
170
|
-
path: string;
|
|
171
|
-
bucket?: string;
|
|
172
|
-
downloadUrl?: string;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
interface DownloadConfig {
|
|
176
|
-
url: string | null;
|
|
177
|
-
fileNotFound?: boolean;
|
|
178
|
-
metadata?: Record<string, unknown>;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
interface StorageSource {
|
|
182
|
-
putObject(props: UploadFileProps): Promise<UploadFileResult>;
|
|
183
|
-
getSignedUrl(pathOrUrl: string, bucket?: string): Promise<DownloadConfig>;
|
|
184
|
-
getObject(path: string, bucket?: string): Promise<FileIcon | null>;
|
|
185
|
-
deleteObject(path: string, bucket?: string): Promise<void>;
|
|
186
|
-
listObjects(path: string, options?: { bucket?: string; maxResults?: number; pageToken?: string }): Promise<unknown>;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
type RebaseData = {
|
|
190
|
-
/** Look up a collection by slug. */
|
|
191
|
-
collection(slug: string): CollectionClient;
|
|
192
|
-
} & {
|
|
193
|
-
/** Dynamic collection access — e.g. client.data.authors */
|
|
194
|
-
[collectionSlug: string]: CollectionClient;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The Rebase client instance. Use \`client.data\`, \`client.auth\`, \`client.admin\`,
|
|
199
|
-
* \`client.storage\`, and \`client.call()\` to interact with your Rebase backend.
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
* // Query a collection
|
|
203
|
-
* const result = await client.data.products.find({ limit: 10 });
|
|
204
|
-
*
|
|
205
|
-
* // Create a record
|
|
206
|
-
* await client.data.products.create({ name: "Camera", price: 299 });
|
|
207
|
-
*
|
|
208
|
-
* // Fluent query
|
|
209
|
-
* const expensive = await client.data.products.where("price", ">", 100).orderBy("price", "desc").limit(5).find();
|
|
210
|
-
*
|
|
211
|
-
* // Auth
|
|
212
|
-
* const session = client.auth.getSession();
|
|
213
|
-
*
|
|
214
|
-
* // Admin
|
|
215
|
-
* const { users } = await client.admin.listUsers();
|
|
216
|
-
*
|
|
217
|
-
* // Custom endpoint
|
|
218
|
-
* const result = await client.call("/my-endpoint", { myData: 123 });
|
|
219
|
-
*/
|
|
220
|
-
interface RebaseClient {
|
|
221
|
-
/** Data access — dynamic collection accessors */
|
|
222
|
-
data: RebaseData;
|
|
223
|
-
/** Authentication methods */
|
|
224
|
-
auth: RebaseAuth;
|
|
225
|
-
/** User/role admin methods */
|
|
226
|
-
admin: RebaseCMS;
|
|
227
|
-
/** Storage operations */
|
|
228
|
-
storage?: StorageSource;
|
|
229
|
-
/** Call a custom server-side endpoint */
|
|
230
|
-
call<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
|
|
231
|
-
/** Direct collection access (shorthand) */
|
|
232
|
-
[collectionSlug: string]: unknown;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** The pre-configured client instance. Already authenticated with the current user session. */
|
|
236
|
-
declare const client: RebaseClient;
|
|
237
|
-
|
|
238
|
-
/** Execution context with user and collection information. */
|
|
239
|
-
interface JSEditorContext {
|
|
240
|
-
/** The user the script is running as. */
|
|
241
|
-
user: {
|
|
242
|
-
uid: string;
|
|
243
|
-
displayName: string | null;
|
|
244
|
-
email: string | null;
|
|
245
|
-
roles?: string[];
|
|
246
|
-
} | null;
|
|
247
|
-
/** Registered collections with their property names. */
|
|
248
|
-
collections: Array<{
|
|
249
|
-
slug: string;
|
|
250
|
-
name: string;
|
|
251
|
-
properties: string[];
|
|
252
|
-
}>;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Execution context — contains info about the selected user and registered collections. */
|
|
256
|
-
declare const context: JSEditorContext;
|
|
257
|
-
`;
|
|
258
|
-
|
|
259
|
-
export type CollectionInfo = {
|
|
260
|
-
slug: string;
|
|
261
|
-
name: string;
|
|
262
|
-
properties: string[];
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
export type JSMonacoEditorProps = {
|
|
266
|
-
value: string;
|
|
267
|
-
onChange: (value: string | undefined) => void;
|
|
268
|
-
onRun?: (selectedText?: string) => void;
|
|
269
|
-
className?: string;
|
|
270
|
-
readOnly?: boolean;
|
|
271
|
-
autoFocus?: boolean;
|
|
272
|
-
/** Collection slugs for basic autocomplete */
|
|
273
|
-
collectionSlugs?: string[];
|
|
274
|
-
/** Full collection info with property names for richer type generation */
|
|
275
|
-
collections?: CollectionInfo[];
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
export const JSMonacoEditor = ({
|
|
279
|
-
value,
|
|
280
|
-
onChange,
|
|
281
|
-
onRun,
|
|
282
|
-
className,
|
|
283
|
-
readOnly = false,
|
|
284
|
-
autoFocus = true,
|
|
285
|
-
collectionSlugs = [],
|
|
286
|
-
collections = []
|
|
287
|
-
}: JSMonacoEditorProps) => {
|
|
288
|
-
const { mode } = useModeController();
|
|
289
|
-
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
290
|
-
const monacoRef = useRef<Monaco | null>(null);
|
|
291
|
-
const onRunRef = useRef(onRun);
|
|
292
|
-
onRunRef.current = onRun;
|
|
293
|
-
const typesRegisteredRef = useRef(false);
|
|
294
|
-
|
|
295
|
-
const handleEditorOnMount: OnMount = (editorInstance, monaco) => {
|
|
296
|
-
editorRef.current = editorInstance;
|
|
297
|
-
monacoRef.current = monaco;
|
|
298
|
-
|
|
299
|
-
// Register Cmd/Ctrl+Enter to run
|
|
300
|
-
editorInstance.addAction({
|
|
301
|
-
id: "run-script",
|
|
302
|
-
label: "Run Script",
|
|
303
|
-
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
|
304
|
-
contextMenuGroupId: "operation",
|
|
305
|
-
contextMenuOrder: 0,
|
|
306
|
-
run: () => {
|
|
307
|
-
if (onRunRef.current) {
|
|
308
|
-
const selection = editorInstance.getSelection();
|
|
309
|
-
let selectedText: string | undefined;
|
|
310
|
-
if (selection && !selection.isEmpty()) {
|
|
311
|
-
selectedText = editorInstance.getModel()?.getValueInRange(selection)?.trim();
|
|
312
|
-
}
|
|
313
|
-
onRunRef.current(selectedText || undefined);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Configure TypeScript/JavaScript defaults for richer completion
|
|
319
|
-
if (!typesRegisteredRef.current) {
|
|
320
|
-
typesRegisteredRef.current = true;
|
|
321
|
-
|
|
322
|
-
// Configure TS compiler for modern JS with top-level await
|
|
323
|
-
// Note: cast through `any` because the bundled type stubs mark
|
|
324
|
-
// `monaco.languages.typescript` as deprecated while the runtime
|
|
325
|
-
// API is fully functional.
|
|
326
|
-
const ts = (monaco.languages as unknown as { typescript: MonacoTypeScriptApi }).typescript;
|
|
327
|
-
ts.typescriptDefaults.setCompilerOptions({
|
|
328
|
-
target: ts.ScriptTarget.ESNext,
|
|
329
|
-
module: ts.ModuleKind.ESNext,
|
|
330
|
-
allowJs: true,
|
|
331
|
-
checkJs: false,
|
|
332
|
-
strict: false,
|
|
333
|
-
noEmit: true,
|
|
334
|
-
allowNonTsExtensions: true
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// Suppress diagnostics that don't apply to our script context.
|
|
338
|
-
// Scripts are executed inside `new AsyncFunction(...)` at runtime,
|
|
339
|
-
// so top-level `await` (1375) and `return` (1108) are valid.
|
|
340
|
-
ts.typescriptDefaults.setDiagnosticsOptions({
|
|
341
|
-
diagnosticCodesToIgnore: [1375, 1108]
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
// Inject the Rebase Client SDK type definitions
|
|
345
|
-
ts.typescriptDefaults.addExtraLib(
|
|
346
|
-
REBASE_CLIENT_TYPES,
|
|
347
|
-
"ts:rebase-client.d.ts"
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
// Generate collection-specific types from registered collections
|
|
351
|
-
if (collections.length > 0) {
|
|
352
|
-
const lines: string[] = [];
|
|
353
|
-
for (const col of collections) {
|
|
354
|
-
// Generate a typed interface for each collection
|
|
355
|
-
const ifaceName = col.slug.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
356
|
-
const propsType = col.properties.length > 0
|
|
357
|
-
? `{ ${col.properties.map(p => `${p}: any`).join("; ")} }`
|
|
358
|
-
: "Record<string, any>";
|
|
359
|
-
lines.push(`/** Collection: ${col.name} (${col.slug}) */`);
|
|
360
|
-
lines.push(`interface ${ifaceName}_Row ${propsType}`);
|
|
361
|
-
lines.push(`declare namespace client.data { const ${col.slug}: CollectionClient<${ifaceName}_Row>; }`);
|
|
362
|
-
}
|
|
363
|
-
ts.typescriptDefaults.addExtraLib(
|
|
364
|
-
lines.join("\n"),
|
|
365
|
-
"ts:rebase-collections.d.ts"
|
|
366
|
-
);
|
|
367
|
-
} else if (collectionSlugs.length > 0) {
|
|
368
|
-
// Fallback: just slug names without property info
|
|
369
|
-
const collectionHints = collectionSlugs.map(slug =>
|
|
370
|
-
`/** Collection: ${slug} */\ndeclare namespace client.data { const ${slug}: CollectionClient; }`
|
|
371
|
-
).join("\n");
|
|
372
|
-
ts.typescriptDefaults.addExtraLib(
|
|
373
|
-
collectionHints,
|
|
374
|
-
"ts:rebase-collections.d.ts"
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (autoFocus) {
|
|
380
|
-
editorInstance.focus();
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
return (
|
|
385
|
-
<div className={cls("relative w-full h-full overflow-hidden", className)}>
|
|
386
|
-
<Editor
|
|
387
|
-
height="100%"
|
|
388
|
-
defaultLanguage="typescript"
|
|
389
|
-
path="rebase-script.ts"
|
|
390
|
-
value={value}
|
|
391
|
-
onChange={onChange}
|
|
392
|
-
onMount={handleEditorOnMount}
|
|
393
|
-
theme={mode === "dark" ? "vs-dark" : "vs"}
|
|
394
|
-
options={{
|
|
395
|
-
minimap: { enabled: false },
|
|
396
|
-
fontSize: 14,
|
|
397
|
-
lineNumbers: "on",
|
|
398
|
-
scrollBeyondLastLine: false,
|
|
399
|
-
automaticLayout: true,
|
|
400
|
-
readOnly,
|
|
401
|
-
tabSize: 2,
|
|
402
|
-
wordWrap: "on",
|
|
403
|
-
suggestOnTriggerCharacters: true,
|
|
404
|
-
quickSuggestions: true,
|
|
405
|
-
parameterHints: { enabled: true }
|
|
406
|
-
}}
|
|
407
|
-
/>
|
|
408
|
-
</div>
|
|
409
|
-
);
|
|
410
|
-
};
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
-
import { ArrowDownToLineIcon, Checkbox, cls, defaultBorderMixin, Label, Select, SelectItem, TextField, Typography } from "@rebasepro/ui";
|
|
3
|
-
import { idNum, useLogTail } from "./useLogTail";
|
|
4
|
-
import type { LogTransport } from "./useLogTail";
|
|
5
|
-
|
|
6
|
-
const LEVEL_COLORS: Record<string, string> = {
|
|
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
|
-
|
|
13
|
-
const SOURCE_COLORS: Record<string, string> = {
|
|
14
|
-
api: "text-sky-600 dark:text-sky-400",
|
|
15
|
-
auth: "text-purple-600 dark:text-purple-400",
|
|
16
|
-
storage: "text-green-600 dark:text-green-500",
|
|
17
|
-
realtime: "text-orange-600 dark:text-orange-400",
|
|
18
|
-
system: "text-surface-600 dark:text-surface-400"
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const TRANSPORT_LABELS: Record<LogTransport, string> = {
|
|
22
|
-
connecting: "Connecting",
|
|
23
|
-
live: "Live",
|
|
24
|
-
polling: "Polling"
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const TRANSPORT_DOTS: Record<LogTransport, string> = {
|
|
28
|
-
connecting: "bg-amber-500",
|
|
29
|
-
live: "bg-green-500",
|
|
30
|
-
polling: "bg-surface-400"
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const TRANSPORT_TITLES: Record<LogTransport, string> = {
|
|
34
|
-
connecting: "Reconnecting to the log stream.",
|
|
35
|
-
live: "Streaming — entries appear as the server writes them.",
|
|
36
|
-
polling: "This server has no log stream; falling back to a refresh every 3 seconds."
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// How close to the bottom edge still counts as "at the bottom".
|
|
40
|
-
const STICK_THRESHOLD = 40;
|
|
41
|
-
|
|
42
|
-
export function LogsExplorer() {
|
|
43
|
-
const [level, setLevel] = useState<string>("all");
|
|
44
|
-
const [source, setSource] = useState<string>("all");
|
|
45
|
-
const [searchInput, setSearchInput] = useState("");
|
|
46
|
-
const [search, setSearch] = useState("");
|
|
47
|
-
const [autoScroll, setAutoScroll] = useState(true);
|
|
48
|
-
// Entries that arrived while the user was scrolled away from the bottom.
|
|
49
|
-
const [newCount, setNewCount] = useState(0);
|
|
50
|
-
const [atBottom, setAtBottom] = useState(true);
|
|
51
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
52
|
-
// Whether the view is stuck to the bottom right now. A ref, not state:
|
|
53
|
-
// the scroll handler and the entry-counting effect both read it synchronously.
|
|
54
|
-
const stickRef = useRef(true);
|
|
55
|
-
const lastMaxIdRef = useRef<number | null>(null);
|
|
56
|
-
|
|
57
|
-
// Debounce the search box so typing doesn't reopen the stream per keystroke.
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
const t = setTimeout(() => setSearch(searchInput), 300);
|
|
60
|
-
return () => clearTimeout(t);
|
|
61
|
-
}, [searchInput]);
|
|
62
|
-
|
|
63
|
-
const { logs, error, transport, dropped } = useLogTail({ level,
|
|
64
|
-
source,
|
|
65
|
-
search });
|
|
66
|
-
|
|
67
|
-
// A filter change replaces the window wholesale — "new since you looked
|
|
68
|
-
// away" stops meaning anything, so the counter starts over.
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
setNewCount(0);
|
|
71
|
-
}, [level, source, search]);
|
|
72
|
-
|
|
73
|
-
// Count what arrived while the user was reading further up. Driven off the
|
|
74
|
-
// rendered window rather than off the transport, so it means the same thing
|
|
75
|
-
// whether the entries were streamed or polled.
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
const max = logs.length > 0 ? idNum(logs[logs.length - 1]) : null;
|
|
78
|
-
const prevMax = lastMaxIdRef.current;
|
|
79
|
-
if (prevMax != null && !stickRef.current) {
|
|
80
|
-
const fresh = logs.filter(e => idNum(e) > prevMax).length;
|
|
81
|
-
if (fresh > 0) setNewCount(c => c + fresh);
|
|
82
|
-
}
|
|
83
|
-
lastMaxIdRef.current = max;
|
|
84
|
-
}, [logs]);
|
|
85
|
-
|
|
86
|
-
const scrollToBottom = useCallback(() => {
|
|
87
|
-
const el = containerRef.current;
|
|
88
|
-
if (!el) return;
|
|
89
|
-
el.scrollTop = el.scrollHeight;
|
|
90
|
-
stickRef.current = true;
|
|
91
|
-
setAtBottom(true);
|
|
92
|
-
setNewCount(0);
|
|
93
|
-
}, []);
|
|
94
|
-
|
|
95
|
-
// Stick to the bottom only while the user is already there. Scrolling up
|
|
96
|
-
// disengages; scrolling back down re-engages. Our own scrollTop writes
|
|
97
|
-
// always land at the bottom, so they can never disengage it.
|
|
98
|
-
const handleScroll = useCallback(() => {
|
|
99
|
-
const el = containerRef.current;
|
|
100
|
-
if (!el) return;
|
|
101
|
-
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;
|
|
102
|
-
stickRef.current = nearBottom;
|
|
103
|
-
setAtBottom(nearBottom);
|
|
104
|
-
if (nearBottom) setNewCount(0);
|
|
105
|
-
}, []);
|
|
106
|
-
|
|
107
|
-
useLayoutEffect(() => {
|
|
108
|
-
if (autoScroll && stickRef.current) {
|
|
109
|
-
const el = containerRef.current;
|
|
110
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
111
|
-
}
|
|
112
|
-
}, [logs, autoScroll]);
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div className="flex flex-col h-[calc(100vh-64px)] w-full bg-surface-50 dark:bg-surface-800">
|
|
116
|
-
{/* Toolbar */}
|
|
117
|
-
<div className={cls(
|
|
118
|
-
"flex gap-2 p-3 border-b items-center flex-wrap shrink-0",
|
|
119
|
-
defaultBorderMixin
|
|
120
|
-
)}>
|
|
121
|
-
<Select
|
|
122
|
-
value={level}
|
|
123
|
-
onValueChange={setLevel}
|
|
124
|
-
size="small"
|
|
125
|
-
placeholder="All Levels"
|
|
126
|
-
>
|
|
127
|
-
<SelectItem value="all">All Levels</SelectItem>
|
|
128
|
-
<SelectItem value="debug">Debug</SelectItem>
|
|
129
|
-
<SelectItem value="info">Info</SelectItem>
|
|
130
|
-
<SelectItem value="warn">Warn</SelectItem>
|
|
131
|
-
<SelectItem value="error">Error</SelectItem>
|
|
132
|
-
</Select>
|
|
133
|
-
<Select
|
|
134
|
-
value={source}
|
|
135
|
-
onValueChange={setSource}
|
|
136
|
-
size="small"
|
|
137
|
-
placeholder="All Sources"
|
|
138
|
-
>
|
|
139
|
-
<SelectItem value="all">All Sources</SelectItem>
|
|
140
|
-
<SelectItem value="api">API</SelectItem>
|
|
141
|
-
<SelectItem value="auth">Auth</SelectItem>
|
|
142
|
-
<SelectItem value="storage">Storage</SelectItem>
|
|
143
|
-
<SelectItem value="realtime">Realtime</SelectItem>
|
|
144
|
-
<SelectItem value="system">System</SelectItem>
|
|
145
|
-
</Select>
|
|
146
|
-
<TextField
|
|
147
|
-
size="small"
|
|
148
|
-
aria-label="Search logs"
|
|
149
|
-
placeholder="Search logs..."
|
|
150
|
-
value={searchInput}
|
|
151
|
-
onChange={e => setSearchInput(e.target.value)}
|
|
152
|
-
className="flex-1 min-w-[200px]"
|
|
153
|
-
/>
|
|
154
|
-
<div className="flex items-center gap-1.5 cursor-pointer ml-2">
|
|
155
|
-
<Checkbox
|
|
156
|
-
id="auto-scroll"
|
|
157
|
-
checked={autoScroll}
|
|
158
|
-
onCheckedChange={(checked: boolean) => {
|
|
159
|
-
setAutoScroll(checked);
|
|
160
|
-
if (checked) scrollToBottom();
|
|
161
|
-
}}
|
|
162
|
-
size="small"
|
|
163
|
-
padding={false}
|
|
164
|
-
/>
|
|
165
|
-
<Label
|
|
166
|
-
htmlFor="auto-scroll"
|
|
167
|
-
className="text-xs select-none cursor-pointer text-surface-600 dark:text-surface-400"
|
|
168
|
-
>
|
|
169
|
-
Auto-scroll
|
|
170
|
-
</Label>
|
|
171
|
-
</div>
|
|
172
|
-
{/* Whether the tail is actually attached. A log view with
|
|
173
|
-
nothing in it is ambiguous — quiet server, or a stream that
|
|
174
|
-
dropped — and this is the difference. */}
|
|
175
|
-
<div className="ml-auto pl-4 flex items-center gap-3">
|
|
176
|
-
<div className="flex items-center gap-1.5" title={TRANSPORT_TITLES[transport]}>
|
|
177
|
-
<span
|
|
178
|
-
aria-hidden
|
|
179
|
-
className={cls("w-1.5 h-1.5 rounded-full shrink-0", TRANSPORT_DOTS[transport])}
|
|
180
|
-
/>
|
|
181
|
-
<Typography variant="caption" color="secondary">
|
|
182
|
-
{TRANSPORT_LABELS[transport]}
|
|
183
|
-
</Typography>
|
|
184
|
-
</div>
|
|
185
|
-
<Typography variant="caption" color="secondary">
|
|
186
|
-
{logs.length} entries
|
|
187
|
-
</Typography>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* A broken tail must stay visible even while stale logs are on
|
|
192
|
-
screen — otherwise the view quietly freezes. */}
|
|
193
|
-
{error && logs.length > 0 && (
|
|
194
|
-
<div className={cls(
|
|
195
|
-
"px-4 py-1.5 border-b bg-amber-50 dark:bg-amber-950/20 shrink-0",
|
|
196
|
-
defaultBorderMixin
|
|
197
|
-
)}>
|
|
198
|
-
<Typography variant="caption" className="text-amber-700 dark:text-amber-400">
|
|
199
|
-
{error}
|
|
200
|
-
</Typography>
|
|
201
|
-
</div>
|
|
202
|
-
)}
|
|
203
|
-
|
|
204
|
-
{/* The server bounds what it holds per connection, so a burst big
|
|
205
|
-
enough leaves a hole in what is on screen. Say so: a tail that
|
|
206
|
-
silently skips entries is worse than one that admits it. */}
|
|
207
|
-
{dropped > 0 && (
|
|
208
|
-
<div className={cls(
|
|
209
|
-
"px-4 py-1.5 border-b bg-amber-50 dark:bg-amber-950/20 shrink-0",
|
|
210
|
-
defaultBorderMixin
|
|
211
|
-
)}>
|
|
212
|
-
<Typography variant="caption" className="text-amber-700 dark:text-amber-400">
|
|
213
|
-
{dropped} {dropped === 1 ? "entry was" : "entries were"} dropped — the log is
|
|
214
|
-
arriving faster than this view can read it. Narrow the filter to keep up.
|
|
215
|
-
</Typography>
|
|
216
|
-
</div>
|
|
217
|
-
)}
|
|
218
|
-
|
|
219
|
-
{/* Log entries */}
|
|
220
|
-
<div className="relative flex-1 min-h-0">
|
|
221
|
-
<div
|
|
222
|
-
ref={containerRef}
|
|
223
|
-
onScroll={handleScroll}
|
|
224
|
-
className="h-full overflow-auto py-2"
|
|
225
|
-
>
|
|
226
|
-
{logs.map(log => (
|
|
227
|
-
<div
|
|
228
|
-
key={log.id}
|
|
229
|
-
className={cls(
|
|
230
|
-
"flex gap-4 px-4 py-[6px] border-b hover:bg-surface-100 dark:hover:bg-surface-900 transition-colors",
|
|
231
|
-
defaultBorderMixin
|
|
232
|
-
)}
|
|
233
|
-
>
|
|
234
|
-
<Typography variant="body2" color="secondary" className="w-[72px] shrink-0 font-mono">
|
|
235
|
-
{new Date(log.timestamp).toLocaleTimeString()}
|
|
236
|
-
</Typography>
|
|
237
|
-
<Typography variant="body2" className={cls("w-[48px] shrink-0 uppercase font-semibold font-mono", LEVEL_COLORS[log.level] || "text-surface-500")}>
|
|
238
|
-
{log.level}
|
|
239
|
-
</Typography>
|
|
240
|
-
<Typography variant="body2" className={cls("w-[80px] shrink-0 font-mono", SOURCE_COLORS[log.source] || "text-surface-500")}>
|
|
241
|
-
[{log.source}]
|
|
242
|
-
</Typography>
|
|
243
|
-
<Typography variant="body2" className="flex-1 font-mono break-all whitespace-pre-wrap text-surface-900 dark:text-surface-100">
|
|
244
|
-
{log.message}
|
|
245
|
-
</Typography>
|
|
246
|
-
</div>
|
|
247
|
-
))}
|
|
248
|
-
{logs.length === 0 && (
|
|
249
|
-
<div className="p-8 text-center">
|
|
250
|
-
<Typography
|
|
251
|
-
variant="body2"
|
|
252
|
-
className={error ? "text-red-600 dark:text-red-500" : undefined}
|
|
253
|
-
color={error ? undefined : "secondary"}
|
|
254
|
-
>
|
|
255
|
-
{error ?? "No log entries yet. Logs will appear here as requests come in."}
|
|
256
|
-
</Typography>
|
|
257
|
-
</div>
|
|
258
|
-
)}
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
{autoScroll && !atBottom && newCount > 0 && (
|
|
262
|
-
<button
|
|
263
|
-
onClick={scrollToBottom}
|
|
264
|
-
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"
|
|
265
|
-
>
|
|
266
|
-
<ArrowDownToLineIcon size={14}/>
|
|
267
|
-
{newCount} new {newCount === 1 ? "entry" : "entries"}
|
|
268
|
-
</button>
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|