From 97116175ab95bcef22873e78fee559265e97cfb0 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Mon, 4 Jan 2021 18:15:09 -0800 Subject: [PATCH] Added comments for Example --- src/Contracts/ViewModels.ts | 2 +- .../SmartUi/SmartUiComponent.test.tsx | 17 +- .../Controls/SmartUi/SmartUiComponent.tsx | 209 ++++++++++-------- src/Explorer/Explorer.ts | 22 +- src/Main.tsx | 37 +++- src/SelfServe/Example/CustomComponent.tsx | 27 ++- src/SelfServe/Example/Example.tsx | 194 +++++++++++++--- src/SelfServe/Example/ExampleApis.tsx | 101 ++++----- src/SelfServe/PropertyDescriptors.tsx | 75 ++++--- src/SelfServe/SelfServeComponentAdapter.tsx | 29 +-- .../SelfServeLoadingComponentAdapter.tsx | 2 +- src/SelfServe/SelfServeUtils.tsx | 49 ++-- 12 files changed, 464 insertions(+), 300 deletions(-) diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 3450c12aa..8c85a3a5a 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -396,7 +396,7 @@ export interface DataExplorerInputsFrame { isAuthWithresourceToken?: boolean; defaultCollectionThroughput?: CollectionCreationDefaults; flights?: readonly string[]; - selfServeType?: SelfServeTypes + selfServeType?: SelfServeTypes; } export interface CollectionCreationDefaults { diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx index 1ae2b0896..b9bace861 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx @@ -1,10 +1,15 @@ import React from "react"; import { shallow } from "enzyme"; -import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent"; +import { SmartUiComponent, Descriptor } from "./SmartUiComponent"; describe("SmartUiComponent", () => { const exampleData: Descriptor = { - onSubmit: async () => {}, + onSubmit: async () => { + return; + }, + initialize: async () => { + return undefined; + }, root: { id: "root", info: { @@ -25,7 +30,7 @@ describe("SmartUiComponent", () => { max: 500, step: 10, defaultValue: 400, - inputType: "spin", + inputType: "spinner", onChange: undefined } }, @@ -71,9 +76,9 @@ describe("SmartUiComponent", () => { dataFieldName: "database", type: "object", choices: [ - { label: "Database 1", key: "db1", value: "database1" }, - { label: "Database 2", key: "db2", value: "database2" }, - { label: "Database 3", key: "db3", value: "database3" } + { label: "Database 1", key: "db1" }, + { label: "Database 2", key: "db2" }, + { label: "Database 3", key: "db3" } ], onChange: undefined, defaultKey: "db2" diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx index 9cc8ba7e9..60934c4e2 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx @@ -9,10 +9,8 @@ import { InputType } from "../../Tables/Constants"; import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { Link, MessageBar, MessageBarType, PrimaryButton, Spinner, SpinnerSize } from "office-ui-fabric-react"; - import * as InputUtils from "./InputUtils"; import "./SmartUiComponent.less"; -import { Widget } from "@phosphor/widgets"; /** * Generic UX renderer @@ -24,10 +22,12 @@ import { Widget } from "@phosphor/widgets"; export type InputTypeValue = "number" | "string" | "boolean" | "object"; -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -export type ChoiceItem = { label: string; key: string; value: any }; +export type NumberInputType = "spinner" | "slider"; -export type InputType = Number | String | Boolean | ChoiceItem | JSX.Element; +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +export type ChoiceItem = { label: string; key: string }; + +export type InputType = number | string | boolean | ChoiceItem | JSX.Element; export interface BaseInput { label: (() => Promise) | string; @@ -45,23 +45,23 @@ export interface NumberInput extends BaseInput { min: (() => Promise) | number; max: (() => Promise) | number; step: (() => Promise) | number; - defaultValue?: (() => Promise) | number; - inputType: "spin" | "slider"; + defaultValue?: number; + inputType: NumberInputType; } export interface BooleanInput extends BaseInput { trueLabel: (() => Promise) | string; falseLabel: (() => Promise) | string; - defaultValue?: (() => Promise) | boolean; + defaultValue?: boolean; } export interface StringInput extends BaseInput { - defaultValue?: (() => Promise) | string; + defaultValue?: string; } export interface ChoiceInput extends BaseInput { choices: (() => Promise) | ChoiceItem[]; - defaultKey?: (() => Promise) | string; + defaultKey?: string; } export interface Info { @@ -83,7 +83,7 @@ export interface Node { export interface Descriptor { root: Node; - initialize?: () => Promise>; + initialize: () => Promise>; onSubmit: (currentValues: Map) => Promise; } @@ -95,13 +95,15 @@ export interface SmartUiComponentProps { interface SmartUiComponentState { currentValues: Map; + baselineValues: Map; errors: Map; - customInputIndex: number + customInputIndex: number; + isRefreshing: boolean; } export class SmartUiComponent extends React.Component { - private customInputs : AnyInput[] = [] - private shouldRenderCustomComponents = true + private customInputs: AnyInput[] = []; + private shouldRenderCustomComponents = true; private static readonly labelStyle = { color: "#393939", @@ -112,17 +114,19 @@ export class SmartUiComponent extends React.Component => { + componentDidUpdate = async (): Promise => { if (!this.customInputs.length) { - return + return; } if (!this.shouldRenderCustomComponents) { this.shouldRenderCustomComponents = true; @@ -130,43 +134,56 @@ export class SmartUiComponent extends React.Component => { - let defaults = new Map() - - if (this.props.descriptor.initialize) { - defaults = await this.props.descriptor.initialize() - } - - await this.setDefaults(this.props.descriptor.root, defaults); - this.setState({ currentValues: defaults }); + this.setState({ currentValues: currentValues, customInputIndex: this.state.customInputIndex + 1 }); }; - private setDefaults = async (currentNode: Node, defaults: Map): Promise => { + private setDefaultValues = async (): Promise => { + this.setState({ isRefreshing: true }); + await this.setDefaults(this.props.descriptor.root); + this.setState({ isRefreshing: false }); + await this.initialize(); + }; + + private initialize = async (): Promise => { + this.setState({ isRefreshing: true }); + let { currentValues, baselineValues } = this.state; + const initialValues = await this.props.descriptor.initialize(); + for (const key of initialValues.keys()) { + currentValues = currentValues.set(key, initialValues.get(key)); + baselineValues = baselineValues.set(key, initialValues.get(key)); + } + this.setState({ currentValues: currentValues, baselineValues: baselineValues, isRefreshing: false }); + }; + + private discard = (): void => { + let { currentValues } = this.state; + const { baselineValues } = this.state; + for (const key of baselineValues.keys()) { + currentValues = currentValues.set(key, baselineValues.get(key)); + } + this.setState({ currentValues: currentValues }); + }; + + private setDefaults = async (currentNode: Node): Promise => { if (currentNode.info && currentNode.info instanceof Function) { currentNode.info = await (currentNode.info as Function)(); } if (currentNode.input) { currentNode.input = await this.getModifiedInput(currentNode.input); - if (!defaults.get(currentNode.input.dataFieldName)) { - defaults.set(currentNode.input.dataFieldName, this.getDefaultValue(currentNode.input)); - } } - await Promise.all(currentNode.children?.map(async (child: Node) => await this.setDefaults(child, defaults))); + await Promise.all(currentNode.children?.map(async (child: Node) => await this.setDefaults(child))); }; private getModifiedInput = async (input: AnyInput): Promise => { @@ -180,23 +197,17 @@ export class SmartUiComponent extends React.Component { switch (input.type) { - case "string": - const stringInput = input as StringInput + case "string": { + const stringInput = input as StringInput; return stringInput.defaultValue ? (stringInput.defaultValue as string) : ""; - case "number": + } + case "number": { return (input as NumberInput).defaultValue as number; - case "boolean": + } + case "boolean": { return (input as BooleanInput).defaultValue as boolean; - default: + } + default: { return (input as ChoiceInput).defaultKey as string; + } } }; @@ -271,7 +283,7 @@ export class SmartUiComponent extends React.Component
@@ -342,7 +354,7 @@ export class SmartUiComponent extends React.Component this.onValidate(input, newValue, props.min, props.max)} onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)} onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)} @@ -377,8 +389,6 @@ export class SmartUiComponent extends React.Component this.onInputChange(input, newValue)} @@ -444,7 +454,7 @@ export class SmartUiComponent extends React.Component ({ key: c.key, - text: c.value + text: c.label }))} styles={{ label: { @@ -457,11 +467,11 @@ export class SmartUiComponent extends React.Component + const element = this.state.currentValues.get(dataFieldName) as JSX.Element; + return element ? element : <>; } else { return input.customElement as JSX.Element; } @@ -469,7 +479,7 @@ export class SmartUiComponent extends React.Component - {node.info && this.renderInfo(node.info as Info)} - {node.input && this.renderInput(node.input)} + + {node.info && this.renderInfo(node.info as Info)} + {node.input && this.renderInput(node.input)} + {node.children && node.children.map(child =>
{this.renderNode(child)}
)} ); @@ -497,27 +509,28 @@ export class SmartUiComponent extends React.Component - {this.renderNode(this.props.descriptor.root)} - - { - await this.props.descriptor.onSubmit(this.state.currentValues) - this.setDefaultValues() - }} - /> - await this.setDefaultValues()} - /> + return this.state.currentValues && this.state.currentValues.size && !this.state.isRefreshing ? ( +
+ + {this.renderNode(this.props.descriptor.root)} + + { + await this.props.descriptor.onSubmit(this.state.currentValues); + this.initialize(); + }} + /> + this.discard()} /> + - +
) : ( - + ); } } diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index a3e113ee2..e0832bfab 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -163,7 +163,7 @@ export default class Explorer { public selectedNode: ko.Observable; public isRefreshingExplorer: ko.Observable; private resourceTree: ResourceTreeAdapter; - private selfServeComponentAdapter: SelfServeComponentAdapter + private selfServeComponentAdapter: SelfServeComponentAdapter; // Resource Token public resourceTokenDatabaseId: ko.Observable; @@ -260,7 +260,7 @@ export default class Explorer { // React adapters private commandBarComponentAdapter: CommandBarComponentAdapter; - private selfServeLoadingComponentAdapter : SelfServeLoadingComponentAdapter; + private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter; private splashScreenAdapter: SplashScreenComponentAdapter; private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; private dialogComponentAdapter: DialogComponentAdapter; @@ -1862,16 +1862,16 @@ export default class Explorer { } public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void { - const selfServeTypeForTest = inputs.features[Constants.Features.selfServeTypeForTest] + const selfServeTypeForTest = inputs.features[Constants.Features.selfServeTypeForTest]; if (selfServeTypeForTest) { - const selfServeType = SelfServeTypes[selfServeTypeForTest?.toLowerCase() as keyof typeof SelfServeTypes] - this.selfServeType(selfServeType ? selfServeType : SelfServeTypes.invalid) + const selfServeType = SelfServeTypes[selfServeTypeForTest?.toLowerCase() as keyof typeof SelfServeTypes]; + this.selfServeType(selfServeType ? selfServeType : SelfServeTypes.invalid); } else if (inputs.selfServeType) { - this.selfServeType(inputs.selfServeType) + this.selfServeType(inputs.selfServeType); } else { - this.selfServeType(SelfServeTypes.none) - this._setLoadingStatusText("Connecting...", "Welcome to Azure Cosmos DB") - this._setConnectingImage() + this.selfServeType(SelfServeTypes.none); + this._setLoadingStatusText("Connecting...", "Welcome to Azure Cosmos DB"); + this._setConnectingImage(); } } @@ -1894,7 +1894,7 @@ export default class Explorer { this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); this.setFeatureFlagsFromFlights(inputs.flights); - this.setSelfServeType(inputs) + this.setSelfServeType(inputs); if (!!inputs.dataExplorerVersion) { this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion); @@ -3011,7 +3011,7 @@ export default class Explorer { private _setConnectingImage() { const connectingImage = document.getElementById("explorerConnectingImage"); - connectingImage.innerHTML=""; + connectingImage.innerHTML = ''; } private _openSetupNotebooksPaneForQuickstart(): void { diff --git a/src/Main.tsx b/src/Main.tsx index 87094a0e3..ee7ec64ba 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -126,8 +126,11 @@ const App: React.FunctionComponent = () => { return (
-
-
+
{/* Main Command Bar - Start */}
@@ -305,7 +308,10 @@ const App: React.FunctionComponent = () => { data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0" >
-
+
{ {/* Global loader - Start */}
-
-
-

-

-

-

- +
+

+

+
{/* Global loader - End */} diff --git a/src/SelfServe/Example/CustomComponent.tsx b/src/SelfServe/Example/CustomComponent.tsx index 32a0f0f79..ebc11a530 100644 --- a/src/SelfServe/Example/CustomComponent.tsx +++ b/src/SelfServe/Example/CustomComponent.tsx @@ -1,15 +1,30 @@ import React from "react"; -import { Text } from "office-ui-fabric-react"; +import { HoverCard, HoverCardType, Stack, Text } from "office-ui-fabric-react"; import { InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent"; interface TextComponentProps { - text: string; - currentValues: Map + text: string; + currentValues: Map; } export class TextComponent extends React.Component { + private onHover = (): JSX.Element => { + return ( + + Choice: {this.props.currentValues.get("choiceInput")?.toString()} + Boolean: {this.props.currentValues.get("booleanInput")?.toString()} + String: {this.props.currentValues.get("stringInput")?.toString()} + Slider: {this.props.currentValues.get("numberSliderInput")?.toString()} + Spinner: {this.props.currentValues.get("numberSpinnerInput")?.toString()} + + ); + }; - public render() { - return {this.props.text}, instanceCount: {this.props.currentValues?.get("instanceCount")} - } + public render(): JSX.Element { + return ( + + {this.props.text} + + ); + } } diff --git a/src/SelfServe/Example/Example.tsx b/src/SelfServe/Example/Example.tsx index 27db27778..2e11647c5 100644 --- a/src/SelfServe/Example/Example.tsx +++ b/src/SelfServe/Example/Example.tsx @@ -5,59 +5,185 @@ import { OnChange, Placeholder, CustomElement, - DefaultStringValue, ChoiceInput, BooleanInput, NumberInput } from "../PropertyDescriptors"; import { SmartUi, ClassInfo, OnSubmit, Initialize } from "../ClassDescriptors"; import { - getPromise, initializeSelfServeExample, - instanceSizeInfo, - instanceSizeOptions, - onInstanceCountChange, + choiceInfo, + choiceOptions, + onSliderChange, onSubmit, renderText, - Sizes, - selfServeExampleInfo + selfServeExampleInfo, + descriptionElement, + initializeNumberMaxValue } from "./ExampleApis"; import { SelfServeBase } from "../SelfServeUtils"; import { ChoiceItem } from "../../Explorer/Controls/SmartUi/SmartUiComponent"; -@SmartUi() -@ClassInfo(getPromise(selfServeExampleInfo)) -@Initialize(initializeSelfServeExample) -@OnSubmit(onSubmit) -export class SelfServeExample extends SelfServeBase { +/* +This is an example self serve class that auto generates UI components for your feature. - @Label(getPromise("Description")) - @CustomElement(renderText("This is the description.")) +Each self serve class + - Needs to extends the SelfServeBase class. + - Needs to have the @SmartUi() descriptor to tell the compiler that UI needs to be generated from this class. + - Needs to have an @OnSubmit() descriptor, a callback for when the submit button is clicked. + - Needs to have an @Initialize() descriptor, to set default values for the inputs. + +You can test this self serve UI by using the featureflag '?feature.selfServeTypeForTest=example' +and plumb in similar feature flags for your own self serve class. + +The default values and functions used for this class can be found in ExampleApis.tsx +*/ + +/* +@SmartUi() + - role: Generated the JSON required to convert this class into the required UI. This is done during compile time. +*/ +@SmartUi() +/* +@OnSubmit() + - input: (currentValues: Map) => Promise + - role: Callback that is triggerred when the submit button is clicked. You should perform your rest API + calls here using the data from the different inputs passed as a Map to this callback function. + + In this example, the onSubmit callback simply sets the value for keys corresponding to the field name + in the SessionStorage. +*/ +@OnSubmit(onSubmit) +/* +@ClassInfo() + - input: Info | () => Promise + - role: Display an Info bar as the first element of the UI. +*/ +@ClassInfo(selfServeExampleInfo) +/* +@Initialize() + - input: () => Promise> + - role: Set default values for the properties of this class. + + The static properties of this class (namely choiceInput, booleanInput, stringInput, numberSliderInput, numberSpinnerInput) + will each correspond to an UI element. Their values can be of 'InputType'. Their defaults can be set by setting + values in a Map corresponding to the field's name. + + Typically, you can make rest calls in the async function passed to @Initialize() to fetch the initial values for + these fields. This is called after the onSubmit callback, to reinitialize the defaults. + + In this example, the initializeSelfServeExample function simply reads the SessionStorage to fetch the default values + for these fields. These are then set when the changes are submitted. +*/ +@Initialize(initializeSelfServeExample) +export class SelfServeExample extends SelfServeBase { + /* + @CustomElement() + - input: JSX.Element | (currentValues: Map => Promise) + - role: Display a custom element by either passing the element itself, or by passing a function that takes the current values + and renders a Component / JSX.Element. + + In this example, we first use a static JSX.Element to show a description text. We also declare a CustomComponent, that + takes a Map of propertyName -> value, as input. It uses this to display a Hoverable Card which shows a snapshot of + the current values. + */ + @CustomElement(descriptionElement) static description: string; - @Label(getPromise("Instance Size")) - @PropertyInfo(getPromise(instanceSizeInfo)) - //@ChoiceInput(getPromise(instanceSizeOptions), getPromise(Sizes.OneCore4Gb)) - @ChoiceInput(getPromise(instanceSizeOptions)) - static instanceSize: ChoiceItem; + /* + @ParentOf() + - input: string[] + - role: Determines which UI elements are the children of which UI element. An array containing the names of the child properties + is passsed. You need to make sure these children are declared in this Class as proeprties. + */ + @ParentOf(["choiceInput", "booleanInput", "stringInput", "numberSliderInput", "numberSpinnerInput"]) + @CustomElement(renderText("Hover to see current values...")) + static currentValues: string; - @Label(getPromise("About")) - @CustomElement(renderText("This is the about .")) - static about: string; + /* + @Label() + - input: string | () => Promise + - role: Adds a label for the UI element. This is ignored for a custom element but is required for all other properties. + */ + @Label("Choice") - @Label("Feature Allowed") - //@BooleanInput("allowed", "not allowed", false) - @BooleanInput("allowed", "not allowed") - static isAllowed: boolean; + /* + @PropertyInfo() + - input: Info | () => Promise + - role: Display an Info bar above the UI element for this property. + */ + @PropertyInfo(choiceInfo) - @Label("Instance Name") + /* + @ChoiceInput() + - input: ChoiceItem[] | () => Promise + - role: Display a dropdown with choices. + */ + @ChoiceInput(choiceOptions) + static choiceInput: ChoiceItem; + + @Label("Boolean") + /* + @BooleanInput() + - input: + trueLabel : string | () => Promise + falseLabel : string | () => Promise + - role: Add a boolean input eith radio buttons for true and false values. + */ + @BooleanInput({ + trueLabel: "allowed", + falseLabel: "not allowed" + }) + static booleanInput: boolean; + + @Label("String") + /* + @PlaceHolder() + - input: string | () => Promise + - role: Adds a placeholder for the string input + */ @Placeholder("instance name") - static instanceName: string; + static stringInput: string; - @Label(getPromise("Instance Count")) - @OnChange(onInstanceCountChange) - @ParentOf(["instanceSize", "about", "instanceName", "isAllowed", ]) - //@NumberInput(getPromise(1), getPromise(5), getPromise(1), "slider", getPromise(0)) - @NumberInput(getPromise(1), getPromise(5), getPromise(1), "slider") - static instanceCount: number; + @Label("Slider") + + /* + @OnChange() + - input: (currentValues: Map, newValue: InputType) => Map + - role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property + changes its value in the UI. This can be used to change other input values based on some other input. + + The new Map of propertyName -> value is returned. + + In this example, the onSliderChange function sets the spinner input to the same value as the slider input + when the slider in moved in the UI. + */ + @OnChange(onSliderChange) + + /* + @NumberInput() + - input: + min : number | () => Promise + max : number | () => Promise + step : number | () => Promise + numberInputType : NumberInputType + - role: Display a numeric input as slider or a spinner. The Min, Max and step to increase by need to be provided as well. + In this example, the Max value is fetched via an async function. This is resolved every time the UI is reloaded. + */ + @NumberInput({ + min: 1, + max: initializeNumberMaxValue, + step: 1, + numberInputType: "slider" + }) + static numberSliderInput: number; + + @Label("Spinner") + @NumberInput({ + min: 1, + max: initializeNumberMaxValue, + step: 1, + numberInputType: "spinner" + }) + static numberSpinnerInput: number; } diff --git a/src/SelfServe/Example/ExampleApis.tsx b/src/SelfServe/Example/ExampleApis.tsx index 5d2f135cf..c292a3ce9 100644 --- a/src/SelfServe/Example/ExampleApis.tsx +++ b/src/SelfServe/Example/ExampleApis.tsx @@ -1,83 +1,70 @@ import React from "react"; import { ChoiceItem, Info, InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent"; import { TextComponent } from "./CustomComponent"; -import {SessionStorageUtility} from "../../Shared/StorageUtility" +import { SessionStorageUtility } from "../../Shared/StorageUtility"; +import { Text } from "office-ui-fabric-react"; -export enum Sizes { - OneCore4Gb = "OneCore4Gb", - TwoCore8Gb = "TwoCore8Gb", - FourCore16Gb = "FourCore16Gb" +export enum Choices { + Choice1 = "Choice1", + Choice2 = "Choice2", + Choice3 = "Choice3" } -export const instanceSizeOptions: ChoiceItem[] = [ - { label: Sizes.OneCore4Gb, key: Sizes.OneCore4Gb, value: Sizes.OneCore4Gb }, - { label: Sizes.TwoCore8Gb, key: Sizes.TwoCore8Gb, value: Sizes.TwoCore8Gb }, - { label: Sizes.FourCore16Gb, key: Sizes.FourCore16Gb, value: Sizes.FourCore16Gb } +export const choiceOptions: ChoiceItem[] = [ + { label: "Choice 1", key: Choices.Choice1 }, + { label: "Choice 2", key: Choices.Choice2 }, + { label: "Choice 3", key: Choices.Choice3 } ]; export const selfServeExampleInfo: Info = { message: "This is a self serve class" }; -export const instanceSizeInfo: Info = { - message: "instance size will be updated in the future" +export const choiceInfo: Info = { + message: "More choices can be added in the future." }; -export const onInstanceCountChange = ( - currentState: Map, - newValue: InputType -): Map => { - currentState.set("instanceCount", newValue); - if ((newValue as number) === 1) { - currentState.set("instanceSize", Sizes.OneCore4Gb); - } +export const onSliderChange = (currentState: Map, newValue: InputType): Map => { + currentState.set("numberSliderInput", newValue); + currentState.set("numberSpinnerInput", newValue); return currentState; }; export const onSubmit = async (currentValues: Map): Promise => { - console.log( - "instanceCount:" + - currentValues.get("instanceCount") + - ", instanceSize:" + - currentValues.get("instanceSize") + - ", instanceName:" + - currentValues.get("instanceName") + - ", isAllowed:" + - currentValues.get("isAllowed") - ); - - SessionStorageUtility.setEntry("instanceCount", currentValues.get("instanceCount")?.toString()) - SessionStorageUtility.setEntry("instanceSize", currentValues.get("instanceSize")?.toString()) - SessionStorageUtility.setEntry("instanceName", currentValues.get("instanceName")?.toString()) - SessionStorageUtility.setEntry("isAllowed", currentValues.get("isAllowed")?.toString()) + SessionStorageUtility.setEntry("choiceInput", currentValues.get("choiceInput")?.toString()); + SessionStorageUtility.setEntry("booleanInput", currentValues.get("booleanInput")?.toString()); + SessionStorageUtility.setEntry("stringInput", currentValues.get("stringInput")?.toString()); + SessionStorageUtility.setEntry("numberSliderInput", currentValues.get("numberSliderInput")?.toString()); + SessionStorageUtility.setEntry("numberSpinnerInput", currentValues.get("numberSpinnerInput")?.toString()); }; -export const initializeSelfServeExample = async () : Promise> => { - let defaults = new Map() - defaults.set("instanceCount", parseInt(SessionStorageUtility.getEntry("instanceCount"))) - defaults.set("instanceSize", SessionStorageUtility.getEntry("instanceSize")) - defaults.set("instanceName", SessionStorageUtility.getEntry("instanceName")) - defaults.set("isAllowed", SessionStorageUtility.getEntry("isAllowed") === "true") - return defaults -}; - -export const delay = (ms: number): Promise => { +const delay = (ms: number): Promise => { return new Promise(resolve => setTimeout(resolve, ms)); }; -export const getPromise = (value: T): (() => Promise) => { - const f = async (): Promise => { - console.log("delay start"); - await delay(100); - console.log("delay end"); - return value; - }; - return f; +export const initializeSelfServeExample = async (): Promise> => { + await delay(1000); + const defaults = new Map(); + defaults.set("choiceInput", SessionStorageUtility.getEntry("choiceInput")); + defaults.set("booleanInput", SessionStorageUtility.getEntry("booleanInput") === "true"); + defaults.set("stringInput", SessionStorageUtility.getEntry("stringInput")); + const numberSliderInput = parseInt(SessionStorageUtility.getEntry("numberSliderInput")); + defaults.set("numberSliderInput", !isNaN(numberSliderInput) ? numberSliderInput : 1); + const numberSpinnerInput = parseInt(SessionStorageUtility.getEntry("numberSpinnerInput")); + defaults.set("numberSpinnerInput", !isNaN(numberSpinnerInput) ? numberSpinnerInput : 1); + return defaults; }; -export const renderText = (text: string) : (currentValues: Map) => Promise => { - const f = async (currentValues: Map): Promise => { - return +export const initializeNumberMaxValue = async (): Promise => { + await delay(2000); + return 5; +}; + +export const descriptionElement = This is an example of Self serve class.; + +export const renderText = (text: string): ((currentValues: Map) => Promise) => { + const elementPromiseFunction = async (currentValues: Map): Promise => { + return ; }; - return f -} + return elementPromiseFunction; +}; diff --git a/src/SelfServe/PropertyDescriptors.tsx b/src/SelfServe/PropertyDescriptors.tsx index 42e8061bd..fe28c6335 100644 --- a/src/SelfServe/PropertyDescriptors.tsx +++ b/src/SelfServe/PropertyDescriptors.tsx @@ -1,15 +1,15 @@ -import { ChoiceItem, Descriptor, Info, InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent"; +import { ChoiceItem, Info, InputType, NumberInputType } from "../Explorer/Controls/SmartUi/SmartUiComponent"; import { addPropertyToMap } from "./SelfServeUtils"; interface Decorator { - name: string, - value: any + name: string; + value: unknown; } const addToMap = (...decorators: Decorator[]): PropertyDecorator => { return (target, property) => { const className = (target as Function).name; - var propertyType = (Reflect.getMetadata("design:type", target, property).name as string).toLowerCase(); + const propertyType = (Reflect.getMetadata("design:type", target, property).name as string).toLowerCase(); addPropertyToMap(target, property.toString(), className, "type", propertyType); addPropertyToMap(target, property.toString(), className, "dataFieldName", property.toString()); @@ -17,69 +17,68 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => { if (!className) { throw new Error("property descriptor applied to non static field!"); } - decorators.map((decorator: Decorator) => addPropertyToMap(target, property.toString(), className, decorator.name, decorator.value)); + decorators.map((decorator: Decorator) => + addPropertyToMap(target, property.toString(), className, decorator.name, decorator.value) + ); }; }; export const OnChange = ( onChange: (currentState: Map, newValue: InputType) => Map ): PropertyDecorator => { - return addToMap({name: "onChange", value: onChange}); + return addToMap({ name: "onChange", value: onChange }); }; -export const CustomElement = (customElement: ((currentValues: Map) => Promise) | JSX.Element): PropertyDecorator => { - return addToMap({name: "customElement", value: customElement}); +export const CustomElement = ( + customElement: ((currentValues: Map) => Promise) | JSX.Element +): PropertyDecorator => { + return addToMap({ name: "customElement", value: customElement }); }; export const PropertyInfo = (info: (() => Promise) | Info): PropertyDecorator => { - return addToMap({name: "info", value: info}); + return addToMap({ name: "info", value: info }); }; export const Placeholder = (placeholder: (() => Promise) | string): PropertyDecorator => { - return addToMap({name: "placeholder", value: placeholder}); + return addToMap({ name: "placeholder", value: placeholder }); }; export const ParentOf = (children: string[]): PropertyDecorator => { - return addToMap({name: "parentOf", value: children}); + return addToMap({ name: "parentOf", value: children }); }; export const Label = (label: (() => Promise) | string): PropertyDecorator => { - return addToMap({name: "label", value: label}); + return addToMap({ name: "label", value: label }); }; -export const NumberInput = (min: (() => Promise) | number, -max: (() => Promise) | number, -step: (() => Promise) | number, -numberInputType: string, -defaultNumberValue?: (() => Promise) | number, -): PropertyDecorator => { +export interface NumberInputOptions { + min: (() => Promise) | number; + max: (() => Promise) | number; + step: (() => Promise) | number; + numberInputType: NumberInputType; +} + +export const NumberInput = (numberInputOptions: NumberInputOptions): PropertyDecorator => { return addToMap( - {name: "min", value: min}, - {name: "max", value: max}, - {name: "step", value: step}, - {name: "defaultValue", value: defaultNumberValue}, - {name: "inputType", value: numberInputType} + { name: "min", value: numberInputOptions.min }, + { name: "max", value: numberInputOptions.max }, + { name: "step", value: numberInputOptions.step }, + { name: "inputType", value: numberInputOptions.numberInputType } ); }; -export const DefaultStringValue = (defaultStringValue: (() => Promise) | string): PropertyDecorator => { - return addToMap({name: "defaultValue", value: defaultStringValue}); -}; +export interface BooleanInputOptions { + trueLabel: (() => Promise) | string; + falseLabel: (() => Promise) | string; +} -export const BooleanInput = (trueLabel: (() => Promise) | string, -falseLabel: (() => Promise) | string, -defaultBooleanValue?: (() => Promise) | boolean): PropertyDecorator => { +export const BooleanInput = (booleanInputOptions: BooleanInputOptions): PropertyDecorator => { return addToMap( - {name: "defaultValue", value: defaultBooleanValue}, - {name: "trueLabel", value: trueLabel}, - {name: "falseLabel", value: falseLabel} + { name: "trueLabel", value: booleanInputOptions.trueLabel }, + { name: "falseLabel", value: booleanInputOptions.falseLabel } ); }; -export const ChoiceInput = (choices: (() => Promise) | ChoiceItem[], -defaultKey?: (() => Promise) | string): PropertyDecorator => { - return addToMap( - {name: "choices", value: choices}, - {name: "defaultKey", value: defaultKey} - ); +export const ChoiceInput = (choices: (() => Promise) | ChoiceItem[]): PropertyDecorator => { + return addToMap({ name: "choices", value: choices }); }; diff --git a/src/SelfServe/SelfServeComponentAdapter.tsx b/src/SelfServe/SelfServeComponentAdapter.tsx index db74bc707..a1298a8cc 100644 --- a/src/SelfServe/SelfServeComponentAdapter.tsx +++ b/src/SelfServe/SelfServeComponentAdapter.tsx @@ -18,28 +18,31 @@ export class SelfServeComponentAdapter implements ReactAdapter { constructor(container: Explorer) { this.container = container; this.parameters = ko.observable(Date.now()); - this.container.selfServeType.subscribe(() => {this.triggerRender()}) + this.container.selfServeType.subscribe(() => { + this.triggerRender(); + }); } - private getDescriptor = (selfServeType : SelfServeTypes) : Descriptor => { + private getDescriptor = (selfServeType: SelfServeTypes): Descriptor => { switch (selfServeType) { case SelfServeTypes.example: - return SelfServeExample.toSmartUiDescriptor() + return SelfServeExample.toSmartUiDescriptor(); default: return undefined; } - } - - public renderComponent(): JSX.Element { - const selfServeType = this.container.selfServeType() - const smartUiDescriptor = this.getDescriptor(selfServeType) + }; - - const element = smartUiDescriptor ? - : + public renderComponent(): JSX.Element { + const selfServeType = this.container.selfServeType(); + const smartUiDescriptor = this.getDescriptor(selfServeType); + + const element = smartUiDescriptor ? ( + + ) : (

Invalid self serve type!

- - return element + ); + + return element; } private triggerRender() { diff --git a/src/SelfServe/SelfServeLoadingComponentAdapter.tsx b/src/SelfServe/SelfServeLoadingComponentAdapter.tsx index c18b176fd..320b9b517 100644 --- a/src/SelfServe/SelfServeLoadingComponentAdapter.tsx +++ b/src/SelfServe/SelfServeLoadingComponentAdapter.tsx @@ -16,7 +16,7 @@ export class SelfServeLoadingComponentAdapter implements ReactAdapter { } public renderComponent(): JSX.Element { - return + return ; } private triggerRender() { diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index 2ad79f91e..7f48871ed 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -13,7 +13,7 @@ import { InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent"; -const SelfServeType = "selfServeType" +const SelfServeType = "selfServeType"; export class SelfServeBase { public static toSmartUiDescriptor(): Descriptor { @@ -32,11 +32,9 @@ export interface CommonInputTypes { min?: (() => Promise) | number; max?: (() => Promise) | number; step?: (() => Promise) | number; - defaultValue?: any; trueLabel?: (() => Promise) | string; falseLabel?: (() => Promise) | string; choices?: (() => Promise) | ChoiceItem[]; - defaultKey?: (() => Promise) | string; inputType?: string; onChange?: (currentState: Map, newValue: InputType) => Map; onSubmit?: (currentValues: Map) => Promise; @@ -52,10 +50,7 @@ const setValue = ( - name: T, - fieldObject: CommonInputTypes -): K => { +const getValue = (name: T, fieldObject: CommonInputTypes): unknown => { return fieldObject[name]; }; @@ -67,10 +62,10 @@ export const addPropertyToMap = ( descriptorValue: any ): void => { const descriptorKey = descriptorName.toString() as keyof CommonInputTypes; - let context = Reflect.getMetadata(metadataKey, target) as Map; + let context = Reflect.getMetadata(metadataKey, target) as Map; if (!context) { - context = new Map(); + context = new Map(); } if (!(context instanceof Map)) { @@ -93,7 +88,7 @@ export const addPropertyToMap = ( }; export const toSmartUiDescriptor = (metadataKey: string, target: Object): void => { - const context = Reflect.getMetadata(metadataKey, target) as Map; + const context = Reflect.getMetadata(metadataKey, target) as Map; Reflect.defineMetadata(metadataKey, context, target); const root = context.get("root"); @@ -105,7 +100,13 @@ export const toSmartUiDescriptor = (metadataKey: string, target: Object): void = ); } - let smartUiDescriptor = { + if (!root?.initialize) { + throw new Error( + "@Initialize decorator not declared for the class. Please ensure @SmartUi is the first decorator used for the class." + ); + } + + const smartUiDescriptor = { onSubmit: root.onSubmit, initialize: root.initialize, root: { @@ -124,12 +125,12 @@ export const toSmartUiDescriptor = (metadataKey: string, target: Object): void = }; const addToDescriptor = ( - context: Map, + context: Map, smartUiDescriptor: Descriptor, root: Node, - key: String + key: string ): void => { - let value = context.get(key); + const value = context.get(key); if (!value) { // should already be added to root const childNode = getChildFromRoot(key, smartUiDescriptor); @@ -149,13 +150,13 @@ const addToDescriptor = ( children: [] } as Node; context.delete(key); - for (let childKey in childrenKeys) { + for (const childKey in childrenKeys) { addToDescriptor(context, smartUiDescriptor, element, childrenKeys[childKey]); } root.children.push(element); }; -const getChildFromRoot = (key: String, smartUiDescriptor: Descriptor): Node => { +const getChildFromRoot = (key: string, smartUiDescriptor: Descriptor): Node => { let i = 0; const children = smartUiDescriptor.root.children; while (i < children.length) { @@ -171,8 +172,8 @@ const getChildFromRoot = (key: String, smartUiDescriptor: Descriptor): Node => { }; const getInput = (value: CommonInputTypes): AnyInput => { - if (!value.label || !value.type || !value.dataFieldName) { - throw new Error("label, onChange, type and dataFieldName are required."); + if (!value.label && !value.customElement) { + throw new Error("label is required."); } switch (value.type) { @@ -197,13 +198,13 @@ const getInput = (value: CommonInputTypes): AnyInput => { }; export enum SelfServeTypes { - none="none", - invalid="invalid", - example="example" + none = "none", + invalid = "invalid", + example = "example" } export const getSelfServeType = (search: string): SelfServeTypes => { const params = new URLSearchParams(search); - const selfServeTypeParam = params.get(SelfServeType)?.toLowerCase() - return SelfServeTypes[selfServeTypeParam as keyof typeof SelfServeTypes] -} + const selfServeTypeParam = params.get(SelfServeType)?.toLowerCase(); + return SelfServeTypes[selfServeTypeParam as keyof typeof SelfServeTypes]; +};