[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

@@ -541,6 +541,7 @@ declare module 'mssql' {
sqlMiRecommendationResults: PaaSSkuRecommendationResultItem[]; sqlMiRecommendationResults: PaaSSkuRecommendationResultItem[];
sqlVmRecommendationResults: IaaSSkuRecommendationResultItem[]; sqlVmRecommendationResults: IaaSSkuRecommendationResultItem[];
instanceRequirements: SqlInstanceRequirements; instanceRequirements: SqlInstanceRequirements;
skuRecommendationReportPaths: string[];
} }
// SKU recommendation enums, mirrored from Microsoft.SqlServer.Migration.SkuRecommendation // SKU recommendation enums, mirrored from Microsoft.SqlServer.Migration.SkuRecommendation
@@ -785,6 +786,7 @@ declare module 'mssql' {
assessmentResult: ServerAssessmentProperties; assessmentResult: ServerAssessmentProperties;
rawAssessmentResult: any; rawAssessmentResult: any;
errors: ErrorModel[]; errors: ErrorModel[];
assessmentReportPath: string;
} }
export interface ISqlMigrationService { export interface ISqlMigrationService {

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata'; import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage'; import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
@@ -865,3 +866,21 @@ export async function getBlobLastBackupFileNamesValues(lastFileNames: azureResou
} }
return lastFileNamesValues; 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 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 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 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 // SKU
export const AZURE_RECOMMENDATION = localize('sql.migration.sku.recommendation', "Azure recommendation"); 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 { SqlDatabaseTree } from './sqlDatabasesTree';
import { SqlMigrationImpactedObjectInfo } from 'mssql'; import { SqlMigrationImpactedObjectInfo } from 'mssql';
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage'; 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 = { export type Issues = {
description: string, description: string,
@@ -24,6 +28,8 @@ export class AssessmentResultsDialog {
private _isOpen: boolean = false; private _isOpen: boolean = false;
private dialog: azdata.window.Dialog | undefined; private dialog: azdata.window.Dialog | undefined;
private _model: MigrationStateModel; private _model: MigrationStateModel;
private _saveButton!: azdata.window.Button;
private static readonly _assessmentReportName: string = 'SqlAssessmentReport.json';
// Dialog Name for Telemetry // Dialog Name for Telemetry
public dialogName: string | undefined; public dialogName: string | undefined;
@@ -71,6 +77,27 @@ export class AssessmentResultsDialog {
this.dialog.cancelButton.label = AssessmentResultsDialog.CancelButtonText; this.dialog.cancelButton.label = AssessmentResultsDialog.CancelButtonText;
this._disposables.push(this.dialog.cancelButton.onClick(async () => await this.cancel())); 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>[] = []; const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog)); dialogSetupPromises.push(this.initializeDialog(this.dialog));

View File

@@ -197,7 +197,7 @@ export class GetAzureRecommendationDialog {
} }
}).component(); }).component();
this._disposables.push(browseButton.onDidClick(async (e) => { this._disposables.push(browseButton.onDidClick(async (e) => {
let folder = await this.handleBrowse(); let folder = await utils.promptUserForFolder();
this._collectDataFolderInput.value = folder; this._collectDataFolderInput.value = folder;
})); }));
@@ -259,7 +259,7 @@ export class GetAzureRecommendationDialog {
} }
}).component(); }).component();
this._disposables.push(openButton.onDidClick(async (e) => { this._disposables.push(openButton.onDidClick(async (e) => {
let folder = await this.handleBrowse(); let folder = await utils.promptUserForFolder();
this._openExistingFolderInput.value = folder; this._openExistingFolderInput.value = folder;
})); }));
@@ -380,23 +380,4 @@ export class GetAzureRecommendationDialog {
public get isOpen(): boolean { public get isOpen(): boolean {
return this._isOpen; 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 constants from '../../constants/strings';
import * as styles from '../../constants/styles'; import * as styles from '../../constants/styles';
import * as mssql from 'mssql'; import * as mssql from 'mssql';
import * as utils from '../../api/utils';
import * as fs from 'fs';
import path = require('path');
export class SkuRecommendationResultsDialog { export class SkuRecommendationResultsDialog {
@@ -23,6 +26,7 @@ export class SkuRecommendationResultsDialog {
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
public title?: string; public title?: string;
public targetName?: string; public targetName?: string;
private _saveButton!: azdata.window.Button;
public targetRecommendations?: mssql.SkuRecommendationResultItem[]; public targetRecommendations?: mssql.SkuRecommendationResultItem[];
public instanceRequirements?: mssql.SqlInstanceRequirements; public instanceRequirements?: mssql.SqlInstanceRequirements;
@@ -491,6 +495,48 @@ export class SkuRecommendationResultsDialog {
// this.dialog.cancelButton.label = SkuRecommendationResultsDialog.CreateTargetButtonText; // this.dialog.cancelButton.label = SkuRecommendationResultsDialog.CreateTargetButtonText;
// this._disposables.push(this.dialog.cancelButton.onClick(async () => console.log(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>[] = []; const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog)); dialogSetupPromises.push(this.initializeDialog(this.dialog));
azdata.window.openDialog(this.dialog); azdata.window.openDialog(this.dialog);

View File

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