From 687dc689a10ccbb369849f1c5f71d7a74dc2435a Mon Sep 17 00:00:00 2001 From: Marius Orhan Date: Fri, 13 Jun 2025 15:45:50 +0200 Subject: [PATCH 1/3] feat(migration-bundle): Add preset management for migration configurations - Add support for saving/loading migration configurations as Directus presets - Add ENV variable support for default configuration values - Implement preset selector dropdown with update functionality - Add modern UI for preset management with visual change indicators - Remove localStorage implementation in favor of native Directus presets - Add API endpoints for preset CRUD operations This allows admins to set default migration configurations via ENV variables while users can save and manage their own presets through the UI. --- packages/migration-bundle/README.md | 59 +++ packages/migration-bundle/package.json | 1 + .../src/migration-endpoint/index.ts | 129 +++++- .../src/migration-module/module.vue | 413 +++++++++++++++++- .../migration-bundle/src/types/extension.ts | 6 + pnpm-lock.yaml | 3 + 6 files changed, 602 insertions(+), 9 deletions(-) diff --git a/packages/migration-bundle/README.md b/packages/migration-bundle/README.md index 86336a4d..8baa7f95 100644 --- a/packages/migration-bundle/README.md +++ b/packages/migration-bundle/README.md @@ -39,6 +39,65 @@ _For larger projects, migrations naturally use significant bandwidth. Limits hav Refer to the Official Guide for details on installing the extension from the Marketplace or manually. +## Configuration + +### Hybrid Configuration System + +The migration module uses a two-tier configuration system combining environment variables and Directus presets for maximum flexibility. + +### 1. Environment Variables (Admin Defaults) + +Administrators can set default values that apply to all users via environment variables: + +```bash +# Default destination URL +MIGRATION_BUNDLE_DEFAULT_URL=https://stage.example.com + +# Default admin token (optional - can be left empty for security) +MIGRATION_BUNDLE_DEFAULT_TOKEN=your-admin-token + +# Default selected options (comma-separated) +MIGRATION_BUNDLE_DEFAULT_OPTIONS=content,users,flows +``` + +Available options for `MIGRATION_BUNDLE_DEFAULT_OPTIONS`: +- `content` - Collections and their data +- `users` - Users, roles, and permissions +- `comments` - Activity comments +- `presets` - User bookmarks +- `dashboards` - Insights dashboards +- `extensions` - Installed extensions +- `flows` - Automation flows + +### 2. Directus Presets (User Configurations) + +Users can save their migration configurations as Directus presets, which are stored in the database and accessible across sessions and devices. + +**Features:** +- **Named Configurations**: Save multiple configs like "Production Sync", "Content Only", "Full Migration" +- **Scope Control**: Save presets for yourself, your role, or globally (admin only) +- **Quick Access**: Load saved configurations from a dropdown menu +- **Override Defaults**: User presets take precedence over environment defaults + +**How to use:** +1. Configure your migration settings (URL, token, options) +2. Click the bookmark icon (📑) to save as a preset +3. Choose a name and scope (personal/role/global) +4. Load saved presets from the dropdown above the form + +### 3. Configuration Priority + +The system follows this priority order: +1. **User-selected preset** (highest priority) +2. **Environment defaults** (if no preset selected) +3. **Empty form** (if neither configured) + +This approach provides: +- **For Admins**: Environment-specific defaults across environments +- **For Users**: Personal saved configurations +- **For Teams**: Shared configurations via role-based presets +- **Best Practice**: Consistent setups with flexibility + ## How to Customize the Migration 1. On the migration module, add the destination server and token and click **Check** diff --git a/packages/migration-bundle/package.json b/packages/migration-bundle/package.json index 8039d9fd..d7a48867 100644 --- a/packages/migration-bundle/package.json +++ b/packages/migration-bundle/package.json @@ -53,6 +53,7 @@ "@directus/extensions-sdk": "13.0.1", "@directus/types": "^13.0.0", "@types/node": "^22.13.4", + "axios": "^1.7.2", "typescript": "^5.7.3", "vue": "^3.5.13" } diff --git a/packages/migration-bundle/src/migration-endpoint/index.ts b/packages/migration-bundle/src/migration-endpoint/index.ts index 697125b9..0988a01a 100644 --- a/packages/migration-bundle/src/migration-endpoint/index.ts +++ b/packages/migration-bundle/src/migration-endpoint/index.ts @@ -45,9 +45,108 @@ export default defineEndpoint({ const storage = toArray(env.STORAGE_LOCATIONS)[0]; - router.get('/*', async (req, res) => res.sendStatus(400)); + router.get('/*', async (req, res) => { + if (req.url === '/defaults') { + // Return ENV-based defaults + const defaults = { + baseURL: env.MIGRATION_BUNDLE_DEFAULT_URL || '', + token: env.MIGRATION_BUNDLE_DEFAULT_TOKEN || '', + options: env.MIGRATION_BUNDLE_DEFAULT_OPTIONS + ? String(env.MIGRATION_BUNDLE_DEFAULT_OPTIONS).split(',').map((s: string) => s.trim()) + : [] + }; + res.json(defaults); + } else if (req.url === '/presets') { + // Get migration presets for current user + const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null; + + if (!accountability?.admin) { + res.sendStatus(401); + return; + } + + try { + // Query presets with proper priority (user > role > global) + const presets = await database('directus_presets') + .where({ + collection: 'migration_bundle', + user: accountability.user + }) + .orWhere(function() { + this.where({ + collection: 'migration_bundle', + user: null, + role: accountability.role + }); + }) + .orWhere(function() { + this.where({ + collection: 'migration_bundle', + user: null, + role: null + }); + }) + .whereNotNull('bookmark') + .orderBy([ + { column: 'user', order: 'desc', nulls: 'last' }, + { column: 'role', order: 'desc', nulls: 'last' }, + { column: 'bookmark', order: 'asc' } + ]); + + res.json({ data: presets }); + } catch (error) { + console.error('Failed to load presets:', error); + res.json({ data: [] }); + } + } else { + res.sendStatus(400); + } + }); router.post('/*', async (req, res) => { + if (req.url === '/presets') { + // Create or update migration preset + const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null; + + if (!accountability?.admin) { + res.sendStatus(401); + return; + } + + try { + const preset = { + collection: 'migration_bundle', + bookmark: req.body.name || `Migration Config ${new Date().toLocaleDateString()}`, + user: req.body.scope === 'user' ? accountability.user : + req.body.scope === 'global' ? null : accountability.user, + role: req.body.scope === 'role' ? accountability.role : null, + icon: 'sync', + color: '#2ECDA7', + layout: 'custom', + layout_options: JSON.stringify({ + baseURL: req.body.baseURL, + token: req.body.token, + selectedOptions: req.body.options + }) + }; + + // Check if preset exists for update + if (req.body.id) { + await database('directus_presets') + .where({ id: req.body.id }) + .update(preset); + res.json({ data: { ...preset, id: req.body.id } }); + } else { + const [id] = await database('directus_presets').insert(preset); + res.json({ data: { ...preset, id } }); + } + } catch (error) { + console.error('Failed to save preset:', error); + res.status(500).json({ error: 'Failed to save preset' }); + } + return; + } + if (!['/run', '/dry-run', '/check'].includes(req.url)) { res.sendStatus(400); throw new Error('Bad Request'); @@ -344,5 +443,33 @@ export default defineEndpoint({ } } }); + + // DELETE endpoint for presets + router.delete('/presets/:id', async (req, res) => { + const accountability: Accountability | null = 'accountability' in req ? req.accountability as Accountability : null; + + if (!accountability?.admin) { + res.sendStatus(401); + return; + } + + try { + const presetId = parseInt(req.params.id); + + // Only allow deletion of user's own presets or if they have proper permissions + await database('directus_presets') + .where({ + id: presetId, + collection: 'migration_bundle', + user: accountability.user + }) + .delete(); + + res.json({ success: true }); + } catch (error) { + console.error('Failed to delete preset:', error); + res.status(500).json({ error: 'Failed to delete preset' }); + } + }); }, }); diff --git a/packages/migration-bundle/src/migration-module/module.vue b/packages/migration-bundle/src/migration-module/module.vue index f7953f55..a6f5cb76 100644 --- a/packages/migration-bundle/src/migration-module/module.vue +++ b/packages/migration-bundle/src/migration-module/module.vue @@ -1,8 +1,8 @@ @@ -133,13 +387,60 @@ export default defineComponent({

