@rebasepro/studio 0.9.1-canary.fd3754b → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ApiKeysView-Dbe5cXkS.js → ApiKeysView-vQKqt2wl.js} +44 -2
- package/dist/ApiKeysView-vQKqt2wl.js.map +1 -0
- package/dist/{JSEditor-Etgw9-Cw.js → JSEditor-BeHHuxhA.js} +3 -3
- package/dist/{JSEditor-Etgw9-Cw.js.map → JSEditor-BeHHuxhA.js.map} +1 -1
- package/dist/LogsExplorer-BCXoGREr.js +298 -0
- package/dist/LogsExplorer-BCXoGREr.js.map +1 -0
- package/dist/{RLSEditor-Cgv_kUVW.js → RLSEditor-rLlMcZPG.js} +42 -28
- package/dist/RLSEditor-rLlMcZPG.js.map +1 -0
- package/dist/{SQLEditor-ZZsy1Mk6.js → SQLEditor-_dCqVAN_.js} +3 -3
- package/dist/SQLEditor-_dCqVAN_.js.map +1 -0
- package/dist/{SchemaVisualizer-D01DzU-f.js → SchemaVisualizer-C_ErehfH.js} +2 -2
- package/dist/{SchemaVisualizer-D01DzU-f.js.map → SchemaVisualizer-C_ErehfH.js.map} +1 -1
- package/dist/components/RLSEditor/PolicyEditor.d.ts +1 -1
- package/dist/components/SQLEditor/SQLEditor.d.ts +2 -10
- package/dist/components/SQLEditor/SQLEditorSidebar.d.ts +1 -1
- package/dist/components/SQLEditor/SchemaBrowser.d.ts +1 -1
- package/dist/components/SQLEditor/sql_editor_types.d.ts +16 -0
- package/dist/index.es.js +58 -105
- package/dist/index.es.js.map +1 -1
- package/dist/utils/sql_utils.d.ts +1 -1
- package/package.json +13 -12
- package/src/components/ApiKeys/ApiKeysView.tsx +41 -1
- package/src/components/JSEditor/JSEditor.tsx +3 -3
- package/src/components/LogsExplorer/LogsExplorer.tsx +168 -46
- package/src/components/RLSEditor/PolicyEditor.tsx +15 -13
- package/src/components/RLSEditor/RLSEditor.tsx +48 -18
- package/src/components/SQLEditor/SQLEditor.tsx +3 -11
- package/src/components/SQLEditor/SQLEditorSidebar.tsx +1 -1
- package/src/components/SQLEditor/SchemaBrowser.tsx +3 -3
- package/src/components/SQLEditor/sql_editor_types.ts +18 -0
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +2 -1
- package/src/components/StudioHomePage.tsx +61 -86
- package/src/utils/sql_utils.test.ts +1 -1
- package/src/utils/sql_utils.ts +1 -1
- package/dist/ApiKeysView-Dbe5cXkS.js.map +0 -1
- package/dist/LogsExplorer-J4xfsuv3.js +0 -206
- package/dist/LogsExplorer-J4xfsuv3.js.map +0 -1
- package/dist/RLSEditor-Cgv_kUVW.js.map +0 -1
- package/dist/SQLEditor-ZZsy1Mk6.js.map +0 -1
- package/dist/index.umd.js +0 -11709
- package/dist/index.umd.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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/
|
|
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[]> = {
|
package/src/utils/sql_utils.ts
CHANGED
|
@@ -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'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":"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"}
|