diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index 0136ae5fff..b521d29275 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -9,13 +9,13 @@ import * as os from 'os'; import * as path from 'path'; import { SemVer } from 'semver'; import * as vscode from 'vscode'; +import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo'; import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess'; import { HttpClient } from './common/httpClient'; import Logger from './common/logger'; import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils'; import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants'; import * as loc from './localizedConstants'; -import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo'; const enum AzdataDeployOption { dontPrompt = 'dontPrompt', @@ -414,14 +414,14 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = } /** - * Prompts user to accept EULA it if was not previously accepted. Stores and returns the user response to EULA prompt. + * Prompts user to accept EULA. Stores and returns the user response to EULA prompt. * @param memento - memento where the user response is stored. * @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system + * @param requireUserAction - if the prompt is required to be acted upon by the user. This is typically 'true' when this method is called to address an Error when the EULA needs to be accepted to proceed. * pre-requisite, the calling code has to ensure that the eula has not yet been previously accepted by the user. * returns true if the user accepted the EULA. */ - -export async function promptForEula(memento: vscode.Memento, userRequested: boolean = false): Promise { +export async function promptForEula(memento: vscode.Memento, userRequested: boolean = false, requireUserAction: boolean = false): Promise { let response: string | undefined = loc.no; const config = getConfig(azdataAcceptEulaKey); if (userRequested) { @@ -434,7 +434,9 @@ export async function promptForEula(memento: vscode.Memento, userRequested: bool if (config === AzdataDeployOption.prompt || userRequested) { Logger.show(); Logger.log(loc.promptForEulaLog(microsoftPrivacyStatementUrl, eulaUrl)); - response = await vscode.window.showInformationMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses); + response = requireUserAction + ? await vscode.window.showErrorMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses) + : await vscode.window.showInformationMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses); Logger.log(loc.userResponseToEulaPrompt(response)); } if (response === loc.doNotAskAgain) { diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts index 08f043b8ac..6f83f7f279 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -60,6 +60,7 @@ export async function activate(context: vscode.ExtensionContext): Promise !!context.globalState.get(constants.eulaAccepted), + promptForEula: (onError: boolean = true): Promise => promptForEula(context.globalState, true /* userRequested */, onError), azdata: { arc: { dc: { diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts index 935e69e938..035734aaa1 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -276,6 +276,19 @@ declare module 'azdata-ext' { export interface IExtension { azdata: IAzdataApi; + + /** + * returns true if AZDATA CLI EULA has been previously accepted by the user. + */ isEulaAccepted(): boolean; + + /** + * Prompts user to accept EULA. Stores and returns the user response to EULA prompt. + * @param requireUserAction - if the prompt is required to be acted upon by the user. This is typically 'true' when this method is called to address an Error when the EULA needs to be accepted to proceed. + * + * pre-requisite, the calling code has to ensure that the EULA has not yet been previously accepted by the user. The code can use @see isEulaAccepted() call to ascertain this. + * returns true if the user accepted the EULA. + */ + promptForEula(requireUserAction?: boolean): Promise } } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index f9773f73d2..8c1e4501c4 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -389,7 +389,8 @@ export interface ITool { finishInitialization(): Promise; install(): Promise; isSameOrNewerThan(version: string): boolean; - validateEula(): boolean; + isEulaAccepted(): boolean; + promptForEula(): Promise; } export const enum BdcDeploymentType { diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index 4d6868694c..af6d852a0e 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -14,7 +14,7 @@ export const subscription = localize('azure.account.subscription', "Subscription export const resourceGroup = localize('azure.account.resourceGroup', "Resource Group"); export const location = localize('azure.account.location', "Azure Location"); export const browse = localize('filePicker.browse', "Browse"); -export const select = localize('filePicker.select', "Select"); +export const select = localize('button.label', "Select"); export const kubeConfigFilePath = localize('kubeConfigClusterPicker.kubeConfigFilePath', "Kube config file path"); export const clusterContextNotFound = localize('kubeConfigClusterPicker.clusterContextNotFound', "No cluster context information found"); export const signIn = localize('azure.signin', "Sign in…"); @@ -34,4 +34,6 @@ export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotD export const optionsNotObjectOrArray = localize('optionsNotObjectOrArray', "FieldInfo.options must be an object if it is not an array"); export const optionsTypeNotFound = localize('optionsTypeNotFound', "When FieldInfo.options is an object it must have 'optionsType' property"); export const optionsTypeRadioOrDropdown = localize('optionsTypeRadioOrDropdown', "When optionsType is not {0} then it must be {1}", OptionsType.Radio, OptionsType.Dropdown); -export const azdataEulaNotAccepted = localize('azdataEulaNotAccepted', "Deployment cannot continue. Azure Data CLI license terms have not been accepted. Execute the command: [Azure Data CLI: Accept EULA] to accept EULA to enable the features that requires Azure Data CLI."); +export const azdataEulaNotAccepted = localize('azdataEulaNotAccepted', "Deployment cannot continue. Azure Data CLI license terms have not yet been accepted. Please accept the EULA to enable the features that requires Azure Data CLI."); +export const azdataEulaDeclined = localize('azdataEulaDeclined', "Deployment cannot continue. Azure Data CLI license terms were declined.You can either Accept EULA to continue or Cancel this operation"); +export const acceptEulaAndSelect = localize('deploymentDialog.RecheckEulaButton', "Accept EULA & Select"); diff --git a/extensions/resource-deployment/src/services/tools/azdataTool.ts b/extensions/resource-deployment/src/services/tools/azdataTool.ts index 307db0dbe9..5b6399645d 100644 --- a/extensions/resource-deployment/src/services/tools/azdataTool.ts +++ b/extensions/resource-deployment/src/services/tools/azdataTool.ts @@ -45,7 +45,7 @@ export class AzdataTool extends ToolBase { return 'https://docs.microsoft.com/sql/big-data-cluster/deploy-install-azdata'; } - public validateEula(): boolean { + public isEulaAccepted(): boolean { if (apiService.azdataApi.isEulaAccepted()) { return true; } else { @@ -54,6 +54,14 @@ export class AzdataTool extends ToolBase { } } + public async promptForEula(): Promise { + const eulaAccepted = await apiService.azdataApi.promptForEula(); + if (!eulaAccepted) { + this.setStatusDescription(loc.azdataEulaDeclined); + } + return eulaAccepted; + } + /* unused */ protected get versionCommand(): Command { return { diff --git a/extensions/resource-deployment/src/services/tools/toolBase.ts b/extensions/resource-deployment/src/services/tools/toolBase.ts index 76080749ec..e1a24e4f16 100644 --- a/extensions/resource-deployment/src/services/tools/toolBase.ts +++ b/extensions/resource-deployment/src/services/tools/toolBase.ts @@ -58,7 +58,9 @@ export abstract class ToolBase implements ITool { protected abstract readonly versionCommand: Command; - public validateEula(): boolean { return true; } + public isEulaAccepted(): boolean { return true; } + + public promptForEula(): Promise { return Promise.resolve(true); } public get dependencyMessages(): string[] { return (this.dependenciesByOsType.get(this.osDistribution) || []).map((msgType: dependencyType) => messageByDependencyType.get(msgType)!); diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 0118f3856b..fea5a3e1d4 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -10,6 +10,7 @@ import { select } from '../localizedConstants'; import { IResourceTypeService } from '../services/resourceTypeService'; import { IToolsService } from '../services/toolsService'; import { getErrorMessage } from '../utils'; +import * as loc from './../localizedConstants'; import { DialogBase } from './dialogBase'; import { createFlexContainer } from './modelViewUtils'; @@ -27,9 +28,9 @@ export class ResourceTypePickerDialog extends DialogBase { private _agreementContainer!: azdata.DivContainer; private _agreementCheckboxChecked: boolean = false; private _installToolButton: azdata.window.Button; - private _recheckEulaButton: azdata.window.Button; private _installationInProgress: boolean = false; private _tools: ITool[] = []; + private _eulaValidationSucceeded: boolean = false; constructor( private toolsService: IToolsService, @@ -39,32 +40,30 @@ export class ResourceTypePickerDialog extends DialogBase { super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true); this._selectedResourceType = defaultResourceType; this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools")); - this._recheckEulaButton = azdata.window.createButton(localize('deploymentDialog.RecheckEulaButton', "Validate EULA")); - this._recheckEulaButton.hidden = true; this._toDispose.push(this._installToolButton.onClick(() => { this.installTools().catch(error => console.log(error)); })); - this._toDispose.push(this._recheckEulaButton.onClick(() => { - this._dialogObject.message = { text: '' }; // clear any previous message. - this._dialogObject.okButton.enabled = this.validateToolsEula(); // re-enable the okButton if validation succeeds. - })); - this._dialogObject.customButtons = [this._installToolButton, this._recheckEulaButton]; + this._dialogObject.customButtons = [this._installToolButton]; this._installToolButton.hidden = true; - this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select"); + 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(() => { + 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; } - return isValid; + 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; @@ -104,8 +103,8 @@ export class ResourceTypePickerDialog extends DialogBase { iconPosition: 'left' }).component(); this._toDispose.push(this._cardGroup.onSelectionChanged(({ cardId }) => { - this._recheckEulaButton.hidden = true; - this._dialogObject.okButton.enabled = true; + this._dialogObject.message = { text: '' }; + this._dialogObject.okButton.label = loc.select; const resourceType = resourceTypes.find(rt => { return rt.name === cardId; }); if (resourceType) { this.selectResourceType(resourceType); @@ -115,7 +114,7 @@ export class ResourceTypePickerDialog extends DialogBase { this._agreementContainer = view.modelBuilder.divContainer().component(); const toolColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolNameColumnHeader', "Tool"), - width: 80 + width: 105 }; const descriptionColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolDescriptionColumnHeader', "Description"), @@ -131,7 +130,7 @@ export class ResourceTypePickerDialog extends DialogBase { }; const minVersionColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolMinimumVersionColumnHeader', "Required Version"), - width: 95 + width: 105 }; const installedPathColumn: azdata.TableColumn = { value: localize('deploymentDialog.toolDiscoveredPathColumnHeader', "Discovered Path or Additional Information"), @@ -283,7 +282,7 @@ export class ResourceTypePickerDialog extends DialogBase { 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) && this.validateToolsEula(); + 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}`); @@ -315,22 +314,41 @@ export class ResourceTypePickerDialog extends DialogBase { text: infoText.join(EOL) }; } + if (!this.areToolsEulaAccepted()) { + this._dialogObject.okButton.label = loc.acceptEulaAndSelect; + } this._toolsLoadingComponent.loading = false; } - private validateToolsEula(): boolean { - const validationSucceeded = this._tools.every(tool => { - const eulaValidated = tool.validateEula(); - if (!eulaValidated) { + 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! + text: [tool.statusDescription!, this._dialogObject.message.text].join(EOL) }; } - return eulaValidated; - }); - this._recheckEulaButton.hidden = validationSucceeded; - return validationSucceeded; + return eulaAccepted; + }).every(isEulaAccepted => isEulaAccepted); + return this._eulaValidationSucceeded; + } + + private async acquireEulaAndProceed(): Promise { + 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() {