Port updates for removing EULA acceptance checkbox from Arc deployments (#12409)

* controller dropdown field to SQL MIAA and Postgres deployment. (#12217)

* saving first draft

* throw if no controllers

* cleanup

* bug fixes

* bug fixes and caching controller access

* pr comments and bug fixes.

* fixes

* fixes

* comment fix

* remove debug prints

* comment fixes

* remove debug logs

* inputValueTransformer returns string|Promise

* PR feedback

* pr fixes

* remove _ from protected fields

* anonymous to full methods

* small fixes

(cherry picked from commit 9cf80113fc)

* fix option sources (#12387)


(cherry picked from commit fca8b85a72)

* Remove azdata eula acceptance from arc deployments (#12292)

* saving to switch tasks

* activate to exports in extApi

* working version - cleanup pending

* improve messages

* apply pr feedback from a different review

* remove unneeded strings

* redo apiService

* remove async from getVersionFromOutput

* remove _ prefix from protected fields

* error message fix

* throw specif errors from azdata extension

* arrow methods to regular methods

* pr feedback

* expand azdata extension api

* pr feedback

* remove unused var

* pr feedback

(cherry picked from commit ba44a2f02e)

Co-authored-by: Arvind Ranasaria <ranasaria@outlook.com>
This commit is contained in:
Charles Gagnon
2020-09-17 15:05:02 -07:00
committed by GitHub
parent 21bb577da8
commit 94e2016a16
44 changed files with 925 additions and 303 deletions

View File

@@ -176,11 +176,11 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave(): void {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});
setModelValues(this.inputComponents, this.wizard.model);
await setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
}
}

View File

@@ -289,8 +289,8 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave() {
setModelValues(this.inputComponents, this.wizard.model);
public async onLeave(): Promise<void> {
await setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
const variableDNSPrefixMapping: { [s: string]: string } = {};

View File

@@ -233,7 +233,7 @@ export class DeploymentProfilePage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave() {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});

View File

@@ -400,8 +400,8 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave(): void {
setModelValues(this.inputComponents, this.wizard.model);
public async onLeave(): Promise<void> {
await setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;

View File

@@ -326,7 +326,7 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
this.form.addFormItems(this.formItems);
}
public onLeave() {
public async onLeave(): Promise<void> {
this.wizard.hideCustomButtons();
this.wizard.wizardObject.message = { text: '' };
}

View File

@@ -74,7 +74,7 @@ export class TargetClusterContextPage extends WizardPageBase<DeployClusterWizard
});
}
public onLeave() {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.registerNavigationValidator((e) => {
return true;
});

View File

@@ -94,9 +94,9 @@ export class DeploymentInputDialog extends DialogBase {
});
}
protected onComplete(): void {
protected async onComplete(): Promise<void> {
const model: Model = new Model();
setModelValues(this.inputComponents, model);
await setModelValues(this.inputComponents, model);
if (instanceOfNotebookBasedDialogInfo(this.dialogInfo)) {
model.setEnvironmentVariables();
if (this.dialogInfo.runNotebook) {
@@ -110,7 +110,7 @@ export class DeploymentInputDialog extends DialogBase {
});
}
} else {
vscode.commands.executeCommand(this.dialogInfo.command, model);
await vscode.commands.executeCommand(this.dialogInfo.command, model);
}
}

View File

