[SQL Migration] Add buttons to allow saving assessment/recommendation reports (#20212)

* Implement save assessment report

* Implement save recommendation report
This commit is contained in:
Raymond Truong
2022-08-15 15:29:40 -07:00
committed by GitHub
parent 10f5b8b76e
commit e64171503a
7 changed files with 116 additions and 24 deletions

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto';
@@ -865,3 +866,21 @@ export async function getBlobLastBackupFileNamesValues(lastFileNames: azureResou
}
return lastFileNamesValues;
}
export async function promptUserForFolder(): Promise<string> {
let path = '';
let options: vscode.OpenDialogOptions = {
defaultUri: vscode.Uri.file(getUserHome()!),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
};
let fileUris = await vscode.window.showOpenDialog(options);
if (fileUris && fileUris?.length > 0 && fileUris[0]) {
path = fileUris[0].fsPath;
}
return path;
}

View File

@@ -94,6 +94,14 @@ export function CAN_BE_MIGRATED(eligibleDbs: number, totalDbs: number): string {
export const ASSESSMENT_MIGRATION_WARNING = localize('sql.migration.assessment.migration.warning', "Databases that are not ready for migration to Azure SQL Managed Instance can be migrated to SQL Server on Azure Virtual Machines.");
export const DATABASES_TABLE_TILE = localize('sql.migration.databases.table.title', "Databases");
export const SQL_SERVER_INSTANCE = localize('sql.migration.sql.server.instance', "SQL Server instance");
export const SAVE_ASSESSMENT_REPORT = localize('sql.migration.save.assessment.report', "Save assessment report");
export const SAVE_RECOMMENDATION_REPORT = localize('sql.migration.save.recommendation.report', "Save recommendation report");
export function SAVE_ASSESSMENT_REPORT_SUCCESS(filePath: string): string {
return localize('sql.migration.save.assessment.report.success', "Successfully saved assessment report to {0}.", filePath);
}
export function SAVE_RECOMMENDATION_REPORT_SUCCESS(filePath: string): string {
return localize('sql.migration.save.recommendation.report.success', "Successfully saved recommendation report to {0}.", filePath);
}
// SKU
export const AZURE_RECOMMENDATION = localize('sql.migration.sku.recommendation', "Azure recommendation");

View File

@@ -9,6 +9,10 @@ import { MigrationStateModel, MigrationTargetType } from '../../models/stateMach
import { SqlDatabaseTree } from './sqlDatabasesTree';
import { SqlMigrationImpactedObjectInfo } from 'mssql';
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
import * as constants from '../../constants/strings';
import * as utils from '../../api/utils';
import * as fs from 'fs';
import path = require('path');
export type Issues = {
description: string,
@@ -24,6 +28,8 @@ export class AssessmentResultsDialog {
private _isOpen: boolean = false;
private dialog: azdata.window.Dialog | undefined;
private _model: MigrationStateModel;
private _saveButton!: azdata.window.Button;
private static readonly _assessmentReportName: string = 'SqlAssessmentReport.json';
// Dialog Name for Telemetry
public dialogName: string | undefined;
@@ -71,6 +77,27 @@ export class AssessmentResultsDialog {
this.dialog.cancelButton.label = AssessmentResultsDialog.CancelButtonText;
this._disposables.push(this.dialog.cancelButton.onClick(async () => await this.cancel()));
this._saveButton = azdata.window.createButton(
constants.SAVE_ASSESSMENT_REPORT,
'left');
this._disposables.push(
this._saveButton.onClick(async () => {
const folder = await utils.promptUserForFolder();
const destinationFilePath = path.join(folder, AssessmentResultsDialog._assessmentReportName);
if (this.model._assessmentReportFilePath) {
fs.copyFile(this.model._assessmentReportFilePath, destinationFilePath, (err) => {
if (err) {
console.log(err);
} else {
void vscode.window.showInformationMessage(constants.SAVE_ASSESSMENT_REPORT_SUCCESS(destinationFilePath));
}
});
} else {
console.log('assessment report not found');
}
}));
this.dialog.customButtons = [this._saveButton];
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));

View File

@@ -197,7 +197,7 @@ export class GetAzureRecommendationDialog {
}
}).component();
this._disposables.push(browseButton.onDidClick(async (e) => {
let folder = await this.handleBrowse();
let folder = await utils.promptUserForFolder();
this._collectDataFolderInput.value = folder;
}));
@@ -259,7 +259,7 @@ export class GetAzureRecommendationDialog {
}
}).component();
this._disposables.push(openButton.onDidClick(async (e) => {
let folder = await this.handleBrowse();
let folder = await utils.promptUserForFolder();
this._openExistingFolderInput.value = folder;
}));
@@ -380,23 +380,4 @@ export class GetAzureRecommendationDialog {
public get isOpen(): boolean {
return this._isOpen;
}
// TO-DO: add validation
private async handleBrowse(): Promise<string> {
let path = '';
let options: vscode.OpenDialogOptions = {
defaultUri: vscode.Uri.file(utils.getUserHome()!),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
};
let fileUris = await vscode.window.showOpenDialog(options);
if (fileUris && fileUris?.length > 0 && fileUris[0]) {
path = fileUris[0].fsPath;
}
return path;
}
}

