mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-14 03:58:33 -05:00
Add new tabbed dashboard, monitoring with breadcrumb navigation (#19995)
* SQL DB monitoring and Dashboard refactor * Merge remote-tracking branch 'origin/main' into dev/brih/feature/sql-migration-dashboard-tabs * update filter text and optimize page load * update migration column order, names and statusbox * add column table sorting * add new migration and pipeline status values, etc * address review feedback
This commit is contained in:
789
extensions/sql-migration/src/dashboard/dashboardTab.ts
Normal file
789
extensions/sql-migration/src/dashboard/dashboardTab.ts
Normal file
@@ -0,0 +1,789 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as styles from '../constants/styles';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations } from '../api/utils';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
interface IActionMetadata {
|
||||
title?: string,
|
||||
description?: string,
|
||||
link?: string,
|
||||
iconPath?: azdata.ThemedIconPath,
|
||||
command?: string;
|
||||
}
|
||||
|
||||
interface StatusCard {
|
||||
container: azdata.DivContainer;
|
||||
count: azdata.TextComponent,
|
||||
textContainer?: azdata.FlexContainer,
|
||||
warningContainer?: azdata.FlexContainer,
|
||||
warningText?: azdata.TextComponent,
|
||||
}
|
||||
|
||||
export const DashboardTabId = 'DashboardTab';
|
||||
|
||||
const maxWidth = 800;
|
||||
const BUTTON_CSS = {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0',
|
||||
'text-align': 'left',
|
||||
};
|
||||
|
||||
export class DashboardTab extends TabBase<DashboardTab> {
|
||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
||||
private _inProgressMigrationButton!: StatusCard;
|
||||
private _inProgressWarningMigrationButton!: StatusCard;
|
||||
private _allMigrationButton!: StatusCard;
|
||||
private _successfulMigrationButton!: StatusCard;
|
||||
private _failedMigrationButton!: StatusCard;
|
||||
private _completingMigrationButton!: StatusCard;
|
||||
private _selectServiceText!: azdata.TextComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = loc.DESKTOP_DASHBOARD_TAB_TITLE;
|
||||
this.id = DashboardTabId;
|
||||
this.icon = IconPathHelper.sqlMigrationLogo;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async create(
|
||||
view: azdata.ModelView,
|
||||
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<DashboardTab> {
|
||||
|
||||
this.view = view;
|
||||
this.openMigrationFcn = openMigrationsFcn;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
let migrations: DatabaseMigration[] = [];
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
migrations = await getCurrentMigrations();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
|
||||
e.message);
|
||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
||||
}
|
||||
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
for (let i = 0; i < inProgressMigrations.length; i++) {
|
||||
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount += 1;
|
||||
}
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
|
||||
this._inProgressMigrationButton.container.display = 'none';
|
||||
this._inProgressWarningMigrationButton.container.display = '';
|
||||
} else {
|
||||
this._inProgressMigrationButton.container.display = '';
|
||||
this._inProgressWarningMigrationButton.container.display = 'none';
|
||||
}
|
||||
|
||||
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
|
||||
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
|
||||
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
|
||||
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
|
||||
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
this.isRefreshing = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
const container = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
const toolbar = view.modelBuilder.toolbarContainer();
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton() },
|
||||
]);
|
||||
|
||||
container.addItem(
|
||||
toolbar.component(),
|
||||
{ CSSStyles: { 'flex': '0 0 auto' } });
|
||||
|
||||
const header = this._createHeader(view);
|
||||
// Files need to have the vscode-file scheme to be loaded by ADS
|
||||
const watermarkUri = vscode.Uri
|
||||
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
|
||||
.with({ scheme: 'vscode-file' });
|
||||
|
||||
container.addItem(header, {
|
||||
CSSStyles: {
|
||||
'background-image': `
|
||||
url(${watermarkUri}),
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': '91.06% 100%',
|
||||
'margin-bottom': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
const tasksContainer = await this._createTasks(view);
|
||||
header.addItem(tasksContainer, {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'margin': '24px'
|
||||
}
|
||||
});
|
||||
container.addItem(
|
||||
await this._createFooter(view),
|
||||
{ CSSStyles: { 'margin': '0 24px' } });
|
||||
|
||||
this.content = container;
|
||||
}
|
||||
|
||||
private _createHeader(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DASHBOARD_TITLE,
|
||||
width: '750px',
|
||||
CSSStyles: { ...styles.DASHBOARD_TITLE_CSS }
|
||||
}).component();
|
||||
|
||||
const descriptionComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DASHBOARD_DESCRIPTION,
|
||||
CSSStyles: { ...styles.NOTE_CSS }
|
||||
}).component();
|
||||
header.addItems([titleComponent, descriptionComponent], {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'padding-left': '24px'
|
||||
}
|
||||
});
|
||||
return header;
|
||||
}
|
||||
|
||||
private async _createTasks(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const tasksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
width: '100%',
|
||||
}).component();
|
||||
|
||||
const migrateButtonMetadata: IActionMetadata = {
|
||||
title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
|
||||
description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
|
||||
iconPath: IconPathHelper.sqlMigrationLogo,
|
||||
command: MenuCommands.StartMigration
|
||||
};
|
||||
|
||||
const preRequisiteListTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.PRE_REQ_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const migrateButton = this._createTaskButton(view, migrateButtonMetadata);
|
||||
|
||||
const preRequisiteListElement = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: [
|
||||
loc.PRE_REQ_1,
|
||||
loc.PRE_REQ_2,
|
||||
loc.PRE_REQ_3
|
||||
],
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS,
|
||||
'padding-left': '12px',
|
||||
'margin': '-0.5em 0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: loc.LEARN_MORE,
|
||||
ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
|
||||
url: 'https://aka.ms/azuresqlmigrationextension',
|
||||
}).component();
|
||||
|
||||
const preReqContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
preRequisiteListTitle,
|
||||
preRequisiteListElement,
|
||||
preRequisiteLearnMoreLink])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
tasksContainer.addItem(migrateButton, {});
|
||||
tasksContainer.addItems(
|
||||
[preReqContainer],
|
||||
{ CSSStyles: { 'margin-left': '20px' } });
|
||||
|
||||
return tasksContainer;
|
||||
}
|
||||
|
||||
private _createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
|
||||
const maxHeight: number = 84;
|
||||
const maxWidth: number = 236;
|
||||
const buttonContainer = view.modelBuilder.button().withProps({
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
description: taskMetaData.description,
|
||||
height: maxHeight,
|
||||
iconHeight: 32,
|
||||
iconPath: taskMetaData.iconPath,
|
||||
iconWidth: 32,
|
||||
label: taskMetaData.title,
|
||||
title: taskMetaData.title,
|
||||
width: maxWidth,
|
||||
CSSStyles: {
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
buttonContainer.onDidClick(async () => {
|
||||
if (taskMetaData.command) {
|
||||
await vscode.commands.executeCommand(taskMetaData.command);
|
||||
}
|
||||
}));
|
||||
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
||||
}
|
||||
|
||||
private async _createFooter(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const footerContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const statusContainer = await this._createMigrationStatusContainer(view);
|
||||
const videoLinksContainer = this._createVideoLinks(view);
|
||||
footerContainer.addItem(statusContainer);
|
||||
footerContainer.addItem(
|
||||
videoLinksContainer,
|
||||
{ CSSStyles: { 'padding-left': '8px', } });
|
||||
|
||||
return footerContainer;
|
||||
}
|
||||
|
||||
private _createVideoLinks(view: azdata.ModelView): azdata.Component {
|
||||
const linksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '440px',
|
||||
height: '365px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
'overflow': 'scroll',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const titleComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.HELP_TITLE,
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
})
|
||||
.component();
|
||||
|
||||
linksContainer.addItems(
|
||||
[titleComponent],
|
||||
{ CSSStyles: { 'margin-bottom': '16px' } });
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_MIGRATE_USING_ADS,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS,
|
||||
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
|
||||
},
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_MI_TUTORIAL,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL,
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
|
||||
},
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_VM_TUTORIAL,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL,
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
|
||||
},
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_DMS_GUIDE,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE,
|
||||
link: 'https://docs.microsoft.com/data-migration/'
|
||||
},
|
||||
];
|
||||
|
||||
linksContainer.addItems(links.map(l => this._createLink(view, l)), {});
|
||||
|
||||
const videoLinks: IActionMetadata[] = [];
|
||||
const videosContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
videosContainer.addItems(videoLinks.map(l => this._createVideoLink(view, l)), {});
|
||||
linksContainer.addItem(videosContainer);
|
||||
|
||||
return linksContainer;
|
||||
}
|
||||
|
||||
private _createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 400;
|
||||
const labelsContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '12px'
|
||||
}
|
||||
}).component();
|
||||
const linkContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '4px'
|
||||
}
|
||||
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
CSSStyles: { ...styles.NOTE_CSS }
|
||||
}).component();
|
||||
const linkComponent = view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: linkMetaData.title!,
|
||||
url: linkMetaData.link!,
|
||||
showLinkIcon: true,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
linkContainer.addItem(linkComponent);
|
||||
labelsContainer.addItems([linkContainer, descriptionComponent]);
|
||||
return labelsContainer;
|
||||
}
|
||||
|
||||
private _createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 150;
|
||||
const videosContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const video1Container = view.modelBuilder.divContainer()
|
||||
.withProps({
|
||||
clickable: true,
|
||||
width: maxWidth,
|
||||
height: '100px'
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
height: '50px',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
video1Container.onDidClick(async () => {
|
||||
if (linkMetaData.link) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
||||
}
|
||||
}));
|
||||
videosContainer.addItem(video1Container, {
|
||||
CSSStyles: {
|
||||
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': 'top',
|
||||
'width': `${maxWidth}px`,
|
||||
'height': '104px',
|
||||
'background-size': `${maxWidth}px 120px`
|
||||
}
|
||||
});
|
||||
videosContainer.addItem(descriptionComponent);
|
||||
return videosContainer;
|
||||
}
|
||||
|
||||
private _createStatusCard(
|
||||
view: azdata.ModelView,
|
||||
cardIconPath: IconPath,
|
||||
cardTitle: string,
|
||||
hasSubtext: boolean = false
|
||||
): StatusCard {
|
||||
const buttonWidth = '400px';
|
||||
const buttonHeight = hasSubtext ? '70px' : '50px';
|
||||
const statusCard = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'width': buttonWidth,
|
||||
'height': buttonHeight,
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusIcon = view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 32,
|
||||
CSSStyles: { 'margin': '0 8px' }
|
||||
}).component();
|
||||
|
||||
const textContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const cardTitleText = view.modelBuilder.text()
|
||||
.withProps({ value: cardTitle })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'width': '240px',
|
||||
}
|
||||
}).component();
|
||||
textContainer.addItem(cardTitleText);
|
||||
|
||||
const cardCount = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: '0',
|
||||
CSSStyles: {
|
||||
...styles.BIG_NUMBER_CSS,
|
||||
'margin': '0 0 0 8px',
|
||||
'text-align': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
let warningContainer;
|
||||
let warningText;
|
||||
if (hasSubtext) {
|
||||
const warningIcon = view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.warning,
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
width: 12,
|
||||
height: 18,
|
||||
}).component();
|
||||
|
||||
const warningDescription = '';
|
||||
warningText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: warningDescription,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'padding-left': '8px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
warningContainer = view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[warningIcon, warningText],
|
||||
{ flex: '0 0 auto' })
|
||||
.withProps({ CSSStyles: { 'align-items': 'center' } })
|
||||
.component();
|
||||
|
||||
textContainer.addItem(warningContainer);
|
||||
}
|
||||
|
||||
statusCard.addItems([
|
||||
statusIcon,
|
||||
textContainer,
|
||||
cardCount,
|
||||
]);
|
||||
|
||||
const compositeButton = view.modelBuilder.divContainer()
|
||||
.withItems([statusCard])
|
||||
.withProps({
|
||||
ariaRole: 'button',
|
||||
ariaLabel: loc.SHOW_STATUS,
|
||||
clickable: true,
|
||||
CSSStyles: {
|
||||
'height': buttonHeight,
|
||||
'margin-bottom': '16px',
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
return {
|
||||
container: compositeButton,
|
||||
count: cardCount,
|
||||
textContainer: textContainer,
|
||||
warningContainer: warningContainer,
|
||||
warningText: warningText
|
||||
};
|
||||
}
|
||||
|
||||
private async _createMigrationStatusContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
const statusContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '400px',
|
||||
height: '365px',
|
||||
justifyContent: 'flex-start',
|
||||
})
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
const statusContainerTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DATABASE_MIGRATION_STATUS,
|
||||
width: '100%',
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
}).component();
|
||||
|
||||
this._refreshButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.REFRESH,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
CSSStyles: { 'float': 'right' }
|
||||
}).component();
|
||||
|
||||
const statusHeadingContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
statusContainerTitle,
|
||||
this._refreshButton,
|
||||
]).withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexFlow: 'row',
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refresh();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
|
||||
const buttonContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-iems': 'center',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
|
||||
buttonContainer.addItem(
|
||||
await this._createServiceSelector(view));
|
||||
|
||||
this._selectServiceText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SELECT_SERVICE_MESSAGE,
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'margin': '10px',
|
||||
'font-weight': '350',
|
||||
'text-align': 'center',
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withItems([statusHeadingContainer, buttonContainer])
|
||||
.withLayout({ flexFlow: 'column', })
|
||||
.component();
|
||||
|
||||
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '272px',
|
||||
})
|
||||
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
|
||||
.component();
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
|
||||
// in progress
|
||||
this._inProgressMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS);
|
||||
this.disposables.push(
|
||||
this._inProgressMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// in progress warning
|
||||
this._inProgressWarningMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS,
|
||||
true);
|
||||
this.disposables.push(
|
||||
this._inProgressWarningMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressWarningMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// successful
|
||||
this._successfulMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.completedMigration,
|
||||
loc.MIGRATION_COMPLETED);
|
||||
this.disposables.push(
|
||||
this._successfulMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._successfulMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// completing
|
||||
this._completingMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.completingCutover,
|
||||
loc.MIGRATION_CUTOVER_CARD);
|
||||
this.disposables.push(
|
||||
this._completingMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._completingMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// failed
|
||||
this._failedMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.error,
|
||||
loc.MIGRATION_FAILED);
|
||||
this.disposables.push(
|
||||
this._failedMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._failedMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// all migrations
|
||||
this._allMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.view,
|
||||
loc.VIEW_ALL);
|
||||
this.disposables.push(
|
||||
this._allMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._allMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
|
||||
.withItem(this._migrationStatusCardsContainer)
|
||||
.component();
|
||||
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
|
||||
statusContainer.addItem(this._selectServiceText, {});
|
||||
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
|
||||
return statusContainer;
|
||||
}
|
||||
|
||||
private async _createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 375,
|
||||
CSSStyles: { ...BUTTON_CSS },
|
||||
})
|
||||
.component();
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed());
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
return this._serviceContextButton;
|
||||
}
|
||||
|
||||
private _updateStatusCard(
|
||||
migrations: DatabaseMigration[],
|
||||
card: StatusCard,
|
||||
status: AdsMigrationStatus,
|
||||
show?: boolean): void {
|
||||
const list = filterMigrations(migrations, status);
|
||||
const count = list?.length || 0;
|
||||
card.container.display = count > 0 || show ? '' : 'none';
|
||||
card.count.value = count.toString();
|
||||
}
|
||||
|
||||
private async _updateSummaryStatus(): Promise<void> {
|
||||
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
const isContextValid = isServiceContextValid(serviceContext);
|
||||
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
|
||||
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
|
||||
this._refreshButton.enabled = isContextValid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 loc from '../constants/strings';
|
||||
import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { getResourceName } from '../api/azure';
|
||||
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
|
||||
|
||||
export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<MigrationDetailsBlobContainerTab> {
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _backupLocationInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupInfoField!: InfoFieldSchema;
|
||||
private _currentRestoringFileInfoField!: InfoFieldSchema;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationDetailsBlobContainerTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationDetailsBlobContainerTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing || this.model?.migration === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
await this.model.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
}
|
||||
|
||||
const migration = this.model?.migration;
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
await this.showMigrationErrors(migration);
|
||||
|
||||
const sqlServerName = migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration.name;
|
||||
const targetServerName = getResourceName(migration.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
|
||||
const blobContainerName
|
||||
= migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName
|
||||
?? migration.properties.migrationStatusDetails?.blobContainerName;
|
||||
|
||||
const backupLocation = storageAccountResourceId && blobContainerName
|
||||
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
|
||||
: blobContainerName;
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
|
||||
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
|
||||
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
try {
|
||||
const formItems: azdata.FormComponent<azdata.Component>[] = [
|
||||
{ component: this.createMigrationToolbarContainer() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{
|
||||
component: this.view.modelBuilder.separator()
|
||||
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
|
||||
.component()
|
||||
},
|
||||
];
|
||||
|
||||
this.content = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false })
|
||||
.withLayout({ width: '100%', padding: '0 0 0 15px' })
|
||||
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
|
||||
.component();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(
|
||||
infoField.flexContainer,
|
||||
{ CSSStyles: { width: infoFieldWidth } });
|
||||
};
|
||||
|
||||
const flexServer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const flexStatus = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
|
||||
|
||||
const flexFile = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: { 'flex': '0', 'width': infoFieldWidth }
|
||||
};
|
||||
|
||||
const flexInfo = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
flexInfo.addItem(flexFile, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as loc from '../constants/strings';
|
||||
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import * as styles from '../constants/styles';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { getResourceName } from '../api/azure';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
|
||||
|
||||
interface ActiveBackupFileSchema {
|
||||
fileName: string,
|
||||
type: string,
|
||||
status: string,
|
||||
dataUploaded: string,
|
||||
copyThroughput: string,
|
||||
backupStartTime: string,
|
||||
firstLSN: string,
|
||||
lastLSN: string
|
||||
}
|
||||
|
||||
export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<MigrationDetailsFileShareTab> {
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _fullBackupFileOnInfoField!: InfoFieldSchema;
|
||||
private _backupLocationInfoField!: InfoFieldSchema;
|
||||
private _lastLSNInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
|
||||
private _currentRestoringFileInfoField!: InfoFieldSchema;
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
private _fileTable!: azdata.TableComponent;
|
||||
private _emptyTableFill!: azdata.FlexContainer;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationDetailsFileShareTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing || this.model?.migration === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
|
||||
try {
|
||||
await this.model.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
}
|
||||
|
||||
const migration = this.model?.migration;
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
await this.showMigrationErrors(migration);
|
||||
|
||||
const sqlServerName = migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration.name;
|
||||
const targetServerName = getResourceName(migration.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
|
||||
(activeBackupSet) => {
|
||||
tableData.push(
|
||||
...activeBackupSet.listOfBackupFiles.map(f => {
|
||||
return {
|
||||
fileName: f.fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: f.status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
|
||||
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
|
||||
|
||||
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
const backupLocation = fileShare?.path! ?? EmptySettingValue;
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
|
||||
|
||||
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
|
||||
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
|
||||
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
|
||||
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
|
||||
|
||||
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
if (tableData.length === 0) {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
|
||||
this._fileTable.height = '50px';
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
} else {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
|
||||
this._fileTable.height = '300px';
|
||||
|
||||
// Sorting files in descending order of backupStartTime
|
||||
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
|
||||
}
|
||||
|
||||
const data = tableData.map(row => [
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
row.dataUploaded,
|
||||
row.copyThroughput,
|
||||
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
]) || [];
|
||||
|
||||
await this._fileTable.updateProperty('data', data);
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
try {
|
||||
this._fileCount = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
width: '500px',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
this._fileTable = this.view.modelBuilder.table()
|
||||
.withProps({
|
||||
ariaLabel: loc.ACTIVE_BACKUP_FILES,
|
||||
CSSStyles: { 'padding-left': '0px', 'max-width': '1020px' },
|
||||
data: [],
|
||||
height: '300px',
|
||||
columns: [
|
||||
{
|
||||
value: 'files',
|
||||
name: loc.ACTIVE_BACKUP_FILES,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
value: 'type',
|
||||
name: loc.TYPE,
|
||||
width: 90,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'status',
|
||||
name: loc.STATUS,
|
||||
width: 60,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'uploaded',
|
||||
name: loc.DATA_UPLOADED,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'throughput',
|
||||
name: loc.COPY_THROUGHPUT,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'starttime',
|
||||
name: loc.BACKUP_START_TIME,
|
||||
width: 130,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'firstlsn',
|
||||
name: loc.FIRST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'lastlsn',
|
||||
name: loc.LAST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
}
|
||||
],
|
||||
}).component();
|
||||
|
||||
const emptyTableImage = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.emptyTable,
|
||||
iconHeight: '100px',
|
||||
iconWidth: '100px',
|
||||
height: '100px',
|
||||
width: '100px',
|
||||
CSSStyles: { 'text-align': 'center' }
|
||||
}).component();
|
||||
|
||||
const emptyTableText = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.EMPTY_TABLE_TEXT,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS,
|
||||
'margin-top': '8px',
|
||||
'text-align': 'center',
|
||||
'width': '300px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._emptyTableFill = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center'
|
||||
}).withItems([
|
||||
emptyTableImage,
|
||||
emptyTableText,
|
||||
]).withProps({
|
||||
width: '100%',
|
||||
display: 'none'
|
||||
}).component();
|
||||
|
||||
const formItems: azdata.FormComponent<azdata.Component>[] = [
|
||||
{ component: this.createMigrationToolbarContainer() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{
|
||||
component: this.view.modelBuilder.separator()
|
||||
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
|
||||
.component()
|
||||
},
|
||||
{ component: this._fileCount },
|
||||
{ component: this._fileTable },
|
||||
{ component: this._emptyTableFill }
|
||||
];
|
||||
|
||||
const formContainer = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false })
|
||||
.withLayout({ width: '100%', padding: '0 0 0 15px' })
|
||||
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
|
||||
.component();
|
||||
|
||||
this.content = formContainer;
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(
|
||||
infoField.flexContainer,
|
||||
{ CSSStyles: { width: infoFieldWidth } });
|
||||
};
|
||||
|
||||
const flexServer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const flexStatus = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', false);
|
||||
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
|
||||
|
||||
const flexFile = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', false);
|
||||
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', false);
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
|
||||
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': infoFieldWidth
|
||||
}
|
||||
};
|
||||
|
||||
const flexInfo = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
flexInfo.addItem(flexFile, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import * as styles from '../constants/styles';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { TabBase } from './tabBase';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { MigrationTargetType } from '../models/stateMachine';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
export const infoFieldLgWidth: string = '330px';
|
||||
export const infoFieldWidth: string = '250px';
|
||||
|
||||
const statusImageSize: number = 14;
|
||||
|
||||
export const MigrationTargetTypeName: loc.LookupTable<string> = {
|
||||
[MigrationTargetType.SQLMI]: loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
|
||||
[MigrationTargetType.SQLVM]: loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE,
|
||||
[MigrationTargetType.SQLDB]: loc.AZURE_SQL_DATABASE,
|
||||
};
|
||||
|
||||
export interface InfoFieldSchema {
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent,
|
||||
icon?: azdata.ImageComponent,
|
||||
}
|
||||
|
||||
export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
protected model!: MigrationCutoverDialogModel;
|
||||
protected databaseLabel!: azdata.TextComponent;
|
||||
protected serviceContext!: MigrationServiceContext;
|
||||
protected onClosedCallback!: () => Promise<void>;
|
||||
|
||||
protected cutoverButton!: azdata.ButtonComponent;
|
||||
protected refreshButton!: azdata.ButtonComponent;
|
||||
protected cancelButton!: azdata.ButtonComponent;
|
||||
protected refreshLoader!: azdata.LoadingComponent;
|
||||
protected copyDatabaseMigrationDetails!: azdata.ButtonComponent;
|
||||
protected newSupportRequest!: azdata.ButtonComponent;
|
||||
protected retryButton!: azdata.ButtonComponent;
|
||||
protected summaryTextComponent: azdata.TextComponent[] = [];
|
||||
|
||||
public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise<void>, statusBar: DashboardStatusBar): Promise<T>;
|
||||
|
||||
protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = '';
|
||||
}
|
||||
|
||||
public async setMigrationContext(
|
||||
serviceContext: MigrationServiceContext,
|
||||
migration: DatabaseMigration): Promise<void> {
|
||||
this.serviceContext = serviceContext;
|
||||
this.model = new MigrationCutoverDialogModel(serviceContext, migration);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
protected createBreadcrumbContainer(): azdata.FlexContainer {
|
||||
const migrationsTabLink = this.view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: loc.BREADCRUMB_MIGRATIONS,
|
||||
url: '',
|
||||
title: loc.BREADCRUMB_MIGRATIONS,
|
||||
CSSStyles: {
|
||||
'padding': '5px 5px 5px 0',
|
||||
'font-size': '13px'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
migrationsTabLink.onDidClick(
|
||||
async (e) => await this.onClosedCallback()));
|
||||
|
||||
const breadCrumbImage = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.breadCrumb,
|
||||
iconHeight: 8,
|
||||
iconWidth: 8,
|
||||
width: 8,
|
||||
height: 8,
|
||||
CSSStyles: { 'padding': '4px' }
|
||||
}).component();
|
||||
|
||||
this.databaseLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
textType: azdata.TextType.Normal,
|
||||
value: '...',
|
||||
CSSStyles: {
|
||||
'font-size': '16px',
|
||||
'font-weight': '600',
|
||||
'margin-block-start': '0',
|
||||
'margin-block-end': '0',
|
||||
}
|
||||
}).component();
|
||||
|
||||
return this.view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[migrationsTabLink, breadCrumbImage, this.databaseLabel],
|
||||
{ flex: '0 0 auto' })
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
})
|
||||
.withProps({
|
||||
height: 20,
|
||||
CSSStyles: { 'padding': '0', 'margin-bottom': '5px' }
|
||||
})
|
||||
.component();
|
||||
}
|
||||
|
||||
protected createMigrationToolbarContainer(): azdata.FlexContainer {
|
||||
const toolbarContainer = this.view.modelBuilder.toolbarContainer();
|
||||
const buttonHeight = 20;
|
||||
this.cutoverButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.cutover,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COMPLETE_CUTOVER,
|
||||
height: buttonHeight,
|
||||
enabled: false,
|
||||
CSSStyles: { 'display': 'none' }
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.cutoverButton.onDidClick(async (e) => {
|
||||
await this.statusBar.clearError();
|
||||
await this.refresh();
|
||||
const dialog = new ConfirmCutoverDialog(this.model);
|
||||
await dialog.initialize();
|
||||
|
||||
if (this.model.CutoverError) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
this.model.CutoverError.message);
|
||||
}
|
||||
}));
|
||||
|
||||
this.cancelButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.cancel,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.CANCEL_MIGRATION,
|
||||
height: buttonHeight,
|
||||
enabled: false,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.cancelButton.onDidClick((e) => {
|
||||
void vscode.window.showInformationMessage(
|
||||
loc.CANCEL_MIGRATION_CONFIRMATION,
|
||||
{ modal: true },
|
||||
loc.YES,
|
||||
loc.NO
|
||||
).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
await this.statusBar.clearError();
|
||||
await this.model.cancelMigration();
|
||||
await this.refresh();
|
||||
if (this.model.CancelMigrationError) {
|
||||
{
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
this.model.CancelMigrationError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
this.retryButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.RETRY_MIGRATION,
|
||||
iconPath: IconPathHelper.retry,
|
||||
enabled: false,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.retryButton.onDidClick(
|
||||
async (e) => {
|
||||
await this.refresh();
|
||||
const retryMigrationDialog = new RetryMigrationDialog(
|
||||
this.context,
|
||||
this.serviceContext,
|
||||
this.model.migration,
|
||||
this.onClosedCallback);
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
));
|
||||
|
||||
this.copyDatabaseMigrationDetails = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.copy,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COPY_MIGRATION_DETAILS,
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.copyDatabaseMigrationDetails.onDidClick(async (e) => {
|
||||
await this.refresh();
|
||||
await vscode.env.clipboard.writeText(this._getMigrationDetails());
|
||||
|
||||
void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
this.newSupportRequest = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.NEW_SUPPORT_REQUEST,
|
||||
iconPath: IconPathHelper.newSupportRequest,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.newSupportRequest.onDidClick(async (e) => {
|
||||
const serviceId = this.model.migration.properties.migrationService;
|
||||
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
|
||||
}));
|
||||
|
||||
this.refreshButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.REFRESH_BUTTON_TEXT,
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.refreshButton.onDidClick(
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this.refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '4px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
toolbarContainer.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.cutoverButton },
|
||||
<azdata.ToolbarComponent>{ component: this.cancelButton },
|
||||
<azdata.ToolbarComponent>{ component: this.retryButton },
|
||||
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.refreshButton },
|
||||
<azdata.ToolbarComponent>{ component: this.refreshLoader },
|
||||
]);
|
||||
|
||||
return this.view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
this.createBreadcrumbContainer(),
|
||||
toolbarContainer.component(),
|
||||
])
|
||||
.withLayout({ flexFlow: 'column', width: '100%' })
|
||||
.component();
|
||||
}
|
||||
|
||||
protected async createInfoCard(
|
||||
label: string,
|
||||
iconPath: azdata.IconPath
|
||||
): Promise<azdata.FlexContainer> {
|
||||
const defaultValue = (0).toLocaleString();
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
width: 168,
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'margin': '0 12px 0 0',
|
||||
'box-sizing': 'border-box',
|
||||
'border': '1px solid rgba(204, 204, 204, 0.5)',
|
||||
'box-shadow': '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
'border-radius': '2px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const labelComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '5px',
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
|
||||
const iconComponent = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: iconPath,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
height: 16,
|
||||
width: 16,
|
||||
CSSStyles: {
|
||||
'margin': '5px 5px 5px 5px',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const textComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: defaultValue,
|
||||
title: defaultValue,
|
||||
CSSStyles: {
|
||||
'font-size': '20px',
|
||||
'font-weight': '600',
|
||||
'margin': '0 5px 0 5px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this.summaryTextComponent.push(textComponent);
|
||||
|
||||
const iconTextComponent = this.view.modelBuilder.flexContainer()
|
||||
.withItems([iconComponent, textComponent])
|
||||
.withLayout({ alignItems: 'center' })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'margin': '0 0 0 5px',
|
||||
'padding': '0',
|
||||
},
|
||||
display: 'inline-flex'
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(iconTextComponent);
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
protected async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent,
|
||||
icon?: azdata.ImageComponent
|
||||
}> {
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'padding-right': '12px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const labelComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
...styles.LIGHT_LABEL_CSS,
|
||||
'margin-bottom': '0',
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
|
||||
const textComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: value,
|
||||
title: value,
|
||||
description: value,
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0 12px',
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'max-width': '230px',
|
||||
'display': 'inline-block',
|
||||
}
|
||||
}).component();
|
||||
|
||||
let iconComponent;
|
||||
if (iconPath) {
|
||||
iconComponent = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: (iconPath === ' ') ? undefined : iconPath,
|
||||
iconHeight: statusImageSize,
|
||||
iconWidth: statusImageSize,
|
||||
height: statusImageSize,
|
||||
width: statusImageSize,
|
||||
title: value,
|
||||
CSSStyles: {
|
||||
'margin': '7px 3px 0 0',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const iconTextComponent = this.view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
iconComponent,
|
||||
textComponent
|
||||
]).withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0',
|
||||
'padding': '0'
|
||||
},
|
||||
display: 'inline-flex'
|
||||
}).component();
|
||||
flexContainer.addItem(iconTextComponent);
|
||||
} else {
|
||||
flexContainer.addItem(textComponent);
|
||||
}
|
||||
|
||||
return {
|
||||
flexContainer: flexContainer,
|
||||
text: textComponent,
|
||||
icon: iconComponent
|
||||
};
|
||||
}
|
||||
|
||||
protected async showMigrationErrors(migration: DatabaseMigration): Promise<void> {
|
||||
const errorMessage = this.getMigrationErrors(migration);
|
||||
if (errorMessage?.length > 0) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_ERROR_DETAILS_TITLE,
|
||||
loc.MIGRATION_ERROR_DETAILS_LABEL,
|
||||
errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMigrationCurrentlyRestoringFile(migration: DatabaseMigration): string | undefined {
|
||||
const lastAppliedBackupFile = this.getMigrationLastAppliedBackupFile(migration);
|
||||
const currentRestoringFile = migration?.properties?.migrationStatusDetails?.currentRestoringFilename;
|
||||
|
||||
return currentRestoringFile === lastAppliedBackupFile
|
||||
&& currentRestoringFile && currentRestoringFile.length > 0
|
||||
? loc.ALL_BACKUPS_RESTORED
|
||||
: currentRestoringFile;
|
||||
}
|
||||
|
||||
protected getMigrationLastAppliedBackupFile(migration: DatabaseMigration): string | undefined {
|
||||
return migration?.properties?.migrationStatusDetails?.lastRestoredFilename
|
||||
|| migration?.properties?.offlineConfiguration?.lastBackupName;
|
||||
}
|
||||
|
||||
private _getMigrationDetails(): string {
|
||||
return JSON.stringify(this.model.migration, undefined, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 loc from '../constants/strings';
|
||||
import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
|
||||
import { CopyProgressDetail, getResourceName } from '../api/azure';
|
||||
import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { EOL } from 'os';
|
||||
|
||||
const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
|
||||
|
||||
const TableColumns = {
|
||||
tableName: 'tableName',
|
||||
status: 'status',
|
||||
dataRead: 'dataRead',
|
||||
dataWritten: 'dataWritten',
|
||||
rowsRead: 'rowsRead',
|
||||
rowsCopied: 'rowsCopied',
|
||||
copyThroughput: 'copyThroughput',
|
||||
copyDuration: 'copyDuration',
|
||||
parallelCopyType: 'parallelCopyType',
|
||||
usedParallelCopies: 'usedParallelCopies',
|
||||
copyStart: 'copyStart',
|
||||
};
|
||||
|
||||
enum SummaryCardIndex {
|
||||
TotalTables = 0,
|
||||
InProgressTables = 1,
|
||||
SuccessfulTables = 2,
|
||||
FailedTables = 3,
|
||||
CanceledTables = 4,
|
||||
}
|
||||
|
||||
export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationDetailsTableTab> {
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _serverObjectsInfoField!: InfoFieldSchema;
|
||||
private _tableFilterInputBox!: azdata.InputBoxComponent;
|
||||
private _columnSortDropdown!: azdata.DropDownComponent;
|
||||
private _columnSortCheckbox!: azdata.CheckBoxComponent;
|
||||
private _progressTable!: azdata.TableComponent;
|
||||
private _progressDetail: CopyProgressDetail[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationDetailsTableTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
await this.model.fetchStatus();
|
||||
await this._loadData();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
}
|
||||
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
const migration = this.model?.migration;
|
||||
await this.showMigrationErrors(this.model?.migration);
|
||||
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
const sqlServerName = migration?.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration?.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration?.name;
|
||||
const targetServerName = getResourceName(migration?.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
const hashSet: loc.LookupTable<number> = {};
|
||||
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
|
||||
await this._populateTableData(hashSet);
|
||||
|
||||
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
|
||||
const cancelledCount =
|
||||
(hashSet[PipelineStatusCodes.Canceled] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.Cancelled] ?? 0);
|
||||
|
||||
const failedCount = hashSet[PipelineStatusCodes.Failed] ?? 0;
|
||||
const inProgressCount =
|
||||
(hashSet[PipelineStatusCodes.Queued] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.CopyFinished] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.Copying] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.PreparingForCopy] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.InProgress] ?? 0);
|
||||
|
||||
const totalCount = migration.properties.migrationStatusDetails?.listOfCopyProgressDetails.length ?? 0;
|
||||
|
||||
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.SuccessfulTables, successCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.FailedTables, failedCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.CanceledTables, cancelledCount);
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
this._serverObjectsInfoField.text.value = totalCount.toLocaleString();
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
}
|
||||
|
||||
private async _populateTableData(hashSet: loc.LookupTable<number> = {}): Promise<void> {
|
||||
if (this._progressTable.data.length > 0) {
|
||||
await this._progressTable.updateProperty('data', []);
|
||||
}
|
||||
|
||||
// Sort table data
|
||||
this._sortTableMigrations(
|
||||
this._progressDetail,
|
||||
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
|
||||
this._columnSortCheckbox.checked === true);
|
||||
|
||||
const data = this._progressDetail.map((d) => {
|
||||
hashSet[d.status] = (hashSet[d.status] ?? 0) + 1;
|
||||
return [
|
||||
d.tableName,
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getPipelineStatusImage(d.status),
|
||||
title: loc.PipelineRunStatus[d.status] ?? d.status?.toUpperCase(),
|
||||
},
|
||||
formatSizeBytes(d.dataRead),
|
||||
formatSizeBytes(d.dataWritten),
|
||||
formatNumber(d.rowsRead),
|
||||
formatNumber(d.rowsCopied),
|
||||
formatSizeKb(d.copyThroughput),
|
||||
formatTime((d.copyDuration ?? 0) * 1000),
|
||||
loc.ParallelCopyType[d.parallelCopyType] ?? d.parallelCopyType,
|
||||
d.usedParallelCopies,
|
||||
formatDateTimeString(d.copyStart),
|
||||
];
|
||||
}) ?? [];
|
||||
|
||||
// Filter tableData
|
||||
const filteredData = this._filterTables(data, this._tableFilterInputBox.value);
|
||||
|
||||
await this._progressTable.updateProperty('data', filteredData);
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
try {
|
||||
this._progressTable = this.view.modelBuilder.table()
|
||||
.withProps({
|
||||
ariaLabel: loc.ACTIVE_BACKUP_FILES,
|
||||
CSSStyles: {
|
||||
'padding-left': '0px',
|
||||
'max-width': '1111px'
|
||||
},
|
||||
data: [],
|
||||
height: '300px',
|
||||
columns: [
|
||||
{
|
||||
value: TableColumns.tableName,
|
||||
name: loc.SQLDB_COL_TABLE_NAME,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 170,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
name: loc.STATUS,
|
||||
value: TableColumns.status,
|
||||
width: 106,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
icon: IconPathHelper.inProgressMigration,
|
||||
showText: true,
|
||||
},
|
||||
{
|
||||
value: TableColumns.dataRead,
|
||||
name: loc.SQLDB_COL_DATA_READ,
|
||||
width: 64,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.dataWritten,
|
||||
name: loc.SQLDB_COL_DATA_WRITTEN,
|
||||
width: 77,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.rowsRead,
|
||||
name: loc.SQLDB_COL_ROWS_READ,
|
||||
width: 68,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.rowsCopied,
|
||||
name: loc.SQLDB_COL_ROWS_COPIED,
|
||||
width: 77,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.copyThroughput,
|
||||
name: loc.SQLDB_COL_COPY_THROUGHPUT,
|
||||
width: 102,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.copyDuration,
|
||||
name: loc.SQLDB_COL_COPY_DURATION,
|
||||
width: 87,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.parallelCopyType,
|
||||
name: loc.SQLDB_COL_PARRALEL_COPY_TYPE,
|
||||
width: 104,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.usedParallelCopies,
|
||||
name: loc.SQLDB_COL_USED_PARALLEL_COPIES,
|
||||
width: 116,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.copyStart,
|
||||
name: loc.SQLDB_COL_COPY_START,
|
||||
width: 140,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
],
|
||||
}).component();
|
||||
|
||||
const formItems: azdata.FormComponent<azdata.Component>[] = [
|
||||
{ component: this.createMigrationToolbarContainer() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{
|
||||
component: this.view.modelBuilder.separator()
|
||||
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
|
||||
.component()
|
||||
},
|
||||
{ component: await this._createStatusBar() },
|
||||
{ component: await this._createTableFilter() },
|
||||
{ component: this._progressTable },
|
||||
];
|
||||
|
||||
this.disposables.push(
|
||||
this._progressTable.onCellAction!(
|
||||
async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
if (buttonState?.column === 1) {
|
||||
const tableName = this._progressTable!.data[rowState.row][0] || null;
|
||||
const tableProgress = this.model.migration.properties.migrationStatusDetails?.listOfCopyProgressDetails?.find(
|
||||
progress => progress.tableName === tableName);
|
||||
const errors = tableProgress?.errors || [];
|
||||
const tableStatus = loc.PipelineRunStatus[tableProgress?.status ?? ''] ?? tableProgress?.status;
|
||||
const statusMessage = loc.TABLE_MIGRATION_STATUS_LABEL(tableStatus);
|
||||
const errorMessage = errors.join(EOL);
|
||||
|
||||
this.showDialogMessage(
|
||||
loc.TABLE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errorMessage);
|
||||
}
|
||||
}));
|
||||
|
||||
const formContainer = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false })
|
||||
.withProps({ width: '100%', CSSStyles: { margin: '0 0 0 5px', padding: '0 15px 0 15px' } })
|
||||
.component();
|
||||
|
||||
this.content = formContainer;
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
}
|
||||
|
||||
private _sortTableMigrations(data: CopyProgressDetail[], columnName: string, ascending: boolean): void {
|
||||
const sortDir = ascending ? -1 : 1;
|
||||
switch (columnName) {
|
||||
case TableColumns.tableName:
|
||||
data.sort((t1, t2) => this.stringCompare(t1.tableName, t2.tableName, sortDir));
|
||||
return;
|
||||
case TableColumns.status:
|
||||
data.sort((t1, t2) => this.stringCompare(t1.status, t2.status, sortDir));
|
||||
return;
|
||||
case TableColumns.dataRead:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.dataRead, t2.dataRead, sortDir));
|
||||
return;
|
||||
case TableColumns.dataWritten:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.dataWritten, t2.dataWritten, sortDir));
|
||||
return;
|
||||
case TableColumns.rowsRead:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.rowsRead, t2.rowsRead, sortDir));
|
||||
return;
|
||||
case TableColumns.rowsCopied:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.rowsCopied, t2.rowsCopied, sortDir));
|
||||
return;
|
||||
case TableColumns.copyThroughput:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.copyThroughput, t2.copyThroughput, sortDir));
|
||||
return;
|
||||
case TableColumns.copyDuration:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.copyDuration, t2.copyDuration, sortDir));
|
||||
return;
|
||||
case TableColumns.parallelCopyType:
|
||||
data.sort((t1, t2) => this.stringCompare(t1.parallelCopyType, t2.parallelCopyType, sortDir));
|
||||
return;
|
||||
case TableColumns.usedParallelCopies:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.usedParallelCopies, t2.usedParallelCopies, sortDir));
|
||||
return;
|
||||
case TableColumns.copyStart:
|
||||
data.sort((t1, t2) => this.dateCompare(t1.copyStart, t2.copyStart, sortDir));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSummaryComponent(cardIndex: number, value: number): void {
|
||||
const stringValue = value.toLocaleString();
|
||||
const textComponent = this.summaryTextComponent[cardIndex];
|
||||
textComponent.value = stringValue;
|
||||
textComponent.title = stringValue;
|
||||
}
|
||||
|
||||
private _filterTables(tables: any[], value: string | undefined): any[] {
|
||||
const lcValue = value?.toLowerCase() ?? '';
|
||||
|
||||
return lcValue.length > 0
|
||||
? tables.filter((table: string[]) =>
|
||||
table.some((col: string | { title: string }) => {
|
||||
return typeof (col) === 'string'
|
||||
? col.toLowerCase().includes(lcValue)
|
||||
: col.title?.toLowerCase().includes(lcValue);
|
||||
}))
|
||||
: tables;
|
||||
}
|
||||
|
||||
private async _createTableFilter(): Promise<azdata.FlexContainer> {
|
||||
this._tableFilterInputBox = this.view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
inputType: 'text',
|
||||
maxLength: 100,
|
||||
width: 268,
|
||||
placeHolder: loc.FILTER_SERVER_OBJECTS_PLACEHOLDER,
|
||||
ariaLabel: loc.FILTER_SERVER_OBJECTS_ARIA_LABEL,
|
||||
})
|
||||
.component();
|
||||
|
||||
this.disposables.push(
|
||||
this._tableFilterInputBox.onTextChanged(
|
||||
async (value) => await this._populateTableData()));
|
||||
|
||||
const sortLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SORT_LABEL,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._columnSortDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
editable: false,
|
||||
width: 150,
|
||||
CSSStyles: { 'margin-left': '5px' },
|
||||
value: <azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.START_TIME },
|
||||
values: [
|
||||
<azdata.CategoryValue>{ name: TableColumns.tableName, displayName: loc.SQLDB_COL_TABLE_NAME },
|
||||
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS },
|
||||
<azdata.CategoryValue>{ name: TableColumns.dataRead, displayName: loc.SQLDB_COL_DATA_READ },
|
||||
<azdata.CategoryValue>{ name: TableColumns.dataWritten, displayName: loc.SQLDB_COL_DATA_WRITTEN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.rowsRead, displayName: loc.SQLDB_COL_ROWS_READ },
|
||||
<azdata.CategoryValue>{ name: TableColumns.rowsCopied, displayName: loc.SQLDB_COL_ROWS_COPIED },
|
||||
<azdata.CategoryValue>{ name: TableColumns.copyThroughput, displayName: loc.SQLDB_COL_COPY_THROUGHPUT },
|
||||
<azdata.CategoryValue>{ name: TableColumns.copyDuration, displayName: loc.SQLDB_COL_COPY_DURATION },
|
||||
<azdata.CategoryValue>{ name: TableColumns.parallelCopyType, displayName: loc.SQLDB_COL_PARRALEL_COPY_TYPE },
|
||||
<azdata.CategoryValue>{ name: TableColumns.usedParallelCopies, displayName: loc.SQLDB_COL_USED_PARALLEL_COPIES },
|
||||
<azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.SQLDB_COL_COPY_START },
|
||||
],
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortDropdown.onValueChanged(
|
||||
async (value) => await this._populateTableData()));
|
||||
|
||||
this._columnSortCheckbox = this.view.modelBuilder.checkBox()
|
||||
.withProps({
|
||||
label: loc.ASCENDING_LABEL,
|
||||
checked: false,
|
||||
CSSStyles: { 'margin-left': '15px' },
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortCheckbox.onChanged(
|
||||
async (value) => await this._populateTableData()));
|
||||
|
||||
const columnSortContainer = this.view.modelBuilder.flexContainer()
|
||||
.withItems([sortLabel, this._columnSortDropdown])
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
},
|
||||
}).component();
|
||||
columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
|
||||
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
'flex-flow': 'wrap',
|
||||
},
|
||||
}).component();
|
||||
flexContainer.addItem(this._tableFilterInputBox, { flex: '0' });
|
||||
flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private async _createStatusBar(): Promise<azdata.FlexContainer> {
|
||||
const serverObjectsLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SERVER_OBJECTS_LABEL,
|
||||
CSSStyles: {
|
||||
'font-weight': '600',
|
||||
'font-size': '14px',
|
||||
'margin': '0 0 5px 0',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
flexWrap: 'wrap',
|
||||
})
|
||||
.component();
|
||||
|
||||
flexContainer.addItems([
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_ALL_TABLES_LABEL, IconPathHelper.allTables),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL, IconPathHelper.inProgressMigration),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL, IconPathHelper.completedMigration),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_FAILED_TABLES_LABEL, IconPathHelper.error),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_CANCELLED_TABLES_LABEL, IconPathHelper.cancel)
|
||||
], { flex: '0 0 auto', CSSStyles: { 'width': '168px' } });
|
||||
|
||||
return this.view.modelBuilder.flexContainer()
|
||||
.withItems([serverObjectsLabel, flexContainer])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
}
|
||||
|
||||
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(
|
||||
infoField.flexContainer,
|
||||
{ CSSStyles: { width: infoFieldLgWidth } });
|
||||
};
|
||||
|
||||
const flexServer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const flexStatus = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._serverObjectsInfoField = await this.createInfoField(loc.SERVER_OBJECTS_FIELD_LABEL, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._serverObjectsInfoField, flexStatus);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: { 'flex': '0', 'width': infoFieldLgWidth }
|
||||
};
|
||||
|
||||
const flexInfo = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
}
|
||||
771
extensions/sql-migration/src/dashboard/migrationsListTab.ts
Normal file
771
extensions/sql-migration/src/dashboard/migrationsListTab.ts
Normal file
@@ -0,0 +1,771 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
|
||||
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { DatabaseMigration, getResourceName } from '../api/azure';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { MigrationMode } from '../models/stateMachine';
|
||||
|
||||
export const MigrationsListTabId = 'MigrationsListTab';
|
||||
|
||||
const TableColumns = {
|
||||
sourceDatabase: 'sourceDatabase',
|
||||
sourceServer: 'sourceServer',
|
||||
status: 'status',
|
||||
mode: 'mode',
|
||||
targetType: 'targetType',
|
||||
targetDatabse: 'targetDatabase',
|
||||
targetServer: 'TargetServer',
|
||||
duration: 'duration',
|
||||
startTime: 'startTime',
|
||||
finishTime: 'finishTime',
|
||||
};
|
||||
|
||||
export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
private _searchBox!: azdata.InputBoxComponent;
|
||||
private _refresh!: azdata.ButtonComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _statusDropdown!: azdata.DropDownComponent;
|
||||
private _columnSortDropdown!: azdata.DropDownComponent;
|
||||
private _columnSortCheckbox!: azdata.CheckBoxComponent;
|
||||
private _statusTable!: azdata.TableComponent;
|
||||
private _refreshLoader!: azdata.LoadingComponent;
|
||||
private _filteredMigrations: DatabaseMigration[] = [];
|
||||
private _openMigrationDetails!: (migration: DatabaseMigration) => Promise<void>;
|
||||
private _migrations: DatabaseMigration[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationsListTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationsListTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this._openMigrationDetails = openMigrationDetails;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
|
||||
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
|
||||
.find(value => value.name === filter.toString());
|
||||
|
||||
this._statusDropdown.value = statusFilter;
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._refresh.enabled = false;
|
||||
this._refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
await this._statusTable.updateProperty('data', []);
|
||||
this._migrations = await getCurrentMigrations();
|
||||
await this._populateMigrationTable();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
|
||||
} finally {
|
||||
this._refreshLoader.loading = false;
|
||||
this._refresh.enabled = true;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(): Promise<void> {
|
||||
this._registerCommands();
|
||||
|
||||
this.content = this.view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[
|
||||
this._createToolbar(),
|
||||
await this._createSearchAndSortContainer(),
|
||||
this._createStatusTable()
|
||||
],
|
||||
{ CSSStyles: { 'width': '100%' } }
|
||||
).withLayout({
|
||||
width: '100%',
|
||||
flexFlow: 'column',
|
||||
}).withProps({ CSSStyles: { 'padding': '0px' } })
|
||||
.component();
|
||||
}
|
||||
|
||||
private _createToolbar(): azdata.ToolbarContainer {
|
||||
const toolbar = this.view.modelBuilder.toolbarContainer();
|
||||
|
||||
this._refresh = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 24,
|
||||
label: loc.REFRESH_BUTTON_LABEL,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._refresh.onDidClick(
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this._refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '6px'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this._refresh },
|
||||
<azdata.ToolbarComponent>{ component: this._refreshLoader },
|
||||
]);
|
||||
|
||||
return toolbar.component();
|
||||
}
|
||||
|
||||
private async _createSearchAndSortContainer(): Promise<azdata.FlexContainer> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 230,
|
||||
}).component();
|
||||
|
||||
const onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(
|
||||
async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._searchBox = this.view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
stopEnterPropagation: true,
|
||||
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
|
||||
width: '200px',
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._searchBox.onTextChanged(
|
||||
async (value) => await this._populateMigrationTable()));
|
||||
|
||||
const searchLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.STATUS_LABEL,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._statusDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS_FILTER,
|
||||
values: this._statusDropdownValues,
|
||||
width: '150px'
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._statusDropdown.onValueChanged(
|
||||
async (value) => await this._populateMigrationTable()));
|
||||
|
||||
const searchContainer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
}).withProps({ CSSStyles: { 'margin-left': '10px' } })
|
||||
.component();
|
||||
searchContainer.addItem(searchLabel, { flex: '0' });
|
||||
searchContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
|
||||
|
||||
const sortLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SORT_LABEL,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._columnSortDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
editable: false,
|
||||
width: 120,
|
||||
CSSStyles: { 'margin-left': '5px' },
|
||||
value: <azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
|
||||
values: [
|
||||
<azdata.CategoryValue>{ name: TableColumns.sourceDatabase, displayName: loc.SRC_DATABASE },
|
||||
<azdata.CategoryValue>{ name: TableColumns.sourceServer, displayName: loc.SRC_SERVER },
|
||||
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS_COLUMN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.mode, displayName: loc.MIGRATION_MODE },
|
||||
<azdata.CategoryValue>{ name: TableColumns.targetType, displayName: loc.AZURE_SQL_TARGET },
|
||||
<azdata.CategoryValue>{ name: TableColumns.targetDatabse, displayName: loc.TARGET_DATABASE_COLUMN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.targetServer, displayName: loc.TARGET_SERVER_COLUMN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.duration, displayName: loc.DURATION },
|
||||
<azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
|
||||
<azdata.CategoryValue>{ name: TableColumns.finishTime, displayName: loc.FINISH_TIME },
|
||||
],
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortDropdown.onValueChanged(
|
||||
async (e) => await this._populateMigrationTable()));
|
||||
|
||||
this._columnSortCheckbox = this.view.modelBuilder.checkBox()
|
||||
.withProps({
|
||||
label: loc.ASCENDING_LABEL,
|
||||
checked: false,
|
||||
CSSStyles: { 'margin-left': '15px' },
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortCheckbox.onChanged(
|
||||
async (e) => await this._populateMigrationTable()));
|
||||
|
||||
const columnSortContainer = this.view.modelBuilder.flexContainer()
|
||||
.withItems([sortLabel, this._columnSortDropdown])
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
},
|
||||
}).component();
|
||||
columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
|
||||
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
'flex-flow': 'wrap',
|
||||
},
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
flexContainer.addItem(this._searchBox, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
flexContainer.addItem(searchContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
|
||||
const container = this.view.modelBuilder.flexContainer()
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
|
||||
container.addItem(flexContainer);
|
||||
return container;
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.Cutover,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(
|
||||
migration => migration.id === migrationId);
|
||||
|
||||
if (canRetryMigration(migration)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
|
||||
await dialog.initialize();
|
||||
if (cutoverDialogModel.CutoverError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
|
||||
}
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
e.message);
|
||||
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewDatabase,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(m => m.id === migrationId);
|
||||
await this._openMigrationDetails(migration!);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewTarget,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewService,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CopyMigration,
|
||||
async (migrationId: string) => {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
|
||||
try {
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
|
||||
}
|
||||
|
||||
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
|
||||
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CancelMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canCancelMigration(migration)) {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
await cutoverDialogModel.cancelMigration();
|
||||
|
||||
if (cutoverDialogModel.CancelMigrationError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.RetryMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canRetryMigration(migration)) {
|
||||
let retryMigrationDialog = new RetryMigrationDialog(
|
||||
this.context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
async () => await this.onDialogClosed());
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
|
||||
const sortDir = ascending ? -1 : 1;
|
||||
switch (columnName) {
|
||||
case TableColumns.sourceDatabase:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
m1.properties.sourceDatabaseName,
|
||||
m2.properties.sourceDatabaseName,
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.sourceServer:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
m1.properties.sourceServerName,
|
||||
m2.properties.sourceServerName,
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.status:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getMigrationStatusWithErrors(m1),
|
||||
getMigrationStatusWithErrors(m2),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.mode:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getMigrationMode(m1),
|
||||
getMigrationMode(m2),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.targetType:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getMigrationTargetType(m1),
|
||||
getMigrationTargetType(m2),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.targetDatabse:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getResourceName(m1.id),
|
||||
getResourceName(m2.id),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.targetServer:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getResourceName(m1.properties.scope),
|
||||
getResourceName(m2.properties.scope),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.duration:
|
||||
migrations.sort((m1, m2) => {
|
||||
if (!m1.properties.startedOn) {
|
||||
return sortDir;
|
||||
} else if (!m2.properties.startedOn) {
|
||||
return -sortDir;
|
||||
}
|
||||
const m1_startedOn = new Date(m1.properties.startedOn);
|
||||
const m2_startedOn = new Date(m2.properties.startedOn);
|
||||
const m1_endedOn = new Date(m1.properties.endedOn ?? Date.now());
|
||||
const m2_endedOn = new Date(m2.properties.endedOn ?? Date.now());
|
||||
const m1_duration = m1_endedOn.getTime() - m1_startedOn.getTime();
|
||||
const m2_duration = m2_endedOn.getTime() - m2_startedOn.getTime();
|
||||
return m1_duration > m2_duration ? -sortDir : sortDir;
|
||||
});
|
||||
return;
|
||||
case TableColumns.startTime:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.dateCompare(
|
||||
m1.properties.startedOn,
|
||||
m2.properties.startedOn,
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.finishTime:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.dateCompare(
|
||||
m1.properties.endedOn,
|
||||
m2.properties.endedOn,
|
||||
sortDir));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async _populateMigrationTable(): Promise<void> {
|
||||
try {
|
||||
this._filteredMigrations = filterMigrations(
|
||||
this._migrations,
|
||||
(<azdata.CategoryValue>this._statusDropdown.value).name,
|
||||
this._searchBox.value!);
|
||||
|
||||
this._sortMigrations(
|
||||
this._filteredMigrations,
|
||||
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
|
||||
this._columnSortCheckbox.checked === true);
|
||||
|
||||
const data: any[] = this._filteredMigrations.map((migration, index) => {
|
||||
return [
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: IconPathHelper.sqlDatabaseLogo,
|
||||
title: migration.properties.sourceDatabaseName ?? EmptySettingValue,
|
||||
}, // sourceDatabase
|
||||
migration.properties.sourceServerName ?? EmptySettingValue, // sourceServer
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getMigrationStatusImage(migration),
|
||||
title: getMigrationStatusWithErrors(migration),
|
||||
}, // statue
|
||||
getMigrationMode(migration), // mode
|
||||
getMigrationTargetType(migration), // targetType
|
||||
getResourceName(migration.id), // targetDatabase
|
||||
getResourceName(migration.properties.scope), // targetServer
|
||||
getMigrationDuration(
|
||||
migration.properties.startedOn,
|
||||
migration.properties.endedOn), // duration
|
||||
getMigrationTime(migration.properties.startedOn), // startTime
|
||||
getMigrationTime(migration.properties.endedOn), // finishTime
|
||||
<azdata.ContextMenuColumnCellValue>{
|
||||
title: '',
|
||||
context: migration.id,
|
||||
commands: this._getMenuCommands(migration), // context menu
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
await this._statusTable.updateProperty('data', data);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.LOAD_MIGRATION_LIST_ERROR,
|
||||
loc.LOAD_MIGRATION_LIST_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
|
||||
}
|
||||
}
|
||||
|
||||
private _createStatusTable(): azdata.TableComponent {
|
||||
const headerCssStyles = undefined;
|
||||
const rowCssStyles = undefined;
|
||||
|
||||
this._statusTable = this.view.modelBuilder.table()
|
||||
.withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS,
|
||||
CSSStyles: { 'margin-left': '10px' },
|
||||
data: [],
|
||||
forceFitColumns: azdata.ColumnSizingMode.AutoFit,
|
||||
height: '500px',
|
||||
columns: [
|
||||
<azdata.HyperlinkColumn>{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.SRC_DATABASE,
|
||||
value: 'sourceDatabase',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
showText: true,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.SRC_SERVER,
|
||||
value: 'sourceServer',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.STATUS_COLUMN,
|
||||
value: 'status',
|
||||
width: 120,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.MIGRATION_MODE,
|
||||
value: 'mode',
|
||||
width: 55,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.AZURE_SQL_TARGET,
|
||||
value: 'targetType',
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.TARGET_DATABASE_COLUMN,
|
||||
value: 'targetDatabase',
|
||||
width: 125,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.TARGET_SERVER_COLUMN,
|
||||
value: 'targetServer',
|
||||
width: 125,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.DURATION,
|
||||
value: 'duration',
|
||||
width: 55,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.START_TIME,
|
||||
value: 'startTime',
|
||||
width: 115,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.FINISH_TIME,
|
||||
value: 'finishTime',
|
||||
width: 115,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: '',
|
||||
value: 'contextMenu',
|
||||
width: 25,
|
||||
type: azdata.ColumnType.contextMenu,
|
||||
}
|
||||
]
|
||||
}).component();
|
||||
|
||||
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
const migration = this._filteredMigrations[rowState.row];
|
||||
switch (buttonState?.column) {
|
||||
case 2:
|
||||
const status = getMigrationStatus(migration);
|
||||
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
|
||||
const errors = this.getMigrationErrors(migration!);
|
||||
|
||||
this.showDialogMessage(
|
||||
loc.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errors);
|
||||
break;
|
||||
case 0:
|
||||
await this._openMigrationDetails(migration);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
return this._statusTable;
|
||||
}
|
||||
|
||||
private _getMenuCommands(migration: DatabaseMigration): string[] {
|
||||
const menuCommands: string[] = [];
|
||||
|
||||
if (getMigrationModeEnum(migration) === MigrationMode.ONLINE &&
|
||||
canCutoverMigration(migration)) {
|
||||
menuCommands.push(MenuCommands.Cutover);
|
||||
}
|
||||
|
||||
menuCommands.push(...[
|
||||
MenuCommands.ViewDatabase,
|
||||
MenuCommands.ViewTarget,
|
||||
MenuCommands.ViewService,
|
||||
MenuCommands.CopyMigration]);
|
||||
|
||||
if (canCancelMigration(migration)) {
|
||||
menuCommands.push(MenuCommands.CancelMigration);
|
||||
}
|
||||
|
||||
return menuCommands;
|
||||
}
|
||||
|
||||
private _statusDropdownValues: azdata.CategoryValue[] = [
|
||||
{ displayName: loc.STATUS_ALL, name: AdsMigrationStatus.ALL },
|
||||
{ displayName: loc.STATUS_ONGOING, name: AdsMigrationStatus.ONGOING },
|
||||
{ displayName: loc.STATUS_COMPLETING, name: AdsMigrationStatus.COMPLETING },
|
||||
{ displayName: loc.STATUS_SUCCEEDED, name: AdsMigrationStatus.SUCCEEDED },
|
||||
{ displayName: loc.STATUS_FAILED, name: AdsMigrationStatus.FAILED }
|
||||
];
|
||||
}
|
||||
148
extensions/sql-migration/src/dashboard/migrationsTab.ts
Normal file
148
extensions/sql-migration/src/dashboard/migrationsTab.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 loc from '../constants/strings';
|
||||
import { AdsMigrationStatus, TabBase } from './tabBase';
|
||||
import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { FileStorageType } from '../models/stateMachine';
|
||||
import { MigrationDetailsTabBase } from './migrationDetailsTabBase';
|
||||
import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab';
|
||||
import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab';
|
||||
import { MigrationDetailsTableTab } from './migrationDetailsTableTab';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
export const MigrationsTabId = 'MigrationsTab';
|
||||
|
||||
export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
private _tab!: azdata.DivContainer;
|
||||
private _migrationsListTab!: MigrationsListTab;
|
||||
private _migrationDetailsTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsFileShareTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
|
||||
private _selectedTabId: string | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = loc.DESKTOP_MIGRATIONS_TAB_TITLE;
|
||||
this.id = MigrationsTabId;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this._migrationsListTab.onDialogClosed();
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationsTab> {
|
||||
|
||||
this.context = context;
|
||||
this.view = view;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(view);
|
||||
await this._openTab(this._migrationsListTab);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
switch (this._selectedTabId) {
|
||||
case undefined:
|
||||
case MigrationsListTabId:
|
||||
return await this._migrationsListTab?.refresh();
|
||||
default:
|
||||
return await this._migrationDetailsTab?.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
this._tab = this.view.modelBuilder.divContainer()
|
||||
.withLayout({ height: '100%' })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px',
|
||||
'padding': '0px',
|
||||
'width': '100%'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this._migrationsListTab = await new MigrationsListTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async (migration) => await this._openMigrationDetails(migration),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationsListTab);
|
||||
|
||||
this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsBlobTab);
|
||||
|
||||
this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsFileShareTab);
|
||||
|
||||
this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsFileShareTab);
|
||||
|
||||
this.content = this._tab;
|
||||
}
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
await this._migrationsListTab?.setMigrationFilter(filter);
|
||||
await this._openTab(this._migrationsListTab);
|
||||
await this._migrationsListTab?.setMigrationFilter(filter);
|
||||
}
|
||||
|
||||
private async _openMigrationDetails(migration: DatabaseMigration): Promise<void> {
|
||||
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
|
||||
case FileStorageType.AzureBlob:
|
||||
this._migrationDetailsTab = this._migrationDetailsBlobTab;
|
||||
break;
|
||||
case FileStorageType.FileShare:
|
||||
this._migrationDetailsTab = this._migrationDetailsFileShareTab;
|
||||
break;
|
||||
case FileStorageType.None:
|
||||
this._migrationDetailsTab = this._migrationDetailsTableTab;
|
||||
break;
|
||||
}
|
||||
|
||||
await this._migrationDetailsTab.setMigrationContext(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration);
|
||||
|
||||
await this._openTab(this._migrationDetailsTab);
|
||||
}
|
||||
|
||||
private async _openMigrationsListTab(): Promise<void> {
|
||||
await this.statusBar.clearError();
|
||||
await this._openTab(this._migrationsListTab);
|
||||
}
|
||||
|
||||
private async _openTab(tab: azdata.Tab): Promise<void> {
|
||||
if (tab.id === this._selectedTabId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._tab.clearItems();
|
||||
this._tab.addItem(tab.content);
|
||||
this._selectedTabId = tab.id;
|
||||
}
|
||||
}
|
||||
@@ -5,793 +5,193 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import * as loc from '../constants/strings';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
|
||||
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
||||
import { filterMigrations } from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
const localize = nls.loadMessageBundle();
|
||||
import { DashboardTab } from './dashboardTab';
|
||||
import { MigrationsTab, MigrationsTabId } from './migrationsTab';
|
||||
import { AdsMigrationStatus } from './tabBase';
|
||||
|
||||
interface IActionMetadata {
|
||||
title?: string,
|
||||
description?: string,
|
||||
link?: string,
|
||||
iconPath?: azdata.ThemedIconPath,
|
||||
command?: string;
|
||||
export interface DashboardStatusBar {
|
||||
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>;
|
||||
clearError: () => Promise<void>;
|
||||
errorTitle: string;
|
||||
errorLabel: string;
|
||||
errorDescription: string;
|
||||
}
|
||||
|
||||
const maxWidth = 800;
|
||||
const BUTTON_CSS = {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0',
|
||||
'text-align': 'left',
|
||||
};
|
||||
|
||||
interface StatusCard {
|
||||
container: azdata.DivContainer;
|
||||
count: azdata.TextComponent,
|
||||
textContainer?: azdata.FlexContainer,
|
||||
warningContainer?: azdata.FlexContainer,
|
||||
warningText?: azdata.TextComponent,
|
||||
}
|
||||
|
||||
export class DashboardWidget {
|
||||
export class DashboardWidget implements DashboardStatusBar {
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
||||
private _view!: azdata.ModelView;
|
||||
private _inProgressMigrationButton!: StatusCard;
|
||||
private _inProgressWarningMigrationButton!: StatusCard;
|
||||
private _allMigrationButton!: StatusCard;
|
||||
private _successfulMigrationButton!: StatusCard;
|
||||
private _failedMigrationButton!: StatusCard;
|
||||
private _completingMigrationButton!: StatusCard;
|
||||
private _selectServiceText!: azdata.TextComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
|
||||
private _tabs!: azdata.TabbedPanelComponent;
|
||||
private _statusInfoBox!: azdata.InfoBoxComponent;
|
||||
private _dashboardTab!: DashboardTab;
|
||||
private _migrationsTab!: MigrationsTab;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private isRefreshing: boolean = false;
|
||||
|
||||
public onDialogClosed = async (): Promise<void> => {
|
||||
const label = await getSelectedServiceStatus();
|
||||
this._serviceContextButton.label = label;
|
||||
this._serviceContextButton.title = label;
|
||||
await this.refreshMigrations();
|
||||
};
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
public errorTitle: string = '';
|
||||
public errorLabel: string = '';
|
||||
public errorDescription: string = '';
|
||||
|
||||
public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise<void> {
|
||||
this.errorTitle = errorTitle;
|
||||
this.errorLabel = errorLabel;
|
||||
this.errorDescription = errorDescription;
|
||||
this._statusInfoBox.style = 'error';
|
||||
this._statusInfoBox.text = errorTitle;
|
||||
await this._updateStatusDisplay(this._statusInfoBox, true);
|
||||
}
|
||||
|
||||
public async clearError(): Promise<void> {
|
||||
await this._updateStatusDisplay(this._statusInfoBox, false);
|
||||
this.errorTitle = '';
|
||||
this.errorLabel = '';
|
||||
this.errorDescription = '';
|
||||
this._statusInfoBox.style = 'success';
|
||||
this._statusInfoBox.text = '';
|
||||
}
|
||||
|
||||
public register(): void {
|
||||
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
|
||||
azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
|
||||
this._view = view;
|
||||
|
||||
const container = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
const header = this.createHeader(view);
|
||||
// Files need to have the vscode-file scheme to be loaded by ADS
|
||||
const watermarkUri = vscode.Uri
|
||||
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
|
||||
.with({ scheme: 'vscode-file' });
|
||||
|
||||
container.addItem(header, {
|
||||
CSSStyles: {
|
||||
'background-image': `
|
||||
url(${watermarkUri}),
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)
|
||||
`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': '91.06% 100%',
|
||||
'margin-bottom': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
const tasksContainer = await this.createTasks(view);
|
||||
header.addItem(tasksContainer, {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'margin': '24px'
|
||||
}
|
||||
});
|
||||
container.addItem(await this.createFooter(view), {
|
||||
CSSStyles: {
|
||||
'margin': '0 24px'
|
||||
}
|
||||
});
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(container);
|
||||
await this.refreshMigrations();
|
||||
const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => {
|
||||
this._tabs.selectTab(MigrationsTabId);
|
||||
await this._migrationsTab.setMigrationFilter(filter);
|
||||
};
|
||||
|
||||
this._dashboardTab = await new DashboardTab().create(
|
||||
view,
|
||||
async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
|
||||
this);
|
||||
this._disposables.push(this._dashboardTab);
|
||||
|
||||
this._migrationsTab = await new MigrationsTab().create(
|
||||
this._context,
|
||||
view,
|
||||
this);
|
||||
this._disposables.push(this._migrationsTab);
|
||||
|
||||
this._tabs = view.modelBuilder.tabbedPanel()
|
||||
.withTabs([this._dashboardTab, this._migrationsTab])
|
||||
.withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px',
|
||||
'padding': '0px',
|
||||
'width': '100%'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._tabs.onTabChanged(
|
||||
async id => {
|
||||
await this.clearError();
|
||||
await this.onDialogClosed();
|
||||
}));
|
||||
|
||||
this._statusInfoBox = view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'error',
|
||||
text: '',
|
||||
announceText: true,
|
||||
isClickable: true,
|
||||
display: 'none',
|
||||
CSSStyles: { 'font-size': '14px' },
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._statusInfoBox.onDidClick(
|
||||
async e => await this.openErrorDialog()));
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([this._statusInfoBox, this._tabs])
|
||||
.component();
|
||||
await view.initializeModel(flexContainer);
|
||||
|
||||
await this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
private createHeader(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const header = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.DASHBOARD_TITLE,
|
||||
width: '750px',
|
||||
CSSStyles: {
|
||||
...styles.DASHBOARD_TITLE_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
const descriptionComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.DASHBOARD_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS
|
||||
}
|
||||
}).component();
|
||||
header.addItems([titleComponent, descriptionComponent], {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'padding-left': '24px'
|
||||
}
|
||||
});
|
||||
return header;
|
||||
public async refresh(): Promise<void> {
|
||||
void this._migrationsTab.refresh();
|
||||
await this._dashboardTab.refresh();
|
||||
}
|
||||
|
||||
private async createTasks(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const tasksContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: '100%',
|
||||
}).component();
|
||||
|
||||
const migrateButtonMetadata: IActionMetadata = {
|
||||
title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
|
||||
description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
|
||||
iconPath: IconPathHelper.sqlMigrationLogo,
|
||||
command: 'sqlmigration.start'
|
||||
};
|
||||
|
||||
const preRequisiteListTitle = view.modelBuilder.text().withProps({
|
||||
value: loc.PRE_REQ_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const migrateButton = this.createTaskButton(view, migrateButtonMetadata);
|
||||
|
||||
const preRequisiteListElement = view.modelBuilder.text().withProps({
|
||||
value: [
|
||||
loc.PRE_REQ_1,
|
||||
loc.PRE_REQ_2,
|
||||
loc.PRE_REQ_3
|
||||
],
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS,
|
||||
'padding-left': '12px',
|
||||
'margin': '-0.5em 0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink().withProps({
|
||||
label: loc.LEARN_MORE,
|
||||
ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
|
||||
url: 'https://aka.ms/azuresqlmigrationextension',
|
||||
}).component();
|
||||
|
||||
const preReqContainer = view.modelBuilder.flexContainer().withItems([
|
||||
preRequisiteListTitle,
|
||||
preRequisiteListElement,
|
||||
preRequisiteLearnMoreLink
|
||||
]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
tasksContainer.addItem(migrateButton, {});
|
||||
tasksContainer.addItems([preReqContainer], {
|
||||
CSSStyles: {
|
||||
'margin-left': '20px'
|
||||
}
|
||||
});
|
||||
return tasksContainer;
|
||||
public async onDialogClosed(): Promise<void> {
|
||||
await this._dashboardTab.onDialogClosed();
|
||||
await this._migrationsTab.onDialogClosed();
|
||||
}
|
||||
|
||||
private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
|
||||
const maxHeight: number = 84;
|
||||
const maxWidth: number = 236;
|
||||
const buttonContainer = view.modelBuilder.button().withProps({
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
description: taskMetaData.description,
|
||||
height: maxHeight,
|
||||
iconHeight: 32,
|
||||
iconPath: taskMetaData.iconPath,
|
||||
iconWidth: 32,
|
||||
label: taskMetaData.title,
|
||||
title: taskMetaData.title,
|
||||
width: maxWidth,
|
||||
CSSStyles: {
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
buttonContainer.onDidClick(async () => {
|
||||
if (taskMetaData.command) {
|
||||
await vscode.commands.executeCommand(taskMetaData.command);
|
||||
}
|
||||
}));
|
||||
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
||||
}
|
||||
private _errorDialogIsOpen: boolean = false;
|
||||
|
||||
public async refreshMigrations(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
protected async openErrorDialog(): Promise<void> {
|
||||
if (this._errorDialogIsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
let migrations: DatabaseMigration[] = [];
|
||||
try {
|
||||
migrations = await getCurrentMigrations();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
||||
void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
|
||||
}
|
||||
const tab = azdata.window.createTab(this.errorTitle);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
|
||||
.component(),
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this.errorDescription,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
rows: 20,
|
||||
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
|
||||
})
|
||||
.component()
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
|
||||
.component();
|
||||
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
for (let i = 0; i < inProgressMigrations.length; i++) {
|
||||
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount += 1;
|
||||
}
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
|
||||
this._inProgressMigrationButton.container.display = 'none';
|
||||
this._inProgressWarningMigrationButton.container.display = '';
|
||||
} else {
|
||||
this._inProgressMigrationButton.container.display = '';
|
||||
this._inProgressWarningMigrationButton.container.display = 'none';
|
||||
}
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
|
||||
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
|
||||
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
|
||||
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
|
||||
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
this.isRefreshing = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
}
|
||||
|
||||
private _updateStatusCard(
|
||||
migrations: DatabaseMigration[],
|
||||
card: StatusCard,
|
||||
status: AdsMigrationStatus,
|
||||
show?: boolean): void {
|
||||
const list = filterMigrations(migrations, status);
|
||||
const count = list?.length || 0;
|
||||
card.container.display = count > 0 || show ? '' : 'none';
|
||||
card.count.value = count.toString();
|
||||
}
|
||||
private createStatusCard(
|
||||
cardIconPath: IconPath,
|
||||
cardTitle: string,
|
||||
hasSubtext: boolean = false
|
||||
): StatusCard {
|
||||
const buttonWidth = '400px';
|
||||
const buttonHeight = hasSubtext ? '70px' : '50px';
|
||||
const statusCard = this._view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'width': buttonWidth,
|
||||
'height': buttonHeight,
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusIcon = this._view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 32,
|
||||
CSSStyles: { 'margin': '0 8px' }
|
||||
}).component();
|
||||
|
||||
const textContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const cardTitleText = this._view.modelBuilder.text()
|
||||
.withProps({ value: cardTitle })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'width': '240px',
|
||||
}
|
||||
}).component();
|
||||
textContainer.addItem(cardTitleText);
|
||||
|
||||
const cardCount = this._view.modelBuilder.text().withProps({
|
||||
value: '0',
|
||||
CSSStyles: {
|
||||
...styles.BIG_NUMBER_CSS,
|
||||
'margin': '0 0 0 8px',
|
||||
'text-align': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
let warningContainer;
|
||||
let warningText;
|
||||
if (hasSubtext) {
|
||||
const warningIcon = this._view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.warning,
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
width: 12,
|
||||
height: 18,
|
||||
}).component();
|
||||
|
||||
const warningDescription = '';
|
||||
warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'padding-left': '8px',
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
this.errorTitle,
|
||||
'errorDialog',
|
||||
450,
|
||||
'flyout');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
|
||||
dialog.okButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
this._disposables.push(
|
||||
dialog.onClosed(async e => {
|
||||
if (e === 'ok') {
|
||||
await this.clearError();
|
||||
}
|
||||
}).component();
|
||||
this._errorDialogIsOpen = false;
|
||||
}));
|
||||
|
||||
warningContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[warningIcon, warningText],
|
||||
{ flex: '0 0 auto' })
|
||||
.withProps({
|
||||
CSSStyles: { 'align-items': 'center' }
|
||||
}).component();
|
||||
|
||||
textContainer.addItem(warningContainer);
|
||||
azdata.window.openDialog(dialog);
|
||||
} catch (error) {
|
||||
this._errorDialogIsOpen = false;
|
||||
}
|
||||
|
||||
statusCard.addItems([
|
||||
statusIcon,
|
||||
textContainer,
|
||||
cardCount,
|
||||
]);
|
||||
|
||||
const compositeButton = this._view.modelBuilder.divContainer()
|
||||
.withItems([statusCard])
|
||||
.withProps({
|
||||
ariaRole: 'button',
|
||||
ariaLabel: loc.SHOW_STATUS,
|
||||
clickable: true,
|
||||
CSSStyles: {
|
||||
'height': buttonHeight,
|
||||
'margin-bottom': '16px',
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
return {
|
||||
container: compositeButton,
|
||||
count: cardCount,
|
||||
textContainer: textContainer,
|
||||
warningContainer: warningContainer,
|
||||
warningText: warningText
|
||||
};
|
||||
}
|
||||
|
||||
private async createFooter(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const footerContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const statusContainer = await this.createMigrationStatusContainer(view);
|
||||
const videoLinksContainer = this.createVideoLinks(view);
|
||||
footerContainer.addItem(statusContainer);
|
||||
footerContainer.addItem(videoLinksContainer, {
|
||||
CSSStyles: {
|
||||
'padding-left': '8px',
|
||||
}
|
||||
});
|
||||
|
||||
return footerContainer;
|
||||
}
|
||||
|
||||
private async createMigrationStatusContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
const statusContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '400px',
|
||||
height: '385px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusContainerTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DATABASE_MIGRATION_STATUS,
|
||||
width: '100%',
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
}).component();
|
||||
|
||||
this._refreshButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.REFRESH,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
CSSStyles: { 'float': 'right' }
|
||||
}).component();
|
||||
|
||||
const statusHeadingContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
statusContainerTitle,
|
||||
this._refreshButton,
|
||||
]).withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexFlow: 'row',
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refreshMigrations();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
|
||||
const buttonContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-iems': 'center',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
|
||||
buttonContainer.addItem(
|
||||
await this.createServiceSelector(this._view));
|
||||
|
||||
this._selectServiceText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SELECT_SERVICE_MESSAGE,
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'margin': '10px',
|
||||
'font-weight': '350',
|
||||
'text-align': 'center',
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withItems([statusHeadingContainer, buttonContainer])
|
||||
.withLayout({ flexFlow: 'column', })
|
||||
.component();
|
||||
|
||||
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '272px',
|
||||
})
|
||||
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
|
||||
.component();
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
|
||||
// in progress
|
||||
this._inProgressMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS);
|
||||
this._disposables.push(
|
||||
this._inProgressMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ONGOING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// in progress warning
|
||||
this._inProgressWarningMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS,
|
||||
true);
|
||||
this._disposables.push(
|
||||
this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ONGOING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressWarningMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// successful
|
||||
this._successfulMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completedMigration,
|
||||
loc.MIGRATION_COMPLETED);
|
||||
this._disposables.push(
|
||||
this._successfulMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.SUCCEEDED,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._successfulMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// completing
|
||||
this._completingMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completingCutover,
|
||||
loc.MIGRATION_CUTOVER_CARD);
|
||||
this._disposables.push(
|
||||
this._completingMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.COMPLETING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._completingMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// failed
|
||||
this._failedMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.error,
|
||||
loc.MIGRATION_FAILED);
|
||||
this._disposables.push(
|
||||
this._failedMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.FAILED,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._failedMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// all migrations
|
||||
this._allMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.view,
|
||||
loc.VIEW_ALL);
|
||||
this._disposables.push(
|
||||
this._allMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ALL,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._allMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
|
||||
.withItem(this._migrationStatusCardsContainer)
|
||||
.component();
|
||||
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
|
||||
statusContainer.addItem(this._selectServiceText, {});
|
||||
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
|
||||
return statusContainer;
|
||||
}
|
||||
|
||||
private async _updateSummaryStatus(): Promise<void> {
|
||||
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
const isContextValid = isServiceContextValid(serviceContext);
|
||||
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
|
||||
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
|
||||
this._refreshButton.enabled = isContextValid;
|
||||
}
|
||||
|
||||
private async createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 375,
|
||||
CSSStyles: { ...BUTTON_CSS },
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._serviceContextButton.onDidClick(async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
return this._serviceContextButton;
|
||||
}
|
||||
|
||||
private createVideoLinks(view: azdata.ModelView): azdata.Component {
|
||||
const linksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '440px',
|
||||
height: '385px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
'overflow': 'scroll',
|
||||
}
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.HELP_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
linksContainer.addItems([titleComponent], {
|
||||
CSSStyles: {
|
||||
'margin-bottom': '16px'
|
||||
}
|
||||
});
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio'),
|
||||
description: localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.'),
|
||||
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
|
||||
},
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)'),
|
||||
description: localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.'),
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
|
||||
},
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)'),
|
||||
description: localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.'),
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
|
||||
},
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides'),
|
||||
description: localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.'),
|
||||
link: 'https://docs.microsoft.com/data-migration/'
|
||||
},
|
||||
];
|
||||
|
||||
linksContainer.addItems(links.map(l => this.createLink(view, l)), {});
|
||||
|
||||
const videoLinks: IActionMetadata[] = [];
|
||||
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
videosContainer.addItems(videoLinks.map(l => this.createVideoLink(view, l)), {});
|
||||
linksContainer.addItem(videosContainer);
|
||||
|
||||
return linksContainer;
|
||||
}
|
||||
|
||||
private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 400;
|
||||
const labelsContainer = view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '12px'
|
||||
}
|
||||
}).component();
|
||||
const linkContainer = view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '4px'
|
||||
}
|
||||
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text().withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS
|
||||
}
|
||||
}).component();
|
||||
const linkComponent = view.modelBuilder.hyperlink().withProps({
|
||||
label: linkMetaData.title!,
|
||||
url: linkMetaData.link!,
|
||||
showLinkIcon: true,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
linkContainer.addItem(linkComponent);
|
||||
labelsContainer.addItems([linkContainer, descriptionComponent]);
|
||||
return labelsContainer;
|
||||
}
|
||||
|
||||
private createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 150;
|
||||
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const video1Container = view.modelBuilder.divContainer().withProps({
|
||||
clickable: true,
|
||||
width: maxWidth,
|
||||
height: '100px'
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text().withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
height: '50px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
video1Container.onDidClick(async () => {
|
||||
if (linkMetaData.link) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
||||
}
|
||||
}));
|
||||
videosContainer.addItem(video1Container, {
|
||||
CSSStyles: {
|
||||
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': 'top',
|
||||
'width': `${maxWidth}px`,
|
||||
'height': '104px',
|
||||
'background-size': `${maxWidth}px 120px`
|
||||
}
|
||||
});
|
||||
videosContainer.addItem(descriptionComponent);
|
||||
return videosContainer;
|
||||
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
|
||||
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
228
extensions/sql-migration/src/dashboard/tabBase.ts
Normal file
228
extensions/sql-migration/src/dashboard/tabBase.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 loc from '../constants/strings';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { EOL } from 'os';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
|
||||
import { util } from 'webpack';
|
||||
|
||||
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
|
||||
export const EmptySettingValue = '-';
|
||||
|
||||
export enum AdsMigrationStatus {
|
||||
ALL = 'all',
|
||||
ONGOING = 'ongoing',
|
||||
SUCCEEDED = 'succeeded',
|
||||
FAILED = 'failed',
|
||||
COMPLETING = 'completing'
|
||||
}
|
||||
|
||||
export const MenuCommands = {
|
||||
Cutover: 'sqlmigration.cutover',
|
||||
ViewDatabase: 'sqlmigration.view.database',
|
||||
ViewTarget: 'sqlmigration.view.target',
|
||||
ViewService: 'sqlmigration.view.service',
|
||||
CopyMigration: 'sqlmigration.copy.migration',
|
||||
CancelMigration: 'sqlmigration.cancel.migration',
|
||||
RetryMigration: 'sqlmigration.retry.migration',
|
||||
StartMigration: 'sqlmigration.start',
|
||||
IssueReporter: 'workbench.action.openIssueReporter',
|
||||
};
|
||||
|
||||
export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
public content!: azdata.Component;
|
||||
public title: string = '';
|
||||
public id!: string;
|
||||
public icon!: azdata.IconPath | undefined;
|
||||
|
||||
protected context!: vscode.ExtensionContext;
|
||||
protected view!: azdata.ModelView;
|
||||
protected disposables: vscode.Disposable[] = [];
|
||||
protected isRefreshing: boolean = false;
|
||||
protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise<void>;
|
||||
protected statusBar!: DashboardStatusBar;
|
||||
|
||||
protected abstract initialize(view: azdata.ModelView): Promise<void>;
|
||||
|
||||
public abstract refresh(): Promise<void>;
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}
|
||||
|
||||
protected numberCompare(number1: number | undefined, number2: number | undefined, sortDir: number): number {
|
||||
if (!number1) {
|
||||
return sortDir;
|
||||
} else if (!number2) {
|
||||
return -sortDir;
|
||||
}
|
||||
return util.comparators.compareNumbers(number1, number2) * -sortDir;
|
||||
}
|
||||
|
||||
protected stringCompare(string1: string | undefined, string2: string | undefined, sortDir: number): number {
|
||||
if (!string1) {
|
||||
return sortDir;
|
||||
} else if (!string2) {
|
||||
return -sortDir;
|
||||
}
|
||||
return string1.localeCompare(string2) * -sortDir;
|
||||
}
|
||||
|
||||
protected dateCompare(stringDate1: string | undefined, stringDate2: string | undefined, sortDir: number): number {
|
||||
if (!stringDate1) {
|
||||
return sortDir;
|
||||
} else if (!stringDate2) {
|
||||
return -sortDir;
|
||||
}
|
||||
return new Date(stringDate1) > new Date(stringDate2) ? -sortDir : sortDir;
|
||||
}
|
||||
|
||||
protected async updateServiceContext(button: azdata.ButtonComponent): Promise<void> {
|
||||
const label = await getSelectedServiceStatus();
|
||||
if (button.label !== label ||
|
||||
button.title !== label) {
|
||||
|
||||
button.label = label;
|
||||
button.title = label;
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected createNewMigrationButton(): azdata.ButtonComponent {
|
||||
const newMigrationButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_MIGRATION_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_MIGRATION_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.addNew,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
newMigrationButton.onDidClick(async () => {
|
||||
const actionId = MenuCommands.StartMigration;
|
||||
const args = {
|
||||
extensionId: SqlMigrationExtensionId,
|
||||
issueTitle: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}));
|
||||
return newMigrationButton;
|
||||
}
|
||||
|
||||
protected createNewSupportRequestButton(): azdata.ButtonComponent {
|
||||
const newSupportRequestButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_SUPPORT_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_SUPPORT_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.newSupportRequest,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
newSupportRequestButton.onDidClick(async () => {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
|
||||
}));
|
||||
return newSupportRequestButton;
|
||||
}
|
||||
|
||||
protected createFeedbackButton(): azdata.ButtonComponent {
|
||||
const feedbackButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_FEEDBACK_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_FEEDBACK_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.sendFeedback,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
feedbackButton.onDidClick(async () => {
|
||||
const actionId = MenuCommands.IssueReporter;
|
||||
const args = {
|
||||
extensionId: SqlMigrationExtensionId,
|
||||
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}));
|
||||
return feedbackButton;
|
||||
}
|
||||
|
||||
protected getMigrationErrors(migration: DatabaseMigration): string {
|
||||
const errors = [];
|
||||
errors.push(migration.properties.provisioningError);
|
||||
errors.push(migration.properties.migrationFailureError?.message);
|
||||
errors.push(...migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason);
|
||||
|
||||
// remove undefined and duplicate error entries
|
||||
return errors
|
||||
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
|
||||
.join(EOL);
|
||||
}
|
||||
|
||||
protected showDialogMessage(
|
||||
title: string,
|
||||
statusMessage: string,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
const tab = azdata.window.createTab(title);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: statusMessage })
|
||||
.component(),
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 15px' } })
|
||||
.component();
|
||||
|
||||
if (errorMessage.length > 0) {
|
||||
flex.addItem(
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: errorMessage,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
height: 100,
|
||||
CSSStyles: { 'overflow': 'hidden auto' },
|
||||
})
|
||||
.component()
|
||||
);
|
||||
}
|
||||
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
title,
|
||||
'messageDialog',
|
||||
450,
|
||||
'normal');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.hidden = true;
|
||||
dialog.cancelButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
|
||||
azdata.window.openDialog(dialog);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user