Skip to content

Commit

Permalink
Improved behavior for accepting EULA. (#12453)
Browse files Browse the repository at this point in the history
* working version of overloading "select" button

* promptForEula to use showErrorMessage

* make parameter optional in promptForEula

* remove test code

* PR feedback

* eula to EULA

* minor fix
  • Loading branch information
ranasaria authored and halerankin committed Sep 25, 2020
1 parent 1103566 commit b6fbc16
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 35 deletions.
12 changes: 7 additions & 5 deletions extensions/azdata/src/azdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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<boolean> {
export async function promptForEula(memento: vscode.Memento, userRequested: boolean = false, requireUserAction: boolean = false): Promise<boolean> {
let response: string | undefined = loc.no;
const config = <AzdataDeployOption>getConfig(azdataAcceptEulaKey);
if (userRequested) {
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions extensions/azdata/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata

return {
isEulaAccepted: () => !!context.globalState.get<boolean>(constants.eulaAccepted),
promptForEula: (onError: boolean = true): Promise<boolean> => promptForEula(context.globalState, true /* userRequested */, onError),
azdata: {
arc: {
dc: {
Expand Down
13 changes: 13 additions & 0 deletions extensions/azdata/src/typings/azdata-ext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>
}
}
3 changes: 2 additions & 1 deletion extensions/resource-deployment/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,8 @@ export interface ITool {
finishInitialization(): Promise<void>;
install(): Promise<void>;
isSameOrNewerThan(version: string): boolean;
validateEula(): boolean;
isEulaAccepted(): boolean;
promptForEula(): Promise<boolean>;
}

export const enum BdcDeploymentType {
Expand Down
6 changes: 4 additions & 2 deletions extensions/resource-deployment/src/localizedConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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…");
Expand All @@ -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");
10 changes: 9 additions & 1 deletion extensions/resource-deployment/src/services/tools/azdataTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -54,6 +54,14 @@ export class AzdataTool extends ToolBase {
}
}

public async promptForEula(): Promise<boolean> {
const eulaAccepted = await apiService.azdataApi.promptForEula();
if (!eulaAccepted) {
this.setStatusDescription(loc.azdataEulaDeclined);
}
return eulaAccepted;
}

/* unused */
protected get versionCommand(): Command {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> { return Promise.resolve(true); }

public get dependencyMessages(): string[] {
return (this.dependenciesByOsType.get(this.osDistribution) || []).map((msgType: dependencyType) => messageByDependencyType.get(msgType)!);
Expand Down
68 changes: 43 additions & 25 deletions extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand All @@ -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;
}
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 isValid;
return true;
});
tab.registerContent((view: azdata.ModelView) => {
const tableWidth = 1126;
Expand Down Expand Up @@ -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);
Expand All @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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<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() {
Expand Down

0 comments on commit b6fbc16

Please sign in to comment.