@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { AgGridReact } from 'ag-grid-react';
|
|
3
|
+
import { ColDef } from 'ag-grid-community';
|
|
4
|
+
import 'ag-grid-community/styles/ag-grid.css';
|
|
5
|
+
import 'ag-grid-community/styles/ag-theme-quartz.css';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { RefreshCw, Plus, Table2, FileCode } from 'lucide-react';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
import { FileEditor } from '@/components/FileEditor';
|
|
10
|
+
|
|
11
|
+
interface ObjectViewProps {
|
|
12
|
+
objectName: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ObjectView({ objectName }: ObjectViewProps) {
|
|
16
|
+
const [rowData, setRowData] = useState<any[]>([]);
|
|
17
|
+
const [columnDefs, setColumnDefs] = useState<ColDef[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [activeTab, setActiveTab] = useState<'data' | 'schema'>('data');
|
|
20
|
+
const [schemaFile, setSchemaFile] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const fetchData = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
// 1. Fetch Schema to build columns
|
|
26
|
+
// Ideally we should cache metadata, but fetching it here is safer for now
|
|
27
|
+
const metaRes = await fetch('/api/metadata');
|
|
28
|
+
const meta = await metaRes.json();
|
|
29
|
+
const objects = Array.isArray(meta) ? meta : meta.objects;
|
|
30
|
+
const currentObj = objects.find((o: any) => o.name === objectName);
|
|
31
|
+
|
|
32
|
+
if (currentObj) {
|
|
33
|
+
const cols: ColDef[] = [
|
|
34
|
+
{ field: 'id', headerName: 'ID', width: 100, pinned: 'left' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
Object.entries(currentObj.fields).forEach(([key, field]: [string, any]) => {
|
|
38
|
+
cols.push({
|
|
39
|
+
field: key,
|
|
40
|
+
headerName: field.label || key,
|
|
41
|
+
flex: 1,
|
|
42
|
+
filter: true
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Add system fields if not present
|
|
47
|
+
if (!cols.find(c => c.field === 'createdAt')) cols.push({ field: 'createdAt', hide: true });
|
|
48
|
+
if (!cols.find(c => c.field === 'updatedAt')) cols.push({ field: 'updatedAt', hide: true });
|
|
49
|
+
|
|
50
|
+
setColumnDefs(cols);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Fetch Data
|
|
54
|
+
const res = await fetch(`/api/objectql/${objectName}`);
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
setRowData(Array.isArray(data) ? data : []);
|
|
57
|
+
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.error(e);
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const fetchSchemaFile = async () => {
|
|
66
|
+
// If we already have the file for this object, don't re-fetch unless objectName changed
|
|
67
|
+
// But here we rely on useEffect deps
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`/api/schema/find?object=${objectName}`);
|
|
70
|
+
if (res.ok) {
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
setSchemaFile(data.file);
|
|
73
|
+
} else {
|
|
74
|
+
setSchemaFile(null);
|
|
75
|
+
console.error('Failed to find schema file');
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(e);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setActiveTab('data');
|
|
84
|
+
}, [objectName]);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (activeTab === 'data') {
|
|
88
|
+
fetchData();
|
|
89
|
+
} else {
|
|
90
|
+
fetchSchemaFile();
|
|
91
|
+
}
|
|
92
|
+
}, [objectName, activeTab]);
|
|
93
|
+
|
|
94
|
+
const defaultColDef = useMemo(() => {
|
|
95
|
+
return {
|
|
96
|
+
sortable: true,
|
|
97
|
+
filter: true,
|
|
98
|
+
resizable: true,
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="flex h-full flex-col">
|
|
104
|
+
{/* Header */}
|
|
105
|
+
<div className="border-b px-6 py-4 flex items-center justify-between bg-card text-card-foreground">
|
|
106
|
+
<div className="flex flex-col gap-1">
|
|
107
|
+
<h1 className="text-2xl font-bold tracking-tight capitalize">{objectName}</h1>
|
|
108
|
+
<div className="flex space-x-1">
|
|
109
|
+
<button
|
|
110
|
+
onClick={() => setActiveTab('data')}
|
|
111
|
+
className={cn(
|
|
112
|
+
"flex items-center space-x-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
|
|
113
|
+
activeTab === 'data'
|
|
114
|
+
? "bg-primary/10 text-primary"
|
|
115
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<Table2 className="h-4 w-4" />
|
|
119
|
+
<span>Data</span>
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => setActiveTab('schema')}
|
|
123
|
+
className={cn(
|
|
124
|
+
"flex items-center space-x-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
|
|
125
|
+
activeTab === 'schema'
|
|
126
|
+
? "bg-primary/10 text-primary"
|
|
127
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
<FileCode className="h-4 w-4" />
|
|
131
|
+
<span>Schema</span>
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{activeTab === 'data' && (
|
|
137
|
+
<div className="flex items-center space-x-2">
|
|
138
|
+
<Button variant="outline" size="sm" onClick={fetchData} disabled={loading}>
|
|
139
|
+
<RefreshCw className={cn("mr-2 h-4 w-4", loading && "animate-spin")} />
|
|
140
|
+
Refresh
|
|
141
|
+
</Button>
|
|
142
|
+
<Button size="sm">
|
|
143
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
144
|
+
New Record
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Content */}
|
|
151
|
+
<div className="flex-1 overflow-hidden p-6 bg-muted/20">
|
|
152
|
+
{activeTab === 'data' ? (
|
|
153
|
+
<div className="rounded-md overflow-hidden bg-card h-full" style={{opacity: loading ? 0.6 : 1}}>
|
|
154
|
+
<div className="ag-theme-quartz h-full w-full">
|
|
155
|
+
<AgGridReact
|
|
156
|
+
rowData={rowData}
|
|
157
|
+
columnDefs={columnDefs}
|
|
158
|
+
defaultColDef={defaultColDef}
|
|
159
|
+
pagination={true}
|
|
160
|
+
paginationPageSize={20}
|
|
161
|
+
rowSelection="multiple"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
) : (
|
|
166
|
+
<div className="h-full bg-card rounded-md border">
|
|
167
|
+
{schemaFile ? (
|
|
168
|
+
<FileEditor filePath={schemaFile} className="h-full border-0" />
|
|
169
|
+
) : (
|
|
170
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
171
|
+
{loading ? 'Loading schema...' : 'Could not find definition file for this object.'}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
4
|
+
import { FileCode, RefreshCw } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { FileEditor } from '@/components/FileEditor';
|
|
7
|
+
|
|
8
|
+
export function SchemaEditor() {
|
|
9
|
+
const [files, setFiles] = useState<string[]>([]);
|
|
10
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetchFiles();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
const fetchFiles = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch('/api/schema/files');
|
|
20
|
+
if (!res.ok) throw new Error('Failed to fetch files');
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
setFiles(data.files || []);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(e);
|
|
25
|
+
setError('Failed to load file list');
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex h-full flex-col p-6 space-y-6">
|
|
31
|
+
<div className="flex items-center justify-between">
|
|
32
|
+
<div className="flex flex-col gap-1">
|
|
33
|
+
<h2 className="text-3xl font-bold tracking-tight">Schema Editor</h2>
|
|
34
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
35
|
+
</div>
|
|
36
|
+
<Button variant="outline" size="sm" onClick={fetchFiles}>
|
|
37
|
+
<RefreshCw className="mr-2 h-4 w-4" /> Refresh
|
|
38
|
+
</Button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 h-full min-h-[500px]">
|
|
42
|
+
{/* File List */}
|
|
43
|
+
<Card className="md:col-span-1 flex flex-col">
|
|
44
|
+
<CardHeader className="py-4">
|
|
45
|
+
<CardTitle className="text-lg">Files</CardTitle>
|
|
46
|
+
<CardDescription>Select a schema file to edit</CardDescription>
|
|
47
|
+
</CardHeader>
|
|
48
|
+
<CardContent className="flex-1 overflow-auto p-2">
|
|
49
|
+
<div className="space-y-1">
|
|
50
|
+
{files.map(file => (
|
|
51
|
+
<button
|
|
52
|
+
key={file}
|
|
53
|
+
onClick={() => setSelectedFile(file)}
|
|
54
|
+
className={cn(
|
|
55
|
+
"w-full text-left px-3 py-2 text-sm rounded-md transition-colors flex items-center",
|
|
56
|
+
selectedFile === file
|
|
57
|
+
? "bg-primary text-primary-foreground font-medium"
|
|
58
|
+
: "hover:bg-muted"
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<FileCode className="mr-2 h-4 w-4 opacity-70" />
|
|
62
|
+
{file}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
{files.length === 0 && (
|
|
66
|
+
<div className="text-sm text-muted-foreground p-2">No schema files found</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</CardContent>
|
|
70
|
+
</Card>
|
|
71
|
+
|
|
72
|
+
{/* Editor */}
|
|
73
|
+
<div className="md:col-span-3 h-full">
|
|
74
|
+
{selectedFile ? (
|
|
75
|
+
<FileEditor filePath={selectedFile} />
|
|
76
|
+
) : (
|
|
77
|
+
<Card className="h-full flex items-center justify-center">
|
|
78
|
+
<div className="text-muted-foreground">Select a file to view content</div>
|
|
79
|
+
</Card>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
darkMode: ["class"],
|
|
4
|
+
content: [
|
|
5
|
+
'./pages/**/*.{ts,tsx}',
|
|
6
|
+
'./components/**/*.{ts,tsx}',
|
|
7
|
+
'./app/**/*.{ts,tsx}',
|
|
8
|
+
'./src/**/*.{ts,tsx}',
|
|
9
|
+
],
|
|
10
|
+
prefix: "",
|
|
11
|
+
theme: {
|
|
12
|
+
container: {
|
|
13
|
+
center: true,
|
|
14
|
+
padding: "2rem",
|
|
15
|
+
screens: {
|
|
16
|
+
"2xl": "1400px",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
extend: {
|
|
20
|
+
colors: {
|
|
21
|
+
border: "hsl(var(--border))",
|
|
22
|
+
input: "hsl(var(--input))",
|
|
23
|
+
ring: "hsl(var(--ring))",
|
|
24
|
+
background: "hsl(var(--background))",
|
|
25
|
+
foreground: "hsl(var(--foreground))",
|
|
26
|
+
primary: {
|
|
27
|
+
DEFAULT: "hsl(var(--primary))",
|
|
28
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
29
|
+
},
|
|
30
|
+
secondary: {
|
|
31
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
32
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
33
|
+
},
|
|
34
|
+
destructive: {
|
|
35
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
36
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
37
|
+
},
|
|
38
|
+
muted: {
|
|
39
|
+
DEFAULT: "hsl(var(--muted))",
|
|
40
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
41
|
+
},
|
|
42
|
+
accent: {
|
|
43
|
+
DEFAULT: "hsl(var(--accent))",
|
|
44
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
45
|
+
},
|
|
46
|
+
popover: {
|
|
47
|
+
DEFAULT: "hsl(var(--popover))",
|
|
48
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
49
|
+
},
|
|
50
|
+
card: {
|
|
51
|
+
DEFAULT: "hsl(var(--card))",
|
|
52
|
+
foreground: "hsl(var(--card-foreground))",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
borderRadius: {
|
|
56
|
+
lg: "var(--radius)",
|
|
57
|
+
md: "calc(var(--radius) - 2px)",
|
|
58
|
+
sm: "calc(var(--radius) - 4px)",
|
|
59
|
+
},
|
|
60
|
+
keyframes: {
|
|
61
|
+
"accordion-down": {
|
|
62
|
+
from: { height: "0" },
|
|
63
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
64
|
+
},
|
|
65
|
+
"accordion-up": {
|
|
66
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
67
|
+
to: { height: "0" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
animation: {
|
|
71
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
72
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
plugins: [require("tailwindcss-animate")],
|
|
77
|
+
}
|
package/tsconfig.json
CHANGED