diff --git a/extensions/sql-migration/README.md b/extensions/sql-migration/README.md
index 0802965491..b8373cb408 100644
--- a/extensions/sql-migration/README.md
+++ b/extensions/sql-migration/README.md
@@ -1,5 +1,5 @@
# Azure SQL Migration
-The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment and migration experience that delivers the following capabilities:
+The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment, recommendation and migration experience that delivers the following capabilities:
- A responsive user interface that provides an easy-to-navigate step-by-step wizard to deliver an integrated assessment, Azure recommendation and migration experience.
- An enhanced assessment engine that can assess SQL Server instances and identify databases that are ready for migration to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines.
- SKU recommender to collect performance data from the source SQL Server instance to generate right-sized Azure SQL recommendation.
diff --git a/extensions/sql-migration/images/sqlMigrationService.svg b/extensions/sql-migration/images/sqlMigrationService.svg
new file mode 100644
index 0000000000..b719591f1a
--- /dev/null
+++ b/extensions/sql-migration/images/sqlMigrationService.svg
@@ -0,0 +1,32 @@
+
diff --git a/extensions/sql-migration/images/view.svg b/extensions/sql-migration/images/view.svg
new file mode 100644
index 0000000000..67438f588a
--- /dev/null
+++ b/extensions/sql-migration/images/view.svg
@@ -0,0 +1,3 @@
+
diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json
index 10d62e2f24..d22330dbf3 100644
--- a/extensions/sql-migration/package.json
+++ b/extensions/sql-migration/package.json
@@ -2,9 +2,9 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
- "version": "0.1.14",
+ "version": "1.0.0",
"publisher": "Microsoft",
- "preview": true,
+ "preview": false,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json
index e38506c24a..be46808da0 100644
--- a/extensions/sql-migration/package.nls.json
+++ b/extensions/sql-migration/package.nls.json
@@ -8,6 +8,7 @@
"start-migration-command": "Migrate to Azure SQL",
"send-feedback-command": "Feedback",
"new-support-request-command": "New support request",
+ "refresh-migrations-command": "Refresh",
"migration-context-menu-category": "Migration Context Menu",
"complete-cutover-menu": "Complete cutover",
"database-details-menu": "Database details",
diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts
index 315714c11c..8af861998e 100644
--- a/extensions/sql-migration/src/api/azure.ts
+++ b/extensions/sql-migration/src/api/azure.ts
@@ -142,10 +142,15 @@ export async function getBlobs(account: azdata.Account, subscription: Subscripti
return blobNames!;
}
-export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise {
+export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise {
+ const sqlMigrationServiceId = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}`;
+ return await getSqlMigrationServiceById(account, subscription, sqlMigrationServiceId);
+}
+
+export async function getSqlMigrationServiceById(account: azdata.Account, subscription: Subscription, sqlMigrationServiceId: string): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`);
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const path = encodeURI(`${sqlMigrationServiceId}?api-version=2022-01-30-preview`);
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
@@ -153,6 +158,20 @@ export async function getSqlMigrationService(account: azdata.Account, subscripti
return response.response.data;
}
+export async function getSqlMigrationServicesByResourceGroup(account: azdata.Account, subscription: Subscription, resouceGroupName: string): Promise {
+ const api = await getAzureCoreAPI();
+ const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resouceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`);
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+ sortResourceArrayByName(response.response.data.value);
+ response.response.data.value.forEach((sms: SqlMigrationService) => {
+ sms.properties.resourceGroup = getResourceGroupFromId(sms.id);
+ });
+ return response.response.data.value;
+}
+
export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription): Promise {
const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`);
@@ -169,7 +188,7 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript
export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`);
+ const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2022-01-30-preview`);
const requestBody = {
'location': regionName
};
@@ -181,7 +200,7 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
const maxRetry = 24;
let i = 0;
for (i = 0; i < maxRetry; i++) {
- const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true);
const creationStatus = asyncResponse.response.data.status;
if (creationStatus === ProvisioningState.Succeeded) {
break;
@@ -196,10 +215,10 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
return response.response.data;
}
-export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise {
+export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2020-09-01-preview`);
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2022-01-30-preview`);
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
@@ -209,15 +228,15 @@ export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, su
};
}
-export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string, sessionId: string): Promise {
+export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2020-09-01-preview`);
+ const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2022-01-30-preview`);
const requestBody = {
'location': regionName,
'keyName': keyName,
};
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, undefined, getSessionIdHeader(sessionId));
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
@@ -239,10 +258,10 @@ export async function getStorageAccountAccessKeys(account: azdata.Account, subsc
};
}
-export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string, sessionId: string): Promise {
+export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2020-09-01-preview`);
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2022-01-30-preview`);
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
@@ -251,7 +270,7 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`);
+ const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, undefined, getSessionIdHeader(sessionId));
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
@@ -264,70 +283,72 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti
};
}
-export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration, sessionId: string): Promise {
- if (!migration.id) {
- throw new Error('NullMigrationId');
- }
-
- const migrationOperationId = migration.properties?.migrationOperationId;
- if (migrationOperationId === undefined &&
- migration.properties.provisioningState === ProvisioningState.Failed) {
- return migration;
- }
+export async function getMigrationDetails(account: azdata.Account, subscription: Subscription, migrationId: string, migrationOperationId?: string): Promise {
const path = migrationOperationId === undefined
- ? encodeURI(`${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`)
- : encodeURI(`${migration.id}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2020-09-01-preview`);
+ ? encodeURI(`${migrationId}?$expand=MigrationStatusDetails&api-version=2022-01-30-preview`)
+ : encodeURI(`${migrationId}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2022-01-30-preview`);
const api = await getAzureCoreAPI();
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
- const migrationUpdate: DatabaseMigration = response.response.data;
- if (migration.properties) {
- migrationUpdate.properties.sourceDatabaseName = migration.properties.sourceDatabaseName;
- migrationUpdate.properties.backupConfiguration = migration.properties.backupConfiguration;
- migrationUpdate.properties.offlineConfiguration = migration.properties.offlineConfiguration;
- }
-
- return migrationUpdate;
+ return response.response.data;
}
-export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string, sessionId: string): Promise {
+export async function getServiceMigrations(account: azdata.Account, subscription: Subscription, resourceId: string): Promise {
+ const path = encodeURI(`${resourceId}/listMigrations?&api-version=2022-01-30-preview`);
const api = await getAzureCoreAPI();
- const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const response = await api.makeAzureRestRequest(
+ account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+
+ return response.response.data.value;
+}
+
+export async function getMigrationTargetInstance(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise {
+ const targetServerId = getMigrationTargetId(migration);
+ const path = encodeURI(`${targetServerId}?api-version=2021-11-01-preview`);
+ const api = await getAzureCoreAPI();
+ const response = await api.makeAzureRestRequest(
+ account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+
+ return response.response.data;
+
+
+ return {};
+}
+
+export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string): Promise {
+ const api = await getAzureCoreAPI();
+ const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
return response.response.data;
}
-export async function listMigrationsBySqlMigrationService(account: azdata.Account, subscription: Subscription, sqlMigrationService: SqlMigrationService, sessionId: string): Promise {
+export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`${sqlMigrationService.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`);
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const path = encodeURI(`${migration.id}/operations/${migration.properties.migrationOperationId}/cutover?api-version=2022-01-30-preview`);
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, undefined);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
return response.response.data.value;
}
-export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise {
+export async function stopMigration(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise {
const api = await getAzureCoreAPI();
- const path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?api-version=2020-09-01-preview`);
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId));
- if (response.errors.length > 0) {
- throw new Error(response.errors.toString());
- }
- return response.response.data.value;
-}
-
-export async function stopMigration(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise {
- const api = await getAzureCoreAPI();
- const path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cancel?api-version=2020-09-01-preview`);
- const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId));
+ const path = encodeURI(`${migration.id}/operations/${migration.properties.migrationOperationId}/cancel?api-version=2022-01-30-preview`);
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, undefined);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
@@ -354,6 +375,17 @@ export function sortResourceArrayByName(resourceArray: SortableAzureResources[])
});
}
+export function getMigrationTargetId(migration: DatabaseMigration): string {
+ // `${targetServerId}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2022-01-30-preview`
+ const paths = migration.id.split('/providers/Microsoft.DataMigration/', 1);
+ return paths[0];
+}
+
+export function getMigrationTargetName(migration: DatabaseMigration): string {
+ const targetServerId = getMigrationTargetId(migration);
+ return getResourceName(targetServerId);
+}
+
export function getResourceGroupFromId(id: string): string {
return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase();
}
@@ -452,6 +484,7 @@ export interface DatabaseMigration {
name: string;
type: string;
}
+
export interface DatabaseMigrationProperties {
scope: string;
provisioningState: 'Succeeded' | 'Failed' | 'Creating';
@@ -460,8 +493,8 @@ export interface DatabaseMigrationProperties {
migrationStatusDetails?: MigrationStatusDetails;
startedOn: string;
endedOn: string;
- sourceSqlConnection: SqlConnectionInfo;
sourceDatabaseName: string;
+ sourceServerName: string;
targetDatabaseCollation: string;
migrationService: string;
migrationOperationId: string;
@@ -469,6 +502,7 @@ export interface DatabaseMigrationProperties {
offlineConfiguration: OfflineConfiguration;
migrationFailureError: ErrorInfo;
}
+
export interface MigrationStatusDetails {
migrationState: string;
startedOn: string;
@@ -528,6 +562,7 @@ export interface BackupSetInfo {
export interface SourceLocation {
fileShare?: DatabaseMigrationFileShare;
azureBlob?: DatabaseMigrationAzureBlob;
+ fileStorageType: 'FileShare' | 'AzureBlob' | 'None';
}
export interface TargetLocation {
diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts
index 0f907f0539..ece16679a2 100644
--- a/extensions/sql-migration/src/api/utils.ts
+++ b/extensions/sql-migration/src/api/utils.ts
@@ -7,8 +7,9 @@ import { window, CategoryValue, DropDownComponent, IconPath } from 'azdata';
import { IconPathHelper } from '../constants/iconPathHelper';
import { DAYS, HRS, MINUTE, SEC } from '../constants/strings';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
-import { MigrationStatus, MigrationContext, ProvisioningState } from '../models/migrationLocalStorage';
+import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto';
+import { DatabaseMigration } from './azure';
export function deepClone(obj: T): T {
if (!obj || typeof obj !== 'object') {
@@ -89,40 +90,34 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date):
}
}
-export function filterMigrations(databaseMigrations: MigrationContext[], statusFilter: string, databaseNameFilter?: string): MigrationContext[] {
- let filteredMigration: MigrationContext[] = [];
+export function filterMigrations(databaseMigrations: DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): DatabaseMigration[] {
+ let filteredMigration: DatabaseMigration[] = [];
if (statusFilter === AdsMigrationStatus.ALL) {
filteredMigration = databaseMigrations;
} else if (statusFilter === AdsMigrationStatus.ONGOING) {
- filteredMigration = databaseMigrations.filter((value) => {
- const status = value.migrationContext.properties?.migrationStatus;
- const provisioning = value.migrationContext.properties?.provisioningState;
- return status === MigrationStatus.InProgress
- || status === MigrationStatus.Creating
- || provisioning === MigrationStatus.Creating;
- });
+ filteredMigration = databaseMigrations.filter(
+ value => {
+ const status = value.properties?.migrationStatus;
+ return status === MigrationStatus.InProgress
+ || status === MigrationStatus.Creating
+ || value.properties?.provisioningState === MigrationStatus.Creating;
+ });
} else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
- filteredMigration = databaseMigrations.filter((value) => {
- const status = value.migrationContext.properties?.migrationStatus;
- return status === MigrationStatus.Succeeded;
- });
+ filteredMigration = databaseMigrations.filter(
+ value => value.properties?.migrationStatus === MigrationStatus.Succeeded);
} else if (statusFilter === AdsMigrationStatus.FAILED) {
- filteredMigration = databaseMigrations.filter((value) => {
- const status = value.migrationContext.properties?.migrationStatus;
- const provisioning = value.migrationContext.properties?.provisioningState;
- return status === MigrationStatus.Failed
- || provisioning === ProvisioningState.Failed;
- });
+ filteredMigration = databaseMigrations.filter(
+ value =>
+ value.properties?.migrationStatus === MigrationStatus.Failed ||
+ value.properties?.provisioningState === ProvisioningState.Failed);
} else if (statusFilter === AdsMigrationStatus.COMPLETING) {
- filteredMigration = databaseMigrations.filter((value) => {
- const status = value.migrationContext.properties?.migrationStatus;
- return status === MigrationStatus.Completing;
- });
+ filteredMigration = databaseMigrations.filter(
+ value => value.properties?.migrationStatus === MigrationStatus.Completing);
}
if (databaseNameFilter) {
- filteredMigration = filteredMigration.filter((value) => {
- return value.migrationContext.name.toLowerCase().includes(databaseNameFilter.toLowerCase());
- });
+ const filter = databaseNameFilter.toLowerCase();
+ filteredMigration = filteredMigration.filter(
+ migration => migration.name?.toLowerCase().includes(filter));
}
return filteredMigration;
}
@@ -144,35 +139,36 @@ export function convertIsoTimeToLocalTime(isoTime: string): Date {
return new Date(isoDate.getTime() + (isoDate.getTimezoneOffset() * 60000));
}
-export type SupportedAutoRefreshIntervals = -1 | 15000 | 30000 | 60000 | 180000 | 300000;
-
export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?: string, useDisplayName: boolean = true): void {
- const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1;
- if (selectedIndex > -1) {
- selectDropDownIndex(dropDown, selectedIndex);
- } else {
- selectDropDownIndex(dropDown, 0);
+ if (dropDown.values && dropDown.values.length > 0) {
+ const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1;
+ if (selectedIndex > -1) {
+ selectDropDownIndex(dropDown, selectedIndex);
+ } else {
+ selectDropDownIndex(dropDown, 0);
+ }
}
}
export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void {
- if (index >= 0 && dropDown.values && index <= dropDown.values.length - 1) {
- const value = dropDown.values[index];
- dropDown.value = value as CategoryValue;
+ if (dropDown.values && dropDown.values.length > 0) {
+ if (index >= 0 && index <= dropDown.values.length - 1) {
+ dropDown.value = dropDown.values[index] as CategoryValue;
+ return;
+ }
}
+ dropDown.value = undefined;
}
export function findDropDownItemIndex(dropDown: DropDownComponent, value: string, useDisplayName: boolean = true): number {
- if (dropDown.values) {
- if (useDisplayName) {
- return dropDown.values.findIndex((v: any) =>
- (v as CategoryValue)?.displayName?.toLowerCase() === value?.toLowerCase());
- } else {
- return dropDown.values.findIndex((v: any) =>
- (v as CategoryValue)?.name?.toLowerCase() === value?.toLowerCase());
- }
+ if (value && dropDown.values && dropDown.values.length > 0) {
+ const searachValue = value?.toLowerCase();
+ return useDisplayName
+ ? dropDown.values.findIndex((v: any) =>
+ (v as CategoryValue)?.displayName?.toLowerCase() === searachValue)
+ : dropDown.values.findIndex((v: any) =>
+ (v as CategoryValue)?.name?.toLowerCase() === searachValue);
}
-
return -1;
}
diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts
index 9497e2fc65..1c20d22848 100644
--- a/extensions/sql-migration/src/constants/helper.ts
+++ b/extensions/sql-migration/src/constants/helper.ts
@@ -4,45 +4,70 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
-import { MigrationContext, MigrationStatus } from '../models/migrationLocalStorage';
-import { MigrationMode, MigrationTargetType } from '../models/stateMachine';
+import { DatabaseMigration } from '../api/azure';
+import { MigrationStatus } from '../models/migrationLocalStorage';
+import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine';
import * as loc from './strings';
export enum SQLTargetAssetType {
SQLMI = 'microsoft.sql/managedinstances',
SQLVM = 'Microsoft.SqlVirtualMachine/sqlVirtualMachines',
+ SQLDB = 'Microsoft.Sql/servers',
}
-export function getMigrationTargetType(migration: MigrationContext): string {
- switch (migration.targetManagedInstance.type) {
- case SQLTargetAssetType.SQLMI:
- return loc.SQL_MANAGED_INSTANCE;
- case SQLTargetAssetType.SQLVM:
- return loc.SQL_VIRTUAL_MACHINE;
- default:
- return '';
+export function getMigrationTargetType(migration: DatabaseMigration): string {
+ const id = migration.id?.toLowerCase();
+ if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
+ return loc.SQL_MANAGED_INSTANCE;
}
+ else if (id?.indexOf(SQLTargetAssetType.SQLVM.toLowerCase()) > -1) {
+ return loc.SQL_VIRTUAL_MACHINE;
+ }
+ else if (id?.indexOf(SQLTargetAssetType.SQLDB.toLowerCase()) > -1) {
+ return loc.SQL_DATABASE;
+ }
+ return '';
}
-export function getMigrationTargetTypeEnum(migration: MigrationContext): MigrationTargetType | undefined {
- switch (migration.targetManagedInstance.type) {
+export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
+ switch (migration.type) {
case SQLTargetAssetType.SQLMI:
return MigrationTargetType.SQLMI;
case SQLTargetAssetType.SQLVM:
return MigrationTargetType.SQLVM;
+ case SQLTargetAssetType.SQLDB:
+ return MigrationTargetType.SQLDB;
default:
return undefined;
}
}
-export function getMigrationMode(migration: MigrationContext): string {
- return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.ONLINE;
+export function getMigrationMode(migration: DatabaseMigration): string {
+ return isOfflineMigation(migration)
+ ? loc.OFFLINE
+ : loc.ONLINE;
}
-export function getMigrationModeEnum(migration: MigrationContext): MigrationMode {
- return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE;
+export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
+ return isOfflineMigation(migration)
+ ? MigrationMode.OFFLINE
+ : MigrationMode.ONLINE;
}
+export function isOfflineMigation(migration: DatabaseMigration): boolean {
+ return migration.properties.offlineConfiguration?.offline === true;
+}
+
+export function isBlobMigration(migration: DatabaseMigration): boolean {
+ return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
+}
+
+export function getMigrationStatus(migration: DatabaseMigration): string {
+ return migration.properties.migrationStatus
+ ?? migration.properties.provisioningState;
+}
+
+
export function canRetryMigration(status: string | undefined): boolean {
return status === undefined ||
status === MigrationStatus.Failed ||
@@ -50,15 +75,14 @@ export function canRetryMigration(status: string | undefined): boolean {
status === MigrationStatus.Canceled;
}
-
-const TABLE_CHECKBOX_INDEX = 0;
-const TABLE_DB_NAME_INDEX = 1;
export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] {
+ const TABLE_CHECKBOX_INDEX = 0;
+ const TABLE_DB_NAME_INDEX = 1;
const sourceDatabaseNames = selectedDbs?.map(dbName => dbName.toLocaleLowerCase()) || [];
if (sourceDatabaseNames?.length > 0) {
for (let i in databaseTableValues) {
const row = databaseTableValues[i];
- const dbName = (row[TABLE_DB_NAME_INDEX].value as string).toLocaleLowerCase();
+ const dbName = (row[TABLE_DB_NAME_INDEX].value as string)?.toLocaleLowerCase();
if (sourceDatabaseNames.indexOf(dbName) > -1) {
row[TABLE_CHECKBOX_INDEX].value = true;
}
diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts
index ddc9e4dae1..5b2d5d8b94 100644
--- a/extensions/sql-migration/src/constants/iconPathHelper.ts
+++ b/extensions/sql-migration/src/constants/iconPathHelper.ts
@@ -43,6 +43,8 @@ export class IconPathHelper {
public static edit: IconPath;
public static restartDataCollection: IconPath;
public static stop: IconPath;
+ public static view: IconPath;
+ public static sqlMigrationService: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -173,5 +175,13 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/stop.svg'),
dark: context.asAbsolutePath('images/stop.svg')
};
+ IconPathHelper.view = {
+ light: context.asAbsolutePath('images/view.svg'),
+ dark: context.asAbsolutePath('images/view.svg')
+ };
+ IconPathHelper.sqlMigrationService = {
+ light: context.asAbsolutePath('images/sqlMigrationService.svg'),
+ dark: context.asAbsolutePath('images/sqlMigrationService.svg'),
+ };
}
}
diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts
index 96e188832b..3ae9d46d99 100644
--- a/extensions/sql-migration/src/constants/strings.ts
+++ b/extensions/sql-migration/src/constants/strings.ts
@@ -65,7 +65,6 @@ export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL = localize('sql.migratio
export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.assessment.button.label', "Refresh assessment");
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target");
-
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance");
export const SKU_RECOMMENDATION_DB_CARD_TEXT = localize('sql.migration.sku.db.card.title', "Azure SQL Database");
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine");
@@ -299,6 +298,25 @@ export function MI_NOT_READY_ERROR(miName: string, state: string): string {
return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state);
}
+export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account");
+export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant");
+export const SELECT_A_SUBSCRIPTION = localize('sql.migration.select.service.select.a.subscription', "Select a subscription");
+export const SELECT_A_LOCATION = localize('sql.migration.select.service.select.a.location', "Select a location");
+export const SELECT_A_RESOURCE_GROUP = localize('sql.migration.select.service.select.a.resource.group', "Select a resource group");
+export const SELECT_A_SERVICE = localize('sql.migration.select.service.select.a.service', "Select a Database Migration Service");
+export const SELECT_ACCOUNT_ERROR = localize('sql.migration.select.service.select.account.error', "An error occurred while loading available Azure accounts.");
+export const SELECT_TENANT_ERROR = localize('sql.migration.select.service.select.tenant.error', "An error occurred while loading available Azure account tenants.");
+export const SELECT_SUBSCRIPTION_ERROR = localize('sql.migration.select.service.select.subscription.error', "An error occurred while loading account subscriptions. Please check your Azure connection and try again.");
+export const SELECT_LOCATION_ERROR = localize('sql.migration.select.service.select.location.error', "An error occurred while loading locations. Please check your Azure connection and try again.");
+export const SELECT_RESOURCE_GROUP_ERROR = localize('sql.migration.select.service.select.resource.group.error', "An error occurred while loading available resource groups. Please check your Azure connection and try again.");
+export const SELECT_SERVICE_ERROR = localize('sql.migration.select.service.select.service.error', "An error occurred while loading available database migration services. Please check your Azure connection and try again.");
+export function ACCOUNT_CREDENTIALS_REFRESH(accountName: string): string {
+ return localize(
+ 'sql.migration.account.credentials.refresh.required',
+ "{0} (requires credentials refresh)",
+ accountName);
+}
+
// database backup page
export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup");
export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location of the database backups to use during migration.");
@@ -453,7 +471,7 @@ export const CREATE = localize('sql.migration.create', "Create");
export const CANCEL = localize('sql.migration.cancel', "Cancel");
export const TYPE = localize('sql.migration.type', "Type");
export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User account");
-export const VIEW_ALL = localize('sql.migration.view.all', "View all");
+export const VIEW_ALL = localize('sql.migration.view.all', "All database migrations");
export const TARGET = localize('sql.migration.target', "Target");
export const AZURE_SQL = localize('sql.migration.azure.sql', "Azure SQL");
export const CLOSE = localize('sql.migration.close', "Close");
@@ -494,6 +512,9 @@ export const NOTEBOOK_SQL_MIGRATION_ASSESSMENT_TITLE = localize('sql.migration.s
export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook.");
// Dashboard
+export function DASHBOARD_REFRESH_MIGRATIONS(error: string): string {
+ return localize('sql.migration.refresh.migrations.error', "An error occurred while refreshing the migrations list: '{0}'. Please check your linked Azure connection and click refresh to try again.", error);
+}
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");
@@ -505,10 +526,9 @@ export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account deta
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");
export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migrations in progress");
-export const MIGRATION_FAILED = localize('sql.migration.failed', "Migrations failed");
-export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migrations completed");
-export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Completing cutover");
-export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migrations not started");
+export const MIGRATION_FAILED = localize('sql.migration.failed', "Database migrations failed");
+export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migrations completed");
+export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Database migrations completing cutover");
export const SHOW_STATUS = localize('sql.migration.show.status', "Show status");
export function MIGRATION_INPROGRESS_WARNING(count: number) {
switch (count) {
@@ -593,6 +613,7 @@ export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "
//Migration status dialog
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account");
export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status.");
+export const SELECT_SERVICE_MESSAGE = localize('sql.migration.status.select.service.MESSAGE', "Select a Database Migration Service to monitor migrations.");
export const STATUS_ALL = localize('sql.migration.status.dropdown.all', "Status: All");
export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing', "Status: Ongoing");
export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing");
@@ -602,11 +623,13 @@ export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migratio
export const ONLINE = localize('sql.migration.online', "Online");
export const OFFLINE = localize('sql.migration.offline', "Offline");
export const DATABASE = localize('sql.migration.database', "Database");
+export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status");
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
export const DURATION = localize('sql.migration.duration', "Duration");
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance', "SQL Managed Instance");
export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine");
+export const SQL_DATABASE = localize('sql.migration.sql.database', "SQL Database");
export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target name");
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode");
export const START_TIME = localize('sql.migration.start.time', "Start time");
@@ -745,3 +768,15 @@ export const MIGRATION_RETRY_ERROR = localize('sql.migration.retry.migration.err
export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')');
export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.');
+
+// Migration Service Section Dialog
+export const MIGRATION_SERVICE_SELECT_TITLE = localize('sql.migration.select.service.title', 'Select Database Migration Service');
+export const MIGRATION_SERVICE_SELECT_APPLY_LABEL = localize('sql.migration.select.service.apply.label', 'Apply');
+export const MIGRATION_SERVICE_CLEAR = localize('sql.migration.select.service.delete.label', 'Clear');
+export const MIGRATION_SERVICE_SELECT_HEADING = localize('sql.migration.select.service.heading', 'Filter the migration list by Database Migration Service');
+export const MIGRATION_SERVICE_SELECT_SERVICE_LABEL = localize('sql.migration.select.service.service.label', 'Azure Database Migration Service');
+export const MIGRATION_SERVICE_SELECT_SERVICE_PROMPT = localize('sql.migration.select.service.prompt', 'Select a Database Migration Service');
+export function MIGRATION_SERVICE_SERVICE_PROMPT(serviceName: string): string {
+ return localize('sql.migration.service.prompt', '{0} (change)', serviceName);
+}
+export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.service.description', 'Azure Database Migration Service');
diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
index 70758528a3..20507936f0 100644
--- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
+++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts
@@ -5,15 +5,17 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
-import { MigrationContext, MigrationLocalStorage } from '../models/migrationLocalStorage';
import { logError, TelemetryViews } from '../telemtery';
import * as loc from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
-import { filterMigrations, SupportedAutoRefreshIntervals } from '../api/utils';
+import { filterMigrations } from '../api/utils';
import * as styles from '../constants/styles';
import * as nls from 'vscode-nls';
+import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
+import { DatabaseMigration } from '../api/azure';
+import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
const localize = nls.loadMessageBundle();
interface IActionMetadata {
@@ -25,7 +27,12 @@ interface IActionMetadata {
}
const maxWidth = 800;
-const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
+const BUTTON_CSS = {
+ 'font-size': '13px',
+ 'line-height': '18px',
+ 'margin': '4px 0',
+ 'text-align': 'left',
+};
interface StatusCard {
container: azdata.DivContainer;
@@ -37,39 +44,33 @@ interface StatusCard {
export class DashboardWidget {
private _context: vscode.ExtensionContext;
-
private _migrationStatusCardsContainer!: azdata.FlexContainer;
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
private _view!: azdata.ModelView;
-
private _inProgressMigrationButton!: StatusCard;
private _inProgressWarningMigrationButton!: StatusCard;
+ private _allMigrationButton!: StatusCard;
private _successfulMigrationButton!: StatusCard;
private _failedMigrationButton!: StatusCard;
private _completingMigrationButton!: StatusCard;
- private _notStartedMigrationCard!: StatusCard;
- private _migrationStatusMap: Map = new Map();
- private _viewAllMigrationsButton!: azdata.ButtonComponent;
+ private _selectServiceText!: azdata.TextComponent;
+ private _serviceContextButton!: azdata.ButtonComponent;
+ private _refreshButton!: azdata.ButtonComponent;
- private _autoRefreshHandle!: NodeJS.Timeout;
private _disposables: vscode.Disposable[] = [];
-
private isRefreshing: boolean = false;
+ public onDialogClosed = async (): Promise => {
+ const label = await getSelectedServiceStatus();
+ this._serviceContextButton.label = label;
+ this._serviceContextButton.title = label;
+ await this.refreshMigrations();
+ };
+
constructor(context: vscode.ExtensionContext) {
this._context = context;
}
- private async getCurrentMigrations(): Promise {
- const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
- return this._migrationStatusMap.get(connectionId)!;
- }
-
- private async setCurrentMigrations(migrations: MigrationContext[]): Promise {
- const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
- this._migrationStatusMap.set(connectionId, migrations);
- }
-
public register(): void {
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
this._view = view;
@@ -82,7 +83,10 @@ export class DashboardWidget {
const header = this.createHeader(view);
// Files need to have the vscode-file scheme to be loaded by ADS
- const watermarkUri = vscode.Uri.file(IconPathHelper.migrationDashboardHeaderBackground.light).with({ scheme: 'vscode-file' });
+ const watermarkUri = vscode.Uri
+ .file(IconPathHelper.migrationDashboardHeaderBackground.light)
+ .with({ scheme: 'vscode-file' });
+
container.addItem(header, {
CSSStyles: {
'background-image': `
@@ -107,11 +111,11 @@ export class DashboardWidget {
'margin': '0 24px'
}
});
- this._disposables.push(this._view.onClosed(e => {
- clearInterval(this._autoRefreshHandle);
- this._disposables.forEach(
- d => { try { d.dispose(); } catch { } });
- }));
+ this._disposables.push(
+ this._view.onClosed(e => {
+ this._disposables.forEach(
+ d => { try { d.dispose(); } catch { } });
+ }));
await view.initializeModel(container);
await this.refreshMigrations();
@@ -119,8 +123,6 @@ export class DashboardWidget {
}
private createHeader(view: azdata.ModelView): azdata.FlexContainer {
- this.setAutoRefresh(refreshFrequency);
-
const header = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
@@ -229,95 +231,71 @@ export class DashboardWidget {
'transition': 'all .5s ease',
}
}).component();
- this._disposables.push(buttonContainer.onDidClick(async () => {
- if (taskMetaData.command) {
- await vscode.commands.executeCommand(taskMetaData.command);
- }
- }));
+ this._disposables.push(
+ buttonContainer.onDidClick(async () => {
+ if (taskMetaData.command) {
+ await vscode.commands.executeCommand(taskMetaData.command);
+ }
+ }));
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
}
- private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
- const classVariable = this;
- clearInterval(this._autoRefreshHandle);
- if (interval !== -1) {
- this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshMigrations(); }, interval);
- }
- }
-
- private async refreshMigrations(): Promise {
+ public async refreshMigrations(): Promise {
if (this.isRefreshing) {
return;
}
this.isRefreshing = true;
- this._viewAllMigrationsButton.enabled = false;
this._migrationStatusCardLoadingContainer.loading = true;
+ let migrations: DatabaseMigration[] = [];
try {
- await this.setCurrentMigrations(await this.getMigrations());
- const migrations = await this.getCurrentMigrations();
- const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
- let warningCount = 0;
- for (let i = 0; i < inProgressMigrations.length; i++) {
- if (
- inProgressMigrations[i].asyncOperationResult?.error?.message ||
- inProgressMigrations[i].migrationContext.properties.migrationFailureError?.message ||
- inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors ||
- inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.restoreBlockingReason
- ) {
- warningCount += 1;
- }
- }
- if (warningCount > 0) {
- this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
- this._inProgressMigrationButton.container.display = 'none';
- this._inProgressWarningMigrationButton.container.display = '';
- } else {
- this._inProgressMigrationButton.container.display = '';
- this._inProgressWarningMigrationButton.container.display = 'none';
- }
-
- this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
- this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
-
- const successfulMigration = filterMigrations(migrations, AdsMigrationStatus.SUCCEEDED);
-
- this._successfulMigrationButton.count.value = successfulMigration.length.toString();
-
- const failedMigrations = filterMigrations(migrations, AdsMigrationStatus.FAILED);
- const failedCount = failedMigrations.length;
- if (failedCount > 0) {
- this._failedMigrationButton.container.display = '';
- this._failedMigrationButton.count.value = failedCount.toString();
- } else {
- this._failedMigrationButton.container.display = 'none';
- }
-
- const completingCutoverMigrations = filterMigrations(migrations, AdsMigrationStatus.COMPLETING);
- const cutoverCount = completingCutoverMigrations.length;
- if (cutoverCount > 0) {
- this._completingMigrationButton.container.display = '';
- this._completingMigrationButton.count.value = cutoverCount.toString();
- } else {
- this._completingMigrationButton.container.display = 'none';
- }
-
- } catch (error) {
- logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', error);
-
- } finally {
- this.isRefreshing = false;
- this._migrationStatusCardLoadingContainer.loading = false;
- this._viewAllMigrationsButton.enabled = true;
+ migrations = await getCurrentMigrations();
+ } catch (e) {
+ logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
+ void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
}
+ const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
+ let warningCount = 0;
+ for (let i = 0; i < inProgressMigrations.length; i++) {
+ if (inProgressMigrations[i].properties.migrationFailureError?.message ||
+ inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
+ inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
+ warningCount += 1;
+ }
+ }
+ if (warningCount > 0) {
+ this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
+ this._inProgressMigrationButton.container.display = 'none';
+ this._inProgressWarningMigrationButton.container.display = '';
+ } else {
+ this._inProgressMigrationButton.container.display = '';
+ this._inProgressWarningMigrationButton.container.display = 'none';
+ }
+
+ this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
+ this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
+
+ this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
+ this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
+ this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
+ this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
+
+ await this._updateSummaryStatus();
+ this.isRefreshing = false;
+ this._migrationStatusCardLoadingContainer.loading = false;
}
- private async getMigrations(): Promise {
- const currentConnection = (await azdata.connection.getCurrentConnection());
- return await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
+ private _updateStatusCard(
+ migrations: DatabaseMigration[],
+ card: StatusCard,
+ status: AdsMigrationStatus,
+ show?: boolean): void {
+ const list = filterMigrations(migrations, status);
+ const count = list?.length || 0;
+ card.container.display = count > 0 || show ? '' : 'none';
+ card.count.value = count.toString();
}
-
private createStatusCard(
cardIconPath: IconPath,
cardTitle: string,
@@ -334,26 +312,27 @@ export class DashboardWidget {
}
}).component();
- const statusIcon = this._view.modelBuilder.image().withProps({
- iconPath: cardIconPath!.light,
- iconHeight: 24,
- iconWidth: 24,
- height: 32,
- CSSStyles: {
- 'margin': '0 8px'
- }
- }).component();
+ const statusIcon = this._view.modelBuilder.image()
+ .withProps({
+ iconPath: cardIconPath!.light,
+ iconHeight: 24,
+ iconWidth: 24,
+ height: 32,
+ CSSStyles: { 'margin': '0 8px' }
+ }).component();
- const textContainer = this._view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'column'
- }).component();
+ const textContainer = this._view.modelBuilder.flexContainer()
+ .withLayout({ flexFlow: 'column' })
+ .component();
- const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({
- CSSStyles: {
- ...styles.SECTION_HEADER_CSS,
- 'width': '240px'
- }
- }).component();
+ const cardTitleText = this._view.modelBuilder.text()
+ .withProps({ value: cardTitle })
+ .withProps({
+ CSSStyles: {
+ ...styles.SECTION_HEADER_CSS,
+ 'width': '240px',
+ }
+ }).component();
textContainer.addItem(cardTitleText);
const cardCount = this._view.modelBuilder.text().withProps({
@@ -368,32 +347,31 @@ export class DashboardWidget {
let warningContainer;
let warningText;
if (hasSubtext) {
- const warningIcon = this._view.modelBuilder.image().withProps({
- iconPath: IconPathHelper.warning,
- iconWidth: 12,
- iconHeight: 12,
- width: 12,
- height: 18
- }).component();
+ const warningIcon = this._view.modelBuilder.image()
+ .withProps({
+ iconPath: IconPathHelper.warning,
+ iconWidth: 12,
+ iconHeight: 12,
+ width: 12,
+ height: 18,
+ }).component();
const warningDescription = '';
- warningText = this._view.modelBuilder.text().withProps({ value: warningDescription }).withProps({
- CSSStyles: {
- ...styles.BODY_CSS,
- 'padding-left': '8px',
- }
- }).component();
+ warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
+ .withProps({
+ CSSStyles: {
+ ...styles.BODY_CSS,
+ 'padding-left': '8px',
+ }
+ }).component();
- warningContainer = this._view.modelBuilder.flexContainer().withItems([
- warningIcon,
- warningText
- ], {
- flex: '0 0 auto'
- }).withProps({
- CSSStyles: {
- 'align-items': 'center'
- }
- }).component();
+ warningContainer = this._view.modelBuilder.flexContainer()
+ .withItems(
+ [warningIcon, warningText],
+ { flex: '0 0 auto' })
+ .withProps({
+ CSSStyles: { 'align-items': 'center' }
+ }).component();
textContainer.addItem(warningContainer);
}
@@ -452,255 +430,243 @@ export class DashboardWidget {
const statusContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '400px',
- height: '360px',
+ height: '385px',
justifyContent: 'flex-start',
}).withProps({
CSSStyles: {
'border': '1px solid rgba(0, 0, 0, 0.1)',
- 'padding': '16px'
+ 'padding': '10px',
}
}).component();
- const statusContainerTitle = view.modelBuilder.text().withProps({
- value: loc.DATABASE_MIGRATION_STATUS,
- CSSStyles: {
- ...styles.SECTION_HEADER_CSS
- }
- }).component();
+ const statusContainerTitle = view.modelBuilder.text()
+ .withProps({
+ value: loc.DATABASE_MIGRATION_STATUS,
+ width: '100%',
+ CSSStyles: { ...styles.SECTION_HEADER_CSS }
+ }).component();
- this._viewAllMigrationsButton = view.modelBuilder.hyperlink().withProps({
- label: loc.VIEW_ALL,
- url: '',
- CSSStyles: {
- ...styles.BODY_CSS
- }
- }).component();
+ this._refreshButton = view.modelBuilder.button()
+ .withProps({
+ label: loc.REFRESH,
+ iconPath: IconPathHelper.refresh,
+ iconHeight: 16,
+ iconWidth: 16,
+ width: 70,
+ CSSStyles: { 'float': 'right' }
+ }).component();
- this._disposables.push(this._viewAllMigrationsButton.onDidClick(async (e) => {
- const migrationStatus = await this.getCurrentMigrations();
- new MigrationStatusDialog(this._context, migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize();
- }));
-
- const refreshButton = view.modelBuilder.hyperlink().withProps({
- label: loc.REFRESH,
- url: '',
- ariaRole: 'button',
- CSSStyles: {
- ...styles.BODY_CSS,
- 'text-align': 'right',
- }
- }).component();
-
- this._disposables.push(refreshButton.onDidClick(async (e) => {
- refreshButton.enabled = false;
- await this.refreshMigrations();
- refreshButton.enabled = true;
- }));
-
- const buttonContainer = view.modelBuilder.flexContainer().withLayout({
- justifyContent: 'flex-end',
- }).component();
-
- buttonContainer.addItem(this._viewAllMigrationsButton, {
- CSSStyles: {
- 'padding-right': '8px',
- 'border-right': '1px solid',
- }
- });
-
- buttonContainer.addItem(refreshButton, {
- CSSStyles: {
- 'padding-left': '8px',
- }
- });
-
- const addAccountImage = view.modelBuilder.image().withProps({
- iconPath: IconPathHelper.addAzureAccount,
- iconHeight: 100,
- iconWidth: 100,
- width: 96,
- height: 96,
- CSSStyles: {
- 'opacity': '50%',
- 'margin': '15% auto 10% auto',
- 'filter': 'drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25))',
- 'display': 'none'
- }
- }).component();
-
- const addAccountText = view.modelBuilder.text().withProps({
- value: loc.ADD_ACCOUNT_MESSAGE,
- width: 198,
- height: 34,
- CSSStyles: {
- ...styles.NOTE_CSS,
- 'margin': 'auto',
- 'text-align': 'center',
- 'display': 'none'
- }
- }).component();
-
- const addAccountButton = view.modelBuilder.button().withProps({
- label: loc.ADD_ACCOUNT,
- width: '100px',
- enabled: true,
- CSSStyles: {
- 'margin': '5% 40%',
- 'display': 'none'
- }
- }).component();
-
- this._disposables.push(addAccountButton.onDidClick(async (e) => {
- await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
- addAccountButton.enabled = false;
- let accounts = await azdata.accounts.getAllAccounts();
-
- if (accounts.length !== 0) {
- await addAccountImage.updateCssStyles({
- 'display': 'none'
- });
- await addAccountText.updateCssStyles({
- 'display': 'none'
- });
- await addAccountButton.updateCssStyles({
- 'display': 'none'
- });
- await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'visible' });
- await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'visible' });
- }
- await this.refreshMigrations();
- }));
-
- const header = view.modelBuilder.flexContainer().withItems(
- [
+ const statusHeadingContainer = view.modelBuilder.flexContainer()
+ .withItems([
statusContainerTitle,
- buttonContainer
- ]
- ).withLayout({
- flexFlow: 'row',
- alignItems: 'center'
- }).component();
+ this._refreshButton,
+ ]).withLayout({
+ alignContent: 'center',
+ alignItems: 'center',
+ flexFlow: 'row',
+ }).component();
- this._migrationStatusCardsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
+ this._disposables.push(
+ this._refreshButton.onDidClick(async (e) => {
+ this._refreshButton.enabled = false;
+ await this.refreshMigrations();
+ this._refreshButton.enabled = true;
+ }));
- let accounts = await azdata.accounts.getAllAccounts();
+ const buttonContainer = view.modelBuilder.flexContainer()
+ .withProps({
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-iems': 'center',
+ },
+ })
+ .component();
- if (accounts.length === 0) {
- await addAccountImage.updateCssStyles({
- 'display': 'block'
- });
- await addAccountText.updateCssStyles({
- 'display': 'block'
- });
- await addAccountButton.updateCssStyles({
- 'display': 'block'
- });
- await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'hidden' });
- await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'hidden' });
- }
+ buttonContainer.addItem(
+ await this.createServiceSelector(this._view));
+ this._selectServiceText = view.modelBuilder.text()
+ .withProps({
+ value: loc.SELECT_SERVICE_MESSAGE,
+ CSSStyles: {
+ 'font-size': '12px',
+ 'margin': '10px',
+ 'font-weight': '350',
+ 'text-align': 'center',
+ 'display': 'none'
+ }
+ }).component();
+
+ const header = view.modelBuilder.flexContainer()
+ .withItems([statusHeadingContainer, buttonContainer])
+ .withLayout({ flexFlow: 'column', })
+ .component();
+
+ this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ height: '272px',
+ })
+ .withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
+ .component();
+
+ await this._updateSummaryStatus();
+
+ // in progress
this._inProgressMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration,
- loc.MIGRATION_IN_PROGRESS
- );
- this._disposables.push(this._inProgressMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
- dialog.initialize();
- }));
+ loc.MIGRATION_IN_PROGRESS);
+ this._disposables.push(
+ this._inProgressMigrationButton.container.onDidClick(async (e) => {
+ const dialog = new MigrationStatusDialog(
+ this._context,
+ AdsMigrationStatus.ONGOING,
+ this.onDialogClosed);
+ await dialog.initialize();
+ }));
this._migrationStatusCardsContainer.addItem(
- this._inProgressMigrationButton.container
- );
+ this._inProgressMigrationButton.container,
+ { flex: '0 0 auto' });
+ // in progress warning
this._inProgressWarningMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS,
- true
- );
- this._disposables.push(this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
- dialog.initialize();
- }));
+ true);
+ this._disposables.push(
+ this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
+ const dialog = new MigrationStatusDialog(
+ this._context,
+ AdsMigrationStatus.ONGOING,
+ this.onDialogClosed);
+ await dialog.initialize();
+ }));
this._migrationStatusCardsContainer.addItem(
- this._inProgressWarningMigrationButton.container
- );
+ this._inProgressWarningMigrationButton.container,
+ { flex: '0 0 auto' });
+ // successful
this._successfulMigrationButton = this.createStatusCard(
IconPathHelper.completedMigration,
- loc.MIGRATION_COMPLETED
- );
- this._disposables.push(this._successfulMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED);
- dialog.initialize();
- }));
+ loc.MIGRATION_COMPLETED);
+ this._disposables.push(
+ this._successfulMigrationButton.container.onDidClick(async (e) => {
+ const dialog = new MigrationStatusDialog(
+ this._context,
+ AdsMigrationStatus.SUCCEEDED,
+ this.onDialogClosed);
+ await dialog.initialize();
+ }));
this._migrationStatusCardsContainer.addItem(
- this._successfulMigrationButton.container
- );
-
+ this._successfulMigrationButton.container,
+ { flex: '0 0 auto' });
+ // completing
this._completingMigrationButton = this.createStatusCard(
IconPathHelper.completingCutover,
- loc.MIGRATION_CUTOVER_CARD
- );
- this._disposables.push(this._completingMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING);
- dialog.initialize();
- }));
+ loc.MIGRATION_CUTOVER_CARD);
+ this._disposables.push(
+ this._completingMigrationButton.container.onDidClick(async (e) => {
+ const dialog = new MigrationStatusDialog(
+ this._context,
+ AdsMigrationStatus.COMPLETING,
+ this.onDialogClosed);
+ await dialog.initialize();
+ }));
this._migrationStatusCardsContainer.addItem(
- this._completingMigrationButton.container
- );
+ this._completingMigrationButton.container,
+ { flex: '0 0 auto' });
+ // failed
this._failedMigrationButton = this.createStatusCard(
IconPathHelper.error,
- loc.MIGRATION_FAILED
- );
- this._disposables.push(this._failedMigrationButton.container.onDidClick(async (e) => {
- const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.FAILED);
- dialog.initialize();
- }));
+ loc.MIGRATION_FAILED);
+ this._disposables.push(
+ this._failedMigrationButton.container.onDidClick(async (e) => {
+ const dialog = new MigrationStatusDialog(
+ this._context,
+ AdsMigrationStatus.FAILED,
+ this.onDialogClosed);
+ await dialog.initialize();
+ }));
this._migrationStatusCardsContainer.addItem(
- this._failedMigrationButton.container
- );
+ this._failedMigrationButton.container,
+ { flex: '0 0 auto' });
- this._notStartedMigrationCard = this.createStatusCard(
- IconPathHelper.notStartedMigration,
- loc.MIGRATION_NOT_STARTED
- );
- this._disposables.push(this._notStartedMigrationCard.container.onDidClick((e) => {
- void vscode.window.showInformationMessage('Feature coming soon');
- }));
+ // all migrations
+ this._allMigrationButton = this.createStatusCard(
+ IconPathHelper.view,
+ loc.VIEW_ALL);
+ this._disposables.push(
+ this._allMigrationButton.container.onDidClick(async (e) => {
+ const dialog = new MigrationStatusDialog(
+ this._context,
+ AdsMigrationStatus.ALL,
+ this.onDialogClosed);
+ await dialog.initialize();
+ }));
+ this._migrationStatusCardsContainer.addItem(
+ this._allMigrationButton.container,
+ { flex: '0 0 auto' });
- this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent().withItem(this._migrationStatusCardsContainer).component();
-
- statusContainer.addItem(
- header, {
- CSSStyles: {
- 'margin-bottom': '16px'
- }
- }
- );
-
- statusContainer.addItem(addAccountImage, {});
- statusContainer.addItem(addAccountText, {});
- statusContainer.addItem(addAccountButton, {});
+ this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
+ .withItem(this._migrationStatusCardsContainer)
+ .component();
+ statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
+ statusContainer.addItem(this._selectServiceText, {});
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
return statusContainer;
}
+ private async _updateSummaryStatus(): Promise {
+ const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
+ const isContextValid = isServiceContextValid(serviceContext);
+ await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
+ await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
+ this._refreshButton.enabled = isContextValid;
+ }
+
+ private async createServiceSelector(view: azdata.ModelView): Promise {
+ const serviceContextLabel = await getSelectedServiceStatus();
+ this._serviceContextButton = view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.sqlMigrationService,
+ iconHeight: 22,
+ iconWidth: 22,
+ label: serviceContextLabel,
+ title: serviceContextLabel,
+ description: loc.MIGRATION_SERVICE_DESCRIPTION,
+ buttonType: azdata.ButtonType.Informational,
+ width: 375,
+ CSSStyles: { ...BUTTON_CSS },
+ })
+ .component();
+
+ this._disposables.push(
+ this._serviceContextButton.onDidClick(async () => {
+ const dialog = new SelectMigrationServiceDialog(this.onDialogClosed);
+ await dialog.initialize();
+ }));
+
+ return this._serviceContextButton;
+ }
+
private createVideoLinks(view: azdata.ModelView): azdata.Component {
- const linksContainer = view.modelBuilder.flexContainer().withLayout({
- flexFlow: 'column',
- width: '400px',
- height: '360px',
- justifyContent: 'flex-start',
- }).withProps({
- CSSStyles: {
- 'border': '1px solid rgba(0, 0, 0, 0.1)',
- 'padding': '16px',
- 'overflow': 'scroll',
- }
- }).component();
+ const linksContainer = view.modelBuilder.flexContainer()
+ .withLayout({
+ flexFlow: 'column',
+ width: '440px',
+ height: '385px',
+ justifyContent: 'flex-start',
+ }).withProps({
+ CSSStyles: {
+ 'border': '1px solid rgba(0, 0, 0, 0.1)',
+ 'padding': '10px',
+ 'overflow': 'scroll',
+ }
+ }).component();
const titleComponent = view.modelBuilder.text().withProps({
value: loc.HELP_TITLE,
CSSStyles: {
@@ -809,11 +775,12 @@ export class DashboardWidget {
...styles.BODY_CSS
}
}).component();
- this._disposables.push(video1Container.onDidClick(async () => {
- if (linkMetaData.link) {
- await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
- }
- }));
+ this._disposables.push(
+ 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)})`,
diff --git a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts
index 65a3697837..5ac98026c9 100644
--- a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts
+++ b/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts
@@ -22,7 +22,10 @@ export class SavedAssessmentDialog {
private context: vscode.ExtensionContext;
private _disposables: vscode.Disposable[] = [];
- constructor(context: vscode.ExtensionContext, stateModel: MigrationStateModel) {
+ constructor(
+ context: vscode.ExtensionContext,
+ stateModel: MigrationStateModel,
+ private readonly _onClosedCallback: () => Promise) {
this.stateModel = stateModel;
this.context = context;
}
@@ -53,7 +56,7 @@ export class SavedAssessmentDialog {
dialog.registerCloseValidator(async () => {
if (this.stateModel.resumeAssessment) {
- if (!this.stateModel.loadSavedInfo()) {
+ if (await !this.stateModel.loadSavedInfo()) {
void vscode.window.showInformationMessage(constants.OPEN_SAVED_INFO_ERROR);
return false;
}
@@ -77,7 +80,11 @@ export class SavedAssessmentDialog {
}
protected async execute() {
- const wizardController = new WizardController(this.context, this.stateModel);
+ const wizardController = new WizardController(
+ this.context,
+ this.stateModel,
+ this._onClosedCallback);
+
await wizardController.openWizard(this.stateModel.sourceConnectionId);
this._isOpen = false;
}
@@ -103,11 +110,11 @@ export class SavedAssessmentDialog {
checked: true
}).component();
- radioStart.onDidChangeCheckedState((e) => {
+ this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = false;
}
- });
+ }));
const radioContinue = view.modelBuilder.radioButton().withProps({
label: constants.RESUME_SESSION,
name: buttonGroup,
@@ -117,11 +124,11 @@ export class SavedAssessmentDialog {
checked: false
}).component();
- radioContinue.onDidChangeCheckedState((e) => {
+ this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = true;
}
- });
+ }));
const flex = view.modelBuilder.flexContainer()
.withLayout({
diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts
index 782593c653..1674dbbd2b 100644
--- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts
+++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts
@@ -95,7 +95,14 @@ export class CreateSqlMigrationServiceDialog {
try {
clearDialogMessage(this._dialogObject);
this._selectedResourceGroup = resourceGroup;
- this._createdMigrationService = await createSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, serviceName!, this._model._sessionId);
+ this._createdMigrationService = await createSqlMigrationService(
+ this._model._azureAccount,
+ subscription,
+ resourceGroup,
+ location,
+ serviceName!,
+ this._model._sessionId);
+
if (this._createdMigrationService.error) {
this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`);
this._statusLoadingComponent.loading = false;
@@ -490,7 +497,12 @@ export class CreateSqlMigrationServiceDialog {
for (let i = 0; i < maxRetries; i++) {
try {
clearDialogMessage(this._dialogObject);
- migrationServiceStatus = await getSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService.name, this._model._sessionId);
+ migrationServiceStatus = await getSqlMigrationService(
+ this._model._azureAccount,
+ subscription,
+ resourceGroup,
+ location,
+ this._createdMigrationService.name);
break;
} catch (e) {
this._dialogObject.message = {
@@ -502,7 +514,13 @@ export class CreateSqlMigrationServiceDialog {
}
await new Promise(r => setTimeout(r, 5000));
}
- const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId);
+ const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(
+ this._model._azureAccount,
+ subscription,
+ resourceGroup,
+ location,
+ this._createdMigrationService!.name);
+
this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => {
return node.nodeName;
});
@@ -536,7 +554,12 @@ export class CreateSqlMigrationServiceDialog {
const subscription = this._model._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
const location = this._model._targetServerInstance.location;
- const keys = await getSqlMigrationServiceAuthKeys(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId);
+ const keys = await getSqlMigrationServiceAuthKeys(
+ this._model._azureAccount,
+ subscription,
+ resourceGroup,
+ location,
+ this._createdMigrationService!.name);
this._copyKey1Button = this._view.modelBuilder.button().withProps({
title: constants.COPY_KEY1,
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
index a11a8c0c92..bfc91795e9 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts
@@ -7,10 +7,11 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as constants from '../../constants/strings';
-import { SqlManagedInstance } from '../../api/azure';
+import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
import * as styles from '../../constants/styles';
+import { isBlobMigration } from '../../constants/helper';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -21,20 +22,17 @@ export class ConfirmCutoverDialog {
}
async initialize(): Promise {
-
- let tab = azdata.window.createTab('');
+ const tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
const completeCutoverText = view.modelBuilder.text().withProps({
value: constants.COMPLETE_CUTOVER,
- CSSStyles: {
- ...styles.PAGE_TITLE_CSS
- }
+ CSSStyles: { ...styles.PAGE_TITLE_CSS }
}).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({
- value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName,
+ value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'margin': '4px 0px 8px'
@@ -42,12 +40,9 @@ export class ConfirmCutoverDialog {
}).component();
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
-
const helpMainText = this._view.modelBuilder.text().withProps({
value: constants.CUTOVER_HELP_MAIN,
- CSSStyles: {
- ...styles.BODY_CSS
- }
+ CSSStyles: { ...styles.BODY_CSS }
}).component();
const helpStepsText = this._view.modelBuilder.text().withProps({
@@ -58,8 +53,9 @@ export class ConfirmCutoverDialog {
}
}).component();
-
- const fileContainer = this.migrationCutoverModel.isBlobMigration() ? this.createBlobFileContainer() : this.createNetworkShareFileContainer();
+ const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
+ ? this.createBlobFileContainer()
+ : this.createNetworkShareFileContainer();
const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({
CSSStyles: {
@@ -76,16 +72,19 @@ export class ConfirmCutoverDialog {
const cutoverWarning = this._view.modelBuilder.infoBox().withProps({
text: constants.COMPLETING_CUTOVER_WARNING,
style: 'warning',
- CSSStyles: {
- ...styles.BODY_CSS
- }
+ CSSStyles: { ...styles.BODY_CSS }
}).component();
-
let infoDisplay = 'none';
- if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
- && (this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
- infoDisplay = 'inline';
+ if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
+ const targetInstance = await getMigrationTargetInstance(
+ this.migrationCutoverModel._serviceConstext.azureAccount!,
+ this.migrationCutoverModel._serviceConstext.subscription!,
+ this.migrationCutoverModel._migration);
+
+ if ((targetInstance)?.sku?.tier === 'BusinessCritical') {
+ infoDisplay = 'inline';
+ }
}
const businessCriticalInfoBox = this._view.modelBuilder.infoBox().withProps({
@@ -111,23 +110,18 @@ export class ConfirmCutoverDialog {
businessCriticalInfoBox
]).component();
-
this._dialogObject.okButton.enabled = false;
this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER;
this._disposables.push(this._dialogObject.okButton.onClick(async (e) => {
await this.migrationCutoverModel.startCutover();
- void vscode.window.showInformationMessage(constants.CUTOVER_IN_PROGRESS(this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName));
+ void vscode.window.showInformationMessage(
+ constants.CUTOVER_IN_PROGRESS(
+ this.migrationCutoverModel._migration.properties.sourceDatabaseName));
}));
const formBuilder = view.modelBuilder.formContainer().withFormItems(
- [
- {
- component: container
- }
- ],
- {
- horizontal: false
- }
+ [{ component: container }],
+ { horizontal: false }
);
const form = formBuilder.withLayout({ width: '100%' }).component();
@@ -144,18 +138,14 @@ export class ConfirmCutoverDialog {
private createBlobFileContainer(): azdata.FlexContainer {
const container = this._view.modelBuilder.flexContainer().withProps({
- CSSStyles: {
- 'margin': '8px 0'
- }
+ CSSStyles: { 'margin': '8px 0' }
}).component();
-
const containerHeading = this._view.modelBuilder.text().withProps({
value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0),
width: 250,
- CSSStyles: {
- ...styles.LABEL_CSS
- }
+ CSSStyles: { ...styles.LABEL_CSS }
}).component();
+ container.addItem(containerHeading, { flex: '0' });
const refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
@@ -165,13 +155,7 @@ export class ConfirmCutoverDialog {
height: 20,
label: constants.REFRESH,
}).component();
-
-
- container.addItem(containerHeading, {
- flex: '0'
- });
-
- refreshButton.onDidClick(async e => {
+ this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
@@ -184,11 +168,8 @@ export class ConfirmCutoverDialog {
} finally {
refreshLoader.loading = false;
}
- });
-
- container.addItem(refreshButton, {
- flex: '0'
- });
+ }));
+ container.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
@@ -197,10 +178,8 @@ export class ConfirmCutoverDialog {
'margin-left': '8px'
}
}).component();
+ container.addItem(refreshLoader, { flex: '0' });
- container.addItem(refreshLoader, {
- flex: '0'
- });
return container;
}
@@ -227,23 +206,18 @@ export class ConfirmCutoverDialog {
}
}).component();
- containerHeading.onDidClick(async e => {
+ this._disposables.push(containerHeading.onDidClick(async e => {
if (expanded) {
containerHeading.iconPath = IconPathHelper.expandButtonClosed;
containerHeading.iconHeight = 12;
- await fileTable.updateCssStyles({
- 'display': 'none'
- });
-
+ await fileTable.updateCssStyles({ 'display': 'none' });
} else {
containerHeading.iconPath = IconPathHelper.expandButtonOpen;
containerHeading.iconHeight = 8;
- await fileTable.updateCssStyles({
- 'display': 'inline'
- });
+ await fileTable.updateCssStyles({ 'display': 'inline' });
}
expanded = !expanded;
- });
+ }));
const refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
@@ -252,16 +226,12 @@ export class ConfirmCutoverDialog {
width: 70,
height: 20,
label: constants.REFRESH,
- CSSStyles: {
- 'margin-top': '13px'
- }
+ CSSStyles: { 'margin-top': '13px' }
}).component();
- headingRow.addItem(containerHeading, {
- flex: '0'
- });
+ headingRow.addItem(containerHeading, { flex: '0' });
- refreshButton.onDidClick(async e => {
+ this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
@@ -276,11 +246,8 @@ export class ConfirmCutoverDialog {
} finally {
refreshLoader.loading = false;
}
- });
-
- headingRow.addItem(refreshButton, {
- flex: '0'
- });
+ }));
+ headingRow.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
@@ -290,20 +257,13 @@ export class ConfirmCutoverDialog {
'height': '13px'
}
}).component();
-
- headingRow.addItem(refreshLoader, {
- flex: '0'
- });
-
+ headingRow.addItem(refreshLoader, { flex: '0' });
container.addItem(headingRow);
const lastScanCompleted = this._view.modelBuilder.text().withProps({
value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())),
- CSSStyles: {
- ...styles.NOTE_CSS
- }
+ CSSStyles: { ...styles.NOTE_CSS }
}).component();
-
container.addItem(lastScanCompleted);
const fileTable = this._view.modelBuilder.table().withProps({
@@ -327,9 +287,7 @@ export class ConfirmCutoverDialog {
data: [],
width: 400,
height: 150,
- CSSStyles: {
- 'display': 'none'
- }
+ CSSStyles: { 'display': 'none' }
}).component();
container.addItem(fileTable);
this.refreshFileTable(fileTable);
@@ -347,9 +305,7 @@ export class ConfirmCutoverDialog {
];
});
} else {
- fileTable.data = [
- [constants.NO_PENDING_BACKUPS]
- ];
+ fileTable.data = [[constants.NO_PENDING_BACKUPS]];
}
}
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
index 677b0d95c7..6b622f39ba 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts
@@ -6,26 +6,24 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
-import { BackupFileInfoStatus, MigrationContext, MigrationStatus } from '../../models/migrationLocalStorage';
+import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings';
-import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
+import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
import { EOL } from 'os';
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
import { logError, TelemetryViews } from '../../telemtery';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
import * as styles from '../../constants/styles';
-import { canRetryMigration } from '../../constants/helper';
+import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
+import { DatabaseMigration, getResourceName } from '../../api/azure';
-const refreshFrequency: SupportedAutoRefreshIntervals = 30000;
const statusImageSize: number = 14;
export class MigrationCutoverDialog {
- private _context: vscode.ExtensionContext;
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _model: MigrationCutoverDialogModel;
- private _migration: MigrationContext;
private _databaseTitleName!: azdata.TextComponent;
private _cutoverButton!: azdata.ButtonComponent;
@@ -52,7 +50,6 @@ export class MigrationCutoverDialog {
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.DeclarativeTableComponent;
- private _autoRefreshHandle!: any;
private _disposables: vscode.Disposable[] = [];
private _emptyTableFill!: azdata.FlexContainer;
@@ -60,10 +57,13 @@ export class MigrationCutoverDialog {
readonly _infoFieldWidth: string = '250px';
- constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
- this._context = context;
- this._migration = migration;
- this._model = new MigrationCutoverDialogModel(migration);
+ constructor(
+ private readonly _context: vscode.ExtensionContext,
+ private readonly _serviceContext: MigrationServiceContext,
+ private readonly _migration: DatabaseMigration,
+ private readonly _onClosedCallback: () => Promise) {
+
+ this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
}
@@ -224,15 +224,14 @@ export class MigrationCutoverDialog {
);
const form = formBuilder.withLayout({ width: '100%' }).component();
- this._disposables.push(this._view.onClosed(e => {
- clearInterval(this._autoRefreshHandle);
- this._disposables.forEach(
- d => { try { d.dispose(); } catch { } });
- }));
+ this._disposables.push(
+ this._view.onClosed(e => {
+ this._disposables.forEach(
+ d => { try { d.dispose(); } catch { } });
+ }));
- return view.initializeModel(form).then(async (value) => {
- await this.refreshStatus();
- });
+ await view.initializeModel(form);
+ await this.refreshStatus();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
@@ -242,9 +241,6 @@ export class MigrationCutoverDialog {
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
- this._disposables.push(this._dialogObject.okButton.onClick(e => {
- clearInterval(this._autoRefreshHandle);
- }));
azdata.window.openDialog(this._dialogObject);
}
@@ -262,7 +258,7 @@ export class MigrationCutoverDialog {
...styles.PAGE_TITLE_CSS
},
width: 950,
- value: this._model._migration.migrationContext.properties.sourceDatabaseName
+ value: this._model._migration.properties.sourceDatabaseName
}).component();
const databaseSubTitle = this._view.modelBuilder.text().withProps({
@@ -282,8 +278,6 @@ export class MigrationCutoverDialog {
width: 950
}).component();
- this.setAutoRefresh(refreshFrequency);
-
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
@@ -314,7 +308,7 @@ export class MigrationCutoverDialog {
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
- 'display': this._isOnlineMigration() ? 'block' : 'none'
+ 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
}
}).component();
@@ -322,16 +316,13 @@ export class MigrationCutoverDialog {
await this.refreshStatus();
const dialog = new ConfirmCutoverDialog(this._model);
await dialog.initialize();
- await this.refreshStatus();
if (this._model.CutoverError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
}
}));
- headerActions.addItem(this._cutoverButton, {
- flex: '0'
- });
+ headerActions.addItem(this._cutoverButton, { flex: '0' });
this._cancelButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cancel,
@@ -377,7 +368,11 @@ export class MigrationCutoverDialog {
this._disposables.push(this._retryButton.onDidClick(
async (e) => {
await this.refreshStatus();
- let retryMigrationDialog = new RetryMigrationDialog(this._context, this._migration);
+ const retryMigrationDialog = new RetryMigrationDialog(
+ this._context,
+ this._serviceContext,
+ this._migration,
+ this._onClosedCallback);
await retryMigrationDialog.openDialog();
}
));
@@ -397,12 +392,14 @@ export class MigrationCutoverDialog {
}
}).component();
- this._disposables.push(this._refreshButton.onDidClick(
- async (e) => await this.refreshStatus()));
+ this._disposables.push(
+ this._refreshButton.onDidClick(async (e) => {
+ this._refreshButton.enabled = false;
+ await this.refreshStatus();
+ this._refreshButton.enabled = true;
+ }));
- headerActions.addItem(this._refreshButton, {
- flex: '0',
- });
+ headerActions.addItem(this._refreshButton, { flex: '0' });
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy,
@@ -425,9 +422,7 @@ export class MigrationCutoverDialog {
headerActions.addItem(this._copyDatabaseMigrationDetails, {
flex: '0',
- CSSStyles: {
- 'margin-left': '5px'
- }
+ CSSStyles: { 'margin-left': '5px' }
});
// create new support request button. Hiding button until sql migration support has been setup.
@@ -443,11 +438,11 @@ export class MigrationCutoverDialog {
}
}).component();
- this._newSupportRequest.onDidClick(async (e) => {
- const serviceId = this._model._migration.controller.id;
+ this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
+ const serviceId = this._model._migration.properties.migrationService;
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
- });
+ }));
headerActions.addItem(this._newSupportRequest, {
flex: '0',
@@ -519,12 +514,12 @@ export class MigrationCutoverDialog {
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
- const isBlobMigration = this._model.isBlobMigration();
+ const _isBlobMigration = isBlobMigration(this._model._migration);
const flexStatus = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
- this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', isBlobMigration);
+ this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
@@ -533,10 +528,10 @@ export class MigrationCutoverDialog {
const flexFile = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
- this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', isBlobMigration);
+ this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', _isBlobMigration);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
- this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', isBlobMigration);
- this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !isBlobMigration);
+ this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
+ this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
@@ -561,33 +556,8 @@ export class MigrationCutoverDialog {
return flexInfo;
}
- private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
- const shouldRefresh = (status: string | undefined) => !status
- || status === MigrationStatus.InProgress
- || status === MigrationStatus.Creating
- || status === MigrationStatus.Completing
- || status === MigrationStatus.Canceling;
-
- if (shouldRefresh(this.getMigrationStatus())) {
- const classVariable = this;
- clearInterval(this._autoRefreshHandle);
- if (interval !== -1) {
- this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshStatus(); }, interval);
- }
- }
- }
-
private getMigrationDetails(): string {
- if (this._model.migrationOpStatus) {
- return (JSON.stringify(
- {
- 'async-operation-details': this._model.migrationOpStatus,
- 'details': this._model.migrationStatus
- }
- , undefined, 2));
- } else {
- return (JSON.stringify(this._model.migrationStatus, undefined, 2));
- }
+ return JSON.stringify(this._model.migrationStatus, undefined, 2);
}
private async refreshStatus(): Promise {
@@ -598,18 +568,13 @@ export class MigrationCutoverDialog {
try {
clearDialogMessage(this._dialogObject);
- if (this._isOnlineMigration()) {
- await this._cutoverButton.updateCssStyles({
- 'display': 'block'
- });
- }
+ await this._cutoverButton.updateCssStyles(
+ { 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
this.isRefreshing = true;
this._refreshLoader.loading = true;
await this._model.fetchStatus();
const errors = [];
- errors.push(this._model.migrationOpStatus.error?.message);
- errors.push(this._model._migration.asyncOperationResult?.error?.message);
errors.push(this._model.migrationStatus.properties.provisioningError);
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
@@ -626,12 +591,12 @@ export class MigrationCutoverDialog {
description: this.getMigrationDetails()
};
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
- const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
- const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName;
+ const sqlServerName = this._model._migration.properties.sourceServerName;
+ const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
- const targetDatabaseName = this._model._migration.migrationContext.name;
- const targetServerName = this._model._migration.targetManagedInstance.name;
+ const targetDatabaseName = this._model._migration.name;
+ const targetServerName = getResourceName(this._model._migration.id);
let targetServerVersion;
if (this._model.migrationStatus.id.includes('managedInstances')) {
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
@@ -644,30 +609,30 @@ export class MigrationCutoverDialog {
const tableData: ActiveBackupFileSchema[] = [];
- this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
+ this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(
+ (activeBackupSet) => {
+ if (this._shouldDisplayBackupFileTable()) {
+ tableData.push(
+ ...activeBackupSet.listOfBackupFiles.map(f => {
+ return {
+ fileName: f.fileName,
+ type: activeBackupSet.backupType,
+ status: f.status,
+ dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
+ copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
+ backupStartTime: activeBackupSet.backupStartDate,
+ firstLSN: activeBackupSet.firstLSN,
+ lastLSN: activeBackupSet.lastLSN
+ };
+ })
+ );
+ }
- if (this._shouldDisplayBackupFileTable()) {
- tableData.push(
- ...activeBackupSet.listOfBackupFiles.map(f => {
- return {
- fileName: f.fileName,
- type: activeBackupSet.backupType,
- status: f.status,
- dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
- copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
- backupStartTime: activeBackupSet.backupStartDate,
- firstLSN: activeBackupSet.firstLSN,
- lastLSN: activeBackupSet.lastLSN
- };
- })
- );
- }
-
- if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
- lastAppliedSSN = activeBackupSet.lastLSN;
- lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
- }
- });
+ if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
+ lastAppliedSSN = activeBackupSet.lastLSN;
+ lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
+ }
+ });
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
@@ -677,21 +642,23 @@ export class MigrationCutoverDialog {
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
- const migrationStatusTextValue = this.getMigrationStatus();
+ const migrationStatusTextValue = this._getMigrationStatus();
this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
let backupLocation;
- const isBlobMigration = this._model.isBlobMigration();
+ const _isBlobMigration = isBlobMigration(this._model._migration);
// Displaying storage accounts and blob container for azure blob backups.
- if (isBlobMigration) {
- const storageAccountResourceId = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
- const blobContainerName = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
- backupLocation = `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`;
+ if (_isBlobMigration) {
+ const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
+ const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
+ backupLocation = storageAccountResourceId && blobContainerName
+ ? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
+ : undefined;
} else {
- const fileShare = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare;
+ const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
backupLocation = fileShare?.path! ?? '-';
}
this._backupLocationInfoField.text.value = backupLocation ?? '-';
@@ -700,7 +667,7 @@ export class MigrationCutoverDialog {
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
- if (isBlobMigration) {
+ if (_isBlobMigration) {
if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
this._currentRestoringFileInfoField.text.value = '-';
} else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
@@ -752,7 +719,7 @@ export class MigrationCutoverDialog {
this._cutoverButton.enabled = false;
if (migrationStatusTextValue === MigrationStatus.InProgress) {
- if (isBlobMigration) {
+ if (_isBlobMigration) {
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
this._cutoverButton.enabled = true;
}
@@ -856,21 +823,14 @@ export class MigrationCutoverDialog {
};
}
- private _isOnlineMigration(): boolean {
- return this._model._migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? false : true;
- }
-
private _shouldDisplayBackupFileTable(): boolean {
- return !this._model.isBlobMigration();
+ return !isBlobMigration(this._model._migration);
}
- private getMigrationStatus(): string {
- if (this._model.migrationStatus) {
- return this._model.migrationStatus.properties.migrationStatus
- ?? this._model.migrationStatus.properties.provisioningState;
- }
- return this._model._migration.migrationContext.properties.migrationStatus
- ?? this._model._migration.migrationContext.properties.provisioningState;
+ private _getMigrationStatus(): string {
+ return this._model.migrationStatus
+ ? getMigrationStatus(this._model.migrationStatus)
+ : getMigrationStatus(this._model._migration);
}
}
diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
index 09cfcce295..196b04b3a1 100644
--- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
+++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts
@@ -3,43 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource, BackupFileInfo, getResourceGroupFromId } from '../../api/azure';
-import { BackupFileInfoStatus, MigrationContext } from '../../models/migrationLocalStorage';
+import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
+import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings';
import { EOL } from 'os';
-import { getMigrationTargetType, getMigrationMode } from '../../constants/helper';
+import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
export class MigrationCutoverDialogModel {
public CutoverError?: Error;
public CancelMigrationError?: Error;
-
public migrationStatus!: DatabaseMigration;
- public migrationOpStatus!: AzureAsyncOperationResource;
- constructor(public _migration: MigrationContext) {
+
+ constructor(
+ public _serviceConstext: MigrationServiceContext,
+ public _migration: DatabaseMigration
+ ) {
}
public async fetchStatus(): Promise {
- if (this._migration.asyncUrl) {
- this.migrationOpStatus = await getMigrationAsyncOperationDetails(
- this._migration.azureAccount,
- this._migration.subscription,
- this._migration.asyncUrl,
- this._migration.sessionId!);
- }
-
- this.migrationStatus = await getMigrationStatus(
- this._migration.azureAccount,
- this._migration.subscription,
- this._migration.migrationContext,
- this._migration.sessionId!);
+ this.migrationStatus = await getMigrationDetails(
+ this._serviceConstext.azureAccount!,
+ this._serviceConstext.subscription!,
+ this._migration.id,
+ this._migration.properties?.migrationOperationId);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus,
{
- 'sessionId': this._migration.sessionId!,
'migrationStatus': this.migrationStatus.properties?.migrationStatus
},
{}
@@ -51,18 +44,16 @@ export class MigrationCutoverDialogModel {
public async startCutover(): Promise {
try {
this.CutoverError = undefined;
- if (this.migrationStatus) {
+ if (this._migration) {
const cutover = await startMigrationCutover(
- this._migration.azureAccount,
- this._migration.subscription,
- this.migrationStatus,
- this._migration.sessionId!
- );
+ this._serviceConstext.azureAccount!,
+ this._serviceConstext.subscription!,
+ this._migration!);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration,
{
- ...this.getTelemetryProps(this._migration),
+ ...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationEndTime': new Date().toString(),
},
{}
@@ -79,8 +70,6 @@ export class MigrationCutoverDialogModel {
public async fetchErrors(): Promise {
const errors = [];
await this.fetchStatus();
- errors.push(this.migrationOpStatus.error?.message);
- errors.push(this._migration.asyncOperationResult?.error?.message);
errors.push(this.migrationStatus.properties.migrationFailureError?.message);
return errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
@@ -93,18 +82,16 @@ export class MigrationCutoverDialogModel {
if (this.migrationStatus) {
const cutoverStartTime = new Date().toString();
await stopMigration(
- this._migration.azureAccount,
- this._migration.subscription,
- this.migrationStatus,
- this._migration.sessionId!
- );
+ this._serviceConstext.azureAccount!,
+ this._serviceConstext.subscription!,
+ this.migrationStatus);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration,
{
- ...this.getTelemetryProps(this._migration),
+ ...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationMode': getMigrationMode(this._migration),
- 'cutoverStartTime': cutoverStartTime
+ 'cutoverStartTime': cutoverStartTime,
},
{}
);
@@ -116,12 +103,8 @@ export class MigrationCutoverDialogModel {
return undefined!;
}
- public isBlobMigration(): boolean {
- return this._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob !== undefined;
- }
-
public confirmCutoverStepsString(): string {
- if (this.isBlobMigration()) {
+ if (isBlobMigration(this.migrationStatus)) {
return `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
@@ -152,16 +135,15 @@ export class MigrationCutoverDialogModel {
return files;
}
- private getTelemetryProps(migration: MigrationContext) {
+ private getTelemetryProps(serviceContext: MigrationServiceContext, migration: DatabaseMigration) {
return {
- 'sessionId': migration.sessionId!,
- 'subscriptionId': migration.subscription.id,
- 'resourceGroup': getResourceGroupFromId(migration.targetManagedInstance.id),
- 'sqlServerName': migration.sourceConnectionProfile.serverName,
- 'sourceDatabaseName': migration.migrationContext.properties.sourceDatabaseName,
+ 'subscriptionId': serviceContext.subscription!.id,
+ 'resourceGroup': getResourceGroupFromId(migration.id),
+ 'sqlServerName': migration.properties.sourceServerName,
+ 'sourceDatabaseName': migration.properties.sourceDatabaseName,
'targetType': getMigrationTargetType(migration),
- 'targetDatabaseName': migration.migrationContext.name,
- 'targetServerName': migration.targetManagedInstance.name,
+ 'targetDatabaseName': migration.name,
+ 'targetServerName': getMigrationTargetName(migration),
};
}
}
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
index e4eb74540b..5f0cbaae06 100644
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
@@ -6,22 +6,19 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
-import { MigrationContext, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage';
+import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings';
-import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage, SupportedAutoRefreshIntervals } from '../../api/utils';
+import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage } from '../../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
-
-const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
-
-const statusImageSize: number = 14;
-const imageCellStyles: azdata.CssStyles = { 'margin': '3px 3px 0 0', 'padding': '0' };
-const statusCellStyles: azdata.CssStyles = { 'margin': '0', 'padding': '0' };
+import { DatabaseMigration, getResourceName } from '../../api/azure';
+import { logError, TelemetryViews } from '../../telemtery';
+import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog';
const MenuCommands = {
Cutover: 'sqlmigration.cutover',
@@ -40,53 +37,56 @@ export class MigrationStatusDialog {
private _view!: azdata.ModelView;
private _searchBox!: azdata.InputBoxComponent;
private _refresh!: azdata.ButtonComponent;
+ private _serviceContextButton!: azdata.ButtonComponent;
private _statusDropdown!: azdata.DropDownComponent;
- private _statusTable!: azdata.DeclarativeTableComponent;
+ private _statusTable!: azdata.TableComponent;
private _refreshLoader!: azdata.LoadingComponent;
- private _autoRefreshHandle!: NodeJS.Timeout;
private _disposables: vscode.Disposable[] = [];
+ private _filteredMigrations: DatabaseMigration[] = [];
private isRefreshing = false;
- constructor(context: vscode.ExtensionContext, migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
+ constructor(
+ context: vscode.ExtensionContext,
+ private _filter: AdsMigrationStatus,
+ private _onClosedCallback: () => Promise) {
+
this._context = context;
- this._model = new MigrationStatusDialogModel(migrations);
- this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
+ this._model = new MigrationStatusDialogModel([]);
+ this._dialogObject = azdata.window.createModelViewDialog(
+ loc.MIGRATION_STATUS,
+ 'MigrationControllerDialog',
+ 'wide');
}
- initialize() {
+ async initialize() {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
this.registerCommands();
- const formBuilder = view.modelBuilder.formContainer().withFormItems(
- [
- {
- component: this.createSearchAndRefreshContainer()
- },
- {
- component: this.createStatusTable()
- }
- ],
- {
- horizontal: false
- }
- );
- const form = formBuilder.withLayout({ width: '100%' }).component();
- this._disposables.push(this._view.onClosed(e => {
- clearInterval(this._autoRefreshHandle);
- this._disposables.forEach(
- d => { try { d.dispose(); } catch { } });
- }));
+ const form = view.modelBuilder.formContainer()
+ .withFormItems(
+ [
+ { component: await this.createSearchAndRefreshContainer() },
+ { component: this.createStatusTable() }
+ ],
+ { horizontal: false }
+ ).withLayout({ width: '100%' })
+ .component();
+ this._disposables.push(
+ this._view.onClosed(async e => {
+ this._disposables.forEach(
+ d => { try { d.dispose(); } catch { } });
- return view.initializeModel(form);
+ await this._onClosedCallback();
+ }));
+
+ await view.initializeModel(form);
+ return await this.refreshTable();
});
this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
- this._disposables.push(this._dialogObject.okButton.onClick(e => {
- clearInterval(this._autoRefreshHandle);
- }));
azdata.window.openDialog(this._dialogObject);
}
@@ -100,111 +100,124 @@ export class MigrationStatusDialog {
private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress;
- private createSearchAndRefreshContainer(): azdata.FlexContainer {
- this._searchBox = this._view.modelBuilder.inputBox().withProps({
- stopEnterPropagation: true,
- placeHolder: loc.SEARCH_FOR_MIGRATIONS,
- width: '360px'
- }).component();
-
- this._disposables.push(this._searchBox.onTextChanged(async (value) => {
- await this.populateMigrationTable();
- }));
-
- this._refresh = this._view.modelBuilder.button().withProps({
- iconPath: IconPathHelper.refresh,
- iconHeight: '16px',
- iconWidth: '20px',
- height: '30px',
- label: loc.REFRESH_BUTTON_LABEL,
- }).component();
+ private async createSearchAndRefreshContainer(): Promise {
+ this._searchBox = this._view.modelBuilder.inputBox()
+ .withProps({
+ stopEnterPropagation: true,
+ placeHolder: loc.SEARCH_FOR_MIGRATIONS,
+ width: '360px'
+ }).component();
+ this._disposables.push(
+ this._searchBox.onTextChanged(
+ async (value) => await this.populateMigrationTable()));
+ this._refresh = this._view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.refresh,
+ iconHeight: '16px',
+ iconWidth: '20px',
+ label: loc.REFRESH_BUTTON_LABEL,
+ }).component();
this._disposables.push(
this._refresh.onDidClick(
- async (e) => { await this.refreshTable(); }));
+ async (e) => await this.refreshTable()));
- const flexContainer = this._view.modelBuilder.flexContainer().withProps({
- width: 900,
- CSSStyles: {
- 'justify-content': 'left'
- },
- }).component();
-
- flexContainer.addItem(this._searchBox, {
- flex: '0'
- });
-
- this._statusDropdown = this._view.modelBuilder.dropDown().withProps({
- ariaLabel: loc.MIGRATION_STATUS_FILTER,
- values: this._model.statusDropdownValues,
- width: '220px'
- }).component();
-
- this._disposables.push(this._statusDropdown.onValueChanged(async (value) => {
- await this.populateMigrationTable();
- }));
+ this._statusDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: loc.MIGRATION_STATUS_FILTER,
+ values: this._model.statusDropdownValues,
+ width: '220px'
+ }).component();
+ this._disposables.push(
+ this._statusDropdown.onValueChanged(
+ async (value) => await this.populateMigrationTable()));
if (this._filter) {
- this._statusDropdown.value = (this._statusDropdown.values).find((value) => {
- return value.name === this._filter;
- });
+ this._statusDropdown.value =
+ (this._statusDropdown.values)
+ .find(value => value.name === this._filter);
}
- flexContainer.addItem(this._statusDropdown, {
- flex: '0',
- CSSStyles: {
- 'margin-left': '20px'
- }
- });
+ this._refreshLoader = this._view.modelBuilder.loadingComponent()
+ .withProps({ loading: false })
+ .component();
- flexContainer.addItem(this._refresh, {
- flex: '0',
- CSSStyles: {
- 'margin-left': '20px'
- }
- });
+ const searchLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: 'Status',
+ CSSStyles: {
+ 'font-size': '13px',
+ 'font-weight': '600',
+ 'margin': '3px 0 0 0',
+ },
+ }).component();
- this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
- loading: false,
- height: '55px'
- }).component();
+ const serviceContextLabel = await getSelectedServiceStatus();
+ this._serviceContextButton = this._view.modelBuilder.button()
+ .withProps({
+ iconPath: IconPathHelper.sqlMigrationService,
+ iconHeight: 22,
+ iconWidth: 22,
+ label: serviceContextLabel,
+ title: serviceContextLabel,
+ description: loc.MIGRATION_SERVICE_DESCRIPTION,
+ buttonType: azdata.ButtonType.Informational,
+ width: 270,
+ }).component();
- flexContainer.addItem(this._refreshLoader, {
- flex: '0 0 auto',
- CSSStyles: {
- 'margin-left': '20px'
- }
- });
- this.setAutoRefresh(refreshFrequency);
- const container = this._view.modelBuilder.flexContainer().withProps({
- width: 1000
- }).component();
- container.addItem(flexContainer, {
- flex: '0 0 auto',
- CSSStyles: {
- 'width': '980px'
- }
- });
+ const onDialogClosed = async (): Promise => {
+ const label = await getSelectedServiceStatus();
+ this._serviceContextButton.label = label;
+ this._serviceContextButton.title = label;
+ await this.refreshTable();
+ };
+
+ this._disposables.push(
+ this._serviceContextButton.onDidClick(
+ async () => {
+ const dialog = new SelectMigrationServiceDialog(onDialogClosed);
+ await dialog.initialize();
+ }));
+
+ const flexContainer = this._view.modelBuilder.flexContainer()
+ .withProps({
+ width: '100%',
+ CSSStyles: {
+ 'justify-content': 'left',
+ 'align-items': 'center',
+ 'padding': '0px',
+ 'display': 'flex',
+ 'flex-direction': 'row',
+ },
+ }).component();
+
+ flexContainer.addItem(this._searchBox, { flex: '0' });
+ flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
+ flexContainer.addItem(searchLabel, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
+ flexContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
+ flexContainer.addItem(this._refresh, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
+ flexContainer.addItem(this._refreshLoader, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } });
+
+ const container = this._view.modelBuilder.flexContainer()
+ .withProps({ width: 1245 })
+ .component();
+ container.addItem(flexContainer, { flex: '0 0 auto', });
return container;
}
- private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
- const classVariable = this;
- clearInterval(this._autoRefreshHandle);
- if (interval !== -1) {
- this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshTable(); }, interval);
- }
- }
-
private registerCommands(): void {
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.Cutover,
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- if (this.canCutoverMigration(migration?.migrationContext.properties.migrationStatus)) {
- const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
+ const migration = this._model._migrations.find(
+ migration => migration.id === migrationId);
+
+ if (this.canCutoverMigration(migration?.properties.migrationStatus)) {
+ const cutoverDialogModel = new MigrationCutoverDialogModel(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
await cutoverDialogModel.fetchStatus();
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
await dialog.initialize();
@@ -224,8 +237,12 @@ export class MigrationStatusDialog {
MenuCommands.ViewDatabase,
async (migrationId: string) => {
try {
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- const dialog = new MigrationCutoverDialog(this._context, migration!);
+ const migration = this._model._migrations.find(migration => migration.id === migrationId);
+ const dialog = new MigrationCutoverDialog(
+ this._context,
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!,
+ this._onClosedCallback);
await dialog.initialize();
} catch (e) {
console.log(e);
@@ -236,8 +253,8 @@ export class MigrationStatusDialog {
MenuCommands.ViewTarget,
async (migrationId: string) => {
try {
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- const url = 'https://portal.azure.com/#resource/' + migration!.targetManagedInstance.id;
+ const migration = this._model._migrations.find(migration => migration.id === migrationId);
+ const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) {
console.log(e);
@@ -248,8 +265,10 @@ export class MigrationStatusDialog {
MenuCommands.ViewService,
async (migrationId: string) => {
try {
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- const dialog = new SqlMigrationServiceDetailsDialog(migration!);
+ const migration = this._model._migrations.find(migration => migration.id === migrationId);
+ const dialog = new SqlMigrationServiceDetailsDialog(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
await dialog.initialize();
} catch (e) {
console.log(e);
@@ -261,17 +280,12 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
+ const migration = this._model._migrations.find(migration => migration.id === migrationId);
+ const cutoverDialogModel = new MigrationCutoverDialogModel(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
await cutoverDialogModel.fetchStatus();
- if (cutoverDialogModel.migrationOpStatus) {
- await vscode.env.clipboard.writeText(JSON.stringify({
- 'async-operation-details': cutoverDialogModel.migrationOpStatus,
- 'details': cutoverDialogModel.migrationStatus
- }, undefined, 2));
- } else {
- await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
- }
+ await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
} catch (e) {
@@ -285,11 +299,13 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- if (this.canCancelMigration(migration?.migrationContext.properties.migrationStatus)) {
+ const migration = this._model._migrations.find(migration => migration.id === migrationId);
+ if (this.canCancelMigration(migration?.properties.migrationStatus)) {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
- const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
+ const cutoverDialogModel = new MigrationCutoverDialogModel(
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!);
await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration();
@@ -312,9 +328,13 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
- const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
- if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) {
- let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!);
+ const migration = this._model._migrations.find(migration => migration.id === migrationId);
+ if (canRetryMigration(migration?.properties.migrationStatus)) {
+ let retryMigrationDialog = new RetryMigrationDialog(
+ this._context,
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration!,
+ this._onClosedCallback);
await retryMigrationDialog.openDialog();
}
else {
@@ -329,75 +349,43 @@ export class MigrationStatusDialog {
private async populateMigrationTable(): Promise {
try {
- const migrations = filterMigrations(
+ this._filteredMigrations = filterMigrations(
this._model._migrations,
(this._statusDropdown.value).name,
this._searchBox.value!);
- migrations.sort((m1, m2) => {
- return new Date(m1.migrationContext.properties?.startedOn) > new Date(m2.migrationContext.properties?.startedOn) ? -1 : 1;
+ this._filteredMigrations.sort((m1, m2) => {
+ return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1;
});
- const data: azdata.DeclarativeTableCellValue[][] = migrations.map((migration, index) => {
+ const data: any[] = this._filteredMigrations.map((migration, index) => {
return [
- { value: this._getDatabaserHyperLink(migration) },
- { value: this._getMigrationStatus(migration) },
- { value: getMigrationMode(migration) },
- { value: getMigrationTargetType(migration) },
- { value: migration.targetManagedInstance.name },
- { value: migration.controller.name },
- {
- value: this._getMigrationDuration(
- migration.migrationContext.properties.startedOn,
- migration.migrationContext.properties.endedOn)
- },
- { value: this._getMigrationTime(migration.migrationContext.properties.startedOn) },
- { value: this._getMigrationTime(migration.migrationContext.properties.endedOn) },
- {
- value: {
- commands: this._getMenuCommands(migration),
- context: migration.migrationContext.id
- },
- }
+ {
+ icon: IconPathHelper.sqlDatabaseLogo,
+ title: migration.properties.sourceDatabaseName ?? '-',
+ }, // database
+ {
+ icon: getMigrationStatusImage(migration.properties.migrationStatus),
+ title: this._getMigrationStatus(migration),
+ }, // statue
+ getMigrationMode(migration), // mode
+ getMigrationTargetType(migration), // targetType
+ getResourceName(migration.id), // targetName
+ getResourceName(migration.properties.migrationService), // migrationService
+ this._getMigrationDuration(
+ migration.properties.startedOn,
+ migration.properties.endedOn), // duration
+ this._getMigrationTime(migration.properties.startedOn), // startTime
+ this._getMigrationTime(migration.properties.endedOn), // endTime
];
});
- await this._statusTable.setDataValues(data);
+ await this._statusTable.updateProperty('data', data);
} catch (e) {
- console.log(e);
+ logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
}
}
- private _getDatabaserHyperLink(migration: MigrationContext): azdata.FlexContainer {
- const imageControl = this._view.modelBuilder.image()
- .withProps({
- iconPath: IconPathHelper.sqlDatabaseLogo,
- iconHeight: statusImageSize,
- iconWidth: statusImageSize,
- height: statusImageSize,
- width: statusImageSize,
- CSSStyles: imageCellStyles
- })
- .component();
-
- const databaseHyperLink = this._view.modelBuilder
- .hyperlink()
- .withProps({
- label: migration.migrationContext.properties.sourceDatabaseName,
- url: '',
- CSSStyles: statusCellStyles
- }).component();
-
- this._disposables.push(databaseHyperLink.onDidClick(
- async (e) => await (new MigrationCutoverDialog(this._context, migration)).initialize()));
-
- return this._view.modelBuilder
- .flexContainer()
- .withItems([imageControl, databaseHyperLink])
- .withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
- .component();
- }
-
private _getMigrationTime(migrationTime: string): string {
return migrationTime
? new Date(migrationTime).toLocaleString()
@@ -420,39 +408,11 @@ export class MigrationStatusDialog {
return '---';
}
- private _getMenuCommands(migration: MigrationContext): string[] {
- const menuCommands: string[] = [];
- const migrationStatus = migration?.migrationContext?.properties?.migrationStatus;
-
- if (getMigrationMode(migration) === loc.ONLINE &&
- this.canCutoverMigration(migrationStatus)) {
- menuCommands.push(MenuCommands.Cutover);
- }
-
- menuCommands.push(...[
- MenuCommands.ViewDatabase,
- MenuCommands.ViewTarget,
- MenuCommands.ViewService,
- MenuCommands.CopyMigration]);
-
- if (this.canCancelMigration(migrationStatus)) {
- menuCommands.push(MenuCommands.CancelMigration);
- }
-
- if (canRetryMigration(migrationStatus)) {
- menuCommands.push(MenuCommands.RetryMigration);
- }
-
- return menuCommands;
- }
-
- private _getMigrationStatus(migration: MigrationContext): azdata.FlexContainer {
- const properties = migration.migrationContext.properties;
+ private _getMigrationStatus(migration: DatabaseMigration): string {
+ const properties = migration.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0;
- if (migration.asyncOperationResult?.error?.message) {
- warningCount++;
- }
+
if (properties.migrationFailureError?.message) {
warningCount++;
}
@@ -463,17 +423,16 @@ export class MigrationStatusDialog {
warningCount++;
}
- return this._getStatusControl(migrationStatus, warningCount, migration);
+ return loc.STATUS_VALUE(migrationStatus, warningCount) + (loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
}
public openCalloutDialog(dialogHeading: string, dialogName?: string, calloutMessageText?: string): void {
- const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false,
- {
- xPos: 0,
- yPos: 0,
- width: 20,
- height: 20
- });
+ const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, {
+ xPos: 0,
+ yPos: 0,
+ width: 20,
+ height: 20
+ });
const tab: azdata.window.DialogTab = azdata.window.createTab('');
tab.registerContent(async view => {
const warningContentContainer = view.modelBuilder.divContainer().component();
@@ -499,73 +458,6 @@ export class MigrationStatusDialog {
azdata.window.openDialog(dialog);
}
- private _getStatusControl(status: string, count: number, migration: MigrationContext): azdata.DivContainer {
- const control = this._view.modelBuilder
- .divContainer()
- .withItems([
- // migration status icon
- this._view.modelBuilder.image()
- .withProps({
- iconPath: getMigrationStatusImage(status),
- iconHeight: statusImageSize,
- iconWidth: statusImageSize,
- height: statusImageSize,
- width: statusImageSize,
- CSSStyles: imageCellStyles
- })
- .component(),
- // migration status text
- this._view.modelBuilder.text().withProps({
- value: loc.STATUS_VALUE(status, count),
- height: statusImageSize,
- CSSStyles: statusCellStyles,
- }).component()
- ])
- .withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
- .component();
-
- if (count > 0) {
- const migrationWarningImage = this._view.modelBuilder.image()
- .withProps({
- iconPath: this._statusInfoMap(status),
- iconHeight: statusImageSize,
- iconWidth: statusImageSize,
- height: statusImageSize,
- width: statusImageSize,
- CSSStyles: imageCellStyles
- }).component();
-
- const migrationWarningCount = this._view.modelBuilder.hyperlink()
- .withProps({
- label: loc.STATUS_WARNING_COUNT(status, count) ?? '',
- ariaLabel: loc.ERROR,
- url: '',
- height: statusImageSize,
- CSSStyles: statusCellStyles,
- }).component();
-
- control.addItems([
- migrationWarningImage,
- migrationWarningCount
- ]);
-
- this._disposables.push(migrationWarningCount.onDidClick(async () => {
- const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
- const errors = await cutoverDialogModel.fetchErrors();
- this.openCalloutDialog(
- status === MigrationStatus.InProgress
- || status === MigrationStatus.Completing
- ? loc.WARNING
- : loc.ERROR,
- 'input-table-row-dialog',
- errors
- );
- }));
- }
-
- return control;
- }
-
private async refreshTable(): Promise {
if (this.isRefreshing) {
return;
@@ -575,8 +467,7 @@ export class MigrationStatusDialog {
try {
clearDialogMessage(this._dialogObject);
this._refreshLoader.loading = true;
- const currentConnection = await azdata.connection.getCurrentConnection();
- this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
+ this._model._migrations = await getCurrentMigrations();
await this.populateMigrationTable();
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
@@ -587,115 +478,111 @@ export class MigrationStatusDialog {
}
}
- private createStatusTable(): azdata.DeclarativeTableComponent {
- const rowCssStyle: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'left',
- 'border-bottom': '1px solid',
- };
+ private createStatusTable(): azdata.TableComponent {
+ const headerCssStyles = undefined;
+ const rowCssStyles = undefined;
- const headerCssStyles: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'left',
- 'border-bottom': '1px solid',
- 'font-weight': 'bold',
- 'padding-left': '0px',
- 'padding-right': '0px'
- };
-
- this._statusTable = this._view.modelBuilder.declarativeTable().withProps({
+ this._statusTable = this._view.modelBuilder.table().withProps({
ariaLabel: loc.MIGRATION_STATUS,
+ data: [],
+ forceFitColumns: azdata.ColumnSizingMode.ForceFit,
+ height: '600px',
+ width: '1095px',
+ display: 'grid',
columns: [
- {
- displayName: loc.DATABASE,
- valueType: azdata.DeclarativeDataType.component,
- width: '90px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.DATABASE,
+ value: 'database',
+ width: 190,
+ type: azdata.ColumnType.hyperlink,
+ icon: IconPathHelper.sqlDatabaseLogo,
+ showText: true,
+ },
+ {
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.STATUS_COLUMN,
+ value: 'status',
+ width: 120,
+ type: azdata.ColumnType.hyperlink,
},
{
- displayName: loc.MIGRATION_STATUS,
- valueType: azdata.DeclarativeDataType.component,
- width: '170px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.MIGRATION_MODE,
+ value: 'mode',
+ width: 85,
+ type: azdata.ColumnType.text,
},
{
- displayName: loc.MIGRATION_MODE,
- valueType: azdata.DeclarativeDataType.string,
- width: '90px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.AZURE_SQL_TARGET,
+ value: 'targetType',
+ width: 120,
+ type: azdata.ColumnType.text,
},
{
- displayName: loc.AZURE_SQL_TARGET,
- valueType: azdata.DeclarativeDataType.string,
- width: '130px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
+ value: 'targetName',
+ width: 125,
+ type: azdata.ColumnType.text,
},
{
- displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
- valueType: azdata.DeclarativeDataType.string,
- width: '130px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.DATABASE_MIGRATION_SERVICE,
+ value: 'migrationService',
+ width: 140,
+ type: azdata.ColumnType.text,
},
{
- displayName: loc.DATABASE_MIGRATION_SERVICE,
- valueType: azdata.DeclarativeDataType.string,
- width: '150px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.DURATION,
+ value: 'duration',
+ width: 50,
+ type: azdata.ColumnType.text,
},
{
- displayName: loc.DURATION,
- valueType: azdata.DeclarativeDataType.string,
- width: '55px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.START_TIME,
+ value: 'startTime',
+ width: 115,
+ type: azdata.ColumnType.text,
},
{
- displayName: loc.START_TIME,
- valueType: azdata.DeclarativeDataType.string,
- width: '140px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
+ cssClass: rowCssStyles,
+ headerCssClass: headerCssStyles,
+ name: loc.FINISH_TIME,
+ value: 'finishTime',
+ width: 115,
+ type: azdata.ColumnType.text,
},
- {
- displayName: loc.FINISH_TIME,
- valueType: azdata.DeclarativeDataType.string,
- width: '140px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles
- },
- {
- displayName: '',
- valueType: azdata.DeclarativeDataType.menu,
- width: '20px',
- isReadOnly: true,
- rowCssStyles: rowCssStyle,
- headerCssStyles: headerCssStyles,
- }
]
}).component();
+
+ this._disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
+ const buttonState = rowState;
+ switch (buttonState?.column) {
+ case 0:
+ case 1:
+ const migration = this._filteredMigrations[rowState.row];
+ const dialog = new MigrationCutoverDialog(
+ this._context,
+ await MigrationLocalStorage.getMigrationServiceContext(),
+ migration,
+ this._onClosedCallback);
+ await dialog.initialize();
+ break;
+ }
+ }));
+
return this._statusTable;
}
-
- private _statusInfoMap(status: string): azdata.IconPath {
- return status === MigrationStatus.InProgress
- || status === MigrationStatus.Creating
- || status === MigrationStatus.Completing
- ? IconPathHelper.warning
- : IconPathHelper.error;
- }
}
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
index b28f56f2b7..f01e971254 100644
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts
@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
+import { DatabaseMigration } from '../../api/azure';
import * as loc from '../../constants/strings';
-import { MigrationContext } from '../../models/migrationLocalStorage';
export class MigrationStatusDialogModel {
public statusDropdownValues: azdata.CategoryValue[] = [
@@ -27,7 +27,7 @@ export class MigrationStatusDialogModel {
}
];
- constructor(public _migrations: MigrationContext[]) {
+ constructor(public _migrations: DatabaseMigration[]) {
}
}
diff --git a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts
index e7e877225e..be0fad765d 100644
--- a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts
+++ b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts
@@ -7,26 +7,26 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from 'mssql';
import { azureResource } from 'azureResource';
-import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure';
+import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName, DatabaseMigration, getMigrationTargetInstance } from '../../api/azure';
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine';
-import { MigrationContext } from '../../models/migrationLocalStorage';
+import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
import * as constants from '../../constants/strings';
export class RetryMigrationDialog {
- private _context: vscode.ExtensionContext;
- private _migration: MigrationContext;
- constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
- this._context = context;
- this._migration = migration;
+ constructor(
+ private readonly _context: vscode.ExtensionContext,
+ private readonly _serviceContext: MigrationServiceContext,
+ private readonly _migration: DatabaseMigration,
+ private readonly _onClosedCallback: () => Promise) {
}
- private createMigrationStateModel(migration: MigrationContext, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): MigrationStateModel {
+ private async createMigrationStateModel(serviceContext: MigrationServiceContext, migration: DatabaseMigration, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): Promise {
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
- const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName;
+ const sourceDatabaseName = migration.properties.sourceDatabaseName;
let savedInfo: SavedInfo;
savedInfo = {
closedPage: 0,
@@ -41,53 +41,56 @@ export class RetryMigrationDialog {
migrationTargetType: getMigrationTargetTypeEnum(migration)!,
// TargetSelection
- azureAccount: migration.azureAccount,
- azureTenant: migration.azureAccount.properties.tenants[0],
- subscription: migration.subscription,
+ azureAccount: serviceContext.azureAccount!,
+ azureTenant: serviceContext.azureAccount!.properties.tenants[0]!,
+ subscription: serviceContext.subscription!,
location: location,
resourceGroup: {
- id: getFullResourceGroupFromId(migration.targetManagedInstance.id),
- name: getResourceGroupFromId(migration.targetManagedInstance.id),
- subscription: migration.subscription
+ id: getFullResourceGroupFromId(migration.id),
+ name: getResourceGroupFromId(migration.id),
+ subscription: serviceContext.subscription!,
},
- targetServerInstance: migration.targetManagedInstance,
+ targetServerInstance: await getMigrationTargetInstance(
+ serviceContext.azureAccount!,
+ serviceContext.subscription!,
+ migration),
// MigrationMode
migrationMode: getMigrationModeEnum(migration),
// DatabaseBackup
- targetDatabaseNames: [migration.migrationContext.name],
+ targetDatabaseNames: [migration.name],
networkContainerType: null,
networkShares: [],
blobs: [],
// Integration Runtime
- sqlMigrationService: migration.controller,
+ sqlMigrationService: serviceContext.migrationService,
};
- const getStorageAccountResourceGroup = (storageAccountResourceId: string) => {
+ const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => {
return {
id: getFullResourceGroupFromId(storageAccountResourceId!),
name: getResourceGroupFromId(storageAccountResourceId!),
- subscription: migration.subscription
+ subscription: this._serviceContext.subscription!
};
};
- const getStorageAccount = (storageAccountResourceId: string) => {
+ const getStorageAccount = (storageAccountResourceId: string): azureResource.AzureGraphResource => {
const storageAccountName = getResourceName(storageAccountResourceId);
return {
type: 'microsoft.storage/storageaccounts',
id: storageAccountResourceId!,
tenantId: savedInfo.azureTenant?.id!,
- subscriptionId: migration.subscription.id,
+ subscriptionId: this._serviceContext.subscription?.id!,
name: storageAccountName,
location: savedInfo.location!.name,
};
};
- const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation;
+ const sourceLocation = migration.properties.backupConfiguration?.sourceLocation;
if (sourceLocation?.fileShare) {
savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE;
- const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!;
+ const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!;
savedInfo.networkShares = [
{
password: '',
@@ -106,9 +109,9 @@ export class RetryMigrationDialog {
blobContainer: {
id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName),
name: sourceLocation?.azureBlob.blobContainerName,
- subscription: migration.subscription
+ subscription: this._serviceContext.subscription!
},
- lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.migrationContext.properties.offlineConfiguration.lastBackupName! : undefined,
+ lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.properties.offlineConfiguration?.lastBackupName! : undefined,
storageAccount: getStorageAccount(storageAccountResourceId!),
resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!),
storageKey: ''
@@ -123,10 +126,18 @@ export class RetryMigrationDialog {
}
public async openDialog(dialogName?: string) {
- const locations = await getLocations(this._migration.azureAccount, this._migration.subscription);
+ const locations = await getLocations(
+ this._serviceContext.azureAccount!,
+ this._serviceContext.subscription!);
+
+ const targetInstance = await getMigrationTargetInstance(
+ this._serviceContext.azureAccount!,
+ this._serviceContext.subscription!,
+ this._migration);
+
let location: azureResource.AzureLocation;
locations.forEach(azureLocation => {
- if (azureLocation.name === this._migration.targetManagedInstance.location) {
+ if (azureLocation.name === targetInstance.location) {
location = azureLocation;
}
});
@@ -146,10 +157,13 @@ export class RetryMigrationDialog {
}
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
- const stateModel = this.createMigrationStateModel(this._migration, connectionId, serverName, api, location!);
+ const stateModel = await this.createMigrationStateModel(this._serviceContext, this._migration, connectionId, serverName, api, location!);
- if (stateModel.loadSavedInfo()) {
- const wizardController = new WizardController(this._context, stateModel);
+ if (await stateModel.loadSavedInfo()) {
+ const wizardController = new WizardController(
+ this._context,
+ stateModel,
+ this._onClosedCallback);
await wizardController.openWizard(stateModel.sourceConnectionId);
} else {
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);
diff --git a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts
new file mode 100644
index 0000000000..f7a20d3e33
--- /dev/null
+++ b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts
@@ -0,0 +1,597 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 * as azurecore from 'azurecore';
+import { MigrationLocalStorage, MigrationServiceContext } from '../../models/migrationLocalStorage';
+import { azureResource } from 'azureResource';
+import * as styles from '../../constants/styles';
+import * as constants from '../../constants/strings';
+import { findDropDownItemIndex, selectDefaultDropdownValue, deepClone } from '../../api/utils';
+import { getFullResourceGroupFromId, getLocations, getSqlMigrationServices, getSubscriptions, SqlMigrationService } from '../../api/azure';
+import { logError, TelemetryViews } from '../../telemtery';
+
+const CONTROL_MARGIN = '20px';
+const INPUT_COMPONENT_WIDTH = '100%';
+const STYLE_HIDE = { 'display': 'none' };
+const STYLE_ShOW = { 'display': 'inline' };
+export const BODY_CSS = {
+ 'font-size': '13px',
+ 'line-height': '18px',
+ 'margin': '4px 0',
+};
+const LABEL_CSS = {
+ ...styles.LABEL_CSS,
+ 'margin': '0 0 0 0',
+ 'font-weight': '600',
+};
+const DROPDOWN_CSS = {
+ 'margin': '-1em 0 0 0',
+};
+const TENANT_DROPDOWN_CSS = {
+ 'margin': '1em 0 0 0',
+};
+
+export class SelectMigrationServiceDialog {
+ private _dialog: azdata.window.Dialog;
+ private _view!: azdata.ModelView;
+ private _disposables: vscode.Disposable[] = [];
+ private _serviceContext!: MigrationServiceContext;
+ private _azureAccounts!: azdata.Account[];
+ private _accountTenants!: azurecore.Tenant[];
+ private _subscriptions!: azureResource.AzureResourceSubscription[];
+ private _locations!: azureResource.AzureLocation[];
+ private _resourceGroups!: azureResource.AzureResourceResourceGroup[];
+ private _sqlMigrationServices!: SqlMigrationService[];
+ private _azureAccountsDropdown!: azdata.DropDownComponent;
+ private _accountTenantDropdown!: azdata.DropDownComponent;
+ private _accountTenantFlexContainer!: azdata.FlexContainer;
+ private _azureSubscriptionDropdown!: azdata.DropDownComponent;
+ private _azureLocationDropdown!: azdata.DropDownComponent;
+ private _azureResourceGroupDropdown!: azdata.DropDownComponent;
+ private _azureServiceDropdownLabel!: azdata.TextComponent;
+ private _azureServiceDropdown!: azdata.DropDownComponent;
+ private _deleteButton!: azdata.window.Button;
+
+ constructor(
+ private readonly _onClosedCallback: () => Promise) {
+ this._dialog = azdata.window.createModelViewDialog(
+ constants.MIGRATION_SERVICE_SELECT_TITLE,
+ 'SelectMigraitonServiceDialog',
+ 460,
+ 'normal');
+ }
+
+ async initialize(): Promise {
+ this._serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
+
+ await this._dialog.registerContent(async (view: azdata.ModelView) => {
+ this._disposables.push(
+ view.onClosed(e => {
+ this._disposables.forEach(
+ d => { try { d.dispose(); } catch { } });
+ }));
+ await this.registerContent(view);
+ });
+
+ this._dialog.okButton.label = constants.MIGRATION_SERVICE_SELECT_APPLY_LABEL;
+ this._dialog.okButton.position = 'left';
+ this._dialog.cancelButton.position = 'right';
+
+ this._deleteButton = azdata.window.createButton(
+ constants.MIGRATION_SERVICE_CLEAR,
+ 'right');
+ this._disposables.push(
+ this._deleteButton.onClick(async (value) => {
+ await MigrationLocalStorage.saveMigrationServiceContext({});
+ await this._onClosedCallback();
+ azdata.window.closeDialog(this._dialog);
+ }));
+ this._dialog.customButtons = [this._deleteButton];
+
+ azdata.window.openDialog(this._dialog);
+ }
+
+ protected async registerContent(view: azdata.ModelView): Promise {
+ this._view = view;
+
+ const flexContainer = this._view.modelBuilder
+ .flexContainer()
+ .withItems([
+ this._createHeading(),
+ this._createAzureAccountsDropdown(),
+ this._createAzureTenantContainer(),
+ this._createServiceSelectionContainer(),
+ ])
+ .withLayout({ flexFlow: 'column' })
+ .withProps({ CSSStyles: { 'padding': CONTROL_MARGIN } })
+ .component();
+
+ await this._view.initializeModel(flexContainer);
+ await this._populateAzureAccountsDropdown();
+ }
+
+ private _createHeading(): azdata.TextComponent {
+ return this._view.modelBuilder.text()
+ .withProps({
+ value: constants.MIGRATION_SERVICE_SELECT_HEADING,
+ CSSStyles: { ...styles.BODY_CSS }
+ }).component();
+ }
+
+ private _createAzureAccountsDropdown(): azdata.FlexContainer {
+ const azureAccountLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
+ requiredIndicator: true,
+ CSSStyles: { ...LABEL_CSS }
+ }).component();
+ this._azureAccountsDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
+ width: INPUT_COMPONENT_WIDTH,
+ editable: true,
+ required: true,
+ fireOnTextChange: true,
+ placeholder: constants.SELECT_AN_ACCOUNT,
+ CSSStyles: { ...DROPDOWN_CSS },
+ }).component();
+ this._disposables.push(
+ this._azureAccountsDropdown.onValueChanged(async (value) => {
+ const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value);
+ this._serviceContext.azureAccount = (selectedIndex > -1)
+ ? deepClone(this._azureAccounts[selectedIndex])
+ : undefined!;
+ await this._populateTentantsDropdown();
+ }));
+
+ const linkAccountButton = this._view.modelBuilder.hyperlink()
+ .withProps({
+ label: constants.ACCOUNT_LINK_BUTTON_LABEL,
+ url: '',
+ CSSStyles: { ...styles.BODY_CSS },
+ }).component();
+
+ this._disposables.push(
+ linkAccountButton.onDidClick(async (event) => {
+ await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
+ await this._populateAzureAccountsDropdown();
+ }));
+
+ return this._view.modelBuilder.flexContainer()
+ .withLayout({ flexFlow: 'column' })
+ .withItems([
+ azureAccountLabel,
+ this._azureAccountsDropdown,
+ linkAccountButton,
+ ]).component();
+ }
+
+ private _createAzureTenantContainer(): azdata.FlexContainer {
+ const azureTenantDropdownLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: constants.AZURE_TENANT,
+ CSSStyles: { ...LABEL_CSS, ...TENANT_DROPDOWN_CSS },
+ }).component();
+ this._accountTenantDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: constants.AZURE_TENANT,
+ width: INPUT_COMPONENT_WIDTH,
+ editable: true,
+ fireOnTextChange: true,
+ placeholder: constants.SELECT_A_TENANT,
+ }).component();
+ this._disposables.push(
+ this._accountTenantDropdown.onValueChanged(async value => {
+ const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value);
+ this._serviceContext.tenant = (selectedIndex > -1)
+ ? deepClone(this._accountTenants[selectedIndex])
+ : undefined!;
+ await this._populateSubscriptionDropdown();
+ }));
+
+ this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer()
+ .withLayout({ flexFlow: 'column' })
+ .withItems([
+ azureTenantDropdownLabel,
+ this._accountTenantDropdown,
+ ])
+ .withProps({ CSSStyles: { ...STYLE_HIDE, } })
+ .component();
+ return this._accountTenantFlexContainer;
+ }
+
+ private _createServiceSelectionContainer(): azdata.FlexContainer {
+ const subscriptionDropdownLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: constants.SUBSCRIPTION,
+ description: constants.TARGET_SUBSCRIPTION_INFO,
+ requiredIndicator: true,
+ CSSStyles: { ...LABEL_CSS }
+ }).component();
+ this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: constants.SUBSCRIPTION,
+ width: INPUT_COMPONENT_WIDTH,
+ editable: true,
+ required: true,
+ fireOnTextChange: true,
+ placeholder: constants.SELECT_A_SUBSCRIPTION,
+ CSSStyles: { ...DROPDOWN_CSS },
+ }).component();
+ this._disposables.push(
+ this._azureSubscriptionDropdown.onValueChanged(async (value) => {
+ const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value);
+ this._serviceContext.subscription = (selectedIndex > -1)
+ ? deepClone(this._subscriptions[selectedIndex])
+ : undefined!;
+ await this._populateLocationDropdown();
+ }));
+
+ const azureLocationLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: constants.LOCATION,
+ description: constants.TARGET_LOCATION_INFO,
+ requiredIndicator: true,
+ CSSStyles: { ...LABEL_CSS }
+ }).component();
+ this._azureLocationDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: constants.LOCATION,
+ width: INPUT_COMPONENT_WIDTH,
+ editable: true,
+ required: true,
+ fireOnTextChange: true,
+ placeholder: constants.SELECT_A_LOCATION,
+ CSSStyles: { ...DROPDOWN_CSS },
+ }).component();
+ this._disposables.push(
+ this._azureLocationDropdown.onValueChanged(async (value) => {
+ const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value);
+ this._serviceContext.location = (selectedIndex > -1)
+ ? deepClone(this._locations[selectedIndex])
+ : undefined!;
+ await this._populateResourceGroupDropdown();
+ }));
+
+ const azureResourceGroupLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: constants.RESOURCE_GROUP,
+ description: constants.TARGET_RESOURCE_GROUP_INFO,
+ requiredIndicator: true,
+ CSSStyles: { ...LABEL_CSS }
+ }).component();
+ this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: constants.RESOURCE_GROUP,
+ width: INPUT_COMPONENT_WIDTH,
+ editable: true,
+ required: true,
+ fireOnTextChange: true,
+ placeholder: constants.SELECT_A_RESOURCE_GROUP,
+ CSSStyles: { ...DROPDOWN_CSS },
+ }).component();
+ this._disposables.push(
+ this._azureResourceGroupDropdown.onValueChanged(async (value) => {
+ const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value);
+ this._serviceContext.resourceGroup = (selectedIndex > -1)
+ ? deepClone(this._resourceGroups[selectedIndex])
+ : undefined!;
+ await this._populateMigrationServiceDropdown();
+ }));
+
+ this._azureServiceDropdownLabel = this._view.modelBuilder.text()
+ .withProps({
+ value: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL,
+ description: constants.TARGET_RESOURCE_INFO,
+ requiredIndicator: true,
+ CSSStyles: { ...LABEL_CSS }
+ }).component();
+ this._azureServiceDropdown = this._view.modelBuilder.dropDown()
+ .withProps({
+ ariaLabel: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL,
+ width: INPUT_COMPONENT_WIDTH,
+ editable: true,
+ required: true,
+ fireOnTextChange: true,
+ placeholder: constants.SELECT_A_SERVICE,
+ CSSStyles: { ...DROPDOWN_CSS },
+ }).component();
+ this._disposables.push(
+ this._azureServiceDropdown.onValueChanged(async (value) => {
+ const selectedIndex = findDropDownItemIndex(this._azureServiceDropdown, value, true);
+ this._serviceContext.migrationService = (selectedIndex > -1)
+ ? deepClone(this._sqlMigrationServices.find(service => service.name === value))
+ : undefined!;
+ await this._updateButtonState();
+ }));
+
+ this._disposables.push(
+ this._dialog.okButton.onClick(async (value) => {
+ await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext);
+ await this._onClosedCallback();
+ }));
+
+ return this._view.modelBuilder.flexContainer()
+ .withItems([
+ subscriptionDropdownLabel,
+ this._azureSubscriptionDropdown,
+ azureLocationLabel,
+ this._azureLocationDropdown,
+ azureResourceGroupLabel,
+ this._azureResourceGroupDropdown,
+ this._azureServiceDropdownLabel,
+ this._azureServiceDropdown,
+ ]).withLayout({ flexFlow: 'column' })
+ .component();
+ }
+
+ private async _updateButtonState(): Promise {
+ this._dialog.okButton.enabled = this._serviceContext.migrationService !== undefined;
+ }
+
+ private async _populateAzureAccountsDropdown(): Promise {
+ try {
+ this._azureAccountsDropdown.loading = true;
+ this._azureAccountsDropdown.values = await this._getAccountDropdownValues();
+ if (this._azureAccountsDropdown.values.length > 0) {
+ selectDefaultDropdownValue(
+ this._azureAccountsDropdown,
+ this._serviceContext.azureAccount?.displayInfo?.userId,
+ false);
+ this._azureAccountsDropdown.loading = false;
+ }
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_populateAzureAccountsDropdown', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_ACCOUNT_ERROR,
+ error.message);
+ } finally {
+ this._azureAccountsDropdown.loading = false;
+ }
+ }
+
+ private async _populateTentantsDropdown(): Promise {
+ try {
+ this._accountTenantDropdown.loading = true;
+ this._accountTenantDropdown.values = this._getTenantDropdownValues(
+ this._serviceContext.azureAccount);
+ await this._accountTenantFlexContainer.updateCssStyles(
+ this._accountTenants.length > 1
+ ? STYLE_ShOW
+ : STYLE_HIDE);
+ if (this._accountTenantDropdown.values.length > 0) {
+ selectDefaultDropdownValue(
+ this._accountTenantDropdown,
+ this._serviceContext.tenant?.id,
+ false);
+ this._accountTenantDropdown.loading = false;
+ }
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_populateTentantsDropdown', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_TENANT_ERROR,
+ error.message);
+ } finally {
+ this._accountTenantDropdown.loading = false;
+ }
+ }
+
+ private async _populateSubscriptionDropdown(): Promise {
+ try {
+ this._azureSubscriptionDropdown.loading = true;
+ this._azureSubscriptionDropdown.values = await this._getSubscriptionDropdownValues(
+ this._serviceContext.azureAccount);
+ if (this._azureSubscriptionDropdown.values.length > 0) {
+ selectDefaultDropdownValue(
+ this._azureSubscriptionDropdown,
+ this._serviceContext.subscription?.id,
+ false);
+ this._azureSubscriptionDropdown.loading = false;
+ }
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_populateSubscriptionDropdown', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_SUBSCRIPTION_ERROR,
+ error.message);
+ } finally {
+ this._azureSubscriptionDropdown.loading = false;
+ }
+ }
+
+ private async _populateLocationDropdown(): Promise {
+ try {
+ this._azureLocationDropdown.loading = true;
+ this._azureLocationDropdown.values = await this._getAzureLocationDropdownValues(
+ this._serviceContext.azureAccount,
+ this._serviceContext.subscription);
+ if (this._azureLocationDropdown.values.length > 0) {
+ selectDefaultDropdownValue(
+ this._azureLocationDropdown,
+ this._serviceContext.location?.displayName,
+ true);
+ this._azureLocationDropdown.loading = false;
+ }
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_populateLocationDropdown', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_LOCATION_ERROR,
+ error.message);
+ } finally {
+ this._azureLocationDropdown.loading = false;
+ }
+ }
+
+ private async _populateResourceGroupDropdown(): Promise {
+ try {
+ this._azureResourceGroupDropdown.loading = true;
+ this._azureResourceGroupDropdown.values = await this._getAzureResourceGroupDropdownValues(
+ this._serviceContext.location);
+ if (this._azureResourceGroupDropdown.values.length > 0) {
+ selectDefaultDropdownValue(
+ this._azureResourceGroupDropdown,
+ this._serviceContext.resourceGroup?.id,
+ false);
+ this._azureResourceGroupDropdown.loading = false;
+ }
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_populateResourceGroupDropdown', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_RESOURCE_GROUP_ERROR,
+ error.message);
+ } finally {
+ this._azureResourceGroupDropdown.loading = false;
+ }
+ }
+
+ private async _populateMigrationServiceDropdown(): Promise {
+ try {
+ this._azureServiceDropdown.loading = true;
+ this._azureServiceDropdown.values = await this._getMigrationServiceDropdownValues(
+ this._serviceContext.azureAccount,
+ this._serviceContext.subscription,
+ this._serviceContext.location,
+ this._serviceContext.resourceGroup);
+
+ if (this._azureServiceDropdown.values.length > 0) {
+ selectDefaultDropdownValue(
+ this._azureServiceDropdown,
+ this._serviceContext?.migrationService?.id,
+ false);
+ }
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_populateMigrationServiceDropdown', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_SERVICE_ERROR,
+ error.message);
+ } finally {
+ this._azureServiceDropdown.loading = false;
+ }
+ }
+
+ private async _getAccountDropdownValues(): Promise {
+ this._azureAccounts = await azdata.accounts.getAllAccounts() || [];
+ return this._azureAccounts.map(account => {
+ return {
+ name: account.displayInfo.userId,
+ displayName: account.isStale
+ ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName)
+ : account.displayInfo.displayName,
+ };
+ });
+ }
+
+ private async _getSubscriptionDropdownValues(account?: azdata.Account): Promise {
+ this._subscriptions = [];
+ if (account?.isStale === false) {
+ try {
+ this._subscriptions = await getSubscriptions(account);
+ this._subscriptions.sort((a, b) => a.name.localeCompare(b.name));
+ } catch (error) {
+ logError(TelemetryViews.SelectMigrationServiceDialog, '_getSubscriptionDropdownValues', error);
+ void vscode.window.showErrorMessage(
+ constants.SELECT_SUBSCRIPTION_ERROR,
+ error.message);
+ }
+ }
+
+ return this._subscriptions.map(subscription => {
+ return {
+ name: subscription.id,
+ displayName: `${subscription.name} - ${subscription.id}`,
+ };
+ });
+ }
+
+ private _getTenantDropdownValues(account?: azdata.Account): azdata.CategoryValue[] {
+ this._accountTenants = account?.isStale === false
+ ? account?.properties?.tenants ?? []
+ : [];
+
+ return this._accountTenants.map(tenant => {
+ return {
+ name: tenant.id,
+ displayName: tenant.displayName,
+ };
+ });
+ }
+
+ private async _getAzureLocationDropdownValues(
+ account?: azdata.Account,
+ subscription?: azureResource.AzureResourceSubscription): Promise {
+ let locations: azureResource.AzureLocation[] = [];
+ if (account && subscription) {
+ // get all available locations
+ locations = await getLocations(account, subscription);
+ this._sqlMigrationServices = await getSqlMigrationServices(
+ account,
+ subscription) || [];
+ this._sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name));
+ } else {
+ this._sqlMigrationServices = [];
+ }
+
+ // keep locaitons with services only
+ this._locations = locations.filter(
+ (loc, i) => this._sqlMigrationServices.some(service => service.location === loc.name));
+ this._locations.sort((a, b) => a.name.localeCompare(b.name));
+ return this._locations.map(loc => {
+ return {
+ name: loc.name,
+ displayName: loc.displayName,
+ };
+ });
+ }
+
+ private async _getAzureResourceGroupDropdownValues(location?: azureResource.AzureLocation): Promise {
+ this._resourceGroups = location
+ ? this._getMigrationServicesResourceGroups(location)
+ : [];
+ this._resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
+ return this._resourceGroups.map(rg => {
+ return {
+ name: rg.id,
+ displayName: rg.name,
+ };
+ });
+ }
+
+ private _getMigrationServicesResourceGroups(location?: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] {
+ const resourceGroups = this._sqlMigrationServices
+ .filter(service => service.location === location?.name)
+ .map(service => service.properties.resourceGroup);
+
+ return resourceGroups
+ .filter((rg, i, arr) => arr.indexOf(rg) === i)
+ .map(rg => {
+ return {
+ id: getFullResourceGroupFromId(rg),
+ name: rg,
+ };
+ });
+ }
+
+ private async _getMigrationServiceDropdownValues(
+ account?: azdata.Account,
+ subscription?: azureResource.AzureResourceSubscription,
+ location?: azureResource.AzureLocation,
+ resourceGroup?: azureResource.AzureResourceResourceGroup): Promise {
+
+ const locationName = location?.name?.toLowerCase();
+ const resourceGroupName = resourceGroup?.name?.toLowerCase();
+
+ return this._sqlMigrationServices
+ .filter(service =>
+ service.location?.toLowerCase() === locationName &&
+ service.properties?.resourceGroup?.toLowerCase() === resourceGroupName)
+ .map(service => {
+ return ({
+ name: service.id,
+ displayName: `${service.name}`,
+ });
+ });
+ }
+}
diff --git a/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts b/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts
index 5975ad86f3..6860beffb6 100644
--- a/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts
+++ b/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts
@@ -5,10 +5,10 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
-import { getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure';
+import { DatabaseMigration, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper';
import * as constants from '../../constants/strings';
-import { MigrationContext } from '../../models/migrationLocalStorage';
+import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import * as styles from '../../constants/styles';
const CONTROL_MARGIN = '10px';
@@ -28,7 +28,10 @@ export class SqlMigrationServiceDetailsDialog {
private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
private _disposables: vscode.Disposable[] = [];
- constructor(private migrationContext: MigrationContext) {
+ constructor(
+ private _serviceContext: MigrationServiceContext,
+ private _migration: DatabaseMigration) {
+
this._dialog = azdata.window.createModelViewDialog(
'',
'SqlMigrationServiceDetailsDialog',
@@ -46,7 +49,8 @@ export class SqlMigrationServiceDetailsDialog {
await this.createServiceContent(
view,
- this.migrationContext);
+ this._serviceContext,
+ this._migration);
});
this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL;
@@ -55,33 +59,31 @@ export class SqlMigrationServiceDetailsDialog {
azdata.window.openDialog(this._dialog);
}
- private async createServiceContent(view: azdata.ModelView, migrationContext: MigrationContext): Promise {
+ private async createServiceContent(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise {
this._migrationServiceAuthKeyTable = this._createIrTable(view);
const serviceNode = (await getSqlMigrationServiceMonitoringData(
- migrationContext.azureAccount,
- migrationContext.subscription,
- migrationContext.controller.properties.resourceGroup,
- migrationContext.controller.location,
- migrationContext.controller.name,
- this.migrationContext.sessionId!
- ));
+ serviceContext.azureAccount!,
+ serviceContext.subscription!,
+ serviceContext.migrationService?.properties.resourceGroup!,
+ serviceContext.migrationService?.location!,
+ serviceContext.migrationService?.name!));
const serviceNodeName = serviceNode.nodes?.map(node => node.nodeName).join(', ')
|| constants.SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE;
const flexContainer = view.modelBuilder
.flexContainer()
.withItems([
- this._createHeading(view, migrationContext),
+ this._createHeading(view, this._migration),
view.modelBuilder
.separator()
.withProps({ width: STRETCH_WIDTH })
.component(),
this._createTextItem(view, constants.SUBSCRIPTION, LABEL_MARGIN),
- this._createTextItem(view, migrationContext.subscription.name, VALUE_MARGIN),
+ this._createTextItem(view, serviceContext.subscription?.name!, VALUE_MARGIN),
this._createTextItem(view, constants.LOCATION, LABEL_MARGIN),
- this._createTextItem(view, migrationContext.controller.location.toUpperCase(), VALUE_MARGIN),
+ this._createTextItem(view, serviceContext.migrationService?.location?.toUpperCase()!, VALUE_MARGIN),
this._createTextItem(view, constants.RESOURCE_GROUP, LABEL_MARGIN),
- this._createTextItem(view, migrationContext.controller.properties.resourceGroup, VALUE_MARGIN),
+ this._createTextItem(view, serviceContext.migrationService?.properties.resourceGroup!, VALUE_MARGIN),
this._createTextItem(view, constants.SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL, LABEL_MARGIN),
this._createTextItem(view, serviceNodeName, VALUE_MARGIN),
this._createTextItem(
@@ -96,10 +98,10 @@ export class SqlMigrationServiceDetailsDialog {
.component();
await view.initializeModel(flexContainer);
- return await this._refreshAuthTable(view, migrationContext);
+ return await this._refreshAuthTable(view, serviceContext, migration);
}
- private _createHeading(view: azdata.ModelView, migrationContext: MigrationContext): azdata.FlexContainer {
+ private _createHeading(view: azdata.ModelView, migration: DatabaseMigration): azdata.FlexContainer {
return view.modelBuilder
.flexContainer()
.withItems([
@@ -120,19 +122,15 @@ export class SqlMigrationServiceDetailsDialog {
view.modelBuilder
.text()
.withProps({
- value: migrationContext.controller.name,
- CSSStyles: {
- ...styles.SECTION_HEADER_CSS
- }
+ value: this._serviceContext.migrationService?.name,
+ CSSStyles: { ...styles.SECTION_HEADER_CSS }
})
.component(),
view.modelBuilder
.text()
.withProps({
value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE,
- CSSStyles: {
- ...styles.SMALL_NOTE_CSS
- }
+ CSSStyles: { ...styles.SMALL_NOTE_CSS }
})
.component(),
])
@@ -197,15 +195,14 @@ export class SqlMigrationServiceDetailsDialog {
};
}
- private async _regenerateAuthKey(view: azdata.ModelView, migrationContext: MigrationContext, keyName: string): Promise {
+ private async _regenerateAuthKey(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration, keyName: string): Promise {
const keys = await regenerateSqlMigrationServiceAuthKey(
- migrationContext.azureAccount,
- migrationContext.subscription,
- migrationContext.controller.properties.resourceGroup,
- migrationContext.controller.location.toUpperCase(),
- migrationContext.controller.name,
- keyName,
- migrationContext.sessionId!);
+ serviceContext.azureAccount!,
+ serviceContext.subscription!,
+ serviceContext.migrationService?.properties.resourceGroup!,
+ serviceContext.migrationService?.properties.location?.toUpperCase()!,
+ serviceContext.migrationService?.name!,
+ keyName);
if (keys?.authKey1 && keyName === AUTH_KEY1) {
await this._updateTableCell(this._migrationServiceAuthKeyTable, 0, 1, keys.authKey1, constants.SERVICE_KEY1_LABEL);
@@ -223,14 +220,13 @@ export class SqlMigrationServiceDetailsDialog {
await vscode.window.showInformationMessage(constants.AUTH_KEY_REFRESHED(keyName));
}
- private async _refreshAuthTable(view: azdata.ModelView, migrationContext: MigrationContext): Promise {
+ private async _refreshAuthTable(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise {
const keys = await getSqlMigrationServiceAuthKeys(
- migrationContext.azureAccount,
- migrationContext.subscription,
- migrationContext.controller.properties.resourceGroup,
- migrationContext.controller.location.toUpperCase(),
- migrationContext.controller.name,
- migrationContext.sessionId!);
+ serviceContext.azureAccount!,
+ serviceContext.subscription!,
+ serviceContext.migrationService?.properties.resourceGroup!,
+ serviceContext.migrationService?.location.toUpperCase()!,
+ serviceContext.migrationService?.name!);
const copyKey1Button = view.modelBuilder
.button()
@@ -275,7 +271,7 @@ export class SqlMigrationServiceDetailsDialog {
})
.component();
this._disposables.push(refreshKey1Button.onDidClick(
- async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY1)));
+ async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY1)));
const refreshKey2Button = view.modelBuilder
.button()
@@ -288,7 +284,7 @@ export class SqlMigrationServiceDetailsDialog {
})
.component();
this._disposables.push(refreshKey2Button.onDidClick(
- async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY2)));
+ async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY2)));
await this._migrationServiceAuthKeyTable.updateProperties({
dataValues: [
diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts
index ef4c18988f..b7475d5b18 100644
--- a/extensions/sql-migration/src/main.ts
+++ b/extensions/sql-migration/src/main.ts
@@ -32,47 +32,54 @@ class SQLMigration {
async registerCommands(): Promise {
const commandDisposables: vscode.Disposable[] = [ // Array of disposables returned by registerCommand
- vscode.commands.registerCommand('sqlmigration.start', async () => {
- await this.launchMigrationWizard();
- }),
- vscode.commands.registerCommand('sqlmigration.openNotebooks', async () => {
- const input = vscode.window.createQuickPick();
- input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
+ vscode.commands.registerCommand(
+ 'sqlmigration.start',
+ async () => await this.launchMigrationWizard()),
+ vscode.commands.registerCommand(
+ 'sqlmigration.openNotebooks',
+ async () => {
+ const input = vscode.window.createQuickPick();
+ input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
- input.items = NotebookPathHelper.getAllMigrationNotebooks();
+ input.items = NotebookPathHelper.getAllMigrationNotebooks();
- this.context.subscriptions.push(input.onDidAccept(async (e) => {
- const selectedNotebook = input.selectedItems[0];
- if (selectedNotebook) {
- try {
- await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
- preview: false,
- initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
- initialDirtyState: false
- });
- } catch (e) {
- void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
+ this.context.subscriptions.push(input.onDidAccept(async (e) => {
+ const selectedNotebook = input.selectedItems[0];
+ if (selectedNotebook) {
+ try {
+ await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
+ preview: false,
+ initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
+ initialDirtyState: false
+ });
+ } catch (e) {
+ void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
+ }
+ input.hide();
}
- input.hide();
- }
- }));
+ }));
- input.show();
- }),
- azdata.tasks.registerTask('sqlmigration.start', async () => {
- await this.launchMigrationWizard();
- }),
- azdata.tasks.registerTask('sqlmigration.newsupportrequest', async () => {
- await this.launchNewSupportRequest();
- }),
- azdata.tasks.registerTask('sqlmigration.sendfeedback', async () => {
- const actionId = 'workbench.action.openIssueReporter';
- const args = {
- extensionId: 'microsoft.sql-migration',
- issueTitle: loc.FEEDBACK_ISSUE_TITLE,
- };
- return await vscode.commands.executeCommand(actionId, args);
- }),
+ input.show();
+ }),
+ azdata.tasks.registerTask(
+ 'sqlmigration.start',
+ async () => await this.launchMigrationWizard()),
+ azdata.tasks.registerTask(
+ 'sqlmigration.newsupportrequest',
+ async () => await this.launchNewSupportRequest()),
+ azdata.tasks.registerTask(
+ 'sqlmigration.sendfeedback',
+ async () => {
+ const actionId = 'workbench.action.openIssueReporter';
+ const args = {
+ extensionId: 'microsoft.sql-migration',
+ issueTitle: loc.FEEDBACK_ISSUE_TITLE,
+ };
+ return await vscode.commands.executeCommand(actionId, args);
+ }),
+ azdata.tasks.registerTask(
+ 'sqlmigration.refreshmigrations',
+ async (e) => await widget?.refreshMigrations()),
];
this.context.subscriptions.push(...commandDisposables);
@@ -97,14 +104,20 @@ class SQLMigration {
if (api) {
this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration);
this.context.subscriptions.push(this.stateModel);
- let savedInfo = this.checkSavedInfo(serverName);
+ const savedInfo = this.checkSavedInfo(serverName);
if (savedInfo) {
this.stateModel.savedInfo = savedInfo;
this.stateModel.serverName = serverName;
- let savedAssessmentDialog = new SavedAssessmentDialog(this.context, this.stateModel);
+ const savedAssessmentDialog = new SavedAssessmentDialog(
+ this.context,
+ this.stateModel,
+ async () => await widget?.onDialogClosed());
await savedAssessmentDialog.openDialog();
} else {
- const wizardController = new WizardController(this.context, this.stateModel);
+ const wizardController = new WizardController(
+ this.context,
+ this.stateModel,
+ async () => await widget?.onDialogClosed());
await wizardController.openWizard(connectionId);
}
}
@@ -131,10 +144,11 @@ class SQLMigration {
}
let sqlMigration: SQLMigration;
+let widget: DashboardWidget;
export async function activate(context: vscode.ExtensionContext) {
sqlMigration = new SQLMigration(context);
await sqlMigration.registerCommands();
- let widget = new DashboardWidget(context);
+ widget = new DashboardWidget(context);
widget.register();
}
diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts
index 69aece0025..90003501a8 100644
--- a/extensions/sql-migration/src/models/migrationLocalStorage.ts
+++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts
@@ -2,11 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import * as vscode from 'vscode';
-import { azureResource } from 'azureResource';
-import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemtery';
-import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails, SqlVMServer, getSubscriptions } from '../api/azure';
import * as azdata from 'azdata';
+import * as vscode from 'vscode';
+import * as azurecore from 'azurecore';
+import { azureResource } from 'azureResource';
+import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure';
+import { deepClone } from '../api/utils';
+import * as loc from '../constants/strings';
export class MigrationLocalStorage {
private static context: vscode.ExtensionContext;
@@ -16,161 +18,75 @@ export class MigrationLocalStorage {
MigrationLocalStorage.context = context;
}
- public static async getMigrationsBySourceConnections(connectionProfile: azdata.connection.ConnectionProfile, refreshStatus?: boolean): Promise {
- const undefinedSessionId = '{undefined}';
- const result: MigrationContext[] = [];
- const validMigrations: MigrationContext[] = [];
- const startTime = new Date().toString();
- // fetch saved migrations
- const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
- for (let i = 0; i < migrationMementos.length; i++) {
- const migration = migrationMementos[i];
- migration.migrationContext = this.removeMigrationSecrets(migration.migrationContext);
- migration.sessionId = migration.sessionId ?? undefinedSessionId;
- if (migration.sourceConnectionProfile.serverName === connectionProfile.serverName) {
- // refresh migration status
- if (refreshStatus) {
- try {
- await this.refreshMigrationAzureAccount(migration);
-
- if (migration.asyncUrl) {
- migration.asyncOperationResult = await getMigrationAsyncOperationDetails(
- migration.azureAccount,
- migration.subscription,
- migration.asyncUrl,
- migration.sessionId!);
- }
-
- migration.migrationContext = await getMigrationStatus(
- migration.azureAccount,
- migration.subscription,
- migration.migrationContext,
- migration.sessionId!);
- }
- catch (e) {
- // Keeping only valid migrations in cache. Clearing all the migrations which return ResourceDoesNotExit error.
- switch (e.message) {
- case 'ResourceDoesNotExist':
- case 'NullMigrationId':
- continue;
- default:
- logError(TelemetryViews.MigrationLocalStorage, 'MigrationBySourceConnectionError', e);
- }
- }
- }
- result.push(migration);
- }
- validMigrations.push(migration);
+ public static async getMigrationServiceContext(): Promise {
+ const connectionProfile = await azdata.connection.getCurrentConnection();
+ if (connectionProfile) {
+ const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
+ return deepClone(await this.context.globalState.get(serverContextKey)) || {};
}
-
- await this.context.globalState.update(this.mementoToken, validMigrations);
-
- sendSqlMigrationActionEvent(
- TelemetryViews.MigrationLocalStorage,
- TelemetryAction.Done,
- {
- 'startTime': startTime,
- 'endTime': new Date().toString()
- },
- {
- 'migrationCount': migrationMementos.length
- }
- );
-
- // only save updated migration context
- if (refreshStatus) {
- const migrations: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
- validMigrations.forEach(migration => {
- const idx = migrations.findIndex(m => m.migrationContext.id === migration.migrationContext.id);
- if (idx > -1) {
- migrations[idx] = migration;
- }
- });
-
- // check global state for migrations count mismatch, avoid saving
- // state if the count has changed when a migration may have been added
- const current: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
- if (current.length === migrations.length) {
- await this.context.globalState.update(this.mementoToken, migrations);
- }
- }
- return result;
+ return {};
}
- public static async refreshMigrationAzureAccount(migration: MigrationContext): Promise {
- if (migration.azureAccount.isStale) {
+ public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): Promise {
+ const connectionProfile = await azdata.connection.getCurrentConnection();
+ if (connectionProfile) {
+ const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
+ return await this.context.globalState.update(serverContextKey, deepClone(serviceContext));
+ }
+ }
+
+ public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise {
+ if (serviceContext.azureAccount?.isStale) {
const accounts = await azdata.accounts.getAllAccounts();
- const account = accounts.find(a => !a.isStale && a.key.accountId === migration.azureAccount.key.accountId);
+ const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId);
if (account) {
const subscriptions = await getSubscriptions(account);
- const subscription = subscriptions.find(s => s.id === migration.subscription.id);
+ const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id);
if (subscription) {
- migration.azureAccount = account;
+ serviceContext.azureAccount = account;
+ await this.saveMigrationServiceContext(serviceContext);
}
}
}
}
-
- public static async saveMigration(
- connectionProfile: azdata.connection.ConnectionProfile,
- migrationContext: DatabaseMigration,
- targetMI: SqlManagedInstance | SqlVMServer,
- azureAccount: azdata.Account,
- subscription: azureResource.AzureResourceSubscription,
- controller: SqlMigrationService,
- asyncURL: string,
- sessionId: string): Promise {
- try {
- let migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
- migrationMementos = migrationMementos.filter(m => m.migrationContext.id !== migrationContext.id);
- migrationMementos.push({
- sourceConnectionProfile: connectionProfile,
- migrationContext: this.removeMigrationSecrets(migrationContext),
- targetManagedInstance: targetMI,
- subscription: subscription,
- azureAccount: azureAccount,
- controller: controller,
- asyncUrl: asyncURL,
- sessionId: sessionId
- });
- await this.context.globalState.update(this.mementoToken, migrationMementos);
- } catch (e) {
- logError(TelemetryViews.MigrationLocalStorage, 'CantSaveMigration', e);
- }
- }
-
- public static async clearMigrations(): Promise {
- await this.context.globalState.update(this.mementoToken, ([] as MigrationContext[]));
- }
-
- public static removeMigrationSecrets(migration: DatabaseMigration): DatabaseMigration {
- // remove secrets from migration context
- if (migration.properties.sourceSqlConnection?.password) {
- migration.properties.sourceSqlConnection.password = '';
- }
- if (migration.properties.backupConfiguration?.sourceLocation?.fileShare?.password) {
- migration.properties.backupConfiguration.sourceLocation.fileShare.password = '';
- }
- if (migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.accountKey) {
- migration.properties.backupConfiguration.sourceLocation.azureBlob.accountKey = '';
- }
- if (migration.properties.backupConfiguration?.targetLocation?.accountKey) {
- migration.properties.backupConfiguration.targetLocation.accountKey = '';
- }
- return migration;
- }
}
-export interface MigrationContext {
- sourceConnectionProfile: azdata.connection.ConnectionProfile,
- migrationContext: DatabaseMigration,
- targetManagedInstance: SqlManagedInstance | SqlVMServer,
- azureAccount: azdata.Account,
- subscription: azureResource.AzureResourceSubscription,
- controller: SqlMigrationService,
- asyncUrl: string,
- asyncOperationResult?: AzureAsyncOperationResource,
- sessionId?: string
+export function isServiceContextValid(serviceContext: MigrationServiceContext): boolean {
+ return (
+ serviceContext.azureAccount?.isStale === false &&
+ serviceContext.location?.id !== undefined &&
+ serviceContext.migrationService?.id !== undefined &&
+ serviceContext.resourceGroup?.id !== undefined &&
+ serviceContext.subscription?.id !== undefined &&
+ serviceContext.tenant?.id !== undefined
+ );
+}
+
+export async function getSelectedServiceStatus(): Promise {
+ const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
+ const serviceName = serviceContext?.migrationService?.name;
+ return serviceName && isServiceContextValid(serviceContext)
+ ? loc.MIGRATION_SERVICE_SERVICE_PROMPT(serviceName)
+ : loc.MIGRATION_SERVICE_SELECT_SERVICE_PROMPT;
+}
+
+export async function getCurrentMigrations(): Promise {
+ const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
+ return isServiceContextValid(serviceContext)
+ ? await getServiceMigrations(
+ serviceContext.azureAccount!,
+ serviceContext.subscription!,
+ serviceContext.migrationService?.id!)
+ : [];
+}
+
+export interface MigrationServiceContext {
+ azureAccount?: azdata.Account,
+ tenant?: azurecore.Tenant,
+ subscription?: azureResource.AzureResourceSubscription,
+ location?: azureResource.AzureLocation,
+ resourceGroup?: azureResource.AzureResourceResourceGroup,
+ migrationService?: SqlMigrationService,
}
export enum MigrationStatus {
diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts
index bb03fd8e69..2b1d837fea 100644
--- a/extensions/sql-migration/src/models/stateMachine.ts
+++ b/extensions/sql-migration/src/models/stateMachine.ts
@@ -8,9 +8,8 @@ import { azureResource } from 'azureResource';
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import * as mssql from 'mssql';
-import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups } from '../api/azure';
+import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups, getSqlMigrationServicesByResourceGroup } from '../api/azure';
import * as constants from '../constants/strings';
-import { MigrationLocalStorage } from './migrationLocalStorage';
import * as nls from 'vscode-nls';
import { v4 as uuidv4 } from 'uuid';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
@@ -58,6 +57,12 @@ export enum NetworkContainerType {
NETWORK_SHARE
}
+export enum FileStorageType {
+ FileShare = 'FileShare',
+ AzureBlob = 'AzureBlob',
+ None = 'None',
+}
+
export enum Page {
DatabaseSelector,
SKURecommendation,
@@ -826,8 +831,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}
accountValues = this._azureAccounts.map((account): azdata.CategoryValue => {
return {
- displayName: account.displayInfo.displayName,
- name: account.displayInfo.userId
+ name: account.displayInfo.userId,
+ displayName: account.isStale
+ ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName)
+ : account.displayInfo.displayName
};
});
} catch (e) {
@@ -871,7 +878,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getSubscriptionsDropdownValues(): Promise {
let subscriptionsValues: azdata.CategoryValue[] = [];
try {
- if (this._azureAccount) {
+ if (this._azureAccount?.isStale === false) {
this._subscriptions = await getSubscriptions(this._azureAccount);
} else {
this._subscriptions = [];
@@ -1471,7 +1478,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
let sqlMigrationServiceValues: azdata.CategoryValue[] = [];
try {
if (this._azureAccount && subscription && resourceGroupName && this._targetServerInstance) {
- this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sms.properties.resourceGroup.toLowerCase() === resourceGroupName.toLowerCase());
+ const services = await getSqlMigrationServicesByResourceGroup(
+ this._azureAccount,
+ subscription,
+ resourceGroupName?.toLowerCase());
+ const targetLoc = this._targetServerInstance.location.toLowerCase();
+ this._sqlMigrationServices = services.filter(sms => sms.location.toLowerCase() === targetLoc);
} else {
this._sqlMigrationServices = [];
}
@@ -1545,6 +1557,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
requestBody.properties.backupConfiguration = {
targetLocation: undefined!,
sourceLocation: {
+ fileStorageType: 'AzureBlob',
azureBlob: {
storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id,
accountKey: this._databaseBackup.blobs[i].storageKey,
@@ -1567,6 +1580,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
accountKey: this._databaseBackup.networkShares[i].storageKey,
},
sourceLocation: {
+ fileStorageType: 'FileShare',
fileShare: {
path: this._databaseBackup.networkShares[i].networkShareLocation,
username: this._databaseBackup.networkShares[i].windowsUser,
@@ -1584,8 +1598,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._targetServerInstance,
this._targetDatabaseNames[i],
requestBody,
- this._sessionId
- );
+ this._sessionId);
+
response.databaseMigration.properties.sourceDatabaseName = this._databasesForMigration[i];
response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!;
response.databaseMigration.properties.offlineConfiguration = requestBody.properties.offlineConfiguration!;
@@ -1621,22 +1635,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}
);
- await MigrationLocalStorage.saveMigration(
- currentConnection!,
- response.databaseMigration,
- this._targetServerInstance,
- this._azureAccount,
- this._targetSubscription,
- this._sqlMigrationService!,
- response.asyncUrl,
- this._sessionId
- );
- void vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._databasesForMigration[i], this._targetServerInstance.name, this._targetDatabaseNames[i]));
+ void vscode.window.showInformationMessage(
+ localize(
+ "sql.migration.starting.migration.message",
+ 'Starting migration for database {0} to {1} - {2}',
+ this._databasesForMigration[i],
+ this._targetServerInstance.name,
+ this._targetDatabaseNames[i]));
}
} catch (e) {
void vscode.window.showErrorMessage(
localize('sql.migration.starting.migration.error', "An error occurred while starting the migration: '{0}'", e.message));
- console.log(e);
+ logError(TelemetryViews.MigrationLocalStorage, 'StartMigrationFailed', e);
}
finally {
// kill existing data collection if user start migration
@@ -1718,7 +1728,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}
}
- public loadSavedInfo(): Boolean {
+ public async loadSavedInfo(): Promise {
try {
this._targetType = this.savedInfo.migrationTargetType || undefined!;
diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts
index e702dbefaf..7bd85d17a7 100644
--- a/extensions/sql-migration/src/telemtery.ts
+++ b/extensions/sql-migration/src/telemtery.ts
@@ -28,7 +28,8 @@ export enum TelemetryViews {
SqlMigrationWizard = 'SqlMigrationWizard',
MigrationLocalStorage = 'MigrationLocalStorage',
SkuRecommendationWizard = 'SkuRecommendationWizard',
- DataCollectionWizard = 'GetAzureRecommendationDialog'
+ DataCollectionWizard = 'GetAzureRecommendationDialog',
+ SelectMigrationServiceDialog = 'SelectMigrationServiceDialog',
}
export enum TelemetryAction {
diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts
index 52598b84ab..9a4318ea3d 100644
--- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts
+++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts
@@ -786,15 +786,27 @@ export class DatabaseBackupPage extends MigrationWizardPage {
await this.switchNetworkContainerFields(this.migrationStateModel._databaseBackup.networkContainerType);
const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
- const queryProvider = azdata.dataprotocol.getProvider((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider);
+ const queryProvider = azdata.dataprotocol.getProvider(
+ (await this.migrationStateModel.getSourceConnectionProfile()).providerId,
+ azdata.DataProviderType.QueryProvider);
+
const query = 'select SUSER_NAME()';
- const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
+ const results = await queryProvider.runQueryAndReturn(
+ await (azdata.connection.getUriForConnection(
+ this.migrationStateModel.sourceConnectionId)), query);
+
const username = results.rows[0][0].displayValue;
- this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!;
+ this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin'
+ ? MigrationSourceAuthenticationType.Sql
+ : connectionProfile.authenticationType === 'Integrated'
+ ? MigrationSourceAuthenticationType.Integrated
+ : undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
- this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser;
+ this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares
+ ? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
+ : '';
this._networkShareTargetDatabaseNames = [];
this._networkShareLocations = [];
@@ -809,7 +821,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
let originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames;
- let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares;
+ let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || [];
let originalBlobs = this.migrationStateModel._databaseBackup.blobs;
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
this.migrationStateModel._targetDatabaseNames = [];
@@ -830,7 +842,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
blob = originalBlobs[dbIndex] ?? blob;
} else {
// network share values are uniform for all dbs in the same migration, except for networkShareLocation
- const previouslySelectedNetworkShare = originalNetworkShares[0];
+ const previouslySelectedNetworkShare = originalNetworkShares.length > 0
+ ? originalNetworkShares[0]
+ : '';
+
if (previouslySelectedNetworkShare) {
networkShare = {
...previouslySelectedNetworkShare,
diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts
index 8570646e7f..20eb9f1602 100644
--- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts
+++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts
@@ -8,43 +8,16 @@ import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
-import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { debounce } from '../api/utils';
import * as styles from '../constants/styles';
-import { selectDatabasesFromList } from '../constants/helper';
-
-const styleLeft: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'left',
- 'white-space': 'nowrap',
- 'text-overflow': 'ellipsis',
- 'overflow': 'hidden',
- 'box-shadow': '0px -1px 0px 0px rgba(243, 242, 241, 1) inset'
-};
-
-const styleCheckBox: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'left',
- 'white-space': 'nowrap',
- 'text-overflow': 'ellipsis',
- 'overflow': 'hidden',
-};
-
-const styleRight: azdata.CssStyles = {
- 'border': 'none',
- 'text-align': 'right',
- 'white-space': 'nowrap',
- 'text-overflow': 'ellipsis',
- 'overflow': 'hidden',
- 'box-shadow': '0px -1px 0px 0px rgba(243, 242, 241, 1) inset'
-};
+import { IconPathHelper } from '../constants/iconPathHelper';
export class DatabaseSelectorPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
- private _databaseSelectorTable!: azdata.DeclarativeTableComponent;
+ private _databaseSelectorTable!: azdata.TableComponent;
private _dbNames!: string[];
private _dbCount!: azdata.TextComponent;
- private _databaseTableValues!: azdata.DeclarativeTableCellValue[][];
+ private _databaseTableValues!: any[];
private _disposables: vscode.Disposable[] = [];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
@@ -115,7 +88,6 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
protected async handleStateChange(e: StateChangeEvent): Promise {
}
-
private createSearchComponent(): azdata.DivContainer {
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
stopEnterPropagation: true,
@@ -137,74 +109,36 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
}
@debounce(500)
- private _filterTableList(value: string): void {
+ private async _filterTableList(value: string, selectedList?: string[]): Promise {
+ const selectedRows: number[] = [];
+ const selectedDatabases = selectedList || this.selectedDbs();
+ let tableRows = this._databaseTableValues;
if (this._databaseTableValues && value?.length > 0) {
- const filter: number[] = [];
- this._databaseTableValues.forEach((row, index) => {
- // undo when bug #16445 is fixed
- // const flexContainer: azdata.FlexContainer = row[1]?.value as azdata.FlexContainer;
- // const textComponent: azdata.TextComponent = flexContainer?.items[1] as azdata.TextComponent;
- // const cellText = textComponent?.value?.toLowerCase();
- const text = row[1]?.value as string;
- const cellText = text?.toLowerCase();
- const searchText: string = value?.toLowerCase();
- if (cellText?.includes(searchText)) {
- filter.push(index);
- }
- });
-
- this._databaseSelectorTable.setFilter(filter);
- } else {
- this._databaseSelectorTable.setFilter(undefined);
+ tableRows = this._databaseTableValues
+ .filter(row => {
+ const searchText = value?.toLowerCase();
+ return row[2]?.toLowerCase()?.indexOf(searchText) > -1 // database name
+ || row[3]?.toLowerCase()?.indexOf(searchText) > -1 // state
+ || row[4]?.toLowerCase()?.indexOf(searchText) > -1 // size
+ || row[5]?.toLowerCase()?.indexOf(searchText) > -1; // last backup date
+ });
}
+
+ for (let row = 0; row < tableRows.length; row++) {
+ const database: string = tableRows[row][2];
+ if (selectedDatabases.includes(database)) {
+ selectedRows.push(row);
+ }
+ }
+
+ await this._databaseSelectorTable.updateProperty('data', tableRows);
+ this._databaseSelectorTable.selectedRows = selectedRows;
+ await this.updateValuesOnSelection();
}
public async createRootContainer(view: azdata.ModelView): Promise {
- const providerId = (await this.migrationStateModel.getSourceConnectionProfile()).providerId;
- const metaDataService = azdata.dataprotocol.getProvider(providerId, azdata.DataProviderType.MetadataProvider);
- const ownerUri = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
- const results = await metaDataService.getDatabases(ownerUri);
- const excludeDbs: string[] = [
- 'master',
- 'tempdb',
- 'msdb',
- 'model'
- ];
- this._dbNames = [];
- let finalResult = results.filter((db) => !excludeDbs.includes(db.options.name));
- finalResult.sort((a, b) => a.options.name.localeCompare(b.options.name));
- this._databaseTableValues = [];
- for (let index in finalResult) {
- let selectable = true;
- if (constants.OFFLINE_CAPS.includes(finalResult[index].options.state)) {
- selectable = false;
- }
- this._databaseTableValues.push([
- {
- value: false,
- style: styleCheckBox,
- enabled: selectable
- },
- {
- value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, finalResult[index].options.name),
- style: styleLeft
- },
- {
- value: `${finalResult[index].options.state}`,
- style: styleLeft
- },
- {
- value: `${finalResult[index].options.sizeInMB}`,
- style: styleRight
- },
- {
- value: `${finalResult[index].options.lastBackup}`,
- style: styleLeft
- }
- ]);
- this._dbNames.push(finalResult[index].options.name);
- }
+ await this._loadDatabaseList(this.migrationStateModel, this.migrationStateModel._assessedDatabaseList);
const text = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION,
@@ -214,70 +148,83 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
}).component();
this._dbCount = this._view.modelBuilder.text().withProps({
- value: constants.DATABASES_SELECTED(this.selectedDbs.length, this._databaseTableValues.length),
+ value: constants.DATABASES_SELECTED(
+ this.selectedDbs().length,
+ this._databaseTableValues.length),
CSSStyles: {
...styles.BODY_CSS,
'margin-top': '8px'
}
}).component();
- this._databaseSelectorTable = this._view.modelBuilder.declarativeTable().withProps(
- {
- enableRowSelection: true,
- width: '100%',
- CSSStyles: {
- 'border': 'none'
- },
+ const cssClass = 'no-borders';
+ this._databaseSelectorTable = this._view.modelBuilder.table()
+ .withProps({
+ data: [],
+ width: 650,
+ height: '100%',
+ forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
- {
- displayName: '',
- valueType: azdata.DeclarativeDataType.boolean,
- width: 20,
- isReadOnly: false,
- showCheckAll: true,
- headerCssStyles: styleCheckBox
+ {
+ value: '',
+ width: 10,
+ type: azdata.ColumnType.checkBox,
+ action: azdata.ActionOnCellCheckboxCheck.selectRow,
+ resizable: false,
+ cssClass: cssClass,
+ headerCssClass: cssClass,
},
{
- displayName: constants.DATABASE,
- // undo when bug #16445 is fixed
- // valueType: azdata.DeclarativeDataType.component,
- valueType: azdata.DeclarativeDataType.string,
- width: '100%',
- isReadOnly: true,
- headerCssStyles: styleLeft
+ value: 'databaseicon',
+ name: '',
+ width: 10,
+ type: azdata.ColumnType.icon,
+ headerCssClass: cssClass,
+ cssClass: cssClass,
+ resizable: false,
},
{
- displayName: constants.STATUS,
- valueType: azdata.DeclarativeDataType.string,
- width: 100,
- isReadOnly: true,
- headerCssStyles: styleLeft
+ name: constants.DATABASE,
+ value: 'database',
+ type: azdata.ColumnType.text,
+ width: 360,
+ cssClass: cssClass,
+ headerCssClass: cssClass,
},
{
- displayName: constants.SIZE,
- valueType: azdata.DeclarativeDataType.string,
- width: 125,
- isReadOnly: true,
- headerCssStyles: styleRight
+ name: constants.STATUS,
+ value: 'status',
+ type: azdata.ColumnType.text,
+ width: 80,
+ cssClass: cssClass,
+ headerCssClass: cssClass,
},
{
- displayName: constants.LAST_BACKUP,
- valueType: azdata.DeclarativeDataType.string,
- width: 150,
- isReadOnly: true,
- headerCssStyles: styleLeft
- }
+ name: constants.SIZE,
+ value: 'size',
+ type: azdata.ColumnType.text,
+ width: 80,
+ cssClass: cssClass,
+ headerCssClass: cssClass,
+ },
+ {
+ name: constants.LAST_BACKUP,
+ value: 'lastBackup',
+ type: azdata.ColumnType.text,
+ width: 130,
+ cssClass: cssClass,
+ headerCssClass: cssClass,
+ },
]
- }
- ).component();
+ }).component();
- this._databaseTableValues = selectDatabasesFromList(this.migrationStateModel._databasesForAssessment, this._databaseTableValues);
- await this._databaseSelectorTable.setDataValues(this._databaseTableValues);
- await this.updateValuesOnSelection();
-
- this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => {
+ this._disposables.push(this._databaseSelectorTable.onRowSelected(async (e) => {
await this.updateValuesOnSelection();
}));
+
+ // load unfiltered table list and pre-select list of databases saved in state
+ await this._filterTableList('', this.migrationStateModel._databasesForAssessment);
+
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
height: '100%',
@@ -293,65 +240,60 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
return flex;
}
+ private async _loadDatabaseList(stateMachine: MigrationStateModel, selectedDatabases: string[]): Promise {
+ const providerId = (await stateMachine.getSourceConnectionProfile()).providerId;
+ const metaDataService = azdata.dataprotocol.getProvider(
+ providerId,
+ azdata.DataProviderType.MetadataProvider);
+ const ownerUri = await azdata.connection.getUriForConnection(
+ stateMachine.sourceConnectionId);
+ const excludeDbs: string[] = [
+ 'master',
+ 'tempdb',
+ 'msdb',
+ 'model'
+ ];
+ const databaseList = (await metaDataService
+ .getDatabases(ownerUri))
+ .filter(database => !excludeDbs.includes(database.options.name))
+ || [];
+
+ databaseList.sort((a, b) => a.options.name.localeCompare(b.options.name));
+ this._dbNames = [];
+
+ this._databaseTableValues = databaseList.map(database => {
+ const databaseName = database.options.name;
+ this._dbNames.push(databaseName);
+ return [
+ selectedDatabases?.indexOf(databaseName) > -1,
+ {
+ icon: IconPathHelper.sqlDatabaseLogo,
+ title: databaseName,
+ },
+ databaseName,
+ database.options.state,
+ database.options.sizeInMB,
+ database.options.lastBackup,
+ ];
+ }) || [];
+ }
+
public selectedDbs(): string[] {
- let result: string[] = [];
- this._databaseSelectorTable?.dataValues?.forEach((arr, index) => {
- if (arr[0].value === true) {
- result.push(this._dbNames[index]);
- }
- });
- return result;
+ const rows = this._databaseSelectorTable?.data || [];
+ const databases = this._databaseSelectorTable?.selectedRows || [];
+ return databases
+ .filter(row => row < rows.length)
+ .map(row => rows[row][2])
+ || [];
}
private async updateValuesOnSelection() {
+ const selectedDatabases = this.selectedDbs() || [];
await this._dbCount.updateProperties({
- 'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length)
+ 'value': constants.DATABASES_SELECTED(
+ selectedDatabases.length,
+ this._databaseSelectorTable.data?.length || 0)
});
- this.migrationStateModel._databasesForAssessment = this.selectedDbs();
+ this.migrationStateModel._databasesForAssessment = selectedDatabases;
}
-
- // undo when bug #16445 is fixed
- private createIconTextCell(icon: IconPath, text: string): string {
- return text;
- }
- // private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer {
- // const cellContainer = this._view.modelBuilder.flexContainer().withProps({
- // CSSStyles: {
- // 'justify-content': 'left'
- // }
- // }).component();
-
- // const iconComponent = this._view.modelBuilder.image().withProps({
- // iconPath: icon,
- // iconWidth: '16px',
- // iconHeight: '16px',
- // width: '20px',
- // height: '20px'
- // }).component();
- // cellContainer.addItem(iconComponent, {
- // flex: '0',
- // CSSStyles: {
- // 'width': '32px'
- // }
- // });
-
- // const textComponent = this._view.modelBuilder.text().withProps({
- // value: text,
- // title: text,
- // CSSStyles: {
- // 'margin': '0px',
- // 'width': '110px'
- // }
- // }).component();
-
- // cellContainer.addItem(textComponent, {
- // CSSStyles: {
- // 'width': 'auto'
- // }
- // });
-
- // return cellContainer;
- // }
- // undo when bug #16445 is fixed
-
}
diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts
index c91c5effe7..822f2c5872 100644
--- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts
+++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts
@@ -44,27 +44,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise {
this._view = view;
- this._statusLoadingComponent = view.modelBuilder.loadingComponent().withItem(this.createDMSDetailsContainer()).component();
+ this._statusLoadingComponent = view.modelBuilder.loadingComponent()
+ .withItem(this.createDMSDetailsContainer())
+ .component();
- this._dmsInfoContainer = this._view.modelBuilder.flexContainer().withItems([
- this._statusLoadingComponent
- ]).component();
+ this._dmsInfoContainer = this._view.modelBuilder.flexContainer()
+ .withItems([this._statusLoadingComponent])
+ .component();
const form = view.modelBuilder.formContainer()
- .withFormItems(
- [
- {
- component: this.migrationServiceDropdownContainer()
- },
- {
- component: this._dmsInfoContainer
- }
- ]
- ).withProps({
- CSSStyles: {
- 'padding-top': '0'
- }
- }).component();
+ .withFormItems([
+ { component: this.migrationServiceDropdownContainer() },
+ { component: this._dmsInfoContainer }
+ ])
+ .withProps({ CSSStyles: { 'padding-top': '0' } })
+ .component();
this._disposables.push(this._view.onClosed(e => {
this._disposables.forEach(
@@ -419,29 +413,26 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this.migrationStateModel._targetSubscription,
this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
this.migrationStateModel._sqlMigrationService.location,
- this.migrationStateModel._sqlMigrationService.name,
- this.migrationStateModel._sessionId);
+ this.migrationStateModel._sqlMigrationService.name);
this.migrationStateModel._sqlMigrationService = migrationService;
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription,
this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
this.migrationStateModel._sqlMigrationService.location,
- this.migrationStateModel._sqlMigrationService!.name,
- this.migrationStateModel._sessionId);
- this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(node => node.nodeName);
+ this.migrationStateModel._sqlMigrationService!.name);
+ this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(
+ node => node.nodeName);
+
const migrationServiceAuthKeys = await getSqlMigrationServiceAuthKeys(
this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription,
this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
this.migrationStateModel._sqlMigrationService.location,
- this.migrationStateModel._sqlMigrationService!.name,
- this.migrationStateModel._sessionId
- );
+ this.migrationStateModel._sqlMigrationService!.name);
- this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => {
- return node.nodeName;
- });
+ this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(
+ node => node.nodeName);
const state = migrationService.properties.integrationRuntimeState;
if (state === 'Online') {
@@ -458,25 +449,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
const data = [
[
+ { value: constants.SERVICE_KEY1_LABEL },
+ { value: migrationServiceAuthKeys.authKey1 },
{
- value: constants.SERVICE_KEY1_LABEL
- },
- {
- value: migrationServiceAuthKeys.authKey1
- },
- {
- value: this._view.modelBuilder.flexContainer().withItems([this._copy1, this._refresh1]).component()
+ value: this._view.modelBuilder.flexContainer()
+ .withItems([this._copy1, this._refresh1])
+ .component()
}
],
[
+ { value: constants.SERVICE_KEY2_LABEL },
+ { value: migrationServiceAuthKeys.authKey2 },
{
- value: constants.SERVICE_KEY2_LABEL
- },
- {
- value: migrationServiceAuthKeys.authKey2
- },
- {
- value: this._view.modelBuilder.flexContainer().withItems([this._copy2, this._refresh2]).component()
+ value: this._view.modelBuilder.flexContainer()
+ .withItems([this._copy2, this._refresh2])
+ .component()
}
]
];
diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts
index 8c61089976..f5a73a59a9 100644
--- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts
+++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts
@@ -186,7 +186,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex);
// Making a clone of the account object to preserve the original tenants
this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount);
- if (this.migrationStateModel._azureAccount.properties.tenants.length > 1) {
+ if (selectedAzureAccount.isStale === false &&
+ this.migrationStateModel._azureAccount.properties.tenants.length > 1) {
this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants;
this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues();
selectDropDownIndex(this._accountTenantDropdown, 0);
diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts
index 8b733bc6ba..a251a18f5a 100644
--- a/extensions/sql-migration/src/wizard/wizardController.ts
+++ b/extensions/sql-migration/src/wizard/wizardController.ts
@@ -17,14 +17,17 @@ import { MigrationModePage } from './migrationModePage';
import { DatabaseSelectorPage } from './databaseSelectorPage';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
import * as styles from '../constants/styles';
+import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
+import { azureResource } from 'azureResource';
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
export class WizardController {
private _wizardObject!: azdata.window.Wizard;
- private _model!: MigrationStateModel;
private _disposables: vscode.Disposable[] = [];
- constructor(private readonly extensionContext: vscode.ExtensionContext, model: MigrationStateModel) {
- this._model = model;
+ constructor(
+ private readonly extensionContext: vscode.ExtensionContext,
+ private readonly _model: MigrationStateModel,
+ private readonly _onClosedCallback: () => Promise) {
}
public async openWizard(connectionId: string): Promise {
@@ -105,13 +108,12 @@ export class WizardController {
});
await Promise.all(wizardSetupPromises);
- this._model.extensionContext.subscriptions.push(this._wizardObject.onPageChanged(async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
- await pages[0].onPageEnter(pageChangeInfo);
- }));
+ this._model.extensionContext.subscriptions.push(
+ this._wizardObject.onPageChanged(
+ async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
+ await pages[0].onPageEnter(pageChangeInfo);
+ }));
- this._model.extensionContext.subscriptions.push(this._wizardObject.doneButton.onClick(async (e) => {
- await stateModel.startMigration();
- }));
this._disposables.push(saveAndCloseButton.onClick(async () => {
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
await this._wizardObject.close();
@@ -134,16 +136,65 @@ export class WizardController {
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
- this._disposables.push(this._wizardObject.doneButton.onClick(e => {
- sendSqlMigrationActionEvent(
- TelemetryViews.SqlMigrationWizard,
- TelemetryAction.PageButtonClick,
- {
- ...this.getTelemetryProps(),
- 'buttonPressed': TelemetryAction.Done,
- 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
- }, {});
- }));
+ this._disposables.push(
+ this._wizardObject.doneButton.onClick(async (e) => {
+ await stateModel.startMigration();
+ await this.updateServiceContext(stateModel);
+ await this._onClosedCallback();
+
+ sendSqlMigrationActionEvent(
+ TelemetryViews.SqlMigrationWizard,
+ TelemetryAction.PageButtonClick,
+ {
+ ...this.getTelemetryProps(),
+ 'buttonPressed': TelemetryAction.Done,
+ 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
+ }, {});
+ }));
+ }
+
+ private async updateServiceContext(stateModel: MigrationStateModel): Promise {
+ const resourceGroup = this._getResourceGroupByName(
+ stateModel._resourceGroups,
+ stateModel._sqlMigrationService?.properties.resourceGroup);
+
+ const subscription = this._getSubscriptionFromResourceId(
+ stateModel._subscriptions,
+ resourceGroup?.id);
+
+ const location = this._getLocationByValue(
+ stateModel._locations,
+ stateModel._sqlMigrationService?.location);
+
+ return await MigrationLocalStorage.saveMigrationServiceContext(
+ {
+ azureAccount: stateModel._azureAccount,
+ tenant: stateModel._azureTenant,
+ subscription: subscription,
+ location: location,
+ resourceGroup: resourceGroup,
+ migrationService: stateModel._sqlMigrationService,
+ });
+ }
+
+ private _getResourceGroupByName(resourceGroups: azureResource.AzureResourceResourceGroup[], displayName?: string): azureResource.AzureResourceResourceGroup | undefined {
+ return resourceGroups.find(rg => rg.name === displayName);
+ }
+
+ private _getLocationByValue(locations: azureResource.AzureLocation[], name?: string): azureResource.AzureLocation | undefined {
+ return locations.find(loc => loc.name === name);
+ }
+
+ private _getSubscriptionFromResourceId(subscriptions: azureResource.AzureResourceSubscription[], resourceId?: string): azureResource.AzureResourceSubscription | undefined {
+ let parts = resourceId?.split('/subscriptions/');
+ if (parts?.length && parts?.length > 1) {
+ parts = parts[1]?.split('/resourcegroups/');
+ if (parts?.length && parts?.length > 0) {
+ const subscriptionId: string = parts[0];
+ return subscriptions.find(sub => sub.id === subscriptionId, 1);
+ }
+ }
+ return undefined;
}
private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) {