diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index a14ed3dd2b..ecf6e438c1 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -377,14 +377,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) { @@ -397,7 +397,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 3a743d2f86..f023a7c9b3 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -59,6 +59,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 b87a199f92..200fc7ac2f 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -240,6 +240,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 a1c09a60dd..27ee3bb26c 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -374,7 +374,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 940b9d58c9..fb9b34820c 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -15,7 +15,7 @@ export const subscriptionDescription = localize('azure.account.subscriptionDescr 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…"); @@ -35,4 +35,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 aad05ee6b8..efe50cf0de 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -9,6 +9,7 @@ import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } fr 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'; @@ -26,9 +27,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, @@ -38,32 +39,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; @@ -103,8 +102,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); @@ -114,7 +113,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"), @@ -130,7 +129,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"), @@ -288,7 +287,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}`); @@ -320,22 +319,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() {