diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 2cb3b8faac..1b23cec42a 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -989,11 +989,24 @@ "variableName": "AZDATA_NB_VAR_SQL_REPLICAS", "options": { "values": [ - "1", - "3" + "%arc.sql.two.replicas%", + "%arc.sql.three.replicas%" ], - "defaultValue": "1", + "defaultValue": "%arc.sql.two.replicas%", "optionsType": "radio" + }, + "dynamicOptions": + { + "target": "AZDATA_NB_VAR_SQL_SERVICE_TIER", + "alternates": [ + { + "selection": "%arc.sql.service.tier.general.purpose%", + "alternateValues": [ + "%arc.sql.one.replica%" + ], + "defaultValue": "%arc.sql.one.replica%" + } + ] } }, { @@ -1069,6 +1082,29 @@ "description": "%memory.limit.greater.than.or.equal.to.requested.memory%" } ] + }, + { + "type": "options", + "label": "%arc.sql.service.tier.label%", + "description": "%arc.sql.service.tier.description%", + "required": true, + "variableName": "AZDATA_NB_VAR_SQL_SERVICE_TIER", + "options": { + "values": [ + "%arc.sql.service.tier.business.critical%", + "%arc.sql.service.tier.general.purpose%" + ], + "defaultValue": "%arc.sql.service.tier.business.critical%", + "optionsType": "radio" + } + }, + { + "type": "checkbox", + "label": "%arc.sql.dev.use.label%", + "description": "%arc.sql.dev.use.description%", + "defaultValue": "false", + "variableName": "AZDATA_NB_VAR_SQL_DEV_USE", + "required": true } ] } diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json index 0654ff4e8b..b7689ebc5a 100644 --- a/extensions/arc/package.nls.json +++ b/extensions/arc/package.nls.json @@ -86,8 +86,13 @@ "arc.sql.invalid.instance.name": "Instance name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 13 characters or fewer in length.", "arc.storage-class.dc.label": "Storage Class", "arc.sql.storage-class.dc.description": "The storage class to be used for all data and logs persistent volumes for all data controller pods that require them.", - "arc.sql.replicas.label": "Replicas", - "arc.sql.replicas.description": "The number of SQL Managed Instance replicas that will be deployed in your Kubernetes cluster for high availability purposes", + "arc.sql.high.availability.label": "High Availability", + "arc.sql.high.availability.description": "Enable additional replicas for high availabilty. The compute and storage configuration selected below will be applied to all replicas.", + "arc.sql.service.tier.general.purpose": "General Purpose (Up to 24 vCores and 128 Gi of RAM, standard high availability)", + "arc.sql.service.tier.business.critical": "[PREVIEW] Business Critical (Unlimited vCores and RAM, advanced high availability)", + "arc.sql.one.replica": "1 replica", + "arc.sql.two.replicas": "2 replicas", + "arc.sql.three.replicas": "3 replicas", "arc.storage-class.data.label": "Storage Class (Data)", "arc.sql.storage-class.data.description": "The storage class to be used for data (.mdf). If no value is specified, the default storage class will be used.", "arc.postgres.storage-class.data.description": "The storage class to be used for data persistent volumes", @@ -103,6 +108,10 @@ "arc.sql.memory-limit.description": "The limit of the capacity of the managed instance as an integer.", "arc.memory-request.label": "Memory Request", "arc.sql.memory-request.description": "The request for the capacity of the managed instance as an integer amount of memory in GBs.", + "arc.sql.service.tier.label": "Service Tier", + "arc.sql.service.tier.description": "Select from the latest vCore service tiers available for SQL Managed Instance - Azure Arc including General Purpose and Business Critical. {0}", + "arc.sql.dev.use.label": "For development use only", + "arc.sql.dev.use.description": "Check the box to indicate this instance will be used for development or testing purposes only. This instance will not be billed.", "arc.postgres.storage-class.backups.description": "The storage class to be used for backup persistent volumes", "arc.password": "Password", "arc.confirm.password": "Confirm password", diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index b252854bcd..3747757d4f 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -242,6 +242,10 @@ export function instanceOfDynamicEnablementInfo(obj: any): obj is DynamicEnablem return (obj)?.target !== undefined && (obj)?.value !== undefined; } +export function instanceOfDynamicOptionsInfo(obj: any): obj is DynamicOptionsInfo { + return (obj)?.target !== undefined && (obj)?.alternates !== undefined; +} + export interface DialogInfoBase { title: string; name: string; @@ -290,6 +294,17 @@ export interface DynamicEnablementInfo { value: string } +export interface DynamicOptionsInfo { + target: string, + alternates: DynamicOptionsAlternates[] +} + +export interface DynamicOptionsAlternates { + selection: string, + alternateValues: string[], + defaultValue: string +} + export interface ValueProviderInfo { providerId: string, triggerField: string @@ -340,6 +355,7 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase { links?: azdata.LinkArea[]; editable?: boolean; // for editable drop-down, enabled?: boolean | DynamicEnablementInfo; + dynamicOptions?: DynamicOptionsInfo; isEvaluated?: boolean; validations?: ValidationInfo[]; valueProvider?: ValueProviderInfo; diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 5c3f5e2283..09703272f1 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -11,7 +11,7 @@ import { IOptionsSourceProvider } from 'resource-deployment'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { getDateTimeString, getErrorMessage, isUserCancelledError, throwUnless } from '../common/utils'; -import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, InitialVariableValues, instanceOfDynamicEnablementInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces'; +import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, InitialVariableValues, instanceOfDynamicEnablementInfo, instanceOfDynamicOptionsInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces'; import * as loc from '../localizedConstants'; import { apiService } from '../services/apiService'; import { valueProviderService } from '../services/valueProviderService'; @@ -41,6 +41,7 @@ export type InputComponentInfo = { getValue: () => Promise; setValue: (value: InputValueType) => void; getDisplayValue?: () => Promise; + setOptions?: (options: OptionsInfo) => void; onValueChanged: vscode.Event; isPassword?: boolean }; @@ -341,6 +342,7 @@ export function initializeWizardPage(context: WizardPageContext): void { }); })); await hookUpDynamicEnablement(context); + await hookUpDynamicOptions(context); await hookUpValueProviders(context); const formBuilder = view.modelBuilder.formContainer().withFormItems( sections.map(section => { return { title: '', component: section }; }), @@ -408,6 +410,58 @@ async function hookUpDynamicEnablement(context: WizardPageContext): Promise { + await Promise.all(context.pageInfo.sections.map(async section => { + if (!section.fields) { + return; + } + await Promise.all(section.fields.map(async field => { + if (instanceOfDynamicOptionsInfo(field.dynamicOptions)) { + const fieldKey = field.variableName || field.label; + const fieldComponent = context.inputComponents[fieldKey]; + const targetComponent = context.inputComponents[field.dynamicOptions.target]; + if (!targetComponent) { + console.error(`Could not find target component ${field.dynamicOptions.target} when hooking up dynamic options for ${field.label}`); + return; + } + const updateOptions = async () => { + const currentValue = await targetComponent.getValue(); + if (field.dynamicOptions && field.options && fieldComponent && fieldComponent.setOptions) { + const targetValueFound = field.dynamicOptions.alternates.find(item => item.selection === currentValue); + if (targetValueFound) { + fieldComponent.setOptions({ + values: targetValueFound.alternateValues, + defaultValue: targetValueFound.defaultValue + }); + } else { + fieldComponent.setOptions({ + values: field.options.values, + defaultValue: (field.options).defaultValue + }); + } + } + }; + targetComponent.onValueChanged(() => { + updateOptions(); + }); + await updateOptions(); + } + })); + })); +} + + async function hookUpValueProviders(context: WizardPageContext): Promise { await Promise.all(context.pageInfo.sections.map(async section => { if (!section.fields) { @@ -861,11 +915,16 @@ async function substituteVariableValues(inputComponents: InputComponents, inputV ); return inputValue; } - +/** + * Renders a label on the left and a checkbox with an empty string label on the right, for use under page sections. + * @param context The context to use to create the field + */ function processCheckboxField(context: FieldContext): void { - const checkbox = createCheckboxInputInfo(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required }); - context.components.push(checkbox.component); + 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 checkbox = createCheckboxInputInfo(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: '', required: context.fieldInfo.required }); + checkbox.labelComponent = label; context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, checkbox); + addLabelInputPairToContainer(context.view, context.components, label, checkbox.component, context.fieldInfo); } /** @@ -1024,7 +1083,8 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (( component: radioGroupLoadingComponentBuilder, labelComponent: label, getValue: async (): Promise => radioGroupLoadingComponentBuilder.value, - setValue: (value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); }, + setValue: (_value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); }, + setOptions: (optionsInfo: OptionsInfo) => { radioGroupLoadingComponentBuilder.loadOptions(optionsInfo); }, getDisplayValue: async (): Promise => radioGroupLoadingComponentBuilder.displayValue, onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged, });