@objectql/studio 1.4.0 → 1.6.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 +12 -0
- package/dist/assets/{index-sazuLrgQ.js → index-BT6-06vQ.js} +130 -85
- package/dist/assets/{index-ClGKK4W3.css → index-DXr7ziDn.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/App.tsx +3 -0
- package/src/components/Sidebar.tsx +13 -1
- package/src/pages/MetadataBrowser.tsx +200 -0
- package/src/pages/ObjectView.tsx +121 -40
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -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-BT6-06vQ.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/studio/assets/index-DXr7ziDn.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Sidebar } from '@/components/Sidebar';
|
|
|
3
3
|
import { Dashboard } from '@/pages/Dashboard';
|
|
4
4
|
import { ObjectView } from '@/pages/ObjectView';
|
|
5
5
|
import { SchemaEditor } from '@/pages/SchemaEditor';
|
|
6
|
+
import { MetadataBrowser } from '@/pages/MetadataBrowser';
|
|
6
7
|
import './index.css';
|
|
7
8
|
|
|
8
9
|
// Wrapper to extract params
|
|
@@ -21,6 +22,7 @@ function App() {
|
|
|
21
22
|
<Routes>
|
|
22
23
|
<Route path="/" element={<Dashboard />} />
|
|
23
24
|
<Route path="/schema" element={<SchemaEditor />} />
|
|
25
|
+
<Route path="/metadata" element={<MetadataBrowser />} />
|
|
24
26
|
<Route path="/object/:name" element={<ObjectViewWrapper />} />
|
|
25
27
|
</Routes>
|
|
26
28
|
</main>
|
|
@@ -30,3 +32,4 @@ function App() {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export default App;
|
|
35
|
+
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { NavLink } from 'react-router-dom';
|
|
2
2
|
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';
|
|
4
4
|
import { cn } from '@/lib/utils';
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
export function Sidebar() {
|
|
7
8
|
const { objects, loading, error } = useMetadata();
|
|
8
9
|
|
|
@@ -33,6 +34,17 @@ export function Sidebar() {
|
|
|
33
34
|
</h4>
|
|
34
35
|
</div>
|
|
35
36
|
|
|
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
|
+
|
|
36
48
|
<NavLink
|
|
37
49
|
to="/schema"
|
|
38
50
|
className={({isActive}) => cn(
|
|
@@ -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;
|
package/src/pages/ObjectView.tsx
CHANGED
|
@@ -13,52 +13,130 @@ interface ObjectViewProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function ObjectView({ objectName }: ObjectViewProps) {
|
|
16
|
-
const [rowData, setRowData] = useState<any[]>([]);
|
|
16
|
+
// const [rowData, setRowData] = useState<any[]>([]); // Using Server Side
|
|
17
17
|
const [columnDefs, setColumnDefs] = useState<ColDef[]>([]);
|
|
18
18
|
const [loading, setLoading] = useState(false);
|
|
19
19
|
const [activeTab, setActiveTab] = useState<'data' | 'schema'>('data');
|
|
20
20
|
const [schemaFile, setSchemaFile] = useState<string | null>(null);
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const [gridApi, setGridApi] = useState<any>(null);
|
|
23
|
+
|
|
24
|
+
// Initial load for columns
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const loadColumns = async () => {
|
|
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', filter: 'agTextColumnFilter' }
|
|
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: 'agTextColumnFilter', // Enforce text filter for simplicity in Infinite Model
|
|
43
|
+
filterParams: {
|
|
44
|
+
filterOptions: ['contains', 'equals'],
|
|
45
|
+
suppressAndOrCondition: true
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
setColumnDefs(cols);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
loadColumns();
|
|
54
|
+
}, [objectName]);
|
|
55
|
+
|
|
56
|
+
const onGridReady = (params: any) => {
|
|
57
|
+
setGridApi(params.api);
|
|
23
58
|
setLoading(true);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
];
|
|
59
|
+
|
|
60
|
+
const datasource = {
|
|
61
|
+
getRows: async (params: any) => {
|
|
62
|
+
const { startRow, endRow, filterModel, sortModel } = params;
|
|
63
|
+
setLoading(true);
|
|
36
64
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
try {
|
|
66
|
+
// 1. Convert Filters
|
|
67
|
+
const filters: any[] = [];
|
|
68
|
+
if (filterModel) {
|
|
69
|
+
for (const key of Object.keys(filterModel)) {
|
|
70
|
+
const model = filterModel[key];
|
|
71
|
+
// agTextColumnFilter model: { filterType: 'text', type: 'contains', filter: 'value' }
|
|
72
|
+
if (model.filterType === 'text') {
|
|
73
|
+
if (model.type === 'contains') {
|
|
74
|
+
filters.push([key, 'contains', model.filter]);
|
|
75
|
+
} else if (model.type === 'equals') {
|
|
76
|
+
filters.push([key, '=', model.filter]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Convert Sort
|
|
83
|
+
const sort = sortModel.map((s: any) => [s.colId, s.sort]);
|
|
84
|
+
|
|
85
|
+
// 3. Fetch Data
|
|
86
|
+
const response = await fetch('/api/objectql', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
op: 'find',
|
|
91
|
+
object: objectName,
|
|
92
|
+
args: {
|
|
93
|
+
skip: startRow,
|
|
94
|
+
limit: endRow - startRow,
|
|
95
|
+
filters: filters.length > 0 ? filters : undefined,
|
|
96
|
+
sort: sort.length > 0 ? sort : undefined
|
|
97
|
+
}
|
|
98
|
+
})
|
|
43
99
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
100
|
+
const resJson = await response.json();
|
|
101
|
+
const rows = resJson.data || resJson; // normalize
|
|
102
|
+
|
|
103
|
+
// 4. Fetch Count (for total pagination)
|
|
104
|
+
// Optimization: Only fetch count if we don't know it or filter changed?
|
|
105
|
+
// For Infinite Scroll simplicity, we fetch it.
|
|
106
|
+
let lastRow = -1;
|
|
107
|
+
if (rows.length < (endRow - startRow)) {
|
|
108
|
+
lastRow = startRow + rows.length;
|
|
109
|
+
} else {
|
|
110
|
+
const countRes = await fetch('/api/objectql', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
op: 'count',
|
|
115
|
+
object: objectName,
|
|
116
|
+
args: filters.length > 0 ? filters : {}
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
const countJson = await countRes.json();
|
|
120
|
+
lastRow = typeof countJson === 'number' ? countJson : countJson.data;
|
|
121
|
+
}
|
|
49
122
|
|
|
50
|
-
|
|
123
|
+
params.successCallback(rows, lastRow);
|
|
124
|
+
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error('Datasource getRows error:', e);
|
|
127
|
+
params.failCallback();
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
}
|
|
51
131
|
}
|
|
132
|
+
};
|
|
52
133
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const data = await res.json();
|
|
56
|
-
setRowData(Array.isArray(data) ? data : []);
|
|
134
|
+
params.api.setDatasource(datasource);
|
|
135
|
+
};
|
|
57
136
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
setLoading(false);
|
|
137
|
+
const refreshData = () => {
|
|
138
|
+
if (gridApi) {
|
|
139
|
+
gridApi.refreshInfiniteCache();
|
|
62
140
|
}
|
|
63
141
|
};
|
|
64
142
|
|
|
@@ -84,18 +162,18 @@ export function ObjectView({ objectName }: ObjectViewProps) {
|
|
|
84
162
|
}, [objectName]);
|
|
85
163
|
|
|
86
164
|
useEffect(() => {
|
|
87
|
-
if (activeTab === '
|
|
88
|
-
fetchData();
|
|
89
|
-
} else {
|
|
165
|
+
if (activeTab === 'schema') {
|
|
90
166
|
fetchSchemaFile();
|
|
91
167
|
}
|
|
92
168
|
}, [objectName, activeTab]);
|
|
93
169
|
|
|
170
|
+
|
|
94
171
|
const defaultColDef = useMemo(() => {
|
|
95
172
|
return {
|
|
96
173
|
sortable: true,
|
|
97
174
|
filter: true,
|
|
98
175
|
resizable: true,
|
|
176
|
+
floatingFilter: true, // Enable Floating Filter
|
|
99
177
|
};
|
|
100
178
|
}, []);
|
|
101
179
|
|
|
@@ -135,7 +213,7 @@ export function ObjectView({ objectName }: ObjectViewProps) {
|
|
|
135
213
|
|
|
136
214
|
{activeTab === 'data' && (
|
|
137
215
|
<div className="flex items-center space-x-2">
|
|
138
|
-
<Button variant="outline" size="sm" onClick={
|
|
216
|
+
<Button variant="outline" size="sm" onClick={refreshData} disabled={loading}>
|
|
139
217
|
<RefreshCw className={cn("mr-2 h-4 w-4", loading && "animate-spin")} />
|
|
140
218
|
Refresh
|
|
141
219
|
</Button>
|
|
@@ -150,15 +228,18 @@ export function ObjectView({ objectName }: ObjectViewProps) {
|
|
|
150
228
|
{/* Content */}
|
|
151
229
|
<div className="flex-1 overflow-hidden p-6 bg-muted/20">
|
|
152
230
|
{activeTab === 'data' ? (
|
|
153
|
-
<div className="
|
|
231
|
+
<div className="rounded-md overflow-hidden bg-card h-full" style={{opacity: loading ? 0.6 : 1}}>
|
|
154
232
|
<div className="ag-theme-quartz h-full w-full">
|
|
155
233
|
<AgGridReact
|
|
156
|
-
|
|
234
|
+
rowModelType="infinite"
|
|
235
|
+
onGridReady={onGridReady}
|
|
157
236
|
columnDefs={columnDefs}
|
|
158
237
|
defaultColDef={defaultColDef}
|
|
159
238
|
pagination={true}
|
|
160
239
|
paginationPageSize={20}
|
|
240
|
+
cacheBlockSize={20}
|
|
161
241
|
rowSelection="multiple"
|
|
242
|
+
maxBlocksInCache={10}
|
|
162
243
|
/>
|
|
163
244
|
</div>
|
|
164
245
|
</div>
|
package/tsconfig.json
CHANGED