Add SQL DB offline migration wizard experience (#20403)

* sql db wizard with target selection

* add database table selection

* add sqldb to service and IR page

* Code complete

* navigation bug fixes

* fix target db selection

* improve sqldb error and status reporting

* fix error count bug

* remove table status inference

* address review feedback

* update resource strings and content

* fix migraton status string, use localized value

* fix ux navigation issues

* fix back/fwd w/o changes from changing data
This commit is contained in:
brian-harris
2022-08-19 18:12:34 -07:00
committed by GitHub
parent c0b09dcedd
commit 7a736b76fa
42 changed files with 5716 additions and 4209 deletions

View File

@@ -118,12 +118,16 @@ export class AssessmentResultsDialog {
this._model._miDbs = selectedDbs;
break;
}
case MigrationTargetType.SQLVM: {
this.didUpdateDatabasesForMigration(this._model._vmDbs, selectedDbs);
this._model._vmDbs = selectedDbs;
break;
}
case MigrationTargetType.SQLDB: {
this.didUpdateDatabasesForMigration(this._model._sqldbDbs, selectedDbs);
this._model._sqldbDbs = selectedDbs;
break;
}
}
await this._skuRecommendationPage.refreshCardText();
this.model.refreshDatabaseBackupPage = true;

View File

@@ -9,25 +9,28 @@ import * as constants from '../../constants/strings';
import { MigrationStateModel } from '../../models/stateMachine';
import { WizardController } from '../../wizard/wizardController';
import * as styles from '../../constants/styles';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
export class SavedAssessmentDialog {
private static readonly OkButtonText: string = constants.NEXT_LABEL;
private static readonly CancelButtonText: string = constants.CANCEL_LABEL;
private _isOpen: boolean = false;
private dialog: azdata.window.Dialog | undefined;
private _rootContainer!: azdata.FlexContainer;
private stateModel: MigrationStateModel;
private context: vscode.ExtensionContext;
private _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>;
private _disposables: vscode.Disposable[] = [];
private _isOpen: boolean = false;
private _rootContainer!: azdata.FlexContainer;
constructor(
context: vscode.ExtensionContext,
stateModel: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) {
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
this.stateModel = stateModel;
this.context = context;
this._serviceContextChangedEvent = serviceContextChangedEvent;
}
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
@@ -36,18 +39,18 @@ export class SavedAssessmentDialog {
try {
this._rootContainer = this.initializePageContent(view);
await view.initializeModel(this._rootContainer);
this._disposables.push(dialog.okButton.onClick(async e => {
await this.execute();
}));
this._disposables.push(dialog.cancelButton.onClick(e => {
this.cancel();
}));
this._disposables.push(
dialog.okButton.onClick(
async e => await this.execute()));
this._disposables.push(
dialog.cancelButton.onClick(
e => this.cancel()));
this._disposables.push(
view.onClosed(
e => this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
this._disposables.push(view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } }
);
}));
resolve();
} catch (ex) {
reject(ex);
@@ -83,7 +86,7 @@ export class SavedAssessmentDialog {
const wizardController = new WizardController(
this.context,
this.stateModel,
this._onClosedCallback);
this._serviceContextChangedEvent);
await wizardController.openWizard(this.stateModel.sourceConnectionId);
this._isOpen = false;
@@ -100,44 +103,39 @@ export class SavedAssessmentDialog {
public initializePageContent(view: azdata.ModelView): azdata.FlexContainer {
const buttonGroup = 'resumeMigration';
const radioStart = view.modelBuilder.radioButton().withProps({
label: constants.START_NEW_SESSION,
name: buttonGroup,
CSSStyles: {
...styles.BODY_CSS,
'margin-bottom': '8px'
},
checked: true
}).component();
const radioStart = view.modelBuilder.radioButton()
.withProps({
label: constants.START_NEW_SESSION,
name: buttonGroup,
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '8px' },
checked: true
}).component();
this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = false;
}
}));
const radioContinue = view.modelBuilder.radioButton().withProps({
label: constants.RESUME_SESSION,
name: buttonGroup,
CSSStyles: {
...styles.BODY_CSS,
},
checked: false
}).component();
this._disposables.push(
radioStart.onDidChangeCheckedState(checked => {
if (checked) {
this.stateModel.resumeAssessment = false;
}
}));
const radioContinue = view.modelBuilder.radioButton()
.withProps({
label: constants.RESUME_SESSION,
name: buttonGroup,
CSSStyles: { ...styles.BODY_CSS },
checked: false
}).component();
this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = true;
}
}));
this._disposables.push(
radioContinue.onDidChangeCheckedState(checked => {
if (checked) {
this.stateModel.resumeAssessment = true;
}
}));
const flex = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
}).withProps({
CSSStyles: {
'padding': '20px 15px',
}
}).component();
.withLayout({ flexFlow: 'column', })
.withProps({ CSSStyles: { 'padding': '20px 15px', } })
.component();
flex.addItem(radioStart, { flex: '0 0 auto' });
flex.addItem(radioContinue, { flex: '0 0 auto' });

View File

