Skip to content

Commit 80977b1

Browse files
committed
Add Data Explorer to dashboard and update server config
Introduces a Data Explorer component to the dashboard for browsing and viewing database objects and records. Updates static file serving in app.module.ts to prevent fallback to index.html for non-UI routes. Sets 'rootDir' in tsconfig.json to './src' for improved TypeScript project structure.
1 parent 9191ce6 commit 80977b1

File tree

3 files changed

+181
-6
lines changed

3 files changed

+181
-6
lines changed

packages/server/src/app.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { join } from 'path';
1414
ServeStaticModule.forRoot({
1515
rootPath: join(__dirname, '../../../ui/dist'),
1616
serveRoot: '/assets/ui',
17+
renderPath: '/assets/ui/*' // Prevent fallback to index.html for api or other routes
1718
}),
1819
],
1920
controllers: [AppController, ViewsController],

packages/server/src/views/dashboard.liquid

Lines changed: 179 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,184 @@
3535
);
3636
}
3737

38+
function DataExplorer({ user }) {
39+
const [objects, setObjects] = useState([]);
40+
const [selectedObject, setSelectedObject] = useState(null);
41+
const [data, setData] = useState([]);
42+
const [loading, setLoading] = useState(false);
43+
const [error, setError] = useState(null);
44+
45+
const getHeaders = () => {
46+
const headers = { 'Content-Type': 'application/json' };
47+
// Since this is the Admin Dashboard, we use the admin identity to browse all data.
48+
// In production, you might want to pass the actual user ID and handle specific permissions,
49+
// or verify the session on the backend and grant admin context.
50+
headers['x-user-id'] = 'admin';
51+
return headers;
52+
};
53+
54+
const fetchObjects = () => {
55+
fetch('/api/object/_schema/object', { headers: getHeaders() })
56+
.then(async res => {
57+
if (!res.ok) throw new Error(await res.text() || res.statusText);
58+
return res.json();
59+
})
60+
.then(result => {
61+
const objNames = Object.keys(result);
62+
setObjects(objNames);
63+
if (objNames.length > 0 && !selectedObject) {
64+
setSelectedObject(objNames[0]);
65+
}
66+
})
67+
.catch(err => console.error("Failed to fetch schema:", err));
68+
};
69+
70+
useEffect(() => {
71+
if (user) fetchObjects();
72+
}, [user]);
73+
74+
const fetchData = () => {
75+
if (!selectedObject) return;
76+
setLoading(true);
77+
setError(null);
78+
79+
fetch(`/api/object/${selectedObject}`, { headers: getHeaders() })
80+
.then(async res => {
81+
if (!res.ok) {
82+
const contentType = res.headers.get("content-type");
83+
if (contentType && contentType.indexOf("application/json") !== -1) {
84+
const json = await res.json();
85+
throw new Error(json.error || "Failed to load data");
86+
}
87+
throw new Error(await res.text() || res.statusText);
88+
}
89+
return res.json();
90+
})
91+
.then(result => {
92+
// Normalize result to array
93+
const items = Array.isArray(result) ? result : (result.list || []);
94+
setData(items);
95+
})
96+
.catch(err => {
97+
console.error(err);
98+
setError(err.message);
99+
setData([]);
100+
})
101+
.finally(() => setLoading(false));
102+
};
103+
104+
useEffect(() => {
105+
if (user) fetchData();
106+
}, [selectedObject, user]);
107+
108+
return (
109+
<div className="flex h-[calc(100vh-140px)] gap-6">
110+
{/* Object List Sidebar */}
111+
<div className="w-64 flex-shrink-0 bg-white rounded-xl border border-gray-200/60 shadow-sm overflow-hidden flex flex-col">
112+
<div className="p-4 border-b border-gray-100 bg-gray-50/50 flex justify-between items-center">
113+
<h3 className="font-semibold text-gray-900 text-xs uppercase tracking-wide">Database Objects</h3>
114+
<button onClick={fetchObjects} className="text-gray-400 hover:text-blue-600 transition-colors" title="Refresh Objects">
115+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
116+
</button>
117+
</div>
118+
<div className="overflow-y-auto flex-1 p-2 space-y-1">
119+
{objects.length === 0 ? (
120+
<div className="p-4 text-center text-gray-400 text-sm">No objects found</div>
121+
) : (
122+
objects.map(obj => (
123+
<button
124+
key={obj}
125+
onClick={() => setSelectedObject(obj)}
126+
className={`w-full text-left px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
127+
selectedObject === obj
128+
? 'bg-blue-50 text-blue-700'
129+
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
130+
}`}
131+
>
132+
{obj}
133+
</button>
134+
))
135+
)}
136+
</div>
137+
</div>
138+
139+
{/* Data Table Area */}
140+
<div className="flex-1 bg-white rounded-xl border border-gray-200/60 shadow-sm overflow-hidden flex flex-col">
141+
<div className="min-h-[60px] px-6 border-b border-gray-100 flex justify-between items-center bg-white">
142+
<div>
143+
<h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
144+
{selectedObject}
145+
<span className="px-2 py-0.5 rounded-full bg-gray-100 text-gray-500 text-xs font-medium">{data.length} records</span>
146+
</h3>
147+
</div>
148+
<div className="flex gap-2">
149+
<Button onClick={fetchData} variant="secondary" className="h-8 text-xs px-3 shadow-none border-gray-200">
150+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-1.5"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
151+
Refresh
152+
</Button>
153+
<Button variant="secondary" className="h-8 text-xs px-3 shadow-none border-gray-200">
154+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-1.5"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
155+
Filter
156+
</Button>
157+
<Button className="h-8 text-xs px-3 shadow-none bg-black hover:bg-gray-800">
158+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-1.5"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
159+
New Record
160+
</Button>
161+
</div>
162+
</div>
163+
164+
<div className="flex-1 overflow-auto bg-white relative">
165+
{loading && (
166+
<div className="absolute inset-0 bg-white/50 backdrop-blur-sm z-10 flex items-center justify-center">
167+
<Spinner className="w-6 h-6 text-blue-500" />
168+
</div>
169+
)}
170+
171+
{error ? (
172+
<div className="flex flex-col items-center justify-center h-full text-red-500 p-8 text-center">
173+
<Icons.Layout className="w-8 h-8 mb-2 opacity-50" />
174+
<p className="font-medium">Error loading data</p>
175+
<p className="text-sm opacity-75 max-w-md break-words">{error}</p>
176+
<Button onClick={fetchData} variant="secondary" className="mt-4">Try Again</Button>
177+
</div>
178+
) : data.length === 0 ? (
179+
<div className="flex flex-col items-center justify-center h-full text-gray-300 p-8">
180+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="mb-4 opacity-50"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg>
181+
<p className="text-sm font-medium text-gray-400">No records found for {selectedObject}</p>
182+
</div>
183+
) : (
184+
<table className="w-full text-left border-collapse text-sm">
185+
<thead className="bg-gray-50/80 sticky top-0 z-10 backdrop-blur-sm">
186+
<tr>
187+
{Object.keys(data[0] || {}).map(key => (
188+
<th key={key} className="px-6 py-3 font-semibold text-gray-500 border-b border-gray-100 whitespace-nowrap text-xs uppercase tracking-wider">
189+
{key}
190+
</th>
191+
))}
192+
</tr>
193+
</thead>
194+
<tbody className="divide-y divide-gray-100">
195+
{data.map((row, idx) => (
196+
<tr key={idx} className="hover:bg-blue-50/30 transition-colors group cursor-default">
197+
{Object.keys(data[0] || {}).map(key => (
198+
<td key={key} className="px-6 py-3.5 text-gray-700 whitespace-nowrap max-w-xs overflow-hidden text-ellipsis font-normal group-hover:text-gray-900">
199+
{typeof row[key] === 'object' ?
200+
<span className="font-mono text-xs text-gray-400">{JSON.stringify(row[key])}</span> :
201+
String(row[key])
202+
}
203+
</td>
204+
))}
205+
</tr>
206+
))}
207+
</tbody>
208+
</table>
209+
)}
210+
</div>
211+
</div>
212+
</div>
213+
);
214+
}
215+
38216
function DashboardApp() {
39217
const [user, setUser] = useState(null);
40218
const [loading, setLoading] = useState(true);
@@ -154,12 +332,7 @@
154332
)}
155333

156334
{activeTab === 'data' && (
157-
<Card title="Data Explorer" description="Browse database objects and schemas directly.">
158-
<div className="p-12 text-center text-gray-400">
159-
<svg className="w-16 h-16 mx-auto mb-4 opacity-20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
160-
<p>Select an object to inspect data.</p>
161-
</div>
162-
</Card>
335+
<DataExplorer user={user} />
163336
)}
164337

165338
</div>

packages/server/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"target": "ES2023",
1010
"sourceMap": true,
1111
"outDir": "./dist",
12+
"rootDir": "./src",
1213
"baseUrl": "./",
1314
"incremental": true,
1415
"skipLibCheck": true,

0 commit comments

Comments
 (0)