From 4381ea447cbaedbfae0ee28e8ce28b93ae0c040d Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Wed, 2 Dec 2020 01:45:59 -0800 Subject: [PATCH] removed type requirement --- .../Controls/Settings/SettingsComponent.tsx | 2 +- .../SelfServe/ClassDescriptors.tsx | 12 +- .../SelfServe/PropertyDescriptors.tsx | 26 ++--- .../SelfServe/SelfServeComponent.tsx | 58 ++++------ .../SelfServe/SelfServeUtils.tsx | 45 +++++--- .../SettingsSubComponents/SelfServe/SqlX.tsx | 73 +++++++++--- .../SmartUi/SmartUiComponent.test.tsx | 20 ++-- .../Controls/SmartUi/SmartUiComponent.tsx | 107 +++++++++++++----- 8 files changed, 217 insertions(+), 126 deletions(-) diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index f79cb198f..7356d720e 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -903,7 +903,7 @@ export class SettingsComponent extends React.Component + content: }); if (!hasDatabaseSharedThroughput(this.collection) && this.collection.offer()) { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/ClassDescriptors.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/ClassDescriptors.tsx index 2f1261b59..40cf77cc8 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/ClassDescriptors.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/ClassDescriptors.tsx @@ -1,5 +1,5 @@ -import { Descriptor, Info } from "../../../SmartUi/SmartUiComponent"; -import { addPropertyToMap, DescriptorType, toSmartUiDescriptor } from "./SelfServeUtils"; +import { Descriptor, Info, InputType } from "../../../SmartUi/SmartUiComponent"; +import { addPropertyToMap, toSmartUiDescriptor } from "./SelfServeUtils"; interface SelfServeBaseCLass { toSmartUiDescriptor: () => Descriptor; @@ -19,6 +19,12 @@ export const SmartUi = (): ClassDecorator => { export const ClassInfo = (info: Info): ClassDecorator => { return (target: Function) => { - addPropertyToMap(target, "root", target.name, "info", info, DescriptorType.ClassDescriptor); + addPropertyToMap(target, "root", target.name, "info", info); + }; +}; + +export const OnSubmit = (onSubmit: (currentValues: Map) => Promise): ClassDecorator => { + return (target: Function) => { + addPropertyToMap(target, "root", target.name, "onSubmit", onSubmit); }; }; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/PropertyDescriptors.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/PropertyDescriptors.tsx index d02c1bfe3..ec4d42985 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/PropertyDescriptors.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/PropertyDescriptors.tsx @@ -1,23 +1,25 @@ -import { EnumItem, Info, InputTypeValue } from "../../../SmartUi/SmartUiComponent"; -import { addPropertyToMap, DescriptorType } from "./SelfServeUtils"; +import { EnumItem, Info, InputType } from "../../../SmartUi/SmartUiComponent"; +import { addPropertyToMap } from "./SelfServeUtils"; const addToMap = (descriptorName: string, descriptorValue: any): PropertyDecorator => { return (target, property) => { const className = (target as Function).name; + var propertyType = Reflect.getMetadata("design:type", target, property); + addPropertyToMap(target, property.toString(), className, "type", propertyType.name); + if (!className) { throw new Error("property descriptor applied to non static field!"); } - addPropertyToMap( - target, - property.toString(), - className, - descriptorName, - descriptorValue, - DescriptorType.PropertyDescriptor - ); + addPropertyToMap(target, property.toString(), className, descriptorName, descriptorValue); }; }; +export const OnChange = ( + onChange: (currentState: Map, newValue: InputType) => Map +): PropertyDecorator => { + return addToMap("onChange", onChange); +}; + export const PropertyInfo = (info: Info): PropertyDecorator => { return addToMap("info", info); }; @@ -30,10 +32,6 @@ export const ParentOf = (children: string[]): PropertyDecorator => { return addToMap("parentOf", children); }; -export const Type = (type: InputTypeValue): PropertyDecorator => { - return addToMap("type", type); -}; - export const Label = (label: string): PropertyDecorator => { return addToMap("label", label); }; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeComponent.tsx index fabeaa5a8..89c88c14c 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeComponent.tsx @@ -2,26 +2,13 @@ import React from "react"; import { Descriptor, InputType, SmartUiComponent } from "../../../SmartUi/SmartUiComponent"; import { SqlX } from "./SqlX"; -interface SelfServeComponentProps { - propertyNames: string[]; -} - -export class SelfServeCmponent extends React.Component { - private properties: any = {}; - - constructor(props: SelfServeComponentProps) { - super(props); - let stringer = "{"; - for (var i = 0; i < props.propertyNames.length; i++) { - stringer += `"${props.propertyNames[i]}":null,`; - } - stringer = stringer.substring(0, stringer.length - 1); - console.log(stringer); - stringer += "}"; - this.properties = JSON.parse(stringer); - } +export class SelfServeCmponent extends React.Component { + private onSubmit = async (currentValues: Map): Promise => { + console.log(currentValues.get("instanceCount"), currentValues.get("instanceSize")); + }; private selfServeData: Descriptor = { + onSubmit: this.onSubmit, root: { id: "root", info: { @@ -37,12 +24,19 @@ export class SelfServeCmponent extends React.Component input: { label: "Instance Count", dataFieldName: "instanceCount", - type: "number", + type: "Number", min: 1, max: 5, step: 1, defaultValue: 1, - inputType: "slider" + inputType: "slider", + onChange: (currentState: Map, newValue: InputType): Map => { + currentState.set("instanceCount", newValue); + if ((newValue as number) === 1) { + currentState.set("instanceSize", "1Core4Gb"); + } + return currentState; + } } }, { @@ -50,33 +44,25 @@ export class SelfServeCmponent extends React.Component input: { label: "Instance Size", dataFieldName: "instanceSize", - type: "enum", + type: "Object", choices: [ { label: "1Core4Gb", key: "1Core4Gb", value: "1Core4Gb" }, { label: "2Core8Gb", key: "2Core8Gb", value: "2Core8Gb" }, { label: "4Core16Gb", key: "4Core16Gb", value: "4Core16Gb" } ], - defaultKey: "1Core4Gb" + defaultKey: "1Core4Gb", + onChange: (currentState: Map, newValue: InputType): Map => { + currentState.set("instanceSize", newValue); + return currentState; + } } } ] } }; - private exampleCallbacks = (newValues: Map): void => { - for (var i = 0; i < this.props.propertyNames.length; i++) { - const prop = this.props.propertyNames[i]; - const newVal = newValues.get(prop); - if (newVal) { - this.properties[`${prop}`] = newVal; - } - } - - console.log(this.properties); - }; - public render(): JSX.Element { - //return - return ; + //return + return ; } } diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeUtils.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeUtils.tsx index 726fe404e..b61d6f86d 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SelfServeUtils.tsx @@ -9,7 +9,8 @@ import { NumberInput, StringInput, BooleanInput, - EnumInput + EnumInput, + InputType } from "../../../SmartUi/SmartUiComponent"; export interface CommonInputTypes { @@ -29,11 +30,8 @@ export interface CommonInputTypes { choices?: EnumItem[]; defaultKey?: string; inputType?: string; -} - -export enum DescriptorType { - ClassDescriptor, - PropertyDescriptor + onChange?: (currentState: Map, newValue: InputType) => Map; + onSubmit?: (currentValues: Map) => Promise; } const setValue = ( @@ -53,13 +51,11 @@ const getValue = { - const propertyKey = property.toString(); const descriptorKey = descriptorName.toString() as keyof CommonInputTypes; let context = Reflect.getMetadata(metadataKey, target) as Map; @@ -67,12 +63,16 @@ export const addPropertyToMap = ( context = new Map(); } + if (!(context instanceof Map)) { + throw new Error("@SmartUi should be the first decorator for the class."); + } + let propertyObject = context.get(propertyKey); if (!propertyObject) { propertyObject = { id: propertyKey }; } - if (getValue(descriptorKey, propertyObject)) { + if (getValue(descriptorKey, propertyObject) && descriptorKey !== "type") { throw new Error("duplicate descriptor"); } @@ -125,7 +125,14 @@ export const toSmartUiDescriptor = (metadataKey: string, target: Object): void = const root = context.get("root"); context.delete("root"); + if (!root || !("onSubmit" in root)) { + throw new Error( + "@OnSubmit decorator not declared for the class. Please ensure @SmartUi is the first decorator used for the class." + ); + } + let smartUiDescriptor = { + onSubmit: root.onSubmit, root: { id: "root", info: root.info, @@ -187,25 +194,27 @@ const getChildFromRoot = (key: String, smartUiDescriptor: Descriptor): CommonInp }; const getInput = (value: CommonInputTypes): AnyInput => { + if (!value.label || !value.type || !value.dataFieldName) { + throw new Error("label, onChange, type and dataFieldName are required."); + } + console.log(value.type); switch (value.type) { - case "number": + case "Number": if (!value.step || !value.defaultValue || !value.inputType) { throw new Error("step, defaultValue and inputType are needed for number type"); } return value as NumberInput; - case "string": + case "String": return value as StringInput; - case "boolean": - if (!value.trueLabel || !value.falseLabel || !value.defaultValue) { + case "Boolean": + if (!value.trueLabel || !value.falseLabel || value.defaultValue === undefined) { throw new Error("truelabel, falselabel and defaultValue are needed for boolean type"); } return value as BooleanInput; - case "enum": + default: if (!value.choices || !value.defaultKey) { throw new Error("choices and defaultKey are needed for enum type"); } return value as EnumInput; - default: - throw new Error("Unknown type"); } }; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SqlX.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SqlX.tsx index e865aab40..0244e85e3 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SqlX.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SelfServe/SqlX.tsx @@ -6,27 +6,37 @@ import { Step, DefaultKey, DefaultValue, - Type, NumberInputType, Choices, ParentOf, - PropertyInfo + PropertyInfo, + OnChange, + TrueLabel, + FalseLabel, + Placeholder } from "./PropertyDescriptors"; -import { Descriptor, EnumItem, Info } from "../../../SmartUi/SmartUiComponent"; -import { SmartUi, ClassInfo, SelfServeClass } from "./ClassDescriptors"; +import { Descriptor, EnumItem, Info, InputType } from "../../../SmartUi/SmartUiComponent"; +import { SmartUi, ClassInfo, SelfServeClass, OnSubmit } from "./ClassDescriptors"; + +enum Sizes { + OneCore4Gb = "OneCore4Gb", + TwoCore8Gb = "TwoCore8Gb", + FourCore16Gb = "FourCore16Gb" +} @SmartUi() -@SelfServeClass() @ClassInfo(SqlX.sqlXInfo) +@OnSubmit(SqlX.onSubmit) +@SelfServeClass() export class SqlX { @PropertyInfo(SqlX.instanceSizeInfo) @Label("Instance Size") @DataFieldName("instanceSize") @Choices(SqlX.instanceSizeOptions) - @DefaultKey("1Core4Gb") - @Type("enum") - static instanceSize: any; + @DefaultKey(Sizes.OneCore4Gb) + static instanceSize: EnumItem; + @OnChange(SqlX.onInstanceCountChange) @Label("Instance Count") @DataFieldName("instanceCount") @Min(1) @@ -34,14 +44,25 @@ export class SqlX { @Step(1) @DefaultValue(1) @NumberInputType("slider") - @Type("number") - @ParentOf(["instanceSize"]) - static instanceCount: any; + @ParentOf(["instanceSize", "instanceName", "isAllowed"]) + static instanceCount: number; + + @Label("Feature Allowed") + @DataFieldName("isAllowed") + @DefaultValue(false) + @TrueLabel("allowed") + @FalseLabel("not allowed") + static isAllowed: boolean; + + @Label("Instance Name") + @DataFieldName("instanceName") + @Placeholder("instance name") + static instanceName: string; static instanceSizeOptions: EnumItem[] = [ - { label: "1Core4Gb", key: "1Core4Gb", value: "1Core4Gb" }, - { label: "2Core8Gb", key: "2Core8Gb", value: "2Core8Gb" }, - { label: "4Core16Gb", key: "4Core16Gb", value: "4Core16Gb" } + { 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 } ]; static sqlXInfo: Info = { @@ -52,6 +73,30 @@ export class SqlX { message: "instance size will be updated in the future" }; + static onInstanceCountChange = ( + currentState: Map, + newValue: InputType + ): Map => { + currentState.set("instanceCount", newValue); + if ((newValue as number) === 1) { + currentState.set("isAllowed", false); + } + return currentState; + }; + + static onSubmit = async (currentValues: Map): Promise => { + console.log( + "instanceCount:" + + currentValues.get("instanceCount") + + ", instanceSize:" + + currentValues.get("instanceSize") + + ", instanceName:" + + currentValues.get("instanceName") + + ", isAllowed:" + + currentValues.get("isAllowed") + ); + }; + public static toSmartUiDescriptor = (): Descriptor => { return Reflect.getMetadata(SqlX.name, SqlX) as Descriptor; }; diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx index b455a59fa..7084c7795 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx @@ -4,6 +4,7 @@ import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent"; describe("SmartUiComponent", () => { const exampleData: Descriptor = { + onSubmit: async (currentValues: Map) => {}, root: { id: "root", info: { @@ -24,7 +25,8 @@ describe("SmartUiComponent", () => { max: 500, step: 10, defaultValue: 400, - inputType: "spin" + inputType: "spin", + onChange: undefined } }, { @@ -37,7 +39,8 @@ describe("SmartUiComponent", () => { max: 500, step: 10, defaultValue: 400, - inputType: "slider" + inputType: "slider", + onChange: undefined } }, { @@ -45,7 +48,8 @@ describe("SmartUiComponent", () => { input: { label: "Container id", dataFieldName: "containerId", - type: "string" + type: "string", + onChange: undefined } }, { @@ -56,7 +60,8 @@ describe("SmartUiComponent", () => { falseLabel: "Disabled", defaultValue: true, dataFieldName: "analyticalStore", - type: "boolean" + type: "boolean", + onChange: undefined } }, { @@ -70,6 +75,7 @@ describe("SmartUiComponent", () => { { label: "Database 2", key: "db2", value: "database2" }, { label: "Database 3", key: "db3", value: "database3" } ], + onChange: undefined, defaultKey: "db2" } } @@ -77,12 +83,8 @@ describe("SmartUiComponent", () => { } }; - const exampleCallbacks = (newValues: Map): void => { - console.log("New values:", newValues); - }; - it("should render", () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx index b7340e412..2e1f6e188 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx @@ -8,7 +8,7 @@ import { Text } from "office-ui-fabric-react/lib/Text"; import { InputType } from "../../Tables/Constants"; import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; -import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react"; +import { Link, MessageBar, MessageBarType, PrimaryButton } from "office-ui-fabric-react"; import * as InputUtils from "./InputUtils"; import "./SmartUiComponent.less"; @@ -21,17 +21,18 @@ import "./SmartUiComponent.less"; * - a descriptor of the UX. */ -export type InputTypeValue = "number" | "string" | "boolean" | "enum"; +export type InputTypeValue = "Number" | "String" | "Boolean" | "Object"; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export type EnumItem = { label: string; key: string; value: any }; -export type InputType = number | string | boolean | EnumItem; +export type InputType = Number | String | Boolean | EnumItem; interface BaseInput { label: string; dataFieldName: string; type: InputTypeValue; + onChange?: (currentState: Map, newValue: InputType) => Map; placeholder?: string; } @@ -80,13 +81,13 @@ export interface Node { export interface Descriptor { root: Node; + onSubmit: (currentValues: Map) => Promise; } /************************** Component implementation starts here ************************************* */ export interface SmartUiComponentProps { descriptor: Descriptor; - onChange: (newValues: Map) => void; } interface SmartUiComponentState { @@ -104,11 +105,37 @@ export class SmartUiComponent extends React.Component => { + const defaults = new Map(); + this.setDefaults(this.props.descriptor.root, defaults); + return defaults; + }; + + private setDefaults = (currentNode: Node, defaults: Map) => { + if (currentNode.input?.dataFieldName) { + defaults.set(currentNode.input.dataFieldName, this.getDefault(currentNode.input)); + } + currentNode.children?.map((child: Node) => this.setDefaults(child, defaults)); + }; + + private getDefault = (input: AnyInput): InputType => { + switch (input.type) { + case "String": + return (input as StringInput).defaultValue; + case "Number": + return (input as NumberInput).defaultValue; + case "Boolean": + return (input as BooleanInput).defaultValue; + default: + return (input as EnumInput).defaultKey; + } + }; + private renderInfo(info: Info): JSX.Element { return ( @@ -122,10 +149,16 @@ export class SmartUiComponent extends React.Component { - const { currentValues } = this.state; - currentValues.set(dataFieldName, newValue); - this.setState({ currentValues }, () => this.props.onChange(this.state.currentValues)); + private onInputChange = (input: AnyInput, newValue: InputType) => { + if (input.onChange) { + const newValues = input.onChange(this.state.currentValues, newValue); + this.setState({ currentValues: newValues }); + } else { + const dataFieldName = input.dataFieldName; + const { currentValues } = this.state; + currentValues.set(dataFieldName, newValue); + this.setState({ currentValues }); + } }; private renderStringInput(input: StringInput): JSX.Element { @@ -138,7 +171,7 @@ export class SmartUiComponent extends React.Component this.onInputChange(newValue, input.dataFieldName)} + onChange={(_, newValue) => this.onInputChange(input, newValue)} styles={{ subComponentStyles: { label: { @@ -161,10 +194,11 @@ export class SmartUiComponent extends React.Component { + private onValidate = (input: AnyInput, value: string, min: number, max: number): string => { const newValue = InputUtils.onValidateValueChange(value, min, max); + const dataFieldName = input.dataFieldName; if (newValue) { - this.onInputChange(newValue, dataFieldName); + this.onInputChange(input, newValue); this.clearError(dataFieldName); return newValue.toString(); } else { @@ -175,20 +209,22 @@ export class SmartUiComponent extends React.Component { + private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => { const newValue = InputUtils.onIncrementValue(value, step, max); + const dataFieldName = input.dataFieldName; if (newValue) { - this.onInputChange(newValue, dataFieldName); + this.onInputChange(input, newValue); this.clearError(dataFieldName); return newValue.toString(); } return undefined; }; - private onDecrement = (value: string, step: number, min: number, dataFieldName: string): string => { + private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => { const newValue = InputUtils.onDecrementValue(value, step, min); + const dataFieldName = input.dataFieldName; if (newValue) { - this.onInputChange(newValue, dataFieldName); + this.onInputChange(input, newValue); this.clearError(dataFieldName); return newValue.toString(); } @@ -205,9 +241,9 @@ export class SmartUiComponent extends React.Component this.onValidate(newValue, min, max, dataFieldName)} - onIncrement={newValue => this.onIncrement(newValue, step, max, dataFieldName)} - onDecrement={newValue => this.onDecrement(newValue, step, min, dataFieldName)} + onValidate={newValue => this.onValidate(input, newValue, min, max)} + onIncrement={newValue => this.onIncrement(input, newValue, step, max)} + onDecrement={newValue => this.onDecrement(input, newValue, step, min)} labelPosition={Position.top} styles={{ label: { @@ -228,7 +264,7 @@ export class SmartUiComponent extends React.Component this.onInputChange(newValue, dataFieldName)} + onChange={newValue => this.onInputChange(input, newValue)} styles={{ titleLabel: { ...SmartUiComponent.labelStyle, @@ -257,12 +293,12 @@ export class SmartUiComponent extends React.Component this.onInputChange(false, dataFieldName) + onSelect: () => this.onInputChange(input, false) }, { label: input.trueLabel, key: "true", - onSelect: () => this.onInputChange(true, dataFieldName) + onSelect: () => this.onInputChange(input, true) } ]} selectedKey={ @@ -278,7 +314,7 @@ export class SmartUiComponent extends React.Component this.onInputChange(item.key.toString(), dataFieldName)} + onChange={(_, item: IDropdownOption) => this.onInputChange(input, item.key.toString())} placeholder={placeholder} options={choices.map(c => ({ key: c.key, @@ -306,16 +342,14 @@ export class SmartUiComponent extends React.Component{this.renderNode(this.props.descriptor.root)}; + const containerStackTokens: IStackTokens = { childrenGap: 20 }; + + return ( + + {this.renderNode(this.props.descriptor.root)} + await this.props.descriptor.onSubmit(this.state.currentValues)} + /> + + ); } }