@qwickapps/server 1.7.2 → 1.8.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 +33 -0
- package/dist/src/core/control-panel.js +5 -5
- package/dist/src/core/control-panel.js.map +1 -1
- package/dist/src/core/gateway.d.ts.map +1 -1
- package/dist/src/core/gateway.js +117 -15
- package/dist/src/core/gateway.js.map +1 -1
- package/dist/src/core/plugin-registry.d.ts +70 -0
- package/dist/src/core/plugin-registry.d.ts.map +1 -1
- package/dist/src/core/plugin-registry.js +94 -0
- package/dist/src/core/plugin-registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
- package/dist/src/plugins/api-keys/index.d.ts +1 -1
- package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/index.js.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
- package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
- package/dist/src/plugins/api-keys/types.d.ts +13 -1
- package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/types.js.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.js +73 -0
- package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/src/plugins/index.d.ts +1 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
- package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
- package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.js +39 -14
- package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
- package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
- package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.js +53 -17
- package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
- package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
- package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
- package/dist/src/plugins/maintenance-plugin.js +222 -34
- package/dist/src/plugins/maintenance-plugin.js.map +1 -1
- package/dist/src/plugins/postgres-plugin.d.ts +12 -0
- package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/src/plugins/postgres-plugin.js +319 -5
- package/dist/src/plugins/postgres-plugin.js.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.js +4 -3
- package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
- package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
- package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.js +1 -0
- package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
- package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
- package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/index.js +3735 -3187
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
- package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/core/control-panel.ts +5 -5
- package/src/core/gateway.ts +135 -15
- package/src/core/plugin-registry.ts +171 -0
- package/src/index.ts +2 -0
- package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
- package/src/plugins/api-keys/index.ts +1 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
- package/src/plugins/api-keys/types.ts +14 -1
- package/src/plugins/diagnostics-plugin.ts +77 -0
- package/src/plugins/index.ts +1 -1
- package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
- package/src/plugins/maintenance/SeedList.tsx +85 -38
- package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
- package/src/plugins/maintenance/seed-executor.ts +56 -17
- package/src/plugins/maintenance-plugin.ts +267 -36
- package/src/plugins/postgres-plugin.ts +410 -5
- package/ui/src/App.tsx +3 -3
- package/ui/src/components/ControlPanelApp.tsx +4 -3
- package/ui/src/dashboard/builtInWidgets.tsx +3 -0
- package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
- package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
- package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
- package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
- package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
- package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/dist-ui/assets/index-0gzisPdy.js +0 -528
- package/dist-ui/assets/index-0gzisPdy.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Seed Management Widget
|
|
3
3
|
*
|
|
4
|
-
* Displays available seed scripts and allows executing them.
|
|
4
|
+
* Displays available seed scripts grouped by folder and allows executing them.
|
|
5
5
|
* Part of the maintenance plugin.
|
|
6
6
|
*
|
|
7
7
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
@@ -13,28 +13,77 @@ import {
|
|
|
13
13
|
CardContent,
|
|
14
14
|
Typography,
|
|
15
15
|
Button,
|
|
16
|
-
|
|
17
|
-
ListItem,
|
|
18
|
-
ListItemText,
|
|
16
|
+
Checkbox,
|
|
19
17
|
CircularProgress,
|
|
20
18
|
Alert,
|
|
21
19
|
Box,
|
|
20
|
+
Accordion,
|
|
21
|
+
AccordionSummary,
|
|
22
|
+
AccordionDetails,
|
|
23
|
+
List,
|
|
24
|
+
ListItem,
|
|
25
|
+
ListItemButton,
|
|
26
|
+
ListItemIcon,
|
|
27
|
+
ListItemText,
|
|
28
|
+
Chip,
|
|
29
|
+
Snackbar,
|
|
30
|
+
Dialog,
|
|
31
|
+
DialogTitle,
|
|
32
|
+
DialogContent,
|
|
33
|
+
DialogActions,
|
|
34
|
+
Paper,
|
|
35
|
+
LinearProgress,
|
|
22
36
|
} from '@mui/material';
|
|
37
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
23
38
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
|
39
|
+
import FolderIcon from '@mui/icons-material/Folder';
|
|
40
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
41
|
+
import ErrorIcon from '@mui/icons-material/Error';
|
|
42
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
24
43
|
|
|
25
44
|
interface SeedScript {
|
|
45
|
+
type: 'file' | 'task';
|
|
26
46
|
name: string;
|
|
27
|
-
path
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
path?: string;
|
|
48
|
+
id?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
size?: number;
|
|
51
|
+
createdAt?: string;
|
|
52
|
+
modifiedAt?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SeedFolder {
|
|
56
|
+
name: string;
|
|
57
|
+
seeds: SeedScript[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ExecutionResult {
|
|
61
|
+
seedName: string;
|
|
62
|
+
success: boolean;
|
|
63
|
+
output?: string;
|
|
64
|
+
error?: string;
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
export function SeedManagementWidget() {
|
|
34
68
|
const [seeds, setSeeds] = useState<SeedScript[]>([]);
|
|
35
69
|
const [loading, setLoading] = useState(true);
|
|
36
70
|
const [error, setError] = useState<string | null>(null);
|
|
37
|
-
const [executing, setExecuting] = useState
|
|
71
|
+
const [executing, setExecuting] = useState(false);
|
|
72
|
+
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
73
|
+
|
|
74
|
+
// Execution dialog state
|
|
75
|
+
const [executionDialogOpen, setExecutionDialogOpen] = useState(false);
|
|
76
|
+
const [executionResults, setExecutionResults] = useState<ExecutionResult[]>([]);
|
|
77
|
+
const [currentlyExecuting, setCurrentlyExecuting] = useState<string | null>(null);
|
|
78
|
+
|
|
79
|
+
// Snackbar state
|
|
80
|
+
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
|
81
|
+
const [snackbarMessage, setSnackbarMessage] = useState('');
|
|
82
|
+
const [snackbarSeverity, setSnackbarSeverity] = useState<'success' | 'error'>('success');
|
|
83
|
+
|
|
84
|
+
// Database reset state
|
|
85
|
+
const [resetDialogOpen, setResetDialogOpen] = useState(false);
|
|
86
|
+
const [resetting, setResetting] = useState(false);
|
|
38
87
|
|
|
39
88
|
useEffect(() => {
|
|
40
89
|
fetchSeeds();
|
|
@@ -42,10 +91,11 @@ export function SeedManagementWidget() {
|
|
|
42
91
|
|
|
43
92
|
const fetchSeeds = async () => {
|
|
44
93
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
94
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
95
|
+
const response = await fetch(`${basePath}/api/maintenance/seeds/discover`);
|
|
96
|
+
if (!response.ok) throw new Error('Failed to fetch seeds');
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
setSeeds(data.seeds || []);
|
|
49
99
|
setError(null);
|
|
50
100
|
} catch (err) {
|
|
51
101
|
setError(err instanceof Error ? err.message : 'Failed to fetch seeds');
|
|
@@ -54,19 +104,213 @@ export function SeedManagementWidget() {
|
|
|
54
104
|
}
|
|
55
105
|
};
|
|
56
106
|
|
|
57
|
-
const
|
|
58
|
-
|
|
107
|
+
const getSeedKey = (seed: SeedScript): string => {
|
|
108
|
+
return seed.type === 'file' ? seed.path! : seed.id!;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getFriendlyName = (seed: SeedScript): string => {
|
|
112
|
+
if (seed.type === 'task') return seed.name;
|
|
113
|
+
|
|
114
|
+
// Extract filename without folder and extension
|
|
115
|
+
// "01-Database/001.initialize-schema.mjs" -> "Initialize Schema"
|
|
116
|
+
const filename = seed.name.replace('.mjs', '');
|
|
117
|
+
const withoutNumber = filename.replace(/^\d+\./, ''); // Remove number prefix
|
|
118
|
+
|
|
119
|
+
// Convert kebab-case/snake-case to Title Case
|
|
120
|
+
return withoutNumber
|
|
121
|
+
.split(/[-_]/)
|
|
122
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
123
|
+
.join(' ');
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const groupByFolder = (): SeedFolder[] => {
|
|
127
|
+
const folderMap = new Map<string, SeedScript[]>();
|
|
128
|
+
|
|
129
|
+
seeds.forEach(seed => {
|
|
130
|
+
let folderName = 'Ungrouped';
|
|
131
|
+
|
|
132
|
+
if (seed.type === 'file' && seed.path) {
|
|
133
|
+
const parts = seed.path.split('/');
|
|
134
|
+
if (parts.length > 1) {
|
|
135
|
+
folderName = parts[0];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!folderMap.has(folderName)) {
|
|
140
|
+
folderMap.set(folderName, []);
|
|
141
|
+
}
|
|
142
|
+
folderMap.get(folderName)!.push(seed);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return Array.from(folderMap.entries())
|
|
146
|
+
.map(([name, seeds]) => ({ name, seeds }))
|
|
147
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handleToggleSeed = (seedKey: string) => {
|
|
151
|
+
const newSelected = new Set(selected);
|
|
152
|
+
if (newSelected.has(seedKey)) {
|
|
153
|
+
newSelected.delete(seedKey);
|
|
154
|
+
} else {
|
|
155
|
+
newSelected.add(seedKey);
|
|
156
|
+
}
|
|
157
|
+
setSelected(newSelected);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleToggleFolder = (folder: SeedFolder) => {
|
|
161
|
+
const folderKeys = folder.seeds.map(getSeedKey);
|
|
162
|
+
const allSelected = folderKeys.every(key => selected.has(key));
|
|
163
|
+
|
|
164
|
+
const newSelected = new Set(selected);
|
|
165
|
+
if (allSelected) {
|
|
166
|
+
folderKeys.forEach(key => newSelected.delete(key));
|
|
167
|
+
} else {
|
|
168
|
+
folderKeys.forEach(key => newSelected.add(key));
|
|
169
|
+
}
|
|
170
|
+
setSelected(newSelected);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Execute a single seed and handle SSE stream
|
|
174
|
+
const executeSeed = async (seedKey: string, friendlyName: string, seedType: string): Promise<ExecutionResult> => {
|
|
175
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
176
|
+
const response = await fetch(`${basePath}/api/maintenance/seeds/execute`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
179
|
+
body: JSON.stringify({ name: seedKey, type: seedType }),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!response.ok && !response.headers.get('content-type')?.includes('text/event-stream')) {
|
|
183
|
+
const errorData = await response.json().catch(() => ({ error: 'Failed to start execution' }));
|
|
184
|
+
throw new Error(errorData.error || 'Failed to start execution');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const reader = response.body?.getReader();
|
|
188
|
+
const decoder = new TextDecoder();
|
|
189
|
+
|
|
190
|
+
let output = '';
|
|
191
|
+
let error = '';
|
|
192
|
+
let exitCode = 1;
|
|
193
|
+
|
|
194
|
+
if (reader) {
|
|
195
|
+
try {
|
|
196
|
+
while (true) {
|
|
197
|
+
const { done, value } = await reader.read();
|
|
198
|
+
if (done) break;
|
|
199
|
+
|
|
200
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
201
|
+
const lines = chunk.split('\n');
|
|
202
|
+
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
if (line.startsWith('data: ')) {
|
|
205
|
+
try {
|
|
206
|
+
const eventData = JSON.parse(line.slice(6));
|
|
207
|
+
|
|
208
|
+
if (eventData.type === 'stdout') {
|
|
209
|
+
output += eventData.data;
|
|
210
|
+
} else if (eventData.type === 'stderr') {
|
|
211
|
+
error += eventData.data;
|
|
212
|
+
} else if (eventData.type === 'exit') {
|
|
213
|
+
const exitData = JSON.parse(eventData.data);
|
|
214
|
+
exitCode = exitData.exitCode;
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Ignore malformed SSE events
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
reader.releaseLock();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
seedName: friendlyName,
|
|
229
|
+
success: exitCode === 0,
|
|
230
|
+
output: output || undefined,
|
|
231
|
+
error: error || (exitCode !== 0 ? 'Execution failed' : undefined),
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleDatabaseReset = async () => {
|
|
236
|
+
setResetting(true);
|
|
237
|
+
setResetDialogOpen(false);
|
|
238
|
+
|
|
59
239
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
240
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
241
|
+
const response = await fetch(`${basePath}/api/maintenance/database/reset`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
throw new Error(data.error || 'Failed to reset database');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
setSnackbarMessage('Database reset successfully. All tables and data have been deleted.');
|
|
253
|
+
setSnackbarSeverity('success');
|
|
254
|
+
setSnackbarOpen(true);
|
|
255
|
+
|
|
256
|
+
// Refresh seeds list after reset
|
|
257
|
+
await fetchSeeds();
|
|
63
258
|
} catch (err) {
|
|
64
|
-
|
|
259
|
+
setSnackbarMessage(err instanceof Error ? err.message : 'Database reset failed');
|
|
260
|
+
setSnackbarSeverity('error');
|
|
261
|
+
setSnackbarOpen(true);
|
|
65
262
|
} finally {
|
|
66
|
-
|
|
263
|
+
setResetting(false);
|
|
67
264
|
}
|
|
68
265
|
};
|
|
69
266
|
|
|
267
|
+
const executeSelected = async () => {
|
|
268
|
+
if (selected.size === 0) return;
|
|
269
|
+
|
|
270
|
+
setExecuting(true);
|
|
271
|
+
setExecutionDialogOpen(true);
|
|
272
|
+
setExecutionResults([]);
|
|
273
|
+
|
|
274
|
+
const selectedSeeds = seeds.filter(seed => selected.has(getSeedKey(seed)));
|
|
275
|
+
const results: ExecutionResult[] = [];
|
|
276
|
+
|
|
277
|
+
for (const seed of selectedSeeds) {
|
|
278
|
+
const seedKey = getSeedKey(seed);
|
|
279
|
+
const friendlyName = getFriendlyName(seed);
|
|
280
|
+
setCurrentlyExecuting(friendlyName);
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const result = await executeSeed(seedKey, friendlyName, seed.type);
|
|
284
|
+
results.push(result);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
results.push({
|
|
287
|
+
seedName: friendlyName,
|
|
288
|
+
success: false,
|
|
289
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
setExecutionResults([...results]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
setCurrentlyExecuting(null);
|
|
297
|
+
setExecuting(false);
|
|
298
|
+
|
|
299
|
+
const successCount = results.filter(r => r.success).length;
|
|
300
|
+
const failCount = results.length - successCount;
|
|
301
|
+
|
|
302
|
+
if (failCount === 0) {
|
|
303
|
+
setSnackbarMessage(`Successfully executed ${successCount} seed${successCount > 1 ? 's' : ''}`);
|
|
304
|
+
setSnackbarSeverity('success');
|
|
305
|
+
setSelected(new Set());
|
|
306
|
+
await fetchSeeds();
|
|
307
|
+
} else {
|
|
308
|
+
setSnackbarMessage(`Completed with ${failCount} error${failCount > 1 ? 's' : ''}`);
|
|
309
|
+
setSnackbarSeverity('error');
|
|
310
|
+
}
|
|
311
|
+
setSnackbarOpen(true);
|
|
312
|
+
};
|
|
313
|
+
|
|
70
314
|
if (loading) {
|
|
71
315
|
return (
|
|
72
316
|
<Card>
|
|
@@ -79,15 +323,43 @@ export function SeedManagementWidget() {
|
|
|
79
323
|
);
|
|
80
324
|
}
|
|
81
325
|
|
|
326
|
+
const folders = groupByFolder();
|
|
327
|
+
|
|
82
328
|
return (
|
|
329
|
+
<>
|
|
83
330
|
<Card>
|
|
84
331
|
<CardContent>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
332
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
333
|
+
<Box>
|
|
334
|
+
<Typography variant="h6">
|
|
335
|
+
Seed Management
|
|
336
|
+
</Typography>
|
|
337
|
+
<Typography variant="body2" color="text.secondary">
|
|
338
|
+
Manage and execute seed scripts
|
|
339
|
+
</Typography>
|
|
340
|
+
</Box>
|
|
341
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
342
|
+
{selected.size > 0 && (
|
|
343
|
+
<Button
|
|
344
|
+
variant="contained"
|
|
345
|
+
color="primary"
|
|
346
|
+
startIcon={executing ? <CircularProgress size={16} /> : <PlayArrowIcon />}
|
|
347
|
+
onClick={executeSelected}
|
|
348
|
+
disabled={executing || resetting}
|
|
349
|
+
>
|
|
350
|
+
Run Selected ({selected.size})
|
|
351
|
+
</Button>
|
|
352
|
+
)}
|
|
353
|
+
<Button
|
|
354
|
+
variant="outlined"
|
|
355
|
+
color="error"
|
|
356
|
+
onClick={() => setResetDialogOpen(true)}
|
|
357
|
+
disabled={executing || resetting}
|
|
358
|
+
>
|
|
359
|
+
Reset Database
|
|
360
|
+
</Button>
|
|
361
|
+
</Box>
|
|
362
|
+
</Box>
|
|
91
363
|
|
|
92
364
|
{error && (
|
|
93
365
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
@@ -98,31 +370,243 @@ export function SeedManagementWidget() {
|
|
|
98
370
|
{seeds.length === 0 ? (
|
|
99
371
|
<Alert severity="info">No seed scripts found</Alert>
|
|
100
372
|
) : (
|
|
101
|
-
<
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
373
|
+
<Box>
|
|
374
|
+
{folders.map((folder) => {
|
|
375
|
+
const folderKeys = folder.seeds.map(getSeedKey);
|
|
376
|
+
const allSelected = folderKeys.every(key => selected.has(key));
|
|
377
|
+
const someSelected = folderKeys.some(key => selected.has(key));
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<Accordion key={folder.name} defaultExpanded>
|
|
381
|
+
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
|
382
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
|
|
383
|
+
<Checkbox
|
|
384
|
+
checked={allSelected}
|
|
385
|
+
indeterminate={someSelected && !allSelected}
|
|
386
|
+
onClick={(e) => {
|
|
387
|
+
e.stopPropagation();
|
|
388
|
+
handleToggleFolder(folder);
|
|
389
|
+
}}
|
|
390
|
+
/>
|
|
391
|
+
<FolderIcon color="primary" />
|
|
392
|
+
<Typography variant="subtitle1" sx={{ flexGrow: 1 }}>
|
|
393
|
+
{folder.name}
|
|
394
|
+
</Typography>
|
|
395
|
+
<Chip label={`${folder.seeds.length} seed${folder.seeds.length > 1 ? 's' : ''}`} size="small" />
|
|
396
|
+
</Box>
|
|
397
|
+
</AccordionSummary>
|
|
398
|
+
<AccordionDetails>
|
|
399
|
+
<List dense>
|
|
400
|
+
{folder.seeds.map((seed) => {
|
|
401
|
+
const seedKey = getSeedKey(seed);
|
|
402
|
+
const isSelected = selected.has(seedKey);
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<ListItem
|
|
406
|
+
key={seedKey}
|
|
407
|
+
disablePadding
|
|
408
|
+
secondaryAction={
|
|
409
|
+
<Button
|
|
410
|
+
variant="outlined"
|
|
411
|
+
size="small"
|
|
412
|
+
startIcon={<PlayArrowIcon />}
|
|
413
|
+
onClick={async () => {
|
|
414
|
+
const friendlyName = getFriendlyName(seed);
|
|
415
|
+
setExecuting(true);
|
|
416
|
+
setExecutionDialogOpen(true);
|
|
417
|
+
setExecutionResults([]);
|
|
418
|
+
setCurrentlyExecuting(friendlyName);
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const result = await executeSeed(seedKey, friendlyName, seed.type);
|
|
422
|
+
setExecutionResults([result]);
|
|
423
|
+
|
|
424
|
+
if (result.success) {
|
|
425
|
+
setSnackbarMessage(`${friendlyName} executed successfully`);
|
|
426
|
+
setSnackbarSeverity('success');
|
|
427
|
+
await fetchSeeds();
|
|
428
|
+
} else {
|
|
429
|
+
setSnackbarMessage(`${friendlyName} execution failed`);
|
|
430
|
+
setSnackbarSeverity('error');
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
setExecutionResults([{
|
|
434
|
+
seedName: friendlyName,
|
|
435
|
+
success: false,
|
|
436
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
437
|
+
}]);
|
|
438
|
+
setSnackbarMessage(`${friendlyName} execution failed`);
|
|
439
|
+
setSnackbarSeverity('error');
|
|
440
|
+
} finally {
|
|
441
|
+
setCurrentlyExecuting(null);
|
|
442
|
+
setExecuting(false);
|
|
443
|
+
setSnackbarOpen(true);
|
|
444
|
+
}
|
|
445
|
+
}}
|
|
446
|
+
disabled={executing}
|
|
447
|
+
>
|
|
448
|
+
Run
|
|
449
|
+
</Button>
|
|
450
|
+
}
|
|
451
|
+
>
|
|
452
|
+
<ListItemButton onClick={() => handleToggleSeed(seedKey)}>
|
|
453
|
+
<ListItemIcon>
|
|
454
|
+
<Checkbox
|
|
455
|
+
edge="start"
|
|
456
|
+
checked={isSelected}
|
|
457
|
+
tabIndex={-1}
|
|
458
|
+
disableRipple
|
|
459
|
+
/>
|
|
460
|
+
</ListItemIcon>
|
|
461
|
+
<ListItemText
|
|
462
|
+
primary={getFriendlyName(seed)}
|
|
463
|
+
secondary={seed.description || seed.name}
|
|
464
|
+
/>
|
|
465
|
+
</ListItemButton>
|
|
466
|
+
</ListItem>
|
|
467
|
+
);
|
|
468
|
+
})}
|
|
469
|
+
</List>
|
|
470
|
+
</AccordionDetails>
|
|
471
|
+
</Accordion>
|
|
472
|
+
);
|
|
473
|
+
})}
|
|
474
|
+
</Box>
|
|
124
475
|
)}
|
|
125
476
|
</CardContent>
|
|
126
477
|
</Card>
|
|
478
|
+
|
|
479
|
+
{/* Execution Dialog */}
|
|
480
|
+
<Dialog
|
|
481
|
+
open={executionDialogOpen}
|
|
482
|
+
onClose={() => !executing && setExecutionDialogOpen(false)}
|
|
483
|
+
maxWidth="md"
|
|
484
|
+
fullWidth
|
|
485
|
+
>
|
|
486
|
+
<DialogTitle>
|
|
487
|
+
Seed Execution
|
|
488
|
+
{!executing && (
|
|
489
|
+
<Button
|
|
490
|
+
onClick={() => setExecutionDialogOpen(false)}
|
|
491
|
+
sx={{ position: 'absolute', right: 8, top: 8 }}
|
|
492
|
+
size="small"
|
|
493
|
+
>
|
|
494
|
+
<CloseIcon />
|
|
495
|
+
</Button>
|
|
496
|
+
)}
|
|
497
|
+
</DialogTitle>
|
|
498
|
+
<DialogContent>
|
|
499
|
+
{executing && currentlyExecuting && (
|
|
500
|
+
<Box sx={{ mb: 2 }}>
|
|
501
|
+
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
502
|
+
Currently executing: {currentlyExecuting}
|
|
503
|
+
</Typography>
|
|
504
|
+
<LinearProgress />
|
|
505
|
+
</Box>
|
|
506
|
+
)}
|
|
507
|
+
|
|
508
|
+
{executionResults.length > 0 && (
|
|
509
|
+
<Box>
|
|
510
|
+
{executionResults.map((result, index) => (
|
|
511
|
+
<Paper
|
|
512
|
+
key={index}
|
|
513
|
+
sx={{
|
|
514
|
+
p: 2,
|
|
515
|
+
mb: 1,
|
|
516
|
+
backgroundColor: result.success ? 'success.dark' : 'error.dark',
|
|
517
|
+
color: 'white',
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
|
521
|
+
{result.success ? (
|
|
522
|
+
<CheckCircleIcon color="inherit" />
|
|
523
|
+
) : (
|
|
524
|
+
<ErrorIcon color="inherit" />
|
|
525
|
+
)}
|
|
526
|
+
<Typography variant="subtitle2" fontWeight="bold">
|
|
527
|
+
{result.seedName}
|
|
528
|
+
</Typography>
|
|
529
|
+
</Box>
|
|
530
|
+
{result.output && (
|
|
531
|
+
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
|
532
|
+
{result.output}
|
|
533
|
+
</Typography>
|
|
534
|
+
)}
|
|
535
|
+
{result.error && (
|
|
536
|
+
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
|
537
|
+
{result.error}
|
|
538
|
+
</Typography>
|
|
539
|
+
)}
|
|
540
|
+
</Paper>
|
|
541
|
+
))}
|
|
542
|
+
</Box>
|
|
543
|
+
)}
|
|
544
|
+
</DialogContent>
|
|
545
|
+
<DialogActions>
|
|
546
|
+
<Button onClick={() => setExecutionDialogOpen(false)} disabled={executing}>
|
|
547
|
+
Close
|
|
548
|
+
</Button>
|
|
549
|
+
</DialogActions>
|
|
550
|
+
</Dialog>
|
|
551
|
+
|
|
552
|
+
{/* Database Reset Confirmation Dialog */}
|
|
553
|
+
<Dialog
|
|
554
|
+
open={resetDialogOpen}
|
|
555
|
+
onClose={() => !resetting && setResetDialogOpen(false)}
|
|
556
|
+
maxWidth="sm"
|
|
557
|
+
fullWidth
|
|
558
|
+
>
|
|
559
|
+
<DialogTitle sx={{ color: 'error.main' }}>
|
|
560
|
+
Reset Database?
|
|
561
|
+
</DialogTitle>
|
|
562
|
+
<DialogContent>
|
|
563
|
+
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
564
|
+
This action cannot be undone!
|
|
565
|
+
</Alert>
|
|
566
|
+
<Typography variant="body1" gutterBottom>
|
|
567
|
+
This will permanently delete:
|
|
568
|
+
</Typography>
|
|
569
|
+
<Box component="ul" sx={{ pl: 2 }}>
|
|
570
|
+
<li>All database tables</li>
|
|
571
|
+
<li>All stored data</li>
|
|
572
|
+
<li>All seed execution history</li>
|
|
573
|
+
<li>All application content</li>
|
|
574
|
+
</Box>
|
|
575
|
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
|
576
|
+
You will need to run the database initialization seeds again to recreate the schema.
|
|
577
|
+
</Typography>
|
|
578
|
+
</DialogContent>
|
|
579
|
+
<DialogActions>
|
|
580
|
+
<Button onClick={() => setResetDialogOpen(false)} disabled={resetting}>
|
|
581
|
+
Cancel
|
|
582
|
+
</Button>
|
|
583
|
+
<Button
|
|
584
|
+
onClick={handleDatabaseReset}
|
|
585
|
+
color="error"
|
|
586
|
+
variant="contained"
|
|
587
|
+
disabled={resetting}
|
|
588
|
+
startIcon={resetting ? <CircularProgress size={16} /> : undefined}
|
|
589
|
+
>
|
|
590
|
+
{resetting ? 'Resetting...' : 'Reset Database'}
|
|
591
|
+
</Button>
|
|
592
|
+
</DialogActions>
|
|
593
|
+
</Dialog>
|
|
594
|
+
|
|
595
|
+
{/* Success/Error Snackbar */}
|
|
596
|
+
<Snackbar
|
|
597
|
+
open={snackbarOpen}
|
|
598
|
+
autoHideDuration={6000}
|
|
599
|
+
onClose={() => setSnackbarOpen(false)}
|
|
600
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
601
|
+
>
|
|
602
|
+
<Alert
|
|
603
|
+
onClose={() => setSnackbarOpen(false)}
|
|
604
|
+
severity={snackbarSeverity}
|
|
605
|
+
sx={{ width: '100%' }}
|
|
606
|
+
>
|
|
607
|
+
{snackbarMessage}
|
|
608
|
+
</Alert>
|
|
609
|
+
</Snackbar>
|
|
610
|
+
</>
|
|
127
611
|
);
|
|
128
612
|
}
|
|
@@ -14,5 +14,6 @@ export { SeedManagementWidget } from './SeedManagementWidget';
|
|
|
14
14
|
export { ServiceControlWidget } from './ServiceControlWidget';
|
|
15
15
|
export { EnvironmentConfigWidget } from './EnvironmentConfigWidget';
|
|
16
16
|
export { DatabaseOpsWidget } from './DatabaseOpsWidget';
|
|
17
|
+
export { DatabaseOperationsWidget } from './DatabaseOperationsWidget';
|
|
17
18
|
export { LogsMaintenanceWidget } from './LogsMaintenanceWidget';
|
|
18
19
|
export { CacheMaintenanceWidget } from './CacheMaintenanceWidget';
|