Skip to content

Commit 5d3ff59

Browse files
authored
Merge pull request #98 from UiPath/feat/statedb-explorer
feat: SQLite state database explorer
2 parents 093ec03 + 189a169 commit 5d3ff59

20 files changed

Lines changed: 1029 additions & 157 deletions

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.71"
3+
version = "0.0.72"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -12,6 +12,7 @@ dependencies = [
1212
"uvicorn[standard]>=0.40.0",
1313
"uipath>=2.10.0, <2.11.0",
1414
"openai",
15+
"aiosqlite>=0.20.0",
1516
]
1617
classifiers = [
1718
"Intended Audience :: Developers",

src/uipath/dev/server/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ def _on_reload_done(t: asyncio.Task[None]) -> None:
200200
app.include_router(evals_router, prefix="/api")
201201
app.include_router(agent_router, prefix="/api")
202202
app.include_router(files_router, prefix="/api")
203+
204+
from uipath.dev.server.routes.statedb import router as statedb_router
205+
206+
app.include_router(statedb_router, prefix="/api")
203207
app.include_router(ws_router)
204208

205209
# Auto-build frontend if source is available and build is stale

src/uipath/dev/server/frontend/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import EvaluatorsView from "./components/evaluators/EvaluatorDetail";
2727
import CreateEvaluatorView from "./components/evaluators/CreateEvaluatorView";
2828
import ExplorerSidebar from "./components/explorer/ExplorerSidebar";
2929
import FileEditor from "./components/explorer/FileEditor";
30+
import StateDbViewer from "./components/explorer/StateDbViewer";
3031
import AgentChatSidebar from "./components/agent/AgentChatSidebar";
3132
import { useExplorerStore } from "./store/useExplorerStore";
3233

@@ -67,6 +68,7 @@ export default function App() {
6768
evaluatorId,
6869
evaluatorFilter,
6970
explorerFile,
71+
stateDbTable,
7072
navigate,
7173
} = useHashRoute();
7274

@@ -342,6 +344,7 @@ export default function App() {
342344
// --- Render main content based on section ---
343345
const renderMainContent = () => {
344346
if (section === "explorer") {
347+
if (stateDbTable !== null) return <StateDbViewer table={stateDbTable} />;
345348
if (explorerTabs.length > 0 || explorerFile) return <FileEditor />;
346349
return (
347350
<div className="flex items-center justify-center h-full text-[var(--text-muted)]">
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type {
2+
StateDbTable,
3+
StateDbTableData,
4+
StateDbQueryResult,
5+
} from "../types/statedb";
6+
7+
const BASE = "/api";
8+
9+
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
10+
const res = await fetch(url, options);
11+
if (!res.ok) {
12+
let errorDetail;
13+
try {
14+
const body = await res.json();
15+
errorDetail = body.detail || res.statusText;
16+
} catch {
17+
errorDetail = res.statusText;
18+
}
19+
const error = new Error(`HTTP ${res.status}`);
20+
(error as any).detail = errorDetail;
21+
(error as any).status = res.status;
22+
throw error;
23+
}
24+
return res.json();
25+
}
26+
27+
export async function getStateDbStatus(): Promise<{ exists: boolean }> {
28+
return fetchJson(`${BASE}/statedb/status`);
29+
}
30+
31+
export async function getStateDbTables(): Promise<StateDbTable[]> {
32+
return fetchJson(`${BASE}/statedb/tables`);
33+
}
34+
35+
export async function getStateDbTableData(
36+
table: string,
37+
limit = 100,
38+
offset = 0,
39+
): Promise<StateDbTableData> {
40+
return fetchJson(
41+
`${BASE}/statedb/tables/${encodeURIComponent(table)}?limit=${limit}&offset=${offset}`,
42+
);
43+
}
44+
45+
export async function executeStateDbQuery(
46+
sql: string,
47+
limit?: number,
48+
): Promise<StateDbQueryResult> {
49+
return fetchJson(`${BASE}/statedb/query`, {
50+
method: "POST",
51+
headers: { "Content-Type": "application/json" },
52+
body: JSON.stringify({ sql, limit }),
53+
});
54+
}

src/uipath/dev/server/frontend/src/components/explorer/ExplorerSidebar.tsx

Lines changed: 151 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { useEffect, useCallback } from "react";
1+
import { useEffect, useCallback, useState } from "react";
22
import { useExplorerStore } from "../../store/useExplorerStore";
33
import { useHashRoute } from "../../hooks/useHashRoute";
44
import { listDirectory } from "../../api/explorer-client";
5+
import { getStateDbStatus, getStateDbTables } from "../../api/statedb-client";
6+
import type { StateDbTable } from "../../types/statedb";
57

68
function FileTreeNode({ path, name, type, depth }: {
79
path: string;
@@ -113,9 +115,114 @@ function FileTreeNode({ path, name, type, depth }: {
113115
);
114116
}
115117

118+
function StateDbSection({ onDbMissing }: { onDbMissing: () => void }) {
119+
const [tables, setTables] = useState<StateDbTable[]>([]);
120+
const [expanded, setExpanded] = useState(true);
121+
const [refreshing, setRefreshing] = useState(false);
122+
const { stateDbTable, navigate } = useHashRoute();
123+
124+
const refresh = useCallback(() => {
125+
setRefreshing(true);
126+
getStateDbStatus()
127+
.then(({ exists }) => {
128+
if (!exists) {
129+
onDbMissing();
130+
return;
131+
}
132+
return getStateDbTables().then(setTables);
133+
})
134+
.catch(console.error)
135+
.finally(() => setRefreshing(false));
136+
}, [onDbMissing]);
137+
138+
useEffect(() => { refresh(); }, [refresh]);
139+
140+
return (
141+
<div>
142+
{/* Section header */}
143+
<div className="flex items-center">
144+
<button
145+
onClick={() => setExpanded(!expanded)}
146+
className="flex-1 text-left flex items-center gap-1 py-[5px] text-[11px] uppercase tracking-wider font-semibold cursor-pointer"
147+
style={{ paddingLeft: "12px", background: "none", border: "none", color: "var(--text-muted)" }}
148+
>
149+
<svg
150+
width="10"
151+
height="10"
152+
viewBox="0 0 10 10"
153+
fill="currentColor"
154+
style={{ transform: expanded ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.15s" }}
155+
>
156+
<path d="M3 1.5L7 5L3 8.5z" />
157+
</svg>
158+
State Database
159+
</button>
160+
<button
161+
onClick={(e) => { e.stopPropagation(); refresh(); }}
162+
className="shrink-0 flex items-center justify-center w-5 h-5 rounded cursor-pointer"
163+
style={{ background: "none", border: "none", color: "var(--text-muted)", marginRight: "8px" }}
164+
onMouseEnter={(e) => { e.currentTarget.style.color = "var(--text-primary)"; }}
165+
onMouseLeave={(e) => { e.currentTarget.style.color = "var(--text-muted)"; }}
166+
title="Refresh tables"
167+
>
168+
<svg
169+
width="12"
170+
height="12"
171+
viewBox="0 0 24 24"
172+
fill="none"
173+
stroke="currentColor"
174+
strokeWidth="2"
175+
strokeLinecap="round"
176+
strokeLinejoin="round"
177+
style={refreshing ? { animation: "spin 0.6s linear infinite" } : undefined}
178+
>
179+
<polyline points="23 4 23 10 17 10" />
180+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
181+
</svg>
182+
</button>
183+
</div>
184+
{expanded && tables.map((t) => (
185+
<button
186+
key={t.name}
187+
onClick={() => navigate(`#/explorer/statedb/${encodeURIComponent(t.name)}`)}
188+
className="w-full text-left flex items-center gap-1 py-[3px] text-[13px] cursor-pointer transition-colors"
189+
style={{
190+
paddingLeft: "28px",
191+
paddingRight: "8px",
192+
background: stateDbTable === t.name
193+
? "color-mix(in srgb, var(--accent) 15%, var(--bg-primary))"
194+
: "transparent",
195+
color: stateDbTable === t.name ? "var(--text-primary)" : "var(--text-secondary)",
196+
border: "none",
197+
}}
198+
onMouseEnter={(e) => {
199+
if (stateDbTable !== t.name) e.currentTarget.style.background = "var(--bg-hover)";
200+
}}
201+
onMouseLeave={(e) => {
202+
if (stateDbTable !== t.name) e.currentTarget.style.background = "transparent";
203+
}}
204+
>
205+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--accent)" }} className="shrink-0">
206+
<rect x="3" y="3" width="18" height="18" rx="2" />
207+
<line x1="3" y1="9" x2="21" y2="9" />
208+
<line x1="3" y1="15" x2="21" y2="15" />
209+
<line x1="9" y1="3" x2="9" y2="21" />
210+
</svg>
211+
<span className="truncate flex-1">{t.name}</span>
212+
<span className="text-[10px] shrink-0" style={{ color: "var(--text-muted)" }}>
213+
{t.row_count}
214+
</span>
215+
</button>
216+
))}
217+
</div>
218+
);
219+
}
220+
116221
export default function ExplorerSidebar() {
117222
const rootChildren = useExplorerStore((s) => s.children[""]);
118223
const { setChildren } = useExplorerStore();
224+
const [hasStateDb, setHasStateDb] = useState(false);
225+
const [filesExpanded, setFilesExpanded] = useState(true);
119226

120227
// Load root directory on mount
121228
useEffect(() => {
@@ -126,22 +233,51 @@ export default function ExplorerSidebar() {
126233
}
127234
}, [rootChildren, setChildren]);
128235

236+
// Check if state.db exists
237+
useEffect(() => {
238+
getStateDbStatus()
239+
.then(({ exists }) => setHasStateDb(exists))
240+
.catch(() => setHasStateDb(false));
241+
}, []);
242+
129243
return (
130244
<div className="flex-1 overflow-y-auto py-1">
131-
{rootChildren ? (
132-
rootChildren.map((entry) => (
133-
<FileTreeNode
134-
key={entry.path}
135-
path={entry.path}
136-
name={entry.name}
137-
type={entry.type}
138-
depth={0}
139-
/>
140-
))
141-
) : (
142-
<p className="text-[11px] px-3 py-2" style={{ color: "var(--text-muted)" }}>
143-
Loading...
144-
</p>
245+
{hasStateDb && (
246+
<StateDbSection onDbMissing={() => setHasStateDb(false)} />
247+
)}
248+
{/* Collapsible FILES section */}
249+
<button
250+
onClick={() => setFilesExpanded(!filesExpanded)}
251+
className="w-full text-left flex items-center gap-1 py-[5px] text-[11px] uppercase tracking-wider font-semibold cursor-pointer"
252+
style={{ paddingLeft: "12px", paddingRight: "8px", background: "none", border: "none", color: "var(--text-muted)" }}
253+
>
254+
<svg
255+
width="10"
256+
height="10"
257+
viewBox="0 0 10 10"
258+
fill="currentColor"
259+
style={{ transform: filesExpanded ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.15s" }}
260+
>
261+
<path d="M3 1.5L7 5L3 8.5z" />
262+
</svg>
263+
Files
264+
</button>
265+
{filesExpanded && (
266+
rootChildren ? (
267+
rootChildren.map((entry) => (
268+
<FileTreeNode
269+
key={entry.path}
270+
path={entry.path}
271+
name={entry.name}
272+
type={entry.type}
273+
depth={0}
274+
/>
275+
))
276+
) : (
277+
<p className="text-[11px] px-3 py-2" style={{ color: "var(--text-muted)" }}>
278+
Loading...
279+
</p>
280+
)
145281
)}
146282
</div>
147283
);

0 commit comments

Comments
 (0)