@@ -27,12 +27,12 @@ export abstract class DialogBase {
this.dispose();
}
private onOkButtonClicked(): void {
this.onComplete();
private async onOkButtonClicked(): Promise<void> {
await this.onComplete();
this.dispose();
}
protected onComplete(): void {
protected async onComplete(): Promise<void> {
}
protected dispose(): void {

View File

@@ -3,27 +3,28 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { azureResource } from 'azureResource';
import * as fs from 'fs';
import { EOL, homedir as os_homedir } from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { azureResource } from 'azureResource';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
import { ArcControllersOptionsSource, OptionsSourceType } from '../helpers/optionSources';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
import * as loc from '../localizedConstants';
import { apiService } from '../services/apiService';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
import { assert, getDateTimeString, getErrorMessage } from '../utils';
import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool';
import { IToolsService } from '../services/toolsService';
import { getDateTimeString, getErrorMessage, throwUnless } from '../utils';
import { WizardInfoBase } from './../interfaces';
import { Model } from './model';
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
import { IToolsService } from '../services/toolsService';
import { KubeCtlToolName, KubeCtlTool } from '../services/tools/kubeCtlTool';
const localize = nls.loadMessageBundle();
export type Validator = () => { valid: boolean, message: string };
export type InputValueTransformer = (inputValue: string) => string;
export type InputValueTransformer = (inputValue: string) => string | Promise<string>;
export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder;
export type InputComponentInfo = {
component: InputComponent;
@@ -353,7 +354,7 @@ function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata
async function processField(context: FieldContext): Promise<void> {
switch (context.fieldInfo.type) {
case FieldType.Options:
processOptionsTypeField(context);
await processOptionsTypeField(context);
break;
case FieldType.DateTimeText:
processDateTimeTextField(context);
@@ -390,12 +391,23 @@ async function processField(context: FieldContext): Promise<void> {
await processKubeStorageClassField(context);
break;
default:
throw new Error(localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", context.fieldInfo.type));
throw new Error(loc.unknownFieldTypeError(context.fieldInfo.type));
}
}
function processOptionsTypeField(context: FieldContext): void {
assert(context.fieldInfo.options !== undefined, `FieldInfo.options must be defined for FieldType:${FieldType.Options}`);
function disableControlButtons(container: azdata.window.Dialog | azdata.window.Wizard): void {
if ('okButton' in container) {
container.okButton.enabled = false;
} else {
container.doneButton.enabled = false;
container.nextButton.enabled = false;
container.backButton.enabled = false;
container.customButtons.forEach(b => b.enabled = false);
}
}
async function processOptionsTypeField(context: FieldContext): Promise<void> {
throwUnless(context.fieldInfo.options !== undefined, loc.optionsNotDefined(context.fieldInfo.type));
if (Array.isArray(context.fieldInfo.options)) {
context.fieldInfo.options = <OptionsInfo>{
values: context.fieldInfo.options,
@@ -403,17 +415,69 @@ function processOptionsTypeField(context: FieldContext): void {
optionsType: OptionsType.Dropdown
};
}
assert(typeof context.fieldInfo.options === 'object', `FieldInfo.options must be an object if it is not an array`);
assert('optionsType' in context.fieldInfo.options, `When FieldInfo.options is an object it must have 'optionsType' property`);
throwUnless(typeof context.fieldInfo.options === 'object', loc.optionsNotObjectOrArray);
throwUnless('optionsType' in context.fieldInfo.options, loc.optionsTypeNotFound);
if (context.fieldInfo.options.source) {
try {
let optionsSource: IOptionsSource;
switch (context.fieldInfo.options.source.type) {
case OptionsSourceType.ArcControllersOptionsSource:
optionsSource = new ArcControllersOptionsSource(context.fieldInfo.options.source.variableNames, context.fieldInfo.options.source.type);
break;
default:
throw new Error(loc.noOptionsSourceDefined(context.fieldInfo.options.source.type));
}
context.fieldInfo.options.source = optionsSource;
context.fieldInfo.options.values = await context.fieldInfo.options.source.getOptions();
}
catch (e) {
disableControlButtons(context.container);
context.container.message = {
text: getErrorMessage(e),
description: '',
level: azdata.window.MessageLevel.Error
};
context.fieldInfo.options.values = [];
}
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
}
let optionsComponent: InputComponent;
if (context.fieldInfo.options.optionsType === OptionsType.Radio) {
processRadioOptionsTypeField(context);
optionsComponent = await processRadioOptionsTypeField(context);
} else {
assert(context.fieldInfo.options.optionsType === OptionsType.Dropdown, `When optionsType is not ${OptionsType.Radio} then it must be ${OptionsType.Dropdown}`);
processDropdownOptionsTypeField(context);
throwUnless(context.fieldInfo.options.optionsType === OptionsType.Dropdown, loc.optionsTypeRadioOrDropdown);
optionsComponent = processDropdownOptionsTypeField(context);
}
if (context.fieldInfo.options.source) {
const optionsSource = context.fieldInfo.options.source;
for (const key of Object.keys(optionsSource.variableNames ?? {})) {
context.fieldInfo.subFields!.push({
label: context.fieldInfo.label,
variableName: optionsSource.variableNames[key]
});
context.onNewInputComponentCreated(optionsSource.variableNames[key], {
component: optionsComponent,
inputValueTransformer: async (controllerName: string) => {
try {
const variableValue = await optionsSource.getVariableValue(key, controllerName);
return variableValue;
} catch (e) {
disableControlButtons(context.container);
context.container.message = {
text: getErrorMessage(e),
description: '',
level: azdata.window.MessageLevel.Error
};
return '';
}
},
isPassword: optionsSource.getIsPassword(key)
});
}
}
}
function processDropdownOptionsTypeField(context: FieldContext): void {
function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDownComponent {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const options = context.fieldInfo.options as OptionsInfo;
const dropdown = createDropdown(context.view, {
@@ -427,6 +491,7 @@ function processDropdownOptionsTypeField(context: FieldContext): void {
dropdown.fireOnTextChange = true;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: dropdown });
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo);
return dropdown;
}
function processDateTimeTextField(context: FieldContext): void {
@@ -579,8 +644,8 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
const readOnlyField = processReadonlyTextField(context, false /*allowEvaluation*/);
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, {
component: readOnlyField.text!,
inputValueTransformer: () => {
readOnlyField.text!.value = substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
inputValueTransformer: async () => {
readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
return readOnlyField.text?.value!;
}
});
@@ -596,14 +661,15 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
* @param inputValue
* @param inputComponents
*/
function substituteVariableValues(inputComponents: InputComponents, inputValue?: string): string | undefined {
Object.keys(inputComponents)
async function substituteVariableValues(inputComponents: InputComponents, inputValue?: string): Promise<string | undefined> {
await Promise.all(Object.keys(inputComponents)
.filter(key => key.startsWith(NoteBookEnvironmentVariablePrefix))
.forEach(key => {
const value = getInputComponentValue(inputComponents, key) ?? '<undefined>';
.map(async key => {
const value = (await getInputComponentValue(inputComponents, key)) ?? '<undefined>';
const re: RegExp = new RegExp(`\\\$\\\(${key}\\\)`, 'gi');
inputValue = inputValue?.replace(re, value);
});
})
);
return inputValue;
}
@@ -984,7 +1050,7 @@ async function handleSelectedAccountChanged(
}
try {
const response = await (await apiService.getAzurecoreApi()).getSubscriptions(selectedAccount, true);
const response = await apiService.azurecoreApi.getSubscriptions(selectedAccount, true);
if (!response) {
return;
}
@@ -1099,7 +1165,7 @@ async function handleSelectedSubscriptionChanged(context: AzureAccountFieldConte
return;
}
try {
const response = await (await apiService.getAzurecoreApi()).getResourceGroups(selectedAccount, selectedSubscription, true);
const response = await apiService.azurecoreApi.getResourceGroups(selectedAccount, selectedSubscription, true);
if (!response) {
return;
}
@@ -1150,8 +1216,7 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext):
width: context.fieldInfo.labelWidth,
cssStyles: context.fieldInfo.labelCSSStyles
});
const azurecoreApi = await apiService.getAzurecoreApi();
const locationValues = context.fieldInfo.locations?.map(l => { return { name: l, displayName: azurecoreApi.getRegionDisplayName(l) }; });
const locationValues = context.fieldInfo.locations?.map(l => { return { name: l, displayName: apiService.azurecoreApi.getRegionDisplayName(l) }; });
const locationDropdown = createDropdown(context.view, {
defaultValue: locationValues?.find(l => l.name === context.fieldInfo.defaultValue),
width: context.fieldInfo.inputWidth,
@@ -1174,7 +1239,7 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext):
label: label.value!,
variableName: context.fieldInfo.displayLocationVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown, inputValueTransformer: (value => azurecoreApi.getRegionDisplayName(value)) });
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown, inputValueTransformer: (value => apiService.azurecoreApi.getRegionDisplayName(value)) });
}
addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo);
return locationDropdown;
@@ -1207,14 +1272,14 @@ export function getPasswordMismatchMessage(fieldName: string): string {
return localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldName);
}
export function setModelValues(inputComponents: InputComponents, model: Model): void {
Object.keys(inputComponents).forEach(key => {
const value = getInputComponentValue(inputComponents, key);
export async function setModelValues(inputComponents: InputComponents, model: Model): Promise<void> {
await Promise.all(Object.keys(inputComponents).map(async key => {
const value = await getInputComponentValue(inputComponents, key);
model.setPropertyValue(key, value);
});
}));
}
function getInputComponentValue(inputComponents: InputComponents, key: string): string | undefined {
async function getInputComponentValue(inputComponents: InputComponents, key: string): Promise<string | undefined> {
const input = inputComponents[key].component;
if (input === undefined) {
return undefined;
@@ -1236,7 +1301,10 @@ function getInputComponentValue(inputComponents: InputComponents, key: string):
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value || '');
value = inputValueTransformer(value ?? '');
if (typeof value !== 'string') {
value = await value;
}
}
return value;
}

View File

@@ -58,7 +58,7 @@ export class NotebookWizard extends WizardBase<NotebookWizard, NotebookWizardPag
}
protected async onOk(): Promise<void> {
setModelValues(this.inputComponents, this.model);
await setModelValues(this.inputComponents, this.model);
const env: NodeJS.ProcessEnv = {};
this.model.setEnvironmentVariables(env, (varName) => {
const isPassword = !!this.inputComponents[varName]?.isPassword;

View File

@@ -33,7 +33,7 @@ export class NotebookWizardAutoSummaryPage extends NotebookWizardPage {
});
}
public onLeave(): void {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.message = { text: '' };
}

View File

@@ -57,7 +57,7 @@ export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
});
}
public onLeave(): void {
public async onLeave(): Promise<void> {
// The following callback registration clears previous navigation validators.
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
@@ -66,7 +66,7 @@ export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
public async onEnter(): Promise<void> {
if (this.pageInfo.isSummaryPage) {
setModelValues(this.wizard.inputComponents, this.wizard.model);
await setModelValues(this.wizard.inputComponents, this.wizard.model);
}
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {

View File

@@ -26,6 +26,7 @@ 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[] = [];
@@ -37,10 +38,15 @@ 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._toDispose.push(this._installToolButton.onClick(() => {
this.installTools().catch(error => console.log(error));
}));
this._dialogObject.customButtons = [this._installToolButton];
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._installToolButton.hidden = true;
this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select");
this._dialogObject.okButton.enabled = false; // this is enabled after all tools are discovered.
@@ -270,12 +276,12 @@ 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._dialogObject.okButton.enabled = !erroredOrFailedTool && messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0) && this.validateToolsEula();
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 after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in 'Deployments' output channel"));
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 'azdata' output channels"));
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: messages.join(EOL)
@@ -305,6 +311,21 @@ export class ResourceTypePickerDialog extends DialogBase {
this._toolsLoadingComponent.loading = false;
}
private validateToolsEula(): boolean {
const validationSucceeded = this._tools.every(tool => {
const eulaValidated = tool.validateEula();
if (!eulaValidated) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: tool.statusDescription!
};
}
return eulaValidated;
});
this._recheckEulaButton.hidden = validationSucceeded;
return validationSucceeded;
}
private get toolRequirements() {
return this.getCurrentProvider().requiredTools;
}
@@ -347,7 +368,7 @@ export class ResourceTypePickerDialog extends DialogBase {
return this._selectedResourceType.getProvider(options)!;
}
protected onComplete(): void {
protected async onComplete(): Promise<void> {
this.toolsService.toolsForCurrentProvider = this._tools;
this.resourceTypeService.startDeployment(this.getCurrentProvider());
}

View File

@@ -31,7 +31,7 @@ export abstract class WizardBase<T, P extends WizardPageBase<T>, M extends Model
this.toDispose.push(this.wizardObject.onPageChanged(async (e) => {
let previousPage = this.pages[e.lastPage];
let newPage = this.pages[e.newPage];
previousPage.onLeave();
await previousPage.onLeave();
await newPage.onEnter();
}));

View File

@@ -25,7 +25,7 @@ export abstract class WizardPageBase<T> {
public async onEnter(): Promise<void> { }
public onLeave(): void { }
public async onLeave(): Promise<void> { }
public abstract initialize(): void;