mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Adding tools and Eula page to Resource Deployment (#13182)
* SQL VM wizard migration to ResourceType Wizard * Revert "SQL VM wizard migration to ResourceType Wizard" This reverts commit e58cd47707a7e2812be20d915f1fe638b96b035f. * migrated notebook wizard * SQL VM wizard migration to ResourceType Wizard * Fixed some imports on SQL VM wizard * migrated sqldb wizard to generic ResourceTypeWizard * Added missing import Solving errors from the merge * Moved some common functionality into ResourceTypeWizard * Changed logic of start deployment * fixed some import after changing files. * added pagelss model and tools and Eula Page * Hacky solution to fix wizard update bugs * Removed tools and Eula components from resourceTypePickerDialog * Removing changes in ext host * reverting every change in ext host dialog * Fixed setting the first provider for resourceTypeWizard. * Some PR related changes -Fixed typo in localized constants -made some code logic concise -Removed unnecessary check in tools&Eula * Added some fixes for compilation error * some refactoring for PRs * moved comment * cleaning up some code to make it more readable * fixed comment typo * Some additional cleaning up of code. * Adding a public getter for model * -Adding error message for failed EULA validation -Removed unnecessary check for selected resource. * Added comment to explain model variable behavior * Added additional comments * Fixed a comment to make it accurate * Better phrasing for a comment
This commit is contained in:
@@ -69,3 +69,13 @@ export function getResourceTypeCategoryLocalizedString(resourceTypeCategory: str
|
||||
return resourceTypeCategory;
|
||||
}
|
||||
}
|
||||
|
||||
export const descriptionText = localize('resourceDeployment.Description', "Description");
|
||||
export const toolText = localize('resourceDeployment.Tool', "Tool");
|
||||
export const statusText = localize('resourceDeployment.Status', "Status");
|
||||
export const versionText = localize('resourceDeployment.Version', "Version");
|
||||
export const requiredVersionText = localize('resourceDeployment.RequiredVersion', "Required Version");
|
||||
export const discoverPathOrAdditionalInformationText = localize('resourceDeployment.discoverPathOrAdditionalInformation', "Discovered Path or Additional Information");
|
||||
export const requiredToolsText = localize('resourceDeployment.requiredTools', "Required tools");
|
||||
export const installToolsText = localize('resourceDeployment.InstallTools', "Install tools");
|
||||
export const optionsText = localize('resourceDeployment.Options', "Options");
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<rd.IEx
|
||||
if (!defaultResourceType) {
|
||||
vscode.window.showErrorMessage(localize('resourceDeployment.UnknownResourceType', "The resource type: {0} is not defined", defaultResourceTypeName));
|
||||
} else {
|
||||
const dialog = new ResourceTypePickerDialog(toolsService, resourceTypeService, defaultResourceType, resourceTypeNameFilters);
|
||||
const dialog = new ResourceTypePickerDialog(resourceTypeService, defaultResourceType, resourceTypeNameFilters);
|
||||
dialog.open();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { createWriteStream, promises as fs } from 'fs';
|
||||
import * as https from 'https';
|
||||
import * as os from 'os';
|
||||
@@ -11,7 +10,6 @@ import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { DeploymentProvider, instanceOfAzureSQLVMDeploymentProvider, instanceOfAzureSQLDBDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookDeploymentProvider, instanceOfNotebookWizardDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfWizardDeploymentProvider, NotebookInfo, NotebookPathInfo, ResourceType, ResourceTypeOption } from '../interfaces';
|
||||
import { DeploymentInputDialog } from '../ui/deploymentInputDialog';
|
||||
import { AzdataService } from './azdataService';
|
||||
import { KubeService } from './kubeService';
|
||||
import { INotebookService } from './notebookService';
|
||||
@@ -25,7 +23,7 @@ const localize = nls.loadMessageBundle();
|
||||
export interface IResourceTypeService {
|
||||
getResourceTypes(filterByPlatform?: boolean): ResourceType[];
|
||||
validateResourceTypes(resourceTypes: ResourceType[]): string[];
|
||||
startDeployment(resourceType: ResourceType, provider: DeploymentProvider): void;
|
||||
startDeployment(resourceType: ResourceType): void;
|
||||
}
|
||||
|
||||
export class ResourceTypeService implements IResourceTypeService {
|
||||
@@ -248,42 +246,12 @@ export class ResourceTypeService implements IResourceTypeService {
|
||||
}
|
||||
|
||||
|
||||
public startDeployment(resourceType: ResourceType, provider: DeploymentProvider): void {
|
||||
const self = this;
|
||||
if (instanceOfDialogDeploymentProvider(provider)) {
|
||||
const dialog = new DeploymentInputDialog(this.notebookService, this.platformService, this.toolsService, provider.dialog);
|
||||
dialog.open();
|
||||
} else if (instanceOfNotebookDeploymentProvider(provider)) {
|
||||
this.notebookService.openNotebook(provider.notebook);
|
||||
} else if (instanceOfDownloadDeploymentProvider(provider)) {
|
||||
const taskName = localize('resourceDeployment.DownloadAndLaunchTaskName', "Download and launch installer, URL: {0}", provider.downloadUrl);
|
||||
azdata.tasks.startBackgroundOperation({
|
||||
displayName: taskName,
|
||||
description: taskName,
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.DownloadingText', "Downloading from: {0}", provider.downloadUrl));
|
||||
self.download(provider.downloadUrl).then(async (downloadedFile) => {
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.DownloadCompleteText', "Successfully downloaded: {0}", downloadedFile));
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.LaunchingProgramText', "Launching: {0}", downloadedFile));
|
||||
await this.platformService.runCommand(downloadedFile, { sudo: true });
|
||||
op.updateStatus(azdata.TaskStatus.Succeeded, localize('resourceDeployment.ProgramLaunchedText', "Successfully launched: {0}", downloadedFile));
|
||||
}, (error) => {
|
||||
op.updateStatus(azdata.TaskStatus.Failed, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (instanceOfWebPageDeploymentProvider(provider)) {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(provider.webPageUrl));
|
||||
} else if (instanceOfCommandDeploymentProvider(provider)) {
|
||||
vscode.commands.executeCommand(provider.command);
|
||||
} else {
|
||||
const wizard = new ResourceTypeWizard(resourceType, provider, new KubeService(), new AzdataService(this.platformService), this.notebookService, this.toolsService, this.platformService);
|
||||
wizard.open();
|
||||
}
|
||||
public startDeployment(resourceType: ResourceType): void {
|
||||
const wizard = new ResourceTypeWizard(resourceType, new KubeService(), new AzdataService(this.platformService), this.notebookService, this.toolsService, this.platformService, this);
|
||||
wizard.open();
|
||||
}
|
||||
|
||||
private download(url: string): Promise<string> {
|
||||
public download(url: string): Promise<string> {
|
||||
const self = this;
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
https.get(url, async function (response) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfWebPageDeploymentProvider } from '../interfaces';
|
||||
import { DeploymentInputDialog } from './deploymentInputDialog';
|
||||
import { ResourceTypeModel } from './resourceTypeModel';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
export class PageLessDeploymentModel extends ResourceTypeModel {
|
||||
|
||||
initialize(): void {
|
||||
this.wizard.setPages([]);
|
||||
}
|
||||
|
||||
async onOk(): Promise<void> {
|
||||
let provider: DeploymentProvider = this.wizard.provider;
|
||||
if (instanceOfDialogDeploymentProvider(provider)) {
|
||||
const dialog = new DeploymentInputDialog(this.wizard.notebookService, this.wizard.platformService, this.wizard.toolsService, provider.dialog);
|
||||
dialog.open();
|
||||
} else if (instanceOfNotebookDeploymentProvider(provider)) {
|
||||
this.wizard.notebookService.openNotebook(provider.notebook);
|
||||
} else if (instanceOfDownloadDeploymentProvider(provider)) {
|
||||
const downloadUrl = provider.downloadUrl;
|
||||
const taskName = localize('resourceDeployment.DownloadAndLaunchTaskName', "Download and launch installer, URL: {0}", downloadUrl);
|
||||
azdata.tasks.startBackgroundOperation({
|
||||
displayName: taskName,
|
||||
description: taskName,
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.DownloadingText', "Downloading from: {0}", downloadUrl));
|
||||
this.wizard.resourceTypeService.download(downloadUrl).then(async (downloadedFile) => {
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.DownloadCompleteText', "Successfully downloaded: {0}", downloadedFile));
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, localize('resourceDeployment.LaunchingProgramText', "Launching: {0}", downloadedFile));
|
||||
await this.wizard.platformService.runCommand(downloadedFile, { sudo: true });
|
||||
op.updateStatus(azdata.TaskStatus.Succeeded, localize('resourceDeployment.ProgramLaunchedText', "Successfully launched: {0}", downloadedFile));
|
||||
}, (error) => {
|
||||
op.updateStatus(azdata.TaskStatus.Failed, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (instanceOfWebPageDeploymentProvider(provider)) {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(provider.webPageUrl));
|
||||
} else if (instanceOfCommandDeploymentProvider(provider)) {
|
||||
vscode.commands.executeCommand(provider.command);
|
||||
}
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,76 +4,41 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } from '../interfaces';
|
||||
import { ResourceType } from '../interfaces';
|
||||
import { IResourceTypeService } from '../services/resourceTypeService';
|
||||
import { IToolsService } from '../services/toolsService';
|
||||
import { getErrorMessage } from '../common/utils';
|
||||
import * as loc from './../localizedConstants';
|
||||
import { DialogBase } from './dialogBase';
|
||||
import { createFlexContainer } from './modelViewUtils';
|
||||
import * as constants from '../constants';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class ResourceTypePickerDialog extends DialogBase {
|
||||
private toolRefreshTimestamp: number = 0;
|
||||
private _resourceTypes!: ResourceType[];
|
||||
private _selectedResourceType: ResourceType;
|
||||
private _view!: azdata.ModelView;
|
||||
private _optionsContainer!: azdata.FlexContainer;
|
||||
private _toolsTable!: azdata.TableComponent;
|
||||
private _resourceTagsListView!: azdata.ListViewComponent;
|
||||
private _resourceSearchBox!: azdata.InputBoxComponent;
|
||||
private _cardGroup!: azdata.RadioCardGroupComponent;
|
||||
private _optionDropDownMap: Map<string, azdata.DropDownComponent> = new Map();
|
||||
private _toolsLoadingComponent!: azdata.LoadingComponent;
|
||||
private _agreementContainer!: azdata.DivContainer;
|
||||
private _agreementCheckboxChecked: boolean = false;
|
||||
private _installToolButton: azdata.window.Button;
|
||||
private _installationInProgress: boolean = false;
|
||||
private _tools: ITool[] = [];
|
||||
private _eulaValidationSucceeded: boolean = false;
|
||||
// array to store listners that are specific to the selected resource. To be cleared after change in selected resource.
|
||||
private _currentResourceTypeDisposables: vscode.Disposable[] = [];
|
||||
private _cardsCache: Map<string, azdata.RadioCard> = new Map();
|
||||
|
||||
constructor(
|
||||
private toolsService: IToolsService,
|
||||
private resourceTypeService: IResourceTypeService,
|
||||
defaultResourceType: ResourceType,
|
||||
private _resourceTypeNameFilters?: string[]) {
|
||||
super(loc.resourceTypePickerDialogTitle, 'ResourceTypePickerDialog', true);
|
||||
this._selectedResourceType = defaultResourceType;
|
||||
this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools"));
|
||||
this._toDispose.push(this._installToolButton.onClick(() => {
|
||||
this.installTools().catch(error => console.log(error));
|
||||
}));
|
||||
this._dialogObject.customButtons = [this._installToolButton];
|
||||
this._installToolButton.hidden = true;
|
||||
this._dialogObject.okButton.label = loc.select;
|
||||
this._dialogObject.okButton.enabled = false; // this is enabled after all tools are discovered.
|
||||
}
|
||||
|
||||
initialize() {
|
||||
let tab = azdata.window.createTab('');
|
||||
this._dialogObject.registerCloseValidator(async () => {
|
||||
const isValid = this._selectedResourceType && (this._selectedResourceType.agreement === undefined || this._agreementCheckboxChecked);
|
||||
if (!isValid) {
|
||||
this._dialogObject.message = {
|
||||
text: localize('deploymentDialog.AcceptAgreements', "You must agree to the license agreements in order to proceed."),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
if (!this._eulaValidationSucceeded && !(await this.acquireEulaAndProceed())) {
|
||||
return false; // we return false so that the workflow does not proceed and user gets to either click acceptEulaAndSelect again or cancel
|
||||
}
|
||||
return true;
|
||||
});
|
||||
tab.registerContent((view: azdata.ModelView) => {
|
||||
const tableWidth = 1126;
|
||||
this._view = view;
|
||||
this._resourceTypes = this.resourceTypeService
|
||||
.getResourceTypes()
|
||||
@@ -102,46 +67,6 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
this.selectResourceType(resourceType);
|
||||
}
|
||||
}));
|
||||
this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this._agreementContainer = view.modelBuilder.divContainer().component();
|
||||
const toolColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolNameColumnHeader', "Tool"),
|
||||
width: 105
|
||||
};
|
||||
const descriptionColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolDescriptionColumnHeader', "Description"),
|
||||
width: 270
|
||||
};
|
||||
const installStatusColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolStatusColumnHeader', "Status"),
|
||||
width: 70
|
||||
};
|
||||
const versionColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolVersionColumnHeader', "Version"),
|
||||
width: 75
|
||||
};
|
||||
const minVersionColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolMinimumVersionColumnHeader', "Required Version"),
|
||||
width: 105
|
||||
};
|
||||
const installedPathColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolDiscoveredPathColumnHeader', "Discovered Path or Additional Information"),
|
||||
width: 580
|
||||
};
|
||||
this._toolsTable = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
|
||||
data: [],
|
||||
columns: [toolColumn, descriptionColumn, installStatusColumn, versionColumn, minVersionColumn, installedPathColumn],
|
||||
width: tableWidth,
|
||||
ariaLabel: localize('deploymentDialog.RequiredToolsTitle', "Required tools")
|
||||
}).component();
|
||||
|
||||
const toolsTableWrapper = view.modelBuilder.divContainer().withLayout({ width: tableWidth }).component();
|
||||
toolsTableWrapper.addItem(this._toolsTable, { CSSStyles: { 'border-left': '1px solid silver', 'border-top': '1px solid silver' } });
|
||||
this._toolsLoadingComponent = view.modelBuilder.loadingComponent().withItem(toolsTableWrapper).withProperties<azdata.LoadingComponentProperties>({
|
||||
loadingCompletedText: localize('deploymentDialog.loadingRequiredToolsCompleted', "Loading required tools information completed"),
|
||||
loadingText: localize('deploymentDialog.loadingRequiredTools', "Loading required tools information"),
|
||||
showText: true
|
||||
}).component();
|
||||
|
||||
const resourceComponents: azdata.Component[] = [];
|
||||
if (this.getAllResourceTags().length !== 0) {
|
||||
@@ -172,16 +97,6 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
{
|
||||
component: this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).withItems(resourceComponents).component(),
|
||||
title: ''
|
||||
}, {
|
||||
component: this._agreementContainer,
|
||||
title: ''
|
||||
},
|
||||
{
|
||||
component: this._optionsContainer,
|
||||
title: localize('deploymentDialog.OptionsTitle', "Options")
|
||||
}, {
|
||||
component: this._toolsLoadingComponent,
|
||||
title: localize('deploymentDialog.RequiredToolsTitle', "Required tools")
|
||||
}
|
||||
],
|
||||
{
|
||||
@@ -263,12 +178,6 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
selectedCardId: '',
|
||||
cards: []
|
||||
});
|
||||
this._agreementCheckboxChecked = false;
|
||||
this._agreementContainer.clearItems();
|
||||
this._optionsContainer.clearItems();
|
||||
this._toolsLoadingComponent.loading = false;
|
||||
this._toolsTable.data = [[]];
|
||||
this._tools = [];
|
||||
this._dialogObject.okButton.enabled = false;
|
||||
}
|
||||
}
|
||||
@@ -276,315 +185,10 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
private selectResourceType(resourceType: ResourceType): void {
|
||||
this._currentResourceTypeDisposables.forEach(disposable => disposable.dispose());
|
||||
this._selectedResourceType = resourceType;
|
||||
this._agreementCheckboxChecked = false;
|
||||
this._agreementContainer.clearItems();
|
||||
if (resourceType.agreement) {
|
||||
this._agreementContainer.addItem(this.createAgreementCheckbox(resourceType.agreement));
|
||||
}
|
||||
|
||||
this._optionsContainer.clearItems();
|
||||
this._optionDropDownMap.clear();
|
||||
if (resourceType.options) {
|
||||
resourceType.options.forEach(option => {
|
||||
const optionLabel = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: option.displayName
|
||||
}).component();
|
||||
optionLabel.width = '150px';
|
||||
|
||||
const optionSelectBox = this._view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
|
||||
values: option.values,
|
||||
value: option.values[0],
|
||||
width: '300px',
|
||||
ariaLabel: option.displayName
|
||||
}).component();
|
||||
|
||||
this._currentResourceTypeDisposables.push(optionSelectBox.onValueChanged(() => {
|
||||
this.updateOkButtonText();
|
||||
this.updateToolsDisplayTable();
|
||||
}));
|
||||
|
||||
this._optionDropDownMap.set(option.name, optionSelectBox);
|
||||
const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
this._optionsContainer.addItem(row);
|
||||
});
|
||||
}
|
||||
this.updateOkButtonText();
|
||||
this.updateToolsDisplayTable();
|
||||
}
|
||||
|
||||
private updateOkButtonText(): void {
|
||||
//handle special case when resource type has different OK button.
|
||||
let text = this.getCurrentOkText();
|
||||
this._dialogObject.okButton.label = text || loc.select;
|
||||
}
|
||||
|
||||
private updateToolsDisplayTable(): void {
|
||||
this.toolRefreshTimestamp = new Date().getTime();
|
||||
const currentRefreshTimestamp = this.toolRefreshTimestamp;
|
||||
const headerRowHeight = 28;
|
||||
this._toolsTable.height = 25 * Math.max(this.toolRequirements.length, 1) + headerRowHeight;
|
||||
if (!this._installationInProgress) { // Wipe the informational message clean unless installation is already in progress.
|
||||
this._dialogObject.message = {
|
||||
text: ''
|
||||
};
|
||||
}
|
||||
this._installToolButton.hidden = true;
|
||||
if (this.toolRequirements.length === 0) {
|
||||
this._toolsLoadingComponent.loading = false;
|
||||
this._dialogObject.okButton.enabled = true;
|
||||
this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']];
|
||||
this._tools = [];
|
||||
} else {
|
||||
this._tools = this.toolRequirements.map(toolReq => this.toolsService.getToolByName(toolReq.name)!);
|
||||
this._toolsLoadingComponent.loading = true;
|
||||
this._dialogObject.okButton.enabled = false;
|
||||
let toolsLoadingErrors: string[] = [];
|
||||
Promise.all(
|
||||
this._tools.map(
|
||||
tool => tool.finishInitialization().catch(() => toolsLoadingErrors.push(`${tool.displayName}:${tool.statusDescription!}`))
|
||||
)
|
||||
)
|
||||
.then(() => this.executeToolsTableWorkflow(currentRefreshTimestamp, toolsLoadingErrors))
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
}
|
||||
|
||||
private executeToolsTableWorkflow(currentRefreshTimestamp: number, toolsLoadingErrors: string[]): void {
|
||||
// If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results
|
||||
if (this.toolRefreshTimestamp !== currentRefreshTimestamp) {
|
||||
return;
|
||||
}
|
||||
let minVersionCheckFailed = false;
|
||||
const toolsToAutoInstall: ITool[] = [];
|
||||
let messages: string[] = toolsLoadingErrors!;
|
||||
let erroredOrFailedTool: boolean = false;
|
||||
this._toolsTable.data = this.toolRequirements.map(toolRequirement => {
|
||||
const tool = this.toolsService.getToolByName(toolRequirement.name)!;
|
||||
// subscribe to onUpdateData event of the tool.
|
||||
this._currentResourceTypeDisposables.push(tool.onDidUpdateData((t: ITool) => {
|
||||
this.updateToolsDisplayTableData(t);
|
||||
}));
|
||||
|
||||
erroredOrFailedTool = erroredOrFailedTool || (tool.status === ToolStatus.Error || tool.status === ToolStatus.Failed);
|
||||
if (tool.status === ToolStatus.NotInstalled) {
|
||||
if (tool.autoInstallSupported) {
|
||||
toolsToAutoInstall.push(tool);
|
||||
}
|
||||
else {
|
||||
messages.push(localize('deploymentDialog.ToolInformation', "'{0}' was not discovered and automated installation is not currently supported. Install '{0}' manually or ensure it is started and discoverable. Once done please restart Azure Data Studio. See [{1}] .", tool.displayName, tool.homePage));
|
||||
}
|
||||
}
|
||||
else if (tool.status === ToolStatus.Installed && toolRequirement.version && !tool.isSameOrNewerThan(toolRequirement.version)) {
|
||||
minVersionCheckFailed = true;
|
||||
messages.push(localize('deploymentDialog.ToolDoesNotMeetVersionRequirement', "'{0}' [ {1} ] does not meet the minimum version requirement, please uninstall it and restart Azure Data Studio.", tool.displayName, tool.homePage));
|
||||
}
|
||||
return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', toolRequirement.version || '', tool.installationPathOrAdditionalInformation || ''];
|
||||
});
|
||||
this._installToolButton.hidden = erroredOrFailedTool || minVersionCheckFailed || (toolsToAutoInstall.length === 0);
|
||||
this._dialogObject.okButton.enabled = !erroredOrFailedTool && messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0);
|
||||
if (messages.length !== 0) {
|
||||
if (messages.length > 1) {
|
||||
messages = messages.map(message => `• ${message}`);
|
||||
}
|
||||
messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed manually to pick up the change. You may find additional details in 'Deployments' and 'Azure Data CLI' output channels"));
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: messages.join(EOL)
|
||||
};
|
||||
}
|
||||
else if ((toolsToAutoInstall.length !== 0) && !this._installationInProgress) {
|
||||
const installationNeededHeader = toolsToAutoInstall.length === 1
|
||||
? localize('deploymentDialog.InstallToolsHintOne', "Tool: {0} is not installed, you can click the \"{1}\" button to install it.", toolsToAutoInstall[0].displayName, this._installToolButton.label)
|
||||
: localize('deploymentDialog.InstallToolsHintMany', "Tools: {0} are not installed, you can click the \"{1}\" button to install them.", toolsToAutoInstall.map(t => t.displayName).join(', '), this._installToolButton.label);
|
||||
let infoText: string[] = [installationNeededHeader];
|
||||
const informationalMessagesArray = this._tools.reduce<string[]>((returnArray, currentTool) => {
|
||||
if (currentTool.autoInstallNeeded) {
|
||||
returnArray.push(...currentTool.dependencyMessages);
|
||||
}
|
||||
return returnArray;
|
||||
}, /* initial Value of return array*/[]);
|
||||
const informationalMessagesSet: Set<string> = new Set<string>(informationalMessagesArray);
|
||||
if (informationalMessagesSet.size > 0) {
|
||||
infoText.push(...informationalMessagesSet.values());
|
||||
}
|
||||
// we don't have scenarios that have mixed type of tools - either we don't support auto install: docker, or we support auto install for all required tools
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Warning,
|
||||
text: infoText.join(EOL)
|
||||
};
|
||||
}
|
||||
if (!this.areToolsEulaAccepted()) {
|
||||
this._dialogObject.okButton.label = loc.acceptEulaAndSelect;
|
||||
}
|
||||
this._toolsLoadingComponent.loading = false;
|
||||
}
|
||||
|
||||
private areToolsEulaAccepted(): boolean {
|
||||
// we run 'map' on each tool before doing 'every' so that we collect eula messages for all tools (instead of bailing out after 1st failure)
|
||||
this._eulaValidationSucceeded = this._tools.map(tool => {
|
||||
const eulaAccepted = tool.isEulaAccepted();
|
||||
if (!eulaAccepted) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: [tool.statusDescription!, this._dialogObject.message.text].join(EOL)
|
||||
};
|
||||
}
|
||||
return eulaAccepted;
|
||||
}).every(isEulaAccepted => isEulaAccepted);
|
||||
return this._eulaValidationSucceeded;
|
||||
}
|
||||
|
||||
private async acquireEulaAndProceed(): Promise<boolean> {
|
||||
this._dialogObject.message = { text: '' };
|
||||
let eulaAccepted = true;
|
||||
for (const tool of this._tools) {
|
||||
eulaAccepted = tool.isEulaAccepted() || await tool.promptForEula();
|
||||
if (!eulaAccepted) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: [tool.statusDescription!, this._dialogObject.message.text].join(EOL)
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return eulaAccepted;
|
||||
}
|
||||
|
||||
private get toolRequirements() {
|
||||
return this.getCurrentProvider().requiredTools;
|
||||
}
|
||||
|
||||
private createAgreementCheckbox(agreementInfo: AgreementInfo): azdata.FlexContainer {
|
||||
const checkbox = this._view.modelBuilder.checkBox().withProperties<azdata.CheckBoxProperties>({
|
||||
ariaLabel: this.getAgreementDisplayText(agreementInfo),
|
||||
required: true
|
||||
}).component();
|
||||
checkbox.checked = false;
|
||||
this._currentResourceTypeDisposables.push(checkbox.onChanged(() => {
|
||||
this._agreementCheckboxChecked = !!checkbox.checked;
|
||||
}));
|
||||
const text = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: agreementInfo.template,
|
||||
links: agreementInfo.links,
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
return createFlexContainer(this._view, [checkbox, text]);
|
||||
}
|
||||
|
||||
private getAgreementDisplayText(agreementInfo: AgreementInfo): string {
|
||||
// the agreement template will have {index} as placeholder for hyperlinks
|
||||
// this method will get the display text after replacing the placeholders
|
||||
let text = agreementInfo.template;
|
||||
for (let i: number = 0; i < agreementInfo.links.length; i++) {
|
||||
text = text.replace(`{${i}}`, agreementInfo.links[i].text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private getCurrentProvider(): DeploymentProvider {
|
||||
const options: { option: string, value: string }[] = [];
|
||||
|
||||
this._optionDropDownMap.forEach((selectBox, option) => {
|
||||
let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue;
|
||||
options.push({ option: option, value: selectedValue.name });
|
||||
});
|
||||
|
||||
return this._selectedResourceType.getProvider(options)!;
|
||||
}
|
||||
|
||||
private getCurrentOkText(): string {
|
||||
const options: { option: string, value: string }[] = [];
|
||||
|
||||
this._optionDropDownMap.forEach((selectBox, option) => {
|
||||
let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue;
|
||||
options.push({ option: option, value: selectedValue.name });
|
||||
});
|
||||
|
||||
return this._selectedResourceType.getOkButtonText(options)!;
|
||||
}
|
||||
|
||||
protected async onComplete(): Promise<void> {
|
||||
this.toolsService.toolsForCurrentProvider = this._tools;
|
||||
this.resourceTypeService.startDeployment(this._selectedResourceType, this.getCurrentProvider());
|
||||
}
|
||||
|
||||
protected updateToolsDisplayTableData(tool: ITool) {
|
||||
this._toolsTable.data = this._toolsTable.data.map(rowData => {
|
||||
if (rowData[0] === tool.displayName) {
|
||||
return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', rowData[4]/* required version*/, tool.installationPathOrAdditionalInformation || ''];
|
||||
} else {
|
||||
return rowData;
|
||||
}
|
||||
});
|
||||
this.setUiControlsEnabled(tool.status !== ToolStatus.Installing); // if installing then disable ui controls else enable them
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param enable - if true the UiControls are set to be enabled, if not they are set to be disabled.
|
||||
*/
|
||||
private setUiControlsEnabled(enable: boolean): void {
|
||||
this._cardGroup.enabled = enable;
|
||||
this._agreementContainer.enabled = enable;
|
||||
this._optionsContainer.enabled = enable;
|
||||
this._dialogObject.cancelButton.enabled = enable;
|
||||
// select and install tools buttons are controlled separately
|
||||
}
|
||||
|
||||
private async installTools(): Promise<void> {
|
||||
this._installToolButton.enabled = false;
|
||||
this._installationInProgress = true;
|
||||
let tool: ITool;
|
||||
try {
|
||||
const toolRequirements = this.toolRequirements;
|
||||
let toolsNotInstalled: ITool[] = [];
|
||||
for (let i: number = 0; i < toolRequirements.length; i++) {
|
||||
const toolReq = toolRequirements[i];
|
||||
tool = this.toolsService.getToolByName(toolReq.name)!;
|
||||
if (tool.autoInstallNeeded) {
|
||||
// Update the informational message
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstallingTool', "Required tool '{0}' [ {1} ] is being installed now.", tool.displayName, tool.homePage)
|
||||
};
|
||||
await this._tools[i].install();
|
||||
if (tool.status === ToolStatus.Installed && toolReq.version && !tool.isSameOrNewerThan(toolReq.version)) {
|
||||
throw new Error(
|
||||
localize('deploymentDialog.ToolDoesNotMeetVersionRequirement', "'{0}' [ {1} ] does not meet the minimum version requirement, please uninstall it and restart Azure Data Studio.",
|
||||
tool.displayName, tool.homePage
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toolsNotInstalled.push(tool);
|
||||
}
|
||||
}
|
||||
// Update the informational message
|
||||
if (toolsNotInstalled.length === 0) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstalledTools', "All required tools are installed now.")
|
||||
};
|
||||
this._dialogObject.okButton.enabled = true;
|
||||
} else {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.PendingInstallation', "Following tools: {0} were still not discovered. Please make sure that they are installed, running and discoverable", toolsNotInstalled.map(t => t.displayName).join(','))
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = tool!.statusDescription || getErrorMessage(error);
|
||||
if (errorMessage) {
|
||||
// Let the tooltip status show the errorMessage just shown so that last status is visible even after showError dialogue has been dismissed.
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: errorMessage
|
||||
};
|
||||
}
|
||||
tool!.showOutputChannel(/*preserveFocus*/false);
|
||||
} finally {
|
||||
this._installationInProgress = false;
|
||||
}
|
||||
this.resourceTypeService.startDeployment(this._selectedResourceType);
|
||||
}
|
||||
|
||||
private getAllResourceTags(): string[] {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { DeploymentProvider, instanceOfAzureSQLDBDeploymentProvider, instanceOfAzureSQLVMDeploymentProvider, instanceOfNotebookWizardDeploymentProvider, instanceOfWizardDeploymentProvider, ResourceType } from '../interfaces';
|
||||
import { DeploymentProvider, instanceOfAzureSQLDBDeploymentProvider, instanceOfAzureSQLVMDeploymentProvider, instanceOfNotebookWizardDeploymentProvider, instanceOfWizardDeploymentProvider, ResourceType, ResourceTypeOptionValue } from '../interfaces';
|
||||
import { DeployClusterWizardModel } from './deployClusterWizard/deployClusterWizardModel';
|
||||
import { DeployAzureSQLVMWizardModel } from './deployAzureSQLVMWizard/deployAzureSQLVMWizardModel';
|
||||
import { WizardPageInfo } from './wizardPageInfo';
|
||||
@@ -18,14 +18,22 @@ import { ResourceTypeModel } from './resourceTypeModel';
|
||||
import { ResourceTypePage } from './resourceTypePage';
|
||||
import { NotebookWizardModel } from './notebookWizard/notebookWizardModel';
|
||||
import { DeployAzureSQLDBWizardModel } from './deployAzureSQLDBWizard/deployAzureSQLDBWizardModel';
|
||||
import { ToolsAndEulaPage } from './toolsAndEulaSettingsPage';
|
||||
import { ResourceTypeService } from '../services/resourceTypeService';
|
||||
import { PageLessDeploymentModel } from './pageLessDeploymentModel';
|
||||
|
||||
export class ResourceTypeWizard {
|
||||
private customButtons: azdata.window.Button[] = [];
|
||||
public pages: ResourceTypePage[] = [];
|
||||
public wizardObject: azdata.window.Wizard;
|
||||
public wizardObject!: azdata.window.Wizard;
|
||||
public toDispose: vscode.Disposable[] = [];
|
||||
public model: ResourceTypeModel;
|
||||
/**
|
||||
* resourceTypeModel depends on the deployment provider and is updated from toolsAndEulaPage.
|
||||
*/
|
||||
private _model!: ResourceTypeModel;
|
||||
private _useGenerateScriptButton!: boolean;
|
||||
public toolsEulaPagePresets!: ResourceTypeOptionValue[];
|
||||
public provider!: DeploymentProvider;
|
||||
|
||||
public get useGenerateScriptButton(): boolean {
|
||||
return this._useGenerateScriptButton;
|
||||
@@ -35,36 +43,36 @@ export class ResourceTypeWizard {
|
||||
this._useGenerateScriptButton = value;
|
||||
}
|
||||
|
||||
//TODO: eventually only resourceType will be passed. For now, we are passing both the resourceType and provider
|
||||
public get model(): ResourceTypeModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public resourceType: ResourceType,
|
||||
public provider: DeploymentProvider,
|
||||
public _kubeService: IKubeService,
|
||||
public azdataService: IAzdataService,
|
||||
public notebookService: INotebookService,
|
||||
public toolsService: IToolsService,
|
||||
public platformService: IPlatformService) {
|
||||
this.wizardObject = azdata.window.createWizard(resourceType.displayName, resourceType.name, 'wide');
|
||||
this.model = this.getResourceProviderModel()!;
|
||||
}
|
||||
|
||||
|
||||
public getResourceProviderModel(): ResourceTypeModel | undefined {
|
||||
if (instanceOfWizardDeploymentProvider(this.provider)) {
|
||||
return new DeployClusterWizardModel(this.provider, this);
|
||||
} else if (instanceOfAzureSQLVMDeploymentProvider(this.provider)) {
|
||||
return new DeployAzureSQLVMWizardModel(this.provider, this);
|
||||
} else if (instanceOfNotebookWizardDeploymentProvider(this.provider)) {
|
||||
return new NotebookWizardModel(this.provider, this);
|
||||
} else if (instanceOfAzureSQLDBDeploymentProvider(this.provider)) {
|
||||
return new DeployAzureSQLDBWizardModel(this.provider, this);
|
||||
public platformService: IPlatformService,
|
||||
public resourceTypeService: ResourceTypeService) {
|
||||
/**
|
||||
* Setting the first provider from the first value of the dropdowns.
|
||||
* If there are no options (dropdowns) then the resource type has only one provider which is set as default here.
|
||||
*/
|
||||
if (resourceType.options) {
|
||||
this.provider = this.resourceType.getProvider(resourceType.options.map(option => { return { option: option.name, value: option.values[0].name }; }))!;
|
||||
} else {
|
||||
this.provider = this.resourceType.providers[0];
|
||||
}
|
||||
// other types are undefined for now.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async open(): Promise<void> {
|
||||
this.model.initialize();
|
||||
|
||||
private createNewWizard() {
|
||||
// closing the current wizard and disposing off any listeners from the closed wizard
|
||||
if (this.wizardObject) {
|
||||
this.close();
|
||||
}
|
||||
this.wizardObject = azdata.window.createWizard(this.resourceType.displayName, this.resourceType.name, 'wide');
|
||||
this.wizardObject.generateScriptButton.hidden = true; // by default generateScriptButton stays hidden.
|
||||
this.wizardObject.customButtons = this.customButtons;
|
||||
this.toDispose.push(this.wizardObject.onPageChanged(async (e) => {
|
||||
@@ -73,39 +81,65 @@ export class ResourceTypeWizard {
|
||||
//if we are changing to the first page from no page before, essentially when we load the wizard for the first time, e.lastPage is -1 and previousPage is undefined.
|
||||
await previousPage?.onLeave(new WizardPageInfo(e.lastPage, this.pages.length));
|
||||
if (this.useGenerateScriptButton) {
|
||||
if (newPage === this.pages.slice(-1)[0]) {
|
||||
// if newPage is the last page
|
||||
this.wizardObject.generateScriptButton.hidden = false; //un-hide generateScriptButton on last page
|
||||
} else {
|
||||
// if newPage is not the last page
|
||||
this.wizardObject.generateScriptButton.hidden = true; //hide generateScriptButton if it is not the last page
|
||||
}
|
||||
this.wizardObject.generateScriptButton.hidden = (newPage === this.pages.slice(-1)[0])
|
||||
? false // if newPage is the last page
|
||||
: true; // if newPage is not the last page
|
||||
}
|
||||
await newPage.onEnter(new WizardPageInfo(e.newPage, this.pages.length));
|
||||
}));
|
||||
|
||||
this.toDispose.push(this.wizardObject.doneButton.onClick(async () => {
|
||||
await this.model.onOk();
|
||||
await this._model.onOk();
|
||||
this.dispose();
|
||||
}));
|
||||
this.toDispose.push(this.wizardObject.generateScriptButton.onClick(async () => {
|
||||
await this.model.onGenerateScript();
|
||||
await this._model.onGenerateScript();
|
||||
this.dispose();
|
||||
this.wizardObject.close(); // close the wizard. This is already hooked up into doneButton, so it is not needed for that button above.
|
||||
}));
|
||||
this.toDispose.push(this.wizardObject.cancelButton.onClick(() => {
|
||||
this.model.onCancel();
|
||||
this._model.onCancel();
|
||||
this.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
private getResourceProviderModel(): ResourceTypeModel {
|
||||
if (instanceOfWizardDeploymentProvider(this.provider)) {
|
||||
return new DeployClusterWizardModel(this.provider, this);
|
||||
} else if (instanceOfAzureSQLVMDeploymentProvider(this.provider)) {
|
||||
return new DeployAzureSQLVMWizardModel(this.provider, this);
|
||||
} else if (instanceOfNotebookWizardDeploymentProvider(this.provider)) {
|
||||
return new NotebookWizardModel(this.provider, this);
|
||||
} else if (instanceOfAzureSQLDBDeploymentProvider(this.provider)) {
|
||||
return new DeployAzureSQLDBWizardModel(this.provider, this);
|
||||
} else {
|
||||
return new PageLessDeploymentModel(this.provider, this);
|
||||
}
|
||||
}
|
||||
|
||||
public async open(): Promise<void> {
|
||||
/**
|
||||
* Currently changing wizard titles and pages does not work without closing and reopening the wizard. (it makes the changes to objects but visually everything remains the same).
|
||||
* Also, the done button listener gets broken when we close and reopen the same dialog
|
||||
* For these reasons, I am creating a new wizard every time user changes the options that requires changes to the wizard's titles and pages.
|
||||
*/
|
||||
this.createNewWizard();
|
||||
this.updateModelFromProvider();
|
||||
await this.wizardObject.open();
|
||||
}
|
||||
|
||||
private updateModelFromProvider() {
|
||||
this._model = this.getResourceProviderModel();
|
||||
this._model.initialize();
|
||||
}
|
||||
|
||||
public addButton(button: azdata.window.Button) {
|
||||
this.customButtons.push(button);
|
||||
}
|
||||
|
||||
public setPages(pages: ResourceTypePage[]) {
|
||||
pages.unshift(new ToolsAndEulaPage(this));
|
||||
this.wizardObject!.pages = pages.map(p => p.pageObject);
|
||||
this.pages = pages;
|
||||
this.pages.forEach((page) => {
|
||||
@@ -121,6 +155,7 @@ export class ResourceTypeWizard {
|
||||
this.toDispose.forEach((disposable: vscode.Disposable) => {
|
||||
disposable.dispose();
|
||||
});
|
||||
this.toDispose = [];
|
||||
}
|
||||
|
||||
public registerDisposable(disposable: vscode.Disposable): void {
|
||||
@@ -134,4 +169,9 @@ export class ResourceTypeWizard {
|
||||
};
|
||||
}
|
||||
|
||||
private async close() {
|
||||
this.dispose();
|
||||
this.wizardObject.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ResourceTypeOptionValue, ToolStatus } from '../interfaces';
|
||||
import { createFlexContainer } from './modelViewUtils';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { IToolsService } from '../services/toolsService';
|
||||
import { getErrorMessage } from '../common/utils';
|
||||
import { ResourceTypePage } from './resourceTypePage';
|
||||
import { ResourceTypeWizard } from './resourceTypeWizard';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class ToolsAndEulaPage extends ResourceTypePage {
|
||||
private form!: azdata.FormBuilder;
|
||||
private view!: azdata.ModelView;
|
||||
private toolRefreshTimestamp: number = 0;
|
||||
private _optionsContainer!: azdata.FlexContainer;
|
||||
private _toolsTable!: azdata.TableComponent;
|
||||
private _optionDropDownMap: Map<string, azdata.DropDownComponent> = new Map();
|
||||
private _toolsLoadingComponent!: azdata.LoadingComponent;
|
||||
private _agreementContainer!: azdata.DivContainer;
|
||||
private _agreementCheckBox!: azdata.CheckBoxComponent;
|
||||
private _installToolButton!: azdata.ButtonComponent;
|
||||
private _installationInProgress: boolean = false;
|
||||
private _tools: ITool[] = [];
|
||||
private _eulaValidationSucceeded: boolean = false;
|
||||
private _isInitialized = false;
|
||||
private _isDoneButtonEnabled = false;
|
||||
private _resourceType: ResourceType;
|
||||
|
||||
public set resourceProvider(provider: DeploymentProvider) {
|
||||
this.wizard.provider = provider;
|
||||
}
|
||||
|
||||
public get toolsService(): IToolsService {
|
||||
return this.wizard.toolsService;
|
||||
}
|
||||
|
||||
constructor(wizard: ResourceTypeWizard) {
|
||||
super(localize('notebookWizard.toolsAndEulaPageTitle', "Deployment pre-requisites"), '', wizard);
|
||||
this._resourceType = wizard.resourceType;
|
||||
}
|
||||
|
||||
public async onEnter(): Promise<void> {
|
||||
this.wizard.wizardObject.generateScriptButton.hidden = true;
|
||||
this.wizard.wizardObject.registerNavigationValidator(async (pcInfo) => {
|
||||
if (!this._eulaValidationSucceeded && !(await this.acquireEulaAndProceed())) {
|
||||
this.wizard.wizardObject.message = {
|
||||
text: localize('deploymentDialog.FailedEulaValidation', "To proceed, you must accept the terms of the End User License Agreement(EULA)"),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false; // we return false so that the workflow does not proceed and user gets to either click acceptEulaAndSelect again or cancel
|
||||
}
|
||||
|
||||
if (!this._isDoneButtonEnabled) {
|
||||
this.wizard.wizardObject.message = {
|
||||
text: localize('deploymentDialog.FailedToolsInstallation', "Some tools were still not discovered. Please make sure that they are installed, running and discoverable"),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
if (this._isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pageObject.registerContent((view: azdata.ModelView) => {
|
||||
this.view = view;
|
||||
const tableWidth = 1126;
|
||||
this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this._agreementContainer = view.modelBuilder.divContainer().component();
|
||||
const toolColumn: azdata.TableColumn = {
|
||||
value: loc.toolText,
|
||||
width: 105
|
||||
};
|
||||
const descriptionColumn: azdata.TableColumn = {
|
||||
value: loc.descriptionText,
|
||||
width: 270
|
||||
};
|
||||
const installStatusColumn: azdata.TableColumn = {
|
||||
value: loc.statusText,
|
||||
width: 70
|
||||
};
|
||||
const versionColumn: azdata.TableColumn = {
|
||||
value: loc.versionText,
|
||||
width: 75
|
||||
};
|
||||
const minVersionColumn: azdata.TableColumn = {
|
||||
value: loc.requiredVersionText,
|
||||
width: 105
|
||||
};
|
||||
const installedPathColumn: azdata.TableColumn = {
|
||||
value: loc.discoverPathOrAdditionalInformationText,
|
||||
width: 580
|
||||
};
|
||||
this._toolsTable = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
|
||||
data: [],
|
||||
columns: [toolColumn, descriptionColumn, installStatusColumn, versionColumn, minVersionColumn, installedPathColumn],
|
||||
width: tableWidth,
|
||||
ariaLabel: loc.requiredToolsText
|
||||
}).component();
|
||||
|
||||
const toolsTableWrapper = view.modelBuilder.divContainer().withLayout({ width: tableWidth }).component();
|
||||
toolsTableWrapper.addItem(this._toolsTable, { CSSStyles: { 'border-left': '1px solid silver', 'border-top': '1px solid silver' } });
|
||||
this._toolsLoadingComponent = view.modelBuilder.loadingComponent().withItem(toolsTableWrapper).withProperties<azdata.LoadingComponentProperties>({
|
||||
loadingCompletedText: localize('deploymentDialog.loadingRequiredToolsCompleted', "Loading required tools information completed"),
|
||||
loadingText: localize('deploymentDialog.loadingRequiredTools', "Loading required tools information"),
|
||||
showText: true
|
||||
}).component();
|
||||
|
||||
|
||||
this._installToolButton = view.modelBuilder.button().withProps({
|
||||
label: loc.installToolsText,
|
||||
CSSStyles: {
|
||||
'display': 'none',
|
||||
},
|
||||
width: '100px',
|
||||
}).component();
|
||||
|
||||
this.wizard.registerDisposable(this._installToolButton.onDidClick(() => {
|
||||
this.installTools().catch(error => console.log(error));
|
||||
}));
|
||||
|
||||
this.form = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: this._optionsContainer,
|
||||
}, {
|
||||
component: this._agreementContainer,
|
||||
}, {
|
||||
component: this._toolsLoadingComponent,
|
||||
title: loc.requiredToolsText
|
||||
}, {
|
||||
component: this._installToolButton
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
return view.initializeModel(this.form!.withLayout({ width: '100%' }).component()).then(() => {
|
||||
this._agreementContainer.clearItems();
|
||||
if (this._resourceType.agreement) {
|
||||
const agreementTitle = this.view.modelBuilder.text().withProps({
|
||||
value: localize('resourceDeployment.AgreementTitle', "Accept terms of use"),
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
this._agreementContainer.addItem(agreementTitle);
|
||||
this._agreementContainer.addItem(this.createAgreementCheckbox(this._resourceType.agreement));
|
||||
} else {
|
||||
this.form.removeFormItem({
|
||||
component: this._agreementContainer
|
||||
});
|
||||
}
|
||||
|
||||
this._optionsContainer.clearItems();
|
||||
this._optionDropDownMap.clear();
|
||||
if (this._resourceType.options) {
|
||||
let resourceTypeOptions: ResourceTypeOptionValue[] = [];
|
||||
const optionsTitle = this.view.modelBuilder.text().withProps({
|
||||
value: 'Options',
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
this._optionsContainer.addItem(optionsTitle);
|
||||
this._resourceType.options.forEach((option, index) => {
|
||||
const optionLabel = this.view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: option.displayName,
|
||||
}).component();
|
||||
optionLabel.width = '150px';
|
||||
|
||||
const optionSelectBox = this.view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
|
||||
values: option.values,
|
||||
value: (this.wizard.toolsEulaPagePresets) ? this.wizard.toolsEulaPagePresets[index] : option.values[0],
|
||||
width: '300px',
|
||||
ariaLabel: option.displayName
|
||||
}).component();
|
||||
|
||||
resourceTypeOptions.push(option.values[0]);
|
||||
|
||||
this.wizard.registerDisposable(optionSelectBox.onValueChanged(async () => {
|
||||
resourceTypeOptions[index] = <ResourceTypeOptionValue>optionSelectBox.value;
|
||||
this.wizard.provider = this.getCurrentProvider();
|
||||
await this.wizard.open();
|
||||
}));
|
||||
|
||||
this._optionDropDownMap.set(option.name, optionSelectBox);
|
||||
const row = this.view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
this._optionsContainer.addItem(row);
|
||||
});
|
||||
this.wizard.toolsEulaPagePresets = resourceTypeOptions;
|
||||
} else {
|
||||
this.form.removeFormItem({
|
||||
component: this._optionsContainer
|
||||
});
|
||||
}
|
||||
|
||||
this.updateOkButtonText();
|
||||
this.updateToolsDisplayTable();
|
||||
});
|
||||
});
|
||||
this._isInitialized = true;
|
||||
}
|
||||
|
||||
|
||||
private createAgreementCheckbox(agreementInfo: AgreementInfo): azdata.FlexContainer {
|
||||
this._agreementCheckBox = this.view.modelBuilder.checkBox().withProperties<azdata.CheckBoxProperties>({
|
||||
ariaLabel: this.getAgreementDisplayText(agreementInfo),
|
||||
required: true
|
||||
}).component();
|
||||
const text = this.view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: agreementInfo.template,
|
||||
links: agreementInfo.links,
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
return createFlexContainer(this.view, [this._agreementCheckBox, text]);
|
||||
}
|
||||
|
||||
private getAgreementDisplayText(agreementInfo: AgreementInfo): string {
|
||||
// the agreement template will have {index} as placeholder for hyperlinks
|
||||
// this method will get the display text after replacing the placeholders
|
||||
let text = agreementInfo.template;
|
||||
for (let i: number = 0; i < agreementInfo.links.length; i++) {
|
||||
text = text.replace(`{${i}}`, agreementInfo.links[i].text);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private async acquireEulaAndProceed(): Promise<boolean> {
|
||||
this.wizard.wizardObject.message = { text: '' };
|
||||
let eulaAccepted = true;
|
||||
for (const tool of this._tools) {
|
||||
eulaAccepted = tool.isEulaAccepted() || await tool.promptForEula();
|
||||
if (!eulaAccepted) {
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: [tool.statusDescription!, this.wizard.wizardObject.message.text].join(EOL)
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return eulaAccepted;
|
||||
}
|
||||
|
||||
private getCurrentProvider(): DeploymentProvider {
|
||||
const options: { option: string, value: string }[] = [];
|
||||
|
||||
this._optionDropDownMap.forEach((selectBox, option) => {
|
||||
let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue;
|
||||
options.push({ option: option, value: selectedValue.name });
|
||||
});
|
||||
|
||||
this.resourceProvider = this._resourceType.getProvider(options)!;
|
||||
return this._resourceType.getProvider(options)!;
|
||||
}
|
||||
|
||||
private getCurrentOkText(): string {
|
||||
const options: { option: string, value: string }[] = [];
|
||||
|
||||
this._optionDropDownMap.forEach((selectBox, option) => {
|
||||
let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue;
|
||||
options.push({ option: option, value: selectedValue.name });
|
||||
});
|
||||
|
||||
return this._resourceType.getOkButtonText(options)!;
|
||||
}
|
||||
|
||||
private updateOkButtonText(): void {
|
||||
//handle special case when resource type has different OK button.
|
||||
let text = this.getCurrentOkText();
|
||||
this.wizard.wizardObject.doneButton.label = text || loc.select;
|
||||
}
|
||||
|
||||
|
||||
private async installTools(): Promise<void> {
|
||||
this._installToolButton.enabled = false;
|
||||
this._installationInProgress = true;
|
||||
let tool: ITool;
|
||||
try {
|
||||
const toolRequirements = this.toolRequirements;
|
||||
let toolsNotInstalled: ITool[] = [];
|
||||
for (let i: number = 0; i < toolRequirements.length; i++) {
|
||||
const toolReq = toolRequirements[i];
|
||||
tool = this.toolsService.getToolByName(toolReq.name)!;
|
||||
if (tool.autoInstallNeeded) {
|
||||
// Update the informational message
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstallingTool', "Required tool '{0}' [ {1} ] is being installed now.", tool.displayName, tool.homePage)
|
||||
};
|
||||
await this._tools[i].install();
|
||||
if (tool.status === ToolStatus.Installed && toolReq.version && !tool.isSameOrNewerThan(toolReq.version)) {
|
||||
throw new Error(
|
||||
localize('deploymentDialog.ToolDoesNotMeetVersionRequirement', "'{0}' [ {1} ] does not meet the minimum version requirement, please uninstall it and restart Azure Data Studio.",
|
||||
tool.displayName, tool.homePage
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toolsNotInstalled.push(tool);
|
||||
}
|
||||
}
|
||||
// Update the informational message
|
||||
if (toolsNotInstalled.length === 0) {
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstalledTools', "All required tools are installed now.")
|
||||
};
|
||||
this.enableDoneButton(true);
|
||||
} else {
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.PendingInstallation', "Following tools: {0} were still not discovered. Please make sure that they are installed, running and discoverable", toolsNotInstalled.map(t => t.displayName).join(','))
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = tool!.statusDescription || getErrorMessage(error);
|
||||
if (errorMessage) {
|
||||
// Let the tooltip status show the errorMessage just shown so that last status is visible even after showError dialogue has been dismissed.
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: errorMessage
|
||||
};
|
||||
}
|
||||
tool!.showOutputChannel(/*preserveFocus*/false);
|
||||
} finally {
|
||||
this._installationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected updateToolsDisplayTableData(tool: ITool) {
|
||||
this._toolsTable.data = this._toolsTable.data.map(rowData => {
|
||||
if (rowData[0] === tool.displayName) {
|
||||
return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', rowData[4]/* required version*/, tool.installationPathOrAdditionalInformation || ''];
|
||||
} else {
|
||||
return rowData;
|
||||
}
|
||||
});
|
||||
this.setUiControlsEnabled(tool.status !== ToolStatus.Installing); // if installing then disable ui controls else enable them
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param enable - if true the UiControls are set to be enabled, if not they are set to be disabled.
|
||||
*/
|
||||
private setUiControlsEnabled(enable: boolean): void {
|
||||
this._agreementContainer.enabled = enable;
|
||||
this._optionsContainer.enabled = enable;
|
||||
this.wizard.wizardObject.cancelButton.enabled = enable;
|
||||
// select and install tools buttons are controlled separately
|
||||
}
|
||||
|
||||
|
||||
protected async onComplete(): Promise<void> {
|
||||
this.toolsService.toolsForCurrentProvider = this._tools;
|
||||
}
|
||||
|
||||
private areToolsEulaAccepted(): boolean {
|
||||
// we run 'map' on each tool before doing 'every' so that we collect eula messages for all tools (instead of bailing out after 1st failure)
|
||||
this._eulaValidationSucceeded = this._tools.map(tool => {
|
||||
const eulaAccepted = tool.isEulaAccepted();
|
||||
if (!eulaAccepted) {
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: [tool.statusDescription!, this.wizard.wizardObject.message.text].join(EOL)
|
||||
};
|
||||
}
|
||||
return eulaAccepted;
|
||||
}).every(isEulaAccepted => isEulaAccepted);
|
||||
return this._eulaValidationSucceeded;
|
||||
}
|
||||
|
||||
private get toolRequirements() {
|
||||
return this.wizard.provider.requiredTools;
|
||||
}
|
||||
|
||||
|
||||
private executeToolsTableWorkflow(currentRefreshTimestamp: number, toolsLoadingErrors: string[]): void {
|
||||
// If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results
|
||||
if (this.toolRefreshTimestamp !== currentRefreshTimestamp) {
|
||||
return;
|
||||
}
|
||||
let minVersionCheckFailed = false;
|
||||
const toolsToAutoInstall: ITool[] = [];
|
||||
let messages: string[] = toolsLoadingErrors!;
|
||||
let erroredOrFailedTool: boolean = false;
|
||||
this._toolsTable.data = this.toolRequirements.map(toolRequirement => {
|
||||
const tool = this.toolsService.getToolByName(toolRequirement.name)!;
|
||||
// subscribe to onUpdateData event of the tool.
|
||||
this.wizard.registerDisposable(tool.onDidUpdateData((t: ITool) => {
|
||||
this.updateToolsDisplayTableData(t);
|
||||
}));
|
||||
|
||||
erroredOrFailedTool = erroredOrFailedTool || (tool.status === ToolStatus.Error || tool.status === ToolStatus.Failed);
|
||||
if (tool.status === ToolStatus.NotInstalled) {
|
||||
if (tool.autoInstallSupported) {
|
||||
toolsToAutoInstall.push(tool);
|
||||
}
|
||||
else {
|
||||
messages.push(localize('deploymentDialog.ToolInformation', "'{0}' was not discovered and automated installation is not currently supported. Install '{0}' manually or ensure it is started and discoverable. Once done please restart Azure Data Studio. See [{1}] .", tool.displayName, tool.homePage));
|
||||
}
|
||||
}
|
||||
else if (tool.status === ToolStatus.Installed && toolRequirement.version && !tool.isSameOrNewerThan(toolRequirement.version)) {
|
||||
minVersionCheckFailed = true;
|
||||
messages.push(localize('deploymentDialog.ToolDoesNotMeetVersionRequirement', "'{0}' [ {1} ] does not meet the minimum version requirement, please uninstall it and restart Azure Data Studio.", tool.displayName, tool.homePage));
|
||||
}
|
||||
return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', toolRequirement.version || '', tool.installationPathOrAdditionalInformation || ''];
|
||||
});
|
||||
this._installToolButton.updateProperties({
|
||||
CSSStyles: {
|
||||
'display': erroredOrFailedTool || minVersionCheckFailed || (toolsToAutoInstall.length === 0) ? 'none' : 'inline'
|
||||
}
|
||||
});
|
||||
this.enableDoneButton(!erroredOrFailedTool && messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0));
|
||||
if (messages.length !== 0) {
|
||||
if (messages.length > 1) {
|
||||
messages = messages.map(message => `• ${message}`);
|
||||
}
|
||||
messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed manually to pick up the change. You may find additional details in 'Deployments' and 'Azure Data CLI' output channels"));
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: messages.join(EOL)
|
||||
};
|
||||
}
|
||||
else if ((toolsToAutoInstall.length !== 0) && !this._installationInProgress) {
|
||||
const installationNeededHeader = toolsToAutoInstall.length === 1
|
||||
? localize('deploymentDialog.InstallToolsHintOne', "Tool: {0} is not installed, you can click the \"{1}\" button to install it.", toolsToAutoInstall[0].displayName, this._installToolButton.label)
|
||||
: localize('deploymentDialog.InstallToolsHintMany', "Tools: {0} are not installed, you can click the \"{1}\" button to install them.", toolsToAutoInstall.map(t => t.displayName).join(', '), this._installToolButton.label);
|
||||
let infoText: string[] = [installationNeededHeader];
|
||||
const informationalMessagesArray = this._tools.reduce<string[]>((returnArray, currentTool) => {
|
||||
if (currentTool.autoInstallNeeded) {
|
||||
returnArray.push(...currentTool.dependencyMessages);
|
||||
}
|
||||
return returnArray;
|
||||
}, /* initial Value of return array*/[]);
|
||||
const informationalMessagesSet: Set<string> = new Set<string>(informationalMessagesArray);
|
||||
if (informationalMessagesSet.size > 0) {
|
||||
infoText.push(...informationalMessagesSet.values());
|
||||
}
|
||||
// we don't have scenarios that have mixed type of tools - either we don't support auto install: docker, or we support auto install for all required tools
|
||||
this.wizard.wizardObject.message = {
|
||||
level: azdata.window.MessageLevel.Warning,
|
||||
text: infoText.join(EOL)
|
||||
};
|
||||
}
|
||||
if (!this.areToolsEulaAccepted()) {
|
||||
this.wizard.wizardObject.doneButton.label = loc.acceptEulaAndSelect;
|
||||
this.wizard.wizardObject.nextButton.label = loc.acceptEulaAndSelect;
|
||||
}
|
||||
this._toolsLoadingComponent.loading = false;
|
||||
}
|
||||
|
||||
private updateToolsDisplayTable(): void {
|
||||
this.toolRefreshTimestamp = new Date().getTime();
|
||||
const currentRefreshTimestamp = this.toolRefreshTimestamp;
|
||||
const headerRowHeight = 28;
|
||||
this._toolsTable.height = 25 * Math.max(this.toolRequirements.length, 1) + headerRowHeight;
|
||||
if (!this._installationInProgress) { // Wipe the informational message clean unless installation is already in progress.
|
||||
this.wizard.wizardObject.message = {
|
||||
text: ''
|
||||
};
|
||||
}
|
||||
this._installToolButton.updateProperties({
|
||||
CSSStyles: {
|
||||
'display': 'none'
|
||||
}
|
||||
});
|
||||
if (this.toolRequirements.length === 0) {
|
||||
this._toolsLoadingComponent.loading = false;
|
||||
this.enableDoneButton(true);
|
||||
this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']];
|
||||
this._tools = [];
|
||||
} else {
|
||||
this._tools = this.toolRequirements.map(toolReq => this.toolsService.getToolByName(toolReq.name)!);
|
||||
this._toolsLoadingComponent.loading = true;
|
||||
this.enableDoneButton(false);
|
||||
let toolsLoadingErrors: string[] = [];
|
||||
Promise.all(
|
||||
this._tools.map(
|
||||
tool => tool.finishInitialization().catch(() => toolsLoadingErrors.push(`${tool.displayName}:${tool.statusDescription!}`))
|
||||
)
|
||||
)
|
||||
.then(() => this.executeToolsTableWorkflow(currentRefreshTimestamp, toolsLoadingErrors))
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
}
|
||||
|
||||
private enableDoneButton(enable: boolean) {
|
||||
this._isDoneButtonEnabled = enable;
|
||||
this.wizard.wizardObject.doneButton.enabled = enable;
|
||||
this.wizard.wizardObject.nextButton.enabled = enable;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user