To get started, enter the destination URL and admin token below, then click Check. This will compare both Directus platform and see if they are compatible. Please make sure the destination instance is on the same version and the same database engine as this instance.

-
+ + +
+
+ + + + + + + + +
+
+ +
- - - + + + Check + + +
@@ -198,6 +499,46 @@ export default defineComponent({
+ + + + + Save Migration Configuration + +
+ +
+
+ + + Cancel + + + Save + + +
+
+ + + + + Delete Preset + Are you sure you want to delete this preset? This action cannot be undone. + + + Cancel + + + Delete + + + + @@ -215,6 +556,39 @@ export default defineComponent({ padding: 0 var(--content-padding); } +.migration-preset-selector { + margin-bottom: var(--theme--form--row-gap); + position: relative; +} + +.migration-preset-selector .preset-row { + display: flex; + gap: 8px; + align-items: stretch; + background-color: var(--theme--background-subdued); + border: var(--theme--border-width) solid var(--theme--border-color); + border-radius: var(--theme--border-radius); + padding: 4px; +} + +.migration-preset-selector .v-select { + flex: 1; + background: transparent; + border: none; +} + +.migration-preset-selector .v-select :deep(.v-input) { + background: transparent; + border: none; + box-shadow: none; +} + +.migration-preset-selector .v-button { + border-radius: calc(var(--theme--border-radius) - 4px); + transition: all 0.2s ease; +} + + .migration-input-container { padding: var(--theme--form--field--input--padding); background-color: var(--theme--background-subdued); @@ -226,8 +600,9 @@ export default defineComponent({ .migration-input { display: grid; - grid-template-columns: 2fr 2fr 1fr; + grid-template-columns: 2fr 2fr 1fr auto; gap: 20px; + align-items: end; } .migration-input:has(+ .migration-validation) { @@ -453,4 +828,26 @@ h3.skipped .icon i::after { .migration-container .pending:has(+ .error) .progress-circular { display: none; } + +/* Fade slide animation */ +.fade-slide-enter-active, +.fade-slide-leave-active { + transition: all 0.2s ease; +} + +.fade-slide-enter-from { + opacity: 0; + transform: translateX(-10px); +} + +.fade-slide-leave-to { + opacity: 0; + transform: translateX(10px); +} + +/* Optional: Add a subtle indicator when preset has changes */ +.migration-preset-selector.has-changes .preset-row { + border-color: var(--theme--primary); + box-shadow: 0 0 0 1px var(--theme--primary-background); +} diff --git a/packages/migration-bundle/src/types/extension.ts b/packages/migration-bundle/src/types/extension.ts index 7088ae68..cd99939a 100644 --- a/packages/migration-bundle/src/types/extension.ts +++ b/packages/migration-bundle/src/types/extension.ts @@ -261,4 +261,10 @@ export interface CommentRaw extends Omit; +} + export type JSONInput = SchemaOverview | Role | RoleRaw | Policy | PolicyRaw | Permission | User | UserRaw | Access | Folder | File | Dashboard | DashboardRaw | FlowRaw | ModifiedFlowRaw | OperationRaw | Panel | Operation | Settings | Translation | Preset | Extension | Comment | CommentRaw | Share | UserCollectionItems | UserCollectionItem; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de1236f1..5c9d156b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -734,6 +734,9 @@ importers: '@types/node': specifier: ^22.13.4 version: 22.15.17 + axios: + specifier: ^1.7.2 + version: 1.9.0 typescript: specifier: ^5.7.3 version: 5.8.3 From a022c5249a52995a33445f25120daee820d7eb0c Mon Sep 17 00:00:00 2001 From: Marius Orhan Date: Mon, 16 Jun 2025 12:07:30 +0200 Subject: [PATCH 2/3] fix: Remove unnecessary axios dependency --- packages/migration-bundle/package.json | 1 - pnpm-lock.yaml | 3 --- 2 files changed, 4 deletions(-) diff --git a/packages/migration-bundle/package.json b/packages/migration-bundle/package.json index d7a48867..8039d9fd 100644 --- a/packages/migration-bundle/package.json +++ b/packages/migration-bundle/package.json @@ -53,7 +53,6 @@ "@directus/extensions-sdk": "13.0.1", "@directus/types": "^13.0.0", "@types/node": "^22.13.4", - "axios": "^1.7.2", "typescript": "^5.7.3", "vue": "^3.5.13" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c9d156b..de1236f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -734,9 +734,6 @@ importers: '@types/node': specifier: ^22.13.4 version: 22.15.17 - axios: - specifier: ^1.7.2 - version: 1.9.0 typescript: specifier: ^5.7.3 version: 5.8.3 From 102aabcab1c4c775a85f11d75857ce9a45661eb9 Mon Sep 17 00:00:00 2001 From: Marius Orhan Date: Mon, 16 Jun 2025 13:38:37 +0200 Subject: [PATCH 3/3] fix: Move ref declarations before usage for better code clarity --- .../src/migration-module/module.vue | 128 +++++++++--------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/packages/migration-bundle/src/migration-module/module.vue b/packages/migration-bundle/src/migration-module/module.vue index a6f5cb76..c9ffa6d3 100644 --- a/packages/migration-bundle/src/migration-module/module.vue +++ b/packages/migration-bundle/src/migration-module/module.vue @@ -1,6 +1,6 @@