mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 18:48:33 -05:00
Dev/brih/feature/switch ads to portal context (#18963)
* Add CodeQL Analysis workflow (#10195) * Add CodeQL Analysis workflow * Fix path * dashboard refactor * update version, readme, minor ui changes * fix merge issue * Revert "Add CodeQL Analysis workflow (#10195)" This reverts commit fe98d586cd75be4758ac544649bb4983accf4acd. * fix context switching issue * fix resource id parsing error and mi api version * mv refresh btn, rm autorefresh, align cards * remove missed autorefresh code * improve error handling and messages * fix typos * remove duplicate/unnecessary _populate* calls * change clear configuration button text * remove confusing watermark text * add stale account handling Co-authored-by: Justin Hutchings <jhutchings1@users.noreply.github.com>
This commit is contained in:
@@ -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<SqlMigrationService> {
|
||||
export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise<SqlMigrationService> {
|
||||
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<SqlMigrationService> {
|
||||
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<SqlMigrationService[]> {
|
||||
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<SqlMigrationService[]> {
|
||||
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<SqlMigrationService> {
|
||||
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<SqlMigrationServiceAuthenticationKeys> {
|
||||
export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise<SqlMigrationServiceAuthenticationKeys> {
|
||||
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<SqlMigrationServiceAuthenticationKeys> {
|
||||
export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string): Promise<SqlMigrationServiceAuthenticationKeys> {
|
||||
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<IntegrationRuntimeMonitoringData> {
|
||||
export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string): Promise<IntegrationRuntimeMonitoringData> {
|
||||
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<StartDatabaseMigrationResponse> {
|
||||
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<DatabaseMigration> {
|
||||
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<DatabaseMigration> {
|
||||
|
||||
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<AzureAsyncOperationResource> {
|
||||
export async function getServiceMigrations(account: azdata.Account, subscription: Subscription, resourceId: string): Promise<DatabaseMigration[]> {
|
||||
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<SqlManagedInstance | SqlVMServer> {
|
||||
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 <SqlManagedInstance>{};
|
||||
}
|
||||
|
||||
export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string): Promise<AzureAsyncOperationResource> {
|
||||
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<DatabaseMigration[]> {
|
||||
export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<any> {
|
||||
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<any> {
|
||||
export async function stopMigration(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<void> {
|
||||
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<void> {
|
||||
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 {
|
||||
|
||||
@@ -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<T>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<string, MigrationContext[]> = 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<void> => {
|
||||
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<MigrationContext[]> {
|
||||
const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
|
||||
return this._migrationStatusMap.get(connectionId)!;
|
||||
}
|
||||
|
||||
private async setCurrentMigrations(migrations: MigrationContext[]): Promise<void> {
|
||||
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(<string>IconPathHelper.migrationDashboardHeaderBackground.light).with({ scheme: 'vscode-file' });
|
||||
const watermarkUri = vscode.Uri
|
||||
.file(<string>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<void> {
|
||||
public async refreshMigrations(): Promise<void> {
|
||||
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<MigrationContext[]> {
|
||||
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<void> {
|
||||
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<azdata.Component> {
|
||||
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(<string>linkMetaData.iconPath?.light)})`,
|
||||
|
||||
@@ -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<void>) {
|
||||
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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
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')
|
||||
&& (<SqlManagedInstance>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 ((<SqlManagedInstance>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]];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<void>) {
|
||||
|
||||
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<void> {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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<DatabaseMigration | undefined> {
|
||||
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<string> {
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>) {
|
||||
|
||||
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<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',
|
||||
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 = (<azdata.CategoryValue[]>this._statusDropdown.values).find((value) => {
|
||||
return value.name === this._filter;
|
||||
});
|
||||
this._statusDropdown.value =
|
||||
(<azdata.CategoryValue[]>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<void> => {
|
||||
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<void> {
|
||||
try {
|
||||
const migrations = filterMigrations(
|
||||
this._filteredMigrations = filterMigrations(
|
||||
this._model._migrations,
|
||||
(<azdata.CategoryValue>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
|
||||
},
|
||||
}
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: IconPathHelper.sqlDatabaseLogo,
|
||||
title: migration.properties.sourceDatabaseName ?? '-',
|
||||
}, // database
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
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<void> {
|
||||
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
|
||||
<azdata.HyperlinkColumn>{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.DATABASE,
|
||||
value: 'database',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
icon: IconPathHelper.sqlDatabaseLogo,
|
||||
showText: true,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
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 = <azdata.ICellActionEventArgs>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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<void>) {
|
||||
}
|
||||
|
||||
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<MigrationStateModel> {
|
||||
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);
|
||||
|
||||
@@ -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<void>) {
|
||||
this._dialog = azdata.window.createModelViewDialog(
|
||||
constants.MIGRATION_SERVICE_SELECT_TITLE,
|
||||
'SelectMigraitonServiceDialog',
|
||||
460,
|
||||
'normal');
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
this._dialog.okButton.enabled = this._serviceContext.migrationService !== undefined;
|
||||
}
|
||||
|
||||
private async _populateAzureAccountsDropdown(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<azdata.CategoryValue[]> {
|
||||
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<azdata.CategoryValue[]> {
|
||||
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<azdata.CategoryValue[]> {
|
||||
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<azdata.CategoryValue[]> {
|
||||
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 <azureResource.AzureResourceResourceGroup>{
|
||||
id: getFullResourceGroupFromId(rg),
|
||||
name: rg,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async _getMigrationServiceDropdownValues(
|
||||
account?: azdata.Account,
|
||||
subscription?: azureResource.AzureResourceSubscription,
|
||||
location?: azureResource.AzureLocation,
|
||||
resourceGroup?: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
|
||||
|
||||
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}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
private async createServiceContent(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
|
||||
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<void> {
|
||||
private async _regenerateAuthKey(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration, keyName: string): Promise<void> {
|
||||
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<void> {
|
||||
private async _refreshAuthTable(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
|
||||
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: [
|
||||
|
||||
@@ -32,47 +32,54 @@ class SQLMigration {
|
||||
|
||||
async registerCommands(): Promise<void> {
|
||||
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<MigrationNotebookInfo>();
|
||||
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<MigrationNotebookInfo>();
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MigrationContext[]> {
|
||||
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<MigrationServiceContext> {
|
||||
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<void> {
|
||||
if (migration.azureAccount.isStale) {
|
||||
public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string> {
|
||||
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<DatabaseMigration[]> {
|
||||
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 {
|
||||
|
||||
@@ -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<azdata.CategoryValue[]> {
|
||||
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<Boolean> {
|
||||
try {
|
||||
this._targetType = this.savedInfo.migrationTargetType || undefined!;
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ export enum TelemetryViews {
|
||||
SqlMigrationWizard = 'SqlMigrationWizard',
|
||||
MigrationLocalStorage = 'MigrationLocalStorage',
|
||||
SkuRecommendationWizard = 'SkuRecommendationWizard',
|
||||
DataCollectionWizard = 'GetAzureRecommendationDialog'
|
||||
DataCollectionWizard = 'GetAzureRecommendationDialog',
|
||||
SelectMigrationServiceDialog = 'SelectMigrationServiceDialog',
|
||||
}
|
||||
|
||||
export enum TelemetryAction {
|
||||
|
||||
@@ -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<azdata.QueryProvider>((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider);
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
(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,
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
|
||||
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<void> {
|
||||
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<azdata.FlexContainer> {
|
||||
const providerId = (await this.migrationStateModel.getSourceConnectionProfile()).providerId;
|
||||
const metaDataService = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(providerId, azdata.DataProviderType.MetadataProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
|
||||
const results = <azdata.DatabaseInfo[]>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
|
||||
<azdata.CheckboxColumn>{
|
||||
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<void> {
|
||||
const providerId = (await stateMachine.getSourceConnectionProfile()).providerId;
|
||||
const metaDataService = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(
|
||||
providerId,
|
||||
azdata.DataProviderType.MetadataProvider);
|
||||
const ownerUri = await azdata.connection.getUriForConnection(
|
||||
stateMachine.sourceConnectionId);
|
||||
const excludeDbs: string[] = [
|
||||
'master',
|
||||
'tempdb',
|
||||
'msdb',
|
||||
'model'
|
||||
];
|
||||
const databaseList = (<azdata.DatabaseInfo[]>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,
|
||||
<azdata.IconColumnCellValue>{
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
@@ -44,27 +44,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
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()
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void>) {
|
||||
}
|
||||
|
||||
public async openWizard(connectionId: string): Promise<void> {
|
||||
@@ -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<void> {
|
||||
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(
|
||||
<MigrationServiceContext>{
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user