@objectql/studio 1.3.1 → 1.5.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/CHANGELOG.md +19 -0
- package/dist/assets/index-CQ64O3eC.js +416 -0
- package/dist/assets/index-ClGKK4W3.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +13 -2
- package/postcss.config.cjs +6 -0
- package/src/App.tsx +22 -36
- package/src/components/FileEditor.tsx +95 -0
- package/src/components/Sidebar.tsx +97 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/hooks/use-metadata.ts +39 -0
- package/src/index.css +57 -21
- package/src/lib/utils.ts +6 -0
- package/src/pages/Dashboard.tsx +36 -0
- package/src/pages/ObjectView.tsx +179 -0
- package/src/pages/SchemaEditor.tsx +85 -0
- package/tailwind.config.cjs +77 -0
- package/tsconfig.json +5 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +6 -0
- package/dist/assets/index-BkeseS5P.js +0 -67
- package/dist/assets/index-YYlEozoH.css +0 -1
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ObjectQL Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/studio/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/studio/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/studio/assets/index-CQ64O3eC.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/studio/assets/index-ClGKK4W3.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectql/studio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Web-based admin studio for ObjectQL database management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"ag-grid-community": "^31.0.0",
|
|
8
|
+
"ag-grid-react": "^31.0.0",
|
|
9
|
+
"class-variance-authority": "^0.7.0",
|
|
10
|
+
"clsx": "^2.1.0",
|
|
11
|
+
"lucide-react": "^0.300.0",
|
|
7
12
|
"react": "^18.2.0",
|
|
8
13
|
"react-dom": "^18.2.0",
|
|
9
|
-
"react-router-dom": "^6.20.0"
|
|
14
|
+
"react-router-dom": "^6.20.0",
|
|
15
|
+
"tailwind-merge": "^2.2.0",
|
|
16
|
+
"tailwindcss-animate": "^1.0.7"
|
|
10
17
|
},
|
|
11
18
|
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.10.0",
|
|
12
20
|
"@types/react": "^18.2.43",
|
|
13
21
|
"@types/react-dom": "^18.2.17",
|
|
14
22
|
"@vitejs/plugin-react": "^4.2.1",
|
|
23
|
+
"autoprefixer": "^10.4.16",
|
|
24
|
+
"postcss": "^8.4.32",
|
|
25
|
+
"tailwindcss": "^3.4.0",
|
|
15
26
|
"typescript": "^5.3.0",
|
|
16
27
|
"vite": "^5.0.8"
|
|
17
28
|
},
|
package/src/App.tsx
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
|
-
import { BrowserRouter, Routes, Route,
|
|
2
|
-
import '
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
1
|
+
import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';
|
|
2
|
+
import { Sidebar } from '@/components/Sidebar';
|
|
3
|
+
import { Dashboard } from '@/pages/Dashboard';
|
|
4
|
+
import { ObjectView } from '@/pages/ObjectView';
|
|
5
|
+
import { SchemaEditor } from '@/pages/SchemaEditor';
|
|
6
|
+
import './index.css';
|
|
7
|
+
|
|
8
|
+
// Wrapper to extract params
|
|
9
|
+
function ObjectViewWrapper() {
|
|
10
|
+
const { name } = useParams();
|
|
11
|
+
if (!name) return null;
|
|
12
|
+
return <ObjectView objectName={name} />;
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
function App() {
|
|
9
16
|
return (
|
|
10
|
-
<BrowserRouter basename="/
|
|
11
|
-
<div className="
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
<Link to="/schema" className="nav-link">Schema</Link>
|
|
20
|
-
</nav>
|
|
21
|
-
</div>
|
|
22
|
-
</header>
|
|
23
|
-
|
|
24
|
-
<main className="app-main">
|
|
25
|
-
<div className="container">
|
|
26
|
-
<Routes>
|
|
27
|
-
<Route path="/" element={<ObjectList />} />
|
|
28
|
-
<Route path="/object/:objectName" element={<DataGrid />} />
|
|
29
|
-
<Route path="/object/:objectName/:recordId" element={<RecordDetail />} />
|
|
30
|
-
<Route path="/schema" element={<SchemaInspector />} />
|
|
31
|
-
<Route path="/schema/:objectName" element={<SchemaInspector />} />
|
|
32
|
-
</Routes>
|
|
33
|
-
</div>
|
|
17
|
+
<BrowserRouter basename="/studio">
|
|
18
|
+
<div className="flex h-screen bg-background text-foreground overflow-hidden font-sans antialiased">
|
|
19
|
+
<Sidebar />
|
|
20
|
+
<main className="flex-1 overflow-auto bg-muted/20">
|
|
21
|
+
<Routes>
|
|
22
|
+
<Route path="/" element={<Dashboard />} />
|
|
23
|
+
<Route path="/schema" element={<SchemaEditor />} />
|
|
24
|
+
<Route path="/object/:name" element={<ObjectViewWrapper />} />
|
|
25
|
+
</Routes>
|
|
34
26
|
</main>
|
|
35
|
-
|
|
36
|
-
<footer className="app-footer">
|
|
37
|
-
<div className="container">
|
|
38
|
-
<p>ObjectQL Console v0.1.0 - Universal Database Management</p>
|
|
39
|
-
</div>
|
|
40
|
-
</footer>
|
|
41
27
|
</div>
|
|
42
28
|
</BrowserRouter>
|
|
43
29
|
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
4
|
+
import { Save } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface FileEditorProps {
|
|
7
|
+
filePath: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
onSaveSuccess?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FileEditor({ filePath, className, onSaveSuccess }: FileEditorProps) {
|
|
13
|
+
const [content, setContent] = useState('');
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const [saving, setSaving] = useState(false);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
const [status, setStatus] = useState<string | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (filePath) {
|
|
21
|
+
loadFile(filePath);
|
|
22
|
+
}
|
|
23
|
+
}, [filePath]);
|
|
24
|
+
|
|
25
|
+
const loadFile = async (file: string) => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`/api/schema/content?file=${encodeURIComponent(file)}`);
|
|
30
|
+
if (!res.ok) throw new Error('Failed to load file');
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
setContent(text);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(e);
|
|
35
|
+
setError('Failed to read file content');
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const saveFile = async () => {
|
|
42
|
+
if (!filePath) return;
|
|
43
|
+
setSaving(true);
|
|
44
|
+
setStatus(null);
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`/api/schema/content?file=${encodeURIComponent(filePath)}`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: content
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) throw new Error('Failed to save');
|
|
51
|
+
setStatus('File saved successfully!');
|
|
52
|
+
setTimeout(() => setStatus(null), 3000);
|
|
53
|
+
if (onSaveSuccess) onSaveSuccess();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
setError('Failed to save file');
|
|
56
|
+
} finally {
|
|
57
|
+
setSaving(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Card className={`flex flex-col h-full ${className}`}>
|
|
63
|
+
<CardHeader className="py-2 px-4 flex flex-row items-center justify-between border-b space-y-0 min-h-[50px]">
|
|
64
|
+
<CardTitle className="text-sm font-mono text-muted-foreground flex items-center">
|
|
65
|
+
{filePath}
|
|
66
|
+
</CardTitle>
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
{status && <span className="text-xs text-green-600 font-medium animate-fade-in">{status}</span>}
|
|
69
|
+
{error && <span className="text-xs text-red-600 font-medium">{error}</span>}
|
|
70
|
+
<Button size="sm" onClick={saveFile} disabled={saving || loading}>
|
|
71
|
+
{saving ? 'Saving...' : (
|
|
72
|
+
<>
|
|
73
|
+
<Save className="mr-2 h-3 w-3" /> Save
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</CardHeader>
|
|
79
|
+
<CardContent className="flex-1 p-0 relative min-h-[400px]">
|
|
80
|
+
{loading && (
|
|
81
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10 transition-all backdrop-blur-sm">
|
|
82
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
<textarea
|
|
86
|
+
className="w-full h-full p-4 font-mono text-sm resize-none focus:outline-none bg-muted/10 leading-normal"
|
|
87
|
+
value={content}
|
|
88
|
+
onChange={(e) => setContent(e.target.value)}
|
|
89
|
+
spellCheck={false}
|
|
90
|
+
disabled={loading}
|
|
91
|
+
/>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { NavLink } from 'react-router-dom';
|
|
2
|
+
import { useMetadata } from '@/hooks/use-metadata';
|
|
3
|
+
import { Database, Home, Loader2, Table2, FileCode, BookOpen } from 'lucide-react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
export function Sidebar() {
|
|
7
|
+
const { objects, loading, error } = useMetadata();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="w-64 border-r bg-card h-screen flex flex-col">
|
|
11
|
+
<div className="p-6 border-b flex items-center space-x-2">
|
|
12
|
+
<Database className="h-6 w-6 text-primary" />
|
|
13
|
+
<span className="font-bold text-lg">ObjectQL Studio</span>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className="flex-1 overflow-auto py-4">
|
|
17
|
+
<nav className="space-y-1 px-2">
|
|
18
|
+
<NavLink
|
|
19
|
+
to="/"
|
|
20
|
+
className={({isActive}) => cn(
|
|
21
|
+
"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",
|
|
22
|
+
isActive && "bg-accent/50 text-accent-foreground"
|
|
23
|
+
)}
|
|
24
|
+
end
|
|
25
|
+
>
|
|
26
|
+
<Home className="h-4 w-4" />
|
|
27
|
+
<span>Dashboard</span>
|
|
28
|
+
</NavLink>
|
|
29
|
+
|
|
30
|
+
<div className="pt-4 px-4 pb-2">
|
|
31
|
+
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
32
|
+
Development
|
|
33
|
+
</h4>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<NavLink
|
|
37
|
+
to="/schema"
|
|
38
|
+
className={({isActive}) => cn(
|
|
39
|
+
"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",
|
|
40
|
+
isActive && "bg-accent/50 text-accent-foreground"
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<FileCode className="h-4 w-4" />
|
|
44
|
+
<span>Schema Editor</span>
|
|
45
|
+
</NavLink>
|
|
46
|
+
|
|
47
|
+
<a
|
|
48
|
+
href="/swagger"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noreferrer"
|
|
51
|
+
className="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"
|
|
52
|
+
>
|
|
53
|
+
<BookOpen className="h-4 w-4" />
|
|
54
|
+
<span>API Docs</span>
|
|
55
|
+
</a>
|
|
56
|
+
|
|
57
|
+
<div className="pt-4 px-4 pb-2">
|
|
58
|
+
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
|
59
|
+
Collections
|
|
60
|
+
</h4>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{loading && (
|
|
64
|
+
<div className="px-4 py-2 text-sm text-muted-foreground flex items-center">
|
|
65
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
66
|
+
Loading...
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{error && (
|
|
71
|
+
<div className="px-4 py-2 text-sm text-red-500">
|
|
72
|
+
Failed to load
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{!loading && objects.map(obj => (
|
|
77
|
+
<NavLink
|
|
78
|
+
key={obj.name}
|
|
79
|
+
to={`/object/${obj.name}`}
|
|
80
|
+
className={({isActive}) => cn(
|
|
81
|
+
"flex items-center space-x-2 px-4 py-2 rounded-md text-sm transition-colors hover:bg-accent hover:text-accent-foreground",
|
|
82
|
+
isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground"
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<Table2 className="h-4 w-4" />
|
|
86
|
+
<span>{obj.name}</span>
|
|
87
|
+
</NavLink>
|
|
88
|
+
))}
|
|
89
|
+
</nav>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="p-4 border-t text-xs text-center text-muted-foreground">
|
|
93
|
+
v1.3.1
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
14
|
+
outline:
|
|
15
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: "h-10 px-4 py-2",
|
|
23
|
+
sm: "h-9 rounded-md px-3",
|
|
24
|
+
lg: "h-11 rounded-md px-8",
|
|
25
|
+
icon: "h-10 w-10",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export interface ButtonProps
|
|
36
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {
|
|
38
|
+
asChild?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
42
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
43
|
+
// Note: I am removing @radix-ui/react-slot dependency assumption by falling back to simple render if needed,
|
|
44
|
+
// but for shadcn strict compliance we need it. I'll assume users can install it or I should have added it.
|
|
45
|
+
// I forgot to add @radix-ui/react-slot to package.json.
|
|
46
|
+
// I will stick to standard button for now if I can't depend on radix.
|
|
47
|
+
// Actually, let's keep it simple and just return 'button'.
|
|
48
|
+
const Comp = "button"
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
52
|
+
ref={ref}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
Button.displayName = "Button"
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Card.displayName = "Card"
|
|
19
|
+
|
|
20
|
+
const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
))
|
|
30
|
+
CardHeader.displayName = "CardHeader"
|
|
31
|
+
|
|
32
|
+
const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLParagraphElement,
|
|
34
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<h3
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
))
|
|
42
|
+
CardTitle.displayName = "CardTitle"
|
|
43
|
+
|
|
44
|
+
const CardDescription = React.forwardRef<
|
|
45
|
+
HTMLParagraphElement,
|
|
46
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<p
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
))
|
|
54
|
+
CardDescription.displayName = "CardDescription"
|
|
55
|
+
|
|
56
|
+
const CardContent = React.forwardRef<
|
|
57
|
+
HTMLDivElement,
|
|
58
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
61
|
+
))
|
|
62
|
+
CardContent.displayName = "CardContent"
|
|
63
|
+
|
|
64
|
+
const CardFooter = React.forwardRef<
|
|
65
|
+
HTMLDivElement,
|
|
66
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<div
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
))
|
|
74
|
+
CardFooter.displayName = "CardFooter"
|
|
75
|
+
|
|
76
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface Field {
|
|
4
|
+
type: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ObjectConfig {
|
|
10
|
+
name: string;
|
|
11
|
+
fields: Record<string, Field>;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useMetadata() {
|
|
16
|
+
const [objects, setObjects] = useState<ObjectConfig[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState<Error | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
fetch('/api/metadata')
|
|
22
|
+
.then(async res => {
|
|
23
|
+
if (!res.ok) throw new Error('Failed to fetch metadata');
|
|
24
|
+
// The API might return { objects: [...] } or just [...]
|
|
25
|
+
// Assuming standard objectql server returns { objects: [...] } or Array
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
// Check format
|
|
28
|
+
const list = Array.isArray(data) ? data : (data.objects || []);
|
|
29
|
+
setObjects(list);
|
|
30
|
+
})
|
|
31
|
+
.catch(err => {
|
|
32
|
+
console.error(err);
|
|
33
|
+
setError(err);
|
|
34
|
+
})
|
|
35
|
+
.finally(() => setLoading(false));
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
return { objects, loading, error };
|
|
39
|
+
}
|
package/src/index.css
CHANGED
|
@@ -1,23 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 222.2 84% 4.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
+
--primary: 222.2 47.4% 11.2%;
|
|
14
|
+
--primary-foreground: 210 40% 98%;
|
|
15
|
+
--secondary: 210 40% 96.1%;
|
|
16
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
17
|
+
--muted: 210 40% 96.1%;
|
|
18
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
+
--accent: 210 40% 96.1%;
|
|
20
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 210 40% 98%;
|
|
23
|
+
--border: 214.3 31.8% 91.4%;
|
|
24
|
+
--input: 214.3 31.8% 91.4%;
|
|
25
|
+
--ring: 222.2 84% 4.9%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 222.2 84% 4.9%;
|
|
31
|
+
--foreground: 210 40% 98%;
|
|
32
|
+
--card: 222.2 84% 4.9%;
|
|
33
|
+
--card-foreground: 210 40% 98%;
|
|
34
|
+
--popover: 222.2 84% 4.9%;
|
|
35
|
+
--popover-foreground: 210 40% 98%;
|
|
36
|
+
--primary: 210 40% 98%;
|
|
37
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
38
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
39
|
+
--secondary-foreground: 210 40% 98%;
|
|
40
|
+
--muted: 217.2 32.6% 17.5%;
|
|
41
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
42
|
+
--accent: 217.2 32.6% 17.5%;
|
|
43
|
+
--accent-foreground: 210 40% 98%;
|
|
44
|
+
--destructive: 0 62.8% 30.6%;
|
|
45
|
+
--destructive-foreground: 210 40% 98%;
|
|
46
|
+
--border: 217.2 32.6% 17.5%;
|
|
47
|
+
--input: 217.2 32.6% 17.5%;
|
|
48
|
+
--ring: 212.7 26.8% 83.9%;
|
|
49
|
+
}
|
|
5
50
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
code {
|
|
17
|
-
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
18
|
-
monospace;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
#root {
|
|
22
|
-
min-height: 100vh;
|
|
51
|
+
|
|
52
|
+
@layer base {
|
|
53
|
+
* {
|
|
54
|
+
@apply border-border;
|
|
55
|
+
}
|
|
56
|
+
body {
|
|
57
|
+
@apply bg-background text-foreground;
|
|
58
|
+
}
|
|
23
59
|
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useMetadata } from '@/hooks/use-metadata';
|
|
2
|
+
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card';
|
|
3
|
+
|
|
4
|
+
export function Dashboard() {
|
|
5
|
+
const { objects } = useMetadata();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="p-8">
|
|
9
|
+
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
|
10
|
+
|
|
11
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
12
|
+
<Card>
|
|
13
|
+
<CardHeader>
|
|
14
|
+
<CardTitle>Total Collections</CardTitle>
|
|
15
|
+
<CardDescription>Registered objects in schema</CardDescription>
|
|
16
|
+
</CardHeader>
|
|
17
|
+
<CardContent>
|
|
18
|
+
<div className="text-2xl font-bold">{objects.length}</div>
|
|
19
|
+
</CardContent>
|
|
20
|
+
</Card>
|
|
21
|
+
|
|
22
|
+
{objects.map(obj => (
|
|
23
|
+
<Card key={obj.name} className="hover:bg-accent/10 transition-colors">
|
|
24
|
+
<CardHeader>
|
|
25
|
+
<CardTitle>{obj.name}</CardTitle>
|
|
26
|
+
<CardDescription className="truncate">{obj.description || 'No description'}</CardDescription>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent className="flex justify-between items-center text-sm">
|
|
29
|
+
<span className="text-muted-foreground">{Object.keys(obj.fields).length} fields</span>
|
|
30
|
+
</CardContent>
|
|
31
|
+
</Card>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|