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; isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
selfServeType?: SelfServeTypes selfServeType?: SelfServeTypes;
} }
export interface CollectionCreationDefaults { export interface CollectionCreationDefaults {

View File

@@ -1,10 +1,15 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SmartUiComponent, Descriptor, InputType } from "./SmartUiComponent"; import { SmartUiComponent, Descriptor } from "./SmartUiComponent";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: Descriptor = { const exampleData: Descriptor = {
onSubmit: async () => {}, onSubmit: async () => {
return;
},
initialize: async () => {
return undefined;
},
root: { root: {
id: "root", id: "root",
info: { info: {
@@ -25,7 +30,7 @@ describe("SmartUiComponent", () => {
max: 500, max: 500,
step: 10, step: 10,
defaultValue: 400, defaultValue: 400,
inputType: "spin", inputType: "spinner",
onChange: undefined onChange: undefined
} }
}, },
@@ -71,9 +76,9 @@ describe("SmartUiComponent", () => {
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
{ label: "Database 1", key: "db1", value: "database1" }, { label: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2", value: "database2" }, { label: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3", value: "database3" } { label: "Database 3", key: "db3" }
], ],
onChange: undefined, onChange: undefined,
defaultKey: "db2" defaultKey: "db2"

View File

@@ -9,10 +9,8 @@ import { InputType } from "../../Tables/Constants";
import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent"; import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Link, MessageBar, MessageBarType, PrimaryButton, Spinner, SpinnerSize } from "office-ui-fabric-react"; import { Link, MessageBar, MessageBarType, PrimaryButton, Spinner, SpinnerSize } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils"; import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less"; import "./SmartUiComponent.less";
import { Widget } from "@phosphor/widgets";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -24,10 +22,12 @@ import { Widget } from "@phosphor/widgets";
export type InputTypeValue = "number" | "string" | "boolean" | "object"; export type InputTypeValue = "number" | "string" | "boolean" | "object";
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ export type NumberInputType = "spinner" | "slider";
export type ChoiceItem = { label: string; key: string; value: any };
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 { export interface BaseInput {
label: (() => Promise<string>) | string; label: (() => Promise<string>) | string;
@@ -45,23 +45,23 @@ export interface NumberInput extends BaseInput {
min: (() => Promise<number>) | number; min: (() => Promise<number>) | number;
max: (() => Promise<number>) | number; max: (() => Promise<number>) | number;
step: (() => Promise<number>) | number; step: (() => Promise<number>) | number;
defaultValue?: (() => Promise<number>) | number; defaultValue?: number;
inputType: "spin" | "slider"; inputType: NumberInputType;
} }
export interface BooleanInput extends BaseInput { export interface BooleanInput extends BaseInput {
trueLabel: (() => Promise<string>) | string; trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string; falseLabel: (() => Promise<string>) | string;
defaultValue?: (() => Promise<boolean>) | boolean; defaultValue?: boolean;
} }
export interface StringInput extends BaseInput { export interface StringInput extends BaseInput {
defaultValue?: (() => Promise<string>) | string; defaultValue?: string;
} }
export interface ChoiceInput extends BaseInput { export interface ChoiceInput extends BaseInput {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]; choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
defaultKey?: (() => Promise<string>) | string; defaultKey?: string;
} }
export interface Info { export interface Info {
@@ -83,7 +83,7 @@ export interface Node {
export interface Descriptor { export interface Descriptor {
root: Node; root: Node;
initialize?: () => Promise<Map<string, InputType>>; initialize: () => Promise<Map<string, InputType>>;
onSubmit: (currentValues: Map<string, InputType>) => Promise<void>; onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
} }
@@ -95,13 +95,15 @@ export interface SmartUiComponentProps {
interface SmartUiComponentState { interface SmartUiComponentState {
currentValues: Map<string, InputType>; currentValues: Map<string, InputType>;
baselineValues: Map<string, InputType>;
errors: Map<string, string>; errors: Map<string, string>;
customInputIndex: number customInputIndex: number;
isRefreshing: boolean;
} }
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> { export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
private customInputs : AnyInput[] = [] private customInputs: AnyInput[] = [];
private shouldRenderCustomComponents = true private shouldRenderCustomComponents = true;
private static readonly labelStyle = { private static readonly labelStyle = {
color: "#393939", color: "#393939",
@@ -112,17 +114,19 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
constructor(props: SmartUiComponentProps) { constructor(props: SmartUiComponentProps) {
super(props); super(props);
this.state = { this.state = {
currentValues: undefined, baselineValues: new Map(),
currentValues: new Map(),
errors: new Map(), errors: new Map(),
customInputIndex: 0 customInputIndex: 0,
isRefreshing: false
}; };
this.setDefaultValues(); this.setDefaultValues();
} }
componentDidUpdate = async () : Promise<void> => { componentDidUpdate = async (): Promise<void> => {
if (!this.customInputs.length) { if (!this.customInputs.length) {
return return;
} }
if (!this.shouldRenderCustomComponents) { if (!this.shouldRenderCustomComponents) {
this.shouldRenderCustomComponents = true; this.shouldRenderCustomComponents = true;
@@ -130,43 +134,56 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
if (this.state.customInputIndex === this.customInputs.length) { if (this.state.customInputIndex === this.customInputs.length) {
this.shouldRenderCustomComponents = false this.shouldRenderCustomComponents = false;
this.setState({customInputIndex: 0}) this.setState({ customInputIndex: 0 });
return return;
} }
const input = this.customInputs[this.state.customInputIndex] const input = this.customInputs[this.state.customInputIndex];
const dataFieldName = input.dataFieldName; 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; const { currentValues } = this.state;
currentValues.set(dataFieldName, element); currentValues.set(dataFieldName, element);
this.setState({ currentValues: currentValues, customInputIndex: this.state.customInputIndex + 1}); 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 });
}; };
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) { if (currentNode.info && currentNode.info instanceof Function) {
currentNode.info = await (currentNode.info as Function)(); currentNode.info = await (currentNode.info as Function)();
} }
if (currentNode.input) { if (currentNode.input) {
currentNode.input = await this.getModifiedInput(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> => { 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) {
if (input.customElement instanceof Function) { if (input.customElement instanceof Function) {
this.customInputs.push(input) this.customInputs.push(input);
} }
return input; return input;
} }
switch (input.type) { switch (input.type) {
case "string": case "string": {
const stringInput = input as StringInput; return input as StringInput;
if (stringInput.defaultValue instanceof Function) { }
stringInput.defaultValue = await (stringInput.defaultValue as Function)(); case "number": {
}
return stringInput;
case "number":
const numberInput = input as NumberInput; const numberInput = input as NumberInput;
if (numberInput.defaultValue instanceof Function) {
numberInput.defaultValue = await (numberInput.defaultValue as Function)();
}
if (numberInput.min instanceof Function) { if (numberInput.min instanceof Function) {
numberInput.min = await (numberInput.min as 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)(); numberInput.step = await (numberInput.step as Function)();
} }
return numberInput; return numberInput;
case "boolean": }
case "boolean": {
const booleanInput = input as BooleanInput; const booleanInput = input as BooleanInput;
if (booleanInput.defaultValue instanceof Function) {
booleanInput.defaultValue = await (booleanInput.defaultValue as Function)();
}
if (booleanInput.trueLabel instanceof Function) { if (booleanInput.trueLabel instanceof Function) {
booleanInput.trueLabel = await (booleanInput.trueLabel as 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)(); booleanInput.falseLabel = await (booleanInput.falseLabel as Function)();
} }
return booleanInput; return booleanInput;
default: }
default: {
const enumInput = input as ChoiceInput; const enumInput = input as ChoiceInput;
if (enumInput.defaultKey instanceof Function) {
enumInput.defaultKey = await (enumInput.defaultKey as Function)();
}
if (enumInput.choices instanceof Function) { if (enumInput.choices instanceof Function) {
enumInput.choices = await (enumInput.choices as Function)(); enumInput.choices = await (enumInput.choices as Function)();
} }
return enumInput; return enumInput;
}
} }
}; };
private getDefaultValue = (input: AnyInput): InputType => { private getDefaultValue = (input: AnyInput): InputType => {
switch (input.type) { switch (input.type) {
case "string": case "string": {
const stringInput = input as StringInput const stringInput = input as StringInput;
return stringInput.defaultValue ? (stringInput.defaultValue as string) : ""; return stringInput.defaultValue ? (stringInput.defaultValue as string) : "";
case "number": }
case "number": {
return (input as NumberInput).defaultValue as number; return (input as NumberInput).defaultValue as number;
case "boolean": }
case "boolean": {
return (input as BooleanInput).defaultValue as boolean; return (input as BooleanInput).defaultValue as boolean;
default: }
default: {
return (input as ChoiceInput).defaultKey as string; 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 { 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 ( return (
<div className="stringInputContainer"> <div className="stringInputContainer">
<div> <div>
@@ -342,7 +354,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput): JSX.Element {
const { label, min, max, defaultValue, dataFieldName, step } = input; const { label, min, max, dataFieldName, step } = input;
const props = { const props = {
label: label as string, label: label as string,
min: min as number, min: min as number,
@@ -351,13 +363,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
step: step as number step: step as number
}; };
const value = this.state.currentValues.get(dataFieldName) as number const value = this.state.currentValues.get(dataFieldName) as number;
if (input.inputType === "spin") { if (input.inputType === "spinner") {
return ( return (
<div> <div>
<SpinButton <SpinButton
{...props} {...props}
value={value.toString()} value={value?.toString()}
onValidate={newValue => this.onValidate(input, newValue, props.min, props.max)} onValidate={newValue => this.onValidate(input, newValue, props.min, props.max)}
onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)} onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)}
onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)} 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") { } else if (input.inputType === "slider") {
return ( return (
<Slider <Slider
// showValue={true}
// valueFormat={}
{...props} {...props}
value={value} value={value}
onChange={newValue => this.onInputChange(input, newValue)} onChange={newValue => this.onInputChange(input, newValue)}
@@ -444,7 +454,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
placeholder={placeholder as string} placeholder={placeholder as string}
options={(choices as ChoiceItem[]).map(c => ({ options={(choices as ChoiceItem[]).map(c => ({
key: c.key, key: c.key,
text: c.value text: c.label
}))} }))}
styles={{ styles={{
label: { 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) { if (input.customElement instanceof Function) {
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
const element = this.state.currentValues.get(dataFieldName) as JSX.Element const element = this.state.currentValues.get(dataFieldName) as JSX.Element;
return element ? element : <></> return element ? element : <></>;
} else { } else {
return input.customElement as JSX.Element; return input.customElement as JSX.Element;
} }
@@ -469,7 +479,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInput(input: AnyInput): JSX.Element { private renderInput(input: AnyInput): JSX.Element {
if (input.customElement) { if (input.customElement) {
return this.renderCustomInput(input) return this.renderCustomInput(input);
} }
switch (input.type) { switch (input.type) {
case "string": case "string":
@@ -484,12 +494,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
private renderNode(node: Node): JSX.Element { private renderNode(node: Node): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 10 }; const containerStackTokens: IStackTokens = { childrenGap: 15 };
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
{node.info && this.renderInfo(node.info as Info)} <Stack.Item>
{node.input && this.renderInput(node.input)} {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>)} {node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack> </Stack>
); );
@@ -497,27 +509,28 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
render(): JSX.Element { render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 20 }; const containerStackTokens: IStackTokens = { childrenGap: 20 };
return this.state.currentValues && this.state.currentValues.size ? ( return this.state.currentValues && this.state.currentValues.size && !this.state.isRefreshing ? (
<Stack tokens={containerStackTokens} styles={{root: {width: 400, padding: 10}}}> <div style={{ overflowX: "auto" }}>
{this.renderNode(this.props.descriptor.root)} <Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
<Stack horizontal tokens={{childrenGap: 10}}> {this.renderNode(this.props.descriptor.root)}
<PrimaryButton <Stack horizontal tokens={{ childrenGap: 10 }}>
styles={{ root: { width: 100 } }} <PrimaryButton
text="submit" styles={{ root: { width: 100 } }}
onClick={async () => { text="submit"
await this.props.descriptor.onSubmit(this.state.currentValues) onClick={async () => {
this.setDefaultValues() await this.props.descriptor.onSubmit(this.state.currentValues);
}} this.initialize();
/> }}
<PrimaryButton />
styles={{ root: { width: 100 } }} <PrimaryButton styles={{ root: { width: 100 } }} text="discard" onClick={() => this.discard()} />
text="discard" </Stack>
onClick={async () => await this.setDefaultValues()}
/>
</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 selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>; public isRefreshingExplorer: ko.Observable<boolean>;
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
private selfServeComponentAdapter: SelfServeComponentAdapter private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
@@ -260,7 +260,7 @@ export default class Explorer {
// React adapters // React adapters
private commandBarComponentAdapter: CommandBarComponentAdapter; private commandBarComponentAdapter: CommandBarComponentAdapter;
private selfServeLoadingComponentAdapter : SelfServeLoadingComponentAdapter; private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private splashScreenAdapter: SplashScreenComponentAdapter; private splashScreenAdapter: SplashScreenComponentAdapter;
private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter; private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter;
private dialogComponentAdapter: DialogComponentAdapter; private dialogComponentAdapter: DialogComponentAdapter;
@@ -1862,16 +1862,16 @@ export default class Explorer {
} }
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void { public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void {
const selfServeTypeForTest = inputs.features[Constants.Features.selfServeTypeForTest] const selfServeTypeForTest = inputs.features[Constants.Features.selfServeTypeForTest];
if (selfServeTypeForTest) { if (selfServeTypeForTest) {
const selfServeType = SelfServeTypes[selfServeTypeForTest?.toLowerCase() as keyof typeof SelfServeTypes] const selfServeType = SelfServeTypes[selfServeTypeForTest?.toLowerCase() as keyof typeof SelfServeTypes];
this.selfServeType(selfServeType ? selfServeType : SelfServeTypes.invalid) this.selfServeType(selfServeType ? selfServeType : SelfServeTypes.invalid);
} else if (inputs.selfServeType) { } else if (inputs.selfServeType) {
this.selfServeType(inputs.selfServeType) this.selfServeType(inputs.selfServeType);
} else { } else {
this.selfServeType(SelfServeTypes.none) this.selfServeType(SelfServeTypes.none);
this._setLoadingStatusText("Connecting...", "Welcome to Azure Cosmos DB") this._setLoadingStatusText("Connecting...", "Welcome to Azure Cosmos DB");
this._setConnectingImage() this._setConnectingImage();
} }
} }
@@ -1894,7 +1894,7 @@ export default class Explorer {
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
this.setSelfServeType(inputs) this.setSelfServeType(inputs);
if (!!inputs.dataExplorerVersion) { if (!!inputs.dataExplorerVersion) {
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion); this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
@@ -3011,7 +3011,7 @@ export default class Explorer {
private _setConnectingImage() { private _setConnectingImage() {
const connectingImage = document.getElementById("explorerConnectingImage"); const connectingImage = document.getElementById("explorerConnectingImage");
connectingImage.innerHTML="<img src=\"../images/HdeConnectCosmosDB.svg\" >"; connectingImage.innerHTML = '<img src="../images/HdeConnectCosmosDB.svg" >';
} }
private _openSetupNotebooksPaneForQuickstart(): void { private _openSetupNotebooksPaneForQuickstart(): void {

View File

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

View File

@@ -1,15 +1,30 @@
import React from "react"; 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"; import { InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
interface TextComponentProps { interface TextComponentProps {
text: string; text: string;
currentValues: Map<string, InputType> currentValues: Map<string, InputType>;
} }
export class TextComponent extends React.Component<TextComponentProps> { 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() { public render(): JSX.Element {
return <Text>{this.props.text}, instanceCount: {this.props.currentValues?.get("instanceCount")}</Text> 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, OnChange,
Placeholder, Placeholder,
CustomElement, CustomElement,
DefaultStringValue,
ChoiceInput, ChoiceInput,
BooleanInput, BooleanInput,
NumberInput NumberInput
} from "../PropertyDescriptors"; } from "../PropertyDescriptors";
import { SmartUi, ClassInfo, OnSubmit, Initialize } from "../ClassDescriptors"; import { SmartUi, ClassInfo, OnSubmit, Initialize } from "../ClassDescriptors";
import { import {
getPromise,
initializeSelfServeExample, initializeSelfServeExample,
instanceSizeInfo, choiceInfo,
instanceSizeOptions, choiceOptions,
onInstanceCountChange, onSliderChange,
onSubmit, onSubmit,
renderText, renderText,
Sizes, selfServeExampleInfo,
selfServeExampleInfo descriptionElement,
initializeNumberMaxValue
} from "./ExampleApis"; } from "./ExampleApis";
import { SelfServeBase } from "../SelfServeUtils"; import { SelfServeBase } from "../SelfServeUtils";
import { ChoiceItem } from "../../Explorer/Controls/SmartUi/SmartUiComponent"; import { ChoiceItem } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
@SmartUi() /*
@ClassInfo(getPromise(selfServeExampleInfo)) This is an example self serve class that auto generates UI components for your feature.
@Initialize(initializeSelfServeExample)
@OnSubmit(onSubmit)
export class SelfServeExample extends SelfServeBase {
@Label(getPromise("Description")) Each self serve class
@CustomElement(renderText("This is the description.")) - 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; static description: string;
@Label(getPromise("Instance Size")) /*
@PropertyInfo(getPromise(instanceSizeInfo)) @ParentOf()
//@ChoiceInput(getPromise(instanceSizeOptions), getPromise(Sizes.OneCore4Gb)) - input: string[]
@ChoiceInput(getPromise(instanceSizeOptions)) - role: Determines which UI elements are the children of which UI element. An array containing the names of the child properties
static instanceSize: ChoiceItem; 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 .")) @Label()
static about: string; - 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) @PropertyInfo()
@BooleanInput("allowed", "not allowed") - input: Info | () => Promise<Info>
static isAllowed: boolean; - 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") @Placeholder("instance name")
static instanceName: string; static stringInput: string;
@Label(getPromise("Instance Count")) @Label("Slider")
@OnChange(onInstanceCountChange)
@ParentOf(["instanceSize", "about", "instanceName", "isAllowed", ]) /*
//@NumberInput(getPromise(1), getPromise(5), getPromise(1), "slider", getPromise(0)) @OnChange()
@NumberInput(getPromise(1), getPromise(5), getPromise(1), "slider") - input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
static instanceCount: number; - 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 React from "react";
import { ChoiceItem, Info, InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent"; import { ChoiceItem, Info, InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
import { TextComponent } from "./CustomComponent"; import { TextComponent } from "./CustomComponent";
import {SessionStorageUtility} from "../../Shared/StorageUtility" import { SessionStorageUtility } from "../../Shared/StorageUtility";
import { Text } from "office-ui-fabric-react";
export enum Sizes { export enum Choices {
OneCore4Gb = "OneCore4Gb", Choice1 = "Choice1",
TwoCore8Gb = "TwoCore8Gb", Choice2 = "Choice2",
FourCore16Gb = "FourCore16Gb" Choice3 = "Choice3"
} }
export const instanceSizeOptions: ChoiceItem[] = [ export const choiceOptions: ChoiceItem[] = [
{ label: Sizes.OneCore4Gb, key: Sizes.OneCore4Gb, value: Sizes.OneCore4Gb }, { label: "Choice 1", key: Choices.Choice1 },
{ label: Sizes.TwoCore8Gb, key: Sizes.TwoCore8Gb, value: Sizes.TwoCore8Gb }, { label: "Choice 2", key: Choices.Choice2 },
{ label: Sizes.FourCore16Gb, key: Sizes.FourCore16Gb, value: Sizes.FourCore16Gb } { label: "Choice 3", key: Choices.Choice3 }
]; ];
export const selfServeExampleInfo: Info = { export const selfServeExampleInfo: Info = {
message: "This is a self serve class" message: "This is a self serve class"
}; };
export const instanceSizeInfo: Info = { export const choiceInfo: Info = {
message: "instance size will be updated in the future" message: "More choices can be added in the future."
}; };
export const onInstanceCountChange = ( export const onSliderChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
currentState: Map<string, InputType>, currentState.set("numberSliderInput", newValue);
newValue: InputType currentState.set("numberSpinnerInput", newValue);
): Map<string, InputType> => {
currentState.set("instanceCount", newValue);
if ((newValue as number) === 1) {
currentState.set("instanceSize", Sizes.OneCore4Gb);
}
return currentState; return currentState;
}; };
export const onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => { export const onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
console.log( SessionStorageUtility.setEntry("choiceInput", currentValues.get("choiceInput")?.toString());
"instanceCount:" + SessionStorageUtility.setEntry("booleanInput", currentValues.get("booleanInput")?.toString());
currentValues.get("instanceCount") + SessionStorageUtility.setEntry("stringInput", currentValues.get("stringInput")?.toString());
", instanceSize:" + SessionStorageUtility.setEntry("numberSliderInput", currentValues.get("numberSliderInput")?.toString());
currentValues.get("instanceSize") + SessionStorageUtility.setEntry("numberSpinnerInput", currentValues.get("numberSpinnerInput")?.toString());
", 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())
}; };
export const initializeSelfServeExample = async () : Promise<Map<string, InputType>> => { const delay = (ms: number): Promise<void> => {
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> => {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
}; };
export const getPromise = <T extends number | string | boolean | ChoiceItem[] | Info>(value: T): (() => Promise<T>) => { export const initializeSelfServeExample = async (): Promise<Map<string, InputType>> => {
const f = async (): Promise<T> => { await delay(1000);
console.log("delay start"); const defaults = new Map<string, InputType>();
await delay(100); defaults.set("choiceInput", SessionStorageUtility.getEntry("choiceInput"));
console.log("delay end"); defaults.set("booleanInput", SessionStorageUtility.getEntry("booleanInput") === "true");
return value; defaults.set("stringInput", SessionStorageUtility.getEntry("stringInput"));
}; const numberSliderInput = parseInt(SessionStorageUtility.getEntry("numberSliderInput"));
return f; 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> => { export const initializeNumberMaxValue = async (): Promise<number> => {
const f = async (currentValues: Map<string, InputType>): Promise<JSX.Element> => { await delay(2000);
return <TextComponent text={text} currentValues={currentValues}/> 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"; import { addPropertyToMap } from "./SelfServeUtils";
interface Decorator { interface Decorator {
name: string, name: string;
value: any value: unknown;
} }
const addToMap = (...decorators: Decorator[]): PropertyDecorator => { const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
return (target, property) => { return (target, property) => {
const className = (target as Function).name; 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, "type", propertyType);
addPropertyToMap(target, property.toString(), className, "dataFieldName", property.toString()); addPropertyToMap(target, property.toString(), className, "dataFieldName", property.toString());
@@ -17,69 +17,68 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
if (!className) { if (!className) {
throw new Error("property descriptor applied to non static field!"); 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 = ( export const OnChange = (
onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType> onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
): PropertyDecorator => { ): 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 => { export const CustomElement = (
return addToMap({name: "customElement", value: 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 => { 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 => { 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 => { 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 => { 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, export interface NumberInputOptions {
max: (() => Promise<number>) | number, min: (() => Promise<number>) | number;
step: (() => Promise<number>) | number, max: (() => Promise<number>) | number;
numberInputType: string, step: (() => Promise<number>) | number;
defaultNumberValue?: (() => Promise<number>) | number, numberInputType: NumberInputType;
): PropertyDecorator => { }
export const NumberInput = (numberInputOptions: NumberInputOptions): PropertyDecorator => {
return addToMap( return addToMap(
{name: "min", value: min}, { name: "min", value: numberInputOptions.min },
{name: "max", value: max}, { name: "max", value: numberInputOptions.max },
{name: "step", value: step}, { name: "step", value: numberInputOptions.step },
{name: "defaultValue", value: defaultNumberValue}, { name: "inputType", value: numberInputOptions.numberInputType }
{name: "inputType", value: numberInputType}
); );
}; };
export const DefaultStringValue = (defaultStringValue: (() => Promise<string>) | string): PropertyDecorator => { export interface BooleanInputOptions {
return addToMap({name: "defaultValue", value: defaultStringValue}); trueLabel: (() => Promise<string>) | string;
}; falseLabel: (() => Promise<string>) | string;
}
export const BooleanInput = (trueLabel: (() => Promise<string>) | string, export const BooleanInput = (booleanInputOptions: BooleanInputOptions): PropertyDecorator => {
falseLabel: (() => Promise<string>) | string,
defaultBooleanValue?: (() => Promise<boolean>) | boolean): PropertyDecorator => {
return addToMap( return addToMap(
{name: "defaultValue", value: defaultBooleanValue}, { name: "trueLabel", value: booleanInputOptions.trueLabel },
{name: "trueLabel", value: trueLabel}, { name: "falseLabel", value: booleanInputOptions.falseLabel }
{name: "falseLabel", value: falseLabel}
); );
}; };
export const ChoiceInput = (choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[], export const ChoiceInput = (choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]): PropertyDecorator => {
defaultKey?: (() => Promise<string>) | string): PropertyDecorator => { return addToMap({ name: "choices", value: choices });
return addToMap(
{name: "choices", value: choices},
{name: "defaultKey", value: defaultKey}
);
}; };

View File

@@ -18,28 +18,31 @@ export class SelfServeComponentAdapter implements ReactAdapter {
constructor(container: Explorer) { constructor(container: Explorer) {
this.container = container; this.container = container;
this.parameters = ko.observable(Date.now()); 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) { switch (selfServeType) {
case SelfServeTypes.example: case SelfServeTypes.example:
return SelfServeExample.toSmartUiDescriptor() return SelfServeExample.toSmartUiDescriptor();
default: default:
return undefined; return undefined;
} }
} };
public renderComponent(): JSX.Element {
const selfServeType = this.container.selfServeType()
const smartUiDescriptor = this.getDescriptor(selfServeType)
public renderComponent(): JSX.Element {
const element = smartUiDescriptor ? const selfServeType = this.container.selfServeType();
<SmartUiComponent descriptor={smartUiDescriptor} /> : const smartUiDescriptor = this.getDescriptor(selfServeType);
const element = smartUiDescriptor ? (
<SmartUiComponent descriptor={smartUiDescriptor} />
) : (
<h1>Invalid self serve type!</h1> <h1>Invalid self serve type!</h1>
);
return element
return element;
} }
private triggerRender() { private triggerRender() {

View File

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

View File

@@ -13,7 +13,7 @@ import {
InputType InputType
} from "../Explorer/Controls/SmartUi/SmartUiComponent"; } from "../Explorer/Controls/SmartUi/SmartUiComponent";
const SelfServeType = "selfServeType" const SelfServeType = "selfServeType";
export class SelfServeBase { export class SelfServeBase {
public static toSmartUiDescriptor(): Descriptor { public static toSmartUiDescriptor(): Descriptor {
@@ -32,11 +32,9 @@ export interface CommonInputTypes {
min?: (() => Promise<number>) | number; min?: (() => Promise<number>) | number;
max?: (() => Promise<number>) | number; max?: (() => Promise<number>) | number;
step?: (() => Promise<number>) | number; step?: (() => Promise<number>) | number;
defaultValue?: any;
trueLabel?: (() => Promise<string>) | string; trueLabel?: (() => Promise<string>) | string;
falseLabel?: (() => Promise<string>) | string; falseLabel?: (() => Promise<string>) | string;
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[]; choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
defaultKey?: (() => Promise<string>) | string;
inputType?: string; inputType?: string;
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>; onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>; onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
@@ -52,10 +50,7 @@ const setValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T
fieldObject[name] = value; fieldObject[name] = value;
}; };
const getValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>( const getValue = <T extends keyof CommonInputTypes>(name: T, fieldObject: CommonInputTypes): unknown => {
name: T,
fieldObject: CommonInputTypes
): K => {
return fieldObject[name]; return fieldObject[name];
}; };
@@ -67,10 +62,10 @@ export const addPropertyToMap = (
descriptorValue: any descriptorValue: any
): void => { ): void => {
const descriptorKey = descriptorName.toString() as keyof CommonInputTypes; 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) { if (!context) {
context = new Map<String, CommonInputTypes>(); context = new Map<string, CommonInputTypes>();
} }
if (!(context instanceof Map)) { if (!(context instanceof Map)) {
@@ -93,7 +88,7 @@ export const addPropertyToMap = (
}; };
export const toSmartUiDescriptor = (metadataKey: string, target: Object): void => { 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); Reflect.defineMetadata(metadataKey, context, target);
const root = context.get("root"); 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, onSubmit: root.onSubmit,
initialize: root.initialize, initialize: root.initialize,
root: { root: {
@@ -124,12 +125,12 @@ export const toSmartUiDescriptor = (metadataKey: string, target: Object): void =
}; };
const addToDescriptor = ( const addToDescriptor = (
context: Map<String, CommonInputTypes>, context: Map<string, CommonInputTypes>,
smartUiDescriptor: Descriptor, smartUiDescriptor: Descriptor,
root: Node, root: Node,
key: String key: string
): void => { ): void => {
let value = context.get(key); const value = context.get(key);
if (!value) { if (!value) {
// should already be added to root // should already be added to root
const childNode = getChildFromRoot(key, smartUiDescriptor); const childNode = getChildFromRoot(key, smartUiDescriptor);
@@ -149,13 +150,13 @@ const addToDescriptor = (
children: [] children: []
} as Node; } as Node;
context.delete(key); context.delete(key);
for (let childKey in childrenKeys) { for (const childKey in childrenKeys) {
addToDescriptor(context, smartUiDescriptor, element, childrenKeys[childKey]); addToDescriptor(context, smartUiDescriptor, element, childrenKeys[childKey]);
} }
root.children.push(element); root.children.push(element);
}; };
const getChildFromRoot = (key: String, smartUiDescriptor: Descriptor): Node => { const getChildFromRoot = (key: string, smartUiDescriptor: Descriptor): Node => {
let i = 0; let i = 0;
const children = smartUiDescriptor.root.children; const children = smartUiDescriptor.root.children;
while (i < children.length) { while (i < children.length) {
@@ -171,8 +172,8 @@ const getChildFromRoot = (key: String, smartUiDescriptor: Descriptor): Node => {
}; };
const getInput = (value: CommonInputTypes): AnyInput => { const getInput = (value: CommonInputTypes): AnyInput => {
if (!value.label || !value.type || !value.dataFieldName) { if (!value.label && !value.customElement) {
throw new Error("label, onChange, type and dataFieldName are required."); throw new Error("label is required.");
} }
switch (value.type) { switch (value.type) {
@@ -197,13 +198,13 @@ const getInput = (value: CommonInputTypes): AnyInput => {
}; };
export enum SelfServeTypes { export enum SelfServeTypes {
none="none", none = "none",
invalid="invalid", invalid = "invalid",
example="example" example = "example"
} }
export const getSelfServeType = (search: string): SelfServeTypes => { export const getSelfServeType = (search: string): SelfServeTypes => {
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
const selfServeTypeParam = params.get(SelfServeType)?.toLowerCase() const selfServeTypeParam = params.get(SelfServeType)?.toLowerCase();
return SelfServeTypes[selfServeTypeParam as keyof typeof SelfServeTypes] return SelfServeTypes[selfServeTypeParam as keyof typeof SelfServeTypes];
} };