cosmos-explorer/src/SelfServe/SelfServeComponent.tsx

219 lines
6.8 KiB
TypeScript
Raw Normal View History

import React from "react";
import { IStackTokens, PrimaryButton, Spinner, SpinnerSize, Stack } from "office-ui-fabric-react";
import {
ChoiceItem,
InputType,
InputTypeValue,
SmartUiComponent,
UiType,
SmartUiDescriptor,
2021-01-20 15:15:01 +00:00
Info,
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
export interface BaseInput {
label: (() => Promise<string>) | string;
dataFieldName: string;
type: InputTypeValue;
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
placeholder?: (() => Promise<string>) | string;
errorMessage?: string;
}
export interface NumberInput extends BaseInput {
min: (() => Promise<number>) | number;
max: (() => Promise<number>) | number;
step: (() => Promise<number>) | number;
defaultValue?: number;
uiType: UiType;
}
export interface BooleanInput extends BaseInput {
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
defaultValue?: boolean;
}
export interface StringInput extends BaseInput {
defaultValue?: string;
}
export interface ChoiceInput extends BaseInput {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
defaultKey?: string;
}
export interface Node {
id: string;
info?: (() => Promise<Info>) | Info;
input?: AnyInput;
children?: Node[];
}
export interface SelfServeDescriptor {
root: Node;
initialize?: () => Promise<Map<string, InputType>>;
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
inputNames?: string[];
}
export type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
export interface SelfServeComponentProps {
descriptor: SelfServeDescriptor;
}
export interface SelfServeComponentState {
root: SelfServeDescriptor;
currentValues: Map<string, InputType>;
baselineValues: Map<string, InputType>;
isRefreshing: boolean;
}
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
componentDidMount(): void {
this.initializeSmartUiComponent();
}
constructor(props: SelfServeComponentProps) {
super(props);
this.state = {
root: this.props.descriptor,
currentValues: new Map(),
baselineValues: new Map(),
2021-01-20 15:15:01 +00:00
isRefreshing: false,
};
}
private initializeSmartUiComponent = async (): Promise<void> => {
this.setState({ isRefreshing: true });
await this.initializeSmartUiNode(this.props.descriptor.root);
await this.setDefaults();
this.setState({ isRefreshing: false });
};
private setDefaults = 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()) {
if (this.props.descriptor.inputNames.indexOf(key) === -1) {
this.setState({ isRefreshing: false });
throw new Error(`${key} is not an input property of this class.`);
}
currentValues = currentValues.set(key, initialValues.get(key));
baselineValues = baselineValues.set(key, initialValues.get(key));
}
this.setState({ currentValues, baselineValues, isRefreshing: false });
};
public 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 });
};
private initializeSmartUiNode = async (currentNode: Node): Promise<void> => {
currentNode.info = await this.getResolvedValue(currentNode.info);
if (currentNode.input) {
currentNode.input = await this.getResolvedInput(currentNode.input);
}
const promises = currentNode.children?.map(async (child: Node) => await this.initializeSmartUiNode(child));
if (promises) {
await Promise.all(promises);
}
};
private getResolvedInput = async (input: AnyInput): Promise<AnyInput> => {
input.label = await this.getResolvedValue(input.label);
input.placeholder = await this.getResolvedValue(input.placeholder);
switch (input.type) {
case "string": {
return input as StringInput;
}
case "number": {
const numberInput = input as NumberInput;
numberInput.min = await this.getResolvedValue(numberInput.min);
numberInput.max = await this.getResolvedValue(numberInput.max);
numberInput.step = await this.getResolvedValue(numberInput.step);
return numberInput;
}
case "boolean": {
const booleanInput = input as BooleanInput;
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
return booleanInput;
}
default: {
const choiceInput = input as ChoiceInput;
choiceInput.choices = await this.getResolvedValue(choiceInput.choices);
return choiceInput;
}
}
};
public async getResolvedValue<T>(value: T | (() => Promise<T>)): Promise<T> {
if (value instanceof Function) {
return value();
}
return value;
}
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 });
}
};
public render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 20 };
return !this.state.isRefreshing ? (
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
<SmartUiComponent
descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues}
onInputChange={this.onInputChange}
/>
<Stack horizontal tokens={{ childrenGap: 10 }}>
<PrimaryButton
id="submitButton"
styles={{ root: { width: 100 } }}
text="submit"
onClick={async () => {
await this.props.descriptor.onSubmit(this.state.currentValues);
this.setDefaults();
}}
/>
<PrimaryButton
id="discardButton"
styles={{ root: { width: 100 } }}
text="discard"
onClick={() => this.discard()}
/>
</Stack>
</Stack>
</div>
) : (
<Spinner
size={SpinnerSize.large}
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
/>
);
}
}