mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 17:23:35 -05:00
Improvements in blob storage support for SQL Migration. (#15693)
* changing the cutover icon on migration cutover page. * Fixing monitoring table and pending log backups * converting file upload times in utc to local time zones * adding autorefresh to dashboard, migration status and cutover dialogs. * Supporting blob container e2e * vbump extension * Fixing some PR comments * Fixed broken blob container dropdown onChange event * Localizing display string in refresh dialog Fixing some localized strings * Fixing var declaration * making a class readonly for 250px width * removing refresh interval dialog and replacing it with hardcoded values. * Fixing summary page IR information. * surfacing test connection error * Clearing intervals on view closed to remove auto refresh.
This commit is contained in:
@@ -58,7 +58,7 @@ export class AssessmentResultsDialog {
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.dialog = azdata.window.createModelViewDialog(this.title, this.title, '90%');
|
||||
this.dialog = azdata.window.createModelViewDialog(this.title, this.title, 'wide');
|
||||
|
||||
this.dialog.okButton.label = AssessmentResultsDialog.OkButtonText;
|
||||
this.dialog.okButton.onClick(async () => await this.execute());
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { createSqlMigrationService, getSqlMigrationService, getResourceGroups, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../../api/azure';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import { createSqlMigrationService, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../../api/azure';
|
||||
import { MigrationStateModel, NetworkContainerType } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as os from 'os';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { IntergrationRuntimePage } from '../../wizard/integrationRuntimePage';
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { selectDropDownIndex } from '../../api/utils';
|
||||
import * as EventEmitter from 'events';
|
||||
|
||||
export class CreateSqlMigrationServiceDialog {
|
||||
|
||||
private _model!: MigrationStateModel;
|
||||
|
||||
private migrationServiceSubscription!: azdata.TextComponent;
|
||||
private migrationServiceResourceGroupDropdown!: azdata.DropDownComponent;
|
||||
private migrationServiceLocation!: azdata.InputBoxComponent;
|
||||
@@ -23,6 +24,7 @@ export class CreateSqlMigrationServiceDialog {
|
||||
private _formSubmitButton!: azdata.ButtonComponent;
|
||||
|
||||
private _statusLoadingComponent!: azdata.LoadingComponent;
|
||||
private _refreshLoadingComponent!: azdata.LoadingComponent;
|
||||
private migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
|
||||
private _connectionStatus!: azdata.InfoBoxComponent;
|
||||
private _copyKey1Button!: azdata.ButtonComponent;
|
||||
@@ -30,18 +32,24 @@ export class CreateSqlMigrationServiceDialog {
|
||||
private _refreshKey1Button!: azdata.ButtonComponent;
|
||||
private _refreshKey2Button!: azdata.ButtonComponent;
|
||||
private _setupContainer!: azdata.FlexContainer;
|
||||
private _resourceGroupPreset!: string;
|
||||
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
|
||||
private createdMigrationService!: SqlMigrationService;
|
||||
private createdMigrationServiceNodeNames!: string[];
|
||||
private _createdMigrationService!: SqlMigrationService;
|
||||
private _selectedResourceGroup!: string;
|
||||
private _testConnectionButton!: azdata.window.Button;
|
||||
|
||||
constructor(private migrationStateModel: MigrationStateModel, private irPage: IntergrationRuntimePage) {
|
||||
private _doneButtonEvent: EventEmitter = new EventEmitter();
|
||||
private _isBlobContainerUsed: boolean = false;
|
||||
|
||||
private irNodes: string[] = [];
|
||||
|
||||
public async createNewDms(migrationStateModel: MigrationStateModel, resourceGroupPreset: string): Promise<CreateSqlMigrationServiceDialogResult> {
|
||||
this._model = migrationStateModel;
|
||||
this._resourceGroupPreset = resourceGroupPreset;
|
||||
this._dialogObject = azdata.window.createModelViewDialog(constants.CREATE_MIGRATION_SERVICE_TITLE, 'MigrationServiceDialog', 'medium');
|
||||
}
|
||||
|
||||
initialize() {
|
||||
let tab = azdata.window.createTab('');
|
||||
this._dialogObject.registerCloseValidator(async () => {
|
||||
return true;
|
||||
@@ -63,9 +71,9 @@ export class CreateSqlMigrationServiceDialog {
|
||||
this.setFormEnabledState(false);
|
||||
|
||||
|
||||
const subscription = this.migrationStateModel._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
|
||||
const location = this.migrationStateModel._targetServerInstance.location;
|
||||
const subscription = this._model._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).displayName;
|
||||
const location = this._model._targetServerInstance.location;
|
||||
const serviceName = this.migrationServiceNameText.value;
|
||||
|
||||
const formValidationErrors = this.validateCreateServiceForm(subscription, resourceGroup, location, serviceName);
|
||||
@@ -78,9 +86,10 @@ export class CreateSqlMigrationServiceDialog {
|
||||
}
|
||||
|
||||
try {
|
||||
this.createdMigrationService = await createSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, serviceName!);
|
||||
if (this.createdMigrationService.error) {
|
||||
this.setDialogMessage(`${this.createdMigrationService.error.code} : ${this.createdMigrationService.error.message}`);
|
||||
this._selectedResourceGroup = resourceGroup;
|
||||
this._createdMigrationService = await createSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, serviceName!);
|
||||
if (this._createdMigrationService.error) {
|
||||
this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`);
|
||||
this._statusLoadingComponent.loading = false;
|
||||
this.setFormEnabledState(true);
|
||||
return;
|
||||
@@ -88,10 +97,22 @@ export class CreateSqlMigrationServiceDialog {
|
||||
this._dialogObject.message = {
|
||||
text: ''
|
||||
};
|
||||
await this.refreshAuthTable();
|
||||
await this.refreshStatus();
|
||||
this._setupContainer.display = 'inline';
|
||||
this._statusLoadingComponent.loading = false;
|
||||
|
||||
if (this._isBlobContainerUsed) {
|
||||
this._dialogObject.okButton.enabled = true;
|
||||
this._statusLoadingComponent.loading = false;
|
||||
this._setupContainer.display = 'none';
|
||||
this._dialogObject.message = {
|
||||
text: constants.DATA_MIGRATION_SERVICE_CREATED_SUCCESSFULLY,
|
||||
level: azdata.window.MessageLevel.Information
|
||||
};
|
||||
} else {
|
||||
await this.refreshStatus();
|
||||
await this.refreshAuthTable();
|
||||
this._setupContainer.display = 'inline';
|
||||
this._testConnectionButton.hidden = false;
|
||||
this._statusLoadingComponent.loading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.setDialogMessage(e.message);
|
||||
@@ -135,13 +156,45 @@ export class CreateSqlMigrationServiceDialog {
|
||||
});
|
||||
});
|
||||
|
||||
this._testConnectionButton = azdata.window.createButton(constants.TEST_CONNECTION);
|
||||
this._testConnectionButton.hidden = true;
|
||||
this._testConnectionButton.onClick(async (e) => {
|
||||
this._refreshLoadingComponent.loading = true;
|
||||
this._connectionStatus.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
try {
|
||||
await this.refreshStatus();
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage(e);
|
||||
}
|
||||
this._connectionStatus.updateCssStyles({
|
||||
'display': 'inline'
|
||||
});
|
||||
this._refreshLoadingComponent.loading = false;
|
||||
});
|
||||
this._dialogObject.customButtons = [this._testConnectionButton];
|
||||
|
||||
this._dialogObject.content = [tab];
|
||||
this._dialogObject.okButton.enabled = false;
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
this._dialogObject.cancelButton.onClick((e) => {
|
||||
});
|
||||
this._dialogObject.okButton.onClick((e) => {
|
||||
this.irPage.populateMigrationService(this.createdMigrationService, this.createdMigrationServiceNodeNames, (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name);
|
||||
this._doneButtonEvent.emit('done', this._createdMigrationService, this._selectedResourceGroup);
|
||||
});
|
||||
|
||||
this._isBlobContainerUsed = this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this._doneButtonEvent.once('done', (createdDms: SqlMigrationService, selectedResourceGroup: string) => {
|
||||
azdata.window.closeDialog(this._dialogObject);
|
||||
resolve(
|
||||
{
|
||||
service: createdDms,
|
||||
resourceGroup: selectedResourceGroup
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -201,7 +254,7 @@ export class CreateSqlMigrationServiceDialog {
|
||||
this.migrationServiceLocation = this._view.modelBuilder.inputBox().withProps({
|
||||
required: true,
|
||||
enabled: false,
|
||||
value: await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)
|
||||
value: await this._model.getLocationDisplayName(this._model._targetServerInstance.location)
|
||||
}).component();
|
||||
|
||||
const targetlabel = this._view.modelBuilder.text().withProps({
|
||||
@@ -254,33 +307,19 @@ export class CreateSqlMigrationServiceDialog {
|
||||
|
||||
private async populateSubscriptions(): Promise<void> {
|
||||
this.migrationServiceResourceGroupDropdown.loading = true;
|
||||
this.migrationServiceSubscription.value = this.migrationStateModel._targetSubscription.name;
|
||||
this.migrationServiceSubscription.value = this._model._targetSubscription.name;
|
||||
await this.populateResourceGroups();
|
||||
}
|
||||
|
||||
private async populateResourceGroups(): Promise<void> {
|
||||
this.migrationServiceResourceGroupDropdown.loading = true;
|
||||
let subscription = this.migrationStateModel._targetSubscription;
|
||||
const resourceGroups = await getResourceGroups(this.migrationStateModel._azureAccount, subscription);
|
||||
let resourceGroupDropdownValues: azdata.CategoryValue[] = [];
|
||||
if (resourceGroups && resourceGroups.length > 0) {
|
||||
resourceGroups.forEach((resourceGroup) => {
|
||||
resourceGroupDropdownValues.push({
|
||||
name: resourceGroup.name,
|
||||
displayName: resourceGroup.name
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resourceGroupDropdownValues = [
|
||||
{
|
||||
displayName: constants.RESOURCE_GROUP_NOT_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
try {
|
||||
this.migrationServiceResourceGroupDropdown.values = await this._model.getAzureResourceGroupDropdownValues(this._model._targetSubscription);
|
||||
const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.displayName.toLowerCase() === this._resourceGroupPreset.toLowerCase());
|
||||
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0];
|
||||
} finally {
|
||||
this.migrationServiceResourceGroupDropdown.loading = false;
|
||||
}
|
||||
this.migrationServiceResourceGroupDropdown.values = resourceGroupDropdownValues;
|
||||
selectDropDownIndex(this.migrationServiceResourceGroupDropdown, 0);
|
||||
this.migrationServiceResourceGroupDropdown.loading = false;
|
||||
}
|
||||
|
||||
private createServiceStatus(): azdata.FlexContainer {
|
||||
@@ -327,9 +366,8 @@ export class CreateSqlMigrationServiceDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
const irSetupStep3Text = this._view.modelBuilder.hyperlink().withProps({
|
||||
label: constants.SERVICE_STEP3,
|
||||
url: '',
|
||||
const irSetupStep3Text = this._view.modelBuilder.text().withProps({
|
||||
value: constants.SERVICE_STEP3,
|
||||
CSSStyles: {
|
||||
'margin-top': '10px',
|
||||
'margin-bottom': '10px',
|
||||
@@ -337,23 +375,6 @@ export class CreateSqlMigrationServiceDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
irSetupStep3Text.onDidClick(async (e) => {
|
||||
refreshLoadingIndicator.loading = true;
|
||||
this._connectionStatus.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
try {
|
||||
await this.refreshStatus();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
this._connectionStatus.updateCssStyles({
|
||||
'display': 'inline'
|
||||
});
|
||||
refreshLoadingIndicator.loading = false;
|
||||
});
|
||||
|
||||
|
||||
this._connectionStatus = this._view.modelBuilder.infoBox().withProps({
|
||||
text: '',
|
||||
style: 'error',
|
||||
@@ -366,7 +387,7 @@ export class CreateSqlMigrationServiceDialog {
|
||||
'width': '350px'
|
||||
};
|
||||
|
||||
const refreshLoadingIndicator = this._view.modelBuilder.loadingComponent().withProps({
|
||||
this._refreshLoadingComponent = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
@@ -428,7 +449,7 @@ export class CreateSqlMigrationServiceDialog {
|
||||
this.migrationServiceAuthKeyTable,
|
||||
irSetupStep3Text,
|
||||
this._connectionStatus,
|
||||
refreshLoadingIndicator
|
||||
this._refreshLoadingComponent
|
||||
], {
|
||||
CSSStyles: {
|
||||
'margin-bottom': '5px'
|
||||
@@ -439,16 +460,28 @@ export class CreateSqlMigrationServiceDialog {
|
||||
}).component();
|
||||
|
||||
this._setupContainer.display = 'none';
|
||||
this._testConnectionButton.hidden = true;
|
||||
return this._setupContainer;
|
||||
}
|
||||
|
||||
private async refreshStatus(): Promise<void> {
|
||||
const subscription = this.migrationStateModel._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
|
||||
const location = this.migrationStateModel._targetServerInstance.location;
|
||||
const migrationServiceStatus = await getSqlMigrationService(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name);
|
||||
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name);
|
||||
this.createdMigrationServiceNodeNames = migrationServiceMonitoringStatus.nodes.map((node) => {
|
||||
const subscription = this._model._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).displayName;
|
||||
const location = this._model._targetServerInstance.location;
|
||||
|
||||
const maxRetries = 5;
|
||||
let migrationServiceStatus!: SqlMigrationService;
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
migrationServiceStatus = await getSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService.name);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
}
|
||||
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name);
|
||||
this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => {
|
||||
return node.nodeName;
|
||||
});
|
||||
if (migrationServiceStatus) {
|
||||
@@ -456,7 +489,7 @@ export class CreateSqlMigrationServiceDialog {
|
||||
|
||||
if (state === 'Online') {
|
||||
this._connectionStatus.updateProperties(<azdata.InfoBoxComponentProperties>{
|
||||
text: constants.SERVICE_READY(this.createdMigrationService!.name, this.createdMigrationServiceNodeNames.join(', ')),
|
||||
text: constants.SERVICE_READY(this._createdMigrationService!.name, this.irNodes.join(', ')),
|
||||
style: 'success',
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
@@ -464,9 +497,9 @@ export class CreateSqlMigrationServiceDialog {
|
||||
});
|
||||
this._dialogObject.okButton.enabled = true;
|
||||
} else {
|
||||
this._connectionStatus.text = constants.SERVICE_NOT_READY(this.createdMigrationService!.name);
|
||||
this._connectionStatus.text = constants.SERVICE_NOT_READY(this._createdMigrationService!.name);
|
||||
this._connectionStatus.updateProperties(<azdata.InfoBoxComponentProperties>{
|
||||
text: constants.SERVICE_NOT_READY(this.createdMigrationService!.name),
|
||||
text: constants.SERVICE_NOT_READY(this._createdMigrationService!.name),
|
||||
style: 'warning',
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
@@ -478,10 +511,10 @@ export class CreateSqlMigrationServiceDialog {
|
||||
|
||||
}
|
||||
private async refreshAuthTable(): Promise<void> {
|
||||
const subscription = this.migrationStateModel._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
|
||||
const location = this.migrationStateModel._targetServerInstance.location;
|
||||
const keys = await getSqlMigrationServiceAuthKeys(this.migrationStateModel._azureAccount, subscription, resourceGroup, location, this.createdMigrationService!.name);
|
||||
const subscription = this._model._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).displayName;
|
||||
const location = this._model._targetServerInstance.location;
|
||||
const keys = await getSqlMigrationServiceAuthKeys(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name);
|
||||
|
||||
this._copyKey1Button = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.copy
|
||||
@@ -557,3 +590,8 @@ export class CreateSqlMigrationServiceDialog {
|
||||
this.migrationServiceNameText.enabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreateSqlMigrationServiceDialogResult {
|
||||
service: SqlMigrationService,
|
||||
resourceGroup: string
|
||||
}
|
||||
|
||||
@@ -41,21 +41,6 @@ export class ConfirmCutoverDialog {
|
||||
|
||||
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
|
||||
|
||||
let infoDisplay = 'none';
|
||||
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
|
||||
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
|
||||
infoDisplay = 'inline';
|
||||
}
|
||||
|
||||
const businessCriticalinfoBox = this._view.modelBuilder.infoBox().withProps({
|
||||
text: constants.BUSINESS_CRITICAL_INFO,
|
||||
style: 'information',
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'display': infoDisplay
|
||||
}
|
||||
}).component();
|
||||
|
||||
const helpMainText = this._view.modelBuilder.text().withProps({
|
||||
value: constants.CUTOVER_HELP_MAIN,
|
||||
CSSStyles: {
|
||||
@@ -73,7 +58,7 @@ export class ConfirmCutoverDialog {
|
||||
}).component();
|
||||
|
||||
|
||||
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(f => f.listOfBackupFiles[0].status !== 'Restored' && f.listOfBackupFiles[0].status !== 'Ignored').length ?? 0;
|
||||
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount ?? 0;
|
||||
const pendingText = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
@@ -93,6 +78,29 @@ export class ConfirmCutoverDialog {
|
||||
this._dialogObject.okButton.enabled = e;
|
||||
});
|
||||
|
||||
const cutoverWarning = this._view.modelBuilder.infoBox().withProps({
|
||||
text: constants.COMPLETING_CUTOVER_WARNING,
|
||||
style: 'warning',
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
|
||||
let infoDisplay = 'none';
|
||||
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
|
||||
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
|
||||
infoDisplay = 'inline';
|
||||
}
|
||||
|
||||
const businessCriticalinfoBox = this._view.modelBuilder.infoBox().withProps({
|
||||
text: constants.BUSINESS_CRITICAL_INFO,
|
||||
style: 'information',
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'display': infoDisplay
|
||||
}
|
||||
}).component();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
@@ -100,11 +108,12 @@ export class ConfirmCutoverDialog {
|
||||
completeCutoverText,
|
||||
sourceDatabaseText,
|
||||
separator,
|
||||
businessCriticalinfoBox,
|
||||
helpMainText,
|
||||
helpStepsText,
|
||||
pendingText,
|
||||
confirmCheckbox
|
||||
confirmCheckbox,
|
||||
cutoverWarning,
|
||||
businessCriticalinfoBox
|
||||
]).component();
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationCutoverDialogModel, MigrationStatus } from './migrationCutoverDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
import { getSqlServerName } from '../../api/utils';
|
||||
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, SupportedAutoRefreshIntervals } from '../../api/utils';
|
||||
import { EOL } from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
|
||||
|
||||
const refreshFrequency: SupportedAutoRefreshIntervals = 30000;
|
||||
|
||||
export class MigrationCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -41,10 +43,13 @@ export class MigrationCutoverDialog {
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
|
||||
private fileTable!: azdata.TableComponent;
|
||||
private _autoRefreshHandle!: any;
|
||||
readonly _infoFieldWidth: string = '250px';
|
||||
|
||||
|
||||
constructor(migration: MigrationContext) {
|
||||
this._model = new MigrationCutoverDialogModel(migration);
|
||||
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 1000);
|
||||
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -65,17 +70,17 @@ export class MigrationCutoverDialog {
|
||||
|
||||
flexServer.addItem(sourceDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexServer.addItem(sourceDetails.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexServer.addItem(sourceVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -93,17 +98,17 @@ export class MigrationCutoverDialog {
|
||||
|
||||
flexTarget.addItem(targetDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetServer.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -122,17 +127,17 @@ export class MigrationCutoverDialog {
|
||||
|
||||
flexStatus.addItem(migrationStatus.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(fullBackupFileOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(backupLocation.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,30 +155,28 @@ export class MigrationCutoverDialog {
|
||||
}).component();
|
||||
flexFile.addItem(lastSSN.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackup.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackupOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
const flexInfo = view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'width': '800px',
|
||||
}
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
flexInfo.addItem(flexServer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -181,7 +184,7 @@ export class MigrationCutoverDialog {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -189,7 +192,7 @@ export class MigrationCutoverDialog {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,7 +200,7 @@ export class MigrationCutoverDialog {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '200px'
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
@@ -213,7 +216,7 @@ export class MigrationCutoverDialog {
|
||||
columns: [
|
||||
{
|
||||
value: loc.ACTIVE_BACKUP_FILES,
|
||||
width: 280,
|
||||
width: 230,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
@@ -226,23 +229,36 @@ export class MigrationCutoverDialog {
|
||||
width: 60,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.DATA_UPLOADED,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.COPY_THROUGHPUT,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.BACKUP_START_TIME,
|
||||
width: 130,
|
||||
type: azdata.ColumnType.text
|
||||
}, {
|
||||
},
|
||||
{
|
||||
value: loc.FIRST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
}, {
|
||||
},
|
||||
{
|
||||
value: loc.LAST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
width: '800px',
|
||||
width: '1100px',
|
||||
height: '300px',
|
||||
fontSize: '12px'
|
||||
}).component();
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
@@ -251,13 +267,13 @@ export class MigrationCutoverDialog {
|
||||
component: this.migrationContainerHeader()
|
||||
},
|
||||
{
|
||||
component: this._view.modelBuilder.separator().withProps({ width: '800px' }).component()
|
||||
component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component()
|
||||
},
|
||||
{
|
||||
component: flexInfo
|
||||
},
|
||||
{
|
||||
component: this._view.modelBuilder.separator().withProps({ width: '800px' }).component()
|
||||
component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component()
|
||||
},
|
||||
{
|
||||
component: this._fileCount
|
||||
@@ -271,11 +287,21 @@ export class MigrationCutoverDialog {
|
||||
}
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
this._view.onClosed(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
});
|
||||
return view.initializeModel(form).then((value) => {
|
||||
this.refreshStatus();
|
||||
});
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
|
||||
this._dialogObject.cancelButton.hidden = true;
|
||||
this._dialogObject.okButton.label = loc.CLOSE;
|
||||
|
||||
this._dialogObject.okButton.onClick(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
});
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
@@ -295,6 +321,7 @@ export class MigrationCutoverDialog {
|
||||
'font-weight': 'bold',
|
||||
'margin': '0px'
|
||||
},
|
||||
width: 950,
|
||||
value: this._model._migration.migrationContext.properties.sourceDatabaseName
|
||||
}).component();
|
||||
|
||||
@@ -303,6 +330,7 @@ export class MigrationCutoverDialog {
|
||||
'font-size': '10px',
|
||||
'margin': '5px 0px'
|
||||
},
|
||||
width: 950,
|
||||
value: loc.DATABASE
|
||||
}).component();
|
||||
|
||||
@@ -311,31 +339,43 @@ export class MigrationCutoverDialog {
|
||||
databaseSubTitle
|
||||
]).withLayout({
|
||||
'flexFlow': 'column'
|
||||
}).withProps({
|
||||
width: 950
|
||||
}).component();
|
||||
|
||||
this.setAutoRefresh(refreshFrequency);
|
||||
|
||||
const titleLogoContainer = this._view.modelBuilder.flexContainer().component();
|
||||
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
titleLogoContainer.addItem(sqlDatbaseLogo, {
|
||||
flex: '0'
|
||||
});
|
||||
titleLogoContainer.addItem(titleContainer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '5px'
|
||||
'margin-left': '5px',
|
||||
'width': '930px'
|
||||
}
|
||||
});
|
||||
|
||||
const headerActions = this._view.modelBuilder.flexContainer().withLayout({
|
||||
}).withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
this._cutoverButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.cutover,
|
||||
iconHeight: '14px',
|
||||
iconWidth: '12px',
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COMPLETE_CUTOVER,
|
||||
height: '20px',
|
||||
width: '130px',
|
||||
enabled: false
|
||||
width: '150px',
|
||||
enabled: false,
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._cutoverButton.onDidClick(async (e) => {
|
||||
@@ -355,7 +395,10 @@ export class MigrationCutoverDialog {
|
||||
iconWidth: '16px',
|
||||
label: loc.CANCEL_MIGRATION,
|
||||
height: '20px',
|
||||
width: '120px'
|
||||
width: '150px',
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._cancelButton.onDidClick((e) => {
|
||||
@@ -378,7 +421,10 @@ export class MigrationCutoverDialog {
|
||||
iconWidth: '16px',
|
||||
label: 'Refresh',
|
||||
height: '20px',
|
||||
width: '65px'
|
||||
width: '100px',
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._refreshButton.onDidClick((e) => {
|
||||
@@ -395,7 +441,10 @@ export class MigrationCutoverDialog {
|
||||
iconWidth: '16px',
|
||||
label: loc.COPY_MIGRATION_DETAILS,
|
||||
height: '20px',
|
||||
width: '150px'
|
||||
width: '200px',
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
|
||||
@@ -435,6 +484,10 @@ export class MigrationCutoverDialog {
|
||||
titleLogoContainer
|
||||
]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
width: 1000
|
||||
}
|
||||
}).component();
|
||||
|
||||
header.addItem(headerActions, {
|
||||
@@ -446,6 +499,14 @@ export class MigrationCutoverDialog {
|
||||
return header;
|
||||
}
|
||||
|
||||
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
|
||||
const classVariable = this;
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
if (interval !== -1) {
|
||||
this._autoRefreshHandle = setInterval(function () { classVariable.refreshStatus(); }, interval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async refreshStatus(): Promise<void> {
|
||||
try {
|
||||
@@ -478,7 +539,6 @@ export class MigrationCutoverDialog {
|
||||
|
||||
const migrationStatusTextValue = this._model.migrationStatus.properties.migrationStatus ? this._model.migrationStatus.properties.migrationStatus : this._model.migrationStatus.properties.provisioningState;
|
||||
|
||||
let fullBackupFileName: string;
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
@@ -486,19 +546,20 @@ export class MigrationCutoverDialog {
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
|
||||
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
|
||||
|
||||
tableData.push(
|
||||
{
|
||||
fileName: activeBackupSet.listOfBackupFiles[0].fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: activeBackupSet.listOfBackupFiles[0].status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].dataWritten)}/ ${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].totalSize)}`,
|
||||
copyThroughput: (activeBackupSet.listOfBackupFiles[0].copyThroughput / 1024).toFixed(2),
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
|
||||
}
|
||||
);
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName.substr(activeBackupSet.listOfBackupFiles[0].fileName.lastIndexOf('.') + 1) === 'bak') {
|
||||
fullBackupFileName = activeBackupSet.listOfBackupFiles[0].fileName;
|
||||
}
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
@@ -514,7 +575,7 @@ export class MigrationCutoverDialog {
|
||||
this._targetVersion.value = targetServerVersion;
|
||||
|
||||
this._migrationStatus.value = migrationStatusTextValue ?? '---';
|
||||
this._fullBackupFile.value = fullBackupFileName! ?? '-';
|
||||
this._fullBackupFile.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
|
||||
let backupLocation;
|
||||
|
||||
const isBlobMigration = this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined;
|
||||
@@ -532,7 +593,7 @@ export class MigrationCutoverDialog {
|
||||
|
||||
this._lastAppliedLSN.value = lastAppliedSSN! ?? '-';
|
||||
this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
|
||||
this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
|
||||
this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
|
||||
@@ -544,7 +605,9 @@ export class MigrationCutoverDialog {
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
new Date(row.backupStartTime).toLocaleString(),
|
||||
row.dataUploaded,
|
||||
row.copyThroughput,
|
||||
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
];
|
||||
@@ -578,7 +641,8 @@ export class MigrationCutoverDialog {
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
'font-weight': 'bold',
|
||||
'margin-bottom': '0'
|
||||
'margin-bottom': '0',
|
||||
'font-size': '12px'
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
@@ -590,7 +654,8 @@ export class MigrationCutoverDialog {
|
||||
'margin-bottom': '0',
|
||||
'width': '100%',
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipses'
|
||||
'text-overflow': 'ellipses',
|
||||
'font-size': '12px'
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(textComponent);
|
||||
@@ -610,6 +675,8 @@ interface ActiveBackupFileSchema {
|
||||
fileName: string,
|
||||
type: string,
|
||||
status: string,
|
||||
dataUploaded: string,
|
||||
copyThroughput: string,
|
||||
backupStartTime: string,
|
||||
firstLSN: string,
|
||||
lastLSN: string
|
||||
|
||||
@@ -10,9 +10,11 @@ import { MigrationContext, MigrationLocalStorage } from '../../models/migrationL
|
||||
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
|
||||
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
import { convertTimeDifferenceToDuration, filterMigrations } from '../../api/utils';
|
||||
import { convertTimeDifferenceToDuration, filterMigrations, SupportedAutoRefreshIntervals } from '../../api/utils';
|
||||
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
|
||||
|
||||
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
|
||||
|
||||
export class MigrationStatusDialog {
|
||||
private _model: MigrationStatusDialogModel;
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
@@ -22,6 +24,7 @@ export class MigrationStatusDialog {
|
||||
private _statusDropdown!: azdata.DropDownComponent;
|
||||
private _statusTable!: azdata.DeclarativeTableComponent;
|
||||
private _refreshLoader!: azdata.LoadingComponent;
|
||||
private _autoRefreshHandle!: NodeJS.Timeout;
|
||||
|
||||
constructor(migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
|
||||
this._model = new MigrationStatusDialogModel(migrations);
|
||||
@@ -65,9 +68,17 @@ export class MigrationStatusDialog {
|
||||
}
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
this._view.onClosed(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
});
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
this._dialogObject.cancelButton.hidden = true;
|
||||
this._dialogObject.okButton.label = loc.CLOSE;
|
||||
this._dialogObject.okButton.onClick(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
});
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
@@ -97,9 +108,10 @@ export class MigrationStatusDialog {
|
||||
});
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 900,
|
||||
CSSStyles: {
|
||||
'justify-content': 'left'
|
||||
}
|
||||
},
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(this._searchBox, {
|
||||
@@ -124,8 +136,25 @@ export class MigrationStatusDialog {
|
||||
'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'
|
||||
}
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
return flexContainer;
|
||||
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
|
||||
let classVariable = this;
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
if (interval !== -1) {
|
||||
this._autoRefreshHandle = setInterval(function () { classVariable.refreshTable(); }, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private populateMigrationTable(): void {
|
||||
@@ -277,7 +306,7 @@ export class MigrationStatusDialog {
|
||||
{
|
||||
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '160px',
|
||||
width: '130px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
|
||||
Reference in New Issue
Block a user