@@ -91,7 +91,14 @@ export class SqlDatabaseTree {
const selectDbMessage = this.createSelectDbMessage();
this._resultComponent = await this.createComponentResult(view);
const treeComponent = await this.createComponent(view, this._targetType === MigrationTargetType.SQLVM ? this._model._vmDbs : this._model._miDbs);
const treeComponent = await this.createComponent(
view,
(this._targetType === MigrationTargetType.SQLVM)
? this._model._vmDbs
: (this._targetType === MigrationTargetType.SQLMI)
? this._model._miDbs
: this._model._sqldbDbs);
this._rootContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
height: '100%',
@@ -101,7 +108,8 @@ export class SqlDatabaseTree {
this._rootContainer.addItem(this._resultComponent, { flex: '0 0 auto' });
this._rootContainer.addItem(selectDbMessage, { flex: '1 1 auto' });
if (this._targetType === MigrationTargetType.SQLMI) {
if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
if (!!this._model._assessmentResults?.issues.find(value => value.databaseRestoreFails) ||
!!this._model._assessmentResults?.databaseAssessments.find(d => !!d.issues.find(issue => issue.databaseRestoreFails))) {
dialog.message = {
@@ -192,7 +200,8 @@ export class SqlDatabaseTree {
}));
this._disposables.push(this._databaseTable.onRowSelected(async (e) => {
if (this._targetType === MigrationTargetType.SQLMI) {
if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
this._activeIssues = this._model._assessmentResults?.databaseAssessments[e.row].issues;
} else {
this._activeIssues = [];
@@ -306,7 +315,8 @@ export class SqlDatabaseTree {
});
this._recommendation.value = constants.WARNINGS_DETAILS;
this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues?.length);
if (this._targetType === MigrationTargetType.SQLMI) {
if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
await this.refreshResults();
}
}));
@@ -388,42 +398,34 @@ export class SqlDatabaseTree {
}
private createNoIssuesText(): azdata.FlexContainer {
let message: azdata.TextComponent;
const failedAssessment = this.handleFailedAssessment();
if (this._targetType === MigrationTargetType.SQLVM) {
message = this._view.modelBuilder.text().withProps({
value: failedAssessment
? constants.NO_RESULTS_AVAILABLE
: constants.NO_ISSUES_FOUND_VM,
CSSStyles: {
...styles.BODY_CSS
}
}).component();
} else {
message = this._view.modelBuilder.text().withProps({
value: failedAssessment
? constants.NO_RESULTS_AVAILABLE
: constants.NO_ISSUES_FOUND_MI,
CSSStyles: {
...styles.BODY_CSS
}
}).component();
}
//TODO: will need to add a SQL DB condition here in the future
this._noIssuesContainer = this._view.modelBuilder.flexContainer().withItems([message]).withProps({
CSSStyles: {
'margin-top': '8px',
'display': 'none'
}
}).component();
const value = failedAssessment
? constants.NO_RESULTS_AVAILABLE
: (this._targetType === MigrationTargetType.SQLVM)
? constants.NO_ISSUES_FOUND_VM
: (this._targetType === MigrationTargetType.SQLMI)
? constants.NO_ISSUES_FOUND_MI
: constants.NO_ISSUES_FOUND_SQLDB;
const message = this._view.modelBuilder.text()
.withProps({
value: value,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._noIssuesContainer = this._view.modelBuilder.flexContainer()
.withItems([message])
.withProps({ CSSStyles: { 'margin-top': '8px', 'display': 'none' } })
.component();
return this._noIssuesContainer;
}
private handleFailedAssessment(): boolean {
const failedAssessment: boolean = this._model._assessmentResults?.assessmentError !== undefined
|| (this._model._assessmentResults?.errors?.length || 0) > 0;
|| (this._model._assessmentResults?.errors?.length ?? 0) > 0;
if (failedAssessment) {
this._dialog.message = {
level: azdata.window.MessageLevel.Warning,
@@ -471,16 +473,12 @@ export class SqlDatabaseTree {
private createAssessmentContainer(): azdata.FlexContainer {
const title = this.createAssessmentTitle();
const bottomContainer = this.createDescriptionContainer();
const container = this._view.modelBuilder.flexContainer().withItems([title, bottomContainer]).withLayout({
flexFlow: 'column'
}).withProps({
CSSStyles: {
'margin-left': '24px'
}
}).component();
const container = this._view.modelBuilder.flexContainer()
.withItems([title, bottomContainer])
.withLayout({ flexFlow: 'column' })
.withProps({ CSSStyles: { 'margin-left': '24px' } })
.component();
return container;
}
@@ -488,14 +486,10 @@ export class SqlDatabaseTree {
private createDescriptionContainer(): azdata.FlexContainer {
const description = this.createDescription();
const impactedObjects = this.createImpactedObjectsDescription();
const container = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row'
}).withProps({
CSSStyles: {
'height': '100%'
}
}).component();
const container = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'row' })
.withProps({ CSSStyles: { 'height': '100%' } })
.component();
container.addItem(description, { flex: '0 0 auto', CSSStyles: { 'width': '200px', 'margin-right': '35px' } });
container.addItem(impactedObjects, { flex: '0 0 auto', CSSStyles: { 'width': '280px' } });
@@ -541,19 +535,8 @@ export class SqlDatabaseTree {
rowCssStyles: rowStyle
},
],
dataValues: [
[
{
value: ''
},
{
value: ''
}
]
],
CSSStyles: {
'margin-top': '12px'
}
dataValues: [[{ value: '' }, { value: '' }]],
CSSStyles: { 'margin-top': '12px' }
}
).component();
@@ -562,36 +545,47 @@ export class SqlDatabaseTree {
this.refreshImpactedObject(impactedObject);
}));
const objectDetailsTitle = this._view.modelBuilder.text().withProps({
value: constants.OBJECT_DETAILS,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin': '12px 0px 0px 0px',
}
}).component();
const objectDetailsTitle = this._view.modelBuilder.text()
.withProps({
value: constants.OBJECT_DETAILS,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin': '12px 0px 0px 0px',
}
}).component();
const objectDescriptionStyle = {
...styles.BODY_CSS,
'margin': '5px 0px 0px 0px',
'word-wrap': 'break-word'
};
this._objectDetailsType = this._view.modelBuilder.text().withProps({
value: constants.TYPES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsType = this._view.modelBuilder.text()
.withProps({
value: constants.TYPES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsName = this._view.modelBuilder.text().withProps({
value: constants.NAMES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsName = this._view.modelBuilder.text()
.withProps({
value: constants.NAMES_LABEL,
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsSample = this._view.modelBuilder.text().withProps({
value: '',
CSSStyles: objectDescriptionStyle
}).component();
this._objectDetailsSample = this._view.modelBuilder.text()
.withProps({
value: '',
CSSStyles: objectDescriptionStyle
}).component();
const container = this._view.modelBuilder.flexContainer().withItems([impactedObjectsTitle, this._impactedObjectsTable, objectDetailsTitle, this._objectDetailsType, this._objectDetailsName, this._objectDetailsSample]).withLayout({
flexFlow: 'column'
}).component();
const container = this._view.modelBuilder.flexContainer()
.withItems([
impactedObjectsTitle,
this._impactedObjectsTable,
objectDetailsTitle,
this._objectDetailsType,
this._objectDetailsName,
this._objectDetailsSample])
.withLayout({ flexFlow: 'column' })
.component();
return container;
}
@@ -607,76 +601,91 @@ export class SqlDatabaseTree {
'width': '200px',
'word-wrap': 'break-word'
};
const descriptionTitle = this._view.modelBuilder.text().withProps({
value: constants.DESCRIPTION,
CSSStyles: LABEL_CSS
}).component();
this._descriptionText = this._view.modelBuilder.text().withProps({
CSSStyles: textStyle
}).component();
const descriptionTitle = this._view.modelBuilder.text()
.withProps({
value: constants.DESCRIPTION,
CSSStyles: LABEL_CSS
}).component();
this._descriptionText = this._view.modelBuilder.text()
.withProps({
CSSStyles: textStyle
}).component();
const recommendationTitle = this._view.modelBuilder.text().withProps({
value: constants.RECOMMENDATION,
CSSStyles: LABEL_CSS
}).component();
this._recommendationText = this._view.modelBuilder.text().withProps({
CSSStyles: textStyle
}).component();
const moreInfo = this._view.modelBuilder.text().withProps({
value: constants.MORE_INFO,
CSSStyles: LABEL_CSS
}).component();
this._moreInfo = this._view.modelBuilder.hyperlink().withProps({
label: '',
url: '',
CSSStyles: textStyle,
ariaLabel: constants.MORE_INFO,
showLinkIcon: true
}).component();
const recommendationTitle = this._view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDATION,
CSSStyles: LABEL_CSS
}).component();
this._recommendationText = this._view.modelBuilder.text()
.withProps({
CSSStyles: textStyle
}).component();
const moreInfo = this._view.modelBuilder.text()
.withProps({
value: constants.MORE_INFO,
CSSStyles: LABEL_CSS
}).component();
this._moreInfo = this._view.modelBuilder.hyperlink()
.withProps({
label: '',
url: '',
CSSStyles: textStyle,
ariaLabel: constants.MORE_INFO,
showLinkIcon: true
}).component();
const container = this._view.modelBuilder.flexContainer().withItems([descriptionTitle, this._descriptionText, recommendationTitle, this._recommendationText, moreInfo, this._moreInfo]).withLayout({
flexFlow: 'column'
}).component();
const container = this._view.modelBuilder.flexContainer()
.withItems([descriptionTitle,
this._descriptionText,
recommendationTitle,
this._recommendationText,
moreInfo,
this._moreInfo])
.withLayout({ flexFlow: 'column' })
.component();
return container;
}
private createAssessmentTitle(): azdata.TextComponent {
this._assessmentTitle = this._view.modelBuilder.text().withProps({
value: '',
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '12px',
'height': '48px',
'width': '540px',
'border-bottom': 'solid 1px'
}
}).component();
this._assessmentTitle = this._view.modelBuilder.text()
.withProps({
value: '',
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '12px',
'height': '48px',
'width': '540px',
'border-bottom': 'solid 1px'
}
}).component();
return this._assessmentTitle;
}
private createTitleComponent(): azdata.TextComponent {
const title = this._view.modelBuilder.text().withProps({
value: constants.TARGET_PLATFORM,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0 0 4px 0'
}
});
return title.component();
return this._view.modelBuilder.text()
.withProps({
value: constants.TARGET_PLATFORM,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0 0 4px 0'
}
}).component();
}
private createPlatformComponent(): azdata.TextComponent {
const impact = this._view.modelBuilder.text().withProps({
value: (this._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE,
CSSStyles: {
...styles.PAGE_SUBTITLE_CSS
}
});
const target = (this._targetType === MigrationTargetType.SQLVM)
? constants.SUMMARY_VM_TYPE
: (this._targetType === MigrationTargetType.SQLMI)
? constants.SUMMARY_MI_TYPE
: constants.SUMMARY_SQLDB_TYPE;
return impact.component();
return this._view.modelBuilder.text()
.withProps({
value: target,
CSSStyles: { ...styles.PAGE_SUBTITLE_CSS }
}).component();
}
private createRecommendationComponent(): azdata.TextComponent {
@@ -718,7 +727,6 @@ export class SqlDatabaseTree {
}
private createImpactedObjectsTable(): azdata.FlexContainer {
const headerStyle: azdata.CssStyles = {
'border': 'none',
'text-align': 'left'
@@ -732,13 +740,11 @@ export class SqlDatabaseTree {
'overflow': 'hidden',
};
this._assessmentResultsTable = this._view.modelBuilder.declarativeTable().withProps(
{
this._assessmentResultsTable = this._view.modelBuilder.declarativeTable()
.withProps({
enableRowSelection: true,
width: '200px',
CSSStyles: {
'table-layout': 'fixed'
},
CSSStyles: { 'table-layout': 'fixed' },
columns: [
{
displayName: '',
@@ -758,21 +764,21 @@ export class SqlDatabaseTree {
}
]
}
).component();
).component();
this._disposables.push(this._assessmentResultsTable.onRowSelected(async (e) => {
const selectedIssue = e.row > -1 ? this._activeIssues[e.row] : undefined;
await this.refreshAssessmentDetails(selectedIssue);
}));
const container = this._view.modelBuilder.flexContainer().withItems([this._assessmentResultsTable]).withLayout({
flexFlow: 'column',
height: '100%'
}).withProps({
CSSStyles: {
'border-right': 'solid 1px'
}
}).component();
const container = this._view.modelBuilder.flexContainer()
.withItems([this._assessmentResultsTable])
.withLayout({
flexFlow: 'column',
height: '100%'
})
.withProps({ CSSStyles: { 'border-right': 'solid 1px' } })
.component();
return container;
}
@@ -788,42 +794,23 @@ export class SqlDatabaseTree {
}
public async refreshResults(): Promise<void> {
if (this._targetType === MigrationTargetType.SQLMI) {
if (this._targetType === MigrationTargetType.SQLMI ||
this._targetType === MigrationTargetType.SQLDB) {
if (this._activeIssues?.length === 0) {
/// show no issues here
await this._assessmentsTable.updateCssStyles({
'display': 'none',
'border-right': 'none'
});
await this._assessmentContainer.updateCssStyles({
'display': 'none'
});
await this._noIssuesContainer.updateCssStyles({
'display': 'flex'
});
await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' });
await this._assessmentContainer.updateCssStyles({ 'display': 'none' });
await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' });
} else {
await this._assessmentContainer.updateCssStyles({
'display': 'flex'
});
await this._assessmentsTable.updateCssStyles({
'display': 'flex',
'border-right': 'solid 1px'
});
await this._noIssuesContainer.updateCssStyles({
'display': 'none'
});
await this._assessmentContainer.updateCssStyles({ 'display': 'flex' });
await this._assessmentsTable.updateCssStyles({ 'display': 'flex', 'border-right': 'solid 1px' });
await this._noIssuesContainer.updateCssStyles({ 'display': 'none' });
}
} else {
await this._assessmentsTable.updateCssStyles({
'display': 'none',
'border-right': 'none'
});
await this._assessmentContainer.updateCssStyles({
'display': 'none'
});
await this._noIssuesContainer.updateCssStyles({
'display': 'flex'
});
await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' });
await this._assessmentContainer.updateCssStyles({ 'display': 'none' });
await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' });
this._recommendationTitle.value = constants.ASSESSMENT_RESULTS;
this._recommendation.value = '';
}
@@ -868,8 +855,9 @@ export class SqlDatabaseTree {
this._impactedObjects = selectedIssue?.impactedObjects || [];
this._recommendationText.value = selectedIssue?.message || constants.NA;
await this._impactedObjectsTable.setDataValues(this._impactedObjects.map(
(object) => [{ value: object.objectType }, { value: object.name }]));
await this._impactedObjectsTable.setDataValues(
this._impactedObjects.map(
(object) => [{ value: object.objectType }, { value: object.name }]));
this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1;
}
@@ -884,56 +872,55 @@ export class SqlDatabaseTree {
let instanceTableValues: azdata.DeclarativeTableCellValue[][] = [];
this._databaseTableValues = [];
this._dbNames = this._model._databasesForAssessment;
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM) ? this._model._vmDbs : this._model._miDbs;
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM)
? this._model._vmDbs
: (this._targetType === MigrationTargetType.SQLMI)
? this._model._miDbs
: this._model._sqldbDbs;
this._serverName = (await this._model.getSourceConnectionProfile()).serverName;
if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) {
instanceTableValues = [
[
instanceTableValues = [[
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft
},
{
value: '0',
style: styleRight
}
]];
this._dbNames.forEach((db) => {
this._databaseTableValues.push([
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
value: selectedDbs.includes(db),
style: styleLeft
},
{
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db),
style: styleLeft
},
{
value: '0',
style: styleRight
}
]
];
this._dbNames.forEach((db) => {
this._databaseTableValues.push(
[
{
value: selectedDbs.includes(db),
style: styleLeft
},
{
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db),
style: styleLeft
},
{
value: '0',
style: styleRight
}
]
);
]);
});
} else {
instanceTableValues = [
[
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft
},
{
value: this._model._assessmentResults?.issues?.length,
style: styleRight
}
]
];
this._model._assessmentResults?.databaseAssessments.sort((db1, db2) => {
return db2.issues?.length - db1.issues?.length;
});
instanceTableValues = [[
{
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
style: styleLeft
},
{
value: this._model._assessmentResults?.issues?.length,
style: styleRight
}
]];
this._model._assessmentResults?.databaseAssessments
.sort((db1, db2) => db2.issues?.length - db1.issues?.length);
// Reset the dbName list so that it is in sync with the table
this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name);
this._model._assessmentResults?.databaseAssessments.forEach((db) => {
@@ -941,23 +928,21 @@ export class SqlDatabaseTree {
if (db.issues.find(item => item.databaseRestoreFails)) {
selectable = false;
}
this._databaseTableValues.push(
[
{
value: selectedDbs.includes(db.name),
style: styleLeft,
enabled: selectable
},
{
value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name),
style: styleLeft
},
{
value: db.issues?.length,
style: styleRight
}
]
);
this._databaseTableValues.push([
{
value: selectedDbs.includes(db.name),
style: styleLeft,
enabled: selectable
},
{
value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name),
style: styleLeft
},
{
value: db.issues?.length,
style: styleRight
}
]);
});
}
await this._instanceTable.setDataValues(instanceTableValues);
@@ -973,47 +958,7 @@ export class SqlDatabaseTree {
});
}
// 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': '100%',
// }
// }).component();
// cellContainer.addItem(textComponent, {
// CSSStyles: {
// 'width': 'auto'
// }
// });
// return cellContainer;
// }
// undo when bug #16445 is fixed
}

