From dc0651aef7b4ab3af32ad5d34c273160fb5fdc54 Mon Sep 17 00:00:00 2001 From: Candice Ye Date: Thu, 21 Oct 2021 16:51:31 -0700 Subject: [PATCH] Added a dynamic Cost Summary section to SQL MIAA Deployment Wizard (#17420) * Added valueprovider for pricing. Pushing this for troubleshooting help. * Committing changes for troubleshooting help. Moved InputValueType to typings file. * Add readonly inputs to list * Fixed ordering of package.json merge items * Estimated cost moved to input page, ValueProvider only takes in a triggerfields[] and not a single string, fixed pricing logic. * Removed pricingModel.ts * Reverted some comments and code changes that were used in debugging. * Changed some values from localizedConstants to single-quote constants' * Changed some values from localizedConstants to single-quote constants' * Added copyright header to pricingUtils.ts * Removed try catch in extension.ts valueproviders, made some values in PricingUtils.ts top-level instead of exporting. * Minor changes, added some comments and localized USD. * Changes pricingutils classes to be constants, and added disposable to Hookupvalueprovider Co-authored-by: Candice Ye Co-authored-by: chgagnon --- extensions/arc/package.json | 93 +++++++++++++- extensions/arc/package.nls.json | 27 ++++ extensions/arc/src/common/pricingUtils.ts | 117 ++++++++++++++++++ extensions/arc/src/extension.ts | 33 +++++ extensions/arc/src/localizedConstants.ts | 8 ++ .../resource-deployment/src/interfaces.ts | 2 +- .../test/ui/validation/validations.test.ts | 2 +- .../src/typings/resource-deployment.d.ts | 8 +- .../src/ui/modelViewUtils.ts | 50 ++++++-- .../src/ui/validation/validations.ts | 2 +- 10 files changed, 321 insertions(+), 21 deletions(-) create mode 100644 extensions/arc/src/common/pricingUtils.ts diff --git a/extensions/arc/package.json b/extensions/arc/package.json index f6a79a07c4..2189b92f9e 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -1079,6 +1079,7 @@ "variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST", "type": "number", "min": 1, + "defaultValue": 2, "required": false, "validations": [ { @@ -1095,6 +1096,7 @@ "type": "number", "min": 1, "required": false, + "defaultValue": 4, "validations": [ { "type": ">=", @@ -1109,6 +1111,7 @@ "variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST", "type": "number", "min": 2, + "defaultValue": 4, "required": false, "validations": [ { @@ -1124,6 +1127,7 @@ "variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT", "type": "number", "min": 2, + "defaultValue": 8, "required": false, "validations": [ { @@ -1136,9 +1140,9 @@ { "type": "options", "label": "%arc.sql.service.tier.label%", + "variableName": "AZDATA_NB_VAR_SQL_SERVICE_TIER", "description": "%arc.sql.service.tier.description%", "required": true, - "variableName": "AZDATA_NB_VAR_SQL_SERVICE_TIER", "options": { "values": [ "%arc.sql.service.tier.business.critical%", @@ -1151,10 +1155,16 @@ { "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 + "description": "%arc.sql.dev.use.description%", + "defaultValue": false + }, + { + "type": "checkbox", + "label": "%arc.sql.license.type.label%", + "variableName": "AZDATA_NB_VAR_SQL_LICENSE_TYPE", + "description": "%arc.sql.license.type.description%", + "defaultValue": false } ] }, @@ -1184,8 +1194,81 @@ "required": false } ] + }, + { + "title": "%arc.sql.cost.summary%", + "fields": [ + { + "label": "%arc.sql.cost.summary.additional.charge%", + "type": "readonly_text", + "enabled": true, + "labelWidth": "750px", + "links": [ + { + "text": "%arc.sql.cost.summary.pricing.details%", + "url": "https://aka.ms/ArcSQLBilling" + } + ] + }, + { + "label": "%arc.sql.cost.summary.cost.vcore%", + "type": "readonly_text", + "isEvaluated": true, + "defaultValue": "0.00 USD", + "valueProvider": { + "providerId": "params-to-cost-per-vcore", + "triggerFields": [ + "AZDATA_NB_VAR_SQL_DEV_USE", + "AZDATA_NB_VAR_SQL_SERVICE_TIER" + ] + } + }, + { + "label": "%arc.sql.cost.summary.vcore.limit%", + "type": "readonly_text", + "isEvaluated": true, + "defaultValue": "x 4", + "valueProvider": { + "providerId": "params-to-vcore-limit", + "triggerFields": [ + "AZDATA_NB_VAR_SQL_CORES_LIMIT" + ] + } + }, + { + "label": "%arc.sql.cost.summary.azure.hybrid.benefit.discount%", + "type": "readonly_text", + "isEvaluated": true, + "defaultValue": "- 0", + "valueProvider": { + "providerId": "params-to-hybrid-benefit-discount", + "triggerFields": [ + "AZDATA_NB_VAR_SQL_CORES_LIMIT", + "AZDATA_NB_VAR_SQL_DEV_USE", + "AZDATA_NB_VAR_SQL_SERVICE_TIER", + "AZDATA_NB_VAR_SQL_LICENSE_TYPE" + ] + } + }, + { + "label": "%arc.sql.cost.summary.estimated.cost.per.month%", + "type": "readonly_text", + "variableName": "AZDATA_NB_VAR_SQL_ESTIMATED_COST", + "defaultValue": "0.00 USD", + "valueProvider": { + "providerId": "params-to-estimated-cost", + "triggerFields": [ + "AZDATA_NB_VAR_SQL_REPLICAS", + "AZDATA_NB_VAR_SQL_CORES_LIMIT", + "AZDATA_NB_VAR_SQL_DEV_USE", + "AZDATA_NB_VAR_SQL_SERVICE_TIER", + "AZDATA_NB_VAR_SQL_LICENSE_TYPE" + ] + } + } + ] } - ] + ] } ] }, diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json index bc75a52b3e..d75e22e66f 100644 --- a/extensions/arc/package.nls.json +++ b/extensions/arc/package.nls.json @@ -10,6 +10,7 @@ "command.removeController.title": "Remove Controller", "command.refresh.title": "Refresh", "command.editConnection.title": "Edit Connection", + "command.estimateCostSqlMiaa.title": "Estimate Cost of SQL Managed Instance - Azure Arc", "arc.openDashboard": "Manage", "resource.type.azure.arc.display.name": "Azure Arc data controller (preview)", @@ -96,6 +97,30 @@ "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.sql.cost.summary.sql.miaa.cost.summary": "SQL Managed Instance - Azure Arc Cost Summary", + "arc.sql.cost.summary.sql.miaa": "SQL managed instance - Azure Arc", + "arc.sql.cost.summary.estimated.cost.per.month": "Estimated cost per month", + "arc.sql.summary.arc.by.microsoft" : "by Microsoft", + "arc.sql.cost.summary": "Cost Summary", + "arc.sql.cost.summary.service.tier": "Service Tier", + "arc.sql.cost.summary.general.purpose": "General Purpose", + "arc.sql.cost.summary.business.critical": "Business Critical", + "arc.sql.cost.summary.cost.vcore": "Cost per vCore (in USD)", + "arc.sql.cost.summary.vcore.limit": "CPU vCores Limit", + "arc.sql.cost.summary.azure.hybrid.benefit.discount": "Azure Hybrid Benefit discount (in USD)", + "arc.sql.cost.summary.sql.connection.info": "SQL Connection Information", + "arc.sql.cost.summary.sql.instance.settings": "SQL Instance Settings", + "arc.sql.cost.summary.service.tier.learn.more.description": "Select from the latest vCore service tiers available for SQL Managed Instance - Azure Arc including General Purpose and Business Critical. {0}", + "arc.sql.cost.summary.service.tier.learn.more.text": "Learn more", + "arc.sql.cost.summary.basics": "Basics", + "arc.sql.cost.summary.subscription": "Subscription", + "arc.sql.cost.summary.resource.group": "Resource group", + "arc.sql.cost.summary.instance.name": "Instance name", + "arc.sql.cost.summary.custom.location": "Custom location", + "arc.sql.cost.summary.admin.account": "Administrator account", + "arc.sql.cost.summary.managed.instance.admin.login": "Managed Instance admin login", + "arc.sql.cost.summary.additional.charge": "Additional charge per usage. See {0} for more detail.", + "arc.sql.cost.summary.pricing.details": "pricing details", "arc.postgres.storage-class.data.description": "The storage class to be used for data persistent volumes", "arc.storage-class.datalogs.label": "Storage Class (Database logs)", "arc.sql.storage-class.datalogs.description": "The storage class to be used for database logs (.ldf). If no value is specified, the default storage class will be used.", @@ -123,6 +148,8 @@ "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.license.type.label": "I already have a SQL Server License", + "arc.sql.license.type.description": "Apply the Azure Hybrid Benefit if you already own a SQL Server License", "arc.sql.pitr.description": "Point in time restore", "arc.sql.retention.days.label": "PITR retention (days)", "arc.sql.retention.days.description": "Specify how long you want to keep your point-in-time backups.", diff --git a/extensions/arc/src/common/pricingUtils.ts b/extensions/arc/src/common/pricingUtils.ts new file mode 100644 index 0000000000..8c9ddf4eef --- /dev/null +++ b/extensions/arc/src/common/pricingUtils.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InputValueType } from 'resource-deployment'; +import * as loc from '../localizedConstants'; + +export const SqlManagedInstanceGeneralPurpose = { + tierName: loc.generalPurposeLabel, + basePricePerCore: 80, + licenseIncludedPricePerCore: 153, + maxMemorySize: 128, + maxVCores: 24, + + replicaOptions: [ + { + text: loc.replicaOne, + value: 1, + } + ], + + defaultReplicaValue: 1 +}; + +const SqlManagedInstanceBusinessCritical = { + tierName: loc.businessCriticalLabel, + + // Set to real values when BC is ready + basePricePerCore: 0, + licenseIncludedPricePerCore: 0, + + replicaOptions: [ + { + text: loc.replicaTwo, + value: 2, + }, + { + text: loc.replicaThree, + value: 3, + } + ], + + defaultReplicaValue: 3 +}; + +export const SqlManagedInstancePricingLink: string = 'https://aka.ms/ArcSQLBilling'; + +export const serviceTierVarName = 'AZDATA_NB_VAR_SQL_SERVICE_TIER'; +export const devUseVarName = 'AZDATA_NB_VAR_SQL_DEV_USE'; +export const vcoresLimitVarName = 'AZDATA_NB_VAR_SQL_CORES_LIMIT'; +export const licenseTypeVarName = 'AZDATA_NB_VAR_SQL_LICENSE_TYPE'; + +// Estimated base price for one vCore. +export function estimatedBasePriceForOneVCore(mapping: { [key: string]: InputValueType }): number { + let price = 0; + if (mapping[devUseVarName] === 'true') { + price = 0; + } else if (mapping[devUseVarName] === 'false') { + if (mapping[serviceTierVarName] === SqlManagedInstanceGeneralPurpose.tierName) { + price = SqlManagedInstanceGeneralPurpose.basePricePerCore; + } else if (mapping[serviceTierVarName] === SqlManagedInstanceBusinessCritical.tierName) { + price = SqlManagedInstanceBusinessCritical.basePricePerCore; + } + } + return price; +} + +// Estimated SQL server license price for one vCore. +export function estimatedSqlServerLicensePriceForOneVCore(mapping: { [key: string]: InputValueType }): number { + let price = 0; + if (mapping[devUseVarName] === 'true') { + price = 0; + } else if (mapping[devUseVarName] === 'false') { + if (mapping[serviceTierVarName] === SqlManagedInstanceGeneralPurpose.tierName) { + price = SqlManagedInstanceGeneralPurpose.licenseIncludedPricePerCore - SqlManagedInstanceGeneralPurpose.basePricePerCore; + } else if (mapping[serviceTierVarName] === SqlManagedInstanceBusinessCritical.tierName) { + price = SqlManagedInstanceBusinessCritical.licenseIncludedPricePerCore - SqlManagedInstanceBusinessCritical.basePricePerCore; + } + } + return price; +} + +// Full price for one vCore. This is shown on the cost summary card. +export function fullPriceForOneVCore(mapping: { [key: string]: InputValueType }): number { + return estimatedBasePriceForOneVCore(mapping) + estimatedSqlServerLicensePriceForOneVCore(mapping); +} + +// Gets number of vCores limit specified +export function numCores(mapping: { [key: string]: InputValueType }): number { + return mapping[vcoresLimitVarName] ? mapping[vcoresLimitVarName] : 0; +} + +// Full price for all selected vCores. +export function vCoreFullPriceForAllCores(mapping: { [key: string]: InputValueType }): number { + return fullPriceForOneVCore(mapping) * numCores(mapping); +} + +// SQL Server License price for all vCores. This is shown on the cost summary card if customer has SQL server license. +export function vCoreSqlServerLicensePriceForAllCores(mapping: { [key: string]: InputValueType }): number { + return estimatedSqlServerLicensePriceForOneVCore(mapping) * numCores(mapping); +} + +// If the customer doesn't already have SQL Server License, AHB discount is set to zero because the price will be included +// in the total cost. If they already have it (they checked the box), then a discount will be applied. +export function azureHybridBenefitDiscount(mapping: { [key: string]: InputValueType }): number { + if (mapping[licenseTypeVarName] === 'true') { + return vCoreSqlServerLicensePriceForAllCores(mapping); + } else { + return 0; + } +} + +// Total price that will be charged to a customer. Is shown on the cost summary card. +export function total(mapping: { [key: string]: InputValueType }): number { + return vCoreFullPriceForAllCores(mapping) - azureHybridBenefitDiscount(mapping); +} diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts index b9ed04a641..6544898fc7 100644 --- a/extensions/arc/src/extension.ts +++ b/extensions/arc/src/extension.ts @@ -14,6 +14,7 @@ import { ConnectToControllerDialog } from './ui/dialogs/connectControllerDialog' import { AzureArcTreeDataProvider } from './ui/tree/azureArcTreeDataProvider'; import { ControllerTreeNode } from './ui/tree/controllerTreeNode'; import { TreeNode } from './ui/tree/treeNode'; +import * as pricing from './common/pricingUtils'; export async function activate(context: vscode.ExtensionContext): Promise { IconPathHelper.setExtensionContext(context); @@ -61,6 +62,38 @@ export async function activate(context: vscode.ExtensionContext): Promisevscode.extensions.getExtension(rd.extension.name)?.exports; context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider))); + // Register valueprovider for getting the calculated cost per VCore. + context.subscriptions.push(rdApi.registerValueProvider({ + id: 'params-to-cost-per-vcore', + getValue: async (mapping: { [key: string]: rd.InputValueType }) => { + return pricing.fullPriceForOneVCore(mapping); + } + })); + + // Register valueprovider for getting the number of CPU VCores Limit input by the user. + context.subscriptions.push(rdApi.registerValueProvider({ + id: 'params-to-vcore-limit', + getValue: async (mapping: { [key: string]: rd.InputValueType }) => { + return 'x ' + pricing.numCores(mapping).toString(); + } + })); + + // Register valueprovider for getting the amount of hybrid benefit discount to be applied. + context.subscriptions.push(rdApi.registerValueProvider({ + id: 'params-to-hybrid-benefit-discount', + getValue: async (mapping: { [key: string]: rd.InputValueType }) => { + return '- ' + pricing.azureHybridBenefitDiscount(mapping).toString(); + } + })); + + // Register valueprovider for getting the total estimated cost. + context.subscriptions.push(rdApi.registerValueProvider({ + id: 'params-to-estimated-cost', + getValue: async (mapping: { [key: string]: rd.InputValueType }) => { + return pricing.total(mapping).toString() + ' ' + loc.USD; + } + })); + return arcApi(treeDataProvider); } diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index d4761ff8cd..f30f918d41 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -274,6 +274,14 @@ export function connectionString(type: string): string { return localize({ key: export function copyConnectionStringToClipboard(type: string): string { return localize({ key: 'arc.copyConnectionStringToClipboard', comment: ['{0} is the name of the type of connection string (e.g. Java)'] }, "Copy {0} Connection String to clipboard", type); } export function copyValueToClipboard(valueName: string): string { return localize({ key: 'arc.copyValueToClipboard', comment: ['{0} is the name of the type of value being copied (e.g. Coordinator endpoint)'] }, "Copy {0} to clipboard", valueName); } +// Pricing Constants +export const replicaOne = localize('arc.replicaOne', "1 replica"); +export const replicaTwo = localize('arc.replicaTwo', "2 replicas"); +export const replicaThree = localize('arc.replicaThree', "3 replicas"); +export const generalPurposeLabel = localize('arc.generalPurposeLabel', "General Purpose (Up to 24 vCores and 128 Gi of RAM, standard high availability)"); +export const businessCriticalLabel = localize('arc.businessCriticalLabel', "[PREVIEW] Business Critical (Unlimited vCores and RAM, advanced high availability)"); +export const USD = localize('arc.USD', "USD"); + // Errors export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A connection is required to show and set database engine settings."); export const miaaConnectionRequired = localize('arc.miaaConnectionRequired', "A connection is required to list the databases on this instance."); diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 3747757d4f..73cdb895e7 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -307,7 +307,7 @@ export interface DynamicOptionsAlternates { export interface ValueProviderInfo { providerId: string, - triggerField: string + triggerFields: string[] } export interface FieldInfoBase { diff --git a/extensions/resource-deployment/src/test/ui/validation/validations.test.ts b/extensions/resource-deployment/src/test/ui/validation/validations.test.ts index 42e8e76bab..cd088824fc 100644 --- a/extensions/resource-deployment/src/test/ui/validation/validations.test.ts +++ b/extensions/resource-deployment/src/test/ui/validation/validations.test.ts @@ -5,10 +5,10 @@ import * as azdata from 'azdata'; import 'mocha'; +import { InputValueType } from 'resource-deployment'; import * as should from 'should'; import * as sinon from 'sinon'; 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 = { diff --git a/extensions/resource-deployment/src/typings/resource-deployment.d.ts b/extensions/resource-deployment/src/typings/resource-deployment.d.ts index 01e4c5a156..cf9f454618 100644 --- a/extensions/resource-deployment/src/typings/resource-deployment.d.ts +++ b/extensions/resource-deployment/src/typings/resource-deployment.d.ts @@ -24,9 +24,15 @@ declare module 'resource-deployment' { getIsPassword?: (variableName: string) => boolean | Promise; } + export type InputValueType = string | number | boolean | undefined; + + /** + * Gets a calculated value based on the given input values. + * @param triggerValues A map of the trigger field names and their current values specified in the valueProvider field info + */ export interface IValueProvider { readonly id: string, - getValue(triggerValue: string): Promise; + getValue(triggerValues: string | {[key: string]: InputValueType}): Promise; } /** diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 09703272f1..308d8b52f4 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -7,7 +7,7 @@ import { azureResource } from 'azureResource'; import * as fs from 'fs'; import { EOL } from 'os'; import * as path from 'path'; -import { IOptionsSourceProvider } from 'resource-deployment'; +import { InputValueType, IOptionsSourceProvider } from 'resource-deployment'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { getDateTimeString, getErrorMessage, isUserCancelledError, throwUnless } from '../common/utils'; @@ -33,7 +33,6 @@ const localize = nls.loadMessageBundle(); */ export type Validator = () => { valid: boolean, message: string }; -export type InputValueType = string | number | undefined; export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder; export type InputComponentInfo = { component: T; @@ -471,20 +470,35 @@ async function hookUpValueProviders(context: WizardPageContext): Promise { if (field.valueProvider) { const fieldKey = field.variableName || field.label; const fieldComponent = context.inputComponents[fieldKey]; - const targetComponent = context.inputComponents[field.valueProvider.triggerField]; - if (!targetComponent) { - console.error(`Could not find target component ${field.valueProvider.triggerField} when hooking up value providers for ${field.label}`); - return; - } const provider = await valueProviderService.getValueProvider(field.valueProvider.providerId); + + let targetComponentLabelToComponent: { [label: string]: InputComponentInfo; } = {}; + + field.valueProvider.triggerFields.forEach((triggerField) => { + const targetComponent = context.inputComponents[triggerField]; + if (!targetComponent) { + console.error(`Could not find target component ${triggerField} when hooking up value providers for ${field.label}`); + return; + } + targetComponentLabelToComponent[triggerField] = targetComponent; + }); + + // If one triggerfield changes value, update the new field value. const updateFields = async () => { - const targetComponentValue = await targetComponent.getValue(); - const newFieldValue = await provider.getValue(targetComponentValue?.toString() ?? ''); + let targetComponentLabelToValue: { [label: string]: InputValueType; } = {}; + for (let label in targetComponentLabelToComponent) { + targetComponentLabelToValue[label] = await targetComponentLabelToComponent[label].getValue(); + } + let newFieldValue = await provider.getValue(targetComponentLabelToValue); fieldComponent.setValue(newFieldValue); }; - targetComponent.onValueChanged(() => { - updateFields(); - }); + + // Set the onValueChanged behavior for each component + for (let label in targetComponentLabelToComponent) { + context.onNewDisposableCreated(targetComponentLabelToComponent[label].onValueChanged(() => { + updateFields(); + })); + } await updateFields(); } })); @@ -863,6 +877,18 @@ function processReadonlyTextField(context: FieldContext, allowEvaluation: boolea const text = context.fieldInfo.defaultValue !== undefined ? createLabel(context.view, { text: context.fieldInfo.defaultValue, description: '', required: false, width: context.fieldInfo.inputWidth }) : undefined; + if (text) { + // If we created the text component then add it to our list of inputs so other fields can utilize it + const onChangedEmitter = new vscode.EventEmitter(); // Stub event since we don't currently support updating this when the dependent fields change + context.onNewDisposableCreated(onChangedEmitter); + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { + component: text, + getValue: async (): Promise => typeof text.value === 'string' ? text.value : text.value?.join(EOL), + setValue: (value: InputValueType) => text.value = value?.toString(), + onValueChanged: onChangedEmitter.event, + }); + } + addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo); return { label: label, text: text }; } diff --git a/extensions/resource-deployment/src/ui/validation/validations.ts b/extensions/resource-deployment/src/ui/validation/validations.ts index ccf0481130..a6595891c1 100644 --- a/extensions/resource-deployment/src/ui/validation/validations.ts +++ b/extensions/resource-deployment/src/ui/validation/validations.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { InputValueType } from 'resource-deployment'; import * as vscode from 'vscode'; import { isUndefinedOrEmpty, throwUnless } from '../../common/utils'; -import { InputValueType } from '../modelViewUtils'; export interface ValidationResult { valid: boolean;