mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Improved Validations for ARC Wizards (#12945)
This commit is contained in:
@@ -226,9 +226,11 @@
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.arc.data.controller.namespace%",
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.data.controller.arc.data.controller.namespace.validation.description%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.arc.data.controller.namespace.validation.description%"
|
||||
}],
|
||||
"defaultValue": "arc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
|
||||
@@ -236,9 +238,11 @@
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.arc.data.controller.name%",
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.data.controller.arc.data.controller.name.validation.description%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.arc.data.controller.name.validation.description%"
|
||||
}],
|
||||
"defaultValue": "arc-dc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
|
||||
@@ -559,18 +563,22 @@
|
||||
"type": "text",
|
||||
"defaultValue": "sqlinstance1",
|
||||
"required": true,
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.sql.invalid.instance.name%"
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
|
||||
"description": "%arc.sql.invalid.instance.name%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.sql.username%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^(?!sa$)",
|
||||
"textValidationDescription": "%arc.sql.invalid.username%"
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?!sa$)",
|
||||
"description": "%arc.sql.invalid.username%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.password%",
|
||||
@@ -607,7 +615,14 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.cores-limit.label%",
|
||||
@@ -615,7 +630,14 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-request.label%",
|
||||
@@ -623,7 +645,12 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-limit.label%",
|
||||
@@ -631,7 +658,12 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -719,9 +751,11 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
|
||||
"type": "text",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%",
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.postgres.server.group.name.validation.description%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%"
|
||||
}],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
@@ -738,6 +772,10 @@
|
||||
"description": "%arc.postgres.server.group.workers.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS",
|
||||
"type": "number",
|
||||
"validations": [{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}],
|
||||
"defaultValue": "0",
|
||||
"min": 0
|
||||
},
|
||||
@@ -745,6 +783,10 @@
|
||||
"label": "%arc.postgres.server.group.port%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT",
|
||||
"type": "number",
|
||||
"validations": [{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}],
|
||||
"defaultValue": "5432",
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
@@ -825,28 +867,48 @@
|
||||
"description": "%arc.postgres.server.group.cores.request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1
|
||||
"min": 1,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.cores.limit.label%",
|
||||
"description": "%arc.postgres.server.group.cores.limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1
|
||||
"min": 1,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.memory.request.label%",
|
||||
"description": "%arc.postgres.server.group.memory.request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 0.25
|
||||
"min": 0.25,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.memory.limit.label%",
|
||||
"description": "%arc.postgres.server.group.memory.limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 0.25
|
||||
"min": 0.25,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -129,6 +129,11 @@
|
||||
"arc.postgres.server.group.memory.limit.label": "Memory limit (GB per node)",
|
||||
"arc.postgres.server.group.memory.limit.description": "The memory limit of the Postgres instance per node in GB.",
|
||||
"arc.agreement": "I accept {0} and {1}.",
|
||||
"arc.agreement.sql.terms.conditions":"Azure SQL managed instance - Azure Arc terms and conditions",
|
||||
"arc.agreement.postgres.terms.conditions":"Azure Arc enabled PostgreSQL Hyperscale terms and conditions"
|
||||
"arc.agreement.sql.terms.conditions": "Azure SQL managed instance - Azure Arc terms and conditions",
|
||||
"arc.agreement.postgres.terms.conditions": "Azure Arc enabled PostgreSQL Hyperscale terms and conditions",
|
||||
"should.be.integer": "Value must be an integer",
|
||||
"requested.cores.less.than.or.equal.to.cores.limit": "Requested cores must be less than or equal to cores limit",
|
||||
"cores.limit.greater.than.or.equal.to.requested.cores": "Cores limit must be greater than or equal to requested cores",
|
||||
"requested.memory.less.than.or.equal.to.memory.limit": "Requested memory must be less than or equal to memory limit",
|
||||
"memory.limit.greater.than.or.equal.to.requested.memory": "Memory limit must be greater than or equal to requested memory"
|
||||
}
|
||||
|
||||
@@ -339,9 +339,11 @@
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%vm_password_confirm%",
|
||||
"required": true,
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
|
||||
"textValidationDescription": "%vm_password_validation_error_message%"
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
|
||||
"description": "%vm_password_validation_error_message%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,41 +1,133 @@
|
||||
{
|
||||
"name": "sample-resource-deployment",
|
||||
"displayName": "%extension-displayName%",
|
||||
"description": "%extension-description%",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Contoso",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.19.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"microsoft.mssql",
|
||||
"microsoft.notebook",
|
||||
"microsoft.resource-deployment"
|
||||
],
|
||||
"contributes": {
|
||||
"resourceDeploymentTypes": [
|
||||
{
|
||||
"name": "sample-resource-deployment",
|
||||
"displayName": "%extension-displayName%",
|
||||
"description": "%extension-description%",
|
||||
"version": "0.0.1",
|
||||
"publisher": "Contoso",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.19.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"microsoft.mssql",
|
||||
"microsoft.notebook",
|
||||
"microsoft.resource-deployment"
|
||||
],
|
||||
"contributes": {
|
||||
"resourceDeploymentTypes": [
|
||||
{
|
||||
"name": "validations-wizard",
|
||||
"displayName": "%validation.wizard.display.name%",
|
||||
"description": "%validation.wizard.description%",
|
||||
"platforms": "*",
|
||||
"icon": {
|
||||
"light": "./images/book.svg",
|
||||
"dark": "./images/book_inverse.svg"
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"notebookWizard": {
|
||||
"notebook": "%deployment-notebook-1%",
|
||||
"runNotebook": false,
|
||||
"codeCellInsertionPosition": 1,
|
||||
"actionText": "%deploy.wizard.action%",
|
||||
"title": "%wizard.new.wizard.title%",
|
||||
"name": "wizard.new.wizard",
|
||||
"labelPosition": "left",
|
||||
"generateSummaryPage": false,
|
||||
"pages": [
|
||||
{
|
||||
"title": "%wizard.select.cluster.title%",
|
||||
"sections": [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"label": "%cores-limit.label%",
|
||||
"description": "%cores-limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"defaultValue": 5,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "is_integer",
|
||||
"description": "%cores.limit.should.be.integer%"
|
||||
},
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%cores-request.label%",
|
||||
"description": "%cores-request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"defaultValue": 2,
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "is_integer",
|
||||
"description": "%requested.cores.should.be.integer%"
|
||||
},
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl"
|
||||
}
|
||||
],
|
||||
"when": true
|
||||
}
|
||||
],
|
||||
"agreement": {
|
||||
"template": "%wizard.data.controller.agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%contoso.agreement.privacy.statement%",
|
||||
"url": "https://go.contoso.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%wizard.agreement.contosoCmd.eula%",
|
||||
"url": "https://aka.ms/eula-contosoCmd-en"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test-wizard",
|
||||
"displayName": "%resource.type.wizard.display.name%",
|
||||
"description": "%resource.type.wizard.description%",
|
||||
"platforms": "*",
|
||||
"icon": {
|
||||
"light": "./images/book.svg",
|
||||
"dark": "./images/book_inverse.svg"
|
||||
},
|
||||
"icon": {
|
||||
"light": "./images/book.svg",
|
||||
"dark": "./images/book_inverse.svg"
|
||||
},
|
||||
"providers": [
|
||||
{
|
||||
"notebookWizard": {
|
||||
"notebook": "%deployment-notebook-1%",
|
||||
"notebook": "%deployment-notebook-1%",
|
||||
"type": "new-arc-control-plane",
|
||||
"runNotebook": false,
|
||||
"codeCellInsertionPosition": 1,
|
||||
@@ -67,7 +159,7 @@
|
||||
"sections": [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
{
|
||||
"type": "readonly_text",
|
||||
"label": "%wizard.project.details.description%",
|
||||
"labelWidth": "600px"
|
||||
@@ -90,39 +182,39 @@
|
||||
"defaultValue": "aks-dev-test",
|
||||
"optionsType": "radio"
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
{
|
||||
"type": "options",
|
||||
"label": "%wizard.data.controllers%",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_CONTROLLER",
|
||||
"editable": false,
|
||||
"options": {
|
||||
"source": {
|
||||
"type": "ArcControllersOptionsSource",
|
||||
"variableNames": {
|
||||
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "ArcControllersOptionsSource",
|
||||
"variableNames": {
|
||||
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
|
||||
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
|
||||
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"values":[
|
||||
"ignored1",
|
||||
"ignored2"
|
||||
],
|
||||
"optionsType": "dropdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "%wizard.dropdown.options.field%",
|
||||
"variableName": "AZDATA_NB_VAR_DROPDOWN_OPTIONS",
|
||||
"type": "options",
|
||||
"options": {
|
||||
"values": ["1","2","3"],
|
||||
"defaultValue": "2",
|
||||
"optionsType": "dropdown"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "%wizard.dropdown.options.field%",
|
||||
"variableName": "AZDATA_NB_VAR_DROPDOWN_OPTIONS",
|
||||
"type": "options",
|
||||
"options": {
|
||||
"values": ["1","2","3"],
|
||||
"defaultValue": "2",
|
||||
"optionsType": "dropdown"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -273,31 +365,31 @@
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_DROPDOWN_OPTIONS)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller.endpoint%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER_ENDPOINT)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller.username%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER_USERNAME)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller.password%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER_PASSWORD)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller.endpoint%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER_ENDPOINT)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller.username%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER_USERNAME)"
|
||||
},
|
||||
{
|
||||
"label": "%wizard.summary.controller.password%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_CONTROLLER_PASSWORD)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -326,159 +418,159 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-data-service",
|
||||
"displayName": "%resource-type-display-name%",
|
||||
"description": "%resource-type-description%",
|
||||
"platforms": [
|
||||
"darwin",
|
||||
"win32",
|
||||
"linux"
|
||||
],
|
||||
"icon": {
|
||||
"light": "./images/book.svg",
|
||||
"dark": "./images/book_inverse.svg"
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"name": "edition",
|
||||
"displayName": "%option-display-name%",
|
||||
"values": [
|
||||
{
|
||||
"name": "evaluation",
|
||||
"displayName": "%option-value-name-1%"
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"displayName": "%option-value-name-2%"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"dialog": {
|
||||
"notebook": "%deployment-notebook-1%",
|
||||
"title": "%dialog-title-1%",
|
||||
"name": "dialog1",
|
||||
"tabs": [
|
||||
{
|
||||
"title": "",
|
||||
"sections": [
|
||||
{
|
||||
"title": "",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%text-field%",
|
||||
"variableName": "AZDATA_NB_VAR_TEXT",
|
||||
"type": "text",
|
||||
"defaultValue": "abc",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_PASSWORD",
|
||||
"type": "password",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%confirm-password%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "kube_cluster_context_picker",
|
||||
"label": "%kube.cluster.context%",
|
||||
"required": true,
|
||||
"inputWidth": "350px",
|
||||
"variableName": "AZDATA_NB_VAR_CLUSTER_CONTEXT",
|
||||
"configFileVariableName": "AZDATA_NB_VAR_CONFIG_FILE"
|
||||
},
|
||||
{
|
||||
"label": "%number-field%",
|
||||
"variableName": "AZDATA_NB_VAR_NUMBER",
|
||||
"type": "number",
|
||||
"defaultValue": "100",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl"
|
||||
}
|
||||
],
|
||||
"when": "edition=evaluation"
|
||||
},
|
||||
{
|
||||
"dialog": {
|
||||
"notebook": "%deployment-notebook-2%",
|
||||
"title": "%dialog-title-2%",
|
||||
"name": "dialog2",
|
||||
"tabs": [
|
||||
{
|
||||
"title": "",
|
||||
"sections": [
|
||||
{
|
||||
"title": "",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%text-field%",
|
||||
"variableName": "AZDATA_NB_VAR_TEXT",
|
||||
"type": "text",
|
||||
"defaultValue": "abc",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_PASSWORD",
|
||||
"type": "password",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%confirm-password%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%number-field%",
|
||||
"variableName": "AZDATA_NB_VAR_NUMBER",
|
||||
"type": "number",
|
||||
"defaultValue": "100",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "docker"
|
||||
}
|
||||
],
|
||||
"when": "edition=standard"
|
||||
}
|
||||
],
|
||||
"agreement": {
|
||||
"template": "%agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%agreement-1-name%",
|
||||
"url": "https://www.contoso.com"
|
||||
},
|
||||
{
|
||||
"text": "%agreement-2-name%",
|
||||
"url": "https://portal.azure.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "x-data-service",
|
||||
"displayName": "%resource-type-display-name%",
|
||||
"description": "%resource-type-description%",
|
||||
"platforms": [
|
||||
"darwin",
|
||||
"win32",
|
||||
"linux"
|
||||
],
|
||||
"icon": {
|
||||
"light": "./images/book.svg",
|
||||
"dark": "./images/book_inverse.svg"
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"name": "edition",
|
||||
"displayName": "%option-display-name%",
|
||||
"values": [
|
||||
{
|
||||
"name": "evaluation",
|
||||
"displayName": "%option-value-name-1%"
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"displayName": "%option-value-name-2%"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"dialog": {
|
||||
"notebook": "%deployment-notebook-1%",
|
||||
"title": "%dialog-title-1%",
|
||||
"name": "dialog1",
|
||||
"tabs": [
|
||||
{
|
||||
"title": "",
|
||||
"sections": [
|
||||
{
|
||||
"title": "",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%text-field%",
|
||||
"variableName": "AZDATA_NB_VAR_TEXT",
|
||||
"type": "text",
|
||||
"defaultValue": "abc",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_PASSWORD",
|
||||
"type": "password",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%confirm-password%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "kube_cluster_context_picker",
|
||||
"label": "%kube.cluster.context%",
|
||||
"required": true,
|
||||
"inputWidth": "350px",
|
||||
"variableName": "AZDATA_NB_VAR_CLUSTER_CONTEXT",
|
||||
"configFileVariableName": "AZDATA_NB_VAR_CONFIG_FILE"
|
||||
},
|
||||
{
|
||||
"label": "%number-field%",
|
||||
"variableName": "AZDATA_NB_VAR_NUMBER",
|
||||
"type": "number",
|
||||
"defaultValue": "100",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl"
|
||||
}
|
||||
],
|
||||
"when": "edition=evaluation"
|
||||
},
|
||||
{
|
||||
"dialog": {
|
||||
"notebook": "%deployment-notebook-2%",
|
||||
"title": "%dialog-title-2%",
|
||||
"name": "dialog2",
|
||||
"tabs": [
|
||||
{
|
||||
"title": "",
|
||||
"sections": [
|
||||
{
|
||||
"title": "",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%text-field%",
|
||||
"variableName": "AZDATA_NB_VAR_TEXT",
|
||||
"type": "text",
|
||||
"defaultValue": "abc",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_PASSWORD",
|
||||
"type": "password",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%confirm-password%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%number-field%",
|
||||
"variableName": "AZDATA_NB_VAR_NUMBER",
|
||||
"type": "number",
|
||||
"defaultValue": "100",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "docker"
|
||||
}
|
||||
],
|
||||
"when": "edition=standard"
|
||||
}
|
||||
],
|
||||
"agreement": {
|
||||
"template": "%agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%agreement-1-name%",
|
||||
"url": "https://www.contoso.com"
|
||||
},
|
||||
{
|
||||
"text": "%agreement-2-name%",
|
||||
"url": "https://portal.azure.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
"resource.type.wizard.display.name": "Test controller",
|
||||
"resource.type.wizard.description": "Creates a Test controller",
|
||||
|
||||
"validation.wizard.display.name": "Validation Wizard",
|
||||
"validation.wizard.description": "A wizard to test out validations",
|
||||
|
||||
"wizard.new.wizard.title": "Create Test controller",
|
||||
"wizard.cluster.environment.title": "What is your target existing Kubernetes cluster environment?",
|
||||
"wizard.select.cluster.title": "Select from installed existing Kubernetes clusters",
|
||||
@@ -52,6 +55,15 @@
|
||||
"wizard.data.controller.agreement": "I accept {0} and {1}.",
|
||||
"contoso.agreement.privacy.statement":"contoso Privacy Statement",
|
||||
"wizard.agreement.contosoCmd.eula":"contoso cmd license terms",
|
||||
"deploy.wizard.action":"Script to notebook"
|
||||
"deploy.wizard.action":"Script to notebook",
|
||||
|
||||
"cores-limit.label": "Cores Limit",
|
||||
"cores-limit.description": "The cores limit must be an integer.",
|
||||
"cores-request.label": "Cores Request",
|
||||
"cores-request.description": "The requested cores must be an integer.",
|
||||
|
||||
"requested.cores.should.be.integer": "Requested cores must be an integer",
|
||||
"cores.limit.should.be.integer": "Cores limit must be an integer",
|
||||
"requested.cores.less.than.or.equal.to.cores.limit": "Requested cores must be <= cores limit",
|
||||
"cores.limit.greater.than.or.equal.to.requested.cores": "Cores limit must be >= requested cores"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user