diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index ebe40cb01f..90d94f4c14 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -76,6 +76,12 @@ export const RUN_VALIDATION = localize('sql.migration.run.validation', "Run vali export const DATABASE_FOR_ASSESSMENT_PAGE_TITLE = localize('sql.migration.database.assessment.title', "Databases for assessment"); export const DATABASE_FOR_ASSESSMENT_DESCRIPTION = localize('sql.migration.database.assessment.description', "Select the databases that you want to assess for migration to Azure SQL."); +// XEvents assessment +export const XEVENTS_ASSESSMENT_TITLE = localize('sql.migration.database.assessment.xevents.title', "Assess extended event sessions"); +export const XEVENTS_ASSESSMENT_DESCRIPTION = localize('sql.migration.database.assessment.xevents.description', "For the selected databases, optionally provide extended event session files to assess ad-hoc or dynamic SQL queries or any DML statements initiated through the application data layer. {0}"); +export const XEVENTS_ASSESSMENT_HELPLINK = localize('sql.migration.database.assessment.xevents.link', "Learn more"); +export const XEVENTS_ASSESSMENT_OPEN_FOLDER = localize('sql.migration.database.assessment.xevents.instructions', "Select a folder where extended events session files (.xel and .xem) are stored"); + // Assessment results and recommendations export const ASSESSMENT_RESULTS_AND_RECOMMENDATIONS_PAGE_TITLE = localize('sql.migration.assessment.results.and.recommendations.title', "Assessment results and recommendations"); export const ASSESSMENT_BLOCKING_ISSUE_TITLE = localize('sql.migration.assessments.blocking.issue', 'This is a blocking issue that will prevent the database migration from succeeding.'); @@ -194,8 +200,6 @@ export const AZURE_RECOMMENDATION_OPEN_EXISTING = localize('sql.migration.sku.az export const AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER = localize('sql.migration.sku.azureRecommendation.collectDataSelectFolder.instructions', "Select a folder on your local drive where performance data will be saved"); export const AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER = localize('sql.migration.sku.azureRecommendation.openExistingSelectFolder.instructions', "Select a folder on your local drive where previously collected performance data was saved"); export const FOLDER_NAME = localize('sql.migration.azureRecommendation.folder.name', "Folder name"); -export const BROWSE = localize('sql.migration.azureRecommendation.browse', "Browse"); -export const OPEN = localize('sql.migration.azureRecommendation.open', "Open"); export const VIEW_DETAILS = localize('sql.migration.sku.viewDetails', "View details"); export function ASSESSED_DBS(totalDbs: number): string { @@ -926,6 +930,10 @@ export const COPY_THROUGHPUT = localize('sql.migration.copy.throughput', "Copy t export const NEW_SUPPORT_REQUEST = localize('sql.migration.newSupportRequest', "New support request"); export const IMPACT = localize('sql.migration.impact', "Impact"); export const ALL_FIELDS_REQUIRED = localize('sql.migration.all.fields.required', 'All fields are required.'); +export const CLEAR = localize('sql.migration.clear', "Clear"); +export const SELECT = localize('sql.migration.select', "Select"); +export const BROWSE = localize('sql.migration.browse', "Browse"); +export const OPEN = localize('sql.migration.open', "Open"); //Summary Page export const START_MIGRATION_TEXT = localize('sql.migration.start.migration.button', "Start migration"); @@ -1170,7 +1178,6 @@ export const OPEN_MIGRATION_DETAILS_ERROR = localize('sql.migration.open.migrati export const OPEN_MIGRATION_TARGET_ERROR = localize('sql.migration.open.migration.target.error', "Error opening migration target"); export const OPEN_MIGRATION_SERVICE_ERROR = localize('sql.migration.open.migration.service.error', "Error opening migration service dialog"); export const LOAD_MIGRATION_LIST_ERROR = localize('sql.migration.load.migration.list.error', "Error loading migrations list"); -export const ERROR_DIALOG_CLEAR_BUTTON_LABEL = localize('sql.migration.error.dialog.clear.button.label', "Clear"); export const ERROR_DIALOG_ARIA_CLICK_VIEW_ERROR_DETAILS = localize('sql.migration.error.aria.view.details', 'Click to view error details'); export interface LookupTable { diff --git a/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts b/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts index 663520ac71..7bc58a26bb 100644 --- a/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts +++ b/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts @@ -102,7 +102,7 @@ export class DashboardStatusBar implements vscode.Disposable { 450, 'flyout'); dialog.content = [tab]; - dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL; + dialog.okButton.label = loc.CLEAR; dialog.okButton.focused = true; dialog.okButton.position = 'left'; dialog.cancelButton.label = loc.CLOSE; diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 8abb384b1e..0337085409 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -12,7 +12,7 @@ import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRestart import { IconPathHelper } from '../constants/iconPathHelper'; import { MigrationNotebookInfo, NotebookPathHelper } from '../constants/notebookPathHelper'; import * as loc from '../constants/strings'; -import { SavedAssessmentDialog } from '../dialog/assessmentResults/savedAssessmentDialog'; +import { SavedAssessmentDialog } from '../dialog/assessment/savedAssessmentDialog'; import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog'; import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel'; import { RestartMigrationDialog } from '../dialog/restartMigration/restartMigrationDialog'; diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessment/assessmentResultsDialog.ts similarity index 100% rename from extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts rename to extensions/sql-migration/src/dialog/assessment/assessmentResultsDialog.ts diff --git a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts b/extensions/sql-migration/src/dialog/assessment/savedAssessmentDialog.ts similarity index 100% rename from extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts rename to extensions/sql-migration/src/dialog/assessment/savedAssessmentDialog.ts diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessment/sqlDatabasesTree.ts similarity index 100% rename from extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts rename to extensions/sql-migration/src/dialog/assessment/sqlDatabasesTree.ts diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 56bf2aabfc..eeecc69642 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -210,6 +210,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _nodeNames!: string[]; public _databasesForAssessment!: string[]; + public _xEventsFilesFolderPath: string = ''; public _assessmentResults!: ServerAssessment; public _assessedDatabaseList!: string[]; public _runAssessments: boolean = true; @@ -400,8 +401,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getDatabaseAssessments(targetType: MigrationTargetType[]): Promise { const connectionString = await getSourceConnectionString(); try { - const xEventsFilesFolderPath = ''; // to-do: collect by prompting the user in the UI - for now, blank = disabled - const response = (await this.migrationService.getAssessments(connectionString, this._databasesForAssessment, xEventsFilesFolderPath))!; + const response = (await this.migrationService.getAssessments(connectionString, this._databasesForAssessment, this._xEventsFilesFolderPath ?? ''))!; this._assessmentApiResponse = response; this._assessedDatabaseList = this._databasesForAssessment.slice(); diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts index 7f6d043ebf..918820a562 100644 --- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts +++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; -import { debounce } from '../api/utils'; +import { debounce, promptUserForFolder } from '../api/utils'; import * as styles from '../constants/styles'; import { IconPathHelper } from '../constants/iconPathHelper'; import { getDatabasesList, excludeDatabases, SourceDatabaseInfo, getSourceConnectionProfile } from '../api/sqlUtils'; @@ -16,11 +16,16 @@ import { getDatabasesList, excludeDatabases, SourceDatabaseInfo, getSourceConnec export class DatabaseSelectorPage extends MigrationWizardPage { private _view!: azdata.ModelView; private _databaseSelectorTable!: azdata.TableComponent; + private _xEventsGroup!: azdata.GroupContainer; + private _xEventsFolderPickerInput!: azdata.InputBoxComponent; + private _xEventsFilesFolderPath!: string; private _dbNames!: string[]; private _dbCount!: azdata.TextComponent; private _databaseTableValues!: any[]; private _disposables: vscode.Disposable[] = []; + private readonly TABLE_WIDTH = 650; + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_FOR_ASSESSMENT_PAGE_TITLE), migrationStateModel); } @@ -61,6 +66,8 @@ export class DatabaseSelectorPage extends MigrationWizardPage { } return true; }); + + this._xEventsFilesFolderPath = this.migrationStateModel._xEventsFilesFolderPath; } public async onPageLeave(): Promise { @@ -73,11 +80,15 @@ export class DatabaseSelectorPage extends MigrationWizardPage { // * no prior assessment // * the prior assessment had an error or // * the assessed databases list is different from the selected databases list + // * the XEvents path has changed this.migrationStateModel._runAssessments = !this.migrationStateModel._assessmentResults || !!this.migrationStateModel._assessmentResults?.assessmentError || assessedDatabases.length === 0 || assessedDatabases.length !== selectedDatabases.length - || assessedDatabases.some(db => selectedDatabases.indexOf(db) < 0); + || assessedDatabases.some(db => selectedDatabases.indexOf(db) < 0) + || this.migrationStateModel._xEventsFilesFolderPath.toLowerCase() !== this._xEventsFilesFolderPath.toLowerCase(); + + this.migrationStateModel._xEventsFilesFolderPath = this._xEventsFilesFolderPath; } protected async handleStateChange(e: StateChangeEvent): Promise { @@ -156,8 +167,9 @@ export class DatabaseSelectorPage extends MigrationWizardPage { this._databaseSelectorTable = this._view.modelBuilder.table() .withProps({ data: [], - width: 650, + width: this.TABLE_WIDTH, height: '100%', + CSSStyles: { 'margin-bottom': '12px' }, forceFitColumns: azdata.ColumnSizingMode.ForceFit, columns: [ { @@ -212,18 +224,91 @@ export class DatabaseSelectorPage extends MigrationWizardPage { // load unfiltered table list and pre-select list of databases saved in state await this._filterTableList('', this.migrationStateModel._databasesForAssessment); + const xEventsDescription = this._view.modelBuilder.text() + .withProps({ + value: constants.XEVENTS_ASSESSMENT_DESCRIPTION, + width: this.TABLE_WIDTH, + CSSStyles: { ...styles.BODY_CSS }, + links: [{ text: constants.XEVENTS_ASSESSMENT_HELPLINK, url: 'https://aka.ms/sql-migration-xe-assess' }] + }).component(); + + const xEventsInstructions = this._view.modelBuilder.text() + .withProps({ + value: constants.XEVENTS_ASSESSMENT_OPEN_FOLDER, + width: this.TABLE_WIDTH, + CSSStyles: { ...styles.LABEL_CSS }, + }).component(); + + this._xEventsFolderPickerInput = this._view.modelBuilder.inputBox() + .withProps({ + placeHolder: constants.FOLDER_NAME, + readOnly: true, + width: 460, + ariaLabel: constants.XEVENTS_ASSESSMENT_OPEN_FOLDER + }).component(); + this._disposables.push( + this._xEventsFolderPickerInput.onTextChanged(async (value) => { + if (value) { + this._xEventsFilesFolderPath = value.trim(); + } + })); + + const xEventsFolderPickerButton = this._view.modelBuilder.button() + .withProps({ + label: constants.OPEN, + width: 80, + }).component(); + this._disposables.push( + xEventsFolderPickerButton.onDidClick( + async () => this._xEventsFolderPickerInput.value = await promptUserForFolder())); + + const xEventsFolderPickerClearButton = this._view.modelBuilder.button() + .withProps({ + label: constants.CLEAR, + width: 80, + }).component(); + this._disposables.push( + xEventsFolderPickerClearButton.onDidClick( + async () => { + this._xEventsFolderPickerInput.value = ''; + this._xEventsFilesFolderPath = ''; + })); + + const xEventsFolderPickerContainer = this._view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { 'flex-direction': 'row', 'align-items': 'left' } + }).withItems([ + this._xEventsFolderPickerInput, + xEventsFolderPickerButton, + xEventsFolderPickerClearButton + ]).component(); + + this._xEventsGroup = this._view.modelBuilder.groupContainer() + .withLayout({ + header: constants.XEVENTS_ASSESSMENT_TITLE, + collapsible: true, + collapsed: true + }).withItems([ + xEventsDescription, + xEventsInstructions, + xEventsFolderPickerContainer + ]).component(); + const flex = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', - height: '100%', + height: '65%', }).withProps({ CSSStyles: { 'margin': '0px 28px 0px 28px' } }).component(); + flex.addItem(text, { flex: '0 0 auto' }); flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' }); flex.addItem(this._dbCount, { flex: '0 0 auto' }); flex.addItem(this._databaseSelectorTable); + flex.addItem(this._xEventsGroup, { flex: '0 0 auto' }); + return flex; } diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 32370c6f9d..8f728b7aee 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -10,7 +10,7 @@ import { MigrationTargetType } from '../api/utils'; import * as contracts from '../service/contracts'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, PerformanceDataSourceOptions, StateChangeEvent, AssessmentRuleId } from '../models/stateMachine'; -import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog'; +import { AssessmentResultsDialog } from '../dialog/assessment/assessmentResultsDialog'; import { SkuRecommendationResultsDialog } from '../dialog/skuRecommendationResults/skuRecommendationResultsDialog'; import { GetAzureRecommendationDialog } from '../dialog/skuRecommendationResults/getAzureRecommendationDialog'; import * as constants from '../constants/strings';