Browse Source
- Create DatabaseMigration.vue with vue-facing-decorator and Tailwind CSS - Add complete UI for comparing and migrating data between Dexie and SQLite - Implement real-time loading states, error handling, and success feedback - Add navigation link to Account page for easy access - Include export functionality for comparison data - Create comprehensive documentation in doc/database-migration-guide.md - Fix all linting issues and ensure code quality standards - Support both contact and settings migration with overwrite options - Add visual difference analysis with summary cards and detailed breakdowns The component provides a professional interface for the migrationService.ts, enabling users to safely transfer data between database systems during the transition from Dexie to SQLite storage.migrate-dexie-to-sqlite
5 changed files with 1305 additions and 95 deletions
@ -0,0 +1,295 @@ |
|||||
|
# Database Migration Guide |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
The Database Migration feature allows you to compare and migrate data between Dexie (IndexedDB) and SQLite databases in the TimeSafari application. This is particularly useful during the transition from the old Dexie-based storage system to the new SQLite-based system. |
||||
|
|
||||
|
## Features |
||||
|
|
||||
|
### 1. Database Comparison |
||||
|
|
||||
|
- Compare data between Dexie and SQLite databases |
||||
|
- View detailed differences in contacts and settings |
||||
|
- Identify added, modified, and missing records |
||||
|
- Export comparison results for analysis |
||||
|
|
||||
|
### 2. Data Migration |
||||
|
|
||||
|
- Migrate contacts from Dexie to SQLite |
||||
|
- Migrate settings from Dexie to SQLite |
||||
|
- Option to overwrite existing records or skip them |
||||
|
- Comprehensive error handling and reporting |
||||
|
|
||||
|
### 3. User Interface |
||||
|
|
||||
|
- Modern, responsive UI built with Tailwind CSS |
||||
|
- Real-time loading states and progress indicators |
||||
|
- Clear success and error messaging |
||||
|
- Export functionality for comparison data |
||||
|
|
||||
|
## Prerequisites |
||||
|
|
||||
|
### Enable Dexie Database |
||||
|
|
||||
|
Before using the migration features, you must enable the Dexie database by setting: |
||||
|
|
||||
|
```typescript |
||||
|
// In constants/app.ts |
||||
|
export const USE_DEXIE_DB = true; |
||||
|
``` |
||||
|
|
||||
|
**Note**: This should only be enabled temporarily during migration. Remember to set it back to `false` after migration is complete. |
||||
|
|
||||
|
## Accessing the Migration Interface |
||||
|
|
||||
|
1. Navigate to the **Account** page in the TimeSafari app |
||||
|
2. Scroll down to find the **Database Migration** link |
||||
|
3. Click the link to open the migration interface |
||||
|
|
||||
|
## Using the Migration Interface |
||||
|
|
||||
|
### Step 1: Compare Databases |
||||
|
|
||||
|
1. Click the **"Compare Databases"** button |
||||
|
2. The system will retrieve data from both Dexie and SQLite databases |
||||
|
3. Review the comparison results showing: |
||||
|
- Summary counts for each database |
||||
|
- Detailed differences (added, modified, missing records) |
||||
|
- Specific records that need attention |
||||
|
|
||||
|
### Step 2: Review Differences |
||||
|
|
||||
|
The comparison results are displayed in several sections: |
||||
|
|
||||
|
#### Summary Cards |
||||
|
|
||||
|
- **Dexie Contacts**: Number of contacts in Dexie database |
||||
|
- **SQLite Contacts**: Number of contacts in SQLite database |
||||
|
- **Dexie Settings**: Number of settings in Dexie database |
||||
|
- **SQLite Settings**: Number of settings in SQLite database |
||||
|
|
||||
|
#### Contact Differences |
||||
|
|
||||
|
- **Added**: Contacts in Dexie but not in SQLite |
||||
|
- **Modified**: Contacts that differ between databases |
||||
|
- **Missing**: Contacts in SQLite but not in Dexie |
||||
|
|
||||
|
#### Settings Differences |
||||
|
|
||||
|
- **Added**: Settings in Dexie but not in SQLite |
||||
|
- **Modified**: Settings that differ between databases |
||||
|
- **Missing**: Settings in SQLite but not in Dexie |
||||
|
|
||||
|
### Step 3: Configure Migration Options |
||||
|
|
||||
|
Before migrating data, configure the migration options: |
||||
|
|
||||
|
- **Overwrite existing records**: When enabled, existing records in SQLite will be updated with data from Dexie. When disabled, existing records will be skipped. |
||||
|
|
||||
|
### Step 4: Migrate Data |
||||
|
|
||||
|
#### Migrate Contacts |
||||
|
|
||||
|
1. Click the **"Migrate Contacts"** button |
||||
|
2. The system will transfer contacts from Dexie to SQLite |
||||
|
3. Review the migration results showing: |
||||
|
- Number of contacts successfully migrated |
||||
|
- Any warnings or errors encountered |
||||
|
|
||||
|
#### Migrate Settings |
||||
|
|
||||
|
1. Click the **"Migrate Settings"** button |
||||
|
2. The system will transfer settings from Dexie to SQLite |
||||
|
3. Review the migration results showing: |
||||
|
- Number of settings successfully migrated |
||||
|
- Any warnings or errors encountered |
||||
|
|
||||
|
### Step 5: Export Comparison (Optional) |
||||
|
|
||||
|
1. Click the **"Export Comparison"** button |
||||
|
2. A JSON file will be downloaded containing the complete comparison data |
||||
|
3. This file can be used for analysis or backup purposes |
||||
|
|
||||
|
## Migration Process Details |
||||
|
|
||||
|
### Contact Migration |
||||
|
|
||||
|
The contact migration process: |
||||
|
|
||||
|
1. **Retrieves** all contacts from Dexie database |
||||
|
2. **Checks** for existing contacts in SQLite by DID |
||||
|
3. **Inserts** new contacts or **updates** existing ones (if overwrite is enabled) |
||||
|
4. **Handles** complex fields like `contactMethods` (JSON arrays) |
||||
|
5. **Reports** success/failure for each contact |
||||
|
|
||||
|
### Settings Migration |
||||
|
|
||||
|
The settings migration process: |
||||
|
|
||||
|
1. **Retrieves** all settings from Dexie database |
||||
|
2. **Focuses** on key user-facing settings: |
||||
|
- `firstName` |
||||
|
- `isRegistered` |
||||
|
- `profileImageUrl` |
||||
|
- `showShortcutBvc` |
||||
|
- `searchBoxes` |
||||
|
3. **Preserves** other settings in SQLite |
||||
|
4. **Reports** success/failure for each setting |
||||
|
|
||||
|
## Error Handling |
||||
|
|
||||
|
### Common Issues |
||||
|
|
||||
|
#### Dexie Database Not Enabled |
||||
|
|
||||
|
**Error**: "Dexie database is not enabled" |
||||
|
**Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts` |
||||
|
|
||||
|
#### Database Connection Issues |
||||
|
|
||||
|
**Error**: "Failed to retrieve Dexie contacts" |
||||
|
**Solution**: Check that the Dexie database is properly initialized and accessible |
||||
|
|
||||
|
#### SQLite Query Errors |
||||
|
|
||||
|
**Error**: "Failed to retrieve SQLite contacts" |
||||
|
**Solution**: Verify that the SQLite database is properly set up and the platform service is working |
||||
|
|
||||
|
#### Migration Failures |
||||
|
|
||||
|
**Error**: "Migration failed: [specific error]" |
||||
|
**Solution**: Review the error details and check data integrity in both databases |
||||
|
|
||||
|
### Error Recovery |
||||
|
|
||||
|
1. **Review** the error messages carefully |
||||
|
2. **Check** the browser console for additional details |
||||
|
3. **Verify** database connectivity and permissions |
||||
|
4. **Retry** the operation if appropriate |
||||
|
5. **Export** comparison data for manual review if needed |
||||
|
|
||||
|
## Best Practices |
||||
|
|
||||
|
### Before Migration |
||||
|
|
||||
|
1. **Backup** your data if possible |
||||
|
2. **Test** the migration on a small dataset first |
||||
|
3. **Verify** that both databases are accessible |
||||
|
4. **Review** the comparison results before migrating |
||||
|
|
||||
|
### During Migration |
||||
|
|
||||
|
1. **Don't** interrupt the migration process |
||||
|
2. **Monitor** the progress and error messages |
||||
|
3. **Note** any warnings or skipped records |
||||
|
4. **Export** comparison data for reference |
||||
|
|
||||
|
### After Migration |
||||
|
|
||||
|
1. **Verify** that data was migrated correctly |
||||
|
2. **Test** the application functionality |
||||
|
3. **Disable** Dexie database (`USE_DEXIE_DB = false`) |
||||
|
4. **Clean up** any temporary files or exports |
||||
|
|
||||
|
## Technical Details |
||||
|
|
||||
|
### Database Schema |
||||
|
|
||||
|
The migration handles the following data structures: |
||||
|
|
||||
|
#### Contacts Table |
||||
|
|
||||
|
```typescript |
||||
|
interface Contact { |
||||
|
did: string; // Decentralized Identifier |
||||
|
name: string; // Contact name |
||||
|
contactMethods: ContactMethod[]; // Array of contact methods |
||||
|
nextPubKeyHashB64: string; // Next public key hash |
||||
|
notes: string; // Contact notes |
||||
|
profileImageUrl: string; // Profile image URL |
||||
|
publicKeyBase64: string; // Public key in base64 |
||||
|
seesMe: boolean; // Visibility flag |
||||
|
registered: boolean; // Registration status |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Settings Table |
||||
|
|
||||
|
```typescript |
||||
|
interface Settings { |
||||
|
id: number; // Settings ID |
||||
|
accountDid: string; // Account DID |
||||
|
activeDid: string; // Active DID |
||||
|
firstName: string; // User's first name |
||||
|
isRegistered: boolean; // Registration status |
||||
|
profileImageUrl: string; // Profile image URL |
||||
|
showShortcutBvc: boolean; // UI preference |
||||
|
searchBoxes: any[]; // Search configuration |
||||
|
// ... other fields |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Migration Logic |
||||
|
|
||||
|
The migration service uses sophisticated comparison logic: |
||||
|
|
||||
|
1. **Primary Key Matching**: Uses DID for contacts, ID for settings |
||||
|
2. **Deep Comparison**: Compares all fields including complex objects |
||||
|
3. **JSON Handling**: Properly handles JSON fields like `contactMethods` and `searchBoxes` |
||||
|
4. **Conflict Resolution**: Provides options for handling existing records |
||||
|
|
||||
|
### Performance Considerations |
||||
|
|
||||
|
- **Batch Processing**: Processes records one by one for reliability |
||||
|
- **Error Isolation**: Individual record failures don't stop the entire migration |
||||
|
- **Memory Management**: Handles large datasets efficiently |
||||
|
- **Progress Reporting**: Provides real-time feedback during migration |
||||
|
|
||||
|
## Troubleshooting |
||||
|
|
||||
|
### Migration Stuck |
||||
|
|
||||
|
If the migration appears to be stuck: |
||||
|
|
||||
|
1. **Check** the browser console for errors |
||||
|
2. **Refresh** the page and try again |
||||
|
3. **Verify** database connectivity |
||||
|
4. **Check** for large datasets that might take time |
||||
|
|
||||
|
### Incomplete Migration |
||||
|
|
||||
|
If migration doesn't complete: |
||||
|
|
||||
|
1. **Review** error messages |
||||
|
2. **Check** data integrity in both databases |
||||
|
3. **Export** comparison data for manual review |
||||
|
4. **Consider** migrating in smaller batches |
||||
|
|
||||
|
### Data Inconsistencies |
||||
|
|
||||
|
If you notice data inconsistencies: |
||||
|
|
||||
|
1. **Export** comparison data |
||||
|
2. **Review** the differences carefully |
||||
|
3. **Manually** verify critical records |
||||
|
4. **Consider** selective migration of specific records |
||||
|
|
||||
|
## Support |
||||
|
|
||||
|
For issues with the Database Migration feature: |
||||
|
|
||||
|
1. **Check** this documentation first |
||||
|
2. **Review** the browser console for error details |
||||
|
3. **Export** comparison data for analysis |
||||
|
4. **Contact** the development team with specific error details |
||||
|
|
||||
|
## Security Considerations |
||||
|
|
||||
|
- **Data Privacy**: Migration data is processed locally and not sent to external servers |
||||
|
- **Access Control**: Only users with access to the account can perform migration |
||||
|
- **Data Integrity**: Migration preserves data integrity and handles conflicts gracefully |
||||
|
- **Audit Trail**: Export functionality provides an audit trail of migration operations |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Note**: This migration tool is designed for the transition period between database systems. Once migration is complete and verified, the Dexie database should be disabled to avoid confusion and potential data conflicts. |
@ -0,0 +1,860 @@ |
|||||
|
<template> |
||||
|
<div class="min-h-screen bg-gray-50 py-8"> |
||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
||||
|
<!-- Header --> |
||||
|
<div class="mb-8"> |
||||
|
<h1 class="text-3xl font-bold text-gray-900">Database Migration</h1> |
||||
|
<p class="mt-2 text-gray-600"> |
||||
|
Compare and migrate data between Dexie (IndexedDB) and SQLite |
||||
|
databases |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Status Banner --> |
||||
|
<div |
||||
|
v-if="!isDexieEnabled" |
||||
|
class="mb-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4" |
||||
|
> |
||||
|
<div class="flex"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-yellow-400" |
||||
|
viewBox="0 0 20 20" |
||||
|
fill="currentColor" |
||||
|
> |
||||
|
<path |
||||
|
fill-rule="evenodd" |
||||
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" |
||||
|
clip-rule="evenodd" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-3"> |
||||
|
<h3 class="text-sm font-medium text-yellow-800"> |
||||
|
Dexie Database Disabled |
||||
|
</h3> |
||||
|
<div class="mt-2 text-sm text-yellow-700"> |
||||
|
<p> |
||||
|
To use migration features, enable Dexie database by setting |
||||
|
<code class="bg-yellow-100 px-1 rounded" |
||||
|
>USE_DEXIE_DB = true</code |
||||
|
> |
||||
|
in |
||||
|
<code class="bg-yellow-100 px-1 rounded">constants/app.ts</code> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Action Buttons --> |
||||
|
<div class="mb-8 flex flex-wrap gap-4"> |
||||
|
<button |
||||
|
:disabled="isLoading || !isDexieEnabled" |
||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" |
||||
|
@click="compareDatabases" |
||||
|
> |
||||
|
<svg |
||||
|
v-if="isLoading" |
||||
|
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
fill="none" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<circle |
||||
|
class="opacity-25" |
||||
|
cx="12" |
||||
|
cy="12" |
||||
|
r="10" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="4" |
||||
|
></circle> |
||||
|
<path |
||||
|
class="opacity-75" |
||||
|
fill="currentColor" |
||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
||||
|
></path> |
||||
|
</svg> |
||||
|
<svg |
||||
|
v-else |
||||
|
class="-ml-1 mr-3 h-5 w-5" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" |
||||
|
/> |
||||
|
</svg> |
||||
|
Compare Databases |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
:disabled="isLoading || !isDexieEnabled || !comparison" |
||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed" |
||||
|
@click="migrateContacts" |
||||
|
> |
||||
|
<svg |
||||
|
class="-ml-1 mr-3 h-5 w-5" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M12 4v16m8-8H4" |
||||
|
/> |
||||
|
</svg> |
||||
|
Migrate Contacts |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
:disabled="isLoading || !isDexieEnabled || !comparison" |
||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed" |
||||
|
@click="migrateSettings" |
||||
|
> |
||||
|
<svg |
||||
|
class="-ml-1 mr-3 h-5 w-5" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" |
||||
|
/> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" |
||||
|
/> |
||||
|
</svg> |
||||
|
Migrate Settings |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
:disabled="!comparison" |
||||
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" |
||||
|
@click="exportComparison" |
||||
|
> |
||||
|
<svg |
||||
|
class="-ml-1 mr-3 h-5 w-5" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" |
||||
|
/> |
||||
|
</svg> |
||||
|
Export Comparison |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Loading State --> |
||||
|
<div v-if="isLoading" class="text-center py-12"> |
||||
|
<div |
||||
|
class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-blue-500 hover:bg-blue-400 transition ease-in-out duration-150 cursor-not-allowed" |
||||
|
> |
||||
|
<svg |
||||
|
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
fill="none" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<circle |
||||
|
class="opacity-25" |
||||
|
cx="12" |
||||
|
cy="12" |
||||
|
r="10" |
||||
|
stroke="currentColor" |
||||
|
stroke-width="4" |
||||
|
></circle> |
||||
|
<path |
||||
|
class="opacity-75" |
||||
|
fill="currentColor" |
||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
||||
|
></path> |
||||
|
</svg> |
||||
|
{{ loadingMessage }} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Error State --> |
||||
|
<div |
||||
|
v-if="error" |
||||
|
class="mb-6 bg-red-50 border border-red-200 rounded-lg p-4" |
||||
|
> |
||||
|
<div class="flex"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-red-400" |
||||
|
viewBox="0 0 20 20" |
||||
|
fill="currentColor" |
||||
|
> |
||||
|
<path |
||||
|
fill-rule="evenodd" |
||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" |
||||
|
clip-rule="evenodd" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-3"> |
||||
|
<h3 class="text-sm font-medium text-red-800">Error</h3> |
||||
|
<div class="mt-2 text-sm text-red-700"> |
||||
|
<p>{{ error }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Success State --> |
||||
|
<div |
||||
|
v-if="successMessage" |
||||
|
class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4" |
||||
|
> |
||||
|
<div class="flex"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-green-400" |
||||
|
viewBox="0 0 20 20" |
||||
|
fill="currentColor" |
||||
|
> |
||||
|
<path |
||||
|
fill-rule="evenodd" |
||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" |
||||
|
clip-rule="evenodd" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-3"> |
||||
|
<h3 class="text-sm font-medium text-green-800">Success</h3> |
||||
|
<div class="mt-2 text-sm text-green-700"> |
||||
|
<p>{{ successMessage }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Comparison Results --> |
||||
|
<div v-if="comparison" class="space-y-6"> |
||||
|
<!-- Summary Cards --> |
||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> |
||||
|
<div class="bg-white overflow-hidden shadow rounded-lg"> |
||||
|
<div class="p-5"> |
||||
|
<div class="flex items-center"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-6 w-6 text-blue-600" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-5 w-0 flex-1"> |
||||
|
<dl> |
||||
|
<dt class="text-sm font-medium text-gray-500 truncate"> |
||||
|
Dexie Contacts |
||||
|
</dt> |
||||
|
<dd class="text-lg font-medium text-gray-900"> |
||||
|
{{ comparison.dexieContacts.length }} |
||||
|
</dd> |
||||
|
</dl> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="bg-white overflow-hidden shadow rounded-lg"> |
||||
|
<div class="p-5"> |
||||
|
<div class="flex items-center"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-6 w-6 text-green-600" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-5 w-0 flex-1"> |
||||
|
<dl> |
||||
|
<dt class="text-sm font-medium text-gray-500 truncate"> |
||||
|
SQLite Contacts |
||||
|
</dt> |
||||
|
<dd class="text-lg font-medium text-gray-900"> |
||||
|
{{ comparison.sqliteContacts.length }} |
||||
|
</dd> |
||||
|
</dl> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="bg-white overflow-hidden shadow rounded-lg"> |
||||
|
<div class="p-5"> |
||||
|
<div class="flex items-center"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-6 w-6 text-purple-600" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" |
||||
|
/> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-5 w-0 flex-1"> |
||||
|
<dl> |
||||
|
<dt class="text-sm font-medium text-gray-500 truncate"> |
||||
|
Dexie Settings |
||||
|
</dt> |
||||
|
<dd class="text-lg font-medium text-gray-900"> |
||||
|
{{ comparison.dexieSettings.length }} |
||||
|
</dd> |
||||
|
</dl> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="bg-white overflow-hidden shadow rounded-lg"> |
||||
|
<div class="p-5"> |
||||
|
<div class="flex items-center"> |
||||
|
<div class="flex-shrink-0"> |
||||
|
<svg |
||||
|
class="h-6 w-6 text-indigo-600" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class="ml-5 w-0 flex-1"> |
||||
|
<dl> |
||||
|
<dt class="text-sm font-medium text-gray-500 truncate"> |
||||
|
SQLite Settings |
||||
|
</dt> |
||||
|
<dd class="text-lg font-medium text-gray-900"> |
||||
|
{{ comparison.sqliteSettings.length }} |
||||
|
</dd> |
||||
|
</dl> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Differences Section --> |
||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
||||
|
<!-- Contacts Differences --> |
||||
|
<div class="bg-white shadow rounded-lg"> |
||||
|
<div class="px-4 py-5 sm:p-6"> |
||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> |
||||
|
Contact Differences |
||||
|
</h3> |
||||
|
|
||||
|
<div class="space-y-4"> |
||||
|
<div |
||||
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" |
||||
|
> |
||||
|
<div class="flex items-center"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-blue-600 mr-2" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6" |
||||
|
/> |
||||
|
</svg> |
||||
|
<span class="text-sm font-medium text-blue-900">Added</span> |
||||
|
</div> |
||||
|
<span class="text-sm font-bold text-blue-900">{{ |
||||
|
comparison.differences.contacts.added.length |
||||
|
}}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" |
||||
|
> |
||||
|
<div class="flex items-center"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-yellow-600 mr-2" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" |
||||
|
/> |
||||
|
</svg> |
||||
|
<span class="text-sm font-medium text-yellow-900" |
||||
|
>Modified</span |
||||
|
> |
||||
|
</div> |
||||
|
<span class="text-sm font-bold text-yellow-900">{{ |
||||
|
comparison.differences.contacts.modified.length |
||||
|
}}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg" |
||||
|
> |
||||
|
<div class="flex items-center"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-red-600 mr-2" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" |
||||
|
/> |
||||
|
</svg> |
||||
|
<span class="text-sm font-medium text-red-900" |
||||
|
>Missing</span |
||||
|
> |
||||
|
</div> |
||||
|
<span class="text-sm font-bold text-red-900">{{ |
||||
|
comparison.differences.contacts.missing.length |
||||
|
}}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Contact Details --> |
||||
|
<div |
||||
|
v-if="comparison.differences.contacts.added.length > 0" |
||||
|
class="mt-4" |
||||
|
> |
||||
|
<h4 class="text-sm font-medium text-gray-900 mb-2"> |
||||
|
Added Contacts: |
||||
|
</h4> |
||||
|
<div class="max-h-32 overflow-y-auto space-y-1"> |
||||
|
<div |
||||
|
v-for="contact in comparison.differences.contacts.added" |
||||
|
:key="contact.did" |
||||
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded" |
||||
|
> |
||||
|
{{ contact.name || "Unnamed" }} ({{ |
||||
|
contact.did.substring(0, 20) |
||||
|
}}...) |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Settings Differences --> |
||||
|
<div class="bg-white shadow rounded-lg"> |
||||
|
<div class="px-4 py-5 sm:p-6"> |
||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> |
||||
|
Settings Differences |
||||
|
</h3> |
||||
|
|
||||
|
<div class="space-y-4"> |
||||
|
<div |
||||
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" |
||||
|
> |
||||
|
<div class="flex items-center"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-blue-600 mr-2" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6" |
||||
|
/> |
||||
|
</svg> |
||||
|
<span class="text-sm font-medium text-blue-900">Added</span> |
||||
|
</div> |
||||
|
<span class="text-sm font-bold text-blue-900">{{ |
||||
|
comparison.differences.settings.added.length |
||||
|
}}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" |
||||
|
> |
||||
|
<div class="flex items-center"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-yellow-600 mr-2" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" |
||||
|
/> |
||||
|
</svg> |
||||
|
<span class="text-sm font-medium text-yellow-900" |
||||
|
>Modified</span |
||||
|
> |
||||
|
</div> |
||||
|
<span class="text-sm font-bold text-yellow-900">{{ |
||||
|
comparison.differences.settings.modified.length |
||||
|
}}</span> |
||||
|
</div> |
||||
|
|
||||
|
<div |
||||
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg" |
||||
|
> |
||||
|
<div class="flex items-center"> |
||||
|
<svg |
||||
|
class="h-5 w-5 text-red-600 mr-2" |
||||
|
fill="none" |
||||
|
stroke="currentColor" |
||||
|
viewBox="0 0 24 24" |
||||
|
> |
||||
|
<path |
||||
|
stroke-linecap="round" |
||||
|
stroke-linejoin="round" |
||||
|
stroke-width="2" |
||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" |
||||
|
/> |
||||
|
</svg> |
||||
|
<span class="text-sm font-medium text-red-900" |
||||
|
>Missing</span |
||||
|
> |
||||
|
</div> |
||||
|
<span class="text-sm font-bold text-red-900">{{ |
||||
|
comparison.differences.settings.missing.length |
||||
|
}}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Settings Details --> |
||||
|
<div |
||||
|
v-if="comparison.differences.settings.added.length > 0" |
||||
|
class="mt-4" |
||||
|
> |
||||
|
<h4 class="text-sm font-medium text-gray-900 mb-2"> |
||||
|
Added Settings: |
||||
|
</h4> |
||||
|
<div class="max-h-32 overflow-y-auto space-y-1"> |
||||
|
<div |
||||
|
v-for="setting in comparison.differences.settings.added" |
||||
|
:key="setting.id" |
||||
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded" |
||||
|
> |
||||
|
ID: {{ setting.id }} - {{ setting.firstName || "Unnamed" }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Migration Options --> |
||||
|
<div class="bg-white shadow rounded-lg"> |
||||
|
<div class="px-4 py-5 sm:p-6"> |
||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4"> |
||||
|
Migration Options |
||||
|
</h3> |
||||
|
|
||||
|
<div class="space-y-4"> |
||||
|
<div class="flex items-center"> |
||||
|
<input |
||||
|
id="overwrite-existing" |
||||
|
v-model="overwriteExisting" |
||||
|
type="checkbox" |
||||
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" |
||||
|
/> |
||||
|
<label |
||||
|
for="overwrite-existing" |
||||
|
class="ml-2 block text-sm text-gray-900" |
||||
|
> |
||||
|
Overwrite existing records in SQLite |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<p class="text-sm text-gray-600"> |
||||
|
When enabled, existing records in SQLite will be updated with |
||||
|
data from Dexie. When disabled, existing records will be skipped |
||||
|
during migration. |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
import { |
||||
|
compareDatabases, |
||||
|
migrateContacts, |
||||
|
migrateSettings, |
||||
|
generateComparisonYaml, |
||||
|
type DataComparison, |
||||
|
type MigrationResult, |
||||
|
} from "../services/migrationService"; |
||||
|
import { USE_DEXIE_DB } from "../constants/app"; |
||||
|
import { logger } from "../utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* Database Migration View Component |
||||
|
* |
||||
|
* This component provides a user interface for comparing and migrating data |
||||
|
* between Dexie (IndexedDB) and SQLite databases. It allows users to: |
||||
|
* |
||||
|
* 1. Compare data between the two databases |
||||
|
* 2. View differences in contacts and settings |
||||
|
* 3. Migrate contacts from Dexie to SQLite |
||||
|
* 4. Migrate settings from Dexie to SQLite |
||||
|
* 5. Export comparison results |
||||
|
* |
||||
|
* The component includes comprehensive error handling, loading states, |
||||
|
* and user-friendly feedback for all operations. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
* @since 2024 |
||||
|
*/ |
||||
|
@Component({ |
||||
|
name: "DatabaseMigration", |
||||
|
}) |
||||
|
export default class DatabaseMigration extends Vue { |
||||
|
// Component state |
||||
|
private isLoading = false; |
||||
|
private loadingMessage = ""; |
||||
|
private error = ""; |
||||
|
private successMessage = ""; |
||||
|
private comparison: DataComparison | null = null; |
||||
|
private overwriteExisting = false; |
||||
|
|
||||
|
/** |
||||
|
* Computed property to check if Dexie database is enabled |
||||
|
* |
||||
|
* @returns {boolean} True if Dexie database is enabled, false otherwise |
||||
|
*/ |
||||
|
get isDexieEnabled(): boolean { |
||||
|
return USE_DEXIE_DB; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Compares data between Dexie and SQLite databases |
||||
|
* |
||||
|
* This method retrieves data from both databases and identifies |
||||
|
* differences. It provides comprehensive feedback and error handling. |
||||
|
* |
||||
|
* @async |
||||
|
* @returns {Promise<void>} |
||||
|
*/ |
||||
|
async compareDatabases(): Promise<void> { |
||||
|
this.setLoading("Comparing databases..."); |
||||
|
this.clearMessages(); |
||||
|
|
||||
|
try { |
||||
|
this.comparison = await compareDatabases(); |
||||
|
this.successMessage = `Comparison completed successfully. Found ${this.comparison.differences.contacts.added.length + this.comparison.differences.settings.added.length} items to migrate.`; |
||||
|
logger.info( |
||||
|
"[DatabaseMigration] Database comparison completed successfully", |
||||
|
); |
||||
|
} catch (error) { |
||||
|
this.error = `Failed to compare databases: ${error}`; |
||||
|
logger.error("[DatabaseMigration] Database comparison failed:", error); |
||||
|
} finally { |
||||
|
this.setLoading(""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Migrates contacts from Dexie to SQLite database |
||||
|
* |
||||
|
* This method transfers contacts from the Dexie database to SQLite, |
||||
|
* with options to overwrite existing records. |
||||
|
* |
||||
|
* @async |
||||
|
* @returns {Promise<void>} |
||||
|
*/ |
||||
|
async migrateContacts(): Promise<void> { |
||||
|
this.setLoading("Migrating contacts..."); |
||||
|
this.clearMessages(); |
||||
|
|
||||
|
try { |
||||
|
const result: MigrationResult = await migrateContacts( |
||||
|
this.overwriteExisting, |
||||
|
); |
||||
|
|
||||
|
if (result.success) { |
||||
|
this.successMessage = `Successfully migrated ${result.contactsMigrated} contacts.`; |
||||
|
if (result.warnings.length > 0) { |
||||
|
this.successMessage += ` ${result.warnings.length} warnings.`; |
||||
|
} |
||||
|
logger.info( |
||||
|
"[DatabaseMigration] Contact migration completed successfully", |
||||
|
result, |
||||
|
); |
||||
|
} else { |
||||
|
this.error = `Migration failed: ${result.errors.join(", ")}`; |
||||
|
logger.error( |
||||
|
"[DatabaseMigration] Contact migration failed:", |
||||
|
result.errors, |
||||
|
); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
this.error = `Failed to migrate contacts: ${error}`; |
||||
|
logger.error("[DatabaseMigration] Contact migration failed:", error); |
||||
|
} finally { |
||||
|
this.setLoading(""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Migrates settings from Dexie to SQLite database |
||||
|
* |
||||
|
* This method transfers settings from the Dexie database to SQLite, |
||||
|
* with options to overwrite existing records. |
||||
|
* |
||||
|
* @async |
||||
|
* @returns {Promise<void>} |
||||
|
*/ |
||||
|
async migrateSettings(): Promise<void> { |
||||
|
this.setLoading("Migrating settings..."); |
||||
|
this.clearMessages(); |
||||
|
|
||||
|
try { |
||||
|
const result: MigrationResult = await migrateSettings( |
||||
|
this.overwriteExisting, |
||||
|
); |
||||
|
|
||||
|
if (result.success) { |
||||
|
this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`; |
||||
|
if (result.warnings.length > 0) { |
||||
|
this.successMessage += ` ${result.warnings.length} warnings.`; |
||||
|
} |
||||
|
logger.info( |
||||
|
"[DatabaseMigration] Settings migration completed successfully", |
||||
|
result, |
||||
|
); |
||||
|
} else { |
||||
|
this.error = `Migration failed: ${result.errors.join(", ")}`; |
||||
|
logger.error( |
||||
|
"[DatabaseMigration] Settings migration failed:", |
||||
|
result.errors, |
||||
|
); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
this.error = `Failed to migrate settings: ${error}`; |
||||
|
logger.error("[DatabaseMigration] Settings migration failed:", error); |
||||
|
} finally { |
||||
|
this.setLoading(""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Exports comparison results to a file |
||||
|
* |
||||
|
* This method generates a YAML-formatted comparison and triggers |
||||
|
* a file download for the user. |
||||
|
* |
||||
|
* @async |
||||
|
* @returns {Promise<void>} |
||||
|
*/ |
||||
|
async exportComparison(): Promise<void> { |
||||
|
if (!this.comparison) { |
||||
|
this.error = "No comparison data available to export"; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const yamlData = generateComparisonYaml(this.comparison); |
||||
|
const blob = new Blob([yamlData], { type: "application/json" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
|
||||
|
const link = document.createElement("a"); |
||||
|
link.href = url; |
||||
|
link.download = `database-comparison-${new Date().toISOString().split("T")[0]}.json`; |
||||
|
document.body.appendChild(link); |
||||
|
link.click(); |
||||
|
document.body.removeChild(link); |
||||
|
URL.revokeObjectURL(url); |
||||
|
|
||||
|
this.successMessage = "Comparison exported successfully"; |
||||
|
logger.info("[DatabaseMigration] Comparison exported successfully"); |
||||
|
} catch (error) { |
||||
|
this.error = `Failed to export comparison: ${error}`; |
||||
|
logger.error("[DatabaseMigration] Export failed:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Sets the loading state and message |
||||
|
* |
||||
|
* @param {string} message - The loading message to display |
||||
|
*/ |
||||
|
private setLoading(message: string): void { |
||||
|
this.isLoading = message !== ""; |
||||
|
this.loadingMessage = message; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clears all error and success messages |
||||
|
*/ |
||||
|
private clearMessages(): void { |
||||
|
this.error = ""; |
||||
|
this.successMessage = ""; |
||||
|
} |
||||
|
} |
||||
|
</script> |
Loading…
Reference in new issue