View File

@@ -390,8 +390,12 @@ export class CreateSqlMigrationServiceDialog {
private async populateResourceGroups(): Promise<void> {
this.migrationServiceResourceGroupDropdown.loading = true;
try {
this._resourceGroups = await utils.getAllResourceGroups(this._model._azureAccount, this._model._targetSubscription);
this.migrationServiceResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups);
this._resourceGroups = await utils.getAllResourceGroups(
this._model._azureAccount,
this._model._targetSubscription);
this.migrationServiceResourceGroupDropdown.values = utils.getResourceDropdownValues(
this._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.name.toLowerCase() === this._resourceGroupPreset.toLowerCase());
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0];

View File

@@ -156,20 +156,21 @@ export class ConfirmCutoverDialog {
height: 20,
label: constants.REFRESH,
}).component();
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
this._disposables.push(
refreshButton.onDidClick(async e => {
try {
refreshLoader.loading = true;
await this.migrationCutoverModel.fetchStatus();
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
container.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
@@ -232,22 +233,23 @@ export class ConfirmCutoverDialog {
headingRow.addItem(containerHeading, { flex: '0' });
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));
this.refreshFileTable(fileTable);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
this._disposables.push(
refreshButton.onDidClick(async e => {
try {
refreshLoader.loading = true;
await this.migrationCutoverModel.fetchStatus();
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));
this.refreshFileTable(fileTable);
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.message
};
} finally {
refreshLoader.loading = false;
}
}));
headingRow.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings';
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
@@ -110,7 +110,7 @@ export class MigrationCutoverDialogModel {
const files: BackupFileInfo[] = [];
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
abs.listOfBackupFiles.forEach(f => {
if (f.status !== BackupFileInfoStatus.Restored) {
if (f.status !== constants.BackupFileInfoStatus.Restored) {
files.push(f);
}
});

View File

@@ -13,6 +13,7 @@ import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
import * as constants from '../../constants/strings';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
export class RetryMigrationDialog {
@@ -20,15 +21,20 @@ export class RetryMigrationDialog {
private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) {
private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
}
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);
private async createMigrationStateModel(
serviceContext: MigrationServiceContext,
migration: DatabaseMigration,
connectionId: string,
serverName: string,
api: mssql.IExtension,
location: azureResource.AzureLocation): Promise<MigrationStateModel> {
const stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
const sourceDatabaseName = migration.properties.sourceDatabaseName;
let savedInfo: SavedInfo;
savedInfo = {
const savedInfo: SavedInfo = {
closedPage: 0,
// DatabaseSelector
@@ -142,7 +148,7 @@ export class RetryMigrationDialog {
}
});
let activeConnection = await azdata.connection.getCurrentConnection();
const activeConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = '';
let serverName: string = '';
if (!activeConnection) {
@@ -163,7 +169,7 @@ export class RetryMigrationDialog {
const wizardController = new WizardController(
this._context,
stateModel,
this._onClosedCallback);
this._serviceContextChangedEvent);
await wizardController.openWizard(stateModel.sourceConnectionId);
} else {
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);

View File

@@ -12,6 +12,7 @@ import * as constants from '../../constants/strings';
import * as utils from '../../api/utils';
import { SqlMigrationService } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery';
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
const CONTROL_MARGIN = '20px';
const INPUT_COMPONENT_WIDTH = '100%';
@@ -56,7 +57,7 @@ export class SelectMigrationServiceDialog {
private _deleteButton!: azdata.window.Button;
constructor(
private readonly _onClosedCallback: () => Promise<void>) {
private readonly onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>) {
this._dialog = azdata.window.createModelViewDialog(
constants.MIGRATION_SERVICE_SELECT_TITLE,
'SelectMigraitonServiceDialog',
@@ -85,10 +86,10 @@ export class SelectMigrationServiceDialog {
'left');
this._disposables.push(
this._deleteButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext({});
await this._onClosedCallback();
await MigrationLocalStorage.saveMigrationServiceContext({}, this.onServiceContextChanged);
azdata.window.closeDialog(this._dialog);
}));
this._dialog.customButtons = [this._deleteButton];
azdata.window.openDialog(this._dialog);
@@ -262,7 +263,7 @@ export class SelectMigrationServiceDialog {
? utils.deepClone(selectedLocation)
: undefined!;
await this._populateResourceGroupDropdown();
await this._populateMigrationServiceDropdown();
this._populateMigrationServiceDropdown();
}
}));
@@ -290,7 +291,7 @@ export class SelectMigrationServiceDialog {
this._serviceContext.resourceGroup = (selectedResourceGroup)
? utils.deepClone(selectedResourceGroup)
: undefined!;
await this._populateMigrationServiceDropdown();
this._populateMigrationServiceDropdown();
}
}));
@@ -323,10 +324,10 @@ export class SelectMigrationServiceDialog {
}));
this._disposables.push(
this._dialog.okButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext);
await this._onClosedCallback();
}));
this._dialog.okButton.onClick(async (value) =>
await MigrationLocalStorage.saveMigrationServiceContext(
this._serviceContext,
this.onServiceContextChanged)));
return this._view.modelBuilder.flexContainer()
.withItems([
@@ -417,8 +418,14 @@ export class SelectMigrationServiceDialog {
private async _populateLocationDropdown(): Promise<void> {
try {
this._azureLocationDropdown.loading = true;
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this._serviceContext.azureAccount, this._serviceContext.subscription);
this._locations = await utils.getSqlMigrationServiceLocations(this._serviceContext.azureAccount, this._serviceContext.subscription, this._sqlMigrationServices);
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(
this._serviceContext.azureAccount,
this._serviceContext.subscription);
this._locations = await utils.getResourceLocations(
this._serviceContext.azureAccount,
this._serviceContext.subscription,
this._sqlMigrationServices);
this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this._locations);
if (this._azureLocationDropdown.values.length > 0) {
utils.selectDefaultDropdownValue(
@@ -439,8 +446,13 @@ export class SelectMigrationServiceDialog {
private async _populateResourceGroupDropdown(): Promise<void> {
try {
this._azureResourceGroupDropdown.loading = true;
this._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this._sqlMigrationServices, this._serviceContext.location!);
this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups);
this._resourceGroups = utils.getServiceResourceGroupsByLocation(
this._sqlMigrationServices,
this._serviceContext.location!);
this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues(
this._resourceGroups,
constants.RESOURCE_GROUP_NOT_FOUND);
if (this._azureResourceGroupDropdown.values.length > 0) {
utils.selectDefaultDropdownValue(
this._azureResourceGroupDropdown,
@@ -457,10 +469,15 @@ export class SelectMigrationServiceDialog {
}
}
private async _populateMigrationServiceDropdown(): Promise<void> {
private _populateMigrationServiceDropdown(): void {
try {
this._azureServiceDropdown.loading = true;
this._azureServiceDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this._sqlMigrationServices, this._serviceContext.location!, this._serviceContext.resourceGroup!);
this._azureServiceDropdown.values = utils.getAzureResourceDropdownValues(
this._sqlMigrationServices,
this._serviceContext.location!,
this._serviceContext.resourceGroup?.name,
constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR);
if (this._azureServiceDropdown.values.length > 0) {
utils.selectDefaultDropdownValue(
this._azureServiceDropdown,

View File

@@ -111,93 +111,86 @@ export class GetAzureRecommendationDialog {
'margin': '0'
},
}).component();
this._disposables.push(collectDataButton.onDidChangeCheckedState(async (e) => {
if (e) {
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.CollectData);
}
}));
this._disposables.push(
collectDataButton.onDidChangeCheckedState(async checked => {
if (checked) {
await this.switchDataSourceContainerFields(
PerformanceDataSourceOptions.CollectData);
}
}));
const openExistingButton = _view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.AZURE_RECOMMENDATION_OPEN_EXISTING,
checked: this._performanceDataSource === PerformanceDataSourceOptions.OpenExisting,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0 12px',
}
CSSStyles: { ...styles.BODY_CSS, 'margin': '0 12px' }
}).component();
this._disposables.push(openExistingButton.onDidChangeCheckedState(async (e) => {
if (e) {
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.OpenExisting);
}
}));
this._disposables.push(
openExistingButton.onDidChangeCheckedState(async checked => {
if (checked) {
await this.switchDataSourceContainerFields(
PerformanceDataSourceOptions.OpenExisting);
}
}));
radioButtonContainer.addItems([
collectDataButton,
openExistingButton
]);
openExistingButton]);
this._collectDataContainer = this.createCollectDataContainer(_view);
this._openExistingContainer = this.createOpenExistingContainer(_view);
const container = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
chooseMethodText,
radioButtonContainer,
this._openExistingContainer,
this._collectDataContainer,
]).component();
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
chooseMethodText,
radioButtonContainer,
this._openExistingContainer,
this._collectDataContainer])
.component();
return container;
}
private createCollectDataContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'column',
'display': 'inline',
}
}).component();
const container = _view.modelBuilder.flexContainer()
.withProps(
{ CSSStyles: { 'flex-direction': 'column', 'display': 'inline' } })
.component();
const instructions = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
CSSStyles: {
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component();
const instructions = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
CSSStyles: { ...styles.LABEL_CSS, 'margin-bottom': '8px' }
}).component();
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'row',
'align-items': 'center',
}
}).component();
const selectFolderContainer = _view.modelBuilder.flexContainer()
.withProps(
{ CSSStyles: { 'flex-direction': 'row', 'align-items': 'center' } })
.component();
this._collectDataFolderInput = _view.modelBuilder.inputBox().withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: {
'margin-right': '12px'
},
}).component();
this._disposables.push(this._collectDataFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
this._collectDataFolderInput = _view.modelBuilder.inputBox()
.withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: { 'margin-right': '12px' },
}).component();
this._disposables.push(
this._collectDataFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
const browseButton = _view.modelBuilder.button().withProps({
label: constants.BROWSE,
width: 100,
CSSStyles: {
'margin': '0'
}
}).component();
const browseButton = _view.modelBuilder.button()
.withProps({
label: constants.BROWSE,
width: 100,
CSSStyles: { 'margin': '0' }
}).component();
this._disposables.push(browseButton.onDidClick(async (e) => {
let folder = await utils.promptUserForFolder();
this._collectDataFolderInput.value = folder;
@@ -205,74 +198,61 @@ export class GetAzureRecommendationDialog {
selectFolderContainer.addItems([
this._collectDataFolderInput,
browseButton,
]);
browseButton]);
container.addItems([
instructions,
selectFolderContainer,
]);
selectFolderContainer]);
return container;
}
private createOpenExistingContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'column',
'display': 'none',
}
}).component();
const container = _view.modelBuilder.flexContainer()
.withProps(
{ CSSStyles: { 'flex-direction': 'column', 'display': 'none', } })
.component();
const instructions = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
CSSStyles: {
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component();
const instructions = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
CSSStyles: { ...styles.LABEL_CSS, 'margin-bottom': '8px' }
}).component();
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'row',
'align-items': 'center',
}
}).component();
const selectFolderContainer = _view.modelBuilder.flexContainer()
.withProps(
{ CSSStyles: { 'flex-direction': 'row', 'align-items': 'center' } })
.component();
this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: {
'margin-right': '12px'
},
CSSStyles: { 'margin-right': '12px' },
}).component();
this._disposables.push(this._openExistingFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
this._disposables.push(
this._openExistingFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
const openButton = _view.modelBuilder.button().withProps({
label: constants.OPEN,
width: 100,
CSSStyles: {
'margin': '0'
}
}).component();
this._disposables.push(openButton.onDidClick(async (e) => {
let folder = await utils.promptUserForFolder();
this._openExistingFolderInput.value = folder;
}));
const openButton = _view.modelBuilder.button()
.withProps({
label: constants.OPEN,
width: 100,
CSSStyles: { 'margin': '0' }
}).component();
this._disposables.push(
openButton.onDidClick(
async (e) => this._openExistingFolderInput.value = await utils.promptUserForFolder()));
selectFolderContainer.addItems([
this._openExistingFolderInput,
openButton,
]);
openButton]);
container.addItems([
instructions,
selectFolderContainer,
]);
selectFolderContainer]);
return container;
}
@@ -281,24 +261,22 @@ export class GetAzureRecommendationDialog {
let okButtonEnabled = false;
switch (containerType) {
case PerformanceDataSourceOptions.CollectData: {
await this._collectDataContainer.updateCssStyles({ 'display': 'inline' });
await this._openExistingContainer.updateCssStyles({ 'display': 'none' });
case PerformanceDataSourceOptions.CollectData:
await utils.updateControlDisplay(this._collectDataContainer, true);
await utils.updateControlDisplay(this._openExistingContainer, false);
if (this._collectDataFolderInput.value) {
okButtonEnabled = true;
}
break;
}
case PerformanceDataSourceOptions.OpenExisting: {
await this._collectDataContainer.updateCssStyles({ 'display': 'none' });
await this._openExistingContainer.updateCssStyles({ 'display': 'inline' });
case PerformanceDataSourceOptions.OpenExisting:
await utils.updateControlDisplay(this._collectDataContainer, false);
await utils.updateControlDisplay(this._openExistingContainer, true);
if (this._openExistingFolderInput.value) {
okButtonEnabled = true;
}
break;
}
}
this.dialog!.okButton.enabled = okButtonEnabled;
}
@@ -306,27 +284,32 @@ export class GetAzureRecommendationDialog {
public async openDialog(dialogName?: string) {
if (!this._isOpen) {
this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(constants.GET_AZURE_RECOMMENDATION, 'GetAzureRecommendationsDialog', 'narrow');
this.dialog = azdata.window.createModelViewDialog(
constants.GET_AZURE_RECOMMENDATION,
'GetAzureRecommendationsDialog',
'narrow');
this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText;
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
this._disposables.push(this.dialog.cancelButton.onClick(() => this._isOpen = false));
this._disposables.push(
this.dialog.okButton.onClick(
async () => await this.execute()));
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));
this._disposables.push(
this.dialog.cancelButton.onClick(
() => this._isOpen = false));
const promise = this.initializeDialog(this.dialog);
azdata.window.openDialog(this.dialog);
await Promise.all(dialogSetupPromises);
await promise;
// if data source was previously selected, default folder value to previously selected
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
case PerformanceDataSourceOptions.CollectData: {
case PerformanceDataSourceOptions.CollectData:
this._collectDataFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
break;
}
case PerformanceDataSourceOptions.OpenExisting: {
case PerformanceDataSourceOptions.OpenExisting:
this._openExistingFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
break;
}
}
await this.switchDataSourceContainerFields(this._performanceDataSource);
@@ -338,16 +321,14 @@ export class GetAzureRecommendationDialog {
this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource;
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
case PerformanceDataSourceOptions.CollectData: {
case PerformanceDataSourceOptions.CollectData:
await this.migrationStateModel.startPerfDataCollection(
this.migrationStateModel._skuRecommendationPerformanceLocation,
this.migrationStateModel._performanceDataQueryIntervalInSeconds,
this.migrationStateModel._staticDataQueryIntervalInSeconds,
this.migrationStateModel._numberOfPerformanceDataQueryIterations,
this.skuRecommendationPage
);
this.skuRecommendationPage);
break;
}
case PerformanceDataSourceOptions.OpenExisting: {
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
const errors: string[] = [];

View File

@@ -25,7 +25,10 @@ export class SkuEditParametersDialog {
private _targetPercentileDropdown!: azdata.DropDownComponent;
private _enablePreviewValue!: boolean;
constructor(public skuRecommendationPage: SKURecommendationPage, public migrationStateModel: MigrationStateModel) {
constructor(
public skuRecommendationPage: SKURecommendationPage,
public migrationStateModel: MigrationStateModel) {
this._enablePreviewValue = true;
}
@@ -35,10 +38,10 @@ export class SkuEditParametersDialog {
try {
const flex = this.createContainer(view);
this._disposables.push(view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
this._disposables.push(
view.onClosed(e =>
this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
await view.initializeModel(flex);
resolve();
@@ -50,56 +53,50 @@ export class SkuEditParametersDialog {
}
private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'margin': '8px 16px',
'flex-direction': 'column',
}
}).component();
const container = _view.modelBuilder.flexContainer()
.withProps(
{ CSSStyles: { 'margin': '8px 16px', 'flex-direction': 'column' } })
.component();
const description = _view.modelBuilder.text().withProps({
value: constants.EDIT_PARAMETERS_TEXT,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
const description = _view.modelBuilder.text()
.withProps({
value: constants.EDIT_PARAMETERS_TEXT,
CSSStyles: { ...styles.BODY_CSS }
})
.component();
const WIZARD_INPUT_COMPONENT_WIDTH = '300px';
const scaleFactorLabel = _view.modelBuilder.text().withProps({
value: constants.SCALE_FACTOR,
description: constants.SCALE_FACTOR_TOOLTIP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS
}
}).component();
this._scaleFactorInput = _view.modelBuilder.inputBox().withProps({
required: true,
validationErrorMessage: constants.INVALID_SCALE_FACTOR,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: {
'margin-top': '-1em',
'margin-bottom': '8px',
},
}).withValidation(c => {
if (Number(c.value) && Number(c.value) > 0) {
return true;
}
return false;
}).component();
const scaleFactorLabel = _view.modelBuilder.text()
.withProps({
value: constants.SCALE_FACTOR,
description: constants.SCALE_FACTOR_TOOLTIP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
this._scaleFactorInput = _view.modelBuilder.inputBox()
.withProps({
required: true,
validationErrorMessage: constants.INVALID_SCALE_FACTOR,
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { 'margin-top': '-1em', 'margin-bottom': '8px' },
}).withValidation(c => {
if (Number(c.value) && Number(c.value) > 0) {
return true;
}
return false;
}).component();
const targetPercentileLabel = _view.modelBuilder.text().withProps({
value: constants.PERCENTAGE_UTILIZATION,
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS,
}
}).component();
const targetPercentileLabel = _view.modelBuilder.text()
.withProps({
value: constants.PERCENTAGE_UTILIZATION,
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
const createPercentageValues = () => {
let values: azdata.CategoryValue[] = [];
const values: azdata.CategoryValue[] = [];
TARGET_PERCENTILE_VALUES.forEach(n => {
const val = n.toString();
values.push({
@@ -109,27 +106,27 @@ export class SkuEditParametersDialog {
});
return values;
};
this._targetPercentileDropdown = _view.modelBuilder.dropDown().withProps({
values: createPercentageValues(),
ariaLabel: constants.PERCENTAGE_UTILIZATION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: false,
required: true,
fireOnTextChange: true,
CSSStyles: {
'margin-top': '-1em',
'margin-bottom': '8px',
},
}).component();
this._targetPercentileDropdown = _view.modelBuilder.dropDown()
.withProps({
values: createPercentageValues(),
ariaLabel: constants.PERCENTAGE_UTILIZATION,
width: WIZARD_INPUT_COMPONENT_WIDTH,
editable: false,
required: true,
fireOnTextChange: true,
CSSStyles: {
'margin-top': '-1em',
'margin-bottom': '8px',
},
}).component();
const enablePreviewLabel = _view.modelBuilder.text().withProps({
value: constants.ENABLE_PREVIEW_SKU,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS,
}
}).component();
const enablePreviewLabel = _view.modelBuilder.text()
.withProps({
value: constants.ENABLE_PREVIEW_SKU,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: { ...styles.LABEL_CSS, }
}).component();
const buttonGroup = 'enablePreviewSKUs';
const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer()
.withProps({
@@ -151,11 +148,12 @@ export class SkuEditParametersDialog {
'margin': '0'
},
}).component();
this._disposables.push(enablePreviewButton.onDidChangeCheckedState(async (e) => {
if (e) {
this._enablePreviewValue = true;
}
}));
this._disposables.push(
enablePreviewButton.onDidChangeCheckedState(async checked => {
if (checked) {
this._enablePreviewValue = true;
}
}));
const disablePreviewButton = _view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
@@ -167,23 +165,21 @@ export class SkuEditParametersDialog {
'margin': '0 12px',
}
}).component();
this._disposables.push(disablePreviewButton.onDidChangeCheckedState(async (e) => {
if (e) {
this._enablePreviewValue = false;
}
}));
this._disposables.push(
disablePreviewButton.onDidChangeCheckedState(checked => {
if (checked) {
this._enablePreviewValue = false;
}
}));
enablePreviewRadioButtonContainer.addItems([
enablePreviewButton,
disablePreviewButton
]);
disablePreviewButton]);
const enablePreviewInfoBox = _view.modelBuilder.infoBox()
.withProps({
text: constants.ENABLE_PREVIEW_SKU_INFO,
style: 'information',
CSSStyles: {
...styles.BODY_CSS,
}
CSSStyles: { ...styles.BODY_CSS, }
}).component();
container.addItems([
@@ -202,12 +198,19 @@ export class SkuEditParametersDialog {
public async openDialog(dialogName?: string) {
if (!this._isOpen) {
this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(constants.EDIT_RECOMMENDATION_PARAMETERS, 'SkuEditParametersDialog', 'narrow');
this.dialog = azdata.window.createModelViewDialog(
constants.EDIT_RECOMMENDATION_PARAMETERS,
'SkuEditParametersDialog',
'narrow');
this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText;
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
this._disposables.push(
this.dialog.okButton.onClick(
async () => await this.execute()));
this._disposables.push(this.dialog.cancelButton.onClick(() => this._isOpen = false));
this._disposables.push(
this.dialog.cancelButton.onClick(
() => this._isOpen = false));
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));

View File

@@ -34,15 +34,13 @@ export class SkuRecommendationResultsDialog {
constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) {
switch (this._targetType) {
case MigrationTargetType.SQLMI:
this.targetName = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
this.targetName = constants.SKU_RECOMMENDATION_MI_CARD_TEXT;
break;
case MigrationTargetType.SQLVM:
this.targetName = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
this.targetName = constants.SKU_RECOMMENDATION_VM_CARD_TEXT;
break;
case MigrationTargetType.SQLDB:
this.targetName = constants.AZURE_SQL_DATABASE;
this.targetName = constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT;
break;
}
@@ -79,7 +77,9 @@ export class SkuRecommendationResultsDialog {
this.targetRecommendations?.forEach((recommendation, index) => {
if (index > 0) {
const separator = _view.modelBuilder.separator().withProps({ width: 750 }).component();
const separator = _view.modelBuilder.separator()
.withProps({ width: 750 })
.component();
container.addItem(separator);
}
@@ -101,7 +101,9 @@ export class SkuRecommendationResultsDialog {
recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem;
if (recommendation.targetSku) {
configuration = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.azureSkuName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
configuration = constants.VM_CONFIGURATION(
recommendation.targetSku.virtualMachineSize!.azureSkuName,
recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
storageSection = this.createSqlVmTargetStorageSection(_view, recommendation);
}
@@ -123,84 +125,73 @@ export class SkuRecommendationResultsDialog {
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
configuration = this._targetType === MigrationTargetType.SQLDB
? constants.DB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
? constants.SQLDB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
: constants.MI_CONFIGURATION(hardwareType, serviceTier, recommendation.targetSku.computeSize!);
const storageLabel = _view.modelBuilder.text().withProps({
value: constants.STORAGE_HEADER,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component();
const storageValue = _view.modelBuilder.text().withProps({
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
const storageLabel = _view.modelBuilder.text()
.withProps({
value: constants.STORAGE_HEADER,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component();
const storageValue = _view.modelBuilder.text()
.withProps({
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
CSSStyles: { ...styles.BODY_CSS, }
}).component();
storageSection.addItems([
storageLabel,
storageValue,
]);
storageValue]);
}
break;
}
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'margin-bottom': '20px',
'flex-direction': 'column',
}
}).component();
if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text().withProps({
value: recommendation.databaseName!,
const recommendationContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-bottom': '20px',
'flex-direction': 'column',
}
}).component();
if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text()
.withProps({
value: recommendation.databaseName!,
CSSStyles: { ...styles.SECTION_HEADER_CSS, }
}).component();
recommendationContainer.addItem(databaseNameLabel);
}
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({
value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '0',
}
}).component();
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({
value: this.targetName,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
}).component();
const targetDeploymentTypeLabel = _view.modelBuilder.text()
.withProps({
value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
}).component();
const targetDeploymentTypeValue = _view.modelBuilder.text()
.withProps({
value: this.targetName,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
}).component();
const azureConfigurationLabel = _view.modelBuilder.text().withProps({
value: constants.AZURE_CONFIGURATION,
CSSStyles: {
...styles.LABEL_CSS,
'margin': '12px 0 0',
}
}).component();
const azureConfigurationValue = _view.modelBuilder.text().withProps({
value: configuration,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0',
}
}).component();
const azureConfigurationLabel = _view.modelBuilder.text()
.withProps({
value: constants.AZURE_CONFIGURATION,
CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', }
}).component();
const azureConfigurationValue = _view.modelBuilder.text()
.withProps({
value: configuration,
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
}).component();
recommendationContainer.addItems([
targetDeploymentTypeLabel,
targetDeploymentTypeValue,
targetDeploymentTypeLabel,
targetDeploymentTypeValue,
azureConfigurationLabel,
azureConfigurationValue,
@@ -209,23 +200,21 @@ export class SkuRecommendationResultsDialog {
const recommendationsReasonSection = _view.modelBuilder.text().withProps({
value: constants.RECOMMENDATION_REASON,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin': '12px 0 0'
}
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin': '12px 0 0' }
}).component();
const reasonsContainer = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications) || [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
const reasonsContainer = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications)
|| [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
justifications?.forEach(text => {
reasonsContainer.addItem(
_view.modelBuilder.text().withProps({
value: text,
CSSStyles: {
...styles.BODY_CSS,
}
CSSStyles: { ...styles.BODY_CSS, }
}).component()
);
});
@@ -235,26 +224,23 @@ export class SkuRecommendationResultsDialog {
recommendationContainer.addItems([
recommendationsReasonSection,
reasonsContainer,
storagePropertiesContainer,
]);
storagePropertiesContainer]);
return recommendationContainer;
}
private createSqlVmTargetStorageSection(_view: azdata.ModelView, recommendation: mssql.IaaSSkuRecommendationResultItem): azdata.FlexContainer {
const recommendedTargetStorageSection = _view.modelBuilder.text().withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
}).component();
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
const recommendedTargetStorageSection = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
}).component();
const recommendedTargetStorageInfo = _view.modelBuilder.text()
.withProps({
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
CSSStyles: { ...styles.BODY_CSS, }
}).component();
const headerCssStyle = {
'border': 'none',
@@ -333,20 +319,21 @@ export class SkuRecommendationResultsDialog {
logDiskTableRow,
];
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storageConfigurationTableRows,
width: 700
}).component();
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storageConfigurationTableRows,
width: 700
}).component();
const container = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
recommendedTargetStorageSection,
recommendedTargetStorageInfo,
storageConfigurationTable,
]).component();
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
recommendedTargetStorageSection,
recommendedTargetStorageInfo,
storageConfigurationTable])
.component();
return container;
}
@@ -375,19 +362,16 @@ export class SkuRecommendationResultsDialog {
break;
case MigrationTargetType.SQLDB:
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements.filter(d => {
return databaseName === d.databaseName;
})[0]!;
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements
.filter((d) => databaseName === d.databaseName)[0]!;
break;
}
const storagePropertiesSection = _view.modelBuilder.text().withProps({
value: constants.SOURCE_PROPERTIES,
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'margin-top': '12px'
}
}).component();
const storagePropertiesSection = _view.modelBuilder.text()
.withProps({
value: constants.SOURCE_PROPERTIES,
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
}).component();
const headerCssStyle = {
'border': 'none',
@@ -407,7 +391,7 @@ export class SkuRecommendationResultsDialog {
};
const columnWidth = 80;
let columns: azdata.DeclarativeTableColumn[] = [
const columns: azdata.DeclarativeTableColumn[] = [
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.DIMENSION,
@@ -450,19 +434,18 @@ export class SkuRecommendationResultsDialog {
ioLatencyRow,
];
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storagePropertiesTableRows,
width: 300
}).component();
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
columns: columns,
dataValues: storagePropertiesTableRows,
width: 300
}).component();
const container = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
storagePropertiesSection,
storagePropertiesTable,
]).component();
const container = _view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([storagePropertiesSection, storagePropertiesTable])
.component();
return container;
}
@@ -537,10 +520,9 @@ export class SkuRecommendationResultsDialog {
}));
this.dialog.customButtons = [this._saveButton];
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));
const promise = this.initializeDialog(this.dialog);
azdata.window.openDialog(this.dialog);
await Promise.all(dialogSetupPromises);
await promise;
}
}

