@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/src/core/control-panel.js +5 -5
  3. package/dist/src/core/control-panel.js.map +1 -1
  4. package/dist/src/core/gateway.d.ts.map +1 -1
  5. package/dist/src/core/gateway.js +117 -15
  6. package/dist/src/core/gateway.js.map +1 -1
  7. package/dist/src/core/plugin-registry.d.ts +70 -0
  8. package/dist/src/core/plugin-registry.d.ts.map +1 -1
  9. package/dist/src/core/plugin-registry.js +94 -0
  10. package/dist/src/core/plugin-registry.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
  14. package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
  15. package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
  16. package/dist/src/plugins/api-keys/index.d.ts +1 -1
  17. package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
  18. package/dist/src/plugins/api-keys/index.js.map +1 -1
  19. package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
  20. package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
  21. package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
  22. package/dist/src/plugins/api-keys/types.d.ts +13 -1
  23. package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
  24. package/dist/src/plugins/api-keys/types.js.map +1 -1
  25. package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
  26. package/dist/src/plugins/diagnostics-plugin.js +73 -0
  27. package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
  28. package/dist/src/plugins/index.d.ts +1 -1
  29. package/dist/src/plugins/index.d.ts.map +1 -1
  30. package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
  31. package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
  32. package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
  33. package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
  34. package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
  35. package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
  36. package/dist/src/plugins/maintenance/SeedList.js +39 -14
  37. package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
  38. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
  39. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
  40. package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
  41. package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
  42. package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
  43. package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
  44. package/dist/src/plugins/maintenance/seed-executor.js +53 -17
  45. package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
  46. package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
  47. package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
  48. package/dist/src/plugins/maintenance-plugin.js +222 -34
  49. package/dist/src/plugins/maintenance-plugin.js.map +1 -1
  50. package/dist/src/plugins/postgres-plugin.d.ts +12 -0
  51. package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
  52. package/dist/src/plugins/postgres-plugin.js +319 -5
  53. package/dist/src/plugins/postgres-plugin.js.map +1 -1
  54. package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
  55. package/dist/ui/src/components/ControlPanelApp.js +4 -3
  56. package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
  57. package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
  58. package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
  59. package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
  60. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
  61. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
  62. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
  63. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
  64. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
  65. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
  66. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
  67. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
  68. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
  69. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
  70. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
  71. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
  72. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
  73. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
  74. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
  75. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
  76. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  77. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
  78. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
  79. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
  80. package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
  81. package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
  82. package/dist/ui/src/dashboard/widgets/index.js +1 -0
  83. package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
  84. package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
  85. package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
  86. package/dist-ui/index.html +1 -1
  87. package/dist-ui-lib/index.js +3735 -3187
  88. package/dist-ui-lib/index.js.map +1 -1
  89. package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
  90. package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  91. package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
  92. package/package.json +2 -2
  93. package/src/core/control-panel.ts +5 -5
  94. package/src/core/gateway.ts +135 -15
  95. package/src/core/plugin-registry.ts +171 -0
  96. package/src/index.ts +2 -0
  97. package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
  98. package/src/plugins/api-keys/index.ts +1 -0
  99. package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
  100. package/src/plugins/api-keys/types.ts +14 -1
  101. package/src/plugins/diagnostics-plugin.ts +77 -0
  102. package/src/plugins/index.ts +1 -1
  103. package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
  104. package/src/plugins/maintenance/SeedList.tsx +85 -38
  105. package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
  106. package/src/plugins/maintenance/seed-executor.ts +56 -17
  107. package/src/plugins/maintenance-plugin.ts +267 -36
  108. package/src/plugins/postgres-plugin.ts +410 -5
  109. package/ui/src/App.tsx +3 -3
  110. package/ui/src/components/ControlPanelApp.tsx +4 -3
  111. package/ui/src/dashboard/builtInWidgets.tsx +3 -0
  112. package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
  113. package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
  114. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
  115. package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
  116. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
  117. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
  118. package/ui/src/dashboard/widgets/index.ts +1 -0
  119. package/dist-ui/assets/index-0gzisPdy.js +0 -528
  120. 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
- List,
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: string;
28
- size: number;
29
- createdAt: string;
30
- modifiedAt: string;
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<string | null>(null);
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
- // TODO: Add proper API endpoint for seed discovery
46
- // const response = await api.get('/maintenance/seeds/discover');
47
- // setSeeds(response.seeds || []);
48
- setSeeds([]);
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 executeSeed = async (seedName: string) => {
58
- setExecuting(seedName);
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
- // TODO: Add proper API endpoint for seed execution
61
- // await api.post(`/maintenance/seeds/execute`, { name: seedName });
62
- alert(`Seed ${seedName} executed successfully`);
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
- alert(`Failed to execute seed: ${err instanceof Error ? err.message : 'Unknown error'}`);
259
+ setSnackbarMessage(err instanceof Error ? err.message : 'Database reset failed');
260
+ setSnackbarSeverity('error');
261
+ setSnackbarOpen(true);
65
262
  } finally {
66
- setExecuting(null);
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
- <Typography variant="h6" gutterBottom>
86
- Seed Management
87
- </Typography>
88
- <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
89
- Manage and execute seed scripts
90
- </Typography>
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
- <List>
102
- {seeds.map((seed) => (
103
- <ListItem
104
- key={seed.name}
105
- secondaryAction={
106
- <Button
107
- variant="contained"
108
- size="small"
109
- startIcon={executing === seed.name ? <CircularProgress size={16} /> : <PlayArrowIcon />}
110
- onClick={() => executeSeed(seed.name)}
111
- disabled={executing !== null}
112
- >
113
- Execute
114
- </Button>
115
- }
116
- >
117
- <ListItemText
118
- primary={seed.name}
119
- secondary={`Modified: ${new Date(seed.modifiedAt).toLocaleDateString()}`}
120
- />
121
- </ListItem>
122
- ))}
123
- </List>
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';