Initial implementation of a generic UI component (#61)
* Add generic component * Add validation. Rename to widgetRenderer * Remove test code from splash screen * Clean up infobox * Fix styling/layout * Move test code into unit test * Replace <input> and <labe> by <TextField> and <Text> respectively. Fix style. * Replace InfoBoxComponent with UI fabric MessageBar. Fix styling for TextField * Use MessageBar for error message * Rename WdigetRendererComponent to SmartUiComponent
This commit is contained in:
parent
3f34936acd
commit
27024ef75c
|
@ -444,6 +444,17 @@ export class KeyCodes {
|
||||||
public static Tab: number = 9;
|
public static Tab: number = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalized per: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
|
||||||
|
export class NormalizedEventKey {
|
||||||
|
public static readonly Space = " ";
|
||||||
|
public static readonly Enter = "Enter";
|
||||||
|
public static readonly Escape = "Escape";
|
||||||
|
public static readonly UpArrow = "ArrowUp";
|
||||||
|
public static readonly DownArrow = "ArrowDown";
|
||||||
|
public static readonly LeftArrow = "ArrowLeft";
|
||||||
|
public static readonly RightArrow = "ArrowRight";
|
||||||
|
}
|
||||||
|
|
||||||
export class TryCosmosExperience {
|
export class TryCosmosExperience {
|
||||||
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
public static extendUrl: string = "https://trycosmosdb.azure.com/api/resource/extendportal?userId={0}";
|
||||||
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
public static deleteUrl: string = "https://trycosmosdb.azure.com/api/resource/deleteportal?userId={0}";
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
@import "../../../../less/Common/Constants.less";
|
||||||
|
|
||||||
|
.radioSwitchComponent {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>span:nth-child(n+2) {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
color: @BaseDark;
|
||||||
|
padding-left: @SmallSpace;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* Horizontal switch component
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import "./RadioSwitchComponent.less";
|
||||||
|
import { Icon } from "office-ui-fabric-react/lib/Icon";
|
||||||
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
|
|
||||||
|
export interface Choice {
|
||||||
|
key: string;
|
||||||
|
onSelect: () => void;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadioSwitchComponentProps {
|
||||||
|
choices: Choice[];
|
||||||
|
selectedKey: string;
|
||||||
|
onSelectionKeyChange?: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="radioSwitchComponent">
|
||||||
|
{this.props.choices.map((choice: Choice) => (
|
||||||
|
<span
|
||||||
|
tabIndex={0}
|
||||||
|
key={choice.key}
|
||||||
|
onClick={() => this.onSelect(choice)}
|
||||||
|
onKeyPress={event => this.onKeyPress(event, choice)}
|
||||||
|
>
|
||||||
|
<Icon iconName={this.props.selectedKey === choice.key ? "RadioBtnOn" : "RadioBtnOff"} />
|
||||||
|
<span className="caption">{choice.label}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSelect(choice: Choice): void {
|
||||||
|
this.props.onSelectionKeyChange && this.props.onSelectionKeyChange(choice.key);
|
||||||
|
choice.onSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyPress(event: React.KeyboardEvent<HTMLSpanElement>, choice: Choice): void {
|
||||||
|
if (event.key === NormalizedEventKey.Enter || event.key === NormalizedEventKey.Space) {
|
||||||
|
this.onSelect(choice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* Utilities for validation */
|
||||||
|
|
||||||
|
export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number => {
|
||||||
|
let numericValue = parseInt(newValue);
|
||||||
|
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
||||||
|
if (minValue !== undefined && numericValue < minValue) {
|
||||||
|
numericValue = minValue;
|
||||||
|
}
|
||||||
|
if (maxValue !== undefined && numericValue > maxValue) {
|
||||||
|
numericValue = maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(numericValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onIncrementValue = (newValue: string, step: number, max?: number): number => {
|
||||||
|
const numericValue = parseInt(newValue);
|
||||||
|
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
||||||
|
const newValue = numericValue + step;
|
||||||
|
return max !== undefined ? Math.min(max, newValue) : newValue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onDecrementValue = (newValue: string, step: number, min?: number): number => {
|
||||||
|
const numericValue = parseInt(newValue);
|
||||||
|
if (!isNaN(numericValue) && isFinite(numericValue)) {
|
||||||
|
const newValue = numericValue - step;
|
||||||
|
return min !== undefined ? Math.max(min, newValue) : newValue;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
@import "../../../../less/Common/Constants.less";
|
||||||
|
|
||||||
|
.widgetRendererContainer {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.inputLabelContainer {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.inputLabel {
|
||||||
|
color: #393939;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import React from "react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent";
|
||||||
|
|
||||||
|
describe("SmartUiComponent", () => {
|
||||||
|
const exampleData: Descriptor = {
|
||||||
|
root: {
|
||||||
|
id: "root",
|
||||||
|
info: {
|
||||||
|
message: "Start at $24/mo per database",
|
||||||
|
link: {
|
||||||
|
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||||
|
text: "More Details"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "throughput",
|
||||||
|
input: {
|
||||||
|
label: "Throughput (input)",
|
||||||
|
dataFieldName: "throughput",
|
||||||
|
type: "number",
|
||||||
|
min: 400,
|
||||||
|
max: 500,
|
||||||
|
step: 10,
|
||||||
|
defaultValue: 400,
|
||||||
|
inputType: "spin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "throughput2",
|
||||||
|
input: {
|
||||||
|
label: "Throughput (Slider)",
|
||||||
|
dataFieldName: "throughput2",
|
||||||
|
type: "number",
|
||||||
|
min: 400,
|
||||||
|
max: 500,
|
||||||
|
step: 10,
|
||||||
|
defaultValue: 400,
|
||||||
|
inputType: "slider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "containerId",
|
||||||
|
input: {
|
||||||
|
label: "Container id",
|
||||||
|
dataFieldName: "containerId",
|
||||||
|
type: "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "analyticalStore",
|
||||||
|
input: {
|
||||||
|
label: "Analytical Store",
|
||||||
|
trueLabel: "Enabled",
|
||||||
|
falseLabel: "Disabled",
|
||||||
|
defaultValue: true,
|
||||||
|
dataFieldName: "analyticalStore",
|
||||||
|
type: "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "database",
|
||||||
|
input: {
|
||||||
|
label: "Database",
|
||||||
|
dataFieldName: "database",
|
||||||
|
type: "enum",
|
||||||
|
choices: [
|
||||||
|
{ label: "Database 1", key: "db1", value: "database1" },
|
||||||
|
{ label: "Database 2", key: "db2", value: "database2" },
|
||||||
|
{ label: "Database 3", key: "db3", value: "database3" }
|
||||||
|
],
|
||||||
|
defaultKey: "db2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exampleCallbacks = (newValues: Map<string, InputType>): void => {
|
||||||
|
console.log("New values:", newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should render", () => {
|
||||||
|
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} onChange={exampleCallbacks} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,335 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Position } from "office-ui-fabric-react/lib/utilities/positioning";
|
||||||
|
import { Slider } from "office-ui-fabric-react/lib/Slider";
|
||||||
|
import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
|
||||||
|
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
|
import { TextField } from "office-ui-fabric-react/lib/TextField";
|
||||||
|
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 * as InputUtils from "./InputUtils";
|
||||||
|
import "./SmartUiComponent.less";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic UX renderer
|
||||||
|
* It takes:
|
||||||
|
* - a JSON object as data
|
||||||
|
* - a Map of callbacks
|
||||||
|
* - a descriptor of the UX.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type InputTypeValue = "number" | "string" | "boolean" | "enum";
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
|
||||||
|
interface BaseInput {
|
||||||
|
label: string;
|
||||||
|
dataFieldName: string;
|
||||||
|
type: InputTypeValue;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For now, this only supports integers
|
||||||
|
*/
|
||||||
|
export interface NumberInput extends BaseInput {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step: number;
|
||||||
|
defaultValue: number;
|
||||||
|
inputType: "spin" | "slider";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BooleanInput extends BaseInput {
|
||||||
|
trueLabel: string;
|
||||||
|
falseLabel: string;
|
||||||
|
defaultValue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringInput extends BaseInput {
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnumInput extends BaseInput {
|
||||||
|
choices: EnumItem[];
|
||||||
|
defaultKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Info {
|
||||||
|
message: string;
|
||||||
|
link?: {
|
||||||
|
href: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyInput = NumberInput | BooleanInput | StringInput | EnumInput;
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: string;
|
||||||
|
info?: Info;
|
||||||
|
input?: AnyInput;
|
||||||
|
children?: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Descriptor {
|
||||||
|
root: Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************** Component implementation starts here ************************************* */
|
||||||
|
|
||||||
|
export interface SmartUiComponentProps {
|
||||||
|
descriptor: Descriptor;
|
||||||
|
onChange: (newValues: Map<string, InputType>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmartUiComponentState {
|
||||||
|
currentValues: Map<string, InputType>;
|
||||||
|
errors: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
||||||
|
private static readonly labelStyle = {
|
||||||
|
color: "#393939",
|
||||||
|
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
fontSize: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: SmartUiComponentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
currentValues: new Map(),
|
||||||
|
errors: new Map()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderInfo(info: Info): JSX.Element {
|
||||||
|
return (
|
||||||
|
<MessageBar>
|
||||||
|
{info.message}
|
||||||
|
<Link href={info.link.href} target="_blank">
|
||||||
|
{info.link.text}
|
||||||
|
</Link>
|
||||||
|
</MessageBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onInputChange = (newValue: string | number | boolean, dataFieldName: string) => {
|
||||||
|
const { currentValues } = this.state;
|
||||||
|
currentValues.set(dataFieldName, newValue);
|
||||||
|
this.setState({ currentValues }, () => this.props.onChange(this.state.currentValues));
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderStringInput(input: StringInput): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="stringInputContainer">
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
id={`${input.dataFieldName}-input`}
|
||||||
|
label={input.label}
|
||||||
|
type="text"
|
||||||
|
value={input.defaultValue}
|
||||||
|
placeholder={input.placeholder}
|
||||||
|
onChange={(_, newValue) => this.onInputChange(newValue, input.dataFieldName)}
|
||||||
|
styles={{
|
||||||
|
subComponentStyles: {
|
||||||
|
label: {
|
||||||
|
root: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearError(dataFieldName: string): void {
|
||||||
|
const { errors } = this.state;
|
||||||
|
errors.delete(dataFieldName);
|
||||||
|
this.setState({ errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
private onValidate = (value: string, min: number, max: number, dataFieldName: string): string => {
|
||||||
|
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
||||||
|
if (newValue) {
|
||||||
|
this.onInputChange(newValue, dataFieldName);
|
||||||
|
this.clearError(dataFieldName);
|
||||||
|
return newValue.toString();
|
||||||
|
} else {
|
||||||
|
const { errors } = this.state;
|
||||||
|
errors.set(dataFieldName, `Invalid value ${value}: must be between ${min} and ${max}`);
|
||||||
|
this.setState({ errors });
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onIncrement = (value: string, step: number, max: number, dataFieldName: string): string => {
|
||||||
|
const newValue = InputUtils.onIncrementValue(value, step, max);
|
||||||
|
if (newValue) {
|
||||||
|
this.onInputChange(newValue, dataFieldName);
|
||||||
|
this.clearError(dataFieldName);
|
||||||
|
return newValue.toString();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDecrement = (value: string, step: number, min: number, dataFieldName: string): string => {
|
||||||
|
const newValue = InputUtils.onDecrementValue(value, step, min);
|
||||||
|
if (newValue) {
|
||||||
|
this.onInputChange(newValue, dataFieldName);
|
||||||
|
this.clearError(dataFieldName);
|
||||||
|
return newValue.toString();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderNumberInput(input: NumberInput): JSX.Element {
|
||||||
|
const { label, min, max, defaultValue, dataFieldName, step } = input;
|
||||||
|
const props = { label, min, max, ariaLabel: label, step };
|
||||||
|
|
||||||
|
if (input.inputType === "spin") {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SpinButton
|
||||||
|
{...props}
|
||||||
|
defaultValue={defaultValue.toString()}
|
||||||
|
onValidate={newValue => this.onValidate(newValue, min, max, dataFieldName)}
|
||||||
|
onIncrement={newValue => this.onIncrement(newValue, step, max, dataFieldName)}
|
||||||
|
onDecrement={newValue => this.onDecrement(newValue, step, min, dataFieldName)}
|
||||||
|
labelPosition={Position.top}
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{this.state.errors.has(dataFieldName) && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (input.inputType === "slider") {
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
// showValue={true}
|
||||||
|
// valueFormat={}
|
||||||
|
{...props}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={newValue => this.onInputChange(newValue, dataFieldName)}
|
||||||
|
styles={{
|
||||||
|
titleLabel: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
valueLabel: SmartUiComponent.labelStyle
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <>Unsupported number input type {input.inputType}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
||||||
|
const { dataFieldName } = input;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="inputLabelContainer">
|
||||||
|
<Text variant="small" nowrap className="inputLabel">
|
||||||
|
{input.label}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<RadioSwitchComponent
|
||||||
|
choices={[
|
||||||
|
{
|
||||||
|
label: input.falseLabel,
|
||||||
|
key: "false",
|
||||||
|
onSelect: () => this.onInputChange(false, dataFieldName)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: input.trueLabel,
|
||||||
|
key: "true",
|
||||||
|
onSelect: () => this.onInputChange(true, dataFieldName)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedKey={
|
||||||
|
(this.state.currentValues.has(dataFieldName)
|
||||||
|
? (this.state.currentValues.get(dataFieldName) as boolean)
|
||||||
|
: input.defaultValue)
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEnumInput(input: EnumInput): JSX.Element {
|
||||||
|
const { label, defaultKey, dataFieldName, choices, placeholder } = input;
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
label={label}
|
||||||
|
selectedKey={
|
||||||
|
this.state.currentValues.has(dataFieldName)
|
||||||
|
? (this.state.currentValues.get(dataFieldName) as string)
|
||||||
|
: defaultKey
|
||||||
|
}
|
||||||
|
onChange={(_, item: IDropdownOption) => this.onInputChange(item.key.toString(), dataFieldName)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
options={choices.map(c => ({
|
||||||
|
key: c.key,
|
||||||
|
text: c.value
|
||||||
|
}))}
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
...SmartUiComponent.labelStyle,
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
dropdown: SmartUiComponent.labelStyle
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderInput(input: AnyInput): JSX.Element {
|
||||||
|
switch (input.type) {
|
||||||
|
case "string":
|
||||||
|
return this.renderStringInput(input as StringInput);
|
||||||
|
case "number":
|
||||||
|
return this.renderNumberInput(input as NumberInput);
|
||||||
|
case "boolean":
|
||||||
|
return this.renderBooleanInput(input as BooleanInput);
|
||||||
|
case "enum":
|
||||||
|
return this.renderEnumInput(input as EnumInput);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown input type: ${input.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNode(node: Node): JSX.Element {
|
||||||
|
const containerStackTokens: IStackTokens = { childrenGap: 10 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
||||||
|
{node.info && this.renderInfo(node.info)}
|
||||||
|
{node.input && this.renderInput(node.input)}
|
||||||
|
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return <>{this.renderNode(this.props.descriptor.root)}</>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`SmartUiComponent should render 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledMessageBarBase>
|
||||||
|
Start at $24/mo per database
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
More Details
|
||||||
|
</StyledLinkBase>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
<div
|
||||||
|
key="throughput"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<CustomizedSpinButton
|
||||||
|
ariaLabel="Throughput (input)"
|
||||||
|
decrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronDownSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultValue="400"
|
||||||
|
disabled={false}
|
||||||
|
incrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronUpSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label="Throughput (input)"
|
||||||
|
labelPosition={0}
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onDecrement={[Function]}
|
||||||
|
onIncrement={[Function]}
|
||||||
|
onValidate={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput2"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledSliderBase
|
||||||
|
ariaLabel="Throughput (Slider)"
|
||||||
|
defaultValue={400}
|
||||||
|
label="Throughput (Slider)"
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onChange={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"titleLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
"valueLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="containerId"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="stringInputContainer"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
id="containerId-input"
|
||||||
|
label="Container id"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"subComponentStyles": Object {
|
||||||
|
"label": Object {
|
||||||
|
"root": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="analyticalStore"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="inputLabelContainer"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="inputLabel"
|
||||||
|
nowrap={true}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Analytical Store
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<RadioSwitchComponent
|
||||||
|
choices={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "false",
|
||||||
|
"label": "Disabled",
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "true",
|
||||||
|
"label": "Enabled",
|
||||||
|
"onSelect": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="database"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
label="Database"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "db1",
|
||||||
|
"text": "database1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db2",
|
||||||
|
"text": "database2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db3",
|
||||||
|
"text": "database3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey="db2"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"dropdown": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"label": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
Loading…
Reference in New Issue