From 866ced5c08b154df0777d7af2ab2bfb6131d9615 Mon Sep 17 00:00:00 2001 From: Brian Harris Date: Thu, 20 May 2021 14:09:45 -0700 Subject: [PATCH] adding feedback dialog and support request buttons --- extensions/sql-migration/images/blueStar.svg | 3 + .../images/newSupportRequest.svg | 4 + .../sql-migration/images/sendFeedback.svg | 3 + .../sql-migration/images/solidBlueStar.svg | 3 + extensions/sql-migration/package.json | 18 +- extensions/sql-migration/package.nls.json | 4 +- .../src/constants/iconPathHelper.ts | 20 ++ .../sql-migration/src/constants/strings.ts | 14 ++ .../src/dialog/feedbackDialog.ts | 184 ++++++++++++++++++ .../migrationCutoverDialog.ts | 26 ++- extensions/sql-migration/src/main.ts | 17 ++ .../sql-migration/src/models/stateMachine.ts | 5 +- extensions/sql-migration/src/telemtery.ts | 13 +- 13 files changed, 306 insertions(+), 8 deletions(-) create mode 100644 extensions/sql-migration/images/blueStar.svg create mode 100644 extensions/sql-migration/images/newSupportRequest.svg create mode 100644 extensions/sql-migration/images/sendFeedback.svg create mode 100644 extensions/sql-migration/images/solidBlueStar.svg create mode 100644 extensions/sql-migration/src/dialog/feedbackDialog.ts diff --git a/extensions/sql-migration/images/blueStar.svg b/extensions/sql-migration/images/blueStar.svg new file mode 100644 index 0000000000..42c870dffe --- /dev/null +++ b/extensions/sql-migration/images/blueStar.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/images/newSupportRequest.svg b/extensions/sql-migration/images/newSupportRequest.svg new file mode 100644 index 0000000000..f930b047c8 --- /dev/null +++ b/extensions/sql-migration/images/newSupportRequest.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/sql-migration/images/sendFeedback.svg b/extensions/sql-migration/images/sendFeedback.svg new file mode 100644 index 0000000000..92b2ab23d0 --- /dev/null +++ b/extensions/sql-migration/images/sendFeedback.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/images/solidBlueStar.svg b/extensions/sql-migration/images/solidBlueStar.svg new file mode 100644 index 0000000000..a740be4d24 --- /dev/null +++ b/extensions/sql-migration/images/solidBlueStar.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index d9e5414a8c..5414f36fe9 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -15,6 +15,8 @@ "activationEvents": [ "onDashboardOpen", "onCommand:sqlmigration.start", + "onCommand:sqlmigration.newsupportrequest", + "onCommand:sqlmigration.sendfeedback", "onCommand:sqlmigration.openNotebooks" ], "main": "./out/main", @@ -33,6 +35,18 @@ "category": "%migration-command-category%", "icon": "./images/migration.svg" }, + { + "command": "sqlmigration.newsupportrequest", + "title": "%new-support-request-command%", + "category": "%migration-command-category%", + "icon": "./images/newSupportRequest.svg" + }, + { + "command": "sqlmigration.sendfeedback", + "title": "%send-feedback-command%", + "category": "%migration-command-category%", + "icon": "./images/sendFeedback.svg" + }, { "command": "sqlmigration.openNotebooks", "title": "%migration-notebook-command-title%", @@ -58,7 +72,9 @@ "col": 0, "widget": { "tasks-widget": [ - "sqlmigration.start" + "sqlmigration.start", + "sqlmigration.newsupportrequest", + "sqlmigration.sendfeedback" ] } }, diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json index 55203c2c4a..b165feed3c 100644 --- a/extensions/sql-migration/package.nls.json +++ b/extensions/sql-migration/package.nls.json @@ -5,5 +5,7 @@ "migration-dashboard-title": "Azure SQL Migration", "migration-dashboard-tasks": "Migration Tasks", "migration-command-category": "Azure SQL Migration", - "start-migration-command": "Migrate to Azure SQL" + "start-migration-command": "Migrate to Azure SQL", + "new-support-request-command": "New support request", + "send-feedback-command": "Feedback" } diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index fc348b100a..13d2c94113 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -30,6 +30,10 @@ export class IconPathHelper { public static cancel: IconPath; public static warning: IconPath; public static info: IconPath; + public static newSupportRequest: IconPath; + public static sendFeedback: IconPath; + public static solidBlueStar: IconPath; + public static blueStar: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -108,5 +112,21 @@ export class IconPathHelper { light: context.asAbsolutePath('images/info.svg'), dark: context.asAbsolutePath('images/infoBox.svg') }; + IconPathHelper.newSupportRequest = { + light: context.asAbsolutePath('images/newSupportRequest.svg'), + dark: context.asAbsolutePath('images/newSupportRequest.svg') + }; + IconPathHelper.sendFeedback = { + light: context.asAbsolutePath('images/sendFeedback.svg'), + dark: context.asAbsolutePath('images/sendFeedback.svg') + }; + IconPathHelper.solidBlueStar = { + light: context.asAbsolutePath('images/solidBlueStar.svg'), + dark: context.asAbsolutePath('images/solidBlueStar.svg') + }; + IconPathHelper.blueStar = { + light: context.asAbsolutePath('images/blueStar.svg'), + dark: context.asAbsolutePath('images/blueStar.svg') + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 1ed2bafb86..20c51b579e 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -315,6 +315,8 @@ export const CANCEL_MIGRATION_CONFIRMATION = localize('sql.cancel.migration.conf export const YES = localize('sql.migration.yes', "Yes"); export const NO = localize('sql.migration.no', "No"); +export const NEW_SUPPORT_REQUEST = localize('sql.migration.newsupportrequest', "New support request"); + //Migration confirm cutover dialog export const BUSINESS_CRITICAL_INFO = localize('sql.migration.bc.info', "Managed Instance migration cutover for Business Critical service tier can take significantly longer than General Purpose as three secondary replicas have to be seeded for Always On High Availability group. This operation duration depends on the size of data. Seeding speed in 90% of cases is 220 GB/hour or higher."); export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "When you are ready to do the migration cutover, perform the following steps to complete the database migration. Please note that the database is ready for cutover only after a full backup has been restored on the target Azure SQL Database Managed Instance."); @@ -421,3 +423,15 @@ export function WARNINGS_COUNT(totalCount: number): string { export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', "Authentication Type"); export const SQL_LOGIN = localize('sql.migration.sql.login', "SQL Login"); export const WINDOWS_AUTHENTICATION = localize('sql.migration.windows.auth', "Windows Authentication"); + +export const FEEDBACK_DIALOG_SUBMIT_BUTTON = localize('sql.migration.feedback.submit.button', "Submit"); +export const FEEDBACK_DIALOG_CANCEL_BUTTON = localize('sql.migration.feedback.cancel.button', "Cancel"); +export const FEEDBACK_DIALOG_TITLE = localize('sql.migration.feedback.title', "Submit Feedback"); +export const FEEDBACK_DIALOG_HEADING = localize('sql.migration.feedback.heading', "Overall, how satisfied or dissatisfied are you with Azure SQL Migration experience?"); +export const FEEDBACK_DIALOG_PLACEHOLDER = localize('sql.migration.feedback.placeholder', "We appreciate your feedback. How can we improve? (optional)."); +export const FEEDBACK_DIALOG_RATING_1 = localize('sql.migration.feedback.rating.one', "Rating one out of five"); +export const FEEDBACK_DIALOG_RATING_2 = localize('sql.migration.feedback.rating.two', "Rating two out of five"); +export const FEEDBACK_DIALOG_RATING_3 = localize('sql.migration.feedback.rating.three', "Rating three out of five"); +export const FEEDBACK_DIALOG_RATING_4 = localize('sql.migration.feedback.rating.four', "Rating four out of five"); +export const FEEDBACK_DIALOG_RATING_5 = localize('sql.migration.feedback.rating.five', 'Rating five out of five'); +export const FEEDBACK_DIALOG_SENT_MESSAGE = localize('sql.migration.feedback.sent.message', 'Thank you for the feedback! Your feedback was submitted successfully.'); diff --git a/extensions/sql-migration/src/dialog/feedbackDialog.ts b/extensions/sql-migration/src/dialog/feedbackDialog.ts new file mode 100644 index 0000000000..41d64c8a5c --- /dev/null +++ b/extensions/sql-migration/src/dialog/feedbackDialog.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import { IconPathHelper } from '../constants/iconPathHelper'; +import * as loc from '../constants/strings'; +import { sendSqlMigrationActionEvent, TelemetryActions, TelemetryViews } from '../telemtery'; + +export class FeedbackDialog { + + private static readonly DialogName: string = 'FeedbackDialog'; + + private _dialog!: azdata.window.Dialog; + private _buttonGroup!: azdata.FlexContainer; + private _isOpen: boolean = false; + private _feedbackRating?: number; + private _feedbackText?: string; + + constructor() { + } + + public async openDialog() { + if (!this._isOpen) { + this._isOpen = true; + this._dialog = azdata.window.createModelViewDialog( + '', + FeedbackDialog.DialogName, + 360, + 'normal', + 'below'); + + this._dialog.registerContent(async view => { + const headingGroup = view.modelBuilder + .flexContainer() + .withItems([ + view.modelBuilder + .image() + .withProperties({ + iconPath: IconPathHelper.sendFeedback, + iconHeight: 32, + iconWidth: 32, + height: 32, + width: 32, + }) + .component(), + view.modelBuilder + .text() + .withProperties({ + value: loc.FEEDBACK_DIALOG_HEADING, + CSSStyles: { + 'margin': '0 0 0 10px', + }, + }) + .component(), + ]) + .withLayout({ + width: '100%', + alignContent: 'flex-start', + flexFlow: 'row', + }) + .component(); + + this._buttonGroup = view.modelBuilder + .flexContainer() + .withItems([ + this._createFeedbackButton(view, 0, loc.FEEDBACK_DIALOG_RATING_1), + this._createFeedbackButton(view, 1, loc.FEEDBACK_DIALOG_RATING_2), + this._createFeedbackButton(view, 2, loc.FEEDBACK_DIALOG_RATING_3), + this._createFeedbackButton(view, 3, loc.FEEDBACK_DIALOG_RATING_4), + this._createFeedbackButton(view, 4, loc.FEEDBACK_DIALOG_RATING_5), + ]) + .withLayout({ + alignContent: 'flex-start', + flexFlow: 'row', + }) + .withProperties({ + display: 'inline-flex', + ariaLabel: loc.FEEDBACK_DIALOG_HEADING, + }) + .component(); + + const feedbackInputBox = view.modelBuilder + .inputBox() + .withProperties({ + rows: 3, + inputType: 'text', + multiline: true, + placeHolder: loc.FEEDBACK_DIALOG_PLACEHOLDER, + CSSStyles: { + 'white-space': 'normal!important', + }, + }) + .component(); + + feedbackInputBox.onTextChanged( + value => this._feedbackText = value); + + const formModel = view.modelBuilder + .formContainer() + .withFormItems([{ + components: [ + { + component: headingGroup, + }, + { + component: this._buttonGroup, + }, + { + component: feedbackInputBox, + } + ], + title: '' + }]) + .withLayout({ width: '100%' }) + .component(); + + await view.initializeModel(formModel); + await this._buttonGroup.items[0].focus(); + }); + + this._dialog.okButton.label = loc.FEEDBACK_DIALOG_SUBMIT_BUTTON; + this._dialog.okButton.onClick(async () => await this._execute()); + + this._dialog.cancelButton.label = loc.FEEDBACK_DIALOG_CANCEL_BUTTON; + this._dialog.cancelButton.onClick(() => this._cancel()); + + azdata.window.openDialog(this._dialog); + } + } + + private async _execute() { + sendSqlMigrationActionEvent( + TelemetryViews.FeedbackDialog, + TelemetryActions.SendFeedback, + { + 'FeedbackRating': this._feedbackRating?.toString() || '', + 'FeedbackMessage': this._feedbackText?.substr(0, 500) || '', + }); + + await vscode.window.showInformationMessage(loc.FEEDBACK_DIALOG_SENT_MESSAGE); + this._isOpen = false; + } + + private _cancel() { + this._isOpen = false; + } + + private _createFeedbackButton(view: azdata.ModelView, index: number, ariaLabel: string): azdata.Component { + const button = view.modelBuilder + .button() + .withProperties({ + ariaLabel: ariaLabel, + height: '26px', + width: '26px', + buttonType: azdata.ButtonType.Normal, + iconHeight: '24px', + iconWidth: '24px', + iconPath: IconPathHelper.blueStar, + CSSStyles: { + 'margin': '0 10px 0 0', + 'padding': '0 0 0 0', + }, + }) + .component(); + + button.onDidClick(() => this._updateButtonImages(index)); + + return button; + } + + private _updateButtonImages(index: number): void { + const items: azdata.Component[] = this._buttonGroup?.items || []; + this._feedbackRating = index; + for (let i = 0; i < items.length; i++) { + const btn = items[i] as azdata.ButtonComponent; + btn.iconPath = i <= index + ? IconPathHelper.solidBlueStar + : IconPathHelper.blueStar; + } + } +} diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 918a432778..c7f2381b35 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -24,6 +24,7 @@ export class MigrationCutoverDialog { private _cancelButton!: azdata.ButtonComponent; private _refreshLoader!: azdata.LoadingComponent; private _copyDatabaseMigrationDetails!: azdata.ButtonComponent; + private _newSupportRequest!: azdata.ButtonComponent; private _serverName!: azdata.TextComponent; private _serverVersion!: azdata.TextComponent; @@ -371,7 +372,6 @@ export class MigrationCutoverDialog { flex: '0' }); - this._refreshButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, iconHeight: '16px', @@ -419,6 +419,30 @@ export class MigrationCutoverDialog { } }); + // create new support request button. Hiding button until sql migration support has been setup. + this._newSupportRequest = this._view.modelBuilder.button().withProps({ + label: loc.NEW_SUPPORT_REQUEST, + iconPath: IconPathHelper.newSupportRequest, + iconHeight: '16px', + iconWidth: '16px', + height: '20px', + width: '140px', + display: 'none' // remove when support requests are setup for sql migrations + }).component(); + + this._newSupportRequest.onDidClick(async (e) => { + const serviceId = this._model._migration.controller.id; + const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`; + await vscode.env.openExternal(vscode.Uri.parse(supportUrl)); + }); + + headerActions.addItem(this._newSupportRequest, { + flex: '0', + CSSStyles: { + 'margin-left': '5px' + } + }); + this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ loading: false, height: '15px' diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index d32ee11ed2..2dc80a96a4 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -10,6 +10,7 @@ import { promises as fs } from 'fs'; import * as loc from './constants/strings'; import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper'; import { IconPathHelper } from './constants/iconPathHelper'; +import { FeedbackDialog } from './dialog/feedbackDialog'; import { DashboardWidget } from './dashboard/sqlServerDashboard'; import { MigrationLocalStorage } from './models/migrationLocalStorage'; @@ -56,6 +57,12 @@ class SQLMigration { }), azdata.tasks.registerTask('sqlmigration.start', async () => { await this.launchMigrationWizard(); + }), + azdata.tasks.registerTask('sqlmigration.newsupportrequest', async () => { + await this.launchNewSupportRequest(); + }), + azdata.tasks.registerTask('sqlmigration.sendfeedback', async () => { + await this.sendFeedback(); }) ]; @@ -77,6 +84,16 @@ class SQLMigration { await wizardController.openWizard(connectionId); } + async launchNewSupportRequest(): Promise { + await vscode.env.openExternal(vscode.Uri.parse( + `https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`)); + } + + async sendFeedback(): Promise { + const dialog = new FeedbackDialog(); + await dialog.openDialog(); + } + stop(): void { } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index c98431f048..3fe2b55505 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -723,8 +723,11 @@ export class MigrationStateModel implements Model, vscode.Disposable { vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._migrationDbs[i], this._targetServerInstance.name, this._targetDatabaseNames[i])); } } catch (e) { + vscode.window.showErrorMessage( + localize('sql.migration.starting.migration.error', "Error message: '{0}'. stack:'{1}'", + e.message, + e.stack)); console.log(e); - vscode.window.showInformationMessage(e); } vscode.commands.executeCommand('sqlmigration.refreshMigrationTiles'); diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts index 4b72c77fac..820189422b 100644 --- a/extensions/sql-migration/src/telemtery.ts +++ b/extensions/sql-migration/src/telemtery.ts @@ -18,12 +18,17 @@ export enum TelemetryViews { AssessmentsDialog = 'AssessmentsDialog', MigrationCutoverDialog = 'MigrationCutoverDialog', MigrationStatusDialog = 'MigrationStatusDialog', - AssessmentsPage = 'AssessmentsPage' + AssessmentsPage = 'AssessmentsPage', + FeedbackDialog = 'FeedbackDialog', } -export function sendSqlMigrationActionEvent(telemetryView: string, telemetryAction: string, additionalProps: TelemetryEventProperties, additionalMeasurements: TelemetryEventMeasures): void { +export enum TelemetryActions { + SendFeedback = 'SendFeedback', +} + +export function sendSqlMigrationActionEvent(telemetryView: TelemetryViews, telemetryAction: TelemetryActions, additionalProps?: TelemetryEventProperties, additionalMeasurements?: TelemetryEventMeasures): void { TelemetryReporter.createActionEvent(telemetryView, telemetryAction) - .withAdditionalProperties(additionalProps) - .withAdditionalMeasurements(additionalMeasurements) + .withAdditionalProperties(additionalProps || {}) + .withAdditionalMeasurements(additionalMeasurements || {}) .send(); }