Skip to content

Commit ac4f07e

Browse files
authored
Merge pull request #9 from LuiseFreese/dev
Merge Enhanced rollback and deployment progress tracking
2 parents eee77a0 + b0643aa commit ac4f07e

24 files changed

+1802
-297
lines changed

docs/DEVELOPER_ARCHITECTURE.md

Lines changed: 209 additions & 129 deletions
Large diffs are not rendered by default.

docs/USAGE-GUIDE.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ The application provides a **modern React wizard interface** with Fluent UI v9 c
8585
- **Real-time Validation**: Final validation before deployment
8686
- **Deploy**: Select **Deploy to Dataverse** to create your solution with live progress tracking
8787

88-
![Deployment progress](media/step-4-deploying.png)
88+
![Deployment progress](media/step-4-progress-indicator.png)
8989

9090
And finally
9191

@@ -322,6 +322,30 @@ The application provides **modular rollback** functionality, giving you granular
322322

323323
![complete rollback](media/rollback-complete.png)
324324

325+
### Enhanced Progress Tracking
326+
327+
The rollback process includes **real-time progress indicators** with step-by-step visibility:
328+
329+
**Visual Progress Features:**
330+
- **Step-by-Step Display**: Each rollback component shown as individual progress steps
331+
- **Active Step Highlighting**: Currently processing step highlighted with pink pulsing animation
332+
- **Status Badges**: Clear "Preparing", "In Progress", "Completed" indicators for each step
333+
- **Time Estimation**: Real-time elapsed time and estimated remaining time
334+
- **Spinner Animations**: Visual feedback for active operations
335+
336+
**Progress Steps Include:**
337+
1. **Preparation** - Validating rollback requirements
338+
2. **Relationships** - Removing entity relationships
339+
3. **Custom Entities** - Deleting custom tables
340+
4. **Global Choices** - Removing custom choice sets
341+
5. **Solution** - Deleting solution container
342+
6. **Publisher** - Removing publisher
343+
7. **Cleanup** - Final cleanup operations
344+
345+
This enhanced progress tracking matches the same visual style and functionality as deployment progress, providing consistent user experience throughout the application.
346+
347+
![rollback progress indicator](/rollback-progress-indicator.png)
348+
325349
### Rollback Options
326350

327351
You can choose exactly what to rollback:
178 KB
Loading
65.3 KB
Loading

rollback-test.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"confirm": true,
3+
"options": {
4+
"relationships": true,
5+
"customEntities": false,
6+
"customGlobalChoices": false,
7+
"solution": false,
8+
"publisher": false
9+
}
10+
}

