From 9bbe39e9e2424235013fb948a6ce7e828b8cf63c Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Mon, 1 Nov 2021 11:27:03 -0700 Subject: [PATCH] Add sample value provider (#17548) --- .../resource-deployment/DEVELOPER_GUIDE.md | 108 ++++++++++++++- .../src/ui/modelViewUtils.ts | 2 +- samples/sample-resource-deployment/.gitignore | 1 + samples/sample-resource-deployment/.yarnrc | 1 + .../notebooks/empty-notebook.ipynb | 37 +++++ .../sample-resource-deployment/package.json | 131 +++++++++++++++++- .../package.nls.json | 15 +- .../src/extension.ts | 18 +++ .../src/sampleValueProvider.ts | 34 +++++ .../src/typings/refs.d.ts | 9 ++ .../sample-resource-deployment/tsconfig.json | 15 ++ samples/sample-resource-deployment/yarn.lock | 25 ++++ 12 files changed, 387 insertions(+), 9 deletions(-) create mode 100644 samples/sample-resource-deployment/.gitignore create mode 100644 samples/sample-resource-deployment/.yarnrc create mode 100644 samples/sample-resource-deployment/notebooks/empty-notebook.ipynb create mode 100644 samples/sample-resource-deployment/src/extension.ts create mode 100644 samples/sample-resource-deployment/src/sampleValueProvider.ts create mode 100644 samples/sample-resource-deployment/src/typings/refs.d.ts create mode 100644 samples/sample-resource-deployment/tsconfig.json create mode 100644 samples/sample-resource-deployment/yarn.lock diff --git a/extensions/resource-deployment/DEVELOPER_GUIDE.md b/extensions/resource-deployment/DEVELOPER_GUIDE.md index 8d04c3e390..6db6a48896 100644 --- a/extensions/resource-deployment/DEVELOPER_GUIDE.md +++ b/extensions/resource-deployment/DEVELOPER_GUIDE.md @@ -86,7 +86,7 @@ There are a number of properties on each `ResourceType`. #### Provider Types -There are a number of different types of providers that can be used which affect what happens when the user selects that provider. These are indicated by what fields the provider contains - the provider is checked in order of top to bottom for each property and use the first type that it finds in the properties for that provider. +There are a number of different types of providers that can be used which affect what happens when the user selects that provider. These are indicated by what fields the provider contains - the provider is checked in order of top to bottom for each property and uses the first type that it finds in the properties for that provider. `Notebook Wizard` - A wizard is opened that can be used to prompt the user for values and display information, and then at the very end will open the specified Notebook with those values injected in. Indicated by the presence of the `notebookWizard` property. @@ -104,4 +104,110 @@ There are a number of different types of providers that can be used which affect **TODO** +### NotebookWizard (extends [WizardInfoBase](#wizardinfobase)) + +See [NotebookWizardInfo](https://github.com/microsoft/azuredatastudio/blob/main/extensions/resource-deployment/src/interfaces.ts#L170) for how it's defined in code. + +`notebook` - The path to the Python-based Notebook that is used as a template for the wizard. + +`pages` - An array of [NotebookWizardPageInfo](#notebookwizardpageinfo) containing information for each page in the Notebook Wizard. + +`codeCellInsertionPosition` - **OPTIONAL** The index of the code cell to insert the injected parameters cell. Default is 0. + +### WizardInfoBase + +`type` - **OPTIONAL** This is an internal type only used for BDC deployment wizards. Any other deployment providers can leave it out. + +`doneAction` + +`scriptAction` - **OPTIONAL** + +`title` + +`name` - **OPTIONAL** + +`pages` - An array of the pages for this wizard. Each wizard implementation will usually have its own page type that extends [PageInfoBase](#pageinfobase). + +`isSummaryPageAutoGenerated` - **OPTIONAL** + +### NotebookWizardPageInfo (extends [PageInfoBase](#pageinfobase)) + +`description` - **OPTIONAL** The page description to display at the top of the page. + +### PageInfoBase + +`title` - The title to display for the page + +`isSummaryPage` - **OPTIONAL** Whether this page is set as a summary page that displays a summary of the Note + +`sections` - An array of [SectionInfo] objects containing the details of each section to display on this page. + +### SectionInfo (extends [FieldInfoBase](#fieldinfobase)) + +`title` - **OPTIONAL** The title to display at the top of the section + +`fields` - **OPTIONAL** An array of [FieldInfo](#fieldinfo) objects containing details for each field in this section. + +`rows` - **OPTIONAL** Used for wide dialogs or wizards, the label for each field will be placed to the left of the field component. + +`collapsible` - **OPTIONAL** Whether the section is collapsible or not. Default is `true`. + +`collapsed` - **OPTIONAL** Whether the section starts off collapsed. Default is `false`. + +`spaceBetweenFields` - **OPTIONAL** A string defining how much space should be between each field. Default is `50px`. + +### FieldInfo + +`subFields` + +`type` + +`defaultValue` + +`confirmationRequired` + +`confirmationLabel` + +`min` + +`max` + +`required` + +`options` + +`placeHolder` + +`description` + +`labelCSSStyles` + +`fontWeight` + +`editable` + +`enabled` + +`dynamicOptions` + +`isEvaluated` + +`validations` + +`valueProvider` - **OPTIONAL** If defined then the value for this field is retrieved using the specified [Value Provider](#value-provider). + +### Value Provider + +When a field specifies a value provider then it is saying that the value for that field is dynamic and will be retrieved from a value provider that is registered by an extension separately. This can be used for more complex logic such as running calculations, reading files, making web requests, etc. + +See [sample-value-provider](https://github.com/microsoft/azuredatastudio/blob/main/samples/sample-resource-deployment/package.json) for an example implementation. + +**NOTE** There is currently some behavior that should be known before using this : + +1. The value providers are hooked up after all the components are made, so order doesn't generally matter (you don't have to define the trigger fields before the target field) when the values are on the same page. +2. If the fields are on different pages then currently the hookup logic is non-deterministic and so you may end up with trigger components not existing yet if they are on a different page which hasn't completed its initialization. **So currently having a value provider that has trigger fields on another page is not something officially supported. Contact the dev team if you need this for your scenario** + +`providerId` - The string ID of this provider, this must be registered by an extension using [registerValueProvider](https://github.com/microsoft/azuredatastudio/blob/main/extensions/resource-deployment/src/typings/resource-deployment.d.ts#L47). + +`triggerFields` - The field IDs (`variableName` or `label`) of the fields that - when changed - will trigger `getValue` to be called and the result set as the value of the dependent field. **NOTE** While `variableName` OR `label` is supported it is generally strongly suggested to use a `variableName` (even if you don't need that variable in the final deployment target) due to potential localization mismatches that could happen between the localized strings in the package.json and the ones used by the `valueProvider`. diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 308d8b52f4..6d323d1b24 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -477,7 +477,7 @@ async function hookUpValueProviders(context: WizardPageContext): Promise { 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}`); + console.error(`Could not find target component '${triggerField}' when hooking up value providers for '${field.label}'`); return; } targetComponentLabelToComponent[triggerField] = targetComponent; diff --git a/samples/sample-resource-deployment/.gitignore b/samples/sample-resource-deployment/.gitignore new file mode 100644 index 0000000000..1fcb1529f8 --- /dev/null +++ b/samples/sample-resource-deployment/.gitignore @@ -0,0 +1 @@ +out diff --git a/samples/sample-resource-deployment/.yarnrc b/samples/sample-resource-deployment/.yarnrc new file mode 100644 index 0000000000..f757a6ac58 --- /dev/null +++ b/samples/sample-resource-deployment/.yarnrc @@ -0,0 +1 @@ +--ignore-engines true \ No newline at end of file diff --git a/samples/sample-resource-deployment/notebooks/empty-notebook.ipynb b/samples/sample-resource-deployment/notebooks/empty-notebook.ipynb new file mode 100644 index 0000000000..efe62c3dd4 --- /dev/null +++ b/samples/sample-resource-deployment/notebooks/empty-notebook.ipynb @@ -0,0 +1,37 @@ +{ + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "Python 3", + "language": "python" + }, + "language_info": { + "name": "python", + "version": "3.8.10", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat_minor": 2, + "nbformat": 4, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Empty Notebook\n", + "\n", + "This is an empty Notebook to show what values are inserted into a Notebook by a deployment provider. The cell below is auto-generated by the provider and inserted into the Notebook that is opened for the user, so the Notebook templates can write logic that uses these values as input from the provider." + ], + "metadata": { + "azdata_cell_guid": "f415068b-89aa-437f-80ef-0a453d99df87" + }, + "attachments": {} + } + ] +} \ No newline at end of file diff --git a/samples/sample-resource-deployment/package.json b/samples/sample-resource-deployment/package.json index deb0e7c314..414eb196b1 100644 --- a/samples/sample-resource-deployment/package.json +++ b/samples/sample-resource-deployment/package.json @@ -12,6 +12,12 @@ "vscode": "*", "azdata": ">=1.19.0" }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:azdata.resource.deploy" + ], "repository": { "type": "git", "url": "https://github.com/Microsoft/azuredatastudio.git" @@ -21,6 +27,11 @@ "microsoft.notebook", "microsoft.resource-deployment" ], + "main": "./out/extension", + "scripts": { + "compile": "node node_modules/typescript/bin/tsc -p ./tsconfig.json", + "watch": "node node_modules/typescript/bin/tsc --watch -p ./tsconfig.json" + }, "contributes": { "resourceDeploymentTypes": [ { @@ -120,20 +131,35 @@ ] }, { - "name": "test-wizard", - "displayName": "%resource.type.wizard.display.name%", - "description": "%resource.type.wizard.description%", + "name": "sample-deployment-type", + "displayName": "%resource.type.sample.display.name%", + "description": "%resource.type.sample.description%", "platforms": "*", "icon": { "light": "./images/book.svg", "dark": "./images/book_inverse.svg" }, + "options": [ + { + "name": "sample-type", + "displayName": "%option-sample-type%", + "values": [ + { + "name": "wizard", + "displayName": "%option-wizard%" + }, + { + "name": "value-provider", + "displayName": "%option-value-provider%" + } + ] + } + ], "providers": [ { "name": "test-wizard", "notebookWizard": { "notebook": "./notebooks/deploy-x-data-service-1.ipynb", - "type": "new-arc-control-plane", "runNotebook": false, "codeCellInsertionPosition": 1, "actionText": "%deploy.wizard.action%", @@ -413,7 +439,94 @@ "name": "kubectl" } ], - "when": true + "when": "sample-type=wizard" + }, + { + "name": "sample-value-provider", + "notebookWizard": { + "notebook": "./notebooks/empty-notebook.ipynb", + "codeCellInsertionPosition": 1, + "actionText": "%deploy.wizard.action%", + "title": "%sample.value-provider.title%", + "name": "sample.value-provider", + "labelPosition": "left", + "generateSummaryPage": false, + "pages": [ + { + "title": "%sample.value-provider.single.title%", + "sections": [ + { + "fields": [ + { + "type": "readonly_text", + "label": "%sample.value-provider.single.field.label%", + "labelWidth": "600px", + "isEvaluated": true, + "defaultValue": "", + "valueProvider": { + "providerId": "sample-resource-deployment.sample-value-provider", + "triggerFields": [ + "AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_SINGLE_NAME" + ] + } + }, + { + "label": "%sample.value-provider.trigger-name.label%", + "type": "text", + "variableName": "AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_SINGLE_NAME", + "defaultValue": "", + "required": true + } + ] + } + ] + }, + { + "title": "%sample.value-provider.multiple.title%", + "sections": [ + { + "fields": [ + { + "type": "readonly_text", + "label": "%sample.value-provider.multiple.field.label%", + "labelWidth": "600px", + "defaultValue": "", + "valueProvider": { + "providerId": "sample-resource-deployment.sample-value-provider", + "triggerFields": [ + "AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_NAME", + "AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_PLACE" + ] + } + }, + { + "label": "%sample.value-provider.trigger-name.label%", + "type": "text", + "variableName": "AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_NAME", + "defaultValue": "", + "required": true + }, + { + "type": "options", + "label": "%sample.value-provider.trigger-place.label%", + "required": true, + "variableName": "AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_PLACE", + "editable": true, + "defaultValue": "Seattle", + "options": [ + "Seattle", + "Redmond", + "Bellevue" + ] + } + ] + } + ] + } + ] + }, + "requiredTools": [ ], + "when": "sample-type=value-provider" } ], "agreements": [ @@ -592,5 +705,13 @@ ] } ] + }, + "dependencies": { + }, + "devDependencies": { + "@types/azdata": "^1.33.0", + "@types/vscode": "^1.61.0", + "@types/node": "14.x", + "typescript": "^4.4.0-dev.20210607" } } diff --git a/samples/sample-resource-deployment/package.nls.json b/samples/sample-resource-deployment/package.nls.json index b54de46b1d..8a6f0b7f6a 100644 --- a/samples/sample-resource-deployment/package.nls.json +++ b/samples/sample-resource-deployment/package.nls.json @@ -16,13 +16,24 @@ "agreement": "I accept {0} and {1}.", "agreement-1-name": "Agreement 1", "agreement-2-name": "Agreement 2", + "option-sample-type": "Sample Type", + "option-wizard": "Wizard", + "option-value-provider": "Value Provider", - "resource.type.wizard.display.name": "Test controller", - "resource.type.wizard.description": "Creates a Test controller", + "resource.type.sample.display.name": "Sample Deployment Types", + "resource.type.sample.description": "Provides samples of various deployment types and their options", "validation.wizard.display.name": "Validation Wizard", "validation.wizard.description": "A wizard to test out validations", + "sample.value-provider.title": "Value Provider Sample", + "sample.value-provider.single.title": "Single Trigger Value Provider Sample", + "sample.value-provider.single.field.label": "Field using single trigger field", + "sample.value-provider.trigger-name.label": "Trigger field (name)", + "sample.value-provider.trigger-place.label": "Trigger field (place)", + "sample.value-provider.multiple.title": "Multiple Trigger Value Provider Sample", + "sample.value-provider.multiple.field.label": "Field using multiple trigger fields", + "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", diff --git a/samples/sample-resource-deployment/src/extension.ts b/samples/sample-resource-deployment/src/extension.ts new file mode 100644 index 0000000000..17537c2b45 --- /dev/null +++ b/samples/sample-resource-deployment/src/extension.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rd from 'resource-deployment'; +import * as vscode from 'vscode'; +import { SampleValueProvider } from './sampleValueProvider'; + +export function activate(context: vscode.ExtensionContext) { + // Get the extension API for the resource deployment extension so we can register the value provider. Because the extension itself has a dependency + // on the resource deployment extension we don't need to activate it here - we're guaranteed that it's already activated by the time activate is called here. + const resourceDeploymentApi = vscode.extensions.getExtension(rd.extension.name)!.exports as rd.IExtension; + // Always register value provider disposables so they're deregistered properly if the extension is deactivated + context.subscriptions.push(resourceDeploymentApi.registerValueProvider(new SampleValueProvider())); +} + +export function deactivate() { } diff --git a/samples/sample-resource-deployment/src/sampleValueProvider.ts b/samples/sample-resource-deployment/src/sampleValueProvider.ts new file mode 100644 index 0000000000..f6987801d2 --- /dev/null +++ b/samples/sample-resource-deployment/src/sampleValueProvider.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rd from 'resource-deployment'; + +/** + * This is a sample value provider used to demonstrate how values are received and can be used to generate the return value. + */ +export class SampleValueProvider implements rd.IValueProvider { + /** + * This ID corresponds to the `providerId` property in the `valueProvider` properties of the package.json definitions. + */ + readonly id = 'sample-resource-deployment.sample-value-provider'; + + /** + * + * @param triggerValues This is an object whose keys correspond to the `variableName` (or `label` if no `variableName` is given) property of the trigger fields. + * This will contain an entry for every trigger field defined in the `valueProvider` property on the target field - even if those values are empty/undefined. + * @returns The calculated input type to return. This is expected to match the type of the target field (so string for text, boolean for checkbox, etc) + */ + public async getValue(triggerValues: { [key: string]: rd.InputValueType; }): Promise { + Object.values(triggerValues) + // Because this example is used by two different fields we have logic here to handle determining which one it came from. + // If you are making a generic provider that you don't want to have know about each field that uses it you can use + // Object.values(triggerValues) to get the array of values and operate on those directly. + if (triggerValues['AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_PLACE'] !== undefined) { + return `Hello ${triggerValues['AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_NAME']} from ${triggerValues['AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_MULTIPLE_PLACE']}!`; + } else { + return `Hello ${triggerValues['AZDATA_NB_VAR_SAMPLE_VALUE_PROVIDER_SINGLE_NAME']}!`; + } + } +} diff --git a/samples/sample-resource-deployment/src/typings/refs.d.ts b/samples/sample-resource-deployment/src/typings/refs.d.ts new file mode 100644 index 0000000000..d0f4a22bb8 --- /dev/null +++ b/samples/sample-resource-deployment/src/typings/refs.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is used to get typing support for the resource-deployment extension API. For external extensions you will need to copy +// https://github.com/microsoft/azuredatastudio/blob/main/extensions/resource-deployment/src/typings/resource-deployment.d.ts +// to your extension repo and reference it there. +/// diff --git a/samples/sample-resource-deployment/tsconfig.json b/samples/sample-resource-deployment/tsconfig.json new file mode 100644 index 0000000000..bb0a62b5b4 --- /dev/null +++ b/samples/sample-resource-deployment/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "sourceMap": true, + "rootDir": "src", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/samples/sample-resource-deployment/yarn.lock b/samples/sample-resource-deployment/yarn.lock new file mode 100644 index 0000000000..0ad3bb18e7 --- /dev/null +++ b/samples/sample-resource-deployment/yarn.lock @@ -0,0 +1,25 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/azdata@^1.33.0": + version "1.33.0" + resolved "https://registry.yarnpkg.com/@types/azdata/-/azdata-1.33.0.tgz#0693e725ec676122d17b8d38bc1226549136b545" + integrity sha512-Q3HnDTRtnqOsKOWwEC2QtU/7WVbWdPZyWSrcZOuLpP2HWGQISRnAYb8SmD7lg4Fe6Uk+Unt/4oipJmUX/tDx2A== + dependencies: + "@types/vscode" "*" + +"@types/node@14.x": + version "14.17.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" + integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== + +"@types/vscode@*", "@types/vscode@^1.61.0": + version "1.61.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.61.0.tgz#c54335b6f84c19c69b1435b17cc0ce3b2cecfeec" + integrity sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg== + +typescript@^4.4.0-dev.20210607: + version "4.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" + integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==