@rebasepro/studio 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
- package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
- package/dist/ApiKeysView-B66TdzhH.js +1738 -0
- package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
- package/dist/BackupsView-DmSMzAbT.js +368 -0
- package/dist/BackupsView-DmSMzAbT.js.map +1 -0
- package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
- package/dist/BranchesView-DP1a87E-.js.map +1 -0
- package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
- package/dist/CronJobsView-TDErHTuK.js.map +1 -0
- package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
- package/dist/JSEditor-B59pwg4c.js.map +1 -0
- package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
- package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
- package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
- package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
- package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
- package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
- package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
- package/dist/SQLEditor-1An7w0j6.js.map +1 -0
- package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
- package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
- package/dist/StorageView-CbsHuCBf.js.map +1 -0
- package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
- package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
- package/dist/components/ApiKeys/scope-words.d.ts +37 -0
- package/dist/components/ApiKeys/scopes.d.ts +113 -0
- package/dist/components/AuthSimulationSelector.d.ts +5 -1
- package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
- package/dist/components/RLSEditor/policyListing.d.ts +44 -0
- package/dist/components/RLSEditor/policyRules.d.ts +49 -0
- package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
- package/dist/components/load-failure.d.ts +1 -1
- package/dist/components/useRunAsUsers.d.ts +25 -0
- package/dist/index.es.js +11 -11
- package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
- package/dist/load-failure-Bd8lFIO7.js.map +1 -0
- package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
- package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +126 -36
- package/package.json +9 -9
- package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
- package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
- package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
- package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
- package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
- package/dist/BackupsView-DYTAD6aW.js +0 -253
- package/dist/BackupsView-DYTAD6aW.js.map +0 -1
- package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
- package/dist/CronJobsView-JwRjQCde.js.map +0 -1
- package/dist/JSEditor-qiuwmcvk.js.map +0 -1
- package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
- package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
- package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
- package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
- package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
- package/dist/StorageView-C52dSZNf.js.map +0 -1
- package/dist/components/ApiKeys/permissions.d.ts +0 -66
- package/dist/load-failure-C6OMJSGC.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JSEditor-B59pwg4c.js","names":[],"sources":["../src/components/JSEditor/JSMonacoEditor.tsx","../src/components/JSEditor/JSEditorSidebar.tsx","../src/components/JSEditor/JSEditor.tsx"],"sourcesContent":["\nimport React, { useRef, useEffect } from \"react\";\nimport Editor, { Monaco, OnMount } from \"@monaco-editor/react\";\nimport type { editor } from \"monaco-editor\";\nimport { cls, defaultBorderMixin, FileIcon } from \"@rebasepro/ui\";\nimport { useModeController } from \"@rebasepro/app\";\nimport { ALL_WHERE_FILTER_OPS } from \"@rebasepro/types\";\n\n/**\n * Shape of `monaco.languages.typescript` at runtime.\n * The bundled type stubs mark this API as deprecated, but it is fully functional.\n */\ninterface MonacoTypeScriptApi {\n typescriptDefaults: {\n setCompilerOptions(options: Record<string, unknown>): void;\n setDiagnosticsOptions(options: Record<string, unknown>): void;\n addExtraLib(content: string, filePath: string): void;\n };\n ScriptTarget: Record<string, number>;\n ModuleKind: Record<string, number>;\n}\n\n/**\n * Ambient type definitions for the Rebase client SDK injected into Monaco.\n *\n * Hand-mirrored from `@rebasepro/client`, so it drifts: this block spent long\n * enough declaring the ten Firestore-era filter operators and a\n * `where?: Record<string, string>` that the editor was autocompleting a query\n * shape the server rejects. The operator union is interpolated from\n * {@link ALL_WHERE_FILTER_OPS} for that reason — it is the one part that cannot\n * fall behind. The rest still has to be kept in step by hand.\n */\nconst REBASE_CLIENT_TYPES = `\n// ─── Rebase Client SDK Type Definitions ─────────────────────────────\n\n/** A flat database row with the id merged at the top level. */\n\ninterface FindParams {\n limit?: number;\n offset?: number;\n page?: number;\n /** \\`{ status: [\"==\", \"active\"] }\\` — a tuple per field, or an array of tuples. */\n where?: Record<string, [WhereFilterOp, any] | [WhereFilterOp, any][]>;\n logical?: LogicalCondition;\n /** \\`[\"created_at\", \"desc\"]\\` */\n orderBy?: [string, \"asc\" | \"desc\"];\n include?: string[];\n searchString?: string;\n}\n\ninterface FindResult<M extends Record<string, any> = any> {\n data: M[];\n meta: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n };\n}\n\ntype WhereFilterOp = ${ALL_WHERE_FILTER_OPS.map(op => `\"${op}\"`).join(\" | \")};\n\ninterface FilterCondition {\n column: string;\n operator: WhereFilterOp;\n value: any;\n}\n\ninterface LogicalCondition {\n type: \"and\" | \"or\";\n conditions: (FilterCondition | LogicalCondition)[];\n}\n\ninterface QueryBuilder<M extends Record<string, any> = any> {\n where(column: keyof M & string, operator: WhereFilterOp, value: any): QueryBuilder<M>;\n orderBy(column: keyof M & string, direction?: \"asc\" | \"desc\"): QueryBuilder<M>;\n limit(count: number): QueryBuilder<M>;\n offset(count: number): QueryBuilder<M>;\n search(searchString: string): QueryBuilder<M>;\n find(): Promise<FindResult<M>>;\n count(): Promise<number>;\n}\n\ninterface CollectionClient<M extends Record<string, any> = any> {\n find(params?: FindParams): Promise<FindResult<M>>;\n findById(id: string | number): Promise<M | undefined>;\n create(data: Partial<M>, id?: string | number): Promise<M>;\n update(id: string | number, data: Partial<M>): Promise<M>;\n delete(id: string | number): Promise<void>;\n where(column: keyof M & string, operator: WhereFilterOp, value: any): QueryBuilder<M>;\n orderBy(column: keyof M & string, direction?: \"asc\" | \"desc\"): QueryBuilder<M>;\n limit(count: number): QueryBuilder<M>;\n offset(count: number): QueryBuilder<M>;\n search(searchString: string): QueryBuilder<M>;\n listen?(params: FindParams | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;\n listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;\n count?(params?: FindParams): Promise<number>;\n}\n\ninterface User {\n uid: string;\n email: string | null;\n displayName: string | null;\n photoURL: string | null;\n providerId: string;\n isAnonymous: boolean;\n emailVerified?: boolean;\n roles?: string[];\n metadata?: Record<string, unknown>;\n}\n\ninterface RebaseSession {\n accessToken: string;\n refreshToken: string;\n expiresAt: number;\n user: User;\n}\n\ntype AuthChangeEvent = 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'USER_UPDATED';\n\ninterface RebaseAuth {\n signInWithEmail(email: string, password: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;\n signUp(email: string, password: string, displayName?: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;\n signInWithGoogle(idToken: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;\n signOut(): Promise<void>;\n refreshSession(): Promise<RebaseSession>;\n getUser(): Promise<User>;\n updateUser(updates: { displayName?: string; photoURL?: string }): Promise<User>;\n resetPasswordForEmail(email: string): Promise<{ success: boolean; message: string }>;\n resetPassword(token: string, password: string): Promise<{ success: boolean; message: string }>;\n changePassword(oldPassword: string, newPassword: string): Promise<{ success: boolean; message: string }>;\n sendVerificationEmail(): Promise<{ success: boolean; message: string }>;\n verifyEmail(token: string): Promise<{ success: boolean; message: string }>;\n getSessions(): Promise<RebaseSession[]>;\n revokeSession(sessionId: string): Promise<{ success: boolean }>;\n revokeAllSessions(): Promise<{ success: boolean }>;\n getSession(): RebaseSession | null;\n onAuthStateChange(callback: (event: AuthChangeEvent, session: RebaseSession | null) => void): () => void;\n}\n\ninterface AdminUser {\n uid: string;\n email: string;\n displayName: string | null;\n photoURL: string | null;\n provider: string;\n roles: string[];\n createdAt: string;\n updatedAt: string;\n}\n\ninterface ListUsersOptions {\n search?: string;\n /** Page size. The server's default is 25. */\n limit?: number;\n offset?: number;\n orderBy?: string;\n orderDir?: \"asc\" | \"desc\";\n}\n\n/** One page of users, and \\`total\\`: how many match in all. */\ninterface UsersPage {\n users: AdminUser[];\n total: number;\n limit: number;\n offset: number;\n}\n\ninterface RebaseCMS {\n /** One page of users (25 unless \\`limit\\` says otherwise) and the total. */\n listUsers(options?: ListUsersOptions): Promise<UsersPage>;\n listUsersPaginated(options?: ListUsersOptions): Promise<UsersPage>;\n getUser(userId: string): Promise<{ user: AdminUser }>;\n createUser(data: { email: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;\n updateUser(userId: string, data: { email?: string; displayName?: string; password?: string; roles?: string[] }): Promise<{ user: AdminUser }>;\n deleteUser(userId: string): Promise<{ success: boolean }>;\n bootstrap(): Promise<{ success: boolean; message: string; user: { uid: string; roles: string[] } }>;\n}\n\ninterface UploadFileProps {\n file: FileIcon;\n fileName?: string;\n path?: string;\n metadata?: Record<string, unknown>;\n bucket?: string;\n}\n\ninterface UploadFileResult {\n path: string;\n bucket?: string;\n downloadUrl?: string;\n}\n\ninterface DownloadConfig {\n url: string | null;\n fileNotFound?: boolean;\n metadata?: Record<string, unknown>;\n}\n\ninterface StorageSource {\n putObject(props: UploadFileProps): Promise<UploadFileResult>;\n getSignedUrl(pathOrUrl: string, bucket?: string): Promise<DownloadConfig>;\n getObject(path: string, bucket?: string): Promise<FileIcon | null>;\n deleteObject(path: string, bucket?: string): Promise<void>;\n listObjects(path: string, options?: { bucket?: string; maxResults?: number; pageToken?: string }): Promise<unknown>;\n}\n\ntype RebaseData = {\n /** Look up a collection by slug. */\n collection(slug: string): CollectionClient;\n} & {\n /** Dynamic collection access — e.g. client.data.authors */\n [collectionSlug: string]: CollectionClient;\n};\n\n/**\n * The Rebase client instance. Use \\`client.data\\`, \\`client.auth\\`, \\`client.admin\\`,\n * \\`client.storage\\`, and \\`client.call()\\` to interact with your Rebase backend.\n *\n * @example\n * // Query a collection\n * const result = await client.data.products.find({ limit: 10 });\n *\n * // Create a record\n * await client.data.products.create({ name: \"Camera\", price: 299 });\n *\n * // Fluent query\n * const expensive = await client.data.products.where(\"price\", \">\", 100).orderBy(\"price\", \"desc\").limit(5).find();\n *\n * // Auth\n * const session = client.auth.getSession();\n *\n * // Admin: one page of users, and how many there are\n * const { users, total } = await client.admin.listUsers({ limit: 50 });\n *\n * // Custom endpoint\n * const result = await client.call(\"/my-endpoint\", { myData: 123 });\n */\ninterface RebaseClient {\n /** Data access — dynamic collection accessors */\n data: RebaseData;\n /** Authentication methods */\n auth: RebaseAuth;\n /** User/role admin methods */\n admin: RebaseCMS;\n /** Storage operations */\n storage?: StorageSource;\n /** Call a custom server-side endpoint */\n call<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;\n /** Direct collection access (shorthand) */\n [collectionSlug: string]: unknown;\n}\n\n/**\n * The client for this run: your own, one that runs every request as the user\n * picked in \"Run as\" (it holds no session, so \\`client.auth.getSession()\\` is null),\n * or an unauthenticated one under \"No Auth\".\n */\ndeclare const client: RebaseClient;\n\n/** Execution context with user and collection information. */\ninterface JSEditorContext {\n /** The user the script runs as: the one picked in \"Run as\", or you. \\`null\\` under \"No Auth\". */\n user: {\n uid: string;\n displayName: string | null;\n email: string | null;\n roles?: string[];\n } | null;\n /** Registered collections with their property names. */\n collections: Array<{\n slug: string;\n name: string;\n properties: string[];\n }>;\n}\n\n/** Execution context — contains info about the selected user and registered collections. */\ndeclare const context: JSEditorContext;\n`;\n\nexport type CollectionInfo = {\n slug: string;\n name: string;\n properties: string[];\n};\n\nexport type JSMonacoEditorProps = {\n value: string;\n onChange: (value: string | undefined) => void;\n onRun?: (selectedText?: string) => void;\n className?: string;\n readOnly?: boolean;\n autoFocus?: boolean;\n /** Collection slugs for basic autocomplete */\n collectionSlugs?: string[];\n /** Full collection info with property names for richer type generation */\n collections?: CollectionInfo[];\n};\n\nexport const JSMonacoEditor = ({\n value,\n onChange,\n onRun,\n className,\n readOnly = false,\n autoFocus = true,\n collectionSlugs = [],\n collections = []\n}: JSMonacoEditorProps) => {\n const { mode } = useModeController();\n const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);\n const monacoRef = useRef<Monaco | null>(null);\n const onRunRef = useRef(onRun);\n onRunRef.current = onRun;\n const typesRegisteredRef = useRef(false);\n\n const handleEditorOnMount: OnMount = (editorInstance, monaco) => {\n editorRef.current = editorInstance;\n monacoRef.current = monaco;\n\n // Register Cmd/Ctrl+Enter to run\n editorInstance.addAction({\n id: \"run-script\",\n label: \"Run Script\",\n keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],\n contextMenuGroupId: \"operation\",\n contextMenuOrder: 0,\n run: () => {\n if (onRunRef.current) {\n const selection = editorInstance.getSelection();\n let selectedText: string | undefined;\n if (selection && !selection.isEmpty()) {\n selectedText = editorInstance.getModel()?.getValueInRange(selection)?.trim();\n }\n onRunRef.current(selectedText || undefined);\n }\n }\n });\n\n // Configure TypeScript/JavaScript defaults for richer completion\n if (!typesRegisteredRef.current) {\n typesRegisteredRef.current = true;\n\n // Configure TS compiler for modern JS with top-level await\n // Note: cast through `any` because the bundled type stubs mark\n // `monaco.languages.typescript` as deprecated while the runtime\n // API is fully functional.\n const ts = (monaco.languages as unknown as { typescript: MonacoTypeScriptApi }).typescript;\n ts.typescriptDefaults.setCompilerOptions({\n target: ts.ScriptTarget.ESNext,\n module: ts.ModuleKind.ESNext,\n allowJs: true,\n checkJs: false,\n strict: false,\n noEmit: true,\n allowNonTsExtensions: true\n });\n\n // Suppress diagnostics that don't apply to our script context.\n // Scripts are executed inside `new AsyncFunction(...)` at runtime,\n // so top-level `await` (1375) and `return` (1108) are valid.\n ts.typescriptDefaults.setDiagnosticsOptions({\n diagnosticCodesToIgnore: [1375, 1108]\n });\n\n // Inject the Rebase Client SDK type definitions\n ts.typescriptDefaults.addExtraLib(\n REBASE_CLIENT_TYPES,\n \"ts:rebase-client.d.ts\"\n );\n\n // Generate collection-specific types from registered collections\n if (collections.length > 0) {\n const lines: string[] = [];\n for (const col of collections) {\n // Generate a typed interface for each collection\n const ifaceName = col.slug.replace(/[^a-zA-Z0-9_]/g, \"_\");\n const propsType = col.properties.length > 0\n ? `{ ${col.properties.map(p => `${p}: any`).join(\"; \")} }`\n : \"Record<string, any>\";\n lines.push(`/** Collection: ${col.name} (${col.slug}) */`);\n lines.push(`interface ${ifaceName}_Row ${propsType}`);\n lines.push(`declare namespace client.data { const ${col.slug}: CollectionClient<${ifaceName}_Row>; }`);\n }\n ts.typescriptDefaults.addExtraLib(\n lines.join(\"\\n\"),\n \"ts:rebase-collections.d.ts\"\n );\n } else if (collectionSlugs.length > 0) {\n // Fallback: just slug names without property info\n const collectionHints = collectionSlugs.map(slug =>\n `/** Collection: ${slug} */\\ndeclare namespace client.data { const ${slug}: CollectionClient; }`\n ).join(\"\\n\");\n ts.typescriptDefaults.addExtraLib(\n collectionHints,\n \"ts:rebase-collections.d.ts\"\n );\n }\n }\n\n if (autoFocus) {\n editorInstance.focus();\n }\n };\n\n return (\n <div className={cls(\"relative w-full h-full overflow-hidden\", className)}>\n <Editor\n height=\"100%\"\n defaultLanguage=\"typescript\"\n path=\"rebase-script.ts\"\n value={value}\n onChange={onChange}\n onMount={handleEditorOnMount}\n theme={mode === \"dark\" ? \"vs-dark\" : \"vs\"}\n options={{\n minimap: { enabled: false },\n fontSize: 14,\n lineNumbers: \"on\",\n scrollBeyondLastLine: false,\n automaticLayout: true,\n readOnly,\n tabSize: 2,\n wordWrap: \"on\",\n suggestOnTriggerCharacters: true,\n quickSuggestions: true,\n parameterHints: { enabled: true }\n }}\n />\n </div>\n );\n};\n","\nimport React, { useState } from \"react\";\nimport {\n cls,\n defaultBorderMixin,\n IconButton,\n iconSize,\n PlusIcon,\n Tab,\n Tabs,\n Tooltip,\n Trash2Icon,\n Typography\n} from \"@rebasepro/ui\";\n\nexport interface JSSnippet {\n id: string;\n name: string;\n code: string;\n createdAt: number;\n isFavorite?: boolean;\n}\n\ninterface CollectionInfo {\n slug: string;\n properties: string[];\n}\n\ninterface JSEditorSidebarProps {\n collections: CollectionInfo[];\n snippets: JSSnippet[];\n history: string[];\n onSelectSnippet: (code: string) => void;\n onDeleteSnippet: (id: string) => void;\n onInsertCode: (code: string) => void;\n}\n\nconst QUICK_REFERENCE: { label: string; code: string; description: string }[] = [\n {\n label: \"Find all\",\n code: \"const result = await client.data.collection(\\\"COLLECTION\\\").find({ limit: 20 });\\nreturn result;\",\n description: \"Fetch records with pagination\"\n },\n {\n label: \"Find by ID\",\n code: \"const item = await client.data.collection(\\\"COLLECTION\\\").findById(\\\"ID\\\");\\nreturn item;\",\n description: \"Fetch a single record\"\n },\n {\n label: \"Create\",\n code: \"const created = await client.data.collection(\\\"COLLECTION\\\").create({\\n // your fields here\\n});\\nreturn created;\",\n description: \"Insert a new record\"\n },\n {\n label: \"Update\",\n code: \"const updated = await client.data.collection(\\\"COLLECTION\\\").update(\\\"ID\\\", {\\n // fields to update\\n});\\nreturn updated;\",\n description: \"Update an existing record\"\n },\n {\n label: \"Delete\",\n code: \"await client.data.collection(\\\"COLLECTION\\\").delete(\\\"ID\\\");\\nreturn { success: true };\",\n description: \"Delete a record\"\n },\n {\n label: \"Fluent query\",\n code: \"const result = await client.data.collection(\\\"COLLECTION\\\")\\n .where(\\\"field\\\", \\\">\\\", value)\\n .orderBy(\\\"field\\\", \\\"desc\\\")\\n .limit(10)\\n .find();\\nreturn result;\",\n description: \"Chained query builder\"\n },\n {\n label: \"Who am I\",\n code: \"return context.user;\",\n description: \"The user this script runs as, or null under No Auth\"\n },\n {\n label: \"Auth: Current session\",\n code: \"const session = client.auth.getSession();\\nreturn session;\",\n description: \"The session this client holds: yours, or none under Run as and No Auth\"\n },\n {\n label: \"Admin: List users\",\n code: \"// One page: raise limit or page with offset while users.length < total\\nconst { users, total } = await client.admin.listUsers({ limit: 50, offset: 0 });\\nreturn { users, total };\",\n description: \"A page of users, and how many there are in all\"\n },\n {\n label: \"Custom endpoint\",\n code: \"const result = await client.call(\\\"/my-endpoint\\\", { key: \\\"value\\\" });\\nreturn result;\",\n description: \"Call a custom API endpoint\"\n }\n];\n\nexport const JSEditorSidebar = ({\n collections,\n snippets,\n history,\n onSelectSnippet,\n onDeleteSnippet,\n onInsertCode\n}: JSEditorSidebarProps) => {\n const [activeTab, setActiveTab] = useState<\"collections\" | \"snippets\" | \"history\" | \"reference\">(\"collections\");\n\n return (\n <div className={cls(\"flex flex-col h-full w-full bg-surface-card border-r\", defaultBorderMixin)}>\n <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as \"collections\" | \"snippets\" | \"history\" | \"reference\")} variant=\"boxy\" className=\"border-b border-hairline\">\n <Tab value=\"collections\">\n <Tooltip title=\"Browse collections\">\n <span className=\"text-xs\">Collections</span>\n </Tooltip>\n </Tab>\n <Tab value=\"reference\">\n <Tooltip title=\"SDK quick reference\">\n <span className=\"text-xs\">Reference</span>\n </Tooltip>\n </Tab>\n <Tab value=\"snippets\">\n <span className=\"text-xs\">Snippets</span>\n </Tab>\n <Tab value=\"history\">\n <span className=\"text-xs\">History</span>\n </Tab>\n </Tabs>\n\n <div className=\"flex-grow overflow-hidden relative\">\n {/* Collections browser */}\n {activeTab === \"collections\" && (\n <div className=\"flex flex-col h-full\">\n <div className={cls(\"flex items-center justify-between px-3 py-2 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark\">\n Collections\n </Typography>\n </div>\n <div className=\"flex-grow overflow-y-auto p-1 space-y-1 no-scrollbar\">\n {collections.length === 0 ? (\n <div className=\"p-4 text-center\">\n <Typography variant=\"caption\" className=\"text-text-disabled dark:text-text-disabled-dark\">\n No collections found\n </Typography>\n </div>\n ) : (\n collections.map(col => (\n <CollectionItem\n key={col.slug}\n collection={col}\n onInsertCode={onInsertCode}\n />\n ))\n )}\n </div>\n </div>\n )}\n\n {/* Quick reference */}\n {activeTab === \"reference\" && (\n <div className=\"flex flex-col h-full\">\n <div className={cls(\"flex items-center justify-between px-3 py-2 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark\">\n SDK Reference\n </Typography>\n </div>\n <div className=\"flex-grow overflow-y-auto p-2 space-y-2 no-scrollbar\">\n {QUICK_REFERENCE.map((ref, i) => (\n <div\n key={i}\n className={cls(\n \"group p-2.5 rounded-lg border hover:border-primary/40 dark:hover:border-primary/40\",\n \"bg-surface-card transition-all cursor-pointer\",\n defaultBorderMixin\n )}\n onClick={() => onInsertCode(ref.code)}\n >\n <div className=\"flex items-center justify-between mb-1\">\n <Typography variant=\"body2\" className=\"text-text-primary dark:text-text-primary-dark font-semibold text-[13px]\">\n {ref.label}\n </Typography>\n <svg className=\"w-3.5 h-3.5 text-text-disabled group-hover:text-primary transition-colors\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"/>\n </svg>\n </div>\n <Typography variant=\"caption\" className=\"text-text-disabled dark:text-text-disabled-dark text-[10px] leading-tight\">\n {ref.description}\n </Typography>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Snippets */}\n {activeTab === \"snippets\" && (() => {\n const favorites = snippets.filter(s => s.isFavorite);\n const others = snippets.filter(s => !s.isFavorite);\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className={cls(\"flex items-center justify-between px-3 py-2 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark\">Snippets</Typography>\n </div>\n <div className=\"flex-grow overflow-y-auto p-2 space-y-2 no-scrollbar\">\n {snippets.length === 0 ? (\n <div className=\"p-4 text-center\">\n <Typography variant=\"caption\" className=\"text-text-disabled dark:text-text-disabled-dark\">\n No saved snippets yet. Use the save button to store reusable code.\n </Typography>\n </div>\n ) : (\n <>\n {favorites.length > 0 && (\n <div className=\"mb-3\">\n <Typography variant=\"caption\" className=\"text-[10px] font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark mb-2 px-1 flex items-center\">\n <svg className=\"w-3 h-3 mr-1 text-amber-500\" fill=\"currentColor\" viewBox=\"0 0 20 20\"><path d=\"M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z\"/></svg>\n Favorites\n </Typography>\n <div className=\"space-y-2\">\n {favorites.map(snippet => (\n <SnippetItem key={snippet.id} snippet={snippet} onSelect={onSelectSnippet} onDelete={onDeleteSnippet}/>\n ))}\n </div>\n </div>\n )}\n {others.length > 0 && (\n <div>\n {favorites.length > 0 && (\n <Typography variant=\"caption\" className=\"text-[10px] font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark mb-2 px-1\">\n Others\n </Typography>\n )}\n <div className=\"space-y-2\">\n {others.map(snippet => (\n <SnippetItem key={snippet.id} snippet={snippet} onSelect={onSelectSnippet} onDelete={onDeleteSnippet}/>\n ))}\n </div>\n </div>\n )}\n </>\n )}\n </div>\n </div>\n );\n })()}\n\n {/* History */}\n {activeTab === \"history\" && (\n <div className=\"flex flex-col h-full\">\n <div className={cls(\"flex items-center justify-between px-3 py-2 border-b bg-surface-sheet min-h-[48px]\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark\">History</Typography>\n </div>\n <div className=\"flex-grow overflow-y-auto p-1 space-y-1 no-scrollbar\">\n {history.length === 0 ? (\n <div className=\"p-4 text-center\">\n <Typography variant=\"caption\" className=\"text-text-disabled dark:text-text-disabled-dark\">\n No execution history yet\n </Typography>\n </div>\n ) : (\n [...history].reverse().map((code, i) => (\n <div\n key={i}\n className=\"p-2 py-1.5 rounded hover:bg-surface-hover cursor-pointer group transition-colors flex items-start\"\n onClick={() => onSelectSnippet(code)}\n >\n <svg className=\"w-3 h-3 mt-1 mr-2 text-text-disabled dark:text-text-disabled-dark group-hover:text-primary transition-colors flex-shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"/>\n </svg>\n <Typography variant=\"caption\" className=\"text-text-secondary dark:text-text-secondary-dark group-hover:text-text-primary dark:group-hover:text-text-primary-dark text-[11px] line-clamp-2 leading-tight flex-grow font-mono\">\n {code.length > 120 ? code.substring(0, 120) + \"…\" : code}\n </Typography>\n </div>\n ))\n )}\n </div>\n </div>\n )}\n </div>\n </div>\n );\n};\n\n// ─── Sub-components ──────────────────────────────────────────────────\n\nfunction CollectionItem({ collection, onInsertCode }: { collection: CollectionInfo; onInsertCode: (code: string) => void }) {\n const [expanded, setExpanded] = useState(false);\n\n return (\n <div>\n <div\n className=\"flex items-center p-1 cursor-pointer hover:bg-surface-hover rounded transition-colors group relative\"\n onClick={() => setExpanded(!expanded)}\n >\n <svg\n className={cls(\"w-3 h-3 mr-1 transition-transform\", expanded && \"rotate-90\")}\n fill=\"currentColor\" viewBox=\"0 0 20 20\"\n >\n <path d=\"M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z\"/>\n </svg>\n <svg className=\"w-3.5 h-3.5 mr-1 shrink-0 text-text-disabled dark:text-text-disabled-dark\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4\"/>\n </svg>\n <Typography variant=\"body2\" className=\"text-text-secondary dark:text-text-secondary-dark text-xs truncate flex-1 min-w-0\">\n {collection.slug}\n </Typography>\n <Tooltip title=\"Insert find() snippet\">\n <IconButton aria-label=\"Insert find() snippet\"\n size=\"smallest\"\n className=\"opacity-0 group-hover:opacity-100 text-text-disabled hover:text-primary transition-all\"\n onClick={(e) => {\n e.stopPropagation();\n onInsertCode(`const result = await client.data.${collection.slug}.find({ limit: 20 });\\nreturn result;`);\n }}\n >\n <PlusIcon size={iconSize.smallest}/>\n </IconButton>\n </Tooltip>\n </div>\n {expanded && collection.properties.length > 0 && (\n <div className={cls(\"ml-5 mt-1 space-y-0.5 border-l\", defaultBorderMixin)}>\n {collection.properties.map(prop => (\n <div\n key={prop}\n className=\"flex items-center p-1 pl-2 hover:bg-surface-hover rounded-r cursor-pointer transition-colors group/prop relative min-h-[28px]\"\n onClick={() => onInsertCode(`\"${prop}\"`)}\n >\n <svg className=\"w-3 h-3 mr-1.5 text-text-disabled dark:text-text-disabled-dark shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1.5} d=\"M9 4.5v15m6-15v15m-10.5-1.5h15c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125h-15c-.621 0-1.125.504-1.125 1.125v12.75c0 .621.504 1.125 1.125 1.125Z\"/></svg>\n <Typography variant=\"caption\" className=\"text-text-primary dark:text-text-primary-dark text-[11px] truncate flex-grow font-mono\">{prop}</Typography>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction SnippetItem({ snippet, onSelect, onDelete }: { snippet: JSSnippet; onSelect: (code: string) => void; onDelete: (id: string) => void }) {\n return (\n <div\n className={cls(\"group p-2 rounded border hover:border-hairline-strong bg-surface-card transition-all cursor-pointer relative\", defaultBorderMixin)}\n onClick={() => onSelect(snippet.code)}\n >\n <Typography variant=\"body2\" className=\"text-text-primary dark:text-text-primary-dark font-medium text-[13px] truncate pr-6\">\n {snippet.name}\n </Typography>\n <Typography variant=\"caption\" className=\"text-text-secondary dark:text-text-secondary-dark text-[10px] block mt-1 truncate font-mono\">\n {snippet.code}\n </Typography>\n <IconButton\n size=\"smallest\"\n className=\"absolute right-1 top-1 opacity-0 group-hover:opacity-100 text-text-disabled hover:text-red-500 transition-opacity\"\n onClick={(e) => {\n e.stopPropagation();\n onDelete(snippet.id);\n }}\n >\n <Trash2Icon size={iconSize.smallest}/>\n </IconButton>\n </div>\n );\n}\n","\nimport React, { useState, useEffect, useCallback, useRef, useMemo } from \"react\";\nimport { Highlight, themes } from \"prism-react-renderer\";\nimport { toSnakeCase } from \"@rebasepro/utils\";\nimport {\n Button,\n CellRendererParams,\n Chip,\n CircularProgress,\n cls,\n defaultBorderMixin,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n DownloadIcon,\n IconButton,\n iconSize,\n Menu,\n MenuIcon,\n MenuItem,\n MoreVerticalIcon,\n PencilIcon,\n PlayIcon,\n PlusIcon,\n ResizablePanels,\n SaveIcon,\n Tab,\n Tabs,\n TerminalIcon,\n TextField,\n Tooltip,\n Typography,\n VirtualTable,\n VirtualTableColumn,\n XIcon\n} from \"@rebasepro/ui\";\nimport { useStudioUrlController, useStudioCollectionRegistry, useStudioSidePanelController } from \"@rebasepro/app\";\nimport { useRebaseContext, useRebaseClient, useSnackbarController, useApiConfig, useTranslation, useModeController, ErrorView, SelectableUser, IconForView } from \"@rebasepro/app\";\n\nimport { createMemoryStorage, createRebaseClient, type CreateAuthOptions } from \"@rebasepro/client\";\nimport { JSMonacoEditor } from \"./JSMonacoEditor\";\nimport { JSEditorSidebar, JSSnippet } from \"./JSEditorSidebar\";\nimport { AuthSimulationSelector } from \"../AuthSimulationSelector\";\nimport { useRunAsUsers } from \"../useRunAsUsers\";\nimport type { AdminCollection } from \"@rebasepro/cms-types\";\n\n// ─── Types ───────────────────────────────────────────────────────────\n\ninterface ConsoleEntry {\n type: \"log\" | \"warn\" | \"error\" | \"info\";\n args: unknown[];\n timestamp: number;\n}\n\ninterface ExecutionResult {\n value: unknown;\n console: ConsoleEntry[];\n duration: number;\n error?: string;\n timestamp: number;\n /** The code that ran: the tab's, or the selection. */\n code: string;\n /**\n * Whether the run read as the signed-in user — not as someone picked in\n * \"Run as\", and not under \"No Auth\". A row it returned is then one the\n * entity editor, which works as the signed-in user, opens as the same row.\n */\n ranAsSignedInUser: boolean;\n}\n\ninterface EditorTab {\n id: string;\n name: string;\n code: string;\n}\n\n// SelectedUser is now SelectableUser from @rebasepro/app\n\n// ─── Constants ───────────────────────────────────────────────────────\n\nconst STORAGE_PREFIX = \"rebase_js_\";\nconst MAX_HISTORY = 50;\n\n/**\n * The starter script.\n *\n * It runs. That is the point: the console opens with the Run button armed, and\n * a starter that reads `collection(\"your_collection\")` made the first thing a\n * new user did be a 404 — a placeholder presented as working code. `slug` is\n * the project's first collection when one is known.\n */\nconst makeDefaultCode = (slug?: string) => `// Available: client (RebaseClient), context\n// Press Cmd+Enter (Ctrl+Enter) to run\n//\n// Examples:\n// const { users, total } = await client.admin.listUsers({ limit: 50, offset: 0 }); // one page of total\n// const me = context.user; // who this script runs as (\"Run as\"), null under No Auth\n${slug\n ? `\nconst result = await client.data.collection(${JSON.stringify(slug)}).find({ limit: 10 });\nreturn result;\n`\n : `\n// Name one of your collections to query it:\n// const result = await client.data.collection(\"posts\").find({ limit: 10 });\n// return result;\n\nreturn context.user;\n`}`;\n\nconst DEFAULT_CODE = makeDefaultCode();\n\n/**\n * The auth of a client built for one run: its own, in memory, and empty.\n *\n * The SDK's default auth restores the session the app saved in `localStorage`,\n * sends its token on every request and keeps refreshing it. A run under \"No\n * Auth\" or as another user must do none of that: it would go out as the\n * signed-in user, and `client.auth` would describe the wrong person.\n */\nfunction runOnlyAuth(): CreateAuthOptions {\n return { storage: createMemoryStorage(), persistSession: false, autoRefresh: false };\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────\n\nfunction loadFromStorage<T>(key: string, fallback: T): T {\n try {\n const raw = localStorage.getItem(STORAGE_PREFIX + key);\n return raw ? JSON.parse(raw) : fallback;\n } catch {\n return fallback;\n }\n}\n\nfunction saveToStorage<T>(key: string, value: T) {\n try {\n localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));\n } catch { /* quota */ }\n}\n\nfunction formatJSON(value: unknown): string {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\n// ─── Helpers: Collection matching for JS results ────────────────────\n\ninterface MatchedJSCollection {\n collectionSlug: string;\n collection: AdminCollection;\n pkColumn: string;\n}\n\n/** The script without its comments: a collection named in one is not where rows came from. */\nfunction withoutComments(code: string): string {\n return code.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\").replace(/\\/\\/[^\\n]*/g, \"\");\n}\n\n/**\n * Given the raw SDK result, try to detect which collection its rows belong to.\n * JS SDK results typically come back as `{ data: [{ id, values }] }` or plain arrays.\n * The heuristic: the executed code, outside its comments, names exactly one\n * collection — `collection(\"<slug>\")` or `client.data.<slug>` — and the result has\n * rows with an \"id\" column. A script naming two could have returned either's\n * rows, and an action on a row would open the other collection's row with the\n * same id, so it gets none.\n */\nfunction detectCollectionsInResult(\n executedCode: string,\n resultValue: unknown,\n collections: AdminCollection[]\n): MatchedJSCollection[] {\n if (!resultValue || !collections?.length) return [];\n const code = withoutComments(executedCode);\n\n // Extract collection slugs mentioned in the code\n const mentionedSlugs = new Set<string>();\n\n // Match: collection(\"slug\") or collection('slug')\n const collectionCallRegex = /\\.collection\\([\"']([^\"']+)[\"']\\)/g;\n let m: RegExpExecArray | null;\n while ((m = collectionCallRegex.exec(code)) !== null) {\n mentionedSlugs.add(m[1]);\n }\n\n // Match: client.data.<slug>. (dot-access pattern)\n const dotAccessRegex = /client\\.data\\.([a-zA-Z_][a-zA-Z0-9_]*)\\./g;\n while ((m = dotAccessRegex.exec(code)) !== null) {\n if (![\"collection\", \"find\", \"findById\", \"create\", \"update\", \"delete\"].includes(m[1])) {\n mentionedSlugs.add(m[1]);\n }\n }\n\n if (mentionedSlugs.size !== 1) return [];\n\n // Check if result has rows with an \"id\" field\n let rows: Record<string, unknown>[] = [];\n const rv = resultValue as Record<string, unknown>;\n if (rv?.data && Array.isArray(rv.data)) {\n rows = rv.data;\n } else if (Array.isArray(resultValue)) {\n rows = resultValue;\n }\n\n const hasId = rows.length > 0 && rows.some(r => r?.id != null);\n if (!hasId) return [];\n\n const matched: MatchedJSCollection[] = [];\n for (const slug of mentionedSlugs) {\n const normalised = toSnakeCase(slug);\n const col = collections.find(c => {\n const tableName = (\"table\" in c ? c.table : undefined) || toSnakeCase(c.slug);\n return c.slug === slug || tableName === normalised || toSnakeCase(c.slug) === normalised;\n });\n if (col) {\n matched.push({\n collectionSlug: col.slug,\n collection: col,\n pkColumn: \"id\"\n });\n }\n }\n\n return matched;\n}\n\n// ─── Main Component ─────────────────────────────────────────────────\n\nexport function JSEditor() {\n // Contexts\n const rebaseContext = useRebaseContext();\n const rebaseClient = useRebaseClient();\n const apiConfig = useApiConfig();\n const snackbar = useSnackbarController();\n const collectionRegistry = useStudioCollectionRegistry();\n const sidePanelController = useStudioSidePanelController();\n const { t } = useTranslation();\n\n // User management for the \"Run as\" picker\n const currentUser = rebaseContext.authController?.user;\n\n // State\n const [tabs, setTabs] = useState<EditorTab[]>(() =>\n loadFromStorage(\"tabs\", [{ id: \"1\",\nname: \"Script 1\",\ncode: DEFAULT_CODE }])\n );\n const [activeTabId, setActiveTabId] = useState<string>(() =>\n loadFromStorage(\"activeTab\", \"1\")\n );\n const [result, setResult] = useState<ExecutionResult | null>(null);\n const [isRunning, setIsRunning] = useState(false);\n const [snippets, setSnippets] = useState<JSSnippet[]>(() =>\n loadFromStorage(\"snippets\", [])\n );\n const [history, setHistory] = useState<string[]>(() =>\n loadFromStorage(\"history\", [])\n );\n const [showSaveDialog, setShowSaveDialog] = useState(false);\n const [snippetName, setSnippetName] = useState(\"\");\n const [resultView, setResultView] = useState<\"json\" | \"table\" | \"console\">(\"json\");\n\n // \"Run as\" user state — null means \"self\" (current admin)\n const [selectedUser, setSelectedUser] = useState<SelectableUser | null>(null);\n const [authMode, setAuthMode] = useState<\"jwt\" | \"none\">(\"jwt\");\n\n const [sidebarSize, setSidebarSize] = useState<number>(() => {\n try {\n const stored = localStorage.getItem(STORAGE_PREFIX + \"sidebar_size\");\n return stored ? Number(stored) : 18;\n } catch { return 18; }\n });\n const [editorHeight, setEditorHeight] = useState<number>(() => {\n try {\n const stored = localStorage.getItem(STORAGE_PREFIX + \"editor_height\");\n return stored ? Number(stored) : 55;\n } catch { return 55; }\n });\n\n // Derived\n const activeTab = tabs.find(t => t.id === activeTabId) ?? tabs[0];\n\n // Collection info for the sidebar and Monaco\n const collectionInfos = useMemo(() => {\n const collections = collectionRegistry?.collections ?? [];\n return collections.map(col => ({\n slug: col.slug,\n name: col.name,\n properties: Object.keys(col.properties ?? {})\n }));\n }, [collectionRegistry?.collections]);\n\n const collectionSlugs = useMemo(() => collectionInfos.map(c => c.slug), [collectionInfos]);\n\n /**\n * Name a real collection in the starter script once the registry knows one.\n *\n * The registry is empty on first paint — in the hosted console it arrives\n * over the network — so the placeholder-free default cannot be built in the\n * `useState` initialiser. Rewriting only a tab whose content is still\n * *exactly* the untouched default means this can never overwrite anything\n * anyone typed, and never fires twice.\n */\n const firstSlug = collectionSlugs[0];\n useEffect(() => {\n if (!firstSlug) return;\n setTabs(prev => {\n if (!prev.some(tab => tab.code === DEFAULT_CODE)) return prev;\n const seeded = makeDefaultCode(firstSlug);\n return prev.map(tab => (tab.code === DEFAULT_CODE ? { ...tab,\ncode: seeded } : tab));\n });\n }, [firstSlug]);\n\n // The \"Run as\" picker: the project's users for an administrator, only\n // themselves for anyone else.\n const { users, currentUser: currentSelectableUser, loading: usersLoading, onSearchTextChange: onUserSearchTextChange, totalCount: userTotalCount } = useRunAsUsers();\n\n // ─── Persistence ─────────────────────────────────────────────\n\n useEffect(() => { saveToStorage(\"tabs\", tabs); }, [tabs]);\n useEffect(() => { saveToStorage(\"activeTab\", activeTabId); }, [activeTabId]);\n useEffect(() => { saveToStorage(\"snippets\", snippets); }, [snippets]);\n useEffect(() => { saveToStorage(\"history\", history); }, [history]);\n\n useEffect(() => {\n try { localStorage.setItem(STORAGE_PREFIX + \"sidebar_size\", sidebarSize.toString()); } catch { /* ignore */ }\n }, [sidebarSize]);\n\n useEffect(() => {\n try { localStorage.setItem(STORAGE_PREFIX + \"editor_height\", editorHeight.toString()); } catch { /* ignore */ }\n }, [editorHeight]);\n\n // ─── Tab management ──────────────────────────────────────────\n\n const updateActiveCode = useCallback((code: string | undefined) => {\n setTabs(prev => prev.map(t => t.id === activeTabId ? { ...t,\ncode: code ?? \"\" } : t));\n }, [activeTabId]);\n\n const addTab = useCallback(() => {\n const id = String(Date.now());\n const newTab: EditorTab = { id,\nname: `Script ${tabs.length + 1}`,\ncode: DEFAULT_CODE };\n setTabs(prev => [...prev, newTab]);\n setActiveTabId(id);\n }, [tabs.length]);\n\n const closeTab = useCallback((tabId: string) => {\n setTabs(prev => {\n const filtered = prev.filter(t => t.id !== tabId);\n if (filtered.length === 0) {\n const fresh = { id: String(Date.now()),\nname: \"Script 1\",\ncode: DEFAULT_CODE };\n setActiveTabId(fresh.id);\n return [fresh];\n }\n if (activeTabId === tabId) {\n setActiveTabId(filtered[filtered.length - 1].id);\n }\n return filtered;\n });\n }, [activeTabId]);\n\n // ─── Create an authenticated client for execution ────────────\n\n const buildClient = useCallback(async () => {\n const apiUrl = apiConfig?.apiUrl;\n const getAuthToken = apiConfig?.getAuthToken;\n\n if (!apiUrl) {\n throw new Error(\"API URL not configured. Make sure apiUrl is set.\");\n }\n\n if (authMode === \"none\") {\n // No credential at all, and no session to find one in.\n const client = createRebaseClient({\n baseUrl: apiUrl,\n auth: runOnlyAuth()\n });\n return { client,\nrelease: () => client.close() };\n }\n\n // If not running as another user safely reuse the application's global client.\n if (!selectedUser || (currentUser && selectedUser.uid === currentUser.uid)) {\n if (!rebaseClient) {\n throw new Error(\"Application client is not initialized.\");\n }\n return { client: rebaseClient };\n }\n\n // Get the current auth token\n const token = getAuthToken ? (await getAuthToken()) ?? undefined : undefined;\n if (!getAuthToken || !token) {\n throw new Error(\"No auth token available. Please sign in first.\");\n }\n\n // A fresh client that runs every request as the selected user: the\n // administrator's own token, and `impersonate` naming who to run as.\n // The server runs data, functions and realtime as that user, and\n // refuses every other route rather than answer as the administrator.\n // It holds no session: the token is asked of the app for each request,\n // so the app's own session is the one that gets refreshed.\n const client = createRebaseClient({\n baseUrl: apiUrl,\n token,\n impersonate: selectedUser.uid,\n auth: runOnlyAuth()\n });\n client.setAuthTokenGetter(getAuthToken);\n\n return { client,\nrelease: () => client.close() };\n }, [apiConfig, rebaseClient, selectedUser, currentUser, authMode]);\n\n // ─── Execution engine ────────────────────────────────────────\n\n const executeCode = useCallback(async (codeOverride?: string) => {\n const code = codeOverride ?? activeTab?.code ?? \"\";\n if (!code.trim()) return;\n\n setIsRunning(true);\n setResult(null);\n\n // Add to history\n setHistory(prev => {\n const deduped = prev.filter(h => h !== code);\n return [...deduped, code].slice(-MAX_HISTORY);\n });\n\n const consoleEntries: ConsoleEntry[] = [];\n const startTime = performance.now();\n let release: (() => void) | undefined;\n // The same choice `buildClient` makes: the app's own client, as the\n // signed-in user, unless \"No Auth\" or someone else is picked.\n const ranAsSignedInUser = authMode !== \"none\" && (!selectedUser || selectedUser.uid === currentUser?.uid);\n\n // Capture console methods\n const originalConsole = {\n log: console.log,\n warn: console.warn,\n error: console.error,\n info: console.info\n };\n\n const captureConsole = (type: ConsoleEntry[\"type\"]) => (...args: unknown[]) => {\n consoleEntries.push({ type,\nargs,\ntimestamp: Date.now() });\n originalConsole[type](...args);\n };\n\n try {\n console.log = captureConsole(\"log\");\n console.warn = captureConsole(\"warn\");\n console.error = captureConsole(\"error\");\n console.info = captureConsole(\"info\");\n\n // Build an authenticated client\n const built = await buildClient();\n release = built.release;\n const client = built.client;\n\n // The user the requests run as: nobody under \"No Auth\", the chosen\n // user when running as someone else, the signed-in user otherwise.\n const context = {\n user: authMode === \"none\"\n ? null\n : selectedUser ?? (currentUser ? {\n uid: currentUser.uid,\n displayName: currentUser.displayName,\n email: currentUser.email,\n roles: currentUser.roles\n } : null),\n collections: collectionInfos\n };\n\n // Create async function with `client` and `context` injected\n\n const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;\n const fn = new AsyncFunction(\"client\", \"context\", code);\n\n const value = await fn(client, context);\n const duration = performance.now() - startTime;\n\n setResult({\n value,\n console: consoleEntries,\n duration,\n timestamp: Date.now(),\n code,\n ranAsSignedInUser\n });\n\n // Auto-detect best view\n const resultObj = value as Record<string, unknown> | undefined;\n if (resultObj?.data && Array.isArray(resultObj.data)) {\n setResultView(\"table\");\n } else if (consoleEntries.length > 0 && value === undefined) {\n setResultView(\"console\");\n } else {\n setResultView(\"json\");\n }\n } catch (err: unknown) {\n const duration = performance.now() - startTime;\n setResult({\n value: undefined,\n console: consoleEntries,\n duration,\n error: err instanceof Error ? err.message : String(err),\n timestamp: Date.now(),\n code,\n ranAsSignedInUser\n });\n setResultView(\"json\");\n } finally {\n console.log = originalConsole.log;\n console.warn = originalConsole.warn;\n console.error = originalConsole.error;\n console.info = originalConsole.info;\n setIsRunning(false);\n\n // A client built for this run holds a socket and timers; release\n // them. The app's own client is left alone.\n release?.();\n }\n }, [activeTab?.code, buildClient, authMode, selectedUser, currentUser, collectionInfos]);\n\n // ─── Snippet management ──────────────────────────────────────\n\n const saveSnippet = useCallback(() => {\n if (!snippetName.trim() || !activeTab?.code.trim()) return;\n const snippet: JSSnippet = {\n id: String(Date.now()),\n name: snippetName.trim(),\n code: activeTab.code,\n createdAt: Date.now()\n };\n setSnippets(prev => [snippet, ...prev]);\n setShowSaveDialog(false);\n setSnippetName(\"\");\n snackbar.open({ type: \"success\",\nmessage: \"Snippet saved\" });\n }, [snippetName, activeTab?.code, snackbar]);\n\n const deleteSnippet = useCallback((id: string) => {\n setSnippets(prev => prev.filter(s => s.id !== id));\n }, []);\n\n // ─── Export ──────────────────────────────────────────────────\n\n const exportResult = useCallback(() => {\n if (!result?.value) return;\n const blob = new Blob([formatJSON(result.value)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `rebase-result-${Date.now()}.json`;\n a.click();\n URL.revokeObjectURL(url);\n }, [result]);\n\n // ─── Table columns for array data ────────────────────────────\n\n const tableData = useMemo(() => {\n if (!result?.value) return { columns: [] as VirtualTableColumn[],\ndata: [] as Record<string, unknown>[] };\n\n let rows: Record<string, unknown>[] = [];\n const val = result.value as Record<string, unknown>;\n if (val?.data && Array.isArray(val.data)) {\n rows = (val.data as Record<string, unknown>[]).map((entity) => ({\n id: entity.id,\n ...(entity.values as Record<string, unknown> ?? {}),\n ...(entity.values ? {} : entity)\n }));\n } else if (Array.isArray(result.value)) {\n rows = result.value;\n }\n\n if (rows.length === 0) return { columns: [] as VirtualTableColumn[],\ndata: [] as Record<string, unknown>[] };\n\n const keys = new Set<string>();\n rows.slice(0, 20).forEach(row => {\n if (row && typeof row === \"object\") {\n Object.keys(row).forEach(k => keys.add(k));\n }\n });\n\n const columns: VirtualTableColumn[] = Array.from(keys).map(key => ({\n key,\n title: key,\n width: key === \"id\" ? 100 : 200\n }));\n\n return { columns,\ndata: rows };\n }, [result]);\n\n // ─── Matched collections for entity actions ──────────────────\n\n // Rows read as someone else are not offered to the entity editor, which\n // opens and saves them as the signed-in user.\n const matchedCollections = useMemo(() => {\n if (!result?.value || result.error || !result.ranAsSignedInUser) return [];\n return detectCollectionsInResult(\n result.code,\n result.value,\n collectionRegistry?.collections ?? []\n );\n }, [result, collectionRegistry?.collections]);\n\n const getRowEntityActions = useCallback((rowData: Record<string, unknown>): { collection: MatchedJSCollection; entityId: string | number }[] => {\n if (!rowData || matchedCollections.length === 0) return [];\n return matchedCollections\n .filter(mc => rowData[mc.pkColumn] != null)\n .map(mc => ({\n collection: mc,\n entityId: rowData[mc.pkColumn] as string | number\n }));\n }, [matchedCollections]);\n\n // ─── Export handlers ─────────────────────────────────────────\n\n const handleExportCSV = useCallback(() => {\n if (tableData.data.length === 0) return;\n const headers = tableData.columns.map(c => c.key).join(\",\");\n const rows = tableData.data.map(row =>\n tableData.columns.map(c => {\n const val = row[c.key];\n const str = val === null || val === undefined ? \"\" : String(val);\n return str.includes(\",\") ? `\"${str}\"` : str;\n }).join(\",\")\n );\n const csv = [headers, ...rows].join(\"\\n\");\n const blob = new Blob([csv], { type: \"text/csv\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `js_results_${new Date().toISOString().slice(0, 19)}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n }, [tableData]);\n\n const handleExportMarkdown = useCallback(() => {\n if (tableData.data.length === 0) return;\n const headers = tableData.columns.map(c => c.key);\n const headerRow = `| ${headers.join(\" | \")} |`;\n const dividerRow = `| ${headers.map(() => \"---\").join(\" | \")} |`;\n const dataRows = tableData.data.map(row =>\n `| ${headers.map(h => {\n const val = row[h];\n if (val === null || val === undefined) return \"\";\n return String(val).replace(/\\|/g, \"\\\\|\").replace(/\\n/g, \" \");\n }).join(\" | \")} |`\n );\n const markdown = [headerRow, dividerRow, ...dataRows].join(\"\\n\");\n navigator.clipboard.writeText(markdown).then(() => {\n snackbar.open({ type: \"success\",\nmessage: t(\"studio_sql_markdown_copied\") });\n }).catch(() => {\n snackbar.open({ type: \"error\",\nmessage: t(\"studio_sql_markdown_copy_failed\") });\n });\n }, [tableData, snackbar, t]);\n\n // ─── Render ──────────────────────────────────────────────────\n\n return (\n <div className=\"flex flex-col h-full w-full\">\n {/* Main content */}\n <div className=\"flex-grow overflow-hidden\">\n <ResizablePanels\n orientation=\"horizontal\"\n panelSizePercent={sidebarSize}\n onPanelSizeChange={setSidebarSize}\n minPanelSizePx={200}\n firstPanel={\n <JSEditorSidebar\n collections={collectionInfos}\n snippets={snippets}\n history={history}\n onSelectSnippet={(code) => updateActiveCode(code)}\n onDeleteSnippet={deleteSnippet}\n onInsertCode={(code) => updateActiveCode(code)}\n />\n }\n secondPanel={\n <div className=\"flex flex-col h-full overflow-hidden\">\n {/* Toolbar: matching SQL Editor layout */}\n <div className={cls(\"flex items-center justify-between pr-2 border-b bg-surface-card\", defaultBorderMixin)}>\n <div className=\"flex items-center flex-grow overflow-hidden mr-4\">\n <div className=\"flex items-center no-scrollbar overflow-x-auto min-w-0\">\n <Tabs value={activeTabId} onValueChange={setActiveTabId} variant=\"boxy\" className=\"w-[unset] flex-shrink-0\" innerClassName=\"bg-surface-card\">\n {tabs.map(tab => (\n <Tab key={tab.id} value={tab.id} className=\"flex items-center justify-between group max-w-[200px]\">\n <TerminalIcon size={iconSize.smallest} className=\"text-amber-500 mr-1.5 flex-shrink-0\"/>\n <span className=\"truncate\">{tab.name}</span>\n {tabs.length > 1 && (\n <IconButton\n size=\"smallest\"\n onClick={(e) => { e.stopPropagation(); closeTab(tab.id); }}\n className=\"ml-1 !p-0.5 opacity-0 group-hover:opacity-100 hover:text-red-500 transition-opacity\"\n >\n <XIcon size={iconSize.smallest}/>\n </IconButton>\n )}\n </Tab>\n ))}\n </Tabs>\n <IconButton\n size=\"small\"\n onClick={addTab}\n className=\"ml-2 flex-shrink-0\"\n >\n <PlusIcon size={iconSize.smallest}/>\n </IconButton>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center justify-end gap-1.5\">\n <Tooltip title=\"Save as snippet\">\n <IconButton aria-label=\"Save as snippet\"\n size=\"small\"\n onClick={() => {\n setSnippetName(\"\");\n setShowSaveDialog(true);\n }}\n disabled={!activeTab?.code.trim()}\n >\n <SaveIcon size={iconSize.smallest}/>\n </IconButton>\n </Tooltip>\n\n {result?.value != null && (\n <Tooltip title=\"Export result as JSON\">\n <IconButton aria-label=\"Export result as JSON\" size=\"small\" onClick={exportResult}>\n <DownloadIcon size={iconSize.smallest}/>\n </IconButton>\n </Tooltip>\n )}\n\n <Button\n size=\"small\"\n color=\"primary\"\n disabled={isRunning || !activeTab?.code.trim()}\n onClick={() => executeCode()}\n >\n {isRunning ? <CircularProgress size=\"smallest\" className=\"mr-2\"/> : <PlayIcon size={iconSize.smallest} className=\"mr-2\"/>}\n Run\n </Button>\n </div>\n </div>\n\n {/* Editor + Results split */}\n <div className=\"flex-grow overflow-hidden\">\n <ResizablePanels\n orientation=\"vertical\"\n panelSizePercent={editorHeight}\n onPanelSizeChange={setEditorHeight}\n minPanelSizePx={100}\n firstPanel={\n <div className=\"h-full w-full overflow-hidden flex flex-col\">\n {/* Auth Simulation UI */}\n <div className=\"p-2 px-3 border-b border-hairline bg-surface-sheet flex items-center shrink-0\">\n <AuthSimulationSelector\n authMode={authMode}\n setAuthMode={setAuthMode}\n selectedUser={selectedUser}\n setSelectedUser={setSelectedUser}\n users={users}\n loading={usersLoading}\n onUserSearchTextChange={onUserSearchTextChange}\n userTotalCount={userTotalCount}\n currentUser={currentSelectableUser}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <JSMonacoEditor\n value={activeTab?.code ?? \"\"}\n onChange={updateActiveCode}\n onRun={(selectedText) => executeCode(selectedText)}\n collectionSlugs={collectionSlugs}\n collections={collectionInfos}\n autoFocus\n />\n </div>\n </div>\n }\n secondPanel={\n <div className=\"h-full w-full flex flex-col bg-surface-well overflow-hidden min-h-0\">\n {/* Result header — matches SQL editor */}\n <div className={cls(\"p-2 px-4 bg-surface-raised border-b shrink-0 flex items-center\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold text-text-disabled dark:text-text-disabled-dark uppercase tracking-widest text-[10px]\">\n {t(\"studio_sql_query_results\")}\n </Typography>\n\n {result && (\n <>\n <div className=\"flex-grow\"/>\n\n <Tabs value={resultView} onValueChange={(val) => setResultView(val as \"json\" | \"table\" | \"console\")} variant=\"pill\" className=\"w-[unset] mr-2\">\n <Tab value=\"json\">JSON</Tab>\n {tableData.data.length > 0 && <Tab value=\"table\">Table</Tab>}\n {result.console.length > 0 && <Tab value=\"console\">Console ({result.console.length})</Tab>}\n </Tabs>\n\n <Chip size=\"smallest\" colorScheme={result.error ? \"redDarker\" : \"greenDarker\"}>\n {result.error ? \"Error\" : `${result.duration.toFixed(0)}ms`}\n </Chip>\n </>\n )}\n </div>\n\n {/* Result content */}\n <div className=\"flex-grow flex flex-col min-h-0 overflow-hidden\">\n {isRunning && (\n <div className=\"flex-grow flex items-center justify-center\">\n <div className=\"text-center\">\n <CircularProgress size=\"medium\"/>\n <Typography variant=\"body2\" className=\"mt-4 text-text-secondary dark:text-text-secondary-dark font-mono tracking-tight animate-pulse\">\n EXECUTING SCRIPT...\n </Typography>\n </div>\n </div>\n )}\n\n {!isRunning && !result && (\n <div className=\"flex-grow flex items-center justify-center text-text-disabled dark:text-text-disabled-dark\">\n <div className=\"text-center\">\n <svg className=\"w-12 h-12 mx-auto mb-4 opacity-50\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\"><path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1} d=\"M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"/></svg>\n <Typography variant=\"body2\">Write JavaScript and press <kbd className=\"px-1.5 py-0.5 rounded bg-surface-raised text-[11px] font-mono\">⌘ Enter</kbd> to run</Typography>\n </div>\n </div>\n )}\n\n {!isRunning && result?.error && (\n <div className=\"flex-grow flex items-center justify-center p-6 overflow-auto\">\n <ErrorView\n title=\"Execution Error\"\n error={result.error}\n />\n </div>\n )}\n\n {!isRunning && result && !result.error && resultView === \"json\" && (\n <pre className=\"flex-grow overflow-auto p-4 text-[13px] font-mono leading-relaxed text-text-primary dark:text-text-primary-dark whitespace-pre-wrap break-words\">\n {result.value === undefined ? (\n <span className=\"text-text-disabled italic\">undefined (no return value)</span>\n ) : (\n <JSONHighlight value={result.value}/>\n )}\n </pre>\n )}\n\n {!isRunning && result && !result.error && resultView === \"table\" && tableData.data.length > 0 && (\n <div className=\"flex-grow flex flex-col overflow-hidden min-h-0\">\n {/* Collection badges bar — matching SQL editor */}\n {matchedCollections.length > 0 && (\n <div className={cls(\"px-4 py-1.5 border-b flex items-center gap-2 shrink-0 bg-surface-sheet\", defaultBorderMixin)}>\n <Tooltip title={t(\"studio_sql_admin_collections_tooltip\")}>\n <Typography variant=\"caption\" className=\"text-[10px] font-semibold uppercase tracking-widest text-text-disabled dark:text-text-disabled-dark mr-1 shrink-0 cursor-help\">{t(\"studio_sql_collections_label\")}</Typography>\n </Tooltip>\n <div className=\"flex items-center gap-1.5 overflow-x-auto no-scrollbar\">\n {matchedCollections.map(mc => (\n <Tooltip key={mc.collectionSlug} title={`${mc.collection.name} (${mc.collectionSlug})`}>\n <span className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium bg-primary/10 dark:bg-primary-dark/15 text-primary dark:text-primary-dark whitespace-nowrap border border-primary/20 dark:border-primary-dark/20\">\n {typeof mc.collection.icon === \"string\" && (\n <IconForView collectionOrView={{ icon: mc.collection.icon } as never} className=\"text-[12px]\"/>\n )}\n {mc.collection.name}\n </span>\n </Tooltip>\n ))}\n </div>\n </div>\n )}\n <div className=\"flex-grow relative h-full min-h-0 min-w-0\">\n <VirtualTable\n data={tableData.data}\n columns={\n matchedCollections.length > 0\n ? [{ key: \"__entity_action__\",\ntitle: \"\",\nwidth: 36,\nsortable: false,\nresizable: false }, ...tableData.columns]\n : tableData.columns\n }\n rowHeight={32}\n headerHeight={32}\n cellRenderer={({ rowData, column, rowIndex }: CellRendererParams<Record<string, unknown>>) => {\n // Entity action column\n if (column.key === \"__entity_action__\") {\n const rowActions = getRowEntityActions(rowData ?? {});\n if (rowActions.length === 0) return <div className=\"h-full w-full\"/>;\n if (rowActions.length === 1) {\n const ra = rowActions[0];\n return (\n <div className=\"h-full flex items-center justify-center\">\n <Tooltip title={t(\"studio_sql_edit_entity\", { name: ra.collection.collection.name,\nid: String(ra.entityId) })}>\n <IconButton aria-label={t(\"studio_sql_edit_entity\", { name: ra.collection.collection.name, id: String(ra.entityId) })}\n size=\"small\"\n className=\"text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300\"\n onClick={(e) => {\n e.stopPropagation();\n sidePanelController.open({\n path: ra.collection.collectionSlug,\n entityId: ra.entityId,\n collection: ra.collection.collection,\n updateUrl: false\n });\n }}\n >\n <PencilIcon size={iconSize.smallest}/>\n </IconButton>\n </Tooltip>\n </div>\n );\n }\n // Multiple matched collections\n return (\n <div className=\"h-full flex items-center justify-center\">\n <Menu\n trigger={\n <IconButton\n size={\"small\"}\n className=\"text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300\"\n onClick={(e) => e.stopPropagation()}\n >\n <MoreVerticalIcon size={iconSize.smallest}/>\n </IconButton>\n }\n >\n {rowActions.map(ra => (\n <MenuItem\n key={ra.collection.collectionSlug}\n dense\n onClick={() => {\n sidePanelController.open({\n path: ra.collection.collectionSlug,\n entityId: ra.entityId,\n collection: ra.collection.collection,\n updateUrl: false\n });\n }}\n >\n {t(\"studio_sql_edit_entity\", { name: ra.collection.collection.name,\nid: String(ra.entityId) })}\n </MenuItem>\n ))}\n </Menu>\n </div>\n );\n }\n\n // Regular data cell\n if (!rowData) return null;\n const val = rowData[column.key];\n const displayValue = typeof val === \"object\" && val !== null ? JSON.stringify(val) : String(val ?? \"\");\n return (\n <div 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\">\n <div className=\"truncate flex-grow\" title={displayValue}>\n {displayValue === \"\" ? <span className=\"text-text-disabled dark:text-text-disabled-dark italic text-[11px]\">NULL</span> : displayValue}\n </div>\n </div>\n );\n }}\n />\n </div>\n </div>\n )}\n\n {!isRunning && result && resultView === \"console\" && (\n <div className=\"flex-grow overflow-auto p-2 space-y-1 font-mono text-[12px]\">\n {result.console.length === 0 ? (\n <Typography variant=\"caption\" className=\"text-text-disabled p-2\">No console output</Typography>\n ) : (\n result.console.map((entry, i) => (\n <div\n key={i}\n className={cls(\n \"px-2 py-1 rounded flex items-start gap-2\",\n entry.type === \"error\" && \"bg-red-50 dark:bg-red-950/30 text-red-700 dark:text-red-400\",\n entry.type === \"warn\" && \"bg-amber-50 dark:bg-amber-950/30 text-amber-700 dark:text-amber-400\",\n entry.type === \"log\" && \"text-text-primary dark:text-text-primary-dark\",\n entry.type === \"info\" && \"text-blue-700 dark:text-blue-400\"\n )}\n >\n <span className=\"text-[10px] opacity-50 flex-shrink-0 mt-0.5\">\n {entry.type === \"error\" ? \"❌\" : entry.type === \"warn\" ? \"⚠️\" : entry.type === \"info\" ? \"ℹ️\" : \"›\"}\n </span>\n <span className=\"whitespace-pre-wrap break-words\">\n {entry.args.map(a => typeof a === \"object\" ? JSON.stringify(a, null, 2) : String(a)).join(\" \")}\n </span>\n </div>\n ))\n )}\n\n {result.value !== undefined && (\n <div className={cls(\"px-2 py-1 mt-2 border-t pt-2\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"text-text-disabled text-[10px] uppercase tracking-wider mb-1 block\">Return Value</Typography>\n <pre className=\"text-text-primary dark:text-text-primary-dark whitespace-pre-wrap break-words text-[12px]\">\n {formatJSON(result.value)}\n </pre>\n </div>\n )}\n </div>\n )}\n </div>\n\n {/* Footer bar — matching SQL editor */}\n {!isRunning && result && !result.error && resultView === \"table\" && tableData.data.length > 0 && (\n <div className={cls(\"p-2 px-4 border-t bg-surface-sheet flex justify-between items-center shrink-0\", defaultBorderMixin)}>\n <div className=\"flex space-x-4\">\n <div className=\"flex items-center text-[11px]\">\n <span className=\"font-semibold text-text-disabled dark:text-text-disabled-dark mr-2 uppercase tracking-tighter\">{t(\"studio_sql_rows\")}</span>\n <span className=\"font-mono text-text-secondary dark:text-text-secondary-dark\">{tableData.data.length}</span>\n </div>\n <div className=\"flex items-center text-[11px]\">\n <span className=\"font-semibold text-text-disabled dark:text-text-disabled-dark mr-2 uppercase tracking-tighter\">{t(\"studio_sql_time\")}</span>\n <span className=\"font-mono text-text-secondary dark:text-text-secondary-dark\">{result.duration.toFixed(0)}ms</span>\n </div>\n </div>\n <div className=\"flex gap-2 overflow-x-auto no-scrollbar items-center px-2\">\n <Button\n size=\"small\"\n variant=\"text\"\n className=\"text-[10px] uppercase font-semibold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap\"\n onClick={handleExportMarkdown}\n >\n {t(\"studio_sql_copy_markdown\")}\n </Button>\n <Button\n size=\"small\"\n variant=\"text\"\n className=\"text-[10px] uppercase font-semibold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap\"\n onClick={exportResult}\n >\n {t(\"studio_sql_export_json\")}\n </Button>\n <Button\n size=\"small\"\n variant=\"text\"\n className=\"text-[10px] uppercase font-semibold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap\"\n onClick={handleExportCSV}\n >\n {t(\"studio_sql_export_csv\")}\n </Button>\n </div>\n </div>\n )}\n </div>\n }\n />\n </div>\n </div>\n }\n />\n </div>\n\n {/* Save snippet dialog */}\n <Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>\n <DialogTitle>Save Snippet</DialogTitle>\n <DialogContent>\n <TextField\n label=\"Snippet name\"\n value={snippetName}\n onChange={(e) => setSnippetName(e.target.value)}\n placeholder=\"e.g. List all products\"\n autoFocus\n onKeyDown={(e) => {\n if (e.key === \"Enter\") saveSnippet();\n }}\n />\n </DialogContent>\n <DialogActions>\n <Button variant=\"text\" onClick={() => setShowSaveDialog(false)}>Cancel</Button>\n <Button variant=\"filled\" color=\"primary\" onClick={saveSnippet} disabled={!snippetName.trim()}>Save</Button>\n </DialogActions>\n </Dialog>\n </div>\n );\n}\n\n// ─── JSON Syntax Highlighting ────────────────────────────────────────\n\nfunction JSONHighlight({ value }: { value: unknown }) {\n const json = formatJSON(value);\n const { mode } = useModeController();\n\n return (\n <Highlight\n theme={mode === \"dark\" ? themes.vsDark : themes.github}\n code={json}\n language=\"json\"\n >\n {({ style, tokens, getLineProps, getTokenProps }) => (\n <span style={{ ...style,\nbackgroundColor: \"transparent\" }}>\n {tokens.map((line, i) => (\n <div key={i} {...getLineProps({ line })}>\n {line.map((token, key) => (\n <span key={key} {...getTokenProps({ token })}/>\n ))}\n </div>\n ))}\n </span>\n )}\n </Highlight>\n );\n}\n"],"mappings":"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"}
|
|
@@ -107,6 +107,8 @@ function useLogTail(filters, limit = 200) {
|
|
|
107
107
|
const [error, setError] = useState(null);
|
|
108
108
|
const [transport, setTransport] = useState("connecting");
|
|
109
109
|
const [dropped, setDropped] = useState(0);
|
|
110
|
+
const [instance, setInstance] = useState(null);
|
|
111
|
+
const instanceRef = useRef(null);
|
|
110
112
|
const apiConfig = useApiConfig();
|
|
111
113
|
const apiBase = useApiBase();
|
|
112
114
|
const getAuthToken = apiConfig?.getAuthToken;
|
|
@@ -171,6 +173,8 @@ function useLogTail(filters, limit = 200) {
|
|
|
171
173
|
const startPolling = () => {
|
|
172
174
|
if (cancelled) return;
|
|
173
175
|
setTransport("polling");
|
|
176
|
+
instanceRef.current = null;
|
|
177
|
+
setInstance(null);
|
|
174
178
|
poll();
|
|
175
179
|
};
|
|
176
180
|
const connect = async () => {
|
|
@@ -201,8 +205,13 @@ function useLogTail(filters, limit = 200) {
|
|
|
201
205
|
for await (const frame of readSSEFrames(resp.body)) {
|
|
202
206
|
if (cancelled) return;
|
|
203
207
|
if (frame.event === "snapshot") {
|
|
204
|
-
const
|
|
205
|
-
|
|
208
|
+
const parsed = JSON.parse(frame.data);
|
|
209
|
+
const entries = parsed.entries;
|
|
210
|
+
const from = typeof parsed.instance === "string" && parsed.instance ? parsed.instance : null;
|
|
211
|
+
const sameSource = from === instanceRef.current;
|
|
212
|
+
instanceRef.current = from;
|
|
213
|
+
setInstance(from);
|
|
214
|
+
setLogs((prev) => sameSource && sameLogs(prev, entries) ? prev : capped(entries));
|
|
206
215
|
} else if (frame.event === "append") {
|
|
207
216
|
const parsed = JSON.parse(frame.data);
|
|
208
217
|
if (parsed.entries.length > 0) setLogs((prev) => capped([...prev, ...parsed.entries]));
|
|
@@ -238,7 +247,8 @@ function useLogTail(filters, limit = 200) {
|
|
|
238
247
|
logs,
|
|
239
248
|
error,
|
|
240
249
|
transport,
|
|
241
|
-
dropped
|
|
250
|
+
dropped,
|
|
251
|
+
instance
|
|
242
252
|
};
|
|
243
253
|
}
|
|
244
254
|
//#endregion
|
|
@@ -268,9 +278,10 @@ var TRANSPORT_DOTS = {
|
|
|
268
278
|
};
|
|
269
279
|
var TRANSPORT_TITLES = {
|
|
270
280
|
connecting: "Reconnecting to the log stream.",
|
|
271
|
-
live: "Streaming — entries appear as
|
|
281
|
+
live: "Streaming — entries appear as this instance writes them.",
|
|
272
282
|
polling: "This server has no log stream; falling back to a refresh every 3 seconds."
|
|
273
283
|
};
|
|
284
|
+
var INSTANCE_TITLE = "Each server process keeps its own log in memory. With more than one instance, you see only the one serving this view, and a reconnect can land on another.";
|
|
274
285
|
var STICK_THRESHOLD = 40;
|
|
275
286
|
function LogsExplorer() {
|
|
276
287
|
const [level, setLevel] = useState("all");
|
|
@@ -287,17 +298,19 @@ function LogsExplorer() {
|
|
|
287
298
|
const t = setTimeout(() => setSearch(searchInput), 300);
|
|
288
299
|
return () => clearTimeout(t);
|
|
289
300
|
}, [searchInput]);
|
|
290
|
-
const { logs, error, transport, dropped } = useLogTail({
|
|
301
|
+
const { logs, error, transport, dropped, instance } = useLogTail({
|
|
291
302
|
level,
|
|
292
303
|
source,
|
|
293
304
|
search
|
|
294
305
|
});
|
|
295
306
|
useEffect(() => {
|
|
296
307
|
setNewCount(0);
|
|
308
|
+
lastMaxIdRef.current = null;
|
|
297
309
|
}, [
|
|
298
310
|
level,
|
|
299
311
|
source,
|
|
300
|
-
search
|
|
312
|
+
search,
|
|
313
|
+
instance
|
|
301
314
|
]);
|
|
302
315
|
useEffect(() => {
|
|
303
316
|
const max = logs.length > 0 ? idNum(logs[logs.length - 1]) : null;
|
|
@@ -423,22 +436,35 @@ function LogsExplorer() {
|
|
|
423
436
|
}),
|
|
424
437
|
/* @__PURE__ */ jsxs("div", {
|
|
425
438
|
className: "ml-auto pl-4 flex items-center gap-3",
|
|
426
|
-
children: [
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
|
|
439
|
+
children: [
|
|
440
|
+
/* @__PURE__ */ jsxs("div", {
|
|
441
|
+
className: "flex items-center gap-1.5",
|
|
442
|
+
title: TRANSPORT_TITLES[transport],
|
|
443
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
444
|
+
"aria-hidden": true,
|
|
445
|
+
className: cls("w-1.5 h-1.5 rounded-full shrink-0", TRANSPORT_DOTS[transport])
|
|
446
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
447
|
+
variant: "caption",
|
|
448
|
+
color: "secondary",
|
|
449
|
+
children: TRANSPORT_LABELS[transport]
|
|
450
|
+
})]
|
|
451
|
+
}),
|
|
452
|
+
/* @__PURE__ */ jsx("div", {
|
|
453
|
+
className: "min-w-0",
|
|
454
|
+
title: INSTANCE_TITLE,
|
|
455
|
+
children: /* @__PURE__ */ jsx(Typography, {
|
|
456
|
+
variant: "caption",
|
|
457
|
+
color: "secondary",
|
|
458
|
+
className: "block truncate max-w-[18rem]",
|
|
459
|
+
children: instance ? `This instance: ${instance}` : "This instance only"
|
|
460
|
+
})
|
|
461
|
+
}),
|
|
462
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
433
463
|
variant: "caption",
|
|
434
464
|
color: "secondary",
|
|
435
|
-
children:
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
variant: "caption",
|
|
439
|
-
color: "secondary",
|
|
440
|
-
children: [logs.length, " entries"]
|
|
441
|
-
})]
|
|
465
|
+
children: [logs.length, " entries"]
|
|
466
|
+
})
|
|
467
|
+
]
|
|
442
468
|
})
|
|
443
469
|
]
|
|
444
470
|
}),
|
|
@@ -504,7 +530,7 @@ function LogsExplorer() {
|
|
|
504
530
|
variant: "body2",
|
|
505
531
|
className: error ? "text-red-600 dark:text-red-500" : void 0,
|
|
506
532
|
color: error ? void 0 : "secondary",
|
|
507
|
-
children: error ?? "No log entries yet. Logs will appear here as
|
|
533
|
+
children: error ?? "No log entries yet. Logs will appear here as this instance serves requests."
|
|
508
534
|
})
|
|
509
535
|
})]
|
|
510
536
|
}), autoScroll && !atBottom && newCount > 0 && /* @__PURE__ */ jsxs("button", {
|
|
@@ -524,4 +550,4 @@ function LogsExplorer() {
|
|
|
524
550
|
//#endregion
|
|
525
551
|
export { LogsExplorer };
|
|
526
552
|
|
|
527
|
-
//# sourceMappingURL=LogsExplorer-
|
|
553
|
+
//# sourceMappingURL=LogsExplorer-BKM4yfP8.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LogsExplorer-BKM4yfP8.js","names":[],"sources":["../src/components/LogsExplorer/useLogTail.ts","../src/components/LogsExplorer/LogsExplorer.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { useApiBase, useApiConfig } from \"@rebasepro/app\";\n\nexport interface 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\nexport interface LogTailFilters {\n /** `\"all\"` and `undefined` both mean unfiltered. */\n level?: string;\n source?: string;\n search?: string;\n}\n\n/**\n * How the view is currently being fed.\n *\n * Surfaced rather than kept private: \"connecting\" and \"polling\" look identical\n * from the outside until something is wrong, and the first question about a log\n * view that seems stuck is whether it is actually live.\n */\nexport type LogTransport = \"connecting\" | \"live\" | \"polling\";\n\n/**\n * Entries kept in the browser.\n *\n * The server window is 200; a tail that never forgets would grow without bound\n * on a busy backend, since nothing else here ever drops an entry.\n */\nconst MAX_ENTRIES = 1000;\n\n/** Fallback cadence, used only against a server with no stream route. */\nconst POLL_MS = 3000;\n\nconst RETRY_BASE_MS = 1000;\nconst RETRY_MAX_MS = 15_000;\n\n/** Ids are `log_<n>` off a monotonic counter, so numeric order is push order. */\nexport const idNum = (entry: LogEntry): number => Number(entry.id.slice(\"log_\".length));\n\n/** The window is contiguous and ordered, so identical ends mean identical content. */\nexport const 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/** One identity for \"nothing\", so clearing an already-empty view is not a change. */\nconst EMPTY: LogEntry[] = [];\n\nconst capped = (entries: LogEntry[]): LogEntry[] =>\n entries.length > MAX_ENTRIES ? entries.slice(entries.length - MAX_ENTRIES) : entries;\n\nexport interface SSEFrame {\n event: string;\n data: string;\n}\n\n/**\n * Split an SSE byte stream into frames.\n *\n * `EventSource` would do this, and cannot be used: logs are admin-only, and\n * `EventSource` has no way to send an `Authorization` header. So the stream is\n * read off `fetch` and framed here.\n *\n * Exported for its own test — the parser is the part with edge cases (a frame\n * split across two chunks, multi-line data, comment keepalives).\n */\nexport async function* readSSEFrames(\n body: ReadableStream<Uint8Array>\n): AsyncGenerator<SSEFrame> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n // `stream: true` keeps a multi-byte character split across two chunks\n // from decoding as two replacement characters.\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, \"\\n\");\n\n let boundary = buffer.indexOf(\"\\n\\n\");\n while (boundary !== -1) {\n const raw = buffer.slice(0, boundary);\n buffer = buffer.slice(boundary + 2);\n const frame = parseFrame(raw);\n if (frame) yield frame;\n boundary = buffer.indexOf(\"\\n\\n\");\n }\n }\n}\n\nfunction parseFrame(raw: string): SSEFrame | null {\n let event = \"message\";\n const data: string[] = [];\n for (const line of raw.split(\"\\n\")) {\n // A line starting with \":\" is a comment. The server sends them as\n // keepalives, and they carry nothing.\n if (!line || line.startsWith(\":\")) continue;\n const colon = line.indexOf(\":\");\n const field = colon === -1 ? line : line.slice(0, colon);\n const value = colon === -1 ? \"\" : line.slice(colon + 1).replace(/^ /, \"\");\n if (field === \"event\") event = value;\n else if (field === \"data\") data.push(value);\n }\n if (data.length === 0) return null;\n return { event,\n data: data.join(\"\\n\") };\n}\n\nconst buildParams = (filters: LogTailFilters, limit: number): string => {\n const params = new URLSearchParams();\n if (filters.level && filters.level !== \"all\") params.set(\"level\", filters.level);\n if (filters.source && filters.source !== \"all\") params.set(\"source\", filters.source);\n if (filters.search) params.set(\"search\", filters.search);\n params.set(\"limit\", String(limit));\n return params.toString();\n};\n\n/**\n * Why the log is not being served, in the server's words where it has any.\n *\n * The admin gate answers 501 with a message naming the setting to change (a\n * backend with no `jwtSecret` and no auth adapter cannot tell an admin from the\n * internet, so it refuses rather than opens). Rendering \"HTTP 501\" over the top\n * of that would throw away the only sentence that says what to do.\n */\nasync function failureMessage(resp: { status: number; json?: () => Promise<unknown> }): Promise<string> {\n try {\n const body = await resp.json?.() as { error?: { message?: string } } | undefined;\n const message = body?.error?.message;\n if (typeof message === \"string\" && message) return message;\n } catch {\n /* not every failure has a JSON body */\n }\n return 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}\n\nexport interface LogTail {\n logs: LogEntry[];\n error: string | null;\n transport: LogTransport;\n /**\n * Entries the server had to discard because this client was not draining the\n * stream fast enough, since the connection opened.\n *\n * Surfaced rather than swallowed. The server bounds what it will hold per\n * connection, so under a heavy enough burst there is a hole in the tail — and\n * a tail with a hole in it that says so beats one that silently lies.\n */\n dropped: number;\n /**\n * The server process whose log this is, as its stream names it — `null`\n * until the stream says, and against a server that does not.\n *\n * Each process keeps its own log in memory, so with several replicas (or\n * split runtime roles) this view shows one of them, and a reconnect can\n * land on another.\n */\n instance: string | null;\n}\n\n/**\n * Tail the server's log buffer.\n *\n * Server-sent events, with the 3s poll kept only as a fallback: the studio and\n * the server are versioned and deployed separately, so a frontend that knows\n * about `/logs/stream` will meet servers that do not. A 404 there is not an\n * error, it is an older backend, and it degrades to what that backend supports\n * instead of showing an empty view.\n *\n * The stream sends its own backlog as a `snapshot` frame before any `append`, so\n * a reconnect restores the window without a second request and without a gap\n * where entries logged mid-handshake would belong to neither call.\n */\nexport function useLogTail(filters: LogTailFilters, limit = 200): LogTail {\n const [logs, setLogs] = useState<LogEntry[]>([]);\n const [error, setError] = useState<string | null>(null);\n const [transport, setTransport] = useState<LogTransport>(\"connecting\");\n const [dropped, setDropped] = useState(0);\n const [instance, setInstance] = useState<string | null>(null);\n // Read inside the stream loop, which outlives renders.\n const instanceRef = useRef<string | null>(null);\n const apiConfig = useApiConfig();\n const apiBase = useApiBase();\n\n // Held in a ref, and deliberately not a dependency of the connection effect.\n // `apiConfig` is an object: a provider that forgets to memoize it hands us a\n // new identity per render, and as a dependency that is a reconnect per\n // render — a tail that hammers the server and never stays up. `apiBase` is a\n // string and compares by value, so it can be a dependency and covers the one\n // thing that actually has to reopen the connection.\n const getAuthToken = apiConfig?.getAuthToken;\n const getAuthTokenRef = useRef(getAuthToken);\n useEffect(() => {\n getAuthTokenRef.current = getAuthToken;\n });\n\n const { level, source, search } = filters;\n\n // A hidden tab is not watching. The connection is dropped rather than left\n // to accumulate, and the reconnect's snapshot rebuilds the window — so this\n // costs nothing but the entries nobody saw.\n const [visible, setVisible] = useState(\n typeof document === \"undefined\" || document.visibilityState === \"visible\"\n );\n useEffect(() => {\n const onChange = () => setVisible(document.visibilityState === \"visible\");\n document.addEventListener(\"visibilitychange\", onChange);\n return () => document.removeEventListener(\"visibilitychange\", onChange);\n }, []);\n\n // A filter change replaces the window wholesale. Clearing here rather than in\n // the connection effect means a reconnect (or a tab coming back) keeps what\n // is on screen until the new snapshot lands.\n //\n // Joined on NUL, written as the escape: the search box takes free text, so any\n // printable separator is a character a user can type, and two filters that\n // differ only either side of it would collide into one key and skip the clear.\n // As a raw byte it would make the whole file binary to grep, which reads as an\n // empty search result rather than as an error.\n const filterKey = `${level}\\u0000${source}\\u0000${search}`;\n // `EMPTY` rather than a fresh `[]` so the run on mount, when there is nothing\n // to clear, is not a state change and does not cost a render.\n useEffect(() => {\n setLogs(EMPTY);\n }, [filterKey]);\n\n useEffect(() => {\n if (!apiBase) {\n setError(\"No API URL configured — cannot load logs.\");\n return;\n }\n if (!visible) return;\n\n let cancelled = false;\n let controller: AbortController | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n let attempt = 0;\n\n const query = buildParams({ level,\n source,\n search }, limit);\n\n const authHeaders = async (): Promise<Record<string, string>> => {\n const headers: Record<string, string> = {};\n const token = await getAuthTokenRef.current?.() ?? null;\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\n return headers;\n };\n\n const retryLater = () => {\n if (cancelled) return;\n const delay = Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_MAX_MS);\n attempt++;\n timer = setTimeout(connect, delay);\n };\n\n /** The fallback. Only ever reached against a server with no stream route. */\n const poll = async () => {\n if (cancelled) return;\n try {\n const resp = await fetch(`${apiBase}/logs?${query}`, { headers: await authHeaders() });\n if (cancelled) return;\n if (!resp.ok) {\n setError(await failureMessage(resp));\n } else {\n const data: { entries?: LogEntry[] } = await resp.json();\n // The query returns newest-first; the view tails like a\n // terminal, so flip to chronological order.\n const entries = (data.entries || []).slice().reverse();\n setLogs(prev => sameLogs(prev, entries) ? prev : entries);\n setError(null);\n }\n } catch (e) {\n if (!cancelled) setError(e instanceof Error ? e.message : \"Could not load logs.\");\n }\n if (!cancelled) timer = setTimeout(poll, POLL_MS);\n };\n\n const startPolling = () => {\n if (cancelled) return;\n setTransport(\"polling\");\n // The query route does not say whose log it is.\n instanceRef.current = null;\n setInstance(null);\n poll();\n };\n\n const connect = async () => {\n if (cancelled) return;\n controller = new AbortController();\n try {\n const resp = await fetch(`${apiBase}/logs/stream?${query}`, {\n headers: { ...await authHeaders(),\n Accept: \"text/event-stream\" },\n signal: controller.signal\n });\n if (cancelled) return;\n\n // 404 is an older server, not a failure — it has the query route\n // but not the stream. Same for a body-less response, which is a\n // client with no streaming support at all.\n if (resp.status === 404 || !resp.body) {\n startPolling();\n return;\n }\n if (!resp.ok) {\n setError(await failureMessage(resp));\n retryLater();\n return;\n }\n\n attempt = 0;\n setError(null);\n setTransport(\"live\");\n // Per-connection: the server's counter resets with the socket, so\n // a stale total would outlive the gap it described.\n setDropped(0);\n\n for await (const frame of readSSEFrames(resp.body)) {\n if (cancelled) return;\n if (frame.event === \"snapshot\") {\n const parsed = JSON.parse(frame.data) as { entries: LogEntry[]; instance?: string };\n const entries = parsed.entries;\n const from = typeof parsed.instance === \"string\" && parsed.instance ? parsed.instance : null;\n // Ids are per process: another instance's window can\n // share its first and last id with this one's.\n const sameSource = from === instanceRef.current;\n instanceRef.current = from;\n setInstance(from);\n setLogs(prev => sameSource && sameLogs(prev, entries) ? prev : capped(entries));\n } else if (frame.event === \"append\") {\n const parsed = JSON.parse(frame.data) as { entries: LogEntry[]; dropped?: number };\n if (parsed.entries.length > 0) {\n setLogs(prev => capped([...prev, ...parsed.entries]));\n }\n if (parsed.dropped) setDropped(n => n + parsed.dropped!);\n } else if (frame.event === \"error\") {\n setError(frame.data);\n }\n }\n\n // The stream ended on its own — a restarted or redeployed\n // server. Reconnecting is the whole point of the backoff.\n if (!cancelled) {\n setTransport(\"connecting\");\n retryLater();\n }\n } catch (e) {\n if (cancelled || (e instanceof Error && e.name === \"AbortError\")) return;\n setTransport(\"connecting\");\n setError(e instanceof Error ? e.message : \"Could not load logs.\");\n retryLater();\n }\n };\n\n connect();\n\n return () => {\n cancelled = true;\n if (timer) clearTimeout(timer);\n controller?.abort();\n };\n }, [level, source, search, limit, apiBase, visible]);\n\n return { logs,\n error,\n transport,\n dropped,\n instance };\n}\n","import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { ArrowDownToLineIcon, Checkbox, cls, defaultBorderMixin, Label, Select, SelectItem, TextField, Typography } from \"@rebasepro/ui\";\nimport { idNum, useLogTail } from \"./useLogTail\";\nimport type { LogTransport } from \"./useLogTail\";\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\nconst TRANSPORT_LABELS: Record<LogTransport, string> = {\n connecting: \"Connecting\",\n live: \"Live\",\n polling: \"Polling\"\n};\n\nconst TRANSPORT_DOTS: Record<LogTransport, string> = {\n connecting: \"bg-amber-500\",\n live: \"bg-green-500\",\n polling: \"bg-surface-400\"\n};\n\nconst TRANSPORT_TITLES: Record<LogTransport, string> = {\n connecting: \"Reconnecting to the log stream.\",\n live: \"Streaming — entries appear as this instance writes them.\",\n polling: \"This server has no log stream; falling back to a refresh every 3 seconds.\"\n};\n\n// The log lives in each server process's memory and nothing gathers the others,\n// so the view says whose it is.\nconst INSTANCE_TITLE = \"Each server process keeps its own log in memory. With more than one instance, \"\n + \"you see only the one serving this view, and a reconnect can land on another.\";\n\n// How close to the bottom edge still counts as \"at the bottom\".\nconst STICK_THRESHOLD = 40;\n\nexport function LogsExplorer() {\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 // 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 entry-counting effect both read it synchronously.\n const stickRef = useRef(true);\n const lastMaxIdRef = useRef<number | null>(null);\n\n // Debounce the search box so typing doesn't reopen the stream per keystroke.\n useEffect(() => {\n const t = setTimeout(() => setSearch(searchInput), 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const { logs, error, transport, dropped, instance } = useLogTail({ level,\n source,\n search });\n\n // A filter change replaces the window wholesale — \"new since you looked\n // away\" stops meaning anything, so the counter starts over. So does a\n // reconnect that lands on another instance: its ids come from its own\n // counter, and comparing them with this one's counts nothing true.\n useEffect(() => {\n setNewCount(0);\n lastMaxIdRef.current = null;\n }, [level, source, search, instance]);\n\n // Count what arrived while the user was reading further up. Driven off the\n // rendered window rather than off the transport, so it means the same thing\n // whether the entries were streamed or polled.\n useEffect(() => {\n const max = logs.length > 0 ? idNum(logs[logs.length - 1]) : null;\n const prevMax = lastMaxIdRef.current;\n if (prevMax != null && !stickRef.current) {\n const fresh = logs.filter(e => idNum(e) > prevMax).length;\n if (fresh > 0) setNewCount(c => c + fresh);\n }\n lastMaxIdRef.current = max;\n }, [logs]);\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-sheet\">\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 aria-label=\"Search logs\"\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 {/* Whether the tail is actually attached. A log view with\n nothing in it is ambiguous — quiet server, or a stream that\n dropped — and this is the difference. */}\n <div className=\"ml-auto pl-4 flex items-center gap-3\">\n <div className=\"flex items-center gap-1.5\" title={TRANSPORT_TITLES[transport]}>\n <span\n aria-hidden\n className={cls(\"w-1.5 h-1.5 rounded-full shrink-0\", TRANSPORT_DOTS[transport])}\n />\n <Typography variant=\"caption\" color=\"secondary\">\n {TRANSPORT_LABELS[transport]}\n </Typography>\n </div>\n <div className=\"min-w-0\" title={INSTANCE_TITLE}>\n <Typography variant=\"caption\" color=\"secondary\" className=\"block truncate max-w-[18rem]\">\n {instance ? `This instance: ${instance}` : \"This instance only\"}\n </Typography>\n </div>\n <Typography variant=\"caption\" color=\"secondary\">\n {logs.length} entries\n </Typography>\n </div>\n </div>\n\n {/* A broken tail 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 {/* The server bounds what it holds per connection, so a burst big\n enough leaves a hole in what is on screen. Say so: a tail that\n silently skips entries is worse than one that admits it. */}\n {dropped > 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 {dropped} {dropped === 1 ? \"entry was\" : \"entries were\"} dropped — the log is\n arriving faster than this view can read it. Narrow the filter to keep up.\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-hover 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 this instance serves requests.\"}\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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"}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { useModeController } from "@rebasepro/app";
|
|
2
|
-
import { useRef } from "react";
|
|
2
|
+
import { useImperativeHandle, useRef } from "react";
|
|
3
3
|
import { cls } from "@rebasepro/ui";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
import Editor from "@monaco-editor/react";
|
|
6
6
|
//#region src/components/SQLEditor/MonacoEditor.tsx
|
|
7
|
-
|
|
7
|
+
/** The editor's selection, trimmed — `undefined` when there is none. */
|
|
8
|
+
function selectedTextOf(instance) {
|
|
9
|
+
const selection = instance.getSelection();
|
|
10
|
+
if (!selection || selection.isEmpty()) return void 0;
|
|
11
|
+
return instance.getModel()?.getValueInRange(selection)?.trim() || void 0;
|
|
12
|
+
}
|
|
13
|
+
var MonacoEditor = ({ ref, value, onChange, onRun, className, readOnly = false, autoFocus = true, schemas }) => {
|
|
8
14
|
const { mode } = useModeController();
|
|
9
15
|
const editorRef = useRef(null);
|
|
10
16
|
const monacoRef = useRef(null);
|
|
@@ -12,6 +18,7 @@ var MonacoEditor = ({ value, onChange, onRun, className, readOnly = false, autoF
|
|
|
12
18
|
onRunRef.current = onRun;
|
|
13
19
|
const schemasRef = useRef(schemas);
|
|
14
20
|
schemasRef.current = schemas;
|
|
21
|
+
useImperativeHandle(ref, () => ({ getSelectedText: () => editorRef.current ? selectedTextOf(editorRef.current) : void 0 }), []);
|
|
15
22
|
const handleEditorOnMount = (editor, monaco) => {
|
|
16
23
|
editorRef.current = editor;
|
|
17
24
|
monacoRef.current = monaco;
|
|
@@ -22,12 +29,7 @@ var MonacoEditor = ({ value, onChange, onRun, className, readOnly = false, autoF
|
|
|
22
29
|
contextMenuGroupId: "operation",
|
|
23
30
|
contextMenuOrder: 0,
|
|
24
31
|
run: () => {
|
|
25
|
-
|
|
26
|
-
const selection = editor.getSelection();
|
|
27
|
-
let selectedText = void 0;
|
|
28
|
-
if (selection && !selection.isEmpty()) selectedText = editor.getModel()?.getValueInRange(selection)?.trim();
|
|
29
|
-
onRunRef.current(selectedText || void 0);
|
|
30
|
-
}
|
|
32
|
+
onRunRef.current?.(selectedTextOf(editor));
|
|
31
33
|
}
|
|
32
34
|
});
|
|
33
35
|
const sqlKeywords = [
|
|
@@ -243,4 +245,4 @@ var MonacoEditor = ({ value, onChange, onRun, className, readOnly = false, autoF
|
|
|
243
245
|
//#endregion
|
|
244
246
|
export { MonacoEditor as t };
|
|
245
247
|
|
|
246
|
-
//# sourceMappingURL=MonacoEditor-
|
|
248
|
+
//# sourceMappingURL=MonacoEditor-CUxy5_k5.js.map
|