@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
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
UpdateApiKeyParams,
|
|
22
22
|
ApiKey,
|
|
23
23
|
ApiKeyWithPlaintext,
|
|
24
|
+
StoreInitializationResult,
|
|
24
25
|
} from '../types.js';
|
|
25
26
|
|
|
26
27
|
// Pool interface (from pg package)
|
|
@@ -239,82 +240,104 @@ export function postgresApiKeyStore(config: PostgresApiKeyStoreConfig): ApiKeySt
|
|
|
239
240
|
return {
|
|
240
241
|
name: 'postgres',
|
|
241
242
|
|
|
242
|
-
async initialize(): Promise<
|
|
243
|
-
if (!autoCreateTables)
|
|
243
|
+
async initialize(): Promise<StoreInitializationResult> {
|
|
244
|
+
if (!autoCreateTables) {
|
|
245
|
+
return { success: true };
|
|
246
|
+
}
|
|
244
247
|
|
|
245
|
-
|
|
248
|
+
try {
|
|
249
|
+
const pool = getPool();
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
251
|
+
// Create table with foreign key to users
|
|
252
|
+
await pool.query(`
|
|
253
|
+
CREATE TABLE IF NOT EXISTS ${tableFullName} (
|
|
254
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
255
|
+
user_id UUID NOT NULL REFERENCES "public"."users"(id) ON DELETE CASCADE,
|
|
256
|
+
name VARCHAR(255) NOT NULL,
|
|
257
|
+
key_hash VARCHAR(64) NOT NULL,
|
|
258
|
+
key_prefix VARCHAR(12) NOT NULL,
|
|
259
|
+
key_type VARCHAR(10) NOT NULL CHECK (key_type IN ('m2m', 'pat')),
|
|
260
|
+
scopes TEXT[] NOT NULL DEFAULT '{}',
|
|
261
|
+
last_used_at TIMESTAMPTZ,
|
|
262
|
+
expires_at TIMESTAMPTZ,
|
|
263
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
264
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
265
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
|
|
269
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_key_prefix ON ${tableFullName}(key_prefix);
|
|
270
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_key_hash ON ${tableFullName}(key_hash);
|
|
271
|
+
`);
|
|
263
272
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
273
|
+
// Migration: Check if table needs user_id column migration
|
|
274
|
+
const needsMigration = await pool.query(`
|
|
275
|
+
SELECT
|
|
276
|
+
NOT EXISTS (
|
|
277
|
+
SELECT 1 FROM information_schema.columns
|
|
278
|
+
WHERE table_schema = '${schema}'
|
|
279
|
+
AND table_name = '${tableName}'
|
|
280
|
+
AND column_name = 'user_id'
|
|
281
|
+
) AS missing_column,
|
|
282
|
+
(SELECT COUNT(*) FROM ${tableFullName}) AS row_count
|
|
283
|
+
`);
|
|
284
|
+
|
|
285
|
+
const { missing_column, row_count } = needsMigration.rows[0] as { missing_column: boolean; row_count: number };
|
|
286
|
+
|
|
287
|
+
if (missing_column) {
|
|
288
|
+
if (row_count > 0) {
|
|
289
|
+
// Table has data but is missing user_id column - requires maintenance
|
|
290
|
+
logger.warn(
|
|
291
|
+
`Table ${tableFullName} has ${row_count} rows but is missing user_id column. ` +
|
|
292
|
+
`Requires maintenance action to migrate or clear data.`
|
|
293
|
+
);
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
error: `Table ${tableFullName} contains ${row_count} rows without user_id column. Migration required.`,
|
|
297
|
+
requiresMaintenance: true,
|
|
298
|
+
};
|
|
299
|
+
} else {
|
|
300
|
+
// Table is empty, safe to add NOT NULL column
|
|
301
|
+
await pool.query(`
|
|
290
302
|
ALTER TABLE ${tableFullName}
|
|
291
303
|
ADD COLUMN user_id UUID NOT NULL REFERENCES "public"."users"(id) ON DELETE CASCADE;
|
|
292
304
|
|
|
293
305
|
CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// Enable RLS if configured
|
|
300
|
-
if (enableRLS) {
|
|
301
|
-
await pool.query(`
|
|
302
|
-
ALTER TABLE ${tableFullName} ENABLE ROW LEVEL SECURITY;
|
|
303
|
-
ALTER TABLE ${tableFullName} FORCE ROW LEVEL SECURITY;
|
|
304
|
-
`);
|
|
306
|
+
`);
|
|
307
|
+
logger.info(`Added user_id column to empty table ${tableFullName}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
305
310
|
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
// Enable RLS if configured
|
|
312
|
+
if (enableRLS) {
|
|
313
|
+
await pool.query(`
|
|
314
|
+
ALTER TABLE ${tableFullName} ENABLE ROW LEVEL SECURITY;
|
|
315
|
+
ALTER TABLE ${tableFullName} FORCE ROW LEVEL SECURITY;
|
|
316
|
+
`);
|
|
317
|
+
|
|
318
|
+
// Create or replace the RLS policy
|
|
319
|
+
await pool.query(`
|
|
320
|
+
DROP POLICY IF EXISTS "${tableName}_owner" ON ${tableFullName};
|
|
321
|
+
`);
|
|
322
|
+
|
|
323
|
+
// RLS policy: users can only access their own keys
|
|
324
|
+
await pool.query(`
|
|
325
|
+
CREATE POLICY "${tableName}_owner" ON ${tableFullName}
|
|
326
|
+
FOR ALL
|
|
327
|
+
USING (user_id::text = current_setting('app.current_user_id', true))
|
|
328
|
+
WITH CHECK (user_id::text = current_setting('app.current_user_id', true));
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
310
331
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
332
|
+
logger.info(`API keys store initialized successfully (table: ${tableFullName})`);
|
|
333
|
+
return { success: true };
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
336
|
+
logger.error(`Failed to initialize API keys store: ${errorMessage}`, { error });
|
|
337
|
+
return {
|
|
338
|
+
success: false,
|
|
339
|
+
error: `Database initialization failed: ${errorMessage}`,
|
|
340
|
+
};
|
|
318
341
|
}
|
|
319
342
|
},
|
|
320
343
|
|
|
@@ -140,6 +140,18 @@ export interface ApiKeyWithPlaintext extends ApiKey {
|
|
|
140
140
|
plaintext_key: string;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Result of store initialization
|
|
145
|
+
*/
|
|
146
|
+
export interface StoreInitializationResult {
|
|
147
|
+
/** Whether initialization succeeded */
|
|
148
|
+
success: boolean;
|
|
149
|
+
/** Error message if initialization failed */
|
|
150
|
+
error?: string;
|
|
151
|
+
/** Additional details about what needs to be done */
|
|
152
|
+
requiresMaintenance?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
/**
|
|
144
156
|
* API key store interface - all storage backends must implement this
|
|
145
157
|
*/
|
|
@@ -149,8 +161,9 @@ export interface ApiKeyStore {
|
|
|
149
161
|
|
|
150
162
|
/**
|
|
151
163
|
* Initialize the store (create tables, RLS policies, etc.)
|
|
164
|
+
* Returns initialization status instead of throwing
|
|
152
165
|
*/
|
|
153
|
-
initialize(): Promise<
|
|
166
|
+
initialize(): Promise<StoreInitializationResult>;
|
|
154
167
|
|
|
155
168
|
/**
|
|
156
169
|
* Create a new API key
|
|
@@ -148,6 +148,83 @@ export function createDiagnosticsPlugin(config: DiagnosticsPluginConfig = {}): P
|
|
|
148
148
|
},
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
// Register maintenance listing endpoint (GET /diagnostics/maintenance)
|
|
152
|
+
registry.addRoute({
|
|
153
|
+
method: 'get',
|
|
154
|
+
path: '/maintenance',
|
|
155
|
+
pluginId: 'diagnostics',
|
|
156
|
+
handler: (_req: Request, res: Response) => {
|
|
157
|
+
try {
|
|
158
|
+
const pluginsNeedingMaintenance = registry.getPluginsNeedingMaintenance();
|
|
159
|
+
|
|
160
|
+
res.json({
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
count: pluginsNeedingMaintenance.length,
|
|
163
|
+
plugins: pluginsNeedingMaintenance.map(info => ({
|
|
164
|
+
pluginId: info.pluginId,
|
|
165
|
+
error: info.error,
|
|
166
|
+
recommendedAction: info.recommendedAction,
|
|
167
|
+
actions: info.actions.map(action => ({
|
|
168
|
+
id: action.id,
|
|
169
|
+
name: action.name,
|
|
170
|
+
description: action.description,
|
|
171
|
+
destructive: action.destructive,
|
|
172
|
+
})),
|
|
173
|
+
})),
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
res.status(500).json({
|
|
177
|
+
error: 'Failed to retrieve maintenance info',
|
|
178
|
+
message: error instanceof Error ? error.message : String(error),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Register maintenance action endpoint (POST /diagnostics/maintenance/:pluginId/:actionId)
|
|
185
|
+
registry.addRoute({
|
|
186
|
+
method: 'post',
|
|
187
|
+
path: '/maintenance/:pluginId/:actionId',
|
|
188
|
+
pluginId: 'diagnostics',
|
|
189
|
+
handler: async (req: Request, res: Response) => {
|
|
190
|
+
try {
|
|
191
|
+
const { pluginId, actionId } = req.params;
|
|
192
|
+
|
|
193
|
+
if (!pluginId || !actionId) {
|
|
194
|
+
return res.status(400).json({
|
|
195
|
+
error: 'Missing required parameters',
|
|
196
|
+
message: 'Both pluginId and actionId are required',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
logger.info(`Executing maintenance action ${actionId} for plugin ${pluginId}`);
|
|
201
|
+
|
|
202
|
+
const result = await registry.runMaintenanceAction(pluginId, actionId);
|
|
203
|
+
|
|
204
|
+
if (result.success) {
|
|
205
|
+
res.json({
|
|
206
|
+
success: true,
|
|
207
|
+
message: result.message || 'Maintenance action completed successfully',
|
|
208
|
+
pluginId,
|
|
209
|
+
actionId,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
res.status(500).json({
|
|
213
|
+
success: false,
|
|
214
|
+
error: result.error || 'Maintenance action failed',
|
|
215
|
+
pluginId,
|
|
216
|
+
actionId,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
res.status(500).json({
|
|
221
|
+
error: 'Failed to execute maintenance action',
|
|
222
|
+
message: error instanceof Error ? error.message : String(error),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
151
228
|
logger.debug('Diagnostics plugin initialized');
|
|
152
229
|
},
|
|
153
230
|
|
package/src/plugins/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { createLogsPlugin } from './logs-plugin.js';
|
|
|
14
14
|
export type { LogsPluginConfig } from './logs-plugin.js';
|
|
15
15
|
|
|
16
16
|
export { createMaintenancePlugin } from './maintenance-plugin.js';
|
|
17
|
-
export type { MaintenancePluginConfig } from './maintenance-plugin.js';
|
|
17
|
+
export type { MaintenancePluginConfig, SeedTask, SeedTaskHandler } from './maintenance-plugin.js';
|
|
18
18
|
export {
|
|
19
19
|
MaintenanceManagementPage,
|
|
20
20
|
MaintenanceStatusWidget,
|
|
@@ -11,6 +11,8 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
11
11
|
export interface SeedExecutorProps {
|
|
12
12
|
apiPrefix: string;
|
|
13
13
|
seedName: string;
|
|
14
|
+
seedType?: string;
|
|
15
|
+
seedOptions?: any;
|
|
14
16
|
onComplete: () => void;
|
|
15
17
|
onCancel: () => void;
|
|
16
18
|
}
|
|
@@ -26,6 +28,8 @@ interface OutputLine {
|
|
|
26
28
|
export const SeedExecutor: React.FC<SeedExecutorProps> = ({
|
|
27
29
|
apiPrefix,
|
|
28
30
|
seedName,
|
|
31
|
+
seedType = 'file',
|
|
32
|
+
seedOptions,
|
|
29
33
|
onComplete,
|
|
30
34
|
onCancel,
|
|
31
35
|
}) => {
|
|
@@ -63,7 +67,11 @@ export const SeedExecutor: React.FC<SeedExecutorProps> = ({
|
|
|
63
67
|
headers: {
|
|
64
68
|
'Content-Type': 'application/json',
|
|
65
69
|
},
|
|
66
|
-
body: JSON.stringify({
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
name: seedName,
|
|
72
|
+
type: seedType,
|
|
73
|
+
options: seedOptions,
|
|
74
|
+
}),
|
|
67
75
|
});
|
|
68
76
|
|
|
69
77
|
if (!response.ok) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Seed List Component
|
|
3
3
|
*
|
|
4
|
-
* Displays available seed scripts with metadata.
|
|
4
|
+
* Displays available seed scripts and custom tasks with metadata.
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
7
|
*/
|
|
@@ -10,10 +10,11 @@ import React, { useEffect, useState } from 'react';
|
|
|
10
10
|
|
|
11
11
|
export interface SeedListProps {
|
|
12
12
|
apiPrefix: string;
|
|
13
|
-
onExecute: (seedName: string) => void;
|
|
13
|
+
onExecute: (seedName: string, type?: string, options?: any) => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface SeedFile {
|
|
17
|
+
type: 'file';
|
|
17
18
|
name: string;
|
|
18
19
|
path: string;
|
|
19
20
|
size: number;
|
|
@@ -21,8 +22,18 @@ interface SeedFile {
|
|
|
21
22
|
modifiedAt: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
interface CustomTask {
|
|
26
|
+
type: 'task';
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
options?: Record<string, any>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type SeedItem = SeedFile | CustomTask;
|
|
34
|
+
|
|
24
35
|
export const SeedList: React.FC<SeedListProps> = ({ apiPrefix, onExecute }) => {
|
|
25
|
-
const [seeds, setSeeds] = useState<
|
|
36
|
+
const [seeds, setSeeds] = useState<SeedItem[]>([]);
|
|
26
37
|
const [loading, setLoading] = useState(true);
|
|
27
38
|
const [error, setError] = useState<string | null>(null);
|
|
28
39
|
|
|
@@ -57,6 +68,14 @@ export const SeedList: React.FC<SeedListProps> = ({ apiPrefix, onExecute }) => {
|
|
|
57
68
|
return new Date(dateString).toLocaleString();
|
|
58
69
|
};
|
|
59
70
|
|
|
71
|
+
const isFileType = (item: SeedItem): item is SeedFile => {
|
|
72
|
+
return item.type === 'file';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isTaskType = (item: SeedItem): item is CustomTask => {
|
|
76
|
+
return item.type === 'task';
|
|
77
|
+
};
|
|
78
|
+
|
|
60
79
|
if (loading) {
|
|
61
80
|
return <div style={{ padding: '20px' }}>Loading seeds...</div>;
|
|
62
81
|
}
|
|
@@ -72,55 +91,83 @@ export const SeedList: React.FC<SeedListProps> = ({ apiPrefix, onExecute }) => {
|
|
|
72
91
|
if (seeds.length === 0) {
|
|
73
92
|
return (
|
|
74
93
|
<div style={{ padding: '20px', color: '#666' }}>
|
|
75
|
-
No seed scripts
|
|
94
|
+
No seed scripts or tasks found.
|
|
76
95
|
</div>
|
|
77
96
|
);
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
return (
|
|
81
100
|
<div style={{ padding: '20px' }}>
|
|
82
|
-
<h3>Available
|
|
101
|
+
<h3>Available Seeds & Tasks ({seeds.length})</h3>
|
|
83
102
|
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '16px' }}>
|
|
84
103
|
<thead>
|
|
85
104
|
<tr style={{ borderBottom: '2px solid #ddd', textAlign: 'left' }}>
|
|
105
|
+
<th style={{ padding: '12px' }}>Type</th>
|
|
86
106
|
<th style={{ padding: '12px' }}>Name</th>
|
|
87
|
-
<th style={{ padding: '12px' }}>
|
|
88
|
-
<th style={{ padding: '12px' }}>
|
|
107
|
+
<th style={{ padding: '12px' }}>Description</th>
|
|
108
|
+
<th style={{ padding: '12px' }}>Details</th>
|
|
89
109
|
<th style={{ padding: '12px' }}>Action</th>
|
|
90
110
|
</tr>
|
|
91
111
|
</thead>
|
|
92
112
|
<tbody>
|
|
93
|
-
{seeds.map((seed) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
{seeds.map((seed) => {
|
|
114
|
+
const itemKey = isFileType(seed) ? seed.name : seed.id;
|
|
115
|
+
return (
|
|
116
|
+
<tr key={itemKey} style={{ borderBottom: '1px solid #eee' }}>
|
|
117
|
+
<td style={{ padding: '12px' }}>
|
|
118
|
+
<span
|
|
119
|
+
style={{
|
|
120
|
+
display: 'inline-block',
|
|
121
|
+
padding: '4px 8px',
|
|
122
|
+
borderRadius: '4px',
|
|
123
|
+
fontSize: '12px',
|
|
124
|
+
fontWeight: 'bold',
|
|
125
|
+
backgroundColor: isFileType(seed) ? '#e3f2fd' : '#f3e5f5',
|
|
126
|
+
color: isFileType(seed) ? '#1976d2' : '#7b1fa2',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{isFileType(seed) ? 'FILE' : 'TASK'}
|
|
130
|
+
</span>
|
|
131
|
+
</td>
|
|
132
|
+
<td style={{ padding: '12px', fontFamily: 'monospace' }}>
|
|
133
|
+
{isFileType(seed) ? seed.name : seed.name}
|
|
134
|
+
</td>
|
|
135
|
+
<td style={{ padding: '12px', fontSize: '14px', color: '#666' }}>
|
|
136
|
+
{isTaskType(seed) ? seed.description : '-'}
|
|
137
|
+
</td>
|
|
138
|
+
<td style={{ padding: '12px', fontSize: '14px', color: '#666' }}>
|
|
139
|
+
{isFileType(seed)
|
|
140
|
+
? `${formatFileSize(seed.size)} • ${formatDate(seed.modifiedAt)}`
|
|
141
|
+
: '-'}
|
|
142
|
+
</td>
|
|
143
|
+
<td style={{ padding: '12px' }}>
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => {
|
|
146
|
+
const displayName = isFileType(seed) ? seed.name : seed.name;
|
|
147
|
+
if (confirm(`Execute ${displayName}?`)) {
|
|
148
|
+
if (isFileType(seed)) {
|
|
149
|
+
onExecute(seed.name, 'file');
|
|
150
|
+
} else {
|
|
151
|
+
onExecute(seed.id, 'task', seed.options);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
style={{
|
|
156
|
+
padding: '6px 12px',
|
|
157
|
+
backgroundColor: '#1976d2',
|
|
158
|
+
color: 'white',
|
|
159
|
+
border: 'none',
|
|
160
|
+
borderRadius: '4px',
|
|
161
|
+
cursor: 'pointer',
|
|
162
|
+
}}
|
|
163
|
+
data-testid={`execute-${itemKey}`}
|
|
164
|
+
>
|
|
165
|
+
Execute
|
|
166
|
+
</button>
|
|
167
|
+
</td>
|
|
168
|
+
</tr>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
124
171
|
</tbody>
|
|
125
172
|
</table>
|
|
126
173
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Seed Management Page
|
|
3
3
|
*
|
|
4
|
-
* Main page for managing and executing seed scripts.
|
|
4
|
+
* Main page for managing and executing seed scripts and custom tasks.
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
7
7
|
*/
|
|
@@ -23,9 +23,13 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
|
|
|
23
23
|
}) => {
|
|
24
24
|
const [activeTab, setActiveTab] = useState<Tab>('list');
|
|
25
25
|
const [selectedSeed, setSelectedSeed] = useState<string | null>(null);
|
|
26
|
+
const [selectedType, setSelectedType] = useState<string>('file');
|
|
27
|
+
const [selectedOptions, setSelectedOptions] = useState<any>(undefined);
|
|
26
28
|
|
|
27
|
-
const handleExecute = (seedName: string) => {
|
|
29
|
+
const handleExecute = (seedName: string, type: string = 'file', options?: any) => {
|
|
28
30
|
setSelectedSeed(seedName);
|
|
31
|
+
setSelectedType(type);
|
|
32
|
+
setSelectedOptions(options);
|
|
29
33
|
setActiveTab('execute');
|
|
30
34
|
};
|
|
31
35
|
|
|
@@ -36,7 +40,7 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
|
|
|
36
40
|
return (
|
|
37
41
|
<PluginManagementPage
|
|
38
42
|
title="Seed Management"
|
|
39
|
-
description="Manage and execute database seed scripts"
|
|
43
|
+
description="Manage and execute database seed scripts and custom tasks"
|
|
40
44
|
>
|
|
41
45
|
<div style={{ marginBottom: '20px' }}>
|
|
42
46
|
<button
|
|
@@ -51,7 +55,7 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
|
|
|
51
55
|
cursor: 'pointer',
|
|
52
56
|
}}
|
|
53
57
|
>
|
|
54
|
-
Available Seeds
|
|
58
|
+
Available Seeds & Tasks
|
|
55
59
|
</button>
|
|
56
60
|
<button
|
|
57
61
|
onClick={() => setActiveTab('history')}
|
|
@@ -76,6 +80,8 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
|
|
|
76
80
|
<SeedExecutor
|
|
77
81
|
apiPrefix={apiPrefix}
|
|
78
82
|
seedName={selectedSeed}
|
|
83
|
+
seedType={selectedType}
|
|
84
|
+
seedOptions={selectedOptions}
|
|
79
85
|
onComplete={handleExecutionComplete}
|
|
80
86
|
onCancel={() => setActiveTab('list')}
|
|
81
87
|
/>
|