mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 01:25:39 -05:00
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:
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user