diff --git a/extensions/sql-migration/images/background.svg b/extensions/sql-migration/images/background.svg new file mode 100644 index 0000000000..9bc057df35 --- /dev/null +++ b/extensions/sql-migration/images/background.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/images/backgroundLogo.svg b/extensions/sql-migration/images/backgroundLogo.svg new file mode 100644 index 0000000000..024ff98147 --- /dev/null +++ b/extensions/sql-migration/images/backgroundLogo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/images/createNotebook.svg b/extensions/sql-migration/images/createNotebook.svg new file mode 100644 index 0000000000..c8d1afea88 --- /dev/null +++ b/extensions/sql-migration/images/createNotebook.svg @@ -0,0 +1 @@ +j_ diff --git a/extensions/sql-migration/images/migration.svg b/extensions/sql-migration/images/migration.svg new file mode 100644 index 0000000000..35d958684b --- /dev/null +++ b/extensions/sql-migration/images/migration.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/images/sqlMiImportHelpThumbnail.svg b/extensions/sql-migration/images/sqlMiImportHelpThumbnail.svg new file mode 100644 index 0000000000..94fbb8932b --- /dev/null +++ b/extensions/sql-migration/images/sqlMiImportHelpThumbnail.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/sql-migration/images/sqlVmImportHelpThumbnail.svg b/extensions/sql-migration/images/sqlVmImportHelpThumbnail.svg new file mode 100644 index 0000000000..1f1f586e01 --- /dev/null +++ b/extensions/sql-migration/images/sqlVmImportHelpThumbnail.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index a0dad23c8b..7d9654830d 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -12,6 +12,7 @@ "azdata": ">=1.19.0" }, "activationEvents": [ + "onDashboardOpen", "onCommand:sqlmigration.start", "onCommand:sqlmigration.testDialog", "onCommand:sqlmigration.openNotebooks" @@ -41,6 +42,35 @@ "title": "%migration-notebook-command-title%", "category": "SQL Migration" } + ], + "dashboard.tabs": [ + { + "id": "migration-dashboard", + "description": "%migration-dashboard-title%", + "provider": "MSSQL", + "title": "%migration-dashboard-title%", + "icon": { + "light": "./images/migration.svg", + "dark": "./images/migration.svg" + }, + "when": "connectionProvider == 'MSSQL' && !mssql:iscloud", + "container": { + "grid-container": [ + { + "name": "", + "row": 0, + "col": 1, + "rowspan": 5, + "colspan": 5, + "widget": { + "modelview": { + "id": "migration.dashboard" + } + } + } + ] + } + } ] }, "dependencies": { diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json index 86980920da..f64b7e61c2 100644 --- a/extensions/sql-migration/package.nls.json +++ b/extensions/sql-migration/package.nls.json @@ -1,5 +1,7 @@ { "displayName": "SQL Migration", "description": "SQL migration description", - "migration-notebook-command-title": "Open SQL migration notebooks" + "migration-notebook-command-title": "Open SQL migration notebooks", + "migration-dashboard-title": "SQL Migration", + "migration-dashboard-tasks": "Migration Tasks" } diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 0687d962c6..d249f3d72b 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -127,8 +127,8 @@ export async function getMigrationControllerAuthKeys(account: azdata.Account, su throw new Error(response.errors.toString()); } return { - keyName1: response?.response?.data?.keyName1 ?? '', - keyName2: response?.response?.data?.keyName2 ?? '' + authKey1: response?.response?.data?.authKey1 ?? '', + authKey2: response?.response?.data?.authKey2 ?? '' }; } @@ -157,7 +157,7 @@ export async function getMigrationControllerMonitoringData(account: azdata.Accou return response.response.data; } -export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, migrationControllerName: string, requestBody: StartDatabaseMigrationRequest): Promise { +export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, migrationControllerName: string, requestBody: StartDatabaseMigrationRequest): Promise { const api = await getAzureCoreAPI(); const host = `https://${regionName}.management.azure.com`; const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${migrationControllerName}?api-version=2020-09-01-preview`; @@ -166,12 +166,23 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti throw new Error(response.errors.toString()); } return { - errors: response.errors, status: response.response.status, databaseMigration: response.response.data }; } +export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise { + const api = await getAzureCoreAPI(); + const host = `https://eastus2euap.management.azure.com`; + const path = `${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + return { + result: response.response.data + }; +} /** * For now only east us euap is supported. Actual API calls will be added in the public release. @@ -223,8 +234,8 @@ export interface MigrationController { } export interface GetMigrationControllerAuthKeysResult { - keyName1: string, - keyName2: string + authKey1: string, + authKey2: string } export interface GetStorageAccountAccessKeysResult { @@ -285,3 +296,8 @@ export interface DatabaseMigration { name: string, type: string } + +export interface StartDatabaseMigrationResponse { + status: number, + databaseMigration: DatabaseMigration +} diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index 2495353572..64c3eec683 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -11,21 +11,37 @@ export interface IconPath { } export class IconPathHelper { - private static context: vscode.ExtensionContext; - public static copy: IconPath; public static refresh: IconPath; + public static sqlMiImportHelpThumbnail: IconPath; + public static sqlVmImportHelpThumbnail: IconPath; + public static migrationDashboardHeaderBackground: IconPath; + public static sqlMigrationLogo: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { - IconPathHelper.context = context; IconPathHelper.copy = { - light: IconPathHelper.context.asAbsolutePath('images/copy.svg'), - dark: IconPathHelper.context.asAbsolutePath('images/copy.svg') + light: context.asAbsolutePath('images/copy.svg'), + dark: context.asAbsolutePath('images/copy.svg') }; IconPathHelper.refresh = { light: context.asAbsolutePath('images/refresh.svg'), dark: context.asAbsolutePath('images/refresh.svg') }; - + IconPathHelper.sqlMiImportHelpThumbnail = { + light: context.asAbsolutePath('images/sqlMiImportHelpThumbnail.svg'), + dark: context.asAbsolutePath('images/sqlMiImportHelpThumbnail.svg') + }; + IconPathHelper.sqlVmImportHelpThumbnail = { + light: context.asAbsolutePath('images/sqlVmImportHelpThumbnail.svg'), + dark: context.asAbsolutePath('images/sqlVmImportHelpThumbnail.svg') + }; + IconPathHelper.migrationDashboardHeaderBackground = { + light: context.asAbsolutePath('images/background.svg'), + dark: context.asAbsolutePath('images/background.svg') + }; + IconPathHelper.sqlMigrationLogo = { + light: context.asAbsolutePath('images/migration.svg'), + dark: context.asAbsolutePath('images/migration.svg') + }; } } diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts new file mode 100644 index 0000000000..7018658f46 --- /dev/null +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -0,0 +1,499 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MigrationLocalStorage } from '../models/migrationLocalStorage'; +import * as loc from '../models/strings'; +import { IconPathHelper } from '../constants/iconPathHelper'; + +interface IActionMetadata { + title?: string, + description?: string, + link?: string, + iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri }, + command?: string; +} + +const maxWidth = 800; + +export class DashboardWidget { + + private _migrationStatusCardsContainer!: azdata.FlexContainer; + private _view!: azdata.ModelView; + + constructor() { + } + + public register(): void { + azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => { + this._view = view; + const container = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: '100%', + height: '100%' + }).component(); + const header = this.createHeader(view); + + const tasksContainer = await this.createTasks(view); + + container.addItem(header, { + CSSStyles: { + 'background-image': `url(${vscode.Uri.file(IconPathHelper.migrationDashboardHeaderBackground.light)})`, + 'width': '1100px', + 'height': '300px', + 'background-size': '100%', + } + }); + header.addItem(tasksContainer, { + CSSStyles: { + 'width': `${maxWidth}px`, + 'height': '150px', + } + }); + + header.addItem(await this.createFooter(view), { + CSSStyles: { + 'margin-top': '20px' + } + }); + + const mainContainer = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + width: '100%', + height: '100%', + position: 'absolute' + }).component(); + mainContainer.addItem(container, { + CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' } + }); + await view.initializeModel(mainContainer); + + this.refreshMigrations(); + }); + } + + private createHeader(view: azdata.ModelView): azdata.FlexContainer { + const header = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: maxWidth, + }).component(); + const titleComponent = view.modelBuilder.text().withProps({ + value: loc.DASHBOARD_TITLE, + CSSStyles: { + 'font-size': '36px', + } + }).component(); + const descComponent = view.modelBuilder.text().withProps({ + value: loc.DASHBOARD_DESCRIPTION, + CSSStyles: { + 'font-size': '12px', + } + }).component(); + header.addItems([titleComponent, descComponent], { + CSSStyles: { + 'width': `${maxWidth}px`, + 'padding-left': '20px' + } + }); + + return header; + } + + private async createTasks(view: azdata.ModelView): Promise { + const tasksContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + width: '100%', + height: '50px', + }).component(); + + const migrateButtonMetadata: IActionMetadata = { + title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE, + description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION, + iconPath: IconPathHelper.sqlMigrationLogo, + command: 'sqlmigration.start' + }; + + const preRequisiteListTitle = view.modelBuilder.text().withProps({ + value: loc.PRE_REQ_TITLE, + CSSStyles: { + 'font-size': '14px', + 'padding-left': '15px', + 'margin-bottom': '-5px' + } + }).component(); + + const migrateButton = this.createTaskButton(view, migrateButtonMetadata); + + const points = `• ${loc.PRE_REQ_1} +• ${loc.PRE_REQ_2} +• ${loc.PRE_REQ_3}`; + + const preRequisiteListElement = view.modelBuilder.text().withProps({ + value: points, + CSSStyles: { + 'padding-left': '15px' + } + }).component(); + + const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink().withProps({ + label: loc.LEARN_MORE, + url: '', //TODO: add link for the pre req document. + CSSStyles: { + 'padding-left': '10px' + } + }).component(); + + const preReqContainer = view.modelBuilder.flexContainer().withItems([ + preRequisiteListTitle, + preRequisiteListElement + ]).withLayout({ + flexFlow: 'column' + }).component(); + + preReqContainer.addItem(preRequisiteLearnMoreLink, { + CSSStyles: { + 'padding-left': '10px' + } + }); + + + tasksContainer.addItem(migrateButton, { + CSSStyles: { + 'margin-top': '20px', + 'padding': '10px' + } + }); + tasksContainer.addItems([preReqContainer], { + CSSStyles: { + 'padding': '10px' + } + }); + + return tasksContainer; + } + + private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component { + const maxHeight: number = 84; + const maxWidth: number = 236; + const buttonContainer = view.modelBuilder.button().withProps({ + buttonType: azdata.ButtonType.Informational, + description: taskMetaData.description, + height: maxHeight, + iconHeight: 32, + iconPath: taskMetaData.iconPath, + iconWidth: 32, + label: taskMetaData.title, + title: taskMetaData.title, + width: maxWidth, + }).component(); + buttonContainer.onDidClick(async () => { + if (taskMetaData.command) { + await vscode.commands.executeCommand(taskMetaData.command); + } + }); + return view.modelBuilder.divContainer().withItems([buttonContainer]).component(); + } + + private async refreshMigrations(): Promise { + this._migrationStatusCardsContainer.clearItems(); + const currentConnection = (await azdata.connection.getCurrentConnection()); + const getMigrations = MigrationLocalStorage.getMigrations(currentConnection); + getMigrations.forEach((migration) => { + const button = this._view.modelBuilder.button().withProps({ + label: `Migration to ${migration.targetManagedInstance.name} using controller ${migration.migrationContext.name}` + }).component(); + button.onDidClick(async (e) => { + }); + this._migrationStatusCardsContainer.addItem( + button + ); + }); + } + + private async createFooter(view: azdata.ModelView): Promise { + const footerContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + width: maxWidth, + height: '500px', + justifyContent: 'flex-start' + }).component(); + const statusContainer = await this.createMigrationStatusContainer(view); + const videoLinksContainer = this.createVideoLinks(view); + footerContainer.addItem(statusContainer); + footerContainer.addItem(videoLinksContainer, { + CSSStyles: { + 'padding-left': '10px', + } + }); + + return footerContainer; + } + + private async createMigrationStatusContainer(view: azdata.ModelView): Promise { + const statusContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: '400px', + height: '280px', + justifyContent: 'flex-start', + }).withProps({ + CSSStyles: { + 'border': '1px solid rgba(0, 0, 0, 0.1)', + 'padding': '15px' + } + }).component(); + + const statusContainerTitle = view.modelBuilder.text().withProps({ + value: loc.DATABASE_MIGRATION_STATUS, + CSSStyles: { + 'font-size': '18px', + 'font-weight': 'bold', + 'margin': '0px', + 'width': '290px' + } + }).component(); + + const viewAllButton = view.modelBuilder.hyperlink().withProps({ + label: loc.VIEW_ALL, + url: '' + }).component(); + + const refreshButton = view.modelBuilder.hyperlink().withProps({ + label: loc.REFRESH, + url: '', + CSSStyles: { + 'text-align': 'right' + } + }).component(); + + refreshButton.onDidClick((e) => { + this.refreshMigrations(); + }); + + const buttonContainer = view.modelBuilder.flexContainer().withLayout({ + justifyContent: 'flex-end', + }).component(); + + buttonContainer.addItem(viewAllButton, { + flex: 'auto', + CSSStyles: { + 'border-right': '1px solid rgba(0, 0, 0, 0.7)', + 'width': '40px', + } + }); + + buttonContainer.addItem(refreshButton, { + flex: 'auto', + CSSStyles: { + 'margin-left': '5px', + 'width': '25px' + } + }); + + const header = view.modelBuilder.flexContainer().withItems( + [ + statusContainerTitle, + buttonContainer + ] + ).withLayout({ + flexFlow: 'row' + }).component(); + + + + this._migrationStatusCardsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + + statusContainer.addItem( + header, { + CSSStyles: { + 'padding': '0px', + 'padding-right': '5px', + 'padding-top': '10px', + 'height': '10px', + 'margin': '0px' + } + } + ); + + statusContainer.addItem(this._migrationStatusCardsContainer, { + CSSStyles: { + 'margin-top': '30px' + } + }); + + return statusContainer; + } + + private createVideoLinks(view: azdata.ModelView): azdata.Component { + const linksContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: '400px', + height: '280px', + justifyContent: 'flex-start', + }).withProps({ + CSSStyles: { + 'border': '1px solid rgba(0, 0, 0, 0.1)', + 'padding': '15px' + } + }).component(); + const titleComponent = view.modelBuilder.text().withProps({ + value: loc.HELP_TITLE, + CSSStyles: { + 'font-size': '18px', + 'font-weight': 'bold', + 'margin': '0px' + } + }).component(); + + linksContainer.addItems([titleComponent], { + CSSStyles: { + 'padding': '0px', + 'padding-right': '5px', + 'padding-top': '10px', + 'height': '10px', + 'margin': '0px' + } + }); + + const links = [{ + title: loc.HELP_LINK1_TITLE, + description: loc.HELP_LINK1_DESCRIPTION, + link: 'www.microsoft.com' //TODO: add proper link over here. + }]; + + const styles = { + 'margin-top': '10px', + 'padding': '10px 10px 10px 0' + }; + linksContainer.addItems(links.map(l => this.createLink(view, l)), { + CSSStyles: styles + }); + + const videosContainer = this.createVideoLinkContainers(view, [ + { + iconPath: IconPathHelper.sqlMiImportHelpThumbnail, + description: loc.HELP_VIDEO1_TITLE, + link: 'https://www.youtube.com/watch?v=sE99cSoFOHs' //TODO: Fix Video link + }, + { + iconPath: IconPathHelper.sqlVmImportHelpThumbnail, + description: loc.HELP_VIDEO2_TITLE, + link: 'https://www.youtube.com/watch?v=R4GCBoxADyQ' //TODO: Fix video link + } + ]); + const viewPanelStyle = { + 'padding': '10px 5px 10px 10px', + 'margin-top': '-15px' + }; + linksContainer.addItem(videosContainer, { + CSSStyles: viewPanelStyle + }); + + return linksContainer; + } + + private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component { + const maxWidth = 400; + const labelsContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: maxWidth, + justifyContent: 'flex-start' + }).component(); + const descriptionComponent = view.modelBuilder.text().withProps({ + value: linkMetaData.description, + width: maxWidth, + CSSStyles: { + 'font-size': '12px', + 'line-height': '16px', + 'margin': '0px' + } + }).component(); + const linkContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + width: maxWidth + 10, + justifyContent: 'flex-start' + }).component(); + const linkComponent = view.modelBuilder.hyperlink().withProps({ + label: linkMetaData.title!, + url: linkMetaData.link!, + showLinkIcon: true, + CSSStyles: { + 'font-size': '14px', + 'margin': '0px' + } + }).component(); + linkContainer.addItem(linkComponent, { + CSSStyles: { + 'font-size': '14px', + 'line-height': '18px', + 'padding': '0 5px 0 0', + } + }); + labelsContainer.addItems([linkContainer, descriptionComponent], { + CSSStyles: { + 'padding': '5px 0 0 0', + } + }); + + return labelsContainer; + } + + private createVideoLinkContainers(view: azdata.ModelView, links: IActionMetadata[]): azdata.Component { + const maxWidth = 400; + const videosContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row', + width: maxWidth, + }).component(); + + links.forEach(link => { + const videoContainer = this.createVideoLink(view, link); + + videosContainer.addItem(videoContainer); + }); + + return videosContainer; + } + + private createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component { + const maxWidth = 150; + const videosContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: maxWidth, + justifyContent: 'flex-start' + }).component(); + const video1Container = view.modelBuilder.divContainer().withProps({ + clickable: true, + width: maxWidth, + height: '100px' + }).component(); + const descriptionComponent = view.modelBuilder.text().withProps({ + value: linkMetaData.description, + width: maxWidth, + height: '50px', + CSSStyles: { + 'font-size': '13px', + 'margin': '0px' + } + }).component(); + video1Container.onDidClick(async () => { + if (linkMetaData.link) { + await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link)); + } + }); + videosContainer.addItem(video1Container, { + CSSStyles: { + 'background-image': `url(${vscode.Uri.file(linkMetaData.iconPath?.light)})`, + 'background-repeat': 'no-repeat', + 'background-position': 'top', + 'width': `${maxWidth}px`, + 'height': '104px', + 'background-size': `${maxWidth}px 120px` + } + }); + videosContainer.addItem(descriptionComponent); + return videosContainer; + } +} diff --git a/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts b/extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts similarity index 97% rename from extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts rename to extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts index 07012cd9f7..708bc37b10 100644 --- a/extensions/sql-migration/src/wizard/createMigrationControllerDialog.ts +++ b/extensions/sql-migration/src/dialog/createMigrationDialog/createMigrationControllerDialog.ts @@ -5,13 +5,13 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../api/azure'; -import { MigrationStateModel } from '../models/stateMachine'; -import * as constants from '../models/strings'; +import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../../api/azure'; +import { MigrationStateModel } from '../../models/stateMachine'; +import * as constants from '../../models/strings'; import * as os from 'os'; import { azureResource } from 'azureResource'; -import { IntergrationRuntimePage } from './integrationRuntimePage'; -import { IconPathHelper } from '../constants/iconPathHelper'; +import { IntergrationRuntimePage } from '../../wizard/integrationRuntimePage'; +import { IconPathHelper } from '../../constants/iconPathHelper'; export class CreateMigrationControllerDialog { @@ -464,7 +464,7 @@ export class CreateMigrationControllerDialog { value: constants.CONTROLLER_KEY1_LABEL }, { - value: keys.keyName1 + value: keys.authKey1 }, { value: this._copyKey1Button @@ -478,7 +478,7 @@ export class CreateMigrationControllerDialog { value: constants.CONTROLLER_KEY2_LABEL }, { - value: keys.keyName2 + value: keys.authKey2 }, { value: this._copyKey2Button diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index 9a34d42995..be6a5e4a9e 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -11,12 +11,15 @@ import { promises as fs } from 'fs'; import * as loc from './models/strings'; import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper'; import { IconPathHelper } from './constants/iconPathHelper'; +import { DashboardWidget } from './dashboard/sqlServerDashboard'; +import { MigrationLocalStorage } from './models/migrationLocalStorage'; class SQLMigration { constructor(private readonly context: vscode.ExtensionContext) { NotebookPathHelper.setExtensionContext(context); IconPathHelper.setExtensionContext(context); + MigrationLocalStorage.setExtensionContext(context); } async start(): Promise { @@ -83,6 +86,8 @@ let sqlMigration: SQLMigration; export async function activate(context: vscode.ExtensionContext) { sqlMigration = new SQLMigration(context); await sqlMigration.registerCommands(); + let widget = new DashboardWidget(); + widget.register(); } export function deactivate(): void { diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index fd04fd6773..19db37a846 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -23,7 +23,7 @@ export class MigrationLocalStorage { const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; dataBaseMigrations = migrationMementos.filter((memento) => { - return memento.connection.serverName === connectionProfile.serverName; + return memento.sourceConnectionProfile.serverName === connectionProfile.serverName; }).map((memento) => { return memento; }); @@ -35,13 +35,13 @@ export class MigrationLocalStorage { return dataBaseMigrations; } - public static saveMigration(connection: azdata.connection.ConnectionProfile, migration: DatabaseMigration, targetMI: SqlManagedInstance, azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription): void { + public static saveMigration(connectionProfile: azdata.connection.ConnectionProfile, migrationContext: DatabaseMigration, targetMI: SqlManagedInstance, azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription): void { try { const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; migrationMementos.push({ - connection: connection, - migration: migration, - targetMI: targetMI, + sourceConnectionProfile: connectionProfile, + migrationContext: migrationContext, + targetManagedInstance: targetMI, subscription: subscription, azureAccount: azureAccount }); @@ -57,9 +57,9 @@ export class MigrationLocalStorage { } export interface MigrationContext { - connection: azdata.connection.ConnectionProfile, - migration: DatabaseMigration, - targetMI: SqlManagedInstance, + sourceConnectionProfile: azdata.connection.ConnectionProfile, + migrationContext: DatabaseMigration, + targetManagedInstance: SqlManagedInstance, azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index e8d03a4c3b..353d000b10 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -453,7 +453,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { Scope: this._targetManagedInstance.id } }; - console.log(requestBody); + const response = await startDatabaseMigration( this.azureAccount, this._targetSubscription, @@ -464,9 +464,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { requestBody ); - console.log(response); - if (!response.error) { - MigrationLocalStorage.saveMigration(currentConnection!, response, this._targetManagedInstance, this.azureAccount, this._targetSubscription); + if (response.status === 201) { + MigrationLocalStorage.saveMigration(currentConnection!, response.databaseMigration, this._targetManagedInstance, this.azureAccount, this._targetSubscription); } vscode.window.showInformationMessage(constants.MIGRATION_STARTED); diff --git a/extensions/sql-migration/src/models/strings.ts b/extensions/sql-migration/src/models/strings.ts index 5b4728115f..cfc007976a 100644 --- a/extensions/sql-migration/src/models/strings.ts +++ b/extensions/sql-migration/src/models/strings.ts @@ -149,7 +149,7 @@ export const CANCEL = localize('sql.migration.cancel', "Cancel"); export const TYPE = localize('sql.migration.type', "Type"); export const PATH = localize('sql.migration.path', "Path"); export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User Account"); - +export const VIEW_ALL = localize('sql.migration.view.all', "View All"); //Summary Page export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "Summary"); @@ -171,3 +171,20 @@ export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pic export const NOTEBOOK_INLINE_MIGRATION_TITLE = localize('sql.migration.inline.migration.notebook.title', "Inline migration"); export const NOTEBOOK_SQL_MIGRATION_ASSESSMENT_TITLE = localize('sql.migration.sql.assessment.notebook.title', "SQL migration assessment"); export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Error opening migration notebook"); + +// Dashboard +export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration"); +export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines."); +export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL"); +export const DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION = localize('sql.migration.dashboard.migrate.task.button.description', "Migrate SQL Server instance to Azure SQL."); +export const DATABASE_MIGRATION_STATUS = localize('sql.migration.database.migration.status', "Database Migration Status"); +export const HELP_VIDEO1_TITLE = localize('sql.migration.dashboard.video1.title', "Migrate to SQL Server to SQL Managed Instance"); +export const HELP_VIDEO2_TITLE = localize('sql.migration.dashboard.video2.title', "Migrate to SQL Server to SQL Virtual Machine"); +export const HELP_LINK1_TITLE = localize('sql.migration.dashboard.link1.title', "Migrating your SQL Server to cloud"); +export const HELP_LINK1_DESCRIPTION = localize('sql.migration.dashboard.link1.description', "Lorem ipsum dolor sit amet, consectetur adipi. Lorem ipsum dolor sit amet, consectetur adipi. Lorem ipsum."); +export const HELP_TITLE = localize('sql.migration.dashboard.help.title', "Help Articles and Video Links"); +export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you need before starting migration:"); +export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account details"); +export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine"); +export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details"); + diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 00d72d1620..ee5e718730 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; -import { CreateMigrationControllerDialog } from './createMigrationControllerDialog'; +import { CreateMigrationControllerDialog } from '../dialog/createMigrationDialog/createMigrationControllerDialog'; import * as constants from '../models/strings'; import * as os from 'os'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';