mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 11:01:37 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6783aa6967 | ||
|
|
ccbc2f74fe | ||
|
|
d7283a6e56 | ||
|
|
0684040d34 | ||
|
|
625eb00be2 | ||
|
|
c5a27a89f3 | ||
|
|
b9a7d5e4bd | ||
|
|
f876c00ca1 | ||
|
|
83ae789aa0 | ||
|
|
6fe4d0a561 | ||
|
|
2eaec9f41d | ||
|
|
2edafe50bb | ||
|
|
24c5686bd6 | ||
|
|
1731aeffbe | ||
|
|
b35ff6451a | ||
|
|
07aa256f4c | ||
|
|
473764de9a |
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
"type": "boolean",
|
||||
"description": "%sqlDatabaseProjects.netCoreDoNotAsk%"
|
||||
},
|
||||
"sqlDatabaseProjects.netCoreDowngradeDoNotShow": {
|
||||
"type": "boolean",
|
||||
"description": "%sqlDatabaseProjects.netCoreDowngradeDoNotShow%"
|
||||
},
|
||||
"sqlDatabaseProjects.nodejsDoNotAsk": {
|
||||
"type": "boolean",
|
||||
"description": "%sqlDatabaseProjects.nodejsDoNotAsk%"
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!));
|
||||
}
|
||||
|
||||
3
extensions/sql-migration/images/retry.svg
Normal file
3
extensions/sql-migration/images/retry.svg
Normal 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 |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "azuredatastudio",
|
||||
"version": "1.33.0",
|
||||
"distro": "01b299c0f340f6cf9dc10290b3aadea449c7940f",
|
||||
"distro": "e39d1a2d41862fb0f5b4e8fc0886680e32ea5e27",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
||||
@@ -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!]);
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user