View File

@@ -9,6 +9,9 @@ import { MigrationStateModel, MigrationTargetType } from '../../models/stateMach
import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles';
import * as mssql from 'mssql';
import * as utils from '../../api/utils';
import * as fs from 'fs';
import path = require('path');
export class SkuRecommendationResultsDialog {
@@ -23,6 +26,7 @@ export class SkuRecommendationResultsDialog {
private _disposables: vscode.Disposable[] = [];
public title?: string;
public targetName?: string;
private _saveButton!: azdata.window.Button;
public targetRecommendations?: mssql.SkuRecommendationResultItem[];
public instanceRequirements?: mssql.SqlInstanceRequirements;
@@ -491,6 +495,48 @@ export class SkuRecommendationResultsDialog {
// this.dialog.cancelButton.label = SkuRecommendationResultsDialog.CreateTargetButtonText;
// this._disposables.push(this.dialog.cancelButton.onClick(async () => console.log(SkuRecommendationResultsDialog.CreateTargetButtonText)));
this._saveButton = azdata.window.createButton(
constants.SAVE_RECOMMENDATION_REPORT,
'left');
this._disposables.push(
this._saveButton.onClick(async () => {
const folder = await utils.promptUserForFolder();
if (this.model._skuRecommendationReportFilePaths) {
let sourceFilePath: string | undefined;
let destinationFilePath: string | undefined;
switch (this._targetType) {
case MigrationTargetType.SQLMI:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlManagedInstance'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlManagedInstance.html');
break;
case MigrationTargetType.SQLVM:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlVirtualMachine'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlVirtualMachine.html');
break;
case MigrationTargetType.SQLDB:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlDatabase'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlDatabase.html');
break;
}
fs.copyFile(sourceFilePath!, destinationFilePath, (err) => {
if (err) {
console.log(err);
} else {
void vscode.window.showInformationMessage(constants.SAVE_RECOMMENDATION_REPORT_SUCCESS(destinationFilePath!));
}
});
} else {
console.log('recommendation report not found');
}
}));
this.dialog.customButtons = [this._saveButton];
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));
azdata.window.openDialog(this.dialog);

View File

@@ -199,6 +199,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _assessedDatabaseList!: string[];
public _runAssessments: boolean = true;
private _assessmentApiResponse!: mssql.AssessmentResult;
public _assessmentReportFilePath: string;
public mementoString: string;
public _databasesForMigration: string[] = [];
@@ -210,6 +211,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _skuRecommendationResults!: SkuRecommendation;
public _skuRecommendationPerformanceDataSource!: PerformanceDataSourceOptions;
private _skuRecommendationApiResponse!: mssql.SkuRecommendationResult;
public _skuRecommendationReportFilePaths: string[];
public _skuRecommendationPerformanceLocation!: string;
private _skuRecommendationRecommendedDatabaseList!: string[];
private _startPerfDataCollectionApiResponse!: mssql.StartPerfDataCollectionResult;
@@ -263,6 +265,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._databaseBackup.networkShares = [];
this._databaseBackup.blobs = [];
this._targetDatabaseNames = [];
this._assessmentReportFilePath = '';
this._skuRecommendationReportFilePaths = [];
this.mementoString = 'sqlMigration.assessmentResults';
this._skuScalingFactor = 100;
@@ -325,6 +329,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}) ?? [],
errors: this._assessmentApiResponse?.errors ?? []
};
this._assessmentReportFilePath = response.assessmentReportPath;
} else {
this._assessmentResults = {
issues: [],
@@ -394,16 +399,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
sqlDbRecommendationResults: response?.sqlDbRecommendationResults ?? [],
sqlMiRecommendationResults: response?.sqlMiRecommendationResults ?? [],
sqlVmRecommendationResults: response?.sqlVmRecommendationResults ?? [],
instanceRequirements: response?.instanceRequirements
instanceRequirements: response?.instanceRequirements,
skuRecommendationReportPaths: response?.skuRecommendationReportPaths
},
};
this._skuRecommendationReportFilePaths = response.skuRecommendationReportPaths;
} else {
this._skuRecommendationResults = {
recommendations: {
sqlDbRecommendationResults: [],
sqlMiRecommendationResults: [],
sqlVmRecommendationResults: [],
instanceRequirements: response?.instanceRequirements
instanceRequirements: response?.instanceRequirements,
skuRecommendationReportPaths: response?.skuRecommendationReportPaths
},
};
}
@@ -416,7 +424,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
sqlDbRecommendationResults: this._skuRecommendationApiResponse?.sqlDbRecommendationResults ?? [],
sqlMiRecommendationResults: this._skuRecommendationApiResponse?.sqlMiRecommendationResults ?? [],
sqlVmRecommendationResults: this._skuRecommendationApiResponse?.sqlVmRecommendationResults ?? [],
instanceRequirements: this._skuRecommendationApiResponse?.instanceRequirements
instanceRequirements: this._skuRecommendationApiResponse?.instanceRequirements,
skuRecommendationReportPaths: this._skuRecommendationApiResponse?.skuRecommendationReportPaths
},
recommendationError: error
};