@rebasepro/studio 0.9.1-canary.fd3754b → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/{ApiKeysView-Dbe5cXkS.js → ApiKeysView-vQKqt2wl.js} +44 -2
  2. package/dist/ApiKeysView-vQKqt2wl.js.map +1 -0
  3. package/dist/{JSEditor-Etgw9-Cw.js → JSEditor-BeHHuxhA.js} +3 -3
  4. package/dist/{JSEditor-Etgw9-Cw.js.map → JSEditor-BeHHuxhA.js.map} +1 -1
  5. package/dist/LogsExplorer-BCXoGREr.js +298 -0
  6. package/dist/LogsExplorer-BCXoGREr.js.map +1 -0
  7. package/dist/{RLSEditor-Cgv_kUVW.js → RLSEditor-rLlMcZPG.js} +42 -28
  8. package/dist/RLSEditor-rLlMcZPG.js.map +1 -0
  9. package/dist/{SQLEditor-ZZsy1Mk6.js → SQLEditor-_dCqVAN_.js} +3 -3
  10. package/dist/SQLEditor-_dCqVAN_.js.map +1 -0
  11. package/dist/{SchemaVisualizer-D01DzU-f.js → SchemaVisualizer-C_ErehfH.js} +2 -2
  12. package/dist/{SchemaVisualizer-D01DzU-f.js.map → SchemaVisualizer-C_ErehfH.js.map} +1 -1
  13. package/dist/components/RLSEditor/PolicyEditor.d.ts +1 -1
  14. package/dist/components/SQLEditor/SQLEditor.d.ts +2 -10
  15. package/dist/components/SQLEditor/SQLEditorSidebar.d.ts +1 -1
  16. package/dist/components/SQLEditor/SchemaBrowser.d.ts +1 -1
  17. package/dist/components/SQLEditor/sql_editor_types.d.ts +16 -0
  18. package/dist/index.es.js +58 -105
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/utils/sql_utils.d.ts +1 -1
  21. package/package.json +13 -12
  22. package/src/components/ApiKeys/ApiKeysView.tsx +41 -1
  23. package/src/components/JSEditor/JSEditor.tsx +3 -3
  24. package/src/components/LogsExplorer/LogsExplorer.tsx +168 -46
  25. package/src/components/RLSEditor/PolicyEditor.tsx +15 -13
  26. package/src/components/RLSEditor/RLSEditor.tsx +48 -18
  27. package/src/components/SQLEditor/SQLEditor.tsx +3 -11
  28. package/src/components/SQLEditor/SQLEditorSidebar.tsx +1 -1
  29. package/src/components/SQLEditor/SchemaBrowser.tsx +3 -3
  30. package/src/components/SQLEditor/sql_editor_types.ts +18 -0
  31. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +2 -1
  32. package/src/components/StudioHomePage.tsx +61 -86
  33. package/src/utils/sql_utils.test.ts +1 -1
  34. package/src/utils/sql_utils.ts +1 -1
  35. package/dist/ApiKeysView-Dbe5cXkS.js.map +0 -1
  36. package/dist/LogsExplorer-J4xfsuv3.js +0 -206
  37. package/dist/LogsExplorer-J4xfsuv3.js.map +0 -1
  38. package/dist/RLSEditor-Cgv_kUVW.js.map +0 -1
  39. package/dist/SQLEditor-ZZsy1Mk6.js.map +0 -1
  40. package/dist/index.umd.js +0 -11709
  41. package/dist/index.umd.js.map +0 -1
@@ -1,19 +1,19 @@
1
- import type { HomePageSection, PluginGenericProps } from "@rebasepro/types";
1
+ import type { AppView, HomePageSection, PluginGenericProps } from "@rebasepro/types";
2
2
  import React, { useEffect, useMemo, useState } from "react";
3
3
  import { Card, cls, Container, ExpandablePanel, Typography } from "@rebasepro/ui";
4
- import { IconForView, useRebaseContext, useRestoreScroll, useSlot } from "@rebasepro/app";
4
+ import { IconForView, useRebaseContext, useRebaseRegistry, useRestoreScroll, useSlot } from "@rebasepro/app";
5
5
  import { useNavigate } from "react-router-dom";
6
6
  import { useStudioBreadcrumbs, SchemaDriftBanner } from "@rebasepro/app";
7
7
 
8
8
  /* ═══════════════════════════════════════════════════════════════
9
- Static studio tool definitions
9
+ Studio tool sections, derived from the registered Studio views
10
10
  ═══════════════════════════════════════════════════════════════ */
11
11
 
12
12
  interface StudioTool {
13
13
  path: string;
14
14
  name: string;
15
15
  description: string;
16
- icon: string;
16
+ icon?: string | React.ReactNode;
17
17
  }
18
18
 
