Skip to content

Commit 9acab4e

Browse files
committed
Add Metadata Browser to Studio app
Introduces a new Metadata Browser page in the Studio app for exploring runtime metadata such as objects, views, permissions, and more. Updates the CLI to serve all metadata file types, adds a sidebar link for navigation, and registers the new route in the app.
1 parent a815c14 commit 9acab4e

5 files changed

Lines changed: 222 additions & 6 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export * from './projects';
2-
export * from './tasks';
31
export * from './kitchen_sink';
2+
export * from './projects';
3+
export * from './tasks';

packages/cli/src/commands/studio.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,13 @@ export async function startStudio(options: { port: number; dir: string, open?: b
118118
}
119119

120120
if (req.url?.startsWith('/api/schema/files')) {
121-
// List all .object.yml files
121+
// List all metadata files
122122
try {
123-
// Find all object.yml files relative to rootDir
123+
// Find all *.*.yml files relative to rootDir
124124
// Note: User might have configured objectql with specific source paths.
125125
// We ignore common build folders to avoid duplicates/editing compiled files.
126-
const files = await glob('**/*.object.yml', {
126+
// We broadly match all objectql-like files: *.object.yml, *.view.yml, etc.
127+
const files = await glob('**/*.{object,app,view,permission,report,validation,workflow,form,data,hook,action}.yml', {
127128
cwd: rootDir,
128129
ignore: ['node_modules/**', 'dist/**', 'build/**', 'out/**', '.git/**', '.next/**']
129130
});

packages/studio/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Sidebar } from '@/components/Sidebar';
33
import { Dashboard } from '@/pages/Dashboard';
44
import { ObjectView } from '@/pages/ObjectView';
55
import { SchemaEditor } from '@/pages/SchemaEditor';
6+
import { MetadataBrowser } from '@/pages/MetadataBrowser';
67
import './index.css';
78

89
// Wrapper to extract params
@@ -21,6 +22,7 @@ function App() {
2122
<Routes>
2223
<Route path="/" element={<Dashboard />} />
2324
<Route path="/schema" element={<SchemaEditor />} />
25+
<Route path="/metadata" element={<MetadataBrowser />} />
2426
<Route path="/object/:name" element={<ObjectViewWrapper />} />
2527
</Routes>
2628
</main>
@@ -30,3 +32,4 @@ function App() {
3032
}
3133

3234
export default App;
35+

packages/studio/src/components/Sidebar.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { NavLink } from 'react-router-dom';
22
import { useMetadata } from '@/hooks/use-metadata';
3-
import { Database, Home, Loader2, Table2, FileCode, BookOpen } from 'lucide-react';
3+
import { Database, Home, Loader2, Table2, FileCode, BookOpen, Layers } from 'lucide-react';
44
import { cn } from '@/lib/utils';
55

6+
67
export function Sidebar() {
78
const { objects, loading, error } = useMetadata();
89

@@ -33,6 +34,17 @@ export function Sidebar() {
3334
</h4>
3435
</div>
3536

37+
<NavLink
38+
to="/metadata"
39+
className={({isActive}) => cn(
40+
"flex items-center space-x-2 px-4 py-2 rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground",
41+
isActive && "bg-accent/50 text-accent-foreground"
42+
)}
43+
>
44+
<Layers className="h-4 w-4" />
45+
<span>Metadata Explorer</span>
46+
</NavLink>
47+
3648
<NavLink
3749
to="/schema"
3850
className={({isActive}) => cn(
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { useState, useEffect } from 'react';
2+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
3+
import { Button } from '@/components/ui/button';
4+
import { Loader2, ArrowLeft, FileJson, Layers, Shield, FileText, Activity, Layout, AlertTriangle, Workflow } from 'lucide-react';
5+
import { cn } from '@/lib/utils';
6+
7+
// Helper to format JSON
8+
function JsonViewer({ data }: { data: any }) {
9+
return (
10+
<pre className="bg-muted p-4 rounded-md overflow-auto text-xs font-mono max-h-[600px]">
11+
{JSON.stringify(data, null, 2)}
12+
</pre>
13+
);
14+
}
15+
16+
const METADATA_TYPES = [
17+
{ id: 'objects', label: 'Objects', icon: Layers },
18+
{ id: 'view', label: 'Views', icon: Layout },
19+
{ id: 'permission', label: 'Permissions', icon: Shield },
20+
{ id: 'report', label: 'Reports', icon: FileText },
21+
{ id: 'validation', label: 'Validations', icon: AlertTriangle },
22+
{ id: 'workflow', label: 'Workflows', icon: Workflow },
23+
{ id: 'form', label: 'Forms', icon: Activity },
24+
{ id: 'app', label: 'Apps', icon: FileJson },
25+
];
26+
27+
export function MetadataBrowser() {
28+
// State
29+
const [selectedType, setSelectedType] = useState<string | null>(null);
30+
const [selectedItem, setSelectedItem] = useState<string | null>(null);
31+
32+
const [items, setItems] = useState<any[]>([]);
33+
const [itemDetail, setItemDetail] = useState<any>(null);
34+
35+
const [loading, setLoading] = useState(false);
36+
const [error, setError] = useState<string | null>(null);
37+
38+
// Fetch list when type changes
39+
useEffect(() => {
40+
if (!selectedType) return;
41+
42+
setLoading(true);
43+
setItems([]);
44+
setSelectedItem(null);
45+
setError(null);
46+
47+
fetch(`/api/metadata/${selectedType}`)
48+
.then(async res => {
49+
if (!res.ok) throw new Error(`Failed to fetch ${selectedType}`);
50+
const data = await res.json();
51+
// API returns { [type]: [...] }
52+
const list = data[selectedType] || data.objects || [];
53+
setItems(list);
54+
})
55+
.catch(err => setError(err.message))
56+
.finally(() => setLoading(false));
57+
58+
}, [selectedType]);
59+
60+
// Fetch detail when item changes
61+
useEffect(() => {
62+
if (!selectedType || !selectedItem) return;
63+
64+
setLoading(true);
65+
setItemDetail(null);
66+
setError(null);
67+
68+
// For objects, the ID is the name. For others, it relies on file structure or id
69+
fetch(`/api/metadata/${selectedType}/${selectedItem}`)
70+
.then(async res => {
71+
if (!res.ok) throw new Error(`Failed to fetch detail for ${selectedItem}`);
72+
const data = await res.json();
73+
setItemDetail(data);
74+
})
75+
.catch(err => setError(err.message))
76+
.finally(() => setLoading(false));
77+
78+
}, [selectedType, selectedItem]);
79+
80+
// --- Render: Main Menu (Type Selection) ---
81+
if (!selectedType) {
82+
return (
83+
<div className="p-8 max-w-6xl mx-auto">
84+
<h1 className="text-3xl font-bold mb-6">Metadata Registry</h1>
85+
<p className="text-muted-foreground mb-8">
86+
Browse the active runtime metadata loaded in the ObjectQL engine.
87+
</p>
88+
89+
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
90+
{METADATA_TYPES.map((type) => {
91+
const Icon = type.icon;
92+
return (
93+
<Card
94+
key={type.id}
95+
className="cursor-pointer hover:bg-accent/50 transition-colors border-2 hover:border-primary/50"
96+
onClick={() => setSelectedType(type.id)}
97+
>
98+
<CardHeader className="flex flex-row items-center space-y-0 space-x-4">
99+
<div className="p-2 bg-primary/10 rounded-full text-primary">
100+
<Icon className="h-6 w-6" />
101+
</div>
102+
<CardTitle className="text-xl">{type.label}</CardTitle>
103+
</CardHeader>
104+
<CardContent>
105+
<p className="text-sm text-muted-foreground">
106+
Browse {type.label.toLowerCase()} definitions
107+
</p>
108+
</CardContent>
109+
</Card>
110+
);
111+
})}
112+
</div>
113+
</div>
114+
);
115+
}
116+
117+
// --- Render: List or Detail ---
118+
return (
119+
<div className="flex h-screen overflow-hidden">
120+
{/* Left Panel: List */}
121+
<div className="w-1/3 border-r bg-card flex flex-col">
122+
<div className="p-4 border-b flex items-center space-x-2">
123+
<Button variant="ghost" size="sm" onClick={() => setSelectedType(null)}>
124+
<ArrowLeft className="h-4 w-4" />
125+
</Button>
126+
<h2 className="font-semibold text-lg capitalize">{selectedType} List</h2>
127+
</div>
128+
129+
<div className="flex-1 overflow-auto p-2 space-y-2">
130+
{error && (
131+
<div className="p-4 text-sm text-red-500 bg-red-50 rounded mb-2">
132+
Error: {error}
133+
</div>
134+
)}
135+
136+
{loading && items.length === 0 && (
137+
<div className="flex justify-center p-8 text-muted-foreground">
138+
<Loader2 className="h-6 w-6 animate-spin mr-2" /> Loading...
139+
</div>
140+
)}
141+
142+
{!loading && items.map((item: any) => {
143+
const id = item.name || item.id;
144+
return (
145+
<div
146+
key={id}
147+
onClick={() => setSelectedItem(id)}
148+
className={cn(
149+
"p-3 rounded-md cursor-pointer text-sm font-medium transition-colors border",
150+
selectedItem === id
151+
? "bg-primary text-primary-foreground border-primary"
152+
: "hover:bg-accent border-transparent"
153+
)}
154+
>
155+
<div className="flex justify-between items-center">
156+
<span>{item.label || item.name}</span>
157+
{item.name !== item.label && (
158+
<span className="text-xs opacity-70 ml-2 font-mono">({item.name})</span>
159+
)}
160+
</div>
161+
</div>
162+
);
163+
})}
164+
165+
{!loading && items.length === 0 && (
166+
<div className="text-center p-8 text-muted-foreground text-sm">
167+
No {selectedType} found.
168+
</div>
169+
)}
170+
</div>
171+
</div>
172+
173+
{/* Right Panel: Detail */}
174+
<div className="flex-1 bg-muted/20 flex flex-col h-full overflow-hidden">
175+
{!selectedItem ? (
176+
<div className="flex-1 flex items-center justify-center text-muted-foreground">
177+
Select an item to view details
178+
</div>
179+
) : (
180+
<div className="flex-1 flex flex-col h-full overflow-hidden">
181+
<div className="p-4 border-b bg-card">
182+
<h2 className="text-xl font-bold">{selectedItem}</h2>
183+
</div>
184+
<div className="flex-1 overflow-auto p-6">
185+
{loading && !itemDetail ? (
186+
<div className="flex items-center text-muted-foreground">
187+
<Loader2 className="h-4 w-4 animate-spin mr-2" /> Loading details...
188+
</div>
189+
) : (
190+
<JsonViewer data={itemDetail} />
191+
)}
192+
</div>
193+
</div>
194+
)}
195+
</div>
196+
</div>
197+
);
198+
}
199+
200+
export default MetadataBrowser;

0 commit comments

Comments
 (0)