src/backend/controllers/rollback-controller.js

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,27 +119,23 @@ class RollbackController {
119119
// Update status to in-progress
120120
this.statusTracker.updateStatus(rollbackId, 'in-progress');
121121

122-
// Track progress phases
123-
const phases = ['relationships', 'entities', 'globalChoices', 'solution', 'publisher'];
124-
let currentPhaseIndex = 0;
125-
126122
// Progress callback to update tracker
127-
// Receives (status, message) where status is the phase name
128-
const progressCallback = (status, message) => {
129-
console.log(`Rollback ${rollbackId}: ${status} - ${message}`);
123+
// Updated to handle new format: (type, message, progressData)
124+
const progressCallback = (type, message, progressData) => {
125+
console.log(`Rollback ${rollbackId}: ${type} - ${message}`);
130126

131-
// Update current phase
132-
const phaseIndex = phases.indexOf(status);
133-
if (phaseIndex >= 0) {
134-
currentPhaseIndex = phaseIndex + 1;
127+
if (type === 'progress' && progressData) {
128+
// Extract progress information from progressData
129+
const percentage = progressData.percentage || 0;
130+
const total = progressData.steps ? progressData.steps.length : 100;
131+
const current = Math.round((percentage / 100) * total);
132+
133+
// Update progress with enhanced data for the frontend
134+
this.statusTracker.updateProgressWithData(rollbackId, current, total, message, progressData);
135+
} else {
136+
// Fallback for any unexpected format
137+
console.log(`Unexpected progress format: ${type}, ${message}`);
135138
}
136-
137-
// Calculate progress (each phase is a step)
138-
const total = phases.length;
139-
const current = currentPhaseIndex;
140-
141-
// Just pass the message without phase numbers
142-
this.statusTracker.updateProgress(rollbackId, current, total, message);
143139
};
144140

145141
// Execute the rollback with progress tracking

src/backend/dataverse-client.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,14 @@ class DataverseClient {
338338
} catch (error) {
339339
const isRetryable = retryableStatusCodes.includes(error.status);
340340
const isLastAttempt = attempt === maxRetries;
341+
342+
// Check for EntityCustomization conflict error
343+
const isEntityCustomizationConflict = error.message &&
344+
(error.message.includes('0x80071151') ||
345+
error.message.includes('EntityCustomization') ||
346+
error.message.includes('solution installation or removal failed'));
341347

342-
if (!isRetryable || isLastAttempt) {
348+
if ((!isRetryable && !isEntityCustomizationConflict) || isLastAttempt) {
343349
// Not retryable or out of retries, throw the error
344350
throw error;
345351
}
@@ -357,7 +363,12 @@ class DataverseClient {
357363

358364
// Handle rate limiting with Retry-After header
359365
let delayMs = retryDelays[attempt];
360-
if (error.status === 429) {
366+
367+
// Special handling for EntityCustomization conflicts - use longer delays
368+
if (isEntityCustomizationConflict) {
369+
delayMs = Math.max(delayMs, 10000 + (attempt * 5000)); // Minimum 10s, increasing by 5s each attempt
370+
this._log(`⚠️ EntityCustomization conflict detected. Retrying in ${delayMs / 1000} seconds (attempt ${attempt + 1}/${maxRetries + 1})...`);
371+
} else if (error.status === 429) {
361372
// Check for Retry-After header (in seconds)
362373
const retryAfter = error.response?.headers?.['retry-after'];
363374
if (retryAfter) {

src/backend/services/deployment-service.js

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
const { BaseService } = require('./base-service');
66
const { performanceMonitor } = require('../performance-monitor');
7+
const { ProgressTracker } = require('../utils/progress-tracker');
78

89
class DeploymentService extends BaseService {
910
constructor(dependencies = {}) {
@@ -38,6 +39,9 @@ class DeploymentService extends BaseService {
3839
async deploySolution(config, progressCallback) {
3940
const deploymentId = this.generateDeploymentId();
4041

42+
// Initialize enhanced progress tracker
43+
const progressTracker = new ProgressTracker('deployment', progressCallback);
44+
4145
// Start performance monitoring
4246
this.performanceMonitor.startOperation(deploymentId, 'deployment', {
4347
entityCount: 0, // Will be updated when we parse the ERD
@@ -59,6 +63,7 @@ class DeploymentService extends BaseService {
5963
config: { ...config, mermaidContent: '[REDACTED]' } // Don't store full content
6064
});
6165

66+
// Legacy progress function for backward compatibility
6267
const progress = (step, message, details = {}) => {
6368
this.updateDeploymentStatus(deploymentId, step, message);
6469
if (progressCallback) {
@@ -67,26 +72,28 @@ class DeploymentService extends BaseService {
6772
};
6873

6974
try {
70-
progress('initialization', 'Initializing deployment...');
71-
72-
// Step 1: Parse ERD content
73-
progress('parsing', 'Parsing ERD content...');
75+
// Step 1: Parse and validate ERD content
76+
progressTracker.startStep('validation', 'Parsing and validating ERD content...');
77+
7478
const parseResult = await this.parseERDContent(config.mermaidContent);
7579

76-
// Step 2: Validate ERD content
80+
// Validate ERD content
7781
if (this.validationService) {
78-
progress('validation', 'Validating ERD');
82+
progressTracker.updateStep('validation', 'Running ERD validation checks...');
7983
const validationResult = await this.validationService.validateERD({
8084
mermaidContent: config.mermaidContent,
8185
options: {}
8286
});
8387
if (!validationResult.success) {
88+
progressTracker.failStep('validation', 'ERD validation failed', validationResult.message);
8489
throw new Error(validationResult.message || 'Invalid ERD syntax');
8590
}
8691
}
8792

88-
// Step 3: Setup Dataverse client
89-
progress('configuration', 'Connecting to Dataverse...');
93+
progressTracker.completeStep('validation', 'ERD content validated successfully');
94+
95+
// Step 2: Setup Dataverse connection
96+
progressTracker.startStep('publisher', 'Connecting to Dataverse...');
9097
const dataverseConfigResult = await this.configRepository.getDataverseConfig();
9198
const dataverseConfig = dataverseConfigResult?.data || dataverseConfigResult;
9299

@@ -98,8 +105,8 @@ class DeploymentService extends BaseService {
98105
keys: Object.keys(dataverseConfig || {})
99106
});
100107

101-
// Step 4: Ensure solution and publisher
102-
progress('publisher', config.useExistingSolution ? 'Using existing solution publisher' : 'Creating publisher');
108+
// Step 3: Ensure publisher
109+
progressTracker.updateStep('publisher', config.useExistingSolution ? 'Using existing solution publisher' : 'Creating publisher...');
103110
const publisherResult = await this.ensurePublisher(config, dataverseConfig);
104111

105112
console.log('🔧 DEBUG: publisherResult:', {
@@ -110,13 +117,18 @@ class DeploymentService extends BaseService {
110117
success: publisherResult?.success
111118
});
112119

113-
progress('solution', config.useExistingSolution ? 'Using existing solution' : 'Creating solution');
120+
progressTracker.completeStep('publisher', 'Publisher setup completed');
121+
122+
// Step 4: Ensure solution
123+
progressTracker.startStep('solution', config.useExistingSolution ? 'Using existing solution' : 'Creating solution...');
114124
// Extract the actual publisher data from the wrapped response
115125
const publisher = publisherResult?.data || publisherResult;
116126
const solutionResult = await this.ensureSolution(config, publisher, dataverseConfig);
117127

118128
// Extract the actual solution data from the wrapped response
119129
const solution = solutionResult?.data || solutionResult;
130+
131+
progressTracker.completeStep('solution', 'Solution setup completed');
120132

121133
console.log('🔧 DEBUG: Final solution being used:', {
122134
uniquename: solution?.uniquename,
@@ -174,11 +186,9 @@ class DeploymentService extends BaseService {
174186
message: 'Deployment completed successfully'
175187
};
176188

177-
// Step 6: Process CDM entities if any
189+
// Step 5: Process CDM entities if any
178190
if (cdmEntities.length > 0) {
179-
// If CDM entities are detected, process them as CDM entities
180-
// regardless of user choice (they can't be created as custom entities anyway)
181-
progress('cdm', `Adding ${cdmEntities.length} CDM Tables...`);
191+
progressTracker.startStep('entities', `Processing ${cdmEntities.length} CDM Tables...`);
182192
console.log('🔧 DEBUG: Processing CDM entities:', {
183193
count: cdmEntities.length,
184194
entities: cdmEntities.map(e => e?.originalEntity?.name || e?.name),
@@ -201,12 +211,21 @@ class DeploymentService extends BaseService {
201211
if (cdmData.summary?.relationshipsCreated) {
202212
results.relationshipsCreated += cdmData.summary.relationshipsCreated;
203213
}
214+
215+
progressTracker.updateStep('entities', `Successfully processed ${cdmEntities.length} CDM entities`);
216+
} else {
217+
progressTracker.updateStep('entities', `CDM entities processing completed with warnings`);
204218
}
205219
}
206220

207-
// Step 7: Process custom entities if any
221+
// Step 6: Process custom entities if any
208222
if (customEntities.length > 0) {
209-
progress('custom-entities', `Creating ${customEntities.length} Custom Tables...`);
223+
if (cdmEntities.length === 0) {
224+
progressTracker.startStep('entities', `Creating ${customEntities.length} Custom Tables...`);
225+
} else {
226+
progressTracker.updateStep('entities', `Creating ${customEntities.length} Custom Tables...`);
227+
}
228+
210229
results.customResults = await this.processCustomEntities(
211230
customEntities,
212231
parseResult.relationships,
@@ -227,6 +246,8 @@ class DeploymentService extends BaseService {
227246
results.warnings.push(...results.customResults.warnings);
228247
}
229248

249+
progressTracker.updateStep('entities', `Successfully created ${results.customResults.entitiesCreated} custom entities`);
250+
230251
console.log('🔍 DEBUG: Updated results after custom entities:', {
231252
entitiesCreated: results.entitiesCreated,
232253
relationshipsCreated: results.relationshipsCreated,
@@ -237,12 +258,18 @@ class DeploymentService extends BaseService {
237258
warnings: results.customResults.warnings?.length || 0
238259
}
239260
});
261+
} else {
262+
progressTracker.updateStep('entities', `Custom entities creation completed with warnings`);
240263
}
241264
}
265+
266+
if (cdmEntities.length > 0 || customEntities.length > 0) {
267+
progressTracker.completeStep('entities', `Entity creation completed - ${results.entitiesCreated} entities created`);
268+
}
242269

243-
// Step 8: Process global choices
270+
// Step 7: Process global choices
244271
if (config.selectedChoices?.length > 0 || config.customChoices?.length > 0) {
245-
progress('global-choices', 'Processing Global Choices...');
272+
progressTracker.startStep('globalChoices', 'Processing Global Choices...');
246273

247274
// Determine the correct publisher prefix to use
248275
let publisherPrefix;
@@ -269,11 +296,22 @@ class DeploymentService extends BaseService {
269296
results.globalChoicesAdded = results.globalChoicesResults.totalAdded || 0;
270297
results.globalChoicesCreated = results.globalChoicesResults.totalCreated || 0;
271298
results.globalChoicesExistingAdded = results.globalChoicesResults.totalExistingAdded || 0;
299+
300+
progressTracker.completeStep('globalChoices', `Global choices processed - ${results.globalChoicesCreated} created, ${results.globalChoicesAdded} added`);
301+
} else {
302+
progressTracker.completeStep('globalChoices', 'Global choices processing completed with warnings');
272303
}
304+
} else {
305+
progressTracker.startStep('globalChoices', 'No global choices to process');
306+
progressTracker.completeStep('globalChoices', 'Skipped global choices - none specified');
273307
}
274308

309+
// Step 8: Process relationships (this happens during entity creation but we track it separately)
310+
progressTracker.startStep('relationships', `Setting up ${parseResult.relationships?.length || 0} relationships...`);
311+
progressTracker.completeStep('relationships', `${results.relationshipsCreated} relationships created successfully`);
312+
275313
// Step 9: Finalize deployment
276-
progress('finalizing', 'Finalizing deployment...');
314+
progressTracker.startStep('finalization', 'Finalizing deployment...');
277315
results.summary = this.generateDeploymentSummary(results);
278316

279317
// Step 10: Record deployment in history
@@ -344,7 +382,9 @@ class DeploymentService extends BaseService {
344382
// End performance monitoring
345383
this.performanceMonitor.endOperation(deploymentId, results);
346384

347-
progress('complete', results.summary, { completed: true });
385+
// Complete the final step and the overall operation
386+
progressTracker.completeStep('finalization', 'Deployment recorded and finalized');
387+
progressTracker.complete(results.summary);
348388

349389
// Use the generated summary as the success message
350390
return this.createSuccess(results, results.summary);
@@ -353,6 +393,9 @@ class DeploymentService extends BaseService {
353393
this.error('Deployment failed', error);
354394
this.updateDeploymentStatus(deploymentId, 'failed', error.message);
355395

396+
// Fail the progress tracker
397+
progressTracker.fail('Deployment failed', error);
398+
356399
// End performance monitoring for failed deployment
357400
this.performanceMonitor.endOperation(deploymentId, {
358401
success: false,

0 commit comments

Comments
 (0)