@objectql/studio 1.3.1
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/assets/index-BkeseS5P.js +67 -0
- package/dist/assets/index-YYlEozoH.css +1 -0
- package/dist/index.html +13 -0
- package/index.html +12 -0
- package/package.json +23 -0
- package/src/App.css +308 -0
- package/src/App.tsx +46 -0
- package/src/components/DataGrid.tsx +226 -0
- package/src/components/ObjectList.tsx +137 -0
- package/src/components/RecordDetail.tsx +227 -0
- package/src/components/SchemaInspector.tsx +204 -0
- package/src/index.css +23 -0
- package/src/main.tsx +10 -0
- package/tsconfig.json +21 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.app{display:flex;flex-direction:column;min-height:100vh}.container{max-width:1200px;margin:0 auto;padding:0 20px;width:100%}.app-header{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:1rem 0;box-shadow:0 2px 10px #0000001a}.app-header .container{display:flex;justify-content:space-between;align-items:center}.logo{font-size:1.5rem;font-weight:700;letter-spacing:-.5px}.logo a{color:#fff;text-decoration:none;transition:opacity .2s}.logo a:hover{opacity:.9}.nav{display:flex;gap:1.5rem}.nav-link{color:#fff;text-decoration:none;padding:.5rem 1rem;border-radius:6px;transition:background .2s;font-weight:500}.nav-link:hover{background:#ffffff26}.app-main{flex:1;padding:2rem 0}.app-footer{background:#2d3748;color:#a0aec0;padding:1.5rem 0;text-align:center;font-size:.875rem}.card{background:#fff;border-radius:8px;padding:1.5rem;box-shadow:0 1px 3px #0000001a;margin-bottom:1.5rem}.card-title{font-size:1.25rem;font-weight:600;margin-bottom:1rem;color:#2d3748}.btn{padding:.5rem 1rem;border:none;border-radius:6px;font-weight:500;cursor:pointer;transition:all .2s;text-decoration:none;display:inline-block;font-size:.875rem}.btn-primary{background:#667eea;color:#fff}.btn-primary:hover{background:#5568d3;transform:translateY(-1px);box-shadow:0 4px 6px #667eea4d}.btn-secondary{background:#e2e8f0;color:#2d3748}.btn-secondary:hover{background:#cbd5e0}.btn-danger{background:#f56565;color:#fff}.btn-danger:hover{background:#e53e3e}.btn-sm{padding:.375rem .75rem;font-size:.8125rem}.table-container{overflow-x:auto;margin-top:1rem}.table{width:100%;border-collapse:collapse;background:#fff}.table th,.table td{padding:.75rem 1rem;text-align:left;border-bottom:1px solid #e2e8f0}.table th{background:#f7fafc;font-weight:600;color:#2d3748;font-size:.875rem;text-transform:uppercase;letter-spacing:.5px}.table tr:hover{background:#f7fafc}.table td{color:#4a5568}.form-group{margin-bottom:1rem}.form-label{display:block;margin-bottom:.5rem;font-weight:500;color:#2d3748;font-size:.875rem}.form-input,.form-textarea,.form-select{width:100%;padding:.5rem .75rem;border:1px solid #e2e8f0;border-radius:6px;font-size:.875rem;transition:border-color .2s}.form-input:focus,.form-textarea:focus,.form-select:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px #667eea1a}.form-textarea{min-height:100px;resize:vertical}.grid{display:grid;gap:1rem}.grid-2{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}.alert{padding:1rem;border-radius:6px;margin-bottom:1rem}.alert-info{background:#e6f7ff;color:#0050b3;border-left:4px solid #1890ff}.alert-error{background:#fff1f0;color:#cf1322;border-left:4px solid #f5222d}.alert-success{background:#f6ffed;color:#389e0d;border-left:4px solid #52c41a}.loading{text-align:center;padding:3rem;color:#a0aec0}.spinner{display:inline-block;width:40px;height:40px;border:4px solid #e2e8f0;border-top-color:#667eea;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.breadcrumb{display:flex;gap:.5rem;margin-bottom:1.5rem;font-size:.875rem;color:#718096}.breadcrumb a{color:#667eea;text-decoration:none}.breadcrumb a:hover{text-decoration:underline}.breadcrumb-separator{color:#cbd5e0}.badge{display:inline-block;padding:.25rem .5rem;border-radius:4px;font-size:.75rem;font-weight:500}.badge-primary{background:#667eea;color:#fff}.badge-secondary{background:#e2e8f0;color:#2d3748}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#f5f5f5}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}#root{min-height:100vh}
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ObjectQL Console</title>
|
|
7
|
+
<script type="module" crossorigin src="/studio/assets/index-BkeseS5P.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/studio/assets/index-YYlEozoH.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ObjectQL Console</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectql/studio",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Web-based admin studio for ObjectQL database management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"react": "^18.2.0",
|
|
8
|
+
"react-dom": "^18.2.0",
|
|
9
|
+
"react-router-dom": "^6.20.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/react": "^18.2.43",
|
|
13
|
+
"@types/react-dom": "^18.2.17",
|
|
14
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
15
|
+
"typescript": "^5.3.0",
|
|
16
|
+
"vite": "^5.0.8"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "tsc && vite build",
|
|
21
|
+
"preview": "vite preview"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/App.css
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
.app {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
min-height: 100vh;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.container {
|
|
8
|
+
max-width: 1200px;
|
|
9
|
+
margin: 0 auto;
|
|
10
|
+
padding: 0 20px;
|
|
11
|
+
width: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Header */
|
|
15
|
+
.app-header {
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
color: white;
|
|
18
|
+
padding: 1rem 0;
|
|
19
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.app-header .container {
|
|
23
|
+
display: flex;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
align-items: center;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.logo {
|
|
29
|
+
font-size: 1.5rem;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
letter-spacing: -0.5px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.logo a {
|
|
35
|
+
color: white;
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
transition: opacity 0.2s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.logo a:hover {
|
|
41
|
+
opacity: 0.9;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.nav {
|
|
45
|
+
display: flex;
|
|
46
|
+
gap: 1.5rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.nav-link {
|
|
50
|
+
color: white;
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
padding: 0.5rem 1rem;
|
|
53
|
+
border-radius: 6px;
|
|
54
|
+
transition: background 0.2s;
|
|
55
|
+
font-weight: 500;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.nav-link:hover {
|
|
59
|
+
background: rgba(255, 255, 255, 0.15);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Main */
|
|
63
|
+
.app-main {
|
|
64
|
+
flex: 1;
|
|
65
|
+
padding: 2rem 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Footer */
|
|
69
|
+
.app-footer {
|
|
70
|
+
background: #2d3748;
|
|
71
|
+
color: #a0aec0;
|
|
72
|
+
padding: 1.5rem 0;
|
|
73
|
+
text-align: center;
|
|
74
|
+
font-size: 0.875rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Card Component */
|
|
78
|
+
.card {
|
|
79
|
+
background: white;
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
padding: 1.5rem;
|
|
82
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
83
|
+
margin-bottom: 1.5rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.card-title {
|
|
87
|
+
font-size: 1.25rem;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
margin-bottom: 1rem;
|
|
90
|
+
color: #2d3748;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Button */
|
|
94
|
+
.btn {
|
|
95
|
+
padding: 0.5rem 1rem;
|
|
96
|
+
border: none;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: all 0.2s;
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
display: inline-block;
|
|
103
|
+
font-size: 0.875rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.btn-primary {
|
|
107
|
+
background: #667eea;
|
|
108
|
+
color: white;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.btn-primary:hover {
|
|
112
|
+
background: #5568d3;
|
|
113
|
+
transform: translateY(-1px);
|
|
114
|
+
box-shadow: 0 4px 6px rgba(102, 126, 234, 0.3);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.btn-secondary {
|
|
118
|
+
background: #e2e8f0;
|
|
119
|
+
color: #2d3748;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.btn-secondary:hover {
|
|
123
|
+
background: #cbd5e0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.btn-danger {
|
|
127
|
+
background: #f56565;
|
|
128
|
+
color: white;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.btn-danger:hover {
|
|
132
|
+
background: #e53e3e;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.btn-sm {
|
|
136
|
+
padding: 0.375rem 0.75rem;
|
|
137
|
+
font-size: 0.8125rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Table */
|
|
141
|
+
.table-container {
|
|
142
|
+
overflow-x: auto;
|
|
143
|
+
margin-top: 1rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.table {
|
|
147
|
+
width: 100%;
|
|
148
|
+
border-collapse: collapse;
|
|
149
|
+
background: white;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.table th,
|
|
153
|
+
.table td {
|
|
154
|
+
padding: 0.75rem 1rem;
|
|
155
|
+
text-align: left;
|
|
156
|
+
border-bottom: 1px solid #e2e8f0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.table th {
|
|
160
|
+
background: #f7fafc;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
color: #2d3748;
|
|
163
|
+
font-size: 0.875rem;
|
|
164
|
+
text-transform: uppercase;
|
|
165
|
+
letter-spacing: 0.5px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.table tr:hover {
|
|
169
|
+
background: #f7fafc;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.table td {
|
|
173
|
+
color: #4a5568;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Form */
|
|
177
|
+
.form-group {
|
|
178
|
+
margin-bottom: 1rem;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.form-label {
|
|
182
|
+
display: block;
|
|
183
|
+
margin-bottom: 0.5rem;
|
|
184
|
+
font-weight: 500;
|
|
185
|
+
color: #2d3748;
|
|
186
|
+
font-size: 0.875rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.form-input,
|
|
190
|
+
.form-textarea,
|
|
191
|
+
.form-select {
|
|
192
|
+
width: 100%;
|
|
193
|
+
padding: 0.5rem 0.75rem;
|
|
194
|
+
border: 1px solid #e2e8f0;
|
|
195
|
+
border-radius: 6px;
|
|
196
|
+
font-size: 0.875rem;
|
|
197
|
+
transition: border-color 0.2s;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.form-input:focus,
|
|
201
|
+
.form-textarea:focus,
|
|
202
|
+
.form-select:focus {
|
|
203
|
+
outline: none;
|
|
204
|
+
border-color: #667eea;
|
|
205
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.form-textarea {
|
|
209
|
+
min-height: 100px;
|
|
210
|
+
resize: vertical;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Grid */
|
|
214
|
+
.grid {
|
|
215
|
+
display: grid;
|
|
216
|
+
gap: 1rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.grid-2 {
|
|
220
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Alert */
|
|
224
|
+
.alert {
|
|
225
|
+
padding: 1rem;
|
|
226
|
+
border-radius: 6px;
|
|
227
|
+
margin-bottom: 1rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.alert-info {
|
|
231
|
+
background: #e6f7ff;
|
|
232
|
+
color: #0050b3;
|
|
233
|
+
border-left: 4px solid #1890ff;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.alert-error {
|
|
237
|
+
background: #fff1f0;
|
|
238
|
+
color: #cf1322;
|
|
239
|
+
border-left: 4px solid #f5222d;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.alert-success {
|
|
243
|
+
background: #f6ffed;
|
|
244
|
+
color: #389e0d;
|
|
245
|
+
border-left: 4px solid #52c41a;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Loading */
|
|
249
|
+
.loading {
|
|
250
|
+
text-align: center;
|
|
251
|
+
padding: 3rem;
|
|
252
|
+
color: #a0aec0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.spinner {
|
|
256
|
+
display: inline-block;
|
|
257
|
+
width: 40px;
|
|
258
|
+
height: 40px;
|
|
259
|
+
border: 4px solid #e2e8f0;
|
|
260
|
+
border-top-color: #667eea;
|
|
261
|
+
border-radius: 50%;
|
|
262
|
+
animation: spin 1s linear infinite;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@keyframes spin {
|
|
266
|
+
to { transform: rotate(360deg); }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Breadcrumb */
|
|
270
|
+
.breadcrumb {
|
|
271
|
+
display: flex;
|
|
272
|
+
gap: 0.5rem;
|
|
273
|
+
margin-bottom: 1.5rem;
|
|
274
|
+
font-size: 0.875rem;
|
|
275
|
+
color: #718096;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.breadcrumb a {
|
|
279
|
+
color: #667eea;
|
|
280
|
+
text-decoration: none;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.breadcrumb a:hover {
|
|
284
|
+
text-decoration: underline;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.breadcrumb-separator {
|
|
288
|
+
color: #cbd5e0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Badge */
|
|
292
|
+
.badge {
|
|
293
|
+
display: inline-block;
|
|
294
|
+
padding: 0.25rem 0.5rem;
|
|
295
|
+
border-radius: 4px;
|
|
296
|
+
font-size: 0.75rem;
|
|
297
|
+
font-weight: 500;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.badge-primary {
|
|
301
|
+
background: #667eea;
|
|
302
|
+
color: white;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.badge-secondary {
|
|
306
|
+
background: #e2e8f0;
|
|
307
|
+
color: #2d3748;
|
|
308
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
|
2
|
+
import './App.css';
|
|
3
|
+
import ObjectList from './components/ObjectList';
|
|
4
|
+
import DataGrid from './components/DataGrid';
|
|
5
|
+
import RecordDetail from './components/RecordDetail';
|
|
6
|
+
import SchemaInspector from './components/SchemaInspector';
|
|
7
|
+
|
|
8
|
+
function App() {
|
|
9
|
+
return (
|
|
10
|
+
<BrowserRouter basename="/console">
|
|
11
|
+
<div className="app">
|
|
12
|
+
<header className="app-header">
|
|
13
|
+
<div className="container">
|
|
14
|
+
<h1 className="logo">
|
|
15
|
+
<Link to="/">ObjectQL Console</Link>
|
|
16
|
+
</h1>
|
|
17
|
+
<nav className="nav">
|
|
18
|
+
<Link to="/" className="nav-link">Objects</Link>
|
|
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>
|
|
34
|
+
</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
|
+
</div>
|
|
42
|
+
</BrowserRouter>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default App;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams, Link, useNavigate } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
interface Record {
|
|
5
|
+
_id?: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function DataGrid() {
|
|
11
|
+
const { objectName } = useParams<{ objectName: string }>();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const [records, setRecords] = useState<Record[]>([]);
|
|
14
|
+
const [fields, setFields] = useState<string[]>([]);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
fetchRecords();
|
|
21
|
+
fetchObjectMetadata();
|
|
22
|
+
}, [objectName]);
|
|
23
|
+
|
|
24
|
+
const fetchObjectMetadata = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`/api/metadata/objects/${objectName}`);
|
|
27
|
+
if (response.ok) {
|
|
28
|
+
const data = await response.json();
|
|
29
|
+
if (data.fields) {
|
|
30
|
+
setFields(data.fields.map((f: any) => f.name || f));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error('Failed to fetch metadata:', e);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const fetchRecords = async () => {
|
|
39
|
+
try {
|
|
40
|
+
setLoading(true);
|
|
41
|
+
const response = await fetch('/api/objectql', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
op: 'find',
|
|
46
|
+
object: objectName,
|
|
47
|
+
args: { limit: 100 }
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await response.json();
|
|
56
|
+
const data = result.data || [];
|
|
57
|
+
setRecords(data);
|
|
58
|
+
|
|
59
|
+
// Auto-detect fields if not already set
|
|
60
|
+
if (data.length > 0 && fields.length === 0) {
|
|
61
|
+
const sampleRecord = data[0];
|
|
62
|
+
const detectedFields = Object.keys(sampleRecord).filter(
|
|
63
|
+
k => !k.startsWith('_') || k === '_id'
|
|
64
|
+
);
|
|
65
|
+
setFields(detectedFields);
|
|
66
|
+
}
|
|
67
|
+
} catch (e: any) {
|
|
68
|
+
console.error('Failed to fetch records:', e);
|
|
69
|
+
setError(e.message);
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleCreate = async (formData: Record) => {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch('/api/objectql', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
op: 'create',
|
|
82
|
+
object: objectName,
|
|
83
|
+
args: { data: formData }
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error('Failed to create record');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setShowCreateForm(false);
|
|
92
|
+
fetchRecords();
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
alert(`Error: ${e.message}`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const formatValue = (value: any): string => {
|
|
99
|
+
if (value === null || value === undefined) return '-';
|
|
100
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
101
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
102
|
+
return String(value);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const getRecordId = (record: Record): string => {
|
|
106
|
+
return record._id || record.id || '';
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (loading) {
|
|
110
|
+
return (
|
|
111
|
+
<div className="loading">
|
|
112
|
+
<div className="spinner"></div>
|
|
113
|
+
<p>Loading records...</p>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div>
|
|
120
|
+
<div className="breadcrumb">
|
|
121
|
+
<Link to="/">Objects</Link>
|
|
122
|
+
<span className="breadcrumb-separator">/</span>
|
|
123
|
+
<span>{objectName}</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="card">
|
|
127
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
|
128
|
+
<h2 className="card-title" style={{ marginBottom: 0 }}>{objectName}</h2>
|
|
129
|
+
<button className="btn btn-primary" onClick={() => setShowCreateForm(!showCreateForm)}>
|
|
130
|
+
{showCreateForm ? 'Cancel' : 'New Record'}
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{error && (
|
|
135
|
+
<div className="alert alert-error">
|
|
136
|
+
<strong>Error:</strong> {error}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{showCreateForm && (
|
|
141
|
+
<CreateRecordForm
|
|
142
|
+
fields={fields}
|
|
143
|
+
onSubmit={handleCreate}
|
|
144
|
+
onCancel={() => setShowCreateForm(false)}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{records.length === 0 ? (
|
|
149
|
+
<div className="alert alert-info">
|
|
150
|
+
No records found. Click "New Record" to create one.
|
|
151
|
+
</div>
|
|
152
|
+
) : (
|
|
153
|
+
<div className="table-container">
|
|
154
|
+
<table className="table">
|
|
155
|
+
<thead>
|
|
156
|
+
<tr>
|
|
157
|
+
{fields.map((field) => (
|
|
158
|
+
<th key={field}>{field}</th>
|
|
159
|
+
))}
|
|
160
|
+
<th>Actions</th>
|
|
161
|
+
</tr>
|
|
162
|
+
</thead>
|
|
163
|
+
<tbody>
|
|
164
|
+
{records.map((record, idx) => (
|
|
165
|
+
<tr key={getRecordId(record) || idx}>
|
|
166
|
+
{fields.map((field) => (
|
|
167
|
+
<td key={field}>{formatValue(record[field])}</td>
|
|
168
|
+
))}
|
|
169
|
+
<td>
|
|
170
|
+
<button
|
|
171
|
+
className="btn btn-sm btn-secondary"
|
|
172
|
+
onClick={() => navigate(`/object/${objectName}/${getRecordId(record)}`)}
|
|
173
|
+
>
|
|
174
|
+
View
|
|
175
|
+
</button>
|
|
176
|
+
</td>
|
|
177
|
+
</tr>
|
|
178
|
+
))}
|
|
179
|
+
</tbody>
|
|
180
|
+
</table>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<div style={{ marginTop: '1rem', color: '#718096', fontSize: '0.875rem' }}>
|
|
185
|
+
Showing {records.length} record{records.length !== 1 ? 's' : ''}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function CreateRecordForm({ fields, onSubmit, onCancel }: {
|
|
193
|
+
fields: string[];
|
|
194
|
+
onSubmit: (data: Record) => void;
|
|
195
|
+
onCancel: () => void;
|
|
196
|
+
}) {
|
|
197
|
+
const [formData, setFormData] = useState<Record>({});
|
|
198
|
+
|
|
199
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
onSubmit(formData);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<form onSubmit={handleSubmit} className="card" style={{ background: '#f7fafc', marginBottom: '1rem' }}>
|
|
206
|
+
<h3 style={{ marginBottom: '1rem', fontSize: '1rem', fontWeight: '600' }}>Create New Record</h3>
|
|
207
|
+
{fields.filter(f => f !== '_id' && f !== 'id').map((field) => (
|
|
208
|
+
<div key={field} className="form-group">
|
|
209
|
+
<label className="form-label">{field}</label>
|
|
210
|
+
<input
|
|
211
|
+
type="text"
|
|
212
|
+
className="form-input"
|
|
213
|
+
value={formData[field] || ''}
|
|
214
|
+
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
))}
|
|
218
|
+
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
|
|
219
|
+
<button type="submit" className="btn btn-primary">Create</button>
|
|
220
|
+
<button type="button" className="btn btn-secondary" onClick={onCancel}>Cancel</button>
|
|
221
|
+
</div>
|
|
222
|
+
</form>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default DataGrid;
|