View File

@@ -0,0 +1,324 @@
/*---------------------------------------------------------------------------------------------
* 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 constants from '../../constants/strings';
import { AzureSqlDatabaseServer } from '../../api/azure';
import { collectSourceDatabaseTableInfo, collectTargetDatabaseTableInfo, TableInfo } from '../../api/sqlUtils';
import { MigrationStateModel } from '../../models/stateMachine';
const DialogName = 'TableMigrationSelection';
export class TableMigrationSelectionDialog {
private _dialog: azdata.window.Dialog | undefined;
private _headingText!: azdata.TextComponent;
private _filterInputBox!: azdata.InputBoxComponent;
private _tableSelectionTable!: azdata.TableComponent;
private _tableLoader!: azdata.LoadingComponent;
private _disposables: vscode.Disposable[] = [];
private _isOpen: boolean = false;
private _model: MigrationStateModel;
private _sourceDatabaseName: string;
private _tableSelectionMap!: Map<string, TableInfo>;
private _targetTableMap!: Map<string, TableInfo>;
private _onSaveCallback: () => Promise<void>;
constructor(
model: MigrationStateModel,
sourceDatabaseName: string,
onSaveCallback: () => Promise<void>
) {
this._model = model;
this._sourceDatabaseName = sourceDatabaseName;
this._onSaveCallback = onSaveCallback;
}
private async _loadData(): Promise<void> {
try {
this._tableLoader.loading = true;
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
if (targetDatabaseInfo) {
const sourceTableList: TableInfo[] = await collectSourceDatabaseTableInfo(
this._model.sourceConnectionId,
this._sourceDatabaseName);
this._tableSelectionMap = new Map();
sourceTableList.forEach(table => {
const sourceTable = targetDatabaseInfo.sourceTables.get(table.tableName);
const isSelected = sourceTable?.selectedForMigration === true;
const tableInfo: TableInfo = {
databaseName: table.databaseName,
rowCount: table.rowCount,
selectedForMigration: isSelected,
tableName: table.tableName,
};
this._tableSelectionMap.set(table.tableName, tableInfo);
});
const targetTableList: TableInfo[] = await collectTargetDatabaseTableInfo(
this._model._targetServerInstance as AzureSqlDatabaseServer,
targetDatabaseInfo.databaseName,
this._model._azureTenant.id,
this._model._targetUserName,
this._model._targetPassword);
this._targetTableMap = new Map();
targetTableList.forEach(table =>
this._targetTableMap.set(
table.tableName, {
databaseName: table.databaseName,
rowCount: table.rowCount,
selectedForMigration: false,
tableName: table.tableName,
}));
}
} catch (error) {
this._dialog!.message = {
text: constants.DATABASE_TABLE_CONNECTION_ERROR,
description: constants.DATABASE_TABLE_CONNECTION_ERROR_MESSAGE(error.message),
level: azdata.window.MessageLevel.Error
};
} finally {
this._tableLoader.loading = false;
await this._loadControls();
}
}
private async _loadControls(): Promise<void> {
const data: any[][] = [];
const filterText = this._filterInputBox.value ?? '';
const selectedItems: number[] = [];
let tableRow = 0;
this._tableSelectionMap.forEach(sourceTable => {
if (filterText?.length === 0 || sourceTable.tableName.indexOf(filterText) > -1) {
let tableStatus = constants.TARGET_TABLE_MISSING;
const targetTable = this._targetTableMap.get(sourceTable.tableName);
if (targetTable) {
const targetTableRowCount = targetTable?.rowCount ?? 0;
tableStatus = targetTableRowCount > 0
? constants.TARGET_TABLE_NOT_EMPTY
: '--';
}
data.push([
sourceTable.selectedForMigration,
sourceTable.tableName,
tableStatus]);
if (sourceTable.selectedForMigration && targetTable) {
selectedItems.push(tableRow);
}
tableRow++;
}
});
await this._tableSelectionTable.updateProperty('data', data);
this._tableSelectionTable.selectedRows = selectedItems;
this._updateRowSelection();
}
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
dialog.registerContent(async (view) => {
this._filterInputBox = view.modelBuilder.inputBox()
.withProps({
inputType: 'search',
placeHolder: constants.TABLE_SELECTION_FILTER,
width: 268,
}).component();
this._disposables.push(
this._filterInputBox.onTextChanged(
async e => await this._loadControls()));
this._headingText = view.modelBuilder.text()
.withProps({ value: constants.DATABASE_LOADING_TABLES })
.component();
this._tableSelectionTable = await this._createSelectionTable(view);
this._tableLoader = view.modelBuilder.loadingComponent()
.withItem(this._tableSelectionTable)
.withProps({
loading: false,
loadingText: constants.DATABASE_TABLE_DATA_LOADING
}).component();
const flex = view.modelBuilder.flexContainer()
.withItems([
this._filterInputBox,
this._headingText,
this._tableLoader],
{ flex: '0 0 auto' })
.withProps({ CSSStyles: { 'margin': '0 0 0 15px' } })
.withLayout({
flexFlow: 'column',
height: '100%',
width: 565,
}).component();
this._disposables.push(
view.onClosed(e =>
this._disposables.forEach(
d => { try { d.dispose(); } catch { } })));
await view.initializeModel(flex);
await this._loadData();
});
}
public async openDialog(dialogTitle: string) {
if (!this._isOpen) {
this._isOpen = true;
this._dialog = azdata.window.createModelViewDialog(
dialogTitle,
DialogName,
600);
this._dialog.okButton.label = constants.TABLE_SELECTION_UPDATE_BUTTON;
this._disposables.push(
this._dialog.okButton.onClick(
async () => this._save()));
this._dialog.cancelButton.label = constants.TABLE_SELECTION_CANCEL_BUTTON;
this._disposables.push(
this._dialog.cancelButton.onClick(
async () => this._isOpen = false));
const promise = this._initializeDialog(this._dialog);
azdata.window.openDialog(this._dialog);
await promise;
}
}
private async _createSelectionTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
const cssClass = 'no-borders';
const table = view.modelBuilder.table()
.withProps({
data: [],
width: 565,
height: '600px',
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [
<azdata.CheckboxColumn>{
value: '',
width: 10,
type: azdata.ColumnType.checkBox,
action: azdata.ActionOnCellCheckboxCheck.selectRow,
resizable: false,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.TABLE_SELECTION_TABLENAME_COLUMN,
value: 'tableName',
type: azdata.ColumnType.text,
width: 300,
cssClass: cssClass,
headerCssClass: cssClass,
},
{
name: constants.TABLE_SELECTION_HASROWS_COLUMN,
value: 'hasRows',
type: azdata.ColumnType.text,
width: 255,
cssClass: cssClass,
headerCssClass: cssClass,
}]
})
.withValidation(() => true)
.component();
let updating: boolean = false;
this._disposables.push(
table.onRowSelected(e => {
if (updating) {
return;
}
updating = true;
// collect table list selected for migration
const selectedRows = this._tableSelectionTable.selectedRows ?? [];
const keepSelectedRows: number[] = [];
// determine if selected rows have a matching target and can be selected
selectedRows.forEach(rowIndex => {
// get selected source table name
const sourceTableName = this._tableSelectionTable.data[rowIndex][1] as string;
// get source table info
const sourceTableInfo = this._tableSelectionMap.get(sourceTableName);
if (sourceTableInfo) {
// see if source table exists on target database
const targetTableInfo = this._targetTableMap.get(sourceTableName);
// keep source table selected
sourceTableInfo.selectedForMigration = targetTableInfo !== undefined;
// update table selection map with new selectedForMigration value
this._tableSelectionMap.set(sourceTableName, sourceTableInfo);
// keep row selected
if (sourceTableInfo.selectedForMigration) {
keepSelectedRows.push(rowIndex);
}
}
});
// if the selected rows are different, update the selectedRows property
if (!this._areEqual(this._tableSelectionTable.selectedRows ?? [], keepSelectedRows)) {
this._tableSelectionTable.selectedRows = keepSelectedRows;
}
this._updateRowSelection();
updating = false;
}));
return table;
}
private _areEqual(source: number[], target: number[]): boolean {
if (source.length === target.length) {
for (let i = 0; i < source.length; i++) {
if (source[i] !== target[i]) {
return false;
}
}
return true;
}
return false;
}
private _updateRowSelection(): void {
this._headingText.value = this._tableSelectionTable.data.length > 0
? constants.TABLE_SELECTED_COUNT(
this._tableSelectionTable.selectedRows?.length ?? 0,
this._tableSelectionTable.data.length)
: this._tableLoader.loading
? constants.DATABASE_LOADING_TABLES
: constants.DATABASE_MISSING_TABLES;
}
private async _save(): Promise<void> {
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
if (targetDatabaseInfo) {
// collect table list selected for migration
const selectedRows = this._tableSelectionTable.selectedRows ?? [];
const selectedTables = new Map<String, TableInfo>();
selectedRows.forEach(rowIndex => {
const tableName = this._tableSelectionTable.data[rowIndex][1] as string;
const tableInfo = this._tableSelectionMap.get(tableName);
if (tableInfo) {
selectedTables.set(tableName, tableInfo);
}
});
// copy table map selection status from grid
this._tableSelectionMap.forEach(tableInfo => {
const selectedTableInfo = selectedTables.get(tableInfo.tableName);
tableInfo.selectedForMigration = selectedTableInfo?.selectedForMigration === true;
this._tableSelectionMap.set(tableInfo.tableName, tableInfo);
});
// save table selection changes to migration source target map
targetDatabaseInfo.sourceTables = this._tableSelectionMap;
this._model._sourceTargetMapping.set(this._sourceDatabaseName, targetDatabaseInfo);
}
await this._onSaveCallback();
this._isOpen = false;
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { MigrationMode, MigrationStateModel, NetworkContainerType } from '../../models/stateMachine';
import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles';
@@ -25,22 +25,20 @@ export class TargetDatabaseSummaryDialog {
this._dialogObject = azdata.window.createModelViewDialog(
constants.DATABASE_TO_BE_MIGRATED,
'TargetDatabaseSummaryDialog',
dialogWidth
);
dialogWidth);
}
async initialize(): Promise<void> {
let tab = azdata.window.createTab('sql.migration.CreateResourceGroupDialog');
const tab = azdata.window.createTab('sql.migration.CreateResourceGroupDialog');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
const databaseCount = this._view.modelBuilder.text().withProps({
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),
CSSStyles: {
...styles.BODY_CSS,
'margin-bottom': '20px'
}
}).component();
const isSqlDbMigration = this._model._targetType === MigrationTargetType.SQLDB;
const databaseCount = this._view.modelBuilder.text()
.withProps({
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '20px' }
}).component();
const headerCssStyle = {
'border': 'none',
@@ -61,7 +59,7 @@ export class TargetDatabaseSummaryDialog {
const columnWidth = 150;
let columns: azdata.DeclarativeTableColumn[] = [
const columns: azdata.DeclarativeTableColumn[] = [
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.SOURCE_DATABASE,
@@ -70,7 +68,6 @@ export class TargetDatabaseSummaryDialog {
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.TARGET_DATABASE_NAME,
@@ -78,46 +75,59 @@ export class TargetDatabaseSummaryDialog {
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}
];
}];
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
columns.push(
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.LOCATION,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.RESOURCE_GROUP,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.SUMMARY_AZURE_STORAGE,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
},
{
valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle,
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
});
} else if (isSqlDbMigration) {
columns.push({
valueType: azdata.DeclarativeDataType.string,
displayName: constants.LOCATION,
displayName: constants.TARGET_TABLE_COUNT_NAME,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
valueType: azdata.DeclarativeDataType.string,
displayName: constants.RESOURCE_GROUP,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
valueType: azdata.DeclarativeDataType.string,
displayName: constants.SUMMARY_AZURE_STORAGE,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle
}, {
valueType: azdata.DeclarativeDataType.string,
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
isReadOnly: true,
width: columnWidth,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyle,
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
});
} else {
columns.push({
@@ -134,59 +144,54 @@ export class TargetDatabaseSummaryDialog {
this._model._databasesForMigration.forEach((db, index) => {
const tableRow: azdata.DeclarativeTableCellValue[] = [];
tableRow.push({
value: db
}, {
value: this._model._targetDatabaseNames[index]
});
tableRow.push(
{ value: db },
{ value: this._model._targetDatabaseNames[index] });
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
tableRow.push({
value: this._model._databaseBackup.blobs[index].storageAccount.location
}, {
value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup!
}, {
value: this._model._databaseBackup.blobs[index].storageAccount.name
}, {
value: this._model._databaseBackup.blobs[index].blobContainer.name
});
tableRow.push(
{ value: this._model._databaseBackup.blobs[index].storageAccount.location },
{ value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup! },
{ value: this._model._databaseBackup.blobs[index].storageAccount.name },
{ value: this._model._databaseBackup.blobs[index].blobContainer.name });
if (this._model._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
tableRow.push({
value: this._model._databaseBackup.blobs[index].lastBackupFile!
});
tableRow.push(
{ value: this._model._databaseBackup.blobs[index].lastBackupFile! });
}
} else if (isSqlDbMigration) {
const totalTables = this._model._sourceTargetMapping.get(db)?.sourceTables.size ?? 0;
let selectedTables = 0;
this._model._sourceTargetMapping.get(db)?.sourceTables.forEach(
tableInfo => selectedTables += tableInfo.selectedForMigration ? 1 : 0);
tableRow.push(
{ value: constants.TOTAL_TABLES_SELECTED(selectedTables, totalTables) });
} else {
tableRow.push({
value: this._model._databaseBackup.networkShares[index].networkShareLocation
});
tableRow.push(
{ value: this._model._databaseBackup.networkShares[index].networkShareLocation });
}
tableRows.push(tableRow);
});
const databaseTable: azdata.DeclarativeTableComponent = this._view.modelBuilder.declarativeTable().withProps({
ariaLabel: constants.DATABASE_TO_BE_MIGRATED,
columns: columns,
dataValues: tableRows,
width: this._tableLength
}).component();
const databaseTable: azdata.DeclarativeTableComponent = this._view.modelBuilder.declarativeTable()
.withProps({
ariaLabel: constants.DATABASE_TO_BE_MIGRATED,
columns: columns,
dataValues: tableRows,
width: this._tableLength
}).component();
const container = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([databaseCount, databaseTable])
.component();
const form = this._view.modelBuilder.formContainer()
.withFormItems(
[{ component: container }],
{ horizontal: false })
.withLayout({ width: '100%' })
.component();
const container = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
}).withItems([
databaseCount,
databaseTable
]).component();
const formBuilder = this._view.modelBuilder.formContainer().withFormItems(
[
{
component: container
}
],
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
this._dialogObject.content = [tab];