19
19
  interface StudioSection {
@@ -21,85 +21,44 @@ interface StudioSection {
21
21
  tools: StudioTool[];
22
22
  }
23
23
 
24
- const SECTIONS: StudioSection[] = [
25
- {
26
- label: "Database",
27
- tools: [
28
- { path: "/schema",
29
- name: "Collections",
30
- description: "Define and manage your data model and collection schemas",
31
- icon: "LayoutList" },
32
- { path: "/schema-visualizer",
33
- name: "Schema Visualizer",
34
- description: "Interactive ERD showing tables, columns, and relationships",
35
- icon: "Network" },
36
- { path: "/sql",
37
- name: "SQL Console",
38
- description: "Execute raw SQL queries directly against your database",
39
- icon: "terminal" },
40
- { path: "/branches",
41
- name: "Branches",
42
- description: "Create and manage isolated database copies for development",
43
- icon: "GitBranch" },
44
- { path: "/rls",
45
- name: "RLS Policies",
46
- description: "Configure Row Level Security for fine-grained data access",
47
- icon: "ShieldCheck" },
48
- { path: "/logs",
49
- name: "Logs Explorer",
50
- description: "Real-time system, query, and authentication logs",
51
- icon: "Activity" }
52
- ]
53
- },
54
- {
55
- label: "Compute",
56
- tools: [
57
- { path: "/js",
58
- name: "JS Console",
59
- description: "Run JavaScript with the Rebase SDK in a live sandbox",
60
- icon: "code" },
61
- { path: "/cron",
62
- name: "Cron Jobs",
63
- description: "Monitor and manage scheduled background tasks",
64
- icon: "Clock" }
65
- ]
66
- },
67
- {
68
- label: "API",
69
- tools: [
70
- { path: "/api",
71
- name: "API Explorer",
72
- description: "Interactive API documentation with live request testing",
73
- icon: "BookOpen" }
74
- ]
75
- },
76
- {
77
- label: "Storage",
78
- tools: [
79
- { path: "/storage",
80
- name: "Storage",
81
- description: "Browse, upload, and manage files in your storage bucket",
82
- icon: "HardDrive" }
83
- ]
84
- },
85
- {
86
- label: "Access Control",
87
- tools: [
88
- { path: "/users",
89
- name: "Users",
90
- description: "Manage developers and assign roles in your workspace",
91
- icon: "group" },
92
- { path: "/roles",
93
- name: "Roles",
94
- description: "Create and configure fine-grained access permissions",
95
- icon: "ShieldEllipsis" },
96
- { path: "/api-keys",
97
- name: "API Keys",
98
- description: "Create and manage scoped keys for machine-to-machine access",
99
- icon: "KeyRound" }
100
- ]
24
+ /** The view metadata the cards need — not the rendered `view` itself. */
25
+ type StudioViewMeta = Pick<AppView, "slug" | "name" | "group" | "description" | "icon" | "hideFromNavigation">;
26
+
27
+ /** Group order for the home page; unknown groups are appended in view order. */
28
+ const GROUP_ORDER = ["Database", "Compute", "Storage", "API", "Access Control"];
29
+
30
+ const UNGROUPED_LABEL = "Tools";
31
+
32
+ /**
33
+ * Build the home page sections from the Studio views that are actually
34
+ * registered, so a card exists if and only if its route does. A hand-written
35
+ * list drifts: it used to advertise Users and Roles pages that 404, while
36
+ * omitting the Backups view that does exist.
37
+ */
38
+ function buildSections(views: StudioViewMeta[]): StudioSection[] {
39
+ const byGroup = new Map<string, StudioTool[]>();
40
+
41
+ for (const view of views) {
42
+ if (view.hideFromNavigation) continue;
43
+ const label = view.group ?? UNGROUPED_LABEL;
44
+ const tools = byGroup.get(label) ?? [];
45
+ tools.push({
46
+ path: `/${view.slug}`,
47
+ name: view.name,
48
+ description: view.description ?? "",
49
+ icon: view.icon
50
+ });
51
+ byGroup.set(label, tools);
101
52
  }
102
- ];
53
+
54
+ const ordered = [
55
+ ...GROUP_ORDER.filter(g => byGroup.has(g)),
56
+ ...[...byGroup.keys()].filter(g => !GROUP_ORDER.includes(g))
57
+ ];
58
+
59
+ return ordered.map(label => ({ label,
60
+ tools: byGroup.get(label)! }));
61
+ }
103
62
 
104
63
  /* ═══════════════════════════════════════════════════════════════ */
105
64
 
@@ -151,6 +110,7 @@ export function StudioHomePage({
151
110
  const context = useRebaseContext();
152
111
  const breadcrumbs = useStudioBreadcrumbs();
153
112
  const navigate = useNavigate();
113
+ const registry = useRebaseRegistry();
154
114
 
155
115
  useEffect(() => {
156
116
  breadcrumbs.set({ breadcrumbs: [] });
@@ -162,10 +122,25 @@ export function StudioHomePage({
162
122
 
163
123
  const pluginActions = useSlot("home.actions", sectionProps);
164
124
 
165
- const filteredSections = useMemo(
166
- () => SECTIONS.filter(s => !hiddenGroups?.includes(s.label)),
167
- [hiddenGroups]
168
- );
125
+ // The collection editor ("schema") is not part of `devViews` — RebaseNavigation
126
+ // injects it when the CMS enables a collection editor. Mirror that condition so
127
+ // the card tracks the route.
128
+ const schemaEnabled = Boolean(registry.studioConfig && registry.cmsConfig?.collectionEditor);
129
+
130
+ const filteredSections = useMemo(() => {
131
+ const views: StudioViewMeta[] = [];
132
+ if (schemaEnabled) {
133
+ views.push({
134
+ slug: "schema",
135
+ name: "Collections",
136
+ group: "Database",
137
+ icon: "LayoutList",
138
+ description: "Define and manage your data model and collection schemas"
139
+ });
140
+ }
141
+ views.push(...(registry.studioConfig?.devViews ?? []));
142
+ return buildSections(views).filter(s => !hiddenGroups?.includes(s.label));
143
+ }, [registry.studioConfig?.devViews, schemaEnabled, hiddenGroups]);
169
144
 
170
145
  const groupNames = useMemo(
171
146
  () => filteredSections.map(s => s.label),
@@ -1,7 +1,7 @@
1
1
 
2
2
  import { describe, it, expect } from "@jest/globals";
3
3
  import { determineTableAndPK, extractTablesFromQuery, resolveQueryCollections } from "./sql_utils";
4
- import { TableInfo } from "../components/SQLEditor/SQLEditor";
4
+ import type { TableInfo } from "../components/SQLEditor/sql_editor_types";
5
5
  import { CollectionConfig } from "@rebasepro/types";
6
6
 
7
7
  const mockSchemas: Record<string, TableInfo[]> = {
@@ -1,5 +1,5 @@
1
1
  import { parseFirst } from "pgsql-ast-parser";
2
- import { TableInfo } from "../components/SQLEditor/SQLEditor";
2
+ import type { TableInfo } from "../components/SQLEditor/sql_editor_types";
3
3
  import { CollectionConfig } from "@rebasepro/types";
4
4
 
5
5
  /**
@@ -1 +0,0 @@
1
- {"version":3,"file":"ApiKeysView-Dbe5cXkS.js","names":[],"sources":["../src/components/ApiKeys/ApiKeysView.tsx"],"sourcesContent":["\nimport React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport {\n Button,\n Card,\n Chip,\n CircularProgress,\n cls,\n defaultBorderMixin,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n iconSize,\n KeyRoundIcon,\n RefreshCwIcon,\n Tooltip,\n Typography,\n TextField,\n Select,\n SelectItem,\n Checkbox,\n CopyIcon,\n PlusIcon as AddIcon,\n Trash2Icon as DeleteIcon,\n AlertCircleIcon,\n CheckCircleIcon\n} from \"@rebasepro/ui\";\nimport { useRebaseClient, useSnackbarController } from \"@rebasepro/app\";\nimport type { RebaseClient } from \"@rebasepro/types\";\n\n/* ═══════════════════════════════════════════════════════════════\n Types — mirrors server api-key-types.ts\n ═══════════════════════════════════════════════════════════════ */\n\ninterface ApiKeyPermission {\n collection: string;\n operations: (\"read\" | \"write\" | \"delete\")[];\n}\n\ninterface ApiKeyMasked {\n id: string;\n name: string;\n key_prefix: string;\n permissions: ApiKeyPermission[];\n rate_limit: number | null;\n created_by: string;\n created_at: string;\n updated_at: string;\n last_used_at: string | null;\n expires_at: string | null;\n revoked_at: string | null;\n}\n\ninterface ApiKeyWithSecret extends ApiKeyMasked {\n key: string;\n}\n\n/* ═══════════════════════════════════════════════════════════════\n Helpers\n ═══════════════════════════════════════════════════════════════ */\n\nfunction formatRelative(iso: string | null | undefined): string {\n if (!iso) return \"—\";\n const d = new Date(iso);\n const now = Date.now();\n const diff = d.getTime() - now;\n const abs = Math.abs(diff);\n if (abs < 60000) return diff > 0 ? \"in <1m\" : \"<1m ago\";\n if (abs < 3600000) { const m = Math.round(abs / 60000); return diff > 0 ? `in ${m}m` : `${m}m ago`; }\n if (abs < 86400000) { const h = Math.round(abs / 3600000); return diff > 0 ? `in ${h}h` : `${h}h ago`; }\n return d.toLocaleDateString();\n}\n\nfunction permissionSummary(perms: ApiKeyPermission[]): string {\n if (perms.length === 0) return \"No permissions\";\n const wildcard = perms.find(p => p.collection === \"*\");\n if (wildcard) {\n return `All collections (${wildcard.operations.join(\", \")})`;\n }\n if (perms.length === 1) {\n return `${perms[0].collection} (${perms[0].operations.join(\", \")})`;\n }\n return `${perms.length} collections`;\n}\n\nfunction isExpired(key: ApiKeyMasked): boolean {\n return !!(key.expires_at && new Date(key.expires_at) < new Date());\n}\n\nfunction keyStatus(key: ApiKeyMasked): { label: string; color: string } {\n if (key.revoked_at) return { label: \"Revoked\", color: \"text-red-500\" };\n if (isExpired(key)) return { label: \"Expired\", color: \"text-amber-500\" };\n return { label: \"Active\", color: \"text-emerald-500\" };\n}\n\n/* ═══════════════════════════════════════════════════════════════\n Main Component\n ═══════════════════════════════════════════════════════════════ */\n\nexport function ApiKeysView() {\n const client = useRebaseClient<RebaseClient>();\n const snackbar = useSnackbarController();\n const [keys, setKeys] = useState<ApiKeyMasked[]>([]);\n const [loading, setLoading] = useState(true);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [showCreate, setShowCreate] = useState(false);\n const [showSecret, setShowSecret] = useState<ApiKeyWithSecret | null>(null);\n const [revoking, setRevoking] = useState<string | null>(null);\n\n const clientRef = useRef(client);\n clientRef.current = client;\n const snackbarRef = useRef(snackbar);\n snackbarRef.current = snackbar;\n\n const loadKeys = useCallback(async () => {\n const c = clientRef.current;\n if (!c?.apiKeys) { setLoading(false); return; }\n try {\n const res = await c.apiKeys.listKeys();\n setKeys(res.keys);\n } catch (e: unknown) {\n snackbarRef.current.open({\n type: \"error\",\n message: e instanceof Error ? e.message : String(e)\n });\n } finally {\n setLoading(false);\n }\n }, []);\n\n useEffect(() => { loadKeys(); }, [loadKeys]);\n\n const handleRevoke = async (id: string) => {\n const c = clientRef.current;\n if (!c?.apiKeys) return;\n setRevoking(id);\n try {\n await c.apiKeys.revokeKey(id);\n snackbarRef.current.open({ type: \"success\", message: \"API key revoked\" });\n await loadKeys();\n if (selectedId === id) setSelectedId(null);\n } catch (e: unknown) {\n snackbarRef.current.open({ type: \"error\", message: e instanceof Error ? e.message : String(e) });\n } finally { setRevoking(null); }\n };\n\n const handleCreated = (keyWithSecret: ApiKeyWithSecret) => {\n setShowCreate(false);\n setShowSecret(keyWithSecret);\n loadKeys();\n };\n\n const selectedKey = keys.find(k => k.id === selectedId);\n const activeKeys = keys.filter(k => !k.revoked_at && !isExpired(k));\n const inactiveKeys = keys.filter(k => k.revoked_at || isExpired(k));\n\n if (loading) return <div className=\"flex items-center justify-center h-full\"><CircularProgress/></div>;\n\n return (\n <>\n <div className=\"flex h-full w-full overflow-hidden bg-white dark:bg-surface-950\">\n {/* ── Key List ── */}\n <div className={cls(\"flex flex-col w-[340px] min-w-[280px] border-r h-full\", defaultBorderMixin)}>\n <div className={cls(\"flex items-center justify-between px-4 py-2.5 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]\", defaultBorderMixin)}>\n <div className=\"flex items-center gap-2\">\n <KeyRoundIcon size={iconSize.smallest} className=\"text-primary\"/>\n <Typography variant=\"subtitle2\" className=\"font-semibold\">API Keys</Typography>\n <Chip size=\"smallest\" className=\"bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300\">{activeKeys.length}</Chip>\n </div>\n <div className=\"flex items-center gap-1\">\n <IconButton size=\"small\" onClick={loadKeys} title=\"Refresh\"><RefreshCwIcon size={iconSize.smallest}/></IconButton>\n <Button size=\"small\" color=\"primary\" onClick={() => setShowCreate(true)} startIcon={<AddIcon size={iconSize.smallest}/>}>\n New\n </Button>\n </div>\n </div>\n <div className=\"flex-1 overflow-y-auto p-2 space-y-1\">\n {activeKeys.length === 0 && inactiveKeys.length === 0 && (\n <div className=\"flex flex-col items-center justify-center h-full gap-3 text-center p-6\">\n <KeyRoundIcon size={iconSize.medium} className=\"text-surface-300 dark:text-surface-600\"/>\n <Typography variant=\"body2\" color=\"secondary\">No API keys yet</Typography>\n <Typography variant=\"caption\" color=\"disabled\">Create a key to enable machine-to-machine authentication</Typography>\n </div>\n )}\n {activeKeys.map(key => (\n <KeyListItem key={key.id} apiKey={key} selected={selectedId === key.id} onClick={() => setSelectedId(key.id)}/>\n ))}\n {inactiveKeys.length > 0 && (\n <>\n <div className=\"px-2 pt-3 pb-1\">\n <Typography variant=\"caption\" color=\"disabled\" className=\"text-[10px] uppercase tracking-wider font-medium\">Revoked / Expired</Typography>\n </div>\n {inactiveKeys.map(key => (\n <KeyListItem key={key.id} apiKey={key} selected={selectedId === key.id} onClick={() => setSelectedId(key.id)}/>\n ))}\n </>\n )}\n </div>\n </div>\n\n {/* ── Detail Panel ── */}\n <div className=\"flex-1 flex flex-col min-w-0 h-full overflow-hidden\">\n {!selectedKey ? (\n <div className=\"flex items-center justify-center h-full\">\n <Typography variant=\"body2\" color=\"disabled\">Select an API key to view details</Typography>\n </div>\n ) : (\n <>\n {/* Header */}\n <div className={cls(\"flex items-center justify-between px-5 py-3 border-b bg-white dark:bg-surface-950 min-h-[56px]\", defaultBorderMixin)}>\n <div className=\"flex items-center gap-3 min-w-0\">\n <KeyRoundIcon size={iconSize.small} className=\"text-primary shrink-0\"/>\n <div className=\"min-w-0\">\n <Typography variant=\"subtitle1\" className=\"font-semibold truncate\">{selectedKey.name}</Typography>\n <Typography variant=\"caption\" color=\"secondary\" className=\"font-mono text-[11px]\">{selectedKey.key_prefix}•••</Typography>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n {!selectedKey.revoked_at && (\n <Button\n size=\"small\"\n color=\"error\"\n variant=\"outlined\"\n onClick={() => handleRevoke(selectedKey.id)}\n disabled={revoking === selectedKey.id}\n startIcon={revoking === selectedKey.id ? <CircularProgress size=\"smallest\"/> : <DeleteIcon size={iconSize.smallest}/>}\n >\n Revoke\n </Button>\n )}\n </div>\n </div>\n\n {/* Stats */}\n <div className=\"px-5 py-4 bg-surface-50 dark:bg-surface-900/50\">\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-3\">\n <StatCard label=\"Status\" value={keyStatus(selectedKey).label} className={keyStatus(selectedKey).color}/>\n <StatCard label=\"Created\" value={formatRelative(selectedKey.created_at)}/>\n <StatCard label=\"Last Used\" value={formatRelative(selectedKey.last_used_at)}/>\n <StatCard label=\"Expires\" value={selectedKey.expires_at ? formatRelative(selectedKey.expires_at) : \"Never\"}/>\n </div>\n <div className=\"grid grid-cols-2 gap-3 mt-3\">\n <StatCard label=\"Rate Limit\" value={selectedKey.rate_limit ? `${selectedKey.rate_limit}/15min` : \"Default (1000/15min)\"}/>\n <StatCard label=\"Created By\" value={selectedKey.created_by} mono/>\n </div>\n </div>\n\n {/* Permissions */}\n <div className={cls(\"flex items-center gap-2 px-5 py-2 border-y bg-white dark:bg-surface-950\", defaultBorderMixin)}>\n <Typography variant=\"subtitle2\" className=\"font-semibold text-[13px]\">Permissions</Typography>\n <Chip size=\"smallest\" className=\"bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300\">\n {selectedKey.permissions.length}\n </Chip>\n </div>\n <div className=\"flex-1 overflow-y-auto px-5 py-3\">\n {selectedKey.permissions.length === 0 ? (\n <Typography variant=\"body2\" color=\"disabled\">No permissions configured</Typography>\n ) : (\n <div className=\"space-y-2\">\n {selectedKey.permissions.map((perm, idx) => (\n <div key={idx} className={cls(\"flex items-center gap-3 px-3 py-2 rounded-lg border\", defaultBorderMixin)}>\n <Typography variant=\"body2\" className=\"font-mono text-[13px] font-medium flex-1\">\n {perm.collection === \"*\" ? \"* (all collections)\" : perm.collection}\n </Typography>\n <div className=\"flex items-center gap-1\">\n {perm.operations.map(op => (\n <Chip key={op} size=\"smallest\" className={cls(\n op === \"read\" && \"bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300\",\n op === \"write\" && \"bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300\",\n op === \"delete\" && \"bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300\"\n )}>{op}</Chip>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </>\n )}\n </div>\n </div>\n\n {/* Create Dialog */}\n {showCreate && (\n <CreateApiKeyDialog\n onClose={() => setShowCreate(false)}\n onCreated={handleCreated}\n />\n )}\n\n {/* Secret Display Dialog */}\n {showSecret && (\n <SecretDisplayDialog\n keyWithSecret={showSecret}\n onClose={() => setShowSecret(null)}\n />\n )}\n </>\n );\n}\n\n/* ═══════════════════════════════════════════════════════════════\n List item\n ═══════════════════════════════════════════════════════════════ */\n\nfunction KeyListItem({ apiKey, selected, onClick }: { apiKey: ApiKeyMasked; selected: boolean; onClick: () => void }) {\n const status = keyStatus(apiKey);\n return (\n <div\n onClick={onClick}\n className={cls(\n \"flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all\",\n selected\n ? \"bg-primary/10 dark:bg-primary/15 ring-1 ring-primary/30\"\n : \"hover:bg-surface-100 dark:hover:bg-surface-950\"\n )}\n >\n <div className={cls(\"w-2 h-2 rounded-full shrink-0\",\n status.label === \"Active\" ? \"bg-emerald-400\" :\n status.label === \"Expired\" ? \"bg-amber-400\" : \"bg-red-400\"\n )}/>\n <div className=\"flex-1 min-w-0\">\n <Typography variant=\"body2\" className=\"truncate font-medium text-[13px]\">{apiKey.name}</Typography>\n <Typography variant=\"caption\" color=\"secondary\" className=\"truncate text-[11px] font-mono\">{apiKey.key_prefix}•••</Typography>\n </div>\n <div className=\"shrink-0\">\n <Typography variant=\"caption\" color=\"disabled\" className=\"text-[10px]\">{permissionSummary(apiKey.permissions)}</Typography>\n </div>\n </div>\n );\n}\n\n/* ═══════════════════════════════════════════════════════════════\n Stat card\n ═══════════════════════════════════════════════════════════════ */\n\nfunction StatCard({ label, value, mono, className }: { label: string; value: string; mono?: boolean; className?: string }) {\n return (\n <div className={cls(\"px-3 py-2 rounded-lg border bg-white dark:bg-surface-900\", defaultBorderMixin)}>\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[10px] uppercase tracking-wider font-medium\">{label}</Typography>\n <Typography variant=\"body2\" className={cls(\n \"mt-0.5 font-semibold text-[13px]\",\n mono && \"font-mono\",\n className\n )}>{value}</Typography>\n </div>\n );\n}\n\n/* ═══════════════════════════════════════════════════════════════\n Create API Key Dialog\n ═══════════════════════════════════════════════════════════════ */\n\ninterface PermissionRow {\n collection: string;\n read: boolean;\n write: boolean;\n delete: boolean;\n}\n\nfunction CreateApiKeyDialog({ onClose, onCreated }: { onClose: () => void; onCreated: (key: ApiKeyWithSecret) => void }) {\n const client = useRebaseClient<RebaseClient>();\n const snackbar = useSnackbarController();\n const [name, setName] = useState(\"\");\n const [permissions, setPermissions] = useState<PermissionRow[]>([{ collection: \"*\", read: true, write: false, delete: false }]);\n const [rateLimit, setRateLimit] = useState(\"\");\n const [expiresIn, setExpiresIn] = useState(\"never\");\n const [creating, setCreating] = useState(false);\n\n const addRow = () => setPermissions([...permissions, { collection: \"\", read: true, write: false, delete: false }]);\n const removeRow = (idx: number) => setPermissions(permissions.filter((_, i) => i !== idx));\n\n const updateRow = (idx: number, field: keyof PermissionRow, value: string | boolean) => {\n setPermissions(permissions.map((row, i) => i === idx ? { ...row, [field]: value } : row));\n };\n\n const handleCreate = async () => {\n if (!client?.apiKeys || !name.trim()) return;\n const apiPerms: ApiKeyPermission[] = permissions\n .filter(r => r.collection.trim())\n .map(r => ({\n collection: r.collection.trim(),\n operations: [\n ...(r.read ? [\"read\" as const] : []),\n ...(r.write ? [\"write\" as const] : []),\n ...(r.delete ? [\"delete\" as const] : [])\n ]\n }))\n .filter(p => p.operations.length > 0);\n\n if (apiPerms.length === 0) {\n snackbar.open({ type: \"error\", message: \"At least one permission is required\" });\n return;\n }\n\n let expires_at: string | null = null;\n if (expiresIn === \"7d\") expires_at = new Date(Date.now() + 7 * 86400000).toISOString();\n else if (expiresIn === \"30d\") expires_at = new Date(Date.now() + 30 * 86400000).toISOString();\n else if (expiresIn === \"90d\") expires_at = new Date(Date.now() + 90 * 86400000).toISOString();\n else if (expiresIn === \"1y\") expires_at = new Date(Date.now() + 365 * 86400000).toISOString();\n\n setCreating(true);\n try {\n const res = await client.apiKeys.createKey({\n name: name.trim(),\n permissions: apiPerms,\n rate_limit: rateLimit ? parseInt(rateLimit, 10) : null,\n expires_at\n });\n onCreated(res.key);\n } catch (e: unknown) {\n snackbar.open({ type: \"error\", message: e instanceof Error ? e.message : String(e) });\n } finally { setCreating(false); }\n };\n\n return (\n <Dialog open onOpenChange={(open) => { if (!open) onClose(); }} maxWidth=\"lg\">\n <DialogTitle>Create API Key</DialogTitle>\n <DialogContent className=\"space-y-4\">\n <TextField\n label=\"Name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Analytics Pipeline\"\n size=\"small\"\n autoFocus\n />\n\n <div>\n <Typography variant=\"caption\" color=\"secondary\" className=\"text-[11px] uppercase tracking-wider font-medium mb-2 block\">\n Permissions\n </Typography>\n <div className=\"space-y-2\">\n {permissions.map((row, idx) => (\n <div key={idx} className={cls(\"flex items-center gap-2 p-2 rounded-lg border\", defaultBorderMixin)}>\n <TextField\n size=\"small\"\n value={row.collection}\n onChange={(e) => updateRow(idx, \"collection\", e.target.value)}\n placeholder=\"Collection slug or *\"\n className=\"flex-1\"\n />\n <label className=\"flex items-center gap-1 text-xs cursor-pointer\">\n <Checkbox size=\"small\" checked={row.read} onCheckedChange={(v) => updateRow(idx, \"read\", !!v)}/>\n <span className=\"text-emerald-600 dark:text-emerald-400\">read</span>\n </label>\n <label className=\"flex items-center gap-1 text-xs cursor-pointer\">\n <Checkbox size=\"small\" checked={row.write} onCheckedChange={(v) => updateRow(idx, \"write\", !!v)}/>\n <span className=\"text-blue-600 dark:text-blue-400\">write</span>\n </label>\n <label className=\"flex items-center gap-1 text-xs cursor-pointer\">\n <Checkbox size=\"small\" checked={row.delete} onCheckedChange={(v) => updateRow(idx, \"delete\", !!v)}/>\n <span className=\"text-red-600 dark:text-red-400\">delete</span>\n </label>\n {permissions.length > 1 && (\n <IconButton size=\"small\" onClick={() => removeRow(idx)}>\n <DeleteIcon size={iconSize.smallest}/>\n </IconButton>\n )}\n </div>\n ))}\n </div>\n <Button size=\"small\" variant=\"text\" onClick={addRow} className=\"mt-1\" startIcon={<AddIcon size={iconSize.smallest}/>}>\n Add collection\n </Button>\n </div>\n\n <div className=\"flex gap-4\">\n <div className=\"flex-1\">\n <Select label=\"Expires\" value={expiresIn} onValueChange={setExpiresIn} size=\"small\" renderValue={(v) =>\n v === \"never\" ? \"Never\" :\n v === \"7d\" ? \"7 days\" :\n v === \"30d\" ? \"30 days\" :\n v === \"90d\" ? \"90 days\" : \"1 year\"\n }>\n <SelectItem value=\"never\">Never</SelectItem>\n <SelectItem value=\"7d\">7 days</SelectItem>\n <SelectItem value=\"30d\">30 days</SelectItem>\n <SelectItem value=\"90d\">90 days</SelectItem>\n <SelectItem value=\"1y\">1 year</SelectItem>\n </Select>\n </div>\n <div className=\"flex-1\">\n <TextField\n label=\"Rate Limit (per 15 min)\"\n value={rateLimit}\n onChange={(e) => setRateLimit(e.target.value.replace(/\\D/g, \"\"))}\n placeholder=\"Default: 1000\"\n size=\"small\"\n />\n </div>\n </div>\n </DialogContent>\n <DialogActions>\n <Button variant=\"text\" onClick={onClose}>Cancel</Button>\n <Button\n color=\"primary\"\n onClick={handleCreate}\n disabled={creating || !name.trim()}\n startIcon={creating ? <CircularProgress size=\"smallest\"/> : undefined}\n >\n Create Key\n </Button>\n </DialogActions>\n </Dialog>\n );\n}\n\n/* ═══════════════════════════════════════════════════════════════\n Secret Display Dialog — shown exactly once after creation\n ═══════════════════════════════════════════════════════════════ */\n\nfunction SecretDisplayDialog({ keyWithSecret, onClose }: { keyWithSecret: ApiKeyWithSecret; onClose: () => void }) {\n const snackbar = useSnackbarController();\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n try {\n await navigator.clipboard.writeText(keyWithSecret.key);\n setCopied(true);\n snackbar.open({ type: \"success\", message: \"API key copied to clipboard\" });\n setTimeout(() => setCopied(false), 2000);\n } catch {\n snackbar.open({ type: \"error\", message: \"Failed to copy\" });\n }\n };\n\n return (\n <Dialog open onOpenChange={(open) => { if (!open) onClose(); }} maxWidth=\"md\">\n <DialogTitle>\n <div className=\"flex items-center gap-2\">\n <CheckCircleIcon size={iconSize.small} className=\"text-emerald-500\"/>\n API Key Created\n </div>\n </DialogTitle>\n <DialogContent>\n <div className=\"p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 mb-4\">\n <div className=\"flex items-center gap-2 mb-1\">\n <AlertCircleIcon size={iconSize.smallest} className=\"text-amber-600 dark:text-amber-400\"/>\n <Typography variant=\"caption\" className=\"font-semibold text-amber-700 dark:text-amber-400\">\n Copy your key now — it won&apos;t be shown again\n </Typography>\n </div>\n <Typography variant=\"caption\" className=\"text-amber-600 dark:text-amber-300\">\n This is the only time the full API key will be displayed. Store it securely.\n </Typography>\n </div>\n\n <div className={cls(\"flex items-center gap-2 p-3 rounded-lg border bg-surface-50 dark:bg-surface-900\", defaultBorderMixin)}>\n <code className=\"flex-1 text-[12px] font-mono break-all text-surface-700 dark:text-surface-300 select-all\">\n {keyWithSecret.key}\n </code>\n <Tooltip title={copied ? \"Copied!\" : \"Copy\"}>\n <IconButton size=\"small\" onClick={handleCopy}>\n {copied\n ? <CheckCircleIcon size={iconSize.smallest} className=\"text-emerald-500\"/>\n : <CopyIcon size={iconSize.smallest}/>\n }\n </IconButton>\n </Tooltip>\n </div>\n\n <div className=\"mt-4 space-y-1\">\n <Typography variant=\"caption\" color=\"secondary\">\n <strong>Name:</strong> {keyWithSecret.name}\n </Typography>\n <Typography variant=\"caption\" color=\"secondary\">\n <strong>Permissions:</strong> {permissionSummary(keyWithSecret.permissions)}\n </Typography>\n </div>\n </DialogContent>\n <DialogActions>\n <Button color=\"primary\" onClick={onClose}>Done</Button>\n </DialogActions>\n </Dialog>\n );\n}\n"],"mappings":"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"}
@@ -1,206 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import { Checkbox, Label, Select, SelectItem, TextField, Typography, cls, defaultBorderMixin } from "@rebasepro/ui";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
- //#region src/components/LogsExplorer/LogsExplorer.tsx
5
- var LEVEL_COLORS = {
6
- debug: "text-surface-500",
7
- info: "text-blue-600 dark:text-blue-500",
8
- warn: "text-amber-600 dark:text-amber-500",
9
- error: "text-red-600 dark:text-red-500"
10
- };
11
- var SOURCE_COLORS = {
12
- api: "text-sky-600 dark:text-sky-400",
13
- auth: "text-purple-600 dark:text-purple-400",
14
- storage: "text-green-600 dark:text-green-500",
15
- realtime: "text-orange-600 dark:text-orange-400",
16
- system: "text-surface-600 dark:text-surface-400"
17
- };
18
- function LogsExplorer() {
19
- const [logs, setLogs] = useState([]);
20
- const [level, setLevel] = useState("all");
21
- const [source, setSource] = useState("all");
22
- const [search, setSearch] = useState("");
23
- const [autoScroll, setAutoScroll] = useState(true);
24
- const containerRef = useRef(null);
25
- const fetchLogs = useCallback(async () => {
26
- try {
27
- const params = new URLSearchParams();
28
- if (level && level !== "all") params.set("level", level);
29
- if (source && source !== "all") params.set("source", source);
30
- if (search) params.set("search", search);
31
- params.set("limit", "200");
32
- const resp = await fetch(`/api/logs?${params}`);
33
- if (resp.ok) setLogs((await resp.json()).entries || []);
34
- } catch {}
35
- }, [
36
- level,
37
- source,
38
- search
39
- ]);
40
- useEffect(() => {
41
- let timeoutId = null;
42
- let cancelled = false;
43
- fetchLogs();
44
- const scheduleNext = () => {
45
- if (cancelled) return;
46
- timeoutId = setTimeout(async () => {
47
- if (document.visibilityState === "visible") await fetchLogs();
48
- scheduleNext();
49
- }, 3e3);
50
- };
51
- scheduleNext();
52
- const handleVisibility = () => {
53
- if (document.visibilityState === "visible") fetchLogs();
54
- };
55
- document.addEventListener("visibilitychange", handleVisibility);
56
- return () => {
57
- cancelled = true;
58
- if (timeoutId) clearTimeout(timeoutId);
59
- document.removeEventListener("visibilitychange", handleVisibility);
60
- };
61
- }, [fetchLogs]);
62
- useEffect(() => {
63
- if (autoScroll && containerRef.current) containerRef.current.scrollTop = containerRef.current.scrollHeight;
64
- }, [logs, autoScroll]);
65
- return /* @__PURE__ */ jsxs("div", {
66
- className: "flex flex-col h-[calc(100vh-64px)] w-full bg-surface-50 dark:bg-surface-800",
67
- children: [/* @__PURE__ */ jsxs("div", {
68
- className: cls("flex gap-2 p-3 border-b items-center flex-wrap shrink-0", defaultBorderMixin),
69
- children: [
70
- /* @__PURE__ */ jsxs(Select, {
71
- value: level,
72
- onValueChange: setLevel,
73
- size: "small",
74
- placeholder: "All Levels",
75
- children: [
76
- /* @__PURE__ */ jsx(SelectItem, {
77
- value: "all",
78
- children: "All Levels"
79
- }),
80
- /* @__PURE__ */ jsx(SelectItem, {
81
- value: "debug",
82
- children: "Debug"
83
- }),
84
- /* @__PURE__ */ jsx(SelectItem, {
85
- value: "info",
86
- children: "Info"
87
- }),
88
- /* @__PURE__ */ jsx(SelectItem, {
89
- value: "warn",
90
- children: "Warn"
91
- }),
92
- /* @__PURE__ */ jsx(SelectItem, {
93
- value: "error",
94
- children: "Error"
95
- })
96
- ]
97
- }),
98
- /* @__PURE__ */ jsxs(Select, {
99
- value: source,
100
- onValueChange: setSource,
101
- size: "small",
102
- placeholder: "All Sources",
103
- children: [
104
- /* @__PURE__ */ jsx(SelectItem, {
105
- value: "all",
106
- children: "All Sources"
107
- }),
108
- /* @__PURE__ */ jsx(SelectItem, {
109
- value: "api",
110
- children: "API"
111
- }),
112
- /* @__PURE__ */ jsx(SelectItem, {
113
- value: "auth",
114
- children: "Auth"
115
- }),
116
- /* @__PURE__ */ jsx(SelectItem, {
117
- value: "storage",
118
- children: "Storage"
119
- }),
120
- /* @__PURE__ */ jsx(SelectItem, {
121
- value: "realtime",
122
- children: "Realtime"
123
- }),
124
- /* @__PURE__ */ jsx(SelectItem, {
125
- value: "system",
126
- children: "System"
127
- })
128
- ]
129
- }),
130
- /* @__PURE__ */ jsx(TextField, {
131
- size: "small",
132
- placeholder: "Search logs...",
133
- value: search,
134
- onChange: (e) => setSearch(e.target.value),
135
- className: "flex-1 min-w-[200px]"
136
- }),
137
- /* @__PURE__ */ jsxs("div", {
138
- className: "flex items-center gap-1.5 cursor-pointer ml-2",
139
- children: [/* @__PURE__ */ jsx(Checkbox, {
140
- id: "auto-scroll",
141
- checked: autoScroll,
142
- onCheckedChange: setAutoScroll,
143
- size: "small",
144
- padding: false
145
- }), /* @__PURE__ */ jsx(Label, {
146
- htmlFor: "auto-scroll",
147
- className: "text-xs select-none cursor-pointer text-surface-600 dark:text-surface-400",
148
- children: "Auto-scroll"
149
- })]
150
- }),
151
- /* @__PURE__ */ jsx("div", {
152
- className: "ml-auto pl-4",
153
- children: /* @__PURE__ */ jsxs(Typography, {
154
- variant: "caption",
155
- color: "secondary",
156
- children: [logs.length, " entries"]
157
- })
158
- })
159
- ]
160
- }), /* @__PURE__ */ jsxs("div", {
161
- ref: containerRef,
162
- className: "flex-1 overflow-auto py-2",
163
- children: [logs.map((log) => /* @__PURE__ */ jsxs("div", {
164
- className: cls("flex gap-4 px-4 py-[6px] border-b hover:bg-surface-100 dark:hover:bg-surface-900 transition-colors", defaultBorderMixin),
165
- children: [
166
- /* @__PURE__ */ jsx(Typography, {
167
- variant: "body2",
168
- color: "secondary",
169
- className: "w-[72px] shrink-0 font-mono",
170
- children: new Date(log.timestamp).toLocaleTimeString()
171
- }),
172
- /* @__PURE__ */ jsx(Typography, {
173
- variant: "body2",
174
- className: cls("w-[48px] shrink-0 uppercase font-semibold font-mono", LEVEL_COLORS[log.level] || "text-surface-500"),
175
- children: log.level
176
- }),
177
- /* @__PURE__ */ jsxs(Typography, {
178
- variant: "body2",
179
- className: cls("w-[80px] shrink-0 font-mono", SOURCE_COLORS[log.source] || "text-surface-500"),
180
- children: [
181
- "[",
182
- log.source,
183
- "]"
184
- ]
185
- }),
186
- /* @__PURE__ */ jsx(Typography, {
187
- variant: "body2",
188
- className: "flex-1 font-mono break-all whitespace-pre-wrap text-surface-900 dark:text-surface-100",
189
- children: log.message
190
- })
191
- ]
192
- }, log.id)), logs.length === 0 && /* @__PURE__ */ jsx("div", {
193
- className: "p-8 text-center",
194
- children: /* @__PURE__ */ jsx(Typography, {
195
- variant: "body2",
196
- color: "secondary",
197
- children: "No log entries yet. Logs will appear here as requests come in."
198
- })
199
- })]
200
- })]
201
- });
202
- }
203
- //#endregion
204
- export { LogsExplorer };
205
-
206
- //# sourceMappingURL=LogsExplorer-J4xfsuv3.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LogsExplorer-J4xfsuv3.js","names":[],"sources":["../src/components/LogsExplorer/LogsExplorer.tsx"],"sourcesContent":["import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport { Select, SelectItem, TextField, Checkbox, Label, Typography, cls, defaultBorderMixin } from \"@rebasepro/ui\";\n\ninterface LogEntry {\n id: string;\n timestamp: string;\n level: \"debug\" | \"info\" | \"warn\" | \"error\";\n source: \"api\" | \"auth\" | \"storage\" | \"realtime\" | \"system\";\n message: string;\n metadata?: Record<string, unknown>;\n}\n\nconst LEVEL_COLORS: Record<string, string> = {\n debug: \"text-surface-500\",\n info: \"text-blue-600 dark:text-blue-500\",\n warn: \"text-amber-600 dark:text-amber-500\",\n error: \"text-red-600 dark:text-red-500\"\n};\n\nconst SOURCE_COLORS: Record<string, string> = {\n api: \"text-sky-600 dark:text-sky-400\",\n auth: \"text-purple-600 dark:text-purple-400\",\n storage: \"text-green-600 dark:text-green-500\",\n realtime: \"text-orange-600 dark:text-orange-400\",\n system: \"text-surface-600 dark:text-surface-400\"\n};\n\nexport function LogsExplorer() {\n const [logs, setLogs] = useState<LogEntry[]>([]);\n const [level, setLevel] = useState<string>(\"all\");\n const [source, setSource] = useState<string>(\"all\");\n const [search, setSearch] = useState(\"\");\n const [autoScroll, setAutoScroll] = useState(true);\n const containerRef = useRef<HTMLDivElement>(null);\n const fetchLogs = useCallback(async () => {\n try {\n const params = new URLSearchParams();\n if (level && level !== \"all\") params.set(\"level\", level);\n if (source && source !== \"all\") params.set(\"source\", source);\n if (search) params.set(\"search\", search);\n params.set(\"limit\", \"200\");\n\n const resp = await fetch(`/api/logs?${params}`);\n if (resp.ok) {\n const data: { entries?: LogEntry[] } = await resp.json();\n setLogs(data.entries || []);\n }\n } catch {\n /* ignore poll failures */\n }\n }, [level, source, search]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let cancelled = false;\n\n fetchLogs();\n\n const scheduleNext = () => {\n if (cancelled) return;\n timeoutId = setTimeout(async () => {\n if (document.visibilityState === \"visible\") {\n await fetchLogs();\n }\n scheduleNext();\n }, 3000);\n };\n\n scheduleNext();\n\n const handleVisibility = () => {\n if (document.visibilityState === \"visible\") {\n fetchLogs();\n }\n };\n document.addEventListener(\"visibilitychange\", handleVisibility);\n\n return () => {\n cancelled = true;\n if (timeoutId) clearTimeout(timeoutId);\n document.removeEventListener(\"visibilitychange\", handleVisibility);\n };\n }, [fetchLogs]);\n\n useEffect(() => {\n if (autoScroll && containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n }, [logs, autoScroll]);\n\n return (\n <div className=\"flex flex-col h-[calc(100vh-64px)] w-full bg-surface-50 dark:bg-surface-800\">\n {/* Toolbar */}\n <div className={cls(\n \"flex gap-2 p-3 border-b items-center flex-wrap shrink-0\",\n defaultBorderMixin\n )}>\n <Select\n value={level}\n onValueChange={setLevel}\n size=\"small\"\n placeholder=\"All Levels\"\n >\n <SelectItem value=\"all\">All Levels</SelectItem>\n <SelectItem value=\"debug\">Debug</SelectItem>\n <SelectItem value=\"info\">Info</SelectItem>\n <SelectItem value=\"warn\">Warn</SelectItem>\n <SelectItem value=\"error\">Error</SelectItem>\n </Select>\n <Select\n value={source}\n onValueChange={setSource}\n size=\"small\"\n placeholder=\"All Sources\"\n >\n <SelectItem value=\"all\">All Sources</SelectItem>\n <SelectItem value=\"api\">API</SelectItem>\n <SelectItem value=\"auth\">Auth</SelectItem>\n <SelectItem value=\"storage\">Storage</SelectItem>\n <SelectItem value=\"realtime\">Realtime</SelectItem>\n <SelectItem value=\"system\">System</SelectItem>\n </Select>\n <TextField\n size=\"small\"\n placeholder=\"Search logs...\"\n value={search}\n onChange={e => setSearch(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={setAutoScroll}\n size=\"small\"\n padding={false}\n />\n <Label\n htmlFor=\"auto-scroll\"\n className=\"text-xs select-none cursor-pointer text-surface-600 dark:text-surface-400\"\n >\n Auto-scroll\n </Label>\n </div>\n <div className=\"ml-auto pl-4\">\n <Typography variant=\"caption\" color=\"secondary\">\n {logs.length} entries\n </Typography>\n </div>\n </div>\n \n {/* Log entries */}\n <div\n ref={containerRef}\n className=\"flex-1 overflow-auto py-2\"\n >\n {logs.map(log => (\n <div\n key={log.id}\n className={cls(\n \"flex gap-4 px-4 py-[6px] border-b hover:bg-surface-100 dark:hover:bg-surface-900 transition-colors\",\n defaultBorderMixin\n )}\n >\n <Typography variant=\"body2\" color=\"secondary\" className=\"w-[72px] shrink-0 font-mono\">\n {new Date(log.timestamp).toLocaleTimeString()}\n </Typography>\n <Typography variant=\"body2\" className={cls(\"w-[48px] shrink-0 uppercase font-semibold font-mono\", LEVEL_COLORS[log.level] || \"text-surface-500\")}>\n {log.level}\n </Typography>\n <Typography variant=\"body2\" className={cls(\"w-[80px] shrink-0 font-mono\", SOURCE_COLORS[log.source] || \"text-surface-500\")}>\n [{log.source}]\n </Typography>\n <Typography variant=\"body2\" className=\"flex-1 font-mono break-all whitespace-pre-wrap text-surface-900 dark:text-surface-100\">\n {log.message}\n </Typography>\n </div>\n ))}\n {logs.length === 0 && (\n <div className=\"p-8 text-center\">\n <Typography variant=\"body2\" color=\"secondary\">\n No log entries yet. Logs will appear here as requests come in.\n </Typography>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;AAYA,IAAM,eAAuC;CACzC,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;AACX;AAEA,IAAM,gBAAwC;CAC1C,KAAK;CACL,MAAM;CACN,SAAS;CACT,UAAU;CACV,QAAQ;AACZ;AAEA,SAAgB,eAAe;CAC3B,MAAM,CAAC,MAAM,WAAW,SAAqB,CAAC,CAAC;CAC/C,MAAM,CAAC,OAAO,YAAY,SAAiB,KAAK;CAChD,MAAM,CAAC,QAAQ,aAAa,SAAiB,KAAK;CAClD,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,IAAI;CACjD,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,YAAY,YAAY,YAAY;EACtC,IAAI;GACA,MAAM,SAAS,IAAI,gBAAgB;GACnC,IAAI,SAAS,UAAU,OAAO,OAAO,IAAI,SAAS,KAAK;GACvD,IAAI,UAAU,WAAW,OAAO,OAAO,IAAI,UAAU,MAAM;GAC3D,IAAI,QAAQ,OAAO,IAAI,UAAU,MAAM;GACvC,OAAO,IAAI,SAAS,KAAK;GAEzB,MAAM,OAAO,MAAM,MAAM,aAAa,QAAQ;GAC9C,IAAI,KAAK,IAEL,SAAQ,MADqC,KAAK,KAAK,GAC1C,WAAW,CAAC,CAAC;EAElC,QAAQ,CAER;CACJ,GAAG;EAAC;EAAO;EAAQ;CAAM,CAAC;CAE1B,gBAAgB;EACZ,IAAI,YAAkD;EACtD,IAAI,YAAY;EAEhB,UAAU;EAEV,MAAM,qBAAqB;GACvB,IAAI,WAAW;GACf,YAAY,WAAW,YAAY;IAC/B,IAAI,SAAS,oBAAoB,WAC7B,MAAM,UAAU;IAEpB,aAAa;GACjB,GAAG,GAAI;EACX;EAEA,aAAa;EAEb,MAAM,yBAAyB;GAC3B,IAAI,SAAS,oBAAoB,WAC7B,UAAU;EAElB;EACA,SAAS,iBAAiB,oBAAoB,gBAAgB;EAE9D,aAAa;GACT,YAAY;GACZ,IAAI,WAAW,aAAa,SAAS;GACrC,SAAS,oBAAoB,oBAAoB,gBAAgB;EACrE;CACJ,GAAG,CAAC,SAAS,CAAC;CAEd,gBAAgB;EACZ,IAAI,cAAc,aAAa,SAC3B,aAAa,QAAQ,YAAY,aAAa,QAAQ;CAE9D,GAAG,CAAC,MAAM,UAAU,CAAC;CAErB,OACI,qBAAC,OAAD;EAAK,WAAU;YAAf,CAEI,qBAAC,OAAD;GAAK,WAAW,IACZ,2DACA,kBACJ;aAHA;IAII,qBAAC,QAAD;KACI,OAAO;KACP,eAAe;KACf,MAAK;KACL,aAAY;eAJhB;MAMI,oBAAC,YAAD;OAAY,OAAM;iBAAM;MAAsB,CAAA;MAC9C,oBAAC,YAAD;OAAY,OAAM;iBAAQ;MAAiB,CAAA;MAC3C,oBAAC,YAAD;OAAY,OAAM;iBAAO;MAAgB,CAAA;MACzC,oBAAC,YAAD;OAAY,OAAM;iBAAO;MAAgB,CAAA;MACzC,oBAAC,YAAD;OAAY,OAAM;iBAAQ;MAAiB,CAAA;KACvC;;IACR,qBAAC,QAAD;KACI,OAAO;KACP,eAAe;KACf,MAAK;KACL,aAAY;eAJhB;MAMI,oBAAC,YAAD;OAAY,OAAM;iBAAM;MAAuB,CAAA;MAC/C,oBAAC,YAAD;OAAY,OAAM;iBAAM;MAAe,CAAA;MACvC,oBAAC,YAAD;OAAY,OAAM;iBAAO;MAAgB,CAAA;MACzC,oBAAC,YAAD;OAAY,OAAM;iBAAU;MAAmB,CAAA;MAC/C,oBAAC,YAAD;OAAY,OAAM;iBAAW;MAAoB,CAAA;MACjD,oBAAC,YAAD;OAAY,OAAM;iBAAS;MAAkB,CAAA;KACzC;;IACR,oBAAC,WAAD;KACI,MAAK;KACL,aAAY;KACZ,OAAO;KACP,WAAU,MAAK,UAAU,EAAE,OAAO,KAAK;KACvC,WAAU;IACb,CAAA;IACD,qBAAC,OAAD;KAAK,WAAU;eAAf,CACI,oBAAC,UAAD;MACI,IAAG;MACH,SAAS;MACT,iBAAiB;MACjB,MAAK;MACL,SAAS;KACZ,CAAA,GACD,oBAAC,OAAD;MACI,SAAQ;MACR,WAAU;gBACb;KAEM,CAAA,CACN;;IACL,oBAAC,OAAD;KAAK,WAAU;eACX,qBAAC,YAAD;MAAY,SAAQ;MAAU,OAAM;gBAApC,CACK,KAAK,QAAO,UACL;;IACX,CAAA;GACJ;MAGL,qBAAC,OAAD;GACI,KAAK;GACL,WAAU;aAFd,CAIK,KAAK,KAAI,QACN,qBAAC,OAAD;IAEI,WAAW,IACP,sGACA,kBACJ;cALJ;KAOI,oBAAC,YAAD;MAAY,SAAQ;MAAQ,OAAM;MAAY,WAAU;gBACnD,IAAI,KAAK,IAAI,SAAS,EAAE,mBAAmB;KACpC,CAAA;KACZ,oBAAC,YAAD;MAAY,SAAQ;MAAQ,WAAW,IAAI,uDAAuD,aAAa,IAAI,UAAU,kBAAkB;gBAC1I,IAAI;KACG,CAAA;KACZ,qBAAC,YAAD;MAAY,SAAQ;MAAQ,WAAW,IAAI,+BAA+B,cAAc,IAAI,WAAW,kBAAkB;gBAAzH;OAA4H;OACtH,IAAI;OAAO;MACL;;KACZ,oBAAC,YAAD;MAAY,SAAQ;MAAQ,WAAU;gBACjC,IAAI;KACG,CAAA;IACX;MAlBI,IAAI,EAkBR,CACR,GACA,KAAK,WAAW,KACb,oBAAC,OAAD;IAAK,WAAU;cACX,oBAAC,YAAD;KAAY,SAAQ;KAAQ,OAAM;eAAY;IAElC,CAAA;GACX,CAAA,CAER;IACJ;;AAEb"}