Added comments for Example

This commit is contained in:
Srinath Narayanan
2021-01-04 18:15:09 -08:00
parent f770bb193e
commit 97116175ab
12 changed files with 464 additions and 300 deletions

View File

@@ -396,7 +396,7 @@ export interface DataExplorerInputsFrame {
isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[];
selfServeType?: SelfServeTypes
selfServeType?: SelfServeTypes;
}
export interface CollectionCreationDefaults {

View File

@@ -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"

View File

@@ -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>) | string;
@@ -45,23 +45,23 @@ export interface NumberInput extends BaseInput {
min: (() => Promise<number>) | number;
max: (() => Promise<number>) | number;
step: (() => Promise<number>) | number;
defaultValue?: (() => Promise<number>) | number;
inputType: "spin" | "slider";
defaultValue?: number;
inputType: NumberInputType;
}
export interface BooleanInput extends BaseInput {
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
defaultValue?: (() => Promise<boolean>) | boolean;
defaultValue?: boolean;
}
export interface StringInput extends BaseInput {
defaultValue?: (() => Promise<string>) | string;
defaultValue?: string;
}
export interface ChoiceInput extends BaseInput {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
defaultKey?: (() => Promise<string>) | string;
defaultKey?: string;
}
export interface Info {
@@ -83,7 +83,7 @@ export interface Node {
export interface Descriptor {
root: Node;
initialize?: () => Promise<Map<string, InputType>>;
initialize: () => Promise<Map<string, InputType>>;
onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
}
@@ -95,13 +95,15 @@ export interface SmartUiComponentProps {
interface SmartUiComponentState {
currentValues: Map<string, InputType>;
baselineValues: Map<string, InputType>;
errors: Map<string, string>;
customInputIndex: number
customInputIndex: number;
isRefreshing: boolean;
}
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
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<SmartUiComponentProps, Sma
constructor(props: SmartUiComponentProps) {
super(props);
this.state = {
currentValues: undefined,
baselineValues: new Map(),
currentValues: new Map(),
errors: new Map(),
customInputIndex: 0
customInputIndex: 0,
isRefreshing: false
};
this.setDefaultValues();
}
componentDidUpdate = async () : Promise<void> => {
componentDidUpdate = async (): Promise<void> => {
if (!this.customInputs.length) {
return
return;
}
if (!this.shouldRenderCustomComponents) {
this.shouldRenderCustomComponents = true;
@@ -130,43 +134,56 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}
if (this.state.customInputIndex === this.customInputs.length) {
this.shouldRenderCustomComponents = false
this.setState({customInputIndex: 0})
return
this.shouldRenderCustomComponents = false;
this.setState({ customInputIndex: 0 });
return;
}
const input = this.customInputs[this.state.customInputIndex]
const input = this.customInputs[this.state.customInputIndex];
const dataFieldName = input.dataFieldName;
const element = await (input.customElement as Function)(this.state.currentValues)
const element = await (input.customElement as Function)(this.state.currentValues);
const { currentValues } = this.state;
currentValues.set(dataFieldName, element);
this.setState({ currentValues: currentValues, customInputIndex: this.state.customInputIndex + 1});
}
private setDefaultValues = async (): Promise<void> => {
let defaults = new Map<string, InputType>()
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<string, InputType>): Promise<void> => {
private setDefaultValues = async (): Promise<void> => {
this.setState({ isRefreshing: true });
await this.setDefaults(this.props.descriptor.root);
this.setState({ isRefreshing: false });
await this.initialize();
};
private initialize = async (): Promise<void> => {
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<void> => {
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<AnyInput> => {
@@ -180,23 +197,17 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
if (input.customElement) {
if (input.customElement instanceof Function) {
this.customInputs.push(input)
this.customInputs.push(input);
}
return input;
}
switch (input.type) {
case "string":
const stringInput = input as StringInput;
if (stringInput.defaultValue instanceof Function) {
stringInput.defaultValue = await (stringInput.defaultValue as Function)();
}
return stringInput;
case "number":
case "string": {
return input as StringInput;
}
case "number": {
const numberInput = input as NumberInput;
if (numberInput.defaultValue instanceof Function) {
numberInput.defaultValue = await (numberInput.defaultValue as Function)();
}
if (numberInput.min instanceof Function) {
numberInput.min = await (numberInput.min as Function)();
}
@@ -207,11 +218,9 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
numberInput.step = await (numberInput.step as Function)();
}
return numberInput;
case "boolean":
}
case "boolean": {
const booleanInput = input as BooleanInput;
if (booleanInput.defaultValue instanceof Function) {
booleanInput.defaultValue = await (booleanInput.defaultValue as Function)();
}
if (booleanInput.trueLabel instanceof Function) {
booleanInput.trueLabel = await (booleanInput.trueLabel as Function)();
}
@@ -219,29 +228,32 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
booleanInput.falseLabel = await (booleanInput.falseLabel as Function)();
}
return booleanInput;
default:
}
default: {
const enumInput = input as ChoiceInput;
if (enumInput.defaultKey instanceof Function) {
enumInput.defaultKey = await (enumInput.defaultKey as Function)();
}
if (enumInput.choices instanceof Function) {
enumInput.choices = await (enumInput.choices as Function)();
}
return enumInput;
}
}
};
private getDefaultValue = (input: AnyInput): InputType => {
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<SmartUiComponentProps, Sma
};
private renderStringInput(input: StringInput): JSX.Element {
const value = this.state.currentValues.get(input.dataFieldName) as string
const value = this.state.currentValues.get(input.dataFieldName) as string;
return (
<div className="stringInputContainer">
<div>
@@ -342,7 +354,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
};
private renderNumberInput(input: NumberInput): JSX.Element {
const { label, min, max, defaultValue, dataFieldName, step } = input;
const { label, min, max, dataFieldName, step } = input;
const props = {
label: label as string,
min: min as number,
@@ -351,13 +363,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
step: step as number
};
const value = this.state.currentValues.get(dataFieldName) as number
if (input.inputType === "spin") {
const value = this.state.currentValues.get(dataFieldName) as number;
if (input.inputType === "spinner") {
return (
<div>
<SpinButton
{...props}
value={value.toString()}
value={value?.toString()}
onValidate={newValue => 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<SmartUiComponentProps, Sma
} else if (input.inputType === "slider") {
return (
<Slider
// showValue={true}
// valueFormat={}
{...props}
value={value}
onChange={newValue => this.onInputChange(input, newValue)}
@@ -444,7 +454,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
placeholder={placeholder as string}
options={(choices as ChoiceItem[]).map(c => ({
key: c.key,
text: c.value
text: c.label
}))}
styles={{
label: {
@@ -457,11 +467,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
);
}
private renderCustomInput(input: AnyInput) : JSX.Element {
private renderCustomInput(input: AnyInput): JSX.Element {
if (input.customElement instanceof Function) {
const dataFieldName = input.dataFieldName;
const element = this.state.currentValues.get(dataFieldName) as JSX.Element
return element ? element : <></>
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<SmartUiComponentProps, Sma
private renderInput(input: AnyInput): JSX.Element {
if (input.customElement) {
return this.renderCustomInput(input)
return this.renderCustomInput(input);
}
switch (input.type) {
case "string":
@@ -484,12 +494,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}
private renderNode(node: Node): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 10 };
const containerStackTokens: IStackTokens = { childrenGap: 15 };
return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
{node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderInput(node.input)}
<Stack.Item>
{node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderInput(node.input)}
</Stack.Item>
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack>
);
@@ -497,27 +509,28 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 20 };
return this.state.currentValues && this.state.currentValues.size ? (
<Stack tokens={containerStackTokens} styles={{root: {width: 400, padding: 10}}}>
{this.renderNode(this.props.descriptor.root)}
<Stack horizontal tokens={{childrenGap: 10}}>
<PrimaryButton
styles={{ root: { width: 100 } }}
text="submit"
onClick={async () => {
await this.props.descriptor.onSubmit(this.state.currentValues)
this.setDefaultValues()
}}
/>
<PrimaryButton
styles={{ root: { width: 100 } }}
text="discard"
onClick={async () => await this.setDefaultValues()}
/>
return this.state.currentValues && this.state.currentValues.size && !this.state.isRefreshing ? (
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
{this.renderNode(this.props.descriptor.root)}
<Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton
styles={{ root: { width: 100 } }}
text="submit"
onClick={async () => {
await this.props.descriptor.onSubmit(this.state.currentValues);
this.initialize();
}}
/>
<PrimaryButton styles={{ root: { width: 100 } }} text="discard" onClick={() => this.discard()} />
</Stack>
</Stack>
</Stack>
</div>
) : (
<Spinner size={SpinnerSize.large} styles={{root: {textAlign: "center", justifyContent: "center", width: "100%", height: "100%"}}}/>
<Spinner
size={SpinnerSize.large}
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
/>
);
}
}

View File

@@ -163,7 +163,7 @@ export default class Explorer {
public selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter;
private selfServeComponentAdapter: SelfServeComponentAdapter
private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token
public resourceTokenDatabaseId: ko.Observable<string>;
@@ -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="<img src=\"../images/HdeConnectCosmosDB.svg\" >";
connectingImage.innerHTML = '<img src="../images/HdeConnectCosmosDB.svg" >';
}
private _openSetupNotebooksPaneForQuickstart(): void {

View File

@@ -126,8 +126,11 @@ const App: React.FunctionComponent = () => {
return (
<div className="flexContainer">
<div id="divSelfServe" className="flexContainer" data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter">
</div>
<div
id="divSelfServe"
className="flexContainer"
data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter"
></div>
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
{/* Main Command Bar - Start */}
<div data-bind="visible: selfServeType() === 'none', react: commandBarComponentAdapter" />
@@ -305,7 +308,10 @@ const App: React.FunctionComponent = () => {
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
>
<form className="connectExplorerFormContainer">
<div className="connectExplorer" data-bind="visible: selfServeType() === 'none', react: splashScreenAdapter" />
<div
className="connectExplorer"
data-bind="visible: selfServeType() === 'none', react: splashScreenAdapter"
/>
</form>
</div>
<div
@@ -375,14 +381,23 @@ const App: React.FunctionComponent = () => {
{/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer">
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter">
</div>
<p className="connectExplorerContent" id="explorerConnectingImage" data-bind="visible: selfServeType() === 'none'">
</p>
<p className="splashLoaderTitle" id="explorerLoadingStatusTitle" data-bind="visible: selfServeType() === 'none'">
</p>
<p className="splashLoaderText" id="explorerLoadingStatusText" role="alert" data-bind="visible: selfServeType() === 'none'">
</p>
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter"></div>
<p
className="connectExplorerContent"
id="explorerConnectingImage"
data-bind="visible: selfServeType() === 'none'"
></p>
<p
className="splashLoaderTitle"
id="explorerLoadingStatusTitle"
data-bind="visible: selfServeType() === 'none'"
></p>
<p
className="splashLoaderText"
id="explorerLoadingStatusText"
role="alert"
data-bind="visible: selfServeType() === 'none'"
></p>
</div>
</div>
{/* Global loader - End */}

View File

@@ -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<string, InputType>
text: string;
currentValues: Map<string, InputType>;
}
export class TextComponent extends React.Component<TextComponentProps> {
private onHover = (): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5, padding: 15 }}>
<Text>Choice: {this.props.currentValues.get("choiceInput")?.toString()}</Text>
<Text>Boolean: {this.props.currentValues.get("booleanInput")?.toString()}</Text>
<Text>String: {this.props.currentValues.get("stringInput")?.toString()}</Text>
<Text>Slider: {this.props.currentValues.get("numberSliderInput")?.toString()}</Text>
<Text>Spinner: {this.props.currentValues.get("numberSpinnerInput")?.toString()}</Text>
</Stack>
);
};
public render() {
return <Text>{this.props.text}, instanceCount: {this.props.currentValues?.get("instanceCount")}</Text>
}
public render(): JSX.Element {
return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<Text styles={{ root: { fontWeight: 600 } }}>{this.props.text}</Text>
</HoverCard>
);
}
}

View File

@@ -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<string, InputType>) => Promise<void>
- 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<Info>
- role: Display an Info bar as the first element of the UI.
*/
@ClassInfo(selfServeExampleInfo)
/*
@Initialize()
- input: () => Promise<Map<string, InputType>>
- 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<string, InputType> => Promise<JSX.Element>)
- 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<string>
- 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<Info>
- role: Display an Info bar above the UI element for this property.
*/
@PropertyInfo(choiceInfo)
@Label("Instance Name")
/*
@ChoiceInput()
- input: ChoiceItem[] | () => Promise<ChoiceItem[]>
- role: Display a dropdown with choices.
*/
@ChoiceInput(choiceOptions)
static choiceInput: ChoiceItem;
@Label("Boolean")
/*
@BooleanInput()
- input:
trueLabel : string | () => Promise<string>
falseLabel : string | () => Promise<string>
- 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<string>
- 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<string, InputType>, newValue: InputType) => Map<string, InputType>
- 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<number>
max : number | () => Promise<number>
step : number | () => Promise<number>
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;
}

View File

@@ -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<string, InputType>,
newValue: InputType
): Map<string, InputType> => {
currentState.set("instanceCount", newValue);
if ((newValue as number) === 1) {
currentState.set("instanceSize", Sizes.OneCore4Gb);
}
export const onSliderChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
currentState.set("numberSliderInput", newValue);
currentState.set("numberSpinnerInput", newValue);
return currentState;
};
export const onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
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<Map<string, InputType>> => {
let defaults = new Map<string, InputType>()
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<void> => {
const delay = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
export const getPromise = <T extends number | string | boolean | ChoiceItem[] | Info>(value: T): (() => Promise<T>) => {
const f = async (): Promise<T> => {
console.log("delay start");
await delay(100);
console.log("delay end");
return value;
};
return f;
export const initializeSelfServeExample = async (): Promise<Map<string, InputType>> => {
await delay(1000);
const defaults = new Map<string, InputType>();
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<string, InputType>) => Promise<JSX.Element> => {
const f = async (currentValues: Map<string, InputType>): Promise<JSX.Element> => {
return <TextComponent text={text} currentValues={currentValues}/>
export const initializeNumberMaxValue = async (): Promise<number> => {
await delay(2000);
return 5;
};
export const descriptionElement = <Text>This is an example of Self serve class.</Text>;
export const renderText = (text: string): ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) => {
const elementPromiseFunction = async (currentValues: Map<string, InputType>): Promise<JSX.Element> => {
return <TextComponent text={text} currentValues={currentValues} />;
};
return f
}
return elementPromiseFunction;
};

View File

@@ -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<string, InputType>, newValue: InputType) => Map<string, InputType>
): PropertyDecorator => {
return addToMap({name: "onChange", value: onChange});
return addToMap({ name: "onChange", value: onChange });
};
export const CustomElement = (customElement: ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) | JSX.Element): PropertyDecorator => {
return addToMap({name: "customElement", value: customElement});
export const CustomElement = (
customElement: ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) | JSX.Element
): PropertyDecorator => {
return addToMap({ name: "customElement", value: customElement });
};
export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecorator => {
return addToMap({name: "info", value: info});
return addToMap({ name: "info", value: info });
};
export const Placeholder = (placeholder: (() => Promise<string>) | 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>) | string): PropertyDecorator => {
return addToMap({name: "label", value: label});
return addToMap({ name: "label", value: label });
};
export const NumberInput = (min: (() => Promise<number>) | number,
max: (() => Promise<number>) | number,
step: (() => Promise<number>) | number,
numberInputType: string,
defaultNumberValue?: (() => Promise<number>) | number,
): PropertyDecorator => {
export interface NumberInputOptions {
min: (() => Promise<number>) | number;
max: (() => Promise<number>) | number;
step: (() => Promise<number>) | 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>) | string): PropertyDecorator => {
return addToMap({name: "defaultValue", value: defaultStringValue});
};
export interface BooleanInputOptions {
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
}
export const BooleanInput = (trueLabel: (() => Promise<string>) | string,
falseLabel: (() => Promise<string>) | string,
defaultBooleanValue?: (() => Promise<boolean>) | 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[]>) | ChoiceItem[],
defaultKey?: (() => Promise<string>) | string): PropertyDecorator => {
return addToMap(
{name: "choices", value: choices},
{name: "defaultKey", value: defaultKey}
);
export const ChoiceInput = (choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]): PropertyDecorator => {
return addToMap({ name: "choices", value: choices });
};

View File

@@ -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 ?
<SmartUiComponent descriptor={smartUiDescriptor} /> :
public renderComponent(): JSX.Element {
const selfServeType = this.container.selfServeType();
const smartUiDescriptor = this.getDescriptor(selfServeType);
const element = smartUiDescriptor ? (
<SmartUiComponent descriptor={smartUiDescriptor} />
) : (
<h1>Invalid self serve type!</h1>
return element
);
return element;
}
private triggerRender() {

View File

@@ -16,7 +16,7 @@ export class SelfServeLoadingComponentAdapter implements ReactAdapter {
}
public renderComponent(): JSX.Element {
return <Spinner size={SpinnerSize.large} />
return <Spinner size={SpinnerSize.large} />;
}
private triggerRender() {

View File

@@ -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>) | number;
max?: (() => Promise<number>) | number;
step?: (() => Promise<number>) | number;
defaultValue?: any;
trueLabel?: (() => Promise<string>) | string;
falseLabel?: (() => Promise<string>) | string;
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
defaultKey?: (() => Promise<string>) | string;
inputType?: string;
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
@@ -52,10 +50,7 @@ const setValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T
fieldObject[name] = value;
};
const getValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
name: T,
fieldObject: CommonInputTypes
): K => {
const getValue = <T extends keyof CommonInputTypes>(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<String, CommonInputTypes>;
let context = Reflect.getMetadata(metadataKey, target) as Map<string, CommonInputTypes>;
if (!context) {
context = new Map<String, CommonInputTypes>();
context = new Map<string, CommonInputTypes>();
}
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<String, CommonInputTypes>;
const context = Reflect.getMetadata(metadataKey, target) as Map<string, CommonInputTypes>;
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<String, CommonInputTypes>,
context: Map<string, CommonInputTypes>,
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];
};