mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Improved Validations for ARC Wizards (#12945)
This commit is contained in:
@@ -40,6 +40,15 @@ export function setEnvironmentVariablesForInstallPaths(tools: ITool[], env: Node
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if input is undefined or empty
|
||||
*
|
||||
* @param input - input value to test
|
||||
*/
|
||||
export function isUndefinedOrEmpty(input: any): boolean {
|
||||
return input === undefined || (typeof input === 'string' && input.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an Error with given {@link message} unless {@link condition} is true.
|
||||
* This also tells the typescript compiler that the condition is 'truthy' in the remainder of the scope
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import * as azdata from 'azdata';
|
||||
import { IOptionsSourceProvider } from 'resource-deployment';
|
||||
import * as vscode from 'vscode';
|
||||
import { ValidationInfo } from './ui/validation/validations';
|
||||
|
||||
export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_';
|
||||
|
||||
@@ -284,9 +285,6 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase {
|
||||
defaultValue?: string;
|
||||
confirmationRequired?: boolean;
|
||||
confirmationLabel?: string;
|
||||
textValidationRequired?: boolean;
|
||||
textValidationRegex?: string;
|
||||
textValidationDescription?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
required?: boolean;
|
||||
@@ -302,6 +300,7 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase {
|
||||
isEvaluated?: boolean;
|
||||
valueLookup?: string; // for fetching dropdown options
|
||||
validationLookup?: string // for fetching text field validations
|
||||
validations?: ValidationInfo[];
|
||||
}
|
||||
|
||||
export interface KubeClusterContextFieldInfo extends FieldInfo {
|
||||
|
||||
@@ -7,12 +7,15 @@ import * as azdata from 'azdata';
|
||||
import 'mocha';
|
||||
import * as should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
import { createValidation, GreaterThanOrEqualsValidation, IntegerValidation, LessThanOrEqualsValidation, RegexValidation, validateInputBoxComponent, Validation, ValidationType, ValidationValueType } from '../../../ui/validation/validations';
|
||||
import * as vscode from 'vscode';
|
||||
import { InputValueType } from '../../../ui/modelViewUtils';
|
||||
import { createValidation, GreaterThanOrEqualsValidation, IntegerValidation, LessThanOrEqualsValidation, RegexValidation, validateInputBoxComponent, Validation, ValidationType } from '../../../ui/validation/validations';
|
||||
|
||||
const inputBox = <azdata.InputBoxComponent>{
|
||||
updateProperty(key: string, value: any) { }
|
||||
};
|
||||
let inputBoxStub: sinon.SinonStub;
|
||||
const validationMessage = 'The field value is not valid';
|
||||
const testValidations = [
|
||||
{
|
||||
type: ValidationType.IsInteger,
|
||||
@@ -34,27 +37,34 @@ const testValidations = [
|
||||
target: 'field1'
|
||||
}
|
||||
];
|
||||
let onValidityChangedEmitter: vscode.EventEmitter<boolean>;
|
||||
|
||||
describe('Validation', () => {
|
||||
beforeEach('validation setup', () => {
|
||||
sinon.restore(); //cleanup all previously defined sinon mocks
|
||||
inputBoxStub = sinon.stub(inputBox, 'updateProperty').resolves();
|
||||
onValidityChangedEmitter = new vscode.EventEmitter<boolean>(); // recreate for every test so that any previous subscriptions on the event are cleared out.
|
||||
});
|
||||
describe('createValidation and validate input Box', () => {
|
||||
beforeEach(() => {
|
||||
sinon.restore(); //cleanup all previously defined sinon mocks
|
||||
inputBoxStub = sinon.stub(inputBox, 'updateProperty').resolves();
|
||||
});
|
||||
testValidations.forEach(testObj => {
|
||||
it(`validationType: ${testObj.type}`, async () => {
|
||||
const validation = createValidation(testObj, async () => undefined, async (_varName: string) => undefined);
|
||||
const validation = createValidation(
|
||||
testObj,
|
||||
async (isValid) => (isValid) ? inputBox.updateProperty('validationErrorMessage', undefined) : inputBox.updateProperty('validationErrorMessage', validationMessage),
|
||||
async () => undefined,
|
||||
async (_varName: string) => undefined,
|
||||
(_variableName) => onValidityChangedEmitter.event,
|
||||
(_disposable: vscode.Disposable) => { }
|
||||
);
|
||||
switch (testObj.type) {
|
||||
case ValidationType.IsInteger: should(validation).be.instanceOf(IntegerValidation); break;
|
||||
case ValidationType.Regex: should(validation).be.instanceOf(RegexValidation); break;
|
||||
case ValidationType.LessThanOrEqualsTo: should(validation).be.instanceOf(LessThanOrEqualsValidation); break;
|
||||
case ValidationType.GreaterThanOrEqualsTo: should(validation).be.instanceOf(GreaterThanOrEqualsValidation); break;
|
||||
default: console.log(`unexpected validation type: ${testObj.type}`); break;
|
||||
}
|
||||
should(await validateInputBoxComponent(inputBox, [validation])).be.false();
|
||||
should(await validateInputBoxComponent(inputBox, [validation])).be.true(); // undefined and '' values are valid so validation should return true. This allows for fields that are not required
|
||||
should(inputBoxStub.calledOnce).be.true();
|
||||
should(inputBoxStub.getCall(0).args[0]).equal('validationErrorMessage');
|
||||
should(inputBoxStub.getCall(0).args[1]).equal(testObj.description);
|
||||
should(inputBoxStub.getCall(0).args[1]).be.undefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -68,7 +78,8 @@ describe('Validation', () => {
|
||||
{ value: 3.14, expected: false },
|
||||
{ value: '3.14e2', expected: true },
|
||||
{ value: 3.14e2, expected: true },
|
||||
{ value: undefined, expected: false },
|
||||
{ value: undefined, expected: true },
|
||||
{ value: '', expected: true },
|
||||
{ value: NaN, expected: false },
|
||||
].forEach((testObj) => {
|
||||
const displayTestValue = getDisplayString(testObj.value);
|
||||
@@ -76,6 +87,7 @@ describe('Validation', () => {
|
||||
const validationDescription = `value: ${displayTestValue} was not an integer`;
|
||||
const validation = new IntegerValidation(
|
||||
{ type: ValidationType.IsInteger, description: validationDescription },
|
||||
async (isValid) => (isValid) ? inputBox.updateProperty('validationErrorMessage', undefined) : inputBox.updateProperty('validationErrorMessage', validationMessage),
|
||||
async () => testObj.value
|
||||
);
|
||||
await testValidation(validation, testObj, validationDescription);
|
||||
@@ -94,13 +106,15 @@ describe('Validation', () => {
|
||||
{ value: '3.14e2', expected: false },
|
||||
{ value: 3.14e2, expected: true }, // value of 3.14e2 literal is 342 which in string matches the testRegex
|
||||
{ value: 'arbitraryString', expected: false },
|
||||
{ value: undefined, expected: false },
|
||||
{ value: undefined, expected: true },
|
||||
{ value: '', expected: true },
|
||||
].forEach(testOb => {
|
||||
const displayTestValue = getDisplayString(testOb.value);
|
||||
it(`regex: /${testRegex}/, testValue:${displayTestValue}, expect result: ${testOb.expected}`, async () => {
|
||||
const validationDescription = `value:${displayTestValue} did not match the regex:/${testRegex}/`;
|
||||
const validation = new RegexValidation(
|
||||
{ type: ValidationType.IsInteger, description: validationDescription, regex: testRegex },
|
||||
async (isValid) => (isValid) ? inputBox.updateProperty('validationErrorMessage', undefined) : inputBox.updateProperty('validationErrorMessage', validationMessage),
|
||||
async () => testOb.value
|
||||
);
|
||||
await testValidation(validation, testOb, validationDescription);
|
||||
@@ -137,13 +151,21 @@ describe('Validation', () => {
|
||||
{ value: 342.15, targetValue: 342.15, expected: true },
|
||||
|
||||
|
||||
// undefined values - if one operand is undefined result is always false
|
||||
{ value: undefined, targetValue: '42', expected: false },
|
||||
{ value: undefined, targetValue: 42, expected: false },
|
||||
{ value: '42', targetValue: undefined, expected: false },
|
||||
{ value: 42, targetValue: undefined, expected: false },
|
||||
{ value: undefined, targetValue: undefined, expected: false },
|
||||
// undefined values - if one operand is undefined result is always true - this is to allow fields that are not a required value to be valid.
|
||||
{ value: undefined, targetValue: '42', expected: true },
|
||||
{ value: undefined, targetValue: 42, expected: true },
|
||||
{ value: '42', targetValue: undefined, expected: true },
|
||||
{ value: 42, targetValue: undefined, expected: true },
|
||||
{ value: undefined, targetValue: '', expected: true },
|
||||
{ value: undefined, targetValue: undefined, expected: true },
|
||||
|
||||
// '' values - if one operand is '' result is always true - this is to allow fields that are not a required value to be valid.
|
||||
{ value: '', targetValue: '42', expected: true },
|
||||
{ value: '', targetValue: 42, expected: true },
|
||||
{ value: '42', targetValue: '', expected: true },
|
||||
{ value: 42, targetValue: '', expected: true },
|
||||
{ value: '', targetValue: undefined, expected: true },
|
||||
{ value: '', targetValue: '', expected: true },
|
||||
].forEach(testObj => {
|
||||
const displayTestValue = getDisplayString(testObj.value);
|
||||
const displayTargetValue = getDisplayString(testObj.targetValue);
|
||||
@@ -151,8 +173,11 @@ describe('Validation', () => {
|
||||
const validationDescription = `${displayTestValue} did not test as <= ${displayTargetValue}`;
|
||||
const validation = new LessThanOrEqualsValidation(
|
||||
{ type: ValidationType.IsInteger, description: validationDescription, target: targetVariableName },
|
||||
async (isValid) => (isValid) ? inputBox.updateProperty('validationErrorMessage', undefined) : inputBox.updateProperty('validationErrorMessage', validationMessage),
|
||||
async () => testObj.value,
|
||||
async (_variableName: string) => testObj.targetValue
|
||||
async (_variableName: string) => testObj.targetValue,
|
||||
(_variableName) => onValidityChangedEmitter.event,
|
||||
(_disposable) => { } // do nothing with the disposable for the test.
|
||||
);
|
||||
await testValidation(validation, testObj, validationDescription);
|
||||
});
|
||||
@@ -181,12 +206,21 @@ describe('Validation', () => {
|
||||
{ value: '342.15', targetValue: 342.15, expected: true },
|
||||
{ value: 342.15, targetValue: 342.15, expected: true },
|
||||
|
||||
// undefined values - if one operand is undefined result is always false
|
||||
{ value: undefined, targetValue: '42', expected: false },
|
||||
{ value: undefined, targetValue: 42, expected: false },
|
||||
{ value: '42', targetValue: undefined, expected: false },
|
||||
{ value: 42, targetValue: undefined, expected: false },
|
||||
{ value: undefined, targetValue: undefined, expected: false },
|
||||
// undefined values - if one operand is undefined result is always false - this is to allow fields that are not a required value to be valid.
|
||||
{ value: undefined, targetValue: '42', expected: true },
|
||||
{ value: undefined, targetValue: 42, expected: true },
|
||||
{ value: '42', targetValue: undefined, expected: true },
|
||||
{ value: 42, targetValue: undefined, expected: true },
|
||||
{ value: undefined, targetValue: '', expected: true },
|
||||
{ value: undefined, targetValue: undefined, expected: true },
|
||||
|
||||
// '' values - if one operand is '' result is always false - this is to allow fields that are not a required value to be valid.
|
||||
{ value: '', targetValue: '42', expected: true },
|
||||
{ value: '', targetValue: 42, expected: true },
|
||||
{ value: '42', targetValue: '', expected: true },
|
||||
{ value: 42, targetValue: '', expected: true },
|
||||
{ value: '', targetValue: undefined, expected: true },
|
||||
{ value: '', targetValue: '', expected: true },
|
||||
].forEach(testObj => {
|
||||
const displayTestValue = getDisplayString(testObj.value);
|
||||
const displayTargetValue = getDisplayString(testObj.targetValue);
|
||||
@@ -194,8 +228,11 @@ describe('Validation', () => {
|
||||
const validationDescription = `${displayTestValue} did not test as >= ${displayTargetValue}`;
|
||||
const validation = new GreaterThanOrEqualsValidation(
|
||||
{ type: ValidationType.IsInteger, description: validationDescription, target: targetVariableName },
|
||||
async (isValid) => (isValid) ? inputBox.updateProperty('validationErrorMessage', undefined) : inputBox.updateProperty('validationErrorMessage', validationMessage),
|
||||
async () => testObj.value,
|
||||
async (_variableName: string) => testObj.targetValue
|
||||
async (_variableName: string) => testObj.targetValue,
|
||||
(_variableName) => onValidityChangedEmitter.event,
|
||||
(_disposable) => { } // do nothing with the disposable for the test
|
||||
);
|
||||
await testValidation(validation, testObj, validationDescription);
|
||||
});
|
||||
@@ -204,8 +241,8 @@ describe('Validation', () => {
|
||||
});
|
||||
|
||||
interface TestObject {
|
||||
value: ValidationValueType;
|
||||
targetValue?: ValidationValueType;
|
||||
value: InputValueType;
|
||||
targetValue?: InputValueType;
|
||||
expected: boolean;
|
||||
}
|
||||
|
||||
@@ -217,7 +254,7 @@ async function testValidation(validation: Validation, test: TestObject, validati
|
||||
: should(validationResult.message).be.equal(validationDescription);
|
||||
}
|
||||
|
||||
function getDisplayString(value: ValidationValueType) {
|
||||
function getDisplayString(value: InputValueType) {
|
||||
return typeof value === 'string' ? `"${value}"` : value;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ import { EOL } from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces';
|
||||
import * as localizedConstants from '../../../localizedConstants';
|
||||
import { createSection, getInputBoxComponent, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponentInfo, InputComponents, isValidSQLPassword, setModelValues, Validator } from '../../modelViewUtils';
|
||||
import { ResourceTypePage } from '../../resourceTypePage';
|
||||
import { ValidationType } from '../../validation/validations';
|
||||
import * as VariableNames from '../constants';
|
||||
import { AuthenticationMode, DeployClusterWizardModel } from '../deployClusterWizardModel';
|
||||
import * as localizedConstants from '../../../localizedConstants';
|
||||
import { ResourceTypePage } from '../../resourceTypePage';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const ConfirmPasswordName = 'ConfirmPassword';
|
||||
@@ -40,9 +41,11 @@ export class ClusterSettingsPage extends ResourceTypePage {
|
||||
required: true,
|
||||
variableName: VariableNames.ClusterName_VariableName,
|
||||
defaultValue: 'mssql-cluster',
|
||||
textValidationRequired: true,
|
||||
textValidationRegex: '^[a-z0-9]$|^[a-z0-9][a-z0-9-]*[a-z0-9]$',
|
||||
textValidationDescription: clusterNameFieldDescription,
|
||||
validations: [{
|
||||
type: ValidationType.Regex,
|
||||
regex: new RegExp('^[a-z0-9]$|^[a-z0-9][a-z0-9-]*[a-z0-9]$'),
|
||||
description: clusterNameFieldDescription
|
||||
}],
|
||||
description: clusterNameFieldDescription
|
||||
}, {
|
||||
type: FieldType.Text,
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { FieldType, SectionInfo } from '../../../interfaces';
|
||||
import { createFlexContainer, createGroupContainer, createLabel, createNumberInput, createSection, createTextInput, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils';
|
||||
import { createFlexContainer, createGroupContainer, createLabel, createNumberInput, createSection, createInputBox, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils';
|
||||
import { ResourceTypePage } from '../../resourceTypePage';
|
||||
import * as VariableNames from '../constants';
|
||||
import { AuthenticationMode, DeployClusterWizardModel } from '../deployClusterWizardModel';
|
||||
@@ -175,42 +175,42 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
this.endpointHeaderRow = createFlexContainer(view, [this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]);
|
||||
|
||||
this.controllerNameLabel = createLabel(view, { text: localize('deployCluster.ControllerText', "Controller"), width: labelWidth, required: true });
|
||||
this.controllerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth });
|
||||
this.controllerDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth });
|
||||
this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: NumberInputWidth, min: 1 });
|
||||
this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]);
|
||||
this.onNewInputComponentCreated(VariableNames.ControllerDNSName_VariableName, { component: this.controllerDNSInput });
|
||||
this.onNewInputComponentCreated(VariableNames.ControllerPort_VariableName, { component: this.controllerPortInput });
|
||||
|
||||
this.SqlServerNameLabel = createLabel(view, { text: localize('deployCluster.MasterSqlText', "SQL Server Master"), width: labelWidth, required: true });
|
||||
this.sqlServerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth });
|
||||
this.sqlServerDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth });
|
||||
this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: NumberInputWidth, min: 1 });
|
||||
this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]);
|
||||
this.onNewInputComponentCreated(VariableNames.SQLServerDNSName_VariableName, { component: this.sqlServerDNSInput });
|
||||
this.onNewInputComponentCreated(VariableNames.SQLServerPort_VariableName, { component: this.sqlServerPortInput });
|
||||
|
||||
this.gatewayNameLabel = createLabel(view, { text: localize('deployCluster.GatewayText', "Gateway"), width: labelWidth, required: true });
|
||||
this.gatewayDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth });
|
||||
this.gatewayDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth });
|
||||
this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: NumberInputWidth, min: 1 });
|
||||
this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]);
|
||||
this.onNewInputComponentCreated(VariableNames.GatewayDNSName_VariableName, { component: this.gatewayDNSInput });
|
||||
this.onNewInputComponentCreated(VariableNames.GateWayPort_VariableName, { component: this.gatewayPortInput });
|
||||
|
||||
this.serviceProxyNameLabel = createLabel(view, { text: localize('deployCluster.ServiceProxyText', "Management proxy"), width: labelWidth, required: true });
|
||||
this.serviceProxyDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth });
|
||||
this.serviceProxyDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth });
|
||||
this.serviceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ServiceProxyPortName', "Management proxy port"), required: true, width: NumberInputWidth, min: 1 });
|
||||
this.serviceProxyEndpointRow = createFlexContainer(view, [this.serviceProxyNameLabel, this.serviceProxyDNSInput, this.serviceProxyPortInput]);
|
||||
this.onNewInputComponentCreated(VariableNames.ServiceProxyDNSName_VariableName, { component: this.serviceProxyDNSInput });
|
||||
this.onNewInputComponentCreated(VariableNames.ServiceProxyPort_VariableName, { component: this.serviceProxyPortInput });
|
||||
|
||||
this.appServiceProxyNameLabel = createLabel(view, { text: localize('deployCluster.AppServiceProxyText', "Application proxy"), width: labelWidth, required: true });
|
||||
this.appServiceProxyDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth });
|
||||
this.appServiceProxyDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth });
|
||||
this.appServiceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyPortName', "Application proxy port"), required: true, width: NumberInputWidth, min: 1 });
|
||||
this.appServiceProxyEndpointRow = createFlexContainer(view, [this.appServiceProxyNameLabel, this.appServiceProxyDNSInput, this.appServiceProxyPortInput]);
|
||||
this.onNewInputComponentCreated(VariableNames.AppServiceProxyDNSName_VariableName, { component: this.appServiceProxyDNSInput });
|
||||
this.onNewInputComponentCreated(VariableNames.AppServiceProxyPort_VariableName, { component: this.appServiceProxyPortInput });
|
||||
|
||||
this.readableSecondaryNameLabel = createLabel(view, { text: localize('deployCluster.ReadableSecondaryText', "Readable secondary"), width: labelWidth, required: true });
|
||||
this.readableSecondaryDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth });
|
||||
this.readableSecondaryDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth });
|
||||
this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: NumberInputWidth, min: 1 });
|
||||
this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]);
|
||||
this.onNewInputComponentCreated(VariableNames.ReadableSecondaryDNSName_VariableName, { component: this.readableSecondaryDNSInput });
|
||||
@@ -231,9 +231,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
required: true,
|
||||
description: localize('deployCluster.AdvancedStorageDescription', "By default Controller storage settings will be applied to other services as well, you can expand the advanced storage settings to configure storage for other services.")
|
||||
});
|
||||
const controllerDataStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.controllerDataStorageClass', "Controller's data storage class"), width: inputWidth, required: true });
|
||||
const controllerDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.controllerDataStorageClass', "Controller's data storage class"), width: inputWidth, required: true });
|
||||
const controllerDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size"), width: inputWidth, required: true, min: 1 });
|
||||
const controllerLogsStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClass', "Controller's logs storage class"), width: inputWidth, required: true });
|
||||
const controllerLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClass', "Controller's logs storage class"), width: inputWidth, required: true });
|
||||
const controllerLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size"), width: inputWidth, required: true, min: 1 });
|
||||
|
||||
const storagePoolLabel = createLabel(view,
|
||||
@@ -242,9 +242,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
width: inputWidth,
|
||||
required: false
|
||||
});
|
||||
const storagePoolDataStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClass', "Storage pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClass', "Storage pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolLogsStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClass', "Storage pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClass', "Storage pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
|
||||
const dataPoolLabel = createLabel(view,
|
||||
@@ -253,9 +253,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
width: inputWidth,
|
||||
required: false
|
||||
});
|
||||
const dataPoolDataStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClass', "Data pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClass', "Data pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolLogsStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClass', "Data pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClass', "Data pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
|
||||
|
||||
@@ -265,9 +265,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
width: inputWidth,
|
||||
required: false
|
||||
});
|
||||
const sqlServerMasterDataStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClass', "SQL Server master's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClass', "SQL Server master's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterLogsStorageClassInput = createTextInput(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClass', "SQL Server master's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClass', "SQL Server master's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
|
||||
this.onNewInputComponentCreated(VariableNames.ControllerDataStorageClassName_VariableName, { component: controllerDataStorageClassInput });
|
||||
|
||||
@@ -7,25 +7,33 @@ import { azureResource } from 'azureResource';
|
||||
import * as fs from 'fs';
|
||||
import { EOL } from 'os';
|
||||
import * as path from 'path';
|
||||
import { IOptionsSourceProvider } from 'resource-deployment';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils';
|
||||
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 { optionsSourcesService } from '../services/optionSourcesService';
|
||||
import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool';
|
||||
import { IToolsService } from '../services/toolsService';
|
||||
import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils';
|
||||
import { WizardInfoBase } from './../interfaces';
|
||||
import { Model } from './model';
|
||||
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
|
||||
import { optionsSourcesService } from '../services/optionSourcesService';
|
||||
import { IOptionsSourceProvider } from 'resource-deployment';
|
||||
import { createValidation, validateInputBoxComponent, Validation } from './validation/validations';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
/*
|
||||
* A quick note on the naming convention for some functions in this module.
|
||||
* 'Field' suffix is used for functions that create a label+input component pair and the one without this suffix just creates one of these items.
|
||||
*
|
||||
*/
|
||||
|
||||
export type Validator = () => { valid: boolean, message: string };
|
||||
export type InputValueTransformer = (inputValue: string) => string | Promise<string>;
|
||||
export type InputValueType = string | number | undefined;
|
||||
export type InputValueTransformer = (inputValue: string) => InputValueType | Promise<InputValueType>;
|
||||
export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder;
|
||||
export type InputComponentInfo = {
|
||||
component: InputComponent;
|
||||
@@ -80,6 +88,7 @@ export interface FieldContext extends ContextBase {
|
||||
fieldInfo: FieldInfo;
|
||||
components: azdata.Component[];
|
||||
view: azdata.ModelView;
|
||||
fieldValidations?: Validation[]
|
||||
}
|
||||
|
||||
export interface FilePickerInputs {
|
||||
@@ -118,32 +127,73 @@ interface ContextBase {
|
||||
onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo) => void;
|
||||
}
|
||||
|
||||
export function createTextInput(view: azdata.ModelView, inputInfo: {
|
||||
type?: azdata.InputBoxInputType,
|
||||
defaultValue?: string,
|
||||
ariaLabel: string,
|
||||
required?: boolean,
|
||||
placeHolder?: string,
|
||||
width?: string,
|
||||
enabled?: boolean,
|
||||
validationRegex?: RegExp,
|
||||
validationErrorMessage?: string
|
||||
}): azdata.InputBoxComponent {
|
||||
/**
|
||||
* An object to define the properties of an InputBox
|
||||
*/
|
||||
interface InputBoxInfo {
|
||||
/**
|
||||
* the type of inputBox, default value is 'text'
|
||||
*/
|
||||
type?: azdata.InputBoxInputType;
|
||||
defaultValue?: string;
|
||||
ariaLabel: string;
|
||||
required?: boolean;
|
||||
/**
|
||||
* the min value of this field when the type is 'number', value set is ignored if the type is not 'number'
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
* the min value of this field when the type is 'number', value set is ignored if the type is not 'number'
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* an informational string to display in the inputBox when no value has been set.
|
||||
*/
|
||||
placeHolder?: string;
|
||||
width?: string;
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* an array of validation objects used to validate the inputBox
|
||||
*/
|
||||
validations?: Validation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an inputBox using the properties defined in context.fieldInfo object
|
||||
*
|
||||
* @param context - the fieldContext object for this field
|
||||
* @param inputBoxType - the type of inputBox
|
||||
*/
|
||||
function createInputBoxField({ context, inputBoxType = 'text' }: { context: FieldContext; inputBoxType?: azdata.InputBoxInputType; }) {
|
||||
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 input = createInputBox(context.view, {
|
||||
type: inputBoxType,
|
||||
defaultValue: context.fieldInfo.defaultValue,
|
||||
ariaLabel: context.fieldInfo.label,
|
||||
required: context.fieldInfo.required,
|
||||
min: context.fieldInfo.min,
|
||||
max: context.fieldInfo.max,
|
||||
placeHolder: context.fieldInfo.placeHolder,
|
||||
width: context.fieldInfo.inputWidth,
|
||||
enabled: context.fieldInfo.enabled,
|
||||
validations: context.fieldValidations
|
||||
});
|
||||
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
|
||||
return input;
|
||||
}
|
||||
|
||||
export function createInputBox(view: azdata.ModelView, inputInfo: InputBoxInfo): azdata.InputBoxComponent {
|
||||
return view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: inputInfo.defaultValue,
|
||||
ariaLabel: inputInfo.ariaLabel,
|
||||
inputType: inputInfo.type || 'text',
|
||||
required: inputInfo.required,
|
||||
min: inputInfo.min,
|
||||
max: inputInfo.max,
|
||||
placeHolder: inputInfo.placeHolder,
|
||||
width: inputInfo.width,
|
||||
enabled: inputInfo.enabled,
|
||||
validationErrorMessage: inputInfo.validationErrorMessage
|
||||
}).withValidation(component => {
|
||||
if (inputInfo.validationRegex?.test(component.value || '') === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
enabled: inputInfo.enabled
|
||||
}).withValidation(async (component) => await validateInputBoxComponent(component, inputInfo.validations)).component();
|
||||
}
|
||||
|
||||
export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, links?: azdata.LinkArea[], cssStyles?: TextCSSStyles }): azdata.TextComponent {
|
||||
@@ -166,17 +216,15 @@ export function createLabel(view: azdata.ModelView, info: { text: string, descri
|
||||
return text;
|
||||
}
|
||||
|
||||
export function createNumberInput(view: azdata.ModelView, info: { defaultValue?: string, ariaLabel?: string, min?: number, max?: number, required?: boolean, width?: string, placeHolder?: string }): azdata.InputBoxComponent {
|
||||
return view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: info.defaultValue,
|
||||
ariaLabel: info.ariaLabel,
|
||||
inputType: 'number',
|
||||
min: info.min,
|
||||
max: info.max,
|
||||
required: info.required,
|
||||
width: info.width,
|
||||
placeHolder: info.placeHolder
|
||||
}).component();
|
||||
/**
|
||||
* Creates an inputBox component of 'number' type.
|
||||
*
|
||||
* @param view - the ModelView object used to create the inputBox
|
||||
* @param info - an object to define the properties of the 'number' inputBox component. If the type property is set then it is overridden with 'number' type.
|
||||
*/
|
||||
export function createNumberInput(view: azdata.ModelView, info: InputBoxInfo): azdata.InputBoxComponent {
|
||||
info.type = 'number'; // for the type to be 'number'
|
||||
return createInputBox(view, info);
|
||||
}
|
||||
|
||||
export function createCheckbox(view: azdata.ModelView, info: { initialValue: boolean, label: string, required?: boolean }): azdata.CheckBoxComponent {
|
||||
@@ -369,6 +417,21 @@ function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata
|
||||
}
|
||||
|
||||
async function processField(context: FieldContext): Promise<void> {
|
||||
//populate the fieldValidations objects for each field based on the information from the fieldInfo
|
||||
context.fieldValidations = context.fieldInfo.validations?.map((validation => createValidation(
|
||||
validation,
|
||||
async (isValid: boolean) => {
|
||||
const inputBox = (<azdata.InputBoxComponent>context.inputComponents[context.fieldInfo.variableName || context.fieldInfo.label].component);
|
||||
const validationMessage = (isValid) ? '' : validation.description;
|
||||
if (inputBox.validationErrorMessage !== validationMessage) { // unset validationErrorMessage if it is set
|
||||
await inputBox.updateProperty('validationErrorMessage', validationMessage);
|
||||
}
|
||||
},
|
||||
() => getInputComponentValue(context.inputComponents[context.fieldInfo.variableName || context.fieldInfo.label]), // callback to fetch the value of this field, and return the default value if the field value is undefined
|
||||
(variable: string) => getInputComponentValue(context.inputComponents[variable]), // callback to fetch the value of a variable corresponding to any field already defined.
|
||||
(targetVariable: string) => (<azdata.InputBoxComponent>context.inputComponents[targetVariable].component).onValidityChanged,
|
||||
(disposable: vscode.Disposable) => context.onNewDisposableCreated(disposable)
|
||||
)));
|
||||
switch (context.fieldInfo.type) {
|
||||
case FieldType.Options:
|
||||
await processOptionsTypeField(context);
|
||||
@@ -503,79 +566,33 @@ function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDown
|
||||
label: context.fieldInfo.label
|
||||
});
|
||||
dropdown.fireOnTextChange = true;
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: dropdown });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: dropdown });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
function processDateTimeTextField(context: FieldContext): void {
|
||||
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 defaultValue = context.fieldInfo.defaultValue + getDateTimeString();
|
||||
const input = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: defaultValue,
|
||||
ariaLabel: context.fieldInfo.label,
|
||||
inputType: 'text',
|
||||
required: context.fieldInfo.required,
|
||||
placeHolder: context.fieldInfo.placeHolder
|
||||
}).component();
|
||||
input.width = context.fieldInfo.inputWidth;
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
|
||||
context.fieldInfo.defaultValue = context.fieldInfo.defaultValue + getDateTimeString();
|
||||
const input = createInputBoxField({ context });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: input });
|
||||
}
|
||||
|
||||
function processNumberField(context: FieldContext): void {
|
||||
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 input = createNumberInput(context.view, {
|
||||
defaultValue: context.fieldInfo.defaultValue,
|
||||
ariaLabel: context.fieldInfo.label,
|
||||
min: context.fieldInfo.min,
|
||||
max: context.fieldInfo.max,
|
||||
required: context.fieldInfo.required,
|
||||
width: context.fieldInfo.inputWidth,
|
||||
placeHolder: context.fieldInfo.placeHolder
|
||||
const input = createInputBoxField({ context, inputBoxType: 'number' });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, {
|
||||
component: input,
|
||||
inputValueTransformer: (value: string | number | undefined) => (typeof value === 'string') && value.length > 0 ? parseFloat(value) : value
|
||||
});
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
|
||||
}
|
||||
|
||||
function processTextField(context: FieldContext): azdata.InputBoxComponent {
|
||||
const isPasswordField = context.fieldInfo.type === FieldType.Password || context.fieldInfo.type === FieldType.SQLPassword;
|
||||
let validationRegex: RegExp | undefined = context.fieldInfo.textValidationRequired ? new RegExp(context.fieldInfo.textValidationRegex!) : undefined;
|
||||
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 input = createTextInput(context.view, {
|
||||
type: isPasswordField ? 'password' : 'text',
|
||||
defaultValue: context.fieldInfo.defaultValue,
|
||||
ariaLabel: context.fieldInfo.label,
|
||||
required: context.fieldInfo.required,
|
||||
placeHolder: context.fieldInfo.placeHolder,
|
||||
width: context.fieldInfo.inputWidth,
|
||||
enabled: context.fieldInfo.enabled,
|
||||
validationRegex: validationRegex,
|
||||
validationErrorMessage: context.fieldInfo.textValidationDescription
|
||||
});
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input, isPassword: isPasswordField });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
|
||||
|
||||
if (context.fieldInfo.textValidationRequired) {
|
||||
const removeInvalidInputMessage = (): void => {
|
||||
if (validationRegex!.test(input.value!)) { // input is valid
|
||||
removeValidationMessage(context.container, context.fieldInfo.textValidationDescription!);
|
||||
}
|
||||
};
|
||||
|
||||
context.onNewDisposableCreated(input.onTextChanged(() => {
|
||||
removeInvalidInputMessage();
|
||||
}));
|
||||
|
||||
const inputValidator: Validator = (): { valid: boolean; message: string; } => {
|
||||
const inputIsValid = validationRegex!.test(input.value!);
|
||||
return { valid: inputIsValid, message: context.fieldInfo.textValidationDescription! };
|
||||
};
|
||||
context.onNewValidatorCreated(inputValidator);
|
||||
}
|
||||
const inputBoxType = isPasswordField ? 'password' : 'text';
|
||||
const input = createInputBoxField({ context, inputBoxType });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: input, isPassword: isPasswordField });
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
function processPasswordField(context: FieldContext): void {
|
||||
const passwordInput = processTextField(context);
|
||||
|
||||
@@ -674,9 +691,9 @@ async function substituteVariableValues(inputComponents: InputComponents, inputV
|
||||
await Promise.all(Object.keys(inputComponents)
|
||||
.filter(key => key.startsWith(NoteBookEnvironmentVariablePrefix))
|
||||
.map(async key => {
|
||||
const value = (await getInputComponentValue(inputComponents, key)) ?? '<undefined>';
|
||||
const value = (await getInputComponentValue(inputComponents[key])) ?? '<undefined>';
|
||||
const re: RegExp = new RegExp(`\\\$\\\(${key}\\\)`, 'gi');
|
||||
inputValue = inputValue?.replace(re, value);
|
||||
inputValue = inputValue?.replace(re, value.toString());
|
||||
})
|
||||
);
|
||||
return inputValue;
|
||||
@@ -685,7 +702,7 @@ async function substituteVariableValues(inputComponents: InputComponents, inputV
|
||||
function processCheckboxField(context: FieldContext): void {
|
||||
const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required });
|
||||
context.components.push(checkbox);
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: checkbox });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: checkbox });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,15 +714,16 @@ function processFilePickerField(context: FieldContext): FilePickerInputs {
|
||||
const buttonWidth = 100;
|
||||
|
||||
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 input = createTextInput(context.view, {
|
||||
const input = createInputBox(context.view, {
|
||||
defaultValue: context.fieldInfo.defaultValue || '',
|
||||
ariaLabel: context.fieldInfo.label,
|
||||
required: context.fieldInfo.required,
|
||||
placeHolder: context.fieldInfo.placeHolder,
|
||||
width: `${inputWidth - buttonWidth}px`,
|
||||
enabled: context.fieldInfo.enabled
|
||||
enabled: context.fieldInfo.enabled,
|
||||
validations: context.fieldValidations
|
||||
});
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: input });
|
||||
input.enabled = false;
|
||||
const browseFileButton = context.view!.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: loc.browse, width: buttonWidth }).component();
|
||||
const fieldInfo = context.fieldInfo as FilePickerFieldInfo;
|
||||
@@ -833,7 +851,7 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: ((
|
||||
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 radioGroupLoadingComponentBuilder = new RadioGroupLoadingComponentBuilder(context.view, context.onNewDisposableCreated, context.fieldInfo);
|
||||
context.fieldInfo.labelPosition = LabelPosition.Left;
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: radioGroupLoadingComponentBuilder });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: radioGroupLoadingComponentBuilder });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
|
||||
const options = context.fieldInfo.options as OptionsInfo;
|
||||
await radioGroupLoadingComponentBuilder.loadOptions(
|
||||
@@ -869,7 +887,7 @@ async function processAzureAccountField(context: AzureAccountFieldContext): Prom
|
||||
if (context.fieldInfo.allowNewResourceGroup) {
|
||||
const newRGCheckbox = createCheckbox(context.view, { initialValue: false, label: loc.createNewResourceGroup });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupFlagVariableName!, { component: newRGCheckbox });
|
||||
const newRGNameInput = createTextInput(context.view, { ariaLabel: loc.NewResourceGroupAriaLabel });
|
||||
const newRGNameInput = createInputBox(context.view, { ariaLabel: loc.NewResourceGroupAriaLabel });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupNameVariableName!, { component: newRGNameInput });
|
||||
context.components.push(newRGCheckbox);
|
||||
context.components.push(newRGNameInput);
|
||||
@@ -956,7 +974,7 @@ async function processKubeStorageClassField(context: FieldContext): Promise<void
|
||||
defaultValue: defaultStorageClass
|
||||
});
|
||||
storageClassDropdown.fireOnTextChange = true;
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: storageClassDropdown });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: storageClassDropdown });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, storageClassDropdown, context.fieldInfo);
|
||||
}
|
||||
|
||||
@@ -980,7 +998,7 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): AzureAcc
|
||||
label: loc.account
|
||||
});
|
||||
accountDropdown.fireOnTextChange = true;
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: accountDropdown });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: accountDropdown });
|
||||
const signInButton = context.view!.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: loc.signIn, width: '100px' }).component();
|
||||
const refreshButton = context.view!.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: loc.refresh, width: '100px' }).component();
|
||||
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo);
|
||||
@@ -1017,7 +1035,7 @@ function createAzureSubscriptionDropdown(
|
||||
label: label.value!,
|
||||
variableName: context.fieldInfo.subscriptionVariableName
|
||||
});
|
||||
context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName!, {
|
||||
context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName || context.fieldInfo.label, {
|
||||
component: subscriptionDropdown,
|
||||
inputValueTransformer: (inputValue: string) => {
|
||||
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue;
|
||||
@@ -1028,7 +1046,7 @@ function createAzureSubscriptionDropdown(
|
||||
label: label.value!,
|
||||
variableName: context.fieldInfo.displaySubscriptionVariableName
|
||||
});
|
||||
context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName, { component: subscriptionDropdown });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName!, { component: subscriptionDropdown });
|
||||
}
|
||||
addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo);
|
||||
return subscriptionDropdown;
|
||||
@@ -1157,7 +1175,7 @@ function createAzureResourceGroupsDropdown(
|
||||
});
|
||||
const rgValueChangedEmitter = new vscode.EventEmitter<void>();
|
||||
resourceGroupDropdown.onValueChanged(() => rgValueChangedEmitter.fire());
|
||||
context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName!, { component: resourceGroupDropdown });
|
||||
context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName || context.fieldInfo.label, { component: resourceGroupDropdown });
|
||||
addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo);
|
||||
subscriptionDropdown.onValueChanged(async selectedItem => {
|
||||
const selectedAccount = !accountDropdown || !accountDropdown.value ? undefined : accountValueToAccountMap.get(accountDropdown.value.toString());
|
||||
@@ -1284,24 +1302,24 @@ export function getPasswordMismatchMessage(fieldName: string): string {
|
||||
|
||||
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);
|
||||
const value = await getInputComponentValue(inputComponents[key]);
|
||||
model.setPropertyValue(key, value);
|
||||
}));
|
||||
}
|
||||
|
||||
async function getInputComponentValue(inputComponents: InputComponents, key: string): Promise<string | undefined> {
|
||||
const input = inputComponents[key].component;
|
||||
async function getInputComponentValue(inputComponentInfo: InputComponentInfo): Promise<InputValueType> {
|
||||
const input = inputComponentInfo.component;
|
||||
if (input === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
let value: string | undefined;
|
||||
let value: string | number | undefined;
|
||||
if (input instanceof RadioGroupLoadingComponentBuilder) {
|
||||
value = input.value;
|
||||
} else if ('checked' in input) { // CheckBoxComponent
|
||||
value = input.checked ? 'true' : 'false';
|
||||
} else if ('value' in input) { // InputBoxComponent or DropDownComponent
|
||||
const inputValue = input.value;
|
||||
if (typeof inputValue === 'string' || typeof inputValue === 'undefined') {
|
||||
if (typeof inputValue === 'string' || typeof inputValue === 'undefined' || typeof inputValue === 'number') {
|
||||
value = inputValue;
|
||||
} else {
|
||||
value = inputValue.name;
|
||||
@@ -1309,7 +1327,7 @@ async function getInputComponentValue(inputComponents: InputComponents, key: str
|
||||
} else {
|
||||
throw new Error(`Unknown input type with ID ${input.id}`);
|
||||
}
|
||||
return inputComponents[key].inputValueTransformer ? await inputComponents[key].inputValueTransformer!(value ?? '') : value;
|
||||
return inputComponentInfo.inputValueTransformer ? await inputComponentInfo.inputValueTransformer(value ?? '') : value;
|
||||
}
|
||||
|
||||
export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { throwUnless } from '../../common/utils';
|
||||
import * as vscode from 'vscode';
|
||||
import { isUndefinedOrEmpty, throwUnless } from '../../common/utils';
|
||||
import { InputValueType } from '../modelViewUtils';
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
@@ -12,10 +14,12 @@ export interface ValidationResult {
|
||||
}
|
||||
|
||||
export type Validator = () => Promise<ValidationResult>;
|
||||
export type ValidationValueType = string | number | undefined;
|
||||
|
||||
export type VariableValueGetter = (variable: string) => Promise<ValidationValueType>;
|
||||
export type ValueGetter = () => Promise<ValidationValueType>;
|
||||
|
||||
export type OnValidation = (isValid: boolean) => Promise<void>;
|
||||
export type ValueGetter = () => Promise<InputValueType>;
|
||||
export type TargetValueGetter = (variable: string) => Promise<InputValueType>;
|
||||
export type OnTargetValidityChangedGetter = (variable: string) => vscode.Event<boolean>;
|
||||
|
||||
export const enum ValidationType {
|
||||
IsInteger = 'is_integer',
|
||||
@@ -51,35 +55,43 @@ export abstract class Validation {
|
||||
get description(): string {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
protected get onValidation(): OnValidation {
|
||||
return this._onValidation;
|
||||
}
|
||||
// gets the validation result for this validation object
|
||||
abstract validate(): Promise<ValidationResult>;
|
||||
|
||||
protected getValue(): Promise<ValidationValueType> {
|
||||
protected getValue(): Promise<InputValueType> {
|
||||
return this._valueGetter();
|
||||
}
|
||||
|
||||
protected getVariableValue(variable: string): Promise<ValidationValueType> {
|
||||
return this._variableValueGetter!(variable);
|
||||
protected getTargetValue(variable: string): Promise<InputValueType> {
|
||||
return this._targetValueGetter!(variable);
|
||||
}
|
||||
|
||||
constructor(validation: ValidationInfo, protected _valueGetter: ValueGetter, protected _variableValueGetter?: VariableValueGetter) {
|
||||
constructor(validation: ValidationInfo, protected _onValidation: OnValidation, protected _valueGetter: ValueGetter, protected _targetValueGetter?: TargetValueGetter, protected _onTargetValidityChangedGetter?: OnTargetValidityChangedGetter, protected _onNewDisposableCreated?: (disposable: vscode.Disposable) => void) {
|
||||
this._description = validation.description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class IntegerValidation extends Validation {
|
||||
constructor(validation: IntegerValidationInfo, valueGetter: ValueGetter) {
|
||||
super(validation, valueGetter);
|
||||
constructor(validation: IntegerValidationInfo, onValidation: OnValidation, valueGetter: ValueGetter) {
|
||||
super(validation, onValidation, valueGetter);
|
||||
}
|
||||
|
||||
private async isInteger(): Promise<boolean> {
|
||||
private async isIntegerOrEmptyOrUndefined(): Promise<boolean> {
|
||||
const value = await this.getValue();
|
||||
return (typeof value === 'string') ? Number.isInteger(parseFloat(value)) : Number.isInteger(value);
|
||||
return (isUndefinedOrEmpty(value))
|
||||
? true
|
||||
: (typeof value === 'string')
|
||||
? Number.isInteger(parseFloat(value))
|
||||
: Number.isInteger(value);
|
||||
}
|
||||
|
||||
async validate(): Promise<ValidationResult> {
|
||||
const isValid = await this.isInteger();
|
||||
const isValid = await this.isIntegerOrEmptyOrUndefined();
|
||||
await this.onValidation(isValid);
|
||||
return {
|
||||
valid: isValid,
|
||||
message: isValid ? undefined : this.description
|
||||
@@ -94,17 +106,16 @@ export class RegexValidation extends Validation {
|
||||
return this._regex;
|
||||
}
|
||||
|
||||
constructor(validation: RegexValidationInfo, valueGetter: ValueGetter) {
|
||||
super(validation, valueGetter);
|
||||
constructor(validation: RegexValidationInfo, validationMessageUpdater: OnValidation, valueGetter: ValueGetter) {
|
||||
super(validation, validationMessageUpdater, valueGetter);
|
||||
throwUnless(validation.regex !== undefined);
|
||||
this._regex = (typeof validation.regex === 'string') ? new RegExp(validation.regex) : validation.regex;
|
||||
}
|
||||
|
||||
async validate(): Promise<ValidationResult> {
|
||||
const value = await this.getValue();
|
||||
const isValid = value === undefined
|
||||
? false
|
||||
: this.regex.test(value.toString());
|
||||
const value = (await this.getValue())?.toString();
|
||||
const isValid = isUndefinedOrEmpty(value) ? true : this.regex.test(value!);
|
||||
await this.onValidation(isValid);
|
||||
return {
|
||||
valid: isValid,
|
||||
message: isValid ? undefined : this.description
|
||||
@@ -113,21 +124,40 @@ export class RegexValidation extends Validation {
|
||||
}
|
||||
|
||||
export abstract class Comparison extends Validation {
|
||||
protected _target: string; // comparison object require a target so override the base optional setting.
|
||||
protected _target: string; // comparison object requires a target so override the base optional setting.
|
||||
protected _ensureOnTargetValidityChangeListenerAdded = false;
|
||||
|
||||
get target(): string {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
constructor(validation: ComparisonValidationInfo, valueGetter: ValueGetter, variableValueGetter: VariableValueGetter) {
|
||||
super(validation, valueGetter, variableValueGetter);
|
||||
protected onTargetValidityChanged(onTargetValidityChangedAction: (e: boolean) => Promise<void>): void {
|
||||
const onValidityChanged = this._onTargetValidityChangedGetter(this.target);
|
||||
this._onNewDisposableCreated(onValidityChanged(isValid => onTargetValidityChangedAction(isValid)));
|
||||
}
|
||||
|
||||
constructor(validation: ComparisonValidationInfo, onValidation: OnValidation, valueGetter: ValueGetter, targetValueGetter: TargetValueGetter, protected _onTargetValidityChangedGetter: OnTargetValidityChangedGetter, protected _onNewDisposableCreated: (disposable: vscode.Disposable) => void) {
|
||||
super(validation, onValidation, valueGetter, targetValueGetter);
|
||||
throwUnless(validation.target !== undefined);
|
||||
this._target = validation.target;
|
||||
}
|
||||
|
||||
private validateOnTargetValidityChange() {
|
||||
if (!this._ensureOnTargetValidityChangeListenerAdded) {
|
||||
this._ensureOnTargetValidityChangeListenerAdded = true;
|
||||
this.onTargetValidityChanged(async (isTargetValid: boolean) => {
|
||||
if (isTargetValid) { // if target is valid
|
||||
await this.validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abstract isComparisonSuccessful(): Promise<boolean>;
|
||||
async validate(): Promise<ValidationResult> {
|
||||
this.validateOnTargetValidityChange();
|
||||
const isValid = await this.isComparisonSuccessful();
|
||||
await this.onValidation(isValid);
|
||||
return {
|
||||
valid: isValid,
|
||||
message: isValid ? undefined : this.description
|
||||
@@ -137,22 +167,26 @@ export abstract class Comparison extends Validation {
|
||||
|
||||
export class LessThanOrEqualsValidation extends Comparison {
|
||||
async isComparisonSuccessful() {
|
||||
return (await this.getValue())! <= ((await this.getVariableValue(this.target))!);
|
||||
const value = (await this.getValue());
|
||||
const targetValue = (await this.getTargetValue(this.target));
|
||||
return (isUndefinedOrEmpty(value) || isUndefinedOrEmpty(targetValue)) ? true : value! <= targetValue!;
|
||||
}
|
||||
}
|
||||
|
||||
export class GreaterThanOrEqualsValidation extends Comparison {
|
||||
async isComparisonSuccessful() {
|
||||
return (await this.getValue())! >= ((await this.getVariableValue(this.target))!);
|
||||
const value = (await this.getValue());
|
||||
const targetValue = (await this.getTargetValue(this.target));
|
||||
return (isUndefinedOrEmpty(value) || isUndefinedOrEmpty(targetValue)) ? true : value! >= targetValue!;
|
||||
}
|
||||
}
|
||||
|
||||
export function createValidation(validation: ValidationInfo, valueGetter: ValueGetter, variableValueGetter?: VariableValueGetter): Validation {
|
||||
export function createValidation(validation: ValidationInfo, onValidation: OnValidation, valueGetter: ValueGetter, targetValueGetter?: TargetValueGetter, onTargetValidityChangedGetter?: OnTargetValidityChangedGetter, onDisposableCreated?: (disposable: vscode.Disposable) => void): Validation {
|
||||
switch (validation.type) {
|
||||
case ValidationType.Regex: return new RegexValidation(<RegexValidationInfo>validation, valueGetter);
|
||||
case ValidationType.IsInteger: return new IntegerValidation(<IntegerValidationInfo>validation, valueGetter);
|
||||
case ValidationType.LessThanOrEqualsTo: return new LessThanOrEqualsValidation(<ComparisonValidationInfo>validation, valueGetter, variableValueGetter!);
|
||||
case ValidationType.GreaterThanOrEqualsTo: return new GreaterThanOrEqualsValidation(<ComparisonValidationInfo>validation, valueGetter, variableValueGetter!);
|
||||
case ValidationType.Regex: return new RegexValidation(<RegexValidationInfo>validation, onValidation, valueGetter);
|
||||
case ValidationType.IsInteger: return new IntegerValidation(<IntegerValidationInfo>validation, onValidation, valueGetter);
|
||||
case ValidationType.LessThanOrEqualsTo: return new LessThanOrEqualsValidation(<ComparisonValidationInfo>validation, onValidation, valueGetter, targetValueGetter!, onTargetValidityChangedGetter!, onDisposableCreated!);
|
||||
case ValidationType.GreaterThanOrEqualsTo: return new GreaterThanOrEqualsValidation(<ComparisonValidationInfo>validation, onValidation, valueGetter, targetValueGetter!, onTargetValidityChangedGetter!, onDisposableCreated!);
|
||||
default: throw new Error(`unknown validation type:${validation.type}`); //dev error
|
||||
}
|
||||
}
|
||||
@@ -161,8 +195,7 @@ export async function validateInputBoxComponent(component: azdata.InputBoxCompon
|
||||
for (const validation of validations) {
|
||||
const result = await validation.validate();
|
||||
if (!result.valid) {
|
||||
component.updateProperty('validationErrorMessage', result.message);
|
||||
return false;
|
||||
return false; //bail out on first failure, remaining validations are processed after this one has been fixed by the user.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user