Compare commits

...

17 Commits

Author SHA1 Message Date
brian-harris
6783aa6967 improve account and tenant selection error handling (#17476) (#17489)
* improve account/tenant selection error handling

* remove extra space from user string
2021-10-26 09:39:05 -07:00
brian-harris
ccbc2f74fe apply unique filter to getLocations api's (#17454) (#17473)
* apply unique filter to getLocations api's

* filter resource locations to distinct list

* simplify location filter
2021-10-26 09:38:38 -07:00
Daniel Grajeda
d7283a6e56 Notebook Views initialization fix (#17109) (#17471)
Separate the Views load from the initialization. This way we can load previously created views, and only add the new views data to the document when needed. For now, this happens only when a view is created.
2021-10-22 15:29:22 -07:00
Charles Gagnon
0684040d34 Fix backups not restoring in correct editor (#17466) (#17468) 2021-10-22 13:36:29 -07:00
Karl Burtram
625eb00be2 Remove duplicate Getting Started contributions (#17465) (#17470) 2021-10-22 12:55:09 -07:00
brian-harris
c5a27a89f3 Retry sql migration (#17376) (#17448) 2021-10-21 21:14:15 -07:00
Charles Gagnon
b9a7d5e4bd Fix URL protocol for non-insiders builds (#17446) 2021-10-21 17:12:39 -07:00
Alan Ren
f876c00ca1 fix scrolling issue (#17443) (#17449) 2021-10-21 16:25:09 -07:00
Z Chen
83ae789aa0 Warning when .NET 6 SDK is detected (#17422) (#17447)
* Check for max supported version

* Separate dialog for downgrade warning

* Address PR comments

* Use markdown link

* Update warning message
2021-10-21 16:24:14 -07:00
Maddy
6fe4d0a561 add path.posix while reading relative paths (#17326) (#17441)
* path.posix

* add test for nested folders scenario

* update message and remove the redundant check
2021-10-21 14:11:33 -07:00
Benjin Dubishar
2eaec9f41d Use correct string when checking "browse" option (#17432) (#17440)
* Correct browse string match

* Deduping const
2021-10-21 14:10:47 -07:00
rajeshka
2edafe50bb remove trailing line after the cursor (#17431) (#17436)
* remove trailing line after the cursor

* Addressed PR

(cherry picked from commit 914ac2b09d)
2021-10-21 11:38:23 -07:00
rajeshka
24c5686bd6 fixing the svg (#17427) (#17429)
(cherry picked from commit d196588661)
2021-10-20 20:11:04 -07:00
rajeshka
1731aeffbe Fix for Split Cell duplicates cell #17400 (#17417) (#17425)
(cherry picked from commit d251bbd1a1)
2021-10-20 16:28:47 -07:00
Lucy Zhang
b35ff6451a add listener for celltype change (#17414) (#17418) 2021-10-20 14:38:35 -07:00
Vasu Bhog
07aa256f4c Move split cell icon (#17383) (#17392)
* move split cell icon before delete icon
2021-10-19 13:01:57 -07:00
Lucy Zhang
473764de9a use setContent instead of addElement (#17386) (#17388) 2021-10-19 10:28:19 -07:00
46 changed files with 961 additions and 470 deletions

View File

@@ -153,27 +153,27 @@ export async function getLocations(appContext: AppContext, account?: AzureAccoun
result.errors.push(error);
return result;
}
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {
try {
const path = `/subscriptions/${subscription.id}/locations?api-version=2020-01-01`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);
result.locations.push(...response.response.data.value);
result.errors.push(...response.errors);
} catch (err) {
const error = new Error(localize('azure.accounts.getLocations.queryError', "Error fetching locations for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
account.displayInfo.displayName,
account.displayInfo.userId,
subscription.id,
subscription.name,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
try {
const path = `/subscriptions/${subscription.id}/locations?api-version=2020-01-01`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);
result.locations.push(...response.response.data.value);
result.errors.push(...response.errors);
} catch (err) {
const error = new Error(localize('azure.accounts.getLocations.queryError', "Error fetching locations for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
account.displayInfo.displayName,
account.displayInfo.userId,
subscription.id,
subscription.name,
account.properties.tenants[0].id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
}));
result.errors.push(error);
}
return result;
}

View File

@@ -89,26 +89,26 @@ export class BookTocManager implements IBookTocManager {
let toc: JupyterBookSection[] = [];
for (const content of contents) {
try {
const contentStat = (await fs.promises.stat(path.join(directory, content)));
const contentStat = (await fs.promises.stat(path.posix.join(directory, content)));
const parsedFile = path.parse(content);
if (contentStat.isFile() && allowedFileExtensions.includes(parsedFile.ext)) {
let filePath = directory === rootDirectory ? path.posix.join(path.posix.sep, parsedFile.name) : path.posix.join(path.posix.sep, path.relative(rootDirectory, directory), parsedFile.name);
let filePath = directory === rootDirectory ? path.posix.join(path.posix.sep, parsedFile.name) : path.posix.join(path.posix.sep, path.posix.relative(rootDirectory, directory), parsedFile.name);
const section: JupyterBookSection = {
title: parsedFile.name,
file: filePath
};
toc.push(section);
} else if (contentStat.isDirectory()) {
let files = await fs.promises.readdir(path.join(directory, content));
let files = await fs.promises.readdir(path.posix.join(directory, content));
let initFile = this.getInitFile(files);
let filePath = directory === rootDirectory ? path.posix.join(path.posix.sep, parsedFile.name, initFile.name) : path.posix.join(path.posix.sep, path.relative(rootDirectory, directory), parsedFile.name, initFile.name);
let filePath = directory === rootDirectory ? path.posix.join(path.posix.sep, parsedFile.name, initFile.name) : path.posix.join(path.posix.sep, path.posix.relative(rootDirectory, directory), parsedFile.name, initFile.name);
let section: JupyterBookSection = {};
section = {
title: parsedFile.name,
file: filePath,
expand_sections: true,
numbered: false,
sections: await this.createTocFromDir(files, path.join(directory, content), rootDirectory)
sections: await this.createTocFromDir(files, path.posix.join(directory, content), rootDirectory)
};
toc.push(section);
}

View File

@@ -85,6 +85,7 @@ describe('BookTocManagerTests', function () {
let rootFolderPath: string;
let root2FolderPath: string;
const subfolder = 'Subfolder';
const subfolder2 = 'Subfolder2';
afterEach(function (): void {
sinon.restore();
@@ -94,7 +95,7 @@ describe('BookTocManagerTests', function () {
rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
bookFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
root2FolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
notebooks = ['notebook1.ipynb', 'notebook2.ipynb', 'notebook3.ipynb', 'index.md'];
notebooks = ['notebook1.ipynb', 'notebook2.ipynb', 'notebook3.ipynb', 'index.md', 'notebook4.ipynb', 'notebook5.ipynb'];
await fs.mkdir(rootFolderPath);
await fs.writeFile(path.join(rootFolderPath, notebooks[0]), '');
@@ -104,6 +105,8 @@ describe('BookTocManagerTests', function () {
await fs.mkdir(root2FolderPath);
await fs.mkdir(path.join(root2FolderPath, subfolder));
await fs.mkdir(path.join(root2FolderPath, subfolder, subfolder2));
await fs.writeFile(path.join(root2FolderPath, notebooks[0]), '');
await fs.writeFile(path.join(root2FolderPath, subfolder, notebooks[1]), '');
await fs.writeFile(path.join(root2FolderPath, subfolder, notebooks[2]), '');
@@ -145,6 +148,51 @@ describe('BookTocManagerTests', function () {
should(bookTocManager.tableofContents.length).be.equal(4);
should(listFiles.length).be.equal(7);
});
it ('should create a table of contents with sections if folder contains subfolders', async () => {
await fs.writeFile(path.join(root2FolderPath, subfolder, subfolder2, notebooks[4]), '');
await fs.writeFile(path.join(root2FolderPath, subfolder, subfolder2, notebooks[5]), '');
let bookTocManager: BookTocManager = new BookTocManager();
await bookTocManager.createBook(bookFolderPath, root2FolderPath);
let listFiles = await fs.promises.readdir(bookFolderPath);
should(bookTocManager.tableofContents.length).be.equal(3);
should(listFiles.length).be.equal(5);
let expectedSubSections: IJupyterBookSectionV2[] = [{
title: 'notebook4',
file: path.posix.join(path.posix.sep, subfolder, subfolder2, 'notebook4')
},
{
title: 'notebook5',
file: path.posix.join(path.posix.sep, subfolder, subfolder2, 'notebook5')
}];
let expectedSection: IJupyterBookSectionV2[] = [{
title: 'index',
file: path.posix.join(path.posix.sep, subfolder, 'index')
},
{
title: 'notebook2',
file: path.posix.join(path.posix.sep, subfolder, 'notebook2')
},
{
title: 'notebook3',
file: path.posix.join(path.posix.sep, subfolder, 'notebook3')
},
{
title: 'Subfolder2',
file: path.posix.join(path.posix.sep, subfolder, subfolder2, 'notebook4'),
sections : expectedSubSections
}];
const index = bookTocManager.tableofContents.findIndex(entry => entry.file === path.posix.join(path.posix.sep, subfolder, 'index'));
should(index).not.be.equal(-1, 'Should find a section with the Subfolder entry');
if (index !== -1) {
let subsection = bookTocManager.tableofContents[index].sections.find(entry => entry.file === path.posix.join(path.posix.sep, subfolder, subfolder2, 'notebook4'));
should(equalSections(subsection, expectedSection[3])).be.true('Should find a subsection with the subfolder2 inside the subfolder');
}
});
});
describe('EditingBooks', () => {

View File

@@ -45,6 +45,10 @@
"type": "boolean",
"description": "%sqlDatabaseProjects.netCoreDoNotAsk%"
},
"sqlDatabaseProjects.netCoreDowngradeDoNotShow": {
"type": "boolean",
"description": "%sqlDatabaseProjects.netCoreDowngradeDoNotShow%"
},
"sqlDatabaseProjects.nodejsDoNotAsk": {
"type": "boolean",
"description": "%sqlDatabaseProjects.nodejsDoNotAsk%"

View File

@@ -35,6 +35,7 @@
"sqlDatabaseProjects.Settings": "Database Projects",
"sqlDatabaseProjects.netCoreInstallLocation": "Full path to .NET Core SDK on the machine.",
"sqlDatabaseProjects.netCoreDoNotAsk": "Whether to prompt the user to install .NET Core when not detected.",
"sqlDatabaseProjects.netCoreDowngradeDoNotShow": "Whether to prompt the user to set .NET SDK version when a newer unsupported version is detected.",
"sqlDatabaseProjects.nodejsDoNotAsk": "Whether to prompt the user to install Node.js when not detected.",
"sqlDatabaseProjects.autorestSqlVersion": "Which version of Autorest.Sql to use from NPM. Latest will be used if not set.",
"sqlDatabaseProjects.welcome": "No database projects currently open.\n[New Project](command:sqlDatabaseProjects.new)\n[Open Project](command:sqlDatabaseProjects.open)\n[Create Project From Database](command:sqlDatabaseProjects.importDatabase)",

View File

@@ -115,8 +115,7 @@ export const defaultUser = localize('default', "default");
export const selectProfileToUse = localize('selectProfileToUse', "Select publish profile to load");
export const selectProfile = localize('selectProfile', "Select Profile");
export const dontUseProfile = localize('dontUseProfile', "Don't use profile");
export const browseForProfile = localize('browseForProfile', "Browse for profile");
export const browseForProfileWithIcon = `$(folder) ${browseForProfile}`;
export const browseForProfileWithIcon = `$(folder) ${localize('browseForProfile', "Browse for profile")}`;
export const chooseAction = localize('chooseAction', "Choose action");
export const chooseSqlcmdVarsToModify = localize('chooseSqlcmdVarsToModify', "Choose SQLCMD variables to modify");
export const enterNewValueForVar = (varName: string) => localize('enterNewValueForVar', "Enter new value for variable '{0}'", varName);
@@ -315,12 +314,14 @@ export const postDeployScriptFriendlyName = localize('postDeployScriptFriendlyNa
export const NetCoreInstallationConfirmation: string = localize('sqlDatabaseProjects.NetCoreInstallationConfirmation', "The .NET Core SDK cannot be located. Project build will not work. Please install .NET Core SDK version 3.1 or update the .NET Core SDK location in settings if already installed.");
export function NetCoreSupportedVersionInstallationConfirmation(installedVersion: string) { return localize('sqlDatabaseProjects.NetCoreSupportedVersionInstallationConfirmation', "Currently installed .NET Core SDK version is {0}, which is not supported. Project build will not work. Please install .NET Core SDK version 3.1 or update the .NET Core SDK supported version location in settings if already installed.", installedVersion); }
export function NetCoreVersionDowngradeConfirmation(installedVersion: string) { return localize('sqlDatabaseProjects.NetCoreVersionDowngradeConfirmation', "Installed .NET SDK version {0} is newer than the currently supported versions. Project build will not work. Please install .NET Core SDK version 3.1 and include a global.json in the project folder specifying the SDK version to use. [More Information](https://docs.microsoft.com/dotnet/core/versions/selection)", installedVersion); }
export const UpdateNetCoreLocation: string = localize('sqlDatabaseProjects.UpdateNetCoreLocation', "Update Location");
export const projectsOutputChannel = localize('sqlDatabaseProjects.outputChannel', "Database Projects");
// Prompt buttons
export const Install: string = localize('sqlDatabaseProjects.Install', "Install");
export const DoNotAskAgain: string = localize('sqlDatabaseProjects.doNotAskAgain', "Don't Ask Again");
export const DoNotShowAgain: string = localize('sqlDatabaseProjects.doNotShowAgain', "Don't Show Again");
// SqlProj file XML names
export const ItemGroup = 'ItemGroup';

View File

@@ -31,7 +31,7 @@ export async function getPublishDatabaseSettings(project: Project, promptForConn
reject();
});
quickPick.onDidChangeSelection(async items => {
if (items[0].label === constants.browseForProfile) {
if (items[0].label === constants.browseForProfileWithIcon) {
const locations = await promptForPublishProfile(project.projectFolderPath);
if (!locations) {
// Clear items so that this event will trigger again if they select the same item

View File

@@ -11,7 +11,7 @@ import * as semver from 'semver';
import { isNullOrUndefined } from 'util';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DoNotAskAgain, Install, NetCoreInstallationConfirmation, NetCoreSupportedVersionInstallationConfirmation, UpdateNetCoreLocation } from '../common/constants';
import { DoNotAskAgain, DoNotShowAgain, Install, NetCoreInstallationConfirmation, NetCoreSupportedVersionInstallationConfirmation, NetCoreVersionDowngradeConfirmation, UpdateNetCoreLocation } from '../common/constants';
import * as utils from '../common/utils';
import { ShellCommandOptions, ShellExecutionHelper } from './shellExecutionHelper';
const localize = nls.loadMessageBundle();
@@ -19,16 +19,19 @@ const localize = nls.loadMessageBundle();
export const DBProjectConfigurationKey: string = 'sqlDatabaseProjects';
export const NetCoreInstallLocationKey: string = 'netCoreSDKLocation';
export const NetCoreDoNotAskAgainKey: string = 'netCoreDoNotAsk';
export const NetCoreDowngradeDoNotShowAgainKey: string = 'netCoreDowngradeDoNotShow';
export const NetCoreNonWindowsDefaultPath = '/usr/local/share';
export const winPlatform: string = 'win32';
export const macPlatform: string = 'darwin';
export const linuxPlatform: string = 'linux';
export const minSupportedNetCoreVersion: string = '3.1.0';
export const maxSupportedNetCoreVersionCutoff: string = '6.0.0'; // un-set this to allow latest
export const enum netCoreInstallState {
netCoreNotPresent,
netCoreVersionNotSupported,
netCoreVersionSupported
netCoreVersionSupported,
netCoreVersionTooHigh
}
const dotnet = os.platform() === 'win32' ? 'dotnet.exe' : 'dotnet';
@@ -46,8 +49,10 @@ export class NetCoreTool extends ShellExecutionHelper {
*/
public async findOrInstallNetCore(): Promise<boolean> {
if ((!this.isNetCoreInstallationPresent || !await this.isNetCoreVersionSupported())) {
if (vscode.workspace.getConfiguration(DBProjectConfigurationKey)[NetCoreDoNotAskAgainKey] !== true) {
if (this.netCoreInstallState === netCoreInstallState.netCoreVersionSupported && vscode.workspace.getConfiguration(DBProjectConfigurationKey)[NetCoreDoNotAskAgainKey] !== true) {
void this.showInstallDialog(); // Removing await so that Build and extension load process doesn't wait on user input
} else if (this.netCoreInstallState === netCoreInstallState.netCoreVersionTooHigh && vscode.workspace.getConfiguration(DBProjectConfigurationKey)[NetCoreDowngradeDoNotShowAgainKey] !== true) {
void this.showDowngradeDialog();
}
return false;
}
@@ -80,6 +85,15 @@ export class NetCoreTool extends ShellExecutionHelper {
}
}
public async showDowngradeDialog(): Promise<void> {
const result = await vscode.window.showErrorMessage(NetCoreVersionDowngradeConfirmation(this.netCoreSdkInstalledVersion!), DoNotShowAgain);
if (result === DoNotShowAgain) {
const config = vscode.workspace.getConfiguration(DBProjectConfigurationKey);
await config.update(NetCoreDowngradeDoNotShowAgainKey, true, vscode.ConfigurationTarget.Global);
}
}
private get isNetCoreInstallationPresent(): boolean {
const netCoreInstallationPresent = (!isNullOrUndefined(this.netcoreInstallLocation) && fs.existsSync(this.netcoreInstallLocation));
if (!netCoreInstallationPresent) {
@@ -122,8 +136,8 @@ export class NetCoreTool extends ShellExecutionHelper {
}
/**
* This function checks if the installed dotnet version is atleast minSupportedNetCoreVersion.
* Versions lower than minSupportedNetCoreVersion aren't supported for building projects.
* This function checks if the installed dotnet version is between minSupportedNetCoreVersion (inclusive) and maxSupportedNetCoreVersionCutoff (exclusive).
* When maxSupportedNetCoreVersionCutoff is not set, the latest dotnet version is assumed to be supported and only the min version is checked.
* Returns: True if installed dotnet version is supported, false otherwise.
* Undefined if dotnet isn't installed.
*/
@@ -131,7 +145,7 @@ export class NetCoreTool extends ShellExecutionHelper {
try {
const spawn = child_process.spawn;
let child: child_process.ChildProcessWithoutNullStreams;
let isSupported: boolean | undefined = undefined;
let installState: netCoreInstallState = netCoreInstallState.netCoreVersionSupported;
const stdoutBuffers: Buffer[] = [];
child = spawn('dotnet --version', [], {
@@ -145,10 +159,21 @@ export class NetCoreTool extends ShellExecutionHelper {
this.netCoreSdkInstalledVersion = Buffer.concat(stdoutBuffers).toString('utf8').trim();
try {
if (semver.gte(this.netCoreSdkInstalledVersion, minSupportedNetCoreVersion)) { // Net core version greater than or equal to minSupportedNetCoreVersion are supported for Build
isSupported = true;
// minSupportedDotnetVersion <= supported version < maxSupportedDotnetVersion
if (semver.gte(this.netCoreSdkInstalledVersion, minSupportedNetCoreVersion)) {
// If maxSupportedNetCoreVersionCutoff is not set, the latest .NET version is allowed
if (maxSupportedNetCoreVersionCutoff) {
if (semver.lt(this.netCoreSdkInstalledVersion, maxSupportedNetCoreVersionCutoff)) {
installState = netCoreInstallState.netCoreVersionSupported;
} else {
installState = netCoreInstallState.netCoreVersionTooHigh;
}
} else {
installState = netCoreInstallState.netCoreVersionSupported;
}
} else {
isSupported = false;
// .NET version is too low
installState = netCoreInstallState.netCoreVersionNotSupported;
}
resolve({ stdout: this.netCoreSdkInstalledVersion });
} catch (err) {
@@ -163,13 +188,8 @@ export class NetCoreTool extends ShellExecutionHelper {
});
});
if (isSupported) {
this.netCoreInstallState = netCoreInstallState.netCoreVersionSupported;
} else {
this.netCoreInstallState = netCoreInstallState.netCoreVersionNotSupported;
}
return isSupported;
this.netCoreInstallState = installState;
return installState === netCoreInstallState.netCoreVersionSupported;
} catch (err) {
console.log(err);
this.netCoreInstallState = netCoreInstallState.netCoreNotPresent;
@@ -185,6 +205,8 @@ export class NetCoreTool extends ShellExecutionHelper {
if (!(await this.findOrInstallNetCore())) {
if (this.netCoreInstallState === netCoreInstallState.netCoreNotPresent) {
throw new DotNetError(NetCoreInstallationConfirmation);
} else if (this.netCoreInstallState === netCoreInstallState.netCoreVersionTooHigh && vscode.workspace.getConfiguration(DBProjectConfigurationKey)[NetCoreDowngradeDoNotShowAgainKey] === true) {
// Assume user has used global.json to override SDK version and proceed with build as is
} else {
throw new DotNetError(NetCoreSupportedVersionInstallationConfirmation(this.netCoreSdkInstalledVersion!));
}

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1.00029H14V5.00027H10V4.00027H12.1953C11.9297 3.54194 11.612 3.12788 11.2422 2.75809C10.8724 2.3883 10.4609 2.0732 10.0078 1.81278C9.55469 1.55237 9.07552 1.35185 8.57031 1.21122C8.0651 1.0706 7.54167 1.00029 7 1.00029C6.28125 1.00029 5.59635 1.12008 4.94531 1.35966C4.29427 1.59924 3.70833 1.93518 3.1875 2.36747C2.66667 2.79976 2.22396 3.31278 1.85938 3.90652C1.49479 4.50027 1.24479 5.14871 1.10938 5.85183L0.125 5.65652C0.229167 5.10964 0.393229 4.59142 0.617188 4.10183C0.841146 3.61225 1.11719 3.15913 1.44531 2.74247C1.77344 2.3258 2.14062 1.94559 2.54688 1.60184C2.95312 1.2581 3.39583 0.971639 3.875 0.742474C4.35417 0.513308 4.85417 0.331017 5.375 0.195601C5.89583 0.0601849 6.4375 -0.00491896 7 0.000289351C7.61458 0.000289351 8.21354 0.078414 8.79688 0.234663C9.38021 0.390913 9.92969 0.617474 10.4453 0.914348C10.9609 1.21122 11.4375 1.56799 11.875 1.98466C12.3125 2.40132 12.6875 2.87007 13 3.3909V1.00029ZM7 13.0002C7.71875 13.0002 8.40365 12.8804 9.05469 12.6409C9.70573 12.4013 10.2917 12.0653 10.8125 11.6331C11.3333 11.2008 11.776 10.6903 12.1406 10.1018C12.5052 9.51327 12.7552 8.86483 12.8906 8.1565L13.8672 8.344C13.7109 9.16692 13.4219 9.92212 13 10.6096C12.5781 11.2971 12.0625 11.8935 11.4531 12.3987C10.8438 12.9039 10.1589 13.2971 9.39844 13.5784C8.63802 13.8596 7.83854 14.0002 7 14.0002C6.38021 14.0002 5.77865 13.9221 5.19531 13.7659C4.61198 13.6096 4.0599 13.383 3.53906 13.0862C3.01823 12.7893 2.54427 12.4351 2.11719 12.0237C1.6901 11.6122 1.31771 11.1409 1 10.6096V13.0002H0V9.00025H4V10.0002H1.80469C2.07031 10.4638 2.38802 10.8805 2.75781 11.2502C3.1276 11.62 3.53906 11.9325 3.99219 12.1877C4.44531 12.4429 4.92188 12.6435 5.42188 12.7893C5.92188 12.9351 6.44792 13.0054 7 13.0002Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.1.8",
"version": "0.1.9",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -10,7 +10,7 @@
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
"engines": {
"vscode": "*",
"azdata": ">=1.29.0"
"azdata": ">=1.33.0"
},
"activationEvents": [
"onDashboardOpen",
@@ -81,6 +81,11 @@
"command": "sqlmigration.cancel.migration",
"title": "%cancel-migration-menu%",
"category": "%migration-context-menu-category%"
},
{
"command": "sqlmigration.retry.migration",
"title": "%retry-migration-menu%",
"category": "%migration-context-menu-category%"
}
],
"menu": {
@@ -116,6 +121,10 @@
{
"command": "sqlmigration.cancel.migration",
"when": "false"
},
{
"command": "sqlmigration.retry.migration",
"when": "false"
}
]
},
@@ -174,4 +183,4 @@
"devDependencies": {
"@types/uuid": "^8.3.1"
}
}
}

View File

@@ -14,5 +14,6 @@
"view-target-menu": "Azure SQL Target details",
"view-service-menu": "Database Migration Service details",
"copy-migration-menu": "Copy migration details",
"cancel-migration-menu": "Cancel migration"
"cancel-migration-menu": "Cancel migration",
"retry-migration-menu": "Retry migration"
}

View File

@@ -38,11 +38,11 @@ export async function getLocations(account: azdata.Account, subscription: Subscr
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
sortResourceArrayByName(response.locations);
const filteredLocations = response.locations.filter(loc => {
return sqlMigrationResourceLocations.includes(loc.displayName);
});
const filteredLocations = response.locations
.filter(loc => sqlMigrationResourceLocations.includes(loc.displayName));
sortResourceArrayByName(filteredLocations);
return filteredLocations;
}
@@ -51,7 +51,7 @@ export type AzureProduct = azureResource.AzureGraphResource;
export async function getResourceGroups(account: azdata.Account, subscription: Subscription): Promise<azureResource.AzureResourceResourceGroup[]> {
const api = await getAzureCoreAPI();
const result = await api.getResourceGroups(account, subscription, false);
const result = await api.getResourceGroups(account, subscription, true);
sortResourceArrayByName(result.resourceGroups);
return result.resourceGroups;
}
@@ -362,6 +362,15 @@ export function getFullResourceGroupFromId(id: string): string {
return id.replace(RegExp('/providers/.*'), '').toLowerCase();
}
export function getResourceName(id: string): string {
const splitResourceId = id.split('/');
return splitResourceId[splitResourceId.length - 1];
}
export function getBlobContainerId(resourceGroupId: string, storageAccountName: string, blobContainerName: string): string {
return `${resourceGroupId}/providers/Microsoft.Storage/storageAccounts/${storageAccountName}/blobServices/default/containers/${blobContainerName}`;
}
export interface SqlMigrationServiceProperties {
name: string;
subscriptionId: string;

View File

@@ -3,7 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MigrationContext } from '../models/migrationLocalStorage';
import { MigrationContext, MigrationStatus } from '../models/migrationLocalStorage';
import { MigrationMode, MigrationTargetType } from '../models/stateMachine';
import * as loc from './strings';
export enum SQLTargetAssetType {
@@ -22,6 +23,28 @@ export function getMigrationTargetType(migration: MigrationContext): string {
}
}
export function getMigrationMode(migration: MigrationContext): string {
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.OFFLINE;
export function getMigrationTargetTypeEnum(migration: MigrationContext): MigrationTargetType | undefined {
switch (migration.targetManagedInstance.type) {
case SQLTargetAssetType.SQLMI:
return MigrationTargetType.SQLMI;
case SQLTargetAssetType.SQLVM:
return MigrationTargetType.SQLVM;
default:
return undefined;
}
}
export function getMigrationMode(migration: MigrationContext): string {
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.ONLINE;
}
export function getMigrationModeEnum(migration: MigrationContext): MigrationMode {
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE;
}
export function canRetryMigration(status: string | undefined): boolean {
return status === undefined ||
status === MigrationStatus.Failed ||
status === MigrationStatus.Succeeded ||
status === MigrationStatus.Canceled;
}

View File

@@ -39,6 +39,7 @@ export class IconPathHelper {
public static newSupportRequest: IconPath;
public static emptyTable: IconPath;
public static addAzureAccount: IconPath;
public static retry: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -153,5 +154,9 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/noAzureAccount.svg'),
dark: context.asAbsolutePath('images/noAzureAccount.svg')
};
IconPathHelper.retry = {
light: context.asAbsolutePath('images/retry.svg'),
dark: context.asAbsolutePath('images/retry.svg')
};
}
}

View File

@@ -92,10 +92,19 @@ export function accountLinkedMessage(count: number): string {
}
export const AZURE_TENANT = localize('sql.migration.azure.tenant', "Azure AD tenant");
export function ACCOUNT_STALE_ERROR(account: AzureAccount) {
return localize('azure.accounts.accountStaleError', "The access token for selected account '{0}' is no longer valid. Select 'Link account' and refresh the account, or select a different account.", `${account.displayInfo.displayName} (${account.displayInfo.userId})`);
return localize(
'azure.accounts.accountStaleError',
"The access token for selected account '{0}' and tenant '{1}' is no longer valid. Select 'Link account' and refresh the account, or select a different account.",
`${account?.displayInfo?.displayName} (${account?.displayInfo?.userId})`,
`${account?.properties?.tenants[0]?.displayName} (${account?.properties?.tenants[0]?.userId})`);
}
export function ACCOUNT_ACCESS_ERROR(account: AzureAccount, error: Error) {
return localize('azure.accounts.accountAccessError', "An error occurred while accessing the selected account '{0}'. Select 'Link account' and refresh the account, or select a different account. Error '{1}'", `${account.displayInfo.displayName} (${account.displayInfo.userId})`, error.message);
return localize(
'azure.accounts.accountAccessError',
"An error occurred while accessing the selected account '{0}' and tenant '{1}'. Select 'Link account' and refresh the account, or select a different account. Error '{2}'",
`${account?.displayInfo?.displayName} (${account?.displayInfo?.userId})`,
`${account?.properties?.tenants[0]?.displayName} (${account?.properties?.tenants[0]?.userId})`,
error.message);
}
// database backup page
@@ -537,7 +546,14 @@ export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type',
export const REFRESH_BUTTON_LABEL = localize('sql.migration.status.refresh.label', 'Refresh');
// Saved Assessment Dialog
export const NEXT_LABEL = localize('sql.migration.saved.assessment.next', "Next");
export const CANCEL_LABEL = localize('sql.migration.saved.assessment.cancel', "Cancel");
export const SAVED_ASSESSMENT_RESULT = localize('sql.migration.saved.assessment.result', "Saved assessment result");
// Retry Migration
export const MIGRATION_CANNOT_RETRY = localize('sql.migration.cannot.retry', 'Migration cannot be retried.');
export const RETRY_MIGRATION = localize('sql.migration.retry.migration', "Retry migration");
export const MIGRATION_RETRY_ERROR = localize('sql.migration.retry.migration.error', 'An error occurred while retrying the migration.');
export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')');
export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.');

View File

@@ -33,6 +33,7 @@ interface StatusCard {
}
export class DashboardWidget {
private _context: vscode.ExtensionContext;
private _migrationStatusCardsContainer!: azdata.FlexContainer;
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
@@ -52,7 +53,8 @@ export class DashboardWidget {
private isRefreshing: boolean = false;
constructor() {
constructor(context: vscode.ExtensionContext) {
this._context = context;
}
private async getCurrentMigrations(): Promise<MigrationContext[]> {
@@ -470,7 +472,7 @@ export class DashboardWidget {
this._disposables.push(this._viewAllMigrationsButton.onDidClick(async (e) => {
const migrationStatus = await this.getCurrentMigrations();
new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize();
new MigrationStatusDialog(this._context, migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize();
}));
const refreshButton = view.modelBuilder.hyperlink().withProps({
@@ -596,7 +598,7 @@ export class DashboardWidget {
loc.MIGRATION_IN_PROGRESS
);
this._disposables.push(this._inProgressMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
dialog.initialize();
}));
@@ -610,7 +612,7 @@ export class DashboardWidget {
true
);
this._disposables.push(this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
dialog.initialize();
}));
@@ -623,7 +625,7 @@ export class DashboardWidget {
loc.MIGRATION_COMPLETED
);
this._disposables.push(this._successfulMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED);
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED);
dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
@@ -636,7 +638,7 @@ export class DashboardWidget {
loc.MIGRATION_CUTOVER_CARD
);
this._disposables.push(this._completingMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING);
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING);
dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
@@ -648,7 +650,7 @@ export class DashboardWidget {
loc.MIGRATION_FAILED
);
this._disposables.push(this._failedMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.FAILED);
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.FAILED);
dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
import { MigrationStateModel, MigrationTargetType, Page } from '../../models/stateMachine';
import { SqlDatabaseTree } from './sqlDatabasesTree';
import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
@@ -32,7 +32,7 @@ export class AssessmentResultsDialog {
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string, private _skuRecommendationPage: SKURecommendationPage, private _targetType: MigrationTargetType) {
this._model = model;
if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= 2) {
if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= Page.DatabaseBackup) {
this._model._databaseAssessment = <string[]>this._model.savedInfo.databaseAssessment;
}
this._tree = new SqlDatabaseTree(this._model, this._targetType);

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { SqlMigrationAssessmentResultItem, SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
import { MigrationStateModel, MigrationTargetType, Page } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import { debounce } from '../../api/utils';
import { IconPath, IconPathHelper } from '../../constants/iconPathHelper';
@@ -142,7 +142,7 @@ export class SqlDatabaseTree {
...styles.BOLD_NOTE_CSS,
'margin': '0px 15px 0px 15px'
},
value: constants.DATABASES(0, this._model._databaseAssessment.length)
value: constants.DATABASES(0, this._model._databaseAssessment?.length)
}).component();
return this._databaseCount;
}
@@ -187,10 +187,7 @@ export class SqlDatabaseTree {
).component();
this._disposables.push(this._databaseTable.onDataChanged(async () => {
await this._databaseCount.updateProperties({
'value': constants.DATABASES(this.selectedDbs().length, this._model._databaseAssessment.length)
});
this._model._databaseSelection = <azdata.DeclarativeTableCellValue[][]>this._databaseTable.dataValues;
await this.updateValuesOnSelection();
}));
this._disposables.push(this._databaseTable.onRowSelected(async (e) => {
@@ -200,7 +197,7 @@ export class SqlDatabaseTree {
this._activeIssues = [];
}
this._dbName.value = this._dbNames[e.row];
this._recommendationTitle.value = constants.ISSUES_COUNT(this._activeIssues.length);
this._recommendationTitle.value = constants.ISSUES_COUNT(this._activeIssues?.length);
this._recommendation.value = constants.ISSUES_DETAILS;
await this._resultComponent.updateCssStyles({
'display': 'block'
@@ -307,7 +304,7 @@ export class SqlDatabaseTree {
'display': 'none'
});
this._recommendation.value = constants.WARNINGS_DETAILS;
this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues.length);
this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues?.length);
if (this._targetType === MigrationTargetType.SQLMI) {
await this.refreshResults();
}
@@ -424,7 +421,7 @@ export class SqlDatabaseTree {
}
private handleFailedAssessment(): boolean {
const failedAssessment: boolean = this._model._assessmentResults.assessmentError !== undefined
const failedAssessment: boolean = this._model._assessmentResults?.assessmentError !== undefined
|| (this._model._assessmentResults?.errors?.length || 0) > 0;
if (failedAssessment) {
this._dialog.message = {
@@ -439,12 +436,12 @@ export class SqlDatabaseTree {
private getAssessmentError(): string {
const errors: string[] = [];
const assessmentError = this._model._assessmentResults.assessmentError;
const assessmentError = this._model._assessmentResults?.assessmentError;
if (assessmentError) {
errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`);
}
if (this._model?._assessmentResults?.errors?.length! > 0) {
errors.push(...this._model._assessmentResults.errors?.map(
errors.push(...this._model._assessmentResults?.errors?.map(
e => `message: ${e.message}${EOL}errorSummary: ${e.errorSummary}${EOL}possibleCauses: ${e.possibleCauses}${EOL}guidance: ${e.guidance}${EOL}errorId: ${e.errorId}`)!);
}
@@ -791,7 +788,7 @@ export class SqlDatabaseTree {
public async refreshResults(): Promise<void> {
if (this._targetType === MigrationTargetType.SQLMI) {
if (this._activeIssues.length === 0) {
if (this._activeIssues?.length === 0) {
/// show no issues here
await this._assessmentsTable.updateCssStyles({
'display': 'none',
@@ -858,7 +855,7 @@ export class SqlDatabaseTree {
|| [];
await this._assessmentResultsTable.setDataValues(assessmentResults);
this._assessmentResultsTable.selectedRow = assessmentResults.length > 0 ? 0 : -1;
this._assessmentResultsTable.selectedRow = assessmentResults?.length > 0 ? 0 : -1;
}
public async refreshAssessmentDetails(selectedIssue?: SqlMigrationAssessmentResultItem): Promise<void> {
@@ -872,7 +869,7 @@ export class SqlDatabaseTree {
await this._impactedObjectsTable.setDataValues(this._impactedObjects.map(
(object) => [{ value: object.objectType }, { value: object.name }]));
this._impactedObjectsTable.selectedRow = this._impactedObjects.length > 0 ? 0 : -1;
this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1;
}
public refreshImpactedObject(impactedObject?: SqlMigrationImpactedObjectInfo): void {
@@ -927,17 +924,17 @@ export class SqlDatabaseTree {
style: styleLeft
},
{
value: this._model._assessmentResults.issues.length,
value: this._model._assessmentResults?.issues?.length,
style: styleRight
}
]
];
this._model._assessmentResults.databaseAssessments.sort((db1, db2) => {
return db2.issues.length - db1.issues.length;
this._model._assessmentResults?.databaseAssessments.sort((db1, db2) => {
return db2.issues?.length - db1.issues?.length;
});
// Reset the dbName list so that it is in sync with the table
this._dbNames = this._model._assessmentResults.databaseAssessments.map(da => da.name);
this._model._assessmentResults.databaseAssessments.forEach((db) => {
this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name);
this._model._assessmentResults?.databaseAssessments.forEach((db) => {
let selectable = true;
if (db.issues.find(item => item.databaseRestoreFails)) {
selectable = false;
@@ -954,7 +951,7 @@ export class SqlDatabaseTree {
style: styleLeft
},
{
value: db.issues.length,
value: db.issues?.length,
style: styleRight
}
]
@@ -962,13 +959,27 @@ export class SqlDatabaseTree {
});
}
await this._instanceTable.setDataValues(instanceTableValues);
if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= 2) {
if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= Page.SKURecommendation && this._targetType === this._model.savedInfo.migrationTargetType) {
await this._databaseTable.setDataValues(this._model.savedInfo.migrationDatabases);
} else {
if (this._model.retryMigration && this._targetType === this._model.savedInfo.migrationTargetType) {
const sourceDatabaseName = this._model.savedInfo.databaseList[0];
const sourceDatabaseIndex = this._dbNames.indexOf(sourceDatabaseName);
this._databaseTableValues[sourceDatabaseIndex][0].value = true;
}
await this._databaseTable.setDataValues(this._databaseTableValues);
await this.updateValuesOnSelection();
}
}
private async updateValuesOnSelection() {
await this._databaseCount.updateProperties({
'value': constants.DATABASES(this.selectedDbs()?.length, this._model._databaseAssessment?.length)
});
this._model._databaseSelection = <azdata.DeclarativeTableCellValue[][]>this._databaseTable.dataValues;
}
// undo when bug #16445 is fixed
private createIconTextCell(icon: IconPath, text: string): string {
return text;

View File

@@ -12,15 +12,19 @@ import * as loc from '../../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
import { EOL } from 'os';
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
import * as styles from '../../constants/styles';
import { canRetryMigration } from '../../constants/helper';
const refreshFrequency: SupportedAutoRefreshIntervals = 30000;
const statusImageSize: number = 14;
export class MigrationCutoverDialog {
private _context: vscode.ExtensionContext;
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _model: MigrationCutoverDialogModel;
private _migration: MigrationContext;
private _databaseTitleName!: azdata.TextComponent;
private _cutoverButton!: azdata.ButtonComponent;
@@ -29,6 +33,7 @@ export class MigrationCutoverDialog {
private _refreshLoader!: azdata.LoadingComponent;
private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
private _newSupportRequest!: azdata.ButtonComponent;
private _retryButton!: azdata.ButtonComponent;
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
@@ -53,7 +58,9 @@ export class MigrationCutoverDialog {
readonly _infoFieldWidth: string = '250px';
constructor(migration: MigrationContext) {
constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
this._context = context;
this._migration = migration;
this._model = new MigrationCutoverDialogModel(migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
}
@@ -301,11 +308,11 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: loc.COMPLETE_CUTOVER,
height: '20px',
width: '150px',
width: '140px',
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
'display': this._isOnlineMigration() ? 'inline' : 'none'
'display': this._isOnlineMigration() ? 'block' : 'none'
}
}).component();
@@ -330,7 +337,7 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: loc.CANCEL_MIGRATION,
height: '20px',
width: '150px',
width: '140px',
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
@@ -353,6 +360,28 @@ export class MigrationCutoverDialog {
flex: '0'
});
this._retryButton = this._view.modelBuilder.button().withProps({
label: loc.RETRY_MIGRATION,
iconPath: IconPathHelper.retry,
enabled: false,
iconHeight: '16px',
iconWidth: '16px',
height: '20px',
width: '120px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._retryButton.onDidClick(
async (e) => {
await this.refreshStatus();
let retryMigrationDialog = new RetryMigrationDialog(this._context, this._migration);
await retryMigrationDialog.openDialog();
}
));
headerActions.addItem(this._retryButton, {
flex: '0',
});
this._refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
@@ -360,7 +389,7 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: 'Refresh',
height: '20px',
width: '100px',
width: '80px',
CSSStyles: {
...styles.BODY_CSS,
}
@@ -379,7 +408,7 @@ export class MigrationCutoverDialog {
iconWidth: '16px',
label: loc.COPY_MIGRATION_DETAILS,
height: '20px',
width: '200px',
width: '160px',
CSSStyles: {
...styles.BODY_CSS,
}
@@ -406,7 +435,7 @@ export class MigrationCutoverDialog {
iconHeight: '16px',
iconWidth: '16px',
height: '20px',
width: '180px',
width: '160px',
CSSStyles: {
...styles.BODY_CSS,
}
@@ -567,7 +596,7 @@ export class MigrationCutoverDialog {
if (this._isOnlineMigration()) {
await this._cutoverButton.updateCssStyles({
'display': 'inline'
'display': 'block'
});
}
@@ -720,6 +749,9 @@ export class MigrationCutoverDialog {
this._cancelButton.enabled =
migrationStatusTextValue === MigrationStatus.Creating ||
migrationStatusTextValue === MigrationStatus.InProgress;
this._retryButton.enabled = canRetryMigration(migrationStatusTextValue);
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
console.log(e);

View File

@@ -14,7 +14,8 @@ import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogError
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
import { getMigrationTargetType, getMigrationMode } from '../../constants/helper';
import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
@@ -29,9 +30,11 @@ const MenuCommands = {
ViewService: 'sqlmigration.view.service',
CopyMigration: 'sqlmigration.copy.migration',
CancelMigration: 'sqlmigration.cancel.migration',
RetryMigration: 'sqlmigration.retry.migration',
};
export class MigrationStatusDialog {
private _context: vscode.ExtensionContext;
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -45,7 +48,8 @@ export class MigrationStatusDialog {
private isRefreshing = false;
constructor(migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
constructor(context: vscode.ExtensionContext, migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
this._context = context;
this._model = new MigrationStatusDialogModel(migrations);
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
}
@@ -221,7 +225,7 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const dialog = new MigrationCutoverDialog(migration!);
const dialog = new MigrationCutoverDialog(this._context, migration!);
await dialog.initialize();
} catch (e) {
console.log(e);
@@ -302,6 +306,25 @@ export class MigrationStatusDialog {
console.log(e);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.RetryMigration,
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) {
let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!);
await retryMigrationDialog.openDialog();
}
else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
}
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_RETRY_ERROR, e);
console.log(e);
}
}));
}
private async populateMigrationTable(): Promise<void> {
@@ -366,7 +389,7 @@ export class MigrationStatusDialog {
}).component();
this._disposables.push(databaseHyperLink.onDidClick(
async (e) => await (new MigrationCutoverDialog(migration)).initialize()));
async (e) => await (new MigrationCutoverDialog(this._context, migration)).initialize()));
return this._view.modelBuilder
.flexContainer()
@@ -416,6 +439,10 @@ export class MigrationStatusDialog {
menuCommands.push(MenuCommands.CancelMigration);
}
if (canRetryMigration(migrationStatus)) {
menuCommands.push(MenuCommands.RetryMigration);
}
return menuCommands;
}

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* 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 mssql from '../../../../mssql';
import { azureResource } from 'azureResource';
import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure';
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo, Page } from '../../models/stateMachine';
import { MigrationContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
export class RetryMigrationDialog {
private _context: vscode.ExtensionContext;
private _migration: MigrationContext;
constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
this._context = context;
this._migration = migration;
}
private createMigrationStateModel(migration: MigrationContext, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): MigrationStateModel {
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName;
let savedInfo: SavedInfo;
savedInfo = {
closedPage: Page.AzureAccount,
// AzureAccount
azureAccount: migration.azureAccount,
azureTenant: migration.azureAccount.properties.tenants[0],
// DatabaseSelector
selectedDatabases: [],
// SKURecommendation
databaseAssessment: [],
databaseList: [sourceDatabaseName],
migrationDatabases: [],
serverAssessment: null,
migrationTargetType: getMigrationTargetTypeEnum(migration)!,
subscription: migration.subscription,
location: location,
resourceGroup: {
id: getFullResourceGroupFromId(migration.targetManagedInstance.id),
name: getResourceGroupFromId(migration.targetManagedInstance.id),
subscription: migration.subscription
},
targetServerInstance: migration.targetManagedInstance,
// MigrationMode
migrationMode: getMigrationModeEnum(migration),
// DatabaseBackup
targetSubscription: migration.subscription,
targetDatabaseNames: [migration.migrationContext.name],
networkContainerType: null,
networkShare: null,
blobs: [],
// Integration Runtime
migrationServiceId: migration.migrationContext.properties.migrationService,
};
const getStorageAccountResourceGroup = (storageAccountResourceId: string) => {
return {
id: getFullResourceGroupFromId(storageAccountResourceId!),
name: getResourceGroupFromId(storageAccountResourceId!),
subscription: migration.subscription
};
};
const getStorageAccount = (storageAccountResourceId: string) => {
const storageAccountName = getResourceName(storageAccountResourceId);
return {
type: 'microsoft.storage/storageaccounts',
id: storageAccountResourceId!,
tenantId: savedInfo.azureTenant?.id!,
subscriptionId: migration.subscription.id,
name: storageAccountName,
location: savedInfo.location!.name,
};
};
const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation;
if (sourceLocation?.fileShare) {
savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE;
const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!;
savedInfo.networkShare = {
password: '',
networkShareLocation: sourceLocation?.fileShare?.path!,
windowsUser: sourceLocation?.fileShare?.username!,
storageAccount: getStorageAccount(storageAccountResourceId!),
resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!),
storageKey: ''
};
} else if (sourceLocation?.azureBlob) {
savedInfo.networkContainerType = NetworkContainerType.BLOB_CONTAINER;
const storageAccountResourceId = sourceLocation?.azureBlob?.storageAccountResourceId!;
savedInfo.blobs = [
{
blobContainer: {
id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName),
name: sourceLocation?.azureBlob.blobContainerName,
subscription: migration.subscription
},
lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.migrationContext.properties.offlineConfiguration.lastBackupName! : undefined,
storageAccount: getStorageAccount(storageAccountResourceId!),
resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!),
storageKey: ''
}
];
}
stateModel.retryMigration = true;
stateModel.savedInfo = savedInfo;
stateModel.serverName = serverName;
return stateModel;
}
public async openDialog(dialogName?: string) {
const locations = await getLocations(this._migration.azureAccount, this._migration.subscription);
let location: azureResource.AzureLocation;
locations.forEach(azureLocation => {
if (azureLocation.name === this._migration.targetManagedInstance.location) {
location = azureLocation;
}
});
let activeConnection = await azdata.connection.getCurrentConnection();
let connectionId: string = '';
let serverName: string = '';
if (!activeConnection) {
const connection = await azdata.connection.openConnectionDialog();
if (connection) {
connectionId = connection.connectionId;
serverName = connection.options.server;
}
} else {
connectionId = activeConnection.connectionId;
serverName = activeConnection.serverName;
}
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
const stateModel = this.createMigrationStateModel(this._migration, connectionId, serverName, api, location!);
const wizardController = new WizardController(this._context, stateModel);
await wizardController.openWizard(stateModel.sourceConnectionId);
}
}

View File

@@ -108,11 +108,7 @@ class SQLMigration {
await wizardController.openWizard(connectionId);
}
}
}
}
private checkSavedInfo(serverName: string): SavedInfo | undefined {
@@ -138,7 +134,7 @@ let sqlMigration: SQLMigration;
export async function activate(context: vscode.ExtensionContext) {
sqlMigration = new SQLMigration(context);
await sqlMigration.registerCommands();
let widget = new DashboardWidget();
let widget = new DashboardWidget(context);
widget.register();
}

View File

@@ -71,6 +71,7 @@ export enum Page {
export enum WizardEntryPoint {
Default = 'Default',
SaveAndClose = 'SaveAndClose',
RetryMigration = 'RetryMigration',
}
export interface DatabaseBackupModel {
@@ -188,6 +189,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public refreshDatabaseBackupPage!: boolean;
public _databaseSelection!: azdata.DeclarativeTableCellValue[][];
public retryMigration!: boolean;
public resumeAssessment!: boolean;
public savedInfo!: SavedInfo;
public closedPage!: number;
@@ -293,7 +295,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
private async generateAssessmentTelemetry(): Promise<void> {
try {
let serverIssues = this._assessmentResults.issues.map(i => {
let serverIssues = this._assessmentResults?.issues.map(i => {
return {
ruleId: i.ruleId,
count: i.impactedObjects.length
@@ -337,10 +339,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
'serverErrors': JSON.stringify(serverErrors),
},
{
'issuesCount': this._assessmentResults.issues.length,
'warningsCount': this._assessmentResults.databaseAssessments.reduce((count, d) => count + d.issues.length, 0),
'issuesCount': this._assessmentResults?.issues.length,
'warningsCount': this._assessmentResults?.databaseAssessments.reduce((count, d) => count + d.issues.length, 0),
'durationInMilliseconds': endTime.getTime() - startTime.getTime(),
'databaseCount': this._assessmentResults.databaseAssessments.length,
'databaseCount': this._assessmentResults?.databaseAssessments.length,
'serverHostCpuCount': this._assessmentApiResponse?.assessmentResult?.cpuCoreCount,
'serverHostPhysicalMemoryInBytes': this._assessmentApiResponse?.assessmentResult?.physicalServerMemory,
'serverDatabases': this._assessmentApiResponse?.assessmentResult?.numberOfUserDatabases,
@@ -626,12 +628,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
let managedInstanceValues: azdata.CategoryValue[] = [];
if (!this._azureAccount) {
if (!this._azureAccount || !subscription) {
return managedInstanceValues;
}
try {
this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => {
if (mi.location.toLowerCase() === location.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup?.name.toLowerCase()) {
if (mi.location.toLowerCase() === location?.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup?.name.toLowerCase()) {
return true;
}
return false;
@@ -678,7 +680,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
try {
if (this._azureAccount && subscription && resourceGroup) {
this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => {
if (virtualMachine.location === location.name) {
if (virtualMachine?.location?.toLowerCase() === location?.name?.toLowerCase()) {
if (virtualMachine.properties.sqlImageOffer) {
return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms.
}
@@ -996,6 +998,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
let wizardEntryPoint = WizardEntryPoint.Default;
if (this.resumeAssessment) {
wizardEntryPoint = WizardEntryPoint.SaveAndClose;
} else if (this.retryMigration) {
wizardEntryPoint = WizardEntryPoint.RetryMigration;
}
if (response.status === 201 || response.status === 200) {
sendSqlMigrationActionEvent(

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
import { deepClone, findDropDownItemIndex, selectDropDownIndex } from '../api/utils';
@@ -82,6 +82,7 @@ export class AccountsSelectionPage extends MigrationWizardPage {
}
if (this.migrationStateModel._azureAccount?.isStale) {
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount)
};
return false;
@@ -111,9 +112,9 @@ export class AccountsSelectionPage extends MigrationWizardPage {
await this._accountTenantFlexContainer.updateCssStyles({
'display': 'none'
});
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 0) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.AzureAccount)) {
(<azdata.CategoryValue[]>this._azureAccountsDropdown.values)?.forEach((account, index) => {
if (account.name === this.migrationStateModel.savedInfo.azureAccount?.displayInfo.userId) {
if (account.name.toLowerCase() === this.migrationStateModel.savedInfo.azureAccount?.displayInfo.userId.toLowerCase()) {
selectDropDownIndex(this._azureAccountsDropdown, index);
}
});
@@ -193,8 +194,6 @@ export class AccountsSelectionPage extends MigrationWizardPage {
this.migrationStateModel._targetSubscription = undefined!;
this.migrationStateModel._databaseBackup.subscription = undefined!;
}
const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex);
this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount);
}));
@@ -233,16 +232,24 @@ export class AccountsSelectionPage extends MigrationWizardPage {
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator(async pageChangeInfo => {
try {
if (!this.migrationStateModel._azureAccount?.isStale) {
this.wizard.message = { text: '', };
if (this.migrationStateModel._azureAccount && !this.migrationStateModel._azureAccount?.isStale) {
const subscriptions = await getSubscriptions(this.migrationStateModel._azureAccount);
if (subscriptions?.length > 0) {
return true;
}
}
this.wizard.message = { text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount) };
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount),
};
} catch (error) {
this.wizard.message = { text: constants.ACCOUNT_ACCESS_ERROR(this.migrationStateModel._azureAccount, error) };
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: constants.ACCOUNT_ACCESS_ERROR(this.migrationStateModel._azureAccount, error),
};
}
return false;

View File

@@ -283,7 +283,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
return true;
}).component();
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) {
this._networkSharePath.value = this.migrationStateModel.savedInfo.networkShare?.networkShareLocation;
}
this._disposables.push(this._networkSharePath.onTextChanged(async (value) => {
@@ -331,7 +331,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
return true;
}).component();
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) {
this._windowsUserAccountText.value = this.migrationStateModel.savedInfo.networkShare?.windowsUser;
}
this._disposables.push(this._windowsUserAccountText.onTextChanged((value) => {
@@ -458,7 +458,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return flexContainer;
}
private createTargetDatabaseContainer(): azdata.FlexContainer {
const headerCssStyles: azdata.CssStyles = {
...styles.LABEL_CSS,
@@ -755,253 +754,265 @@ export class DatabaseBackupPage extends MigrationWizardPage {
return container;
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (this.migrationStateModel.refreshDatabaseBackupPage) {
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) {
this.migrationStateModel._migrationDbs = this.migrationStateModel.savedInfo.databaseList;
}
const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE;
const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1;
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration;
this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => {
column.width = isOfflineMigration ? WIZARD_TABLE_COLUMN_WIDTH_SMALL : WIZARD_TABLE_COLUMN_WIDTH;
});
try {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) {
this.migrationStateModel._migrationDbs = this.migrationStateModel.savedInfo.databaseList;
}
const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE;
const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1;
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration;
this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => {
column.width = isOfflineMigration ? WIZARD_TABLE_COLUMN_WIDTH_SMALL : WIZARD_TABLE_COLUMN_WIDTH;
});
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode) {
if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
this._networkShareButton.checked = true;
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) {
if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
this._networkShareButton.checked = true;
} else {
this._networkShareButton.checked = false;
this._networkTableContainer.display = 'none';
await this._networkShareContainer.updateCssStyles({ 'display': 'none' });
}
} else {
this._networkShareButton.checked = false;
this._networkTableContainer.display = 'none';
await this._networkShareContainer.updateCssStyles({ 'display': 'none' });
}
} else {
this._networkShareButton.checked = false;
this._networkTableContainer.display = 'none';
await this._networkShareContainer.updateCssStyles({ 'display': 'none' });
}
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode) {
if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
this._blobContainerButton.checked = true;
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) {
if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
this._blobContainerButton.checked = true;
} else {
this._blobContainerButton.checked = false;
this._blobTableContainer.display = 'none';
await this._blobContainer.updateCssStyles({ 'display': 'none' });
}
} else {
this._blobContainerButton.checked = false;
this._blobTableContainer.display = 'none';
await this._blobContainer.updateCssStyles({ 'display': 'none' });
}
} else {
this._blobContainerButton.checked = false;
this._blobTableContainer.display = 'none';
await this._blobContainer.updateCssStyles({ 'display': 'none' });
}
await this._targetDatabaseContainer.updateCssStyles({ 'display': 'none' });
await this._networkShareStorageAccountDetails.updateCssStyles({ 'display': 'none' });
const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider);
const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
await this._targetDatabaseContainer.updateCssStyles({ 'display': 'none' });
await this._networkShareStorageAccountDetails.updateCssStyles({ 'display': 'none' });
const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider);
const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
this._networkShareTargetDatabaseNames = [];
this._blobContainerTargetDatabaseNames = [];
this._blobContainerResourceGroupDropdowns = [];
this._blobContainerStorageAccountDropdowns = [];
this._blobContainerDropdowns = [];
this._blobContainerLastBackupFileDropdowns = [];
this._networkShareTargetDatabaseNames = [];
this._blobContainerTargetDatabaseNames = [];
this._blobContainerResourceGroupDropdowns = [];
this._blobContainerStorageAccountDropdowns = [];
this._blobContainerDropdowns = [];
this._blobContainerLastBackupFileDropdowns = [];
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
this._existingDatabases = await this.migrationStateModel.getManagedDatabases();
}
this.migrationStateModel._targetDatabaseNames = [];
this.migrationStateModel._databaseBackup.blobs = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
this.migrationStateModel._targetDatabaseNames.push('');
this.migrationStateModel._databaseBackup.blobs.push(<Blob>{});
const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({
required: true,
value: db,
width: WIZARD_TABLE_COLUMN_WIDTH
}).withValidation(c => {
if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
return false;
}
return true;
}).component();
this._disposables.push(targetDatabaseInput.onTextChanged(async (value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
await this.validateFields();
}));
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) {
targetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index];
} else {
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) {
this._existingDatabases = await this.migrationStateModel.getManagedDatabases();
}
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
const blobTargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({
required: true,
value: db,
}).withValidation(c => {
if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
return false;
}
return true;
}).component();
this._disposables.push(blobTargetDatabaseInput.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
}));
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) {
blobTargetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index];
} else {
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
}
this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput);
const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP,
editable: true,
fireOnTextChange: true,
required: true,
}).component();
const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT,
editable: true,
fireOnTextChange: true,
required: true,
enabled: false,
}).component();
const blobContainerDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER,
editable: true,
fireOnTextChange: true,
required: true,
enabled: false,
}).component();
const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
editable: true,
fireOnTextChange: true,
required: true,
enabled: false,
}).component();
this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value);
if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex);
await this.loadBlobStorageDropdown(index);
await blobContainerStorageAccountDropdown.updateProperties({ enabled: true });
} else {
await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP);
}
}));
this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown);
this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value);
if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex);
await this.loadBlobContainerDropdown(index);
await blobContainerDropdown.updateProperties({ enabled: true });
} else {
await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT);
}
}));
this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown);
this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value);
if (selectedIndex > -1 && !blobContainerErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(selectedIndex);
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
await this.loadBlobLastBackupFileDropdown(index);
await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true });
this.migrationStateModel._targetDatabaseNames = [];
this.migrationStateModel._databaseBackup.blobs = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
this.migrationStateModel._targetDatabaseNames.push('');
this.migrationStateModel._databaseBackup.blobs.push(<Blob>{});
const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({
required: true,
value: db,
width: WIZARD_TABLE_COLUMN_WIDTH
}).withValidation(c => {
if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
return false;
}
return true;
}).component();
this._disposables.push(targetDatabaseInput.onTextChanged(async (value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
await this.validateFields();
}));
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) {
targetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index];
} else {
await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER);
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
}
}));
this._blobContainerDropdowns.push(blobContainerDropdown);
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
this._disposables.push(blobContainerLastBackupFileDropdown.onValueChanged(value => {
const selectedIndex = findDropDownItemIndex(blobContainerLastBackupFileDropdown, value);
if (selectedIndex > -1 && !blobFileErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = this.migrationStateModel.getBlobLastBackupFileName(selectedIndex);
const blobTargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({
required: true,
value: db,
}).withValidation(c => {
if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values.
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
return false;
}
if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
return false;
}
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
return false;
}
return true;
}).component();
this._disposables.push(blobTargetDatabaseInput.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
}));
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) {
blobTargetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index];
} else {
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
}
this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput);
const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP,
editable: true,
fireOnTextChange: true,
required: true,
}).component();
const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT,
editable: true,
fireOnTextChange: true,
required: true,
enabled: false,
}).component();
const blobContainerDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER,
editable: true,
fireOnTextChange: true,
required: true,
enabled: false,
}).component();
const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
editable: true,
fireOnTextChange: true,
required: true,
enabled: false,
}).component();
this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value);
if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex);
await this.loadBlobStorageDropdown(index);
await blobContainerStorageAccountDropdown.updateProperties({ enabled: true });
} else {
await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP);
}
}));
this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown);
this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown);
this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value);
if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex);
await this.loadBlobContainerDropdown(index);
await blobContainerDropdown.updateProperties({ enabled: true });
} else {
await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT);
}
}));
this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown);
this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value);
if (selectedIndex > -1 && !blobContainerErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(selectedIndex);
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
await this.loadBlobLastBackupFileDropdown(index);
await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true });
}
} else {
await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER);
}
}));
this._blobContainerDropdowns.push(blobContainerDropdown);
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
this._disposables.push(blobContainerLastBackupFileDropdown.onValueChanged(value => {
const selectedIndex = findDropDownItemIndex(blobContainerLastBackupFileDropdown, value);
if (selectedIndex > -1 && !blobFileErrorStrings.includes(value)) {
this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = this.migrationStateModel.getBlobLastBackupFileName(selectedIndex);
}
}));
this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown);
}
});
let data: azdata.DeclarativeTableCellValue[][] = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
targetRow.push({
value: db
});
targetRow.push({
value: this._networkShareTargetDatabaseNames[index]
});
data.push(targetRow);
});
this._networkShareTargetDatabaseNamesTable.dataValues = data;
data = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
targetRow.push({
value: db
});
targetRow.push({
value: this._blobContainerTargetDatabaseNames[index]
});
targetRow.push({
value: this._blobContainerResourceGroupDropdowns[index]
});
targetRow.push({
value: this._blobContainerStorageAccountDropdowns[index]
});
targetRow.push({
value: this._blobContainerDropdowns[index]
});
targetRow.push({
value: this._blobContainerLastBackupFileDropdowns[index]
});
data.push(targetRow);
});
await this._blobContainerTargetDatabaseNamesTable.setDataValues(data);
await this.getSubscriptionValues();
this.migrationStateModel.refreshDatabaseBackupPage = false;
} catch (error) {
console.log(error);
let errorText = error?.message;
if (errorText === constants.INVALID_OWNER_URI) {
errorText = constants.DATABASE_BACKUP_PAGE_LOAD_ERROR;
}
});
let data: azdata.DeclarativeTableCellValue[][] = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
targetRow.push({
value: db
});
targetRow.push({
value: this._networkShareTargetDatabaseNames[index]
});
data.push(targetRow);
});
this._networkShareTargetDatabaseNamesTable.dataValues = data;
data = [];
this.migrationStateModel._migrationDbs.forEach((db, index) => {
const targetRow: azdata.DeclarativeTableCellValue[] = [];
targetRow.push({
value: db
});
targetRow.push({
value: this._blobContainerTargetDatabaseNames[index]
});
targetRow.push({
value: this._blobContainerResourceGroupDropdowns[index]
});
targetRow.push({
value: this._blobContainerStorageAccountDropdowns[index]
});
targetRow.push({
value: this._blobContainerDropdowns[index]
});
targetRow.push({
value: this._blobContainerLastBackupFileDropdowns[index]
});
data.push(targetRow);
});
await this._blobContainerTargetDatabaseNamesTable.setDataValues(data);
await this.getSubscriptionValues();
this.migrationStateModel.refreshDatabaseBackupPage = false;
this.wizard.message = {
text: errorText,
description: error?.stack,
level: azdata.window.MessageLevel.Error
};
}
}
this.wizard.registerNavigationValidator((pageChangeInfo) => {
@@ -1131,7 +1142,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._blobTableContainer.display = (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none';
//Preserving the database Names between the 2 tables.
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) {
this.migrationStateModel._targetDatabaseNames = this.migrationStateModel.savedInfo.targetDatabaseNames;
}
@@ -1150,7 +1161,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
await this.validateFields();
}
private async validateFields(): Promise<void> {
await this._sqlSourceUsernameInput.validate();
await this._sqlSourcePassword.validate();
@@ -1175,7 +1185,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}
private async getSubscriptionValues(): Promise<void> {
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) {
this.migrationStateModel._targetSubscription = <azureResource.AzureResourceSubscription>this.migrationStateModel.savedInfo.targetSubscription;
this.migrationStateModel._targetServerInstance = <SqlManagedInstance | SqlVMServer>this.migrationStateModel.savedInfo.targetServerInstance;
}
@@ -1196,7 +1206,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
this._networkShareStorageAccountResourceGroupDropdown.loading = true;
try {
this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription);
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._networkShareStorageAccountResourceGroupDropdown.values) {
if (this.hasSavedInfo(NetworkContainerType.NETWORK_SHARE, this._networkShareStorageAccountResourceGroupDropdown.values)) {
this._networkShareStorageAccountResourceGroupDropdown.values.forEach((resource, index) => {
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.networkShare?.resourceGroup?.id?.toLowerCase()) {
selectDropDownIndex(this._networkShareStorageAccountResourceGroupDropdown, index);
@@ -1231,7 +1241,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription);
this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => {
dropDown.values = resourceGroupValues;
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && dropDown.values) {
if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, dropDown.values)) {
dropDown.values.forEach((resource, resourceIndex) => {
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.resourceGroup?.id?.toLowerCase()) {
selectDropDownIndex(dropDown, resourceIndex);
@@ -1251,8 +1261,8 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private async loadBlobStorageDropdown(index: number): Promise<void> {
this._blobContainerStorageAccountDropdowns[index].loading = true;
try {
this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].resourceGroup);
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._blobContainerStorageAccountDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index].storageAccount) {
this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup);
if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, this._blobContainerStorageAccountDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index]?.storageAccount)) {
this._blobContainerStorageAccountDropdowns[index].values!.forEach((resource, resourceIndex) => {
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.storageAccount?.id?.toLowerCase()) {
selectDropDownIndex(this._blobContainerStorageAccountDropdowns[index], resourceIndex);
@@ -1271,9 +1281,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private async loadBlobContainerDropdown(index: number): Promise<void> {
this._blobContainerDropdowns[index].loading = true;
try {
const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount);
const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount);
this._blobContainerDropdowns[index].values = blobContainerValues;
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._blobContainerDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index].blobContainer) {
if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, this._blobContainerDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index]?.blobContainer)) {
this._blobContainerDropdowns[index].values!.forEach((resource, resourceIndex) => {
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.blobContainer?.id?.toLowerCase()) {
selectDropDownIndex(this._blobContainerDropdowns[index], resourceIndex);
@@ -1292,9 +1302,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
private async loadBlobLastBackupFileDropdown(index: number): Promise<void> {
this._blobContainerLastBackupFileDropdowns[index].loading = true;
try {
const blobLastBackupFileValues = await this.migrationStateModel.getBlobLastBackupFileNameValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount, this.migrationStateModel._databaseBackup.blobs[index].blobContainer);
const blobLastBackupFileValues = await this.migrationStateModel.getBlobLastBackupFileNameValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
this._blobContainerLastBackupFileDropdowns[index].values = blobLastBackupFileValues;
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._blobContainerLastBackupFileDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index].lastBackupFile) {
if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, this._blobContainerLastBackupFileDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index]?.lastBackupFile)) {
this._blobContainerLastBackupFileDropdowns[index].values!.forEach((resource, resourceIndex) => {
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.lastBackupFile!.toLowerCase()) {
selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[index], resourceIndex);
@@ -1334,4 +1344,12 @@ export class DatabaseBackupPage extends MigrationWizardPage {
selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0);
await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps);
}
private hasSavedInfo(networkContainerType: NetworkContainerType, values: any): boolean {
if (this.migrationStateModel._databaseBackup.networkContainerType === networkContainerType &&
(this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) && values)) {
return true;
}
return false;
}
}

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { debounce } from '../api/utils';
@@ -275,17 +275,26 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
}
).component();
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 1) {
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseSelector) {
await this._databaseSelectorTable.setDataValues(this.migrationStateModel.savedInfo.selectedDatabases);
} else {
if (this.migrationStateModel.retryMigration) {
const sourceDatabaseName = this.migrationStateModel.savedInfo.databaseList[0];
this._databaseTableValues.forEach((row, index) => {
const dbName = row[1].value as string;
if (dbName?.toLowerCase() === sourceDatabaseName?.toLowerCase()) {
row[0].value = true;
} else {
row[0].enabled = false;
}
});
}
await this._databaseSelectorTable.setDataValues(this._databaseTableValues);
await this.updateValuesOnSelection();
}
this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => {
await this._dbCount.updateProperties({
'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length)
});
this.migrationStateModel._databaseAssessment = this.selectedDbs();
this.migrationStateModel.databaseSelectorTableValues = <azdata.DeclarativeTableCellValue[][]>this._databaseSelectorTable.dataValues;
await this.updateValuesOnSelection();
}));
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
@@ -314,6 +323,14 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
return result;
}
private async updateValuesOnSelection() {
await this._dbCount.updateProperties({
'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length)
});
this.migrationStateModel._databaseAssessment = this.selectedDbs();
this.migrationStateModel.databaseSelectorTableValues = <azdata.DeclarativeTableCellValue[][]>this._databaseSelectorTable.dataValues;
}
// undo when bug #16445 is fixed
private createIconTextCell(icon: IconPath, text: string): string {
return text;

View File

@@ -86,7 +86,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
}
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime)) {
this.migrationStateModel._targetSubscription = <azureResource.AzureResourceSubscription>this.migrationStateModel.savedInfo.targetSubscription;
this.migrationStateModel._targetServerInstance = <SqlManagedInstance | SqlVMServer>this.migrationStateModel.savedInfo.targetServerInstance;
}
@@ -391,7 +391,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this._resourceGroupDropdown.loading = true;
try {
this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription);
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._resourceGroupDropdown.values) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._resourceGroupDropdown.values)) {
this._resourceGroupDropdown.values.forEach((resource, resourceIndex) => {
const resourceId = this.migrationStateModel.savedInfo?.migrationServiceId?.toLowerCase();
if (resourceId && (<azdata.CategoryValue>resource).name.toLowerCase() === getFullResourceGroupFromId(resourceId)) {
@@ -409,8 +409,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
try {
this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, <SqlManagedInstance>this.migrationStateModel._targetServerInstance, resourceGroupName);
const selectedSqlMigrationService = this._dmsDropdown.values.find(v => v.displayName.toLowerCase() === this.migrationStateModel._sqlMigrationService?.name?.toLowerCase());
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._dmsDropdown.values) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._dmsDropdown.values)) {
this._dmsDropdown.values.forEach((resource, resourceIndex) => {
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.migrationServiceId?.toLowerCase()) {
selectDropDownIndex(this._dmsDropdown, resourceIndex);

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationMode, MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import * as styles from '../constants/styles';
@@ -113,7 +113,7 @@ export class MigrationModePage extends MigrationWizardPage {
}
}).component();
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 3) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) {
if (this.migrationStateModel.savedInfo.migrationMode === MigrationMode.ONLINE) {
onlineButton.checked = true;
offlineButton.checked = false;

View File

@@ -197,6 +197,14 @@ export class SKURecommendationPage extends MigrationWizardPage {
}));
await this._view.initializeModel(this._rootContainer);
if (this.hasSavedInfo()) {
if (this.migrationStateModel.savedInfo.migrationTargetType === MigrationTargetType.SQLMI) {
this.migrationStateModel._miDbs = this.migrationStateModel.savedInfo.databaseList;
} else {
this.migrationStateModel._vmDbs = this.migrationStateModel.savedInfo.databaseList;
}
}
}
private createStatusComponent(view: azdata.ModelView): azdata.TextComponent {
@@ -300,7 +308,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
}).component();
let serverName = '';
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.serverName) {
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.serverName)) {
serverName = this.migrationStateModel.serverName;
} else {
serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
@@ -505,7 +513,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._migrationDbs = miDbs;
} else {
this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM;
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) {
if ((this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation)) {
this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel.savedInfo.databaseList.length, this.migrationStateModel._databaseAssessment.length);
} else {
this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(vmDbs.length, this.migrationStateModel._databaseAssessment.length);
@@ -540,12 +548,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this.migrationStateModel.getDatabaseAssessments(MigrationTargetType.SQLMI);
}
const assessmentError = this.migrationStateModel._assessmentResults.assessmentError;
const assessmentError = this.migrationStateModel._assessmentResults?.assessmentError;
if (assessmentError) {
errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`);
}
if (this.migrationStateModel?._assessmentResults?.errors?.length! > 0) {
errors.push(...this.migrationStateModel._assessmentResults.errors?.map(
errors.push(...this.migrationStateModel._assessmentResults?.errors?.map(
e => `message: ${e.message}${EOL}errorSummary: ${e.errorSummary}${EOL}possibleCauses: ${e.possibleCauses}${EOL}guidance: ${e.guidance}${EOL}errorId: ${e.errorId}`)!);
}
@@ -566,11 +574,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
} else {
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults.databaseAssessments.length);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults?.databaseAssessments?.length);
}
}
if ((this.migrationStateModel.resumeAssessment) && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) {
if (this.hasSavedInfo()) {
if (this.migrationStateModel.savedInfo.migrationTargetType) {
this._rbg.selectedCardId = this.migrationStateModel.savedInfo.migrationTargetType;
await this.refreshCardText();
@@ -602,9 +610,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this.assessmentGroupContainer.updateCssStyles({ 'display': display });
this.assessmentGroupContainer.display = display;
display = this._rbg.selectedCardId
display = (this._rbg.selectedCardId
&& (!failedAssessment || this._skipAssessmentCheckbox.checked)
&& this.migrationStateModel._migrationDbs.length > 0
&& this.migrationStateModel._migrationDbs.length > 0)
? 'inline'
: 'none';
await this._targetContainer.updateCssStyles({ 'display': display });
@@ -614,7 +622,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private async populateSubscriptionDropdown(): Promise<void> {
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) {
if (this.hasSavedInfo()) {
this.migrationStateModel._azureAccount = <azdata.Account>this.migrationStateModel.savedInfo.azureAccount;
}
if (!this.migrationStateModel._targetSubscription) {
@@ -628,9 +636,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._managedInstanceSubscriptionDropdown.loading = false;
this._resourceDropdown.loading = false;
}
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._managedInstanceSubscriptionDropdown.values) {
this._managedInstanceSubscriptionDropdown.values.forEach((subscription, index) => {
if ((<azdata.CategoryValue>subscription).name === this.migrationStateModel.savedInfo?.subscription?.id) {
if (this.hasSavedInfo() && this._managedInstanceSubscriptionDropdown.values) {
this._managedInstanceSubscriptionDropdown.values!.forEach((subscription, index) => {
if ((<azdata.CategoryValue>subscription).name.toLowerCase() === this.migrationStateModel.savedInfo?.subscription?.id.toLowerCase()) {
selectDropDownIndex(this._managedInstanceSubscriptionDropdown, index);
}
});
@@ -645,9 +653,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._azureLocationDropdown.loading = true;
try {
this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription);
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._azureResourceGroupDropdown.values) {
if (this.hasSavedInfo() && this._azureResourceGroupDropdown.values) {
this._azureResourceGroupDropdown.values.forEach((resourceGroup, index) => {
if (resourceGroup.name === this.migrationStateModel.savedInfo?.resourceGroup?.id) {
if (resourceGroup.name.toLowerCase() === this.migrationStateModel.savedInfo?.resourceGroup?.id.toLowerCase()) {
selectDropDownIndex(this._azureResourceGroupDropdown, index);
}
});
@@ -655,7 +663,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
selectDropDownIndex(this._azureResourceGroupDropdown, 0);
}
this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription);
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._azureLocationDropdown.values) {
if (this.hasSavedInfo() && this._azureLocationDropdown.values) {
this._azureLocationDropdown.values.forEach((location, index) => {
if (location.displayName === this.migrationStateModel.savedInfo?.location?.displayName) {
selectDropDownIndex(this._azureLocationDropdown, index);
@@ -690,9 +698,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._location,
this.migrationStateModel._resourceGroup);
}
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._resourceDropdown.values) {
if (this.hasSavedInfo() && this._resourceDropdown.values) {
this._resourceDropdown.values.forEach((resource, index) => {
if (resource.displayName === this.migrationStateModel.savedInfo?.targetServerInstance?.name) {
if (resource.displayName.toLowerCase() === this.migrationStateModel.savedInfo?.targetServerInstance?.name.toLowerCase()) {
selectDropDownIndex(this._resourceDropdown, index);
}
});
@@ -708,9 +716,6 @@ export class SKURecommendationPage extends MigrationWizardPage {
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
this.wizard.registerNavigationValidator((pageChangeInfo) => {
if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) {
this.migrationStateModel._migrationDbs = this.migrationStateModel.savedInfo.databaseList;
}
const errors: string[] = [];
this.wizard.message = {
text: '',
@@ -785,8 +790,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline';
if (this.migrationStateModel._assessmentResults) {
const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments?.length;
const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments?.filter(db => db.issues?.length === 0).length;
const dbCount = this.migrationStateModel._assessmentResults?.databaseAssessments?.length;
const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults?.databaseAssessments?.filter(db => db.issues?.length === 0).length;
this._rbg.cards[0].descriptions[1].textValue = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount);
this._rbg.cards[1].descriptions[1].textValue = constants.CAN_BE_MIGRATED(dbCount, dbCount);
@@ -837,6 +842,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
}).component();
return this._assessmentInfo;
}
private hasSavedInfo(): boolean {
return this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation);
}
}

View File

@@ -112,7 +112,7 @@ export class SummaryPage extends MigrationWizardPage {
]
);
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames.length > 0) {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames?.length > 0) {
this._flexContainer.addItem(createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames.join(', ')));
}
}

View File

@@ -64,7 +64,7 @@ export class WizardController {
const wizardSetupPromises: Thenable<void>[] = [];
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
wizardSetupPromises.push(this._wizardObject.open());
if (this._model.resumeAssessment) {
if (this._model.retryMigration || this._model.resumeAssessment) {
if (this._model.savedInfo.closedPage >= Page.MigrationMode) {
this._model.refreshDatabaseBackupPage = true;
}

View File

@@ -1,7 +1,7 @@
{
"name": "azuredatastudio",
"version": "1.33.0",
"distro": "01b299c0f340f6cf9dc10290b3aadea449c7940f",
"distro": "e39d1a2d41862fb0f5b4e8fc0886680e32ea5e27",
"author": {
"name": "Microsoft Corporation"
},

View File

@@ -16,10 +16,6 @@ const defaultOptions: IAutoColumnSizeOptions = {
extraColumnHeaderWidth: 0
};
// Set the max number of rows to scan to 10 since the result grid viewport in query editor is usually around 10 rows but can go up to 20 rows for notebooks.
// In most cases, 10 rows are enough to get a reasonable width and will cut down measuring costs for large notebooks.
const MAX_ROWS_TO_SCAN = 10;
export class AutoColumnSize<T extends Slick.SlickData> implements Slick.Plugin<T> {
private _grid!: Slick.Grid<T>;
private _$container!: JQuery;
@@ -166,7 +162,7 @@ export class AutoColumnSize<T extends Slick.SlickData> implements Slick.Plugin<T
let data = this._grid.getData() as Slick.DataProvider<T>;
let viewPort = this._grid.getViewport();
let start = Math.max(0, viewPort.top);
let end = Math.min(data.getLength(), MAX_ROWS_TO_SCAN);
let end = Math.min(data.getLength(), viewPort.bottom);
let allTexts: Array<string>[] = [];
let rowElements: JQuery[] = [];
@@ -183,6 +179,9 @@ export class AutoColumnSize<T extends Slick.SlickData> implements Slick.Plugin<T
let templates = this.getMaxTextTemplates(allTexts, columnDefs, colIndices, data, rowElements);
let widths = this.getTemplateWidths(rowElements, templates);
rowElements.forEach(rowElement => {
this.deleteRow(rowElement);
});
return widths.map((width) => Math.min(this._options.maxWidth, width));
}
@@ -193,7 +192,7 @@ export class AutoColumnSize<T extends Slick.SlickData> implements Slick.Plugin<T
let data = this._grid.getData() as Slick.DataProvider<T>;
let viewPort = this._grid.getViewport();
let start = Math.max(0, viewPort.top);
let end = Math.min(data.getLength(), MAX_ROWS_TO_SCAN);
let end = Math.min(data.getLength(), viewPort.bottom);
for (let i = start; i < end; i++) {
texts.push(data.getItem(i)[columnDef.field!]);
}

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1H14V14H1V1ZM13 13V8H2V13H13ZM13 7V2H2V7H13Z" fill="#323130"/>
</svg>

Before

Width:  |  Height:  |  Size: 178 B

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -29,10 +29,11 @@ export const SearchViewFocusedKey = new RawContextKey<boolean>('notebookSearchVi
export const InputBoxFocusedKey = new RawContextKey<boolean>('inputBoxFocus', false);
export const SearchInputBoxFocusedKey = new RawContextKey<boolean>('searchInputBoxFocus', false);
// !! Do not change these or updates won't be able to deserialize editors correctly !!
export const UNTITLED_NOTEBOOK_TYPEID = 'workbench.editorinputs.untitledNotebookInput';
export const UNTITLED_QUERY_EDITOR_TYPEID = 'workbench.editorinputs.untitledQueryInput';
export const FILE_QUERY_EDITOR_TYPEID = 'workbench.editorinputs.fileQueryInput';
export const RESOURCE_VIEWER_TYPEID = 'workbench.editorinputs.resourceViewerInput';
export const UNTITLED_QUERY_EDITOR_TYPEID = 'workbench.editorInput.untitledQueryInput';
export const FILE_QUERY_EDITOR_TYPEID = 'workbench.editorInput.fileQueryInput';
export const RESOURCE_VIEWER_TYPEID = 'workbench.editorInput.resourceViewerInput';
export const JUPYTER_PROVIDER_ID = 'jupyter';

View File

@@ -82,6 +82,9 @@ export class SplitCellAction extends CellActionBase {
this._register(context.cell.onCurrentEditModeChanged(currentMode => {
this.enabled = currentMode === CellEditModes.WYSIWYG ? false : true;
}));
this._register(context.cell.notebookModel.onCellTypeChanged(_ => {
this.enabled = context.cell.currentMode === CellEditModes.WYSIWYG ? false : true;
}));
}
}

View File

@@ -59,10 +59,6 @@ export class CellToolbarComponent {
this._actionBar = new Taskbar(taskbar);
this._actionBar.context = context;
let splitCellButton = this.instantiationService.createInstance(SplitCellAction, 'notebook.SplitCellAtCursor', this.buttonSplitCell, 'masked-icon icon-split-cell');
splitCellButton.setListener(context);
splitCellButton.enabled = this.cellModel.cellType !== 'markdown';
let addCellsButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codeCellsPreview', "Add cell"), 'masked-pseudo code');
let addCodeCellButton = this.instantiationService.createInstance(AddCellAction, 'notebook.AddCodeCell', localize('codePreview', "Code cell"), 'masked-pseudo code');
@@ -74,6 +70,10 @@ export class CellToolbarComponent {
let moveCellDownButton = this.instantiationService.createInstance(MoveCellAction, 'notebook.MoveCellDown', 'masked-icon move-down', this.buttonMoveDown);
let moveCellUpButton = this.instantiationService.createInstance(MoveCellAction, 'notebook.MoveCellUp', 'masked-icon move-up', this.buttonMoveUp);
let splitCellButton = this.instantiationService.createInstance(SplitCellAction, 'notebook.SplitCellAtCursor', this.buttonSplitCell, 'masked-icon icon-split-cell');
splitCellButton.setListener(context);
splitCellButton.enabled = this.cellModel.cellType !== 'markdown';
let deleteButton = this.instantiationService.createInstance(DeleteCellAction, 'notebook.DeleteCell', 'masked-icon delete', this.buttonDelete);
let moreActionsContainer = DOM.$('li.action-item');
@@ -106,10 +106,10 @@ export class CellToolbarComponent {
);
}
taskbarContent.push(
{ action: splitCellButton },
{ element: addCellDropdownContainer },
{ action: moveCellDownButton },
{ action: moveCellUpButton },
{ action: splitCellButton },
{ action: deleteButton },
{ element: moreActionsContainer });

View File

@@ -476,21 +476,28 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
viewsDropdownMenuActionViewItem.setActionContext(this._notebookParams.notebookUri);
}
this._actionBar.setContent([
{ element: buttonDropdownContainer },
{ action: this._runAllCellsAction },
{ element: Taskbar.createTaskbarSeparator() },
{ element: kernelContainer },
{ element: attachToContainer },
{ element: spacerElement },
]);
if (this._showToolbarActions) {
this._actionBar.addElement(viewsDropdownContainer);
this._actionBar.addAction(collapseCellsAction);
this._actionBar.addAction(clearResultsButton);
this._actionBar.addAction(this._trustedAction);
this._actionBar.addAction(runParametersAction);
this._actionBar.setContent([
{ element: buttonDropdownContainer },
{ action: this._runAllCellsAction },
{ element: Taskbar.createTaskbarSeparator() },
{ element: kernelContainer },
{ element: attachToContainer },
{ element: spacerElement },
{ element: viewsDropdownContainer },
{ action: collapseCellsAction },
{ action: clearResultsButton },
{ action: this._trustedAction },
{ action: runParametersAction },
]);
} else {
this._actionBar.setContent([
{ element: buttonDropdownContainer },
{ action: this._runAllCellsAction },
{ element: Taskbar.createTaskbarSeparator() },
{ element: kernelContainer },
{ element: attachToContainer },
]);
}
} else {
let kernelContainer = document.createElement('div');
@@ -523,18 +530,25 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) });
this._actionBar.context = this._notebookParams.notebookUri;
this._actionBar.setContent([
{ action: addCodeCellButton },
{ action: addTextCellButton },
{ element: kernelContainer },
{ element: attachToContainer },
{ action: this._runAllCellsAction },
]);
if (this._showToolbarActions) {
this._actionBar.addAction(this._trustedAction);
this._actionBar.addAction(clearResultsButton);
this._actionBar.addAction(collapseCellsAction);
this._actionBar.setContent([
{ action: addCodeCellButton },
{ action: addTextCellButton },
{ element: kernelContainer },
{ element: attachToContainer },
{ action: this._trustedAction },
{ action: this._runAllCellsAction },
{ action: clearResultsButton },
{ action: collapseCellsAction },
]);
} else {
this._actionBar.setContent([
{ action: addCodeCellButton },
{ action: addTextCellButton },
{ element: kernelContainer },
{ element: attachToContainer },
{ action: this._runAllCellsAction },
]);
}
}
}

View File

@@ -46,7 +46,7 @@ export class NotebookEditorComponent extends AngularDisposable {
public views: NotebookViewsExtension;
public activeView: INotebookView;
public viewMode: ViewMode;
public ViewMode = ViewMode;
public ViewMode = ViewMode; //For use of the enum in the template
constructor(
@Inject(ILogService) private readonly logService: ILogService,

View File

@@ -208,7 +208,7 @@ suite('NotebookViewModel', function (): void {
viewModel.initialize();
let cell = viewModel.cells[0];
let cellMeta = notebookViews.getCellMetadata(cell);
let cellMeta = notebookViews.getExtensionCellMetadata(cell);
assert(!isUndefinedOrNull(cellMeta.views.find(v => v.guid === viewModel.guid)));
assert.deepStrictEqual(viewModel.getCellMetadata(cell), cellMeta.views.find(v => v.guid === viewModel.guid));

View File

@@ -76,6 +76,19 @@ suite('NotebookViews', function (): void {
notebookViews = await initializeExtension();
});
test('should not modify the notebook document until a view is created', async () => {
//Create some content
notebookViews.notebook.addCell(CellTypes.Code, 0);
const cell = notebookViews.notebook.cells[0];
assert.strictEqual(notebookViews.getExtensionMetadata(), undefined);
assert.strictEqual(notebookViews.getExtensionCellMetadata(cell), undefined);
//Check that the view is created
notebookViews.createNewView(defaultViewName);
assert.notStrictEqual(notebookViews.getExtensionMetadata(), undefined);
});
test('create new view', async function (): Promise<void> {
assert.strictEqual(notebookViews.getViews().length, 0, 'notebook should not initially generate any views');

View File

@@ -9,27 +9,36 @@ import { deepClone } from 'vs/base/common/objects';
export class NotebookExtension<TNotebookMeta, TCellMeta> {
readonly version = 1;
readonly extensionName = 'azuredatastudio';
readonly extensionNamespace = 'extensions';
public getNotebookMetadata(notebook: INotebookModel): TNotebookMeta {
private _extensionName: string;
public constructor(extensionName: string) {
this._extensionName = extensionName;
}
public get extensionName(): string {
return this._extensionName;
}
public getExtensionMetadata(notebook: INotebookModel): TNotebookMeta {
const metadata = notebook.getMetaValue(this.extensionNamespace) || {};
return metadata[this.extensionName] as TNotebookMeta;
}
public setNotebookMetadata(notebook: INotebookModel, metadata: TNotebookMeta) {
public setExtensionMetadata(notebook: INotebookModel, metadata: TNotebookMeta) {
const meta = {};
meta[this.extensionName] = metadata;
notebook.setMetaValue(this.extensionNamespace, meta);
notebook.serializationStateChanged(NotebookChangeType.MetadataChanged);
}
public getCellMetadata(cell: ICellModel): TCellMeta {
public getExtensionCellMetadata(cell: ICellModel): TCellMeta {
const namespaceMeta = cell.metadata[this.extensionNamespace] || {};
return namespaceMeta[this.extensionName] as TCellMeta;
}
public setCellMetadata(cell: ICellModel, metadata: TCellMeta) {
public setExtensionCellMetadata(cell: ICellModel, metadata: TCellMeta) {
const meta = {};
meta[this.extensionName] = metadata;
cell.metadata[this.extensionNamespace] = meta;

View File

@@ -573,6 +573,11 @@ export class NotebookModel extends Disposable implements INotebookModel {
//Get selection value from current cell
let newCellContent = model.getValueInRange(selection);
let startPosition = selection.getStartPosition();
//If the cursor is at the beginning of the cell with no selection, return
if (newCellContent.length === 0 && startPosition.lineNumber === 1 && startPosition.column === 1) {
return undefined;
}
//Get content after selection
let tailRange = range.setStartPosition(selection.endLineNumber, selection.endColumn);
@@ -631,6 +636,10 @@ export class NotebookModel extends Disposable implements INotebookModel {
partialSource = source.slice(tailRange.startLineNumber - 1, tailRange.startLineNumber)[0].slice(tailRange.startColumn - 1);
tailSource.splice(0, 1, partialSource);
}
//Remove the trailing empty line after the cursor
if (tailSource[0] === '\r\n' || tailSource[0] === '\n') {
tailSource.splice(0, 1);
}
tailCell.source = tailSource;
tailCellIndex = newCellIndex + 1;
this.insertCell(tailCell, tailCellIndex);

View File

@@ -51,11 +51,11 @@ export class NotebookViewModel implements INotebookView {
}
protected initializeCell(cell: ICellModel, idx: number) {
let meta = this._notebookViews.getCellMetadata(cell);
let meta = this._notebookViews.getExtensionCellMetadata(cell);
if (!meta) {
this._notebookViews.initializeCell(cell);
meta = this._notebookViews.getCellMetadata(cell);
meta = this._notebookViews.getExtensionCellMetadata(cell);
}
// Ensure that we are not duplicting view entries in cell metadata
@@ -91,7 +91,7 @@ export class NotebookViewModel implements INotebookView {
}
public getCellMetadata(cell: ICellModel): INotebookViewCell {
const meta = this._notebookViews.getCellMetadata(cell);
const meta = this._notebookViews.getExtensionCellMetadata(cell);
return meta?.views?.find(view => view.guid === this.guid);
}

View File

@@ -13,30 +13,40 @@ import { INotebookView, INotebookViewCell, INotebookViewCellMetadata, INotebookV
export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetadata, INotebookViewCellMetadata> implements INotebookViews {
static readonly defaultViewName = localize('notebookView.untitledView', "Untitled View");
static readonly extension = 'notebookviews';
readonly maxNameIterationAttempts = 100;
readonly extension = 'azuredatastudio';
override readonly version = 1;
protected _metadata: INotebookViewMetadata;
protected _metadata: INotebookViewMetadata | undefined;
private _initialized: boolean = false;
private _onViewDeleted = new Emitter<void>();
private _onActiveViewChanged = new Emitter<void>();
constructor(protected _notebook: INotebookModel) {
super();
this.loadOrInitialize();
super(NotebookViewsExtension.extension);
this.load();
}
public loadOrInitialize() {
this._metadata = this.getNotebookMetadata(this._notebook);
public load(): void {
this._metadata = this.getExtensionMetadata();
if (this._metadata) {
this._metadata.views = this._metadata.views.map(view => NotebookViewModel.load(view.guid, this));
this._initialized = true;
}
}
public initialize() {
this._metadata = this.getExtensionMetadata();
if (!this._metadata) {
this.initializeNotebook();
this.initializeCells();
this.commit();
} else {
this._metadata.views = this._metadata.views.map(view => NotebookViewModel.load(view.guid, this));
}
this._initialized = true;
}
protected initializeNotebook() {
@@ -59,12 +69,17 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
views: []
};
this.setCellMetadata(cell, meta);
this.setExtensionCellMetadata(cell, meta);
}
public createNewView(name?: string): INotebookView {
const viewName = name || this.generateDefaultViewName();
// If the notebook has not been initialized, do it now
if (!this.initialized) {
this.initialize();
}
const view = new NotebookViewModel(viewName, this);
view.initialize(true);
@@ -77,21 +92,21 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
}
public removeView(guid: string) {
let viewToRemove = this._metadata.views.findIndex(view => view.guid === guid);
if (viewToRemove !== -1) {
let removedView = this._metadata.views.splice(viewToRemove, 1);
let viewToRemove = this._metadata?.views.findIndex(view => view.guid === guid);
if (viewToRemove >= 0) {
let removedView = this._metadata?.views.splice(viewToRemove, 1);
// Remove view data for each cell
if (removedView.length === 1) {
this._notebook?.cells.forEach((cell) => {
let meta = this.getCellMetadata(cell);
let meta = this.getExtensionCellMetadata(cell);
meta.views = meta.views.filter(x => x.guid !== removedView[0].guid);
this.setCellMetadata(cell, meta);
this.setExtensionCellMetadata(cell, meta);
});
}
}
if (guid === this._metadata.activeView) {
if (guid === this._metadata?.activeView) {
this._metadata.activeView = undefined;
}
@@ -113,13 +128,13 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
}
public updateCell(cell: ICellModel, currentView: INotebookView, cellData: INotebookViewCell, override: boolean = false) {
const cellMetadata = this.getCellMetadata(cell);
const cellMetadata = this.getExtensionCellMetadata(cell);
if (cellMetadata) {
const viewToUpdate = cellMetadata.views.findIndex(view => view.guid === currentView.guid);
if (viewToUpdate >= 0) {
cellMetadata.views[viewToUpdate] = override ? cellData : { ...cellMetadata.views[viewToUpdate], ...cellData };
this.setCellMetadata(cell, cellMetadata);
this.setExtensionCellMetadata(cell, cellMetadata);
}
}
}
@@ -129,7 +144,7 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
}
public getViews(): INotebookView[] {
return this._metadata.views;
return this._metadata?.views ?? [];
}
public get metadata(): INotebookViewMetadata {
@@ -137,21 +152,27 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
}
public getCells(): INotebookViewCellMetadata[] {
return this._notebook.cells.map(cell => this.getCellMetadata(cell));
return this._notebook.cells.map(cell => this.getExtensionCellMetadata(cell));
}
public override getExtensionMetadata(): INotebookViewMetadata {
return super.getExtensionMetadata(this._notebook);
}
public getActiveView(): INotebookView {
return this.getViews().find(view => view.guid === this._metadata.activeView);
return this.getViews().find(view => view.guid === this._metadata?.activeView);
}
public setActiveView(view: INotebookView) {
this._metadata.activeView = view.guid;
this._onActiveViewChanged.fire();
if (this._metadata) {
this._metadata.activeView = view.guid;
this._onActiveViewChanged.fire();
}
}
public commit() {
this._metadata = Object.assign({}, this._metadata);
this.setNotebookMetadata(this._notebook, this._metadata);
this.setExtensionMetadata(this._notebook, this._metadata);
}
public viewNameIsTaken(name: string): boolean {
@@ -165,4 +186,8 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
public get onActiveViewChanged(): Event<void> {
return this._onActiveViewChanged.event;
}
public get initialized(): boolean {
return this._initialized;
}
}

View File

@@ -394,7 +394,7 @@ import 'vs/workbench/contrib/surveys/browser/languageSurveys.contribution';
// Welcome
import 'vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay';
import 'vs/workbench/contrib/welcome/page/browser/welcomePage.contribution';
import 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution';
// import 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution'; // {{SQL CARBON EDIT}} - remove vscode getting started
import 'vs/workbench/contrib/welcome/walkThrough/browser/walkThrough.contribution';
// Call Hierarchy