mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 03:00:23 +00:00
Added SelfServeComponent
This commit is contained in:
@@ -37,7 +37,7 @@ export class Registerer {
|
|||||||
|
|
||||||
// If any of the ko observable change inside parameters, trigger a new render.
|
// If any of the ko observable change inside parameters, trigger a new render.
|
||||||
ko.computed(() => ko.toJSON(adapter.parameters)).subscribe(() =>
|
ko.computed(() => ko.toJSON(adapter.parameters)).subscribe(() =>
|
||||||
ReactDOM.render(adapter.renderComponent(), element)
|
ReactDOM.render(adapter.renderComponent(), element)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initial rendering at mount point
|
// Initial rendering at mount point
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SmartUiComponent, Descriptor, UiType } from "./SmartUiComponent";
|
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent";
|
||||||
|
|
||||||
describe("SmartUiComponent", () => {
|
describe("SmartUiComponent", () => {
|
||||||
let initializeCalled = false;
|
const exampleData: SmartUiDescriptor = {
|
||||||
let fetchMaxCalled = false;
|
|
||||||
|
|
||||||
const initializeMock = async () => {
|
|
||||||
initializeCalled = true;
|
|
||||||
return new Map();
|
|
||||||
};
|
|
||||||
const fetchMaxvalue = async () => {
|
|
||||||
fetchMaxCalled = true;
|
|
||||||
return 500;
|
|
||||||
};
|
|
||||||
|
|
||||||
const exampleData: Descriptor = {
|
|
||||||
onSubmit: async () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
initialize: initializeMock,
|
|
||||||
root: {
|
root: {
|
||||||
id: "root",
|
id: "root",
|
||||||
info: {
|
info: {
|
||||||
@@ -37,7 +21,7 @@ describe("SmartUiComponent", () => {
|
|||||||
dataFieldName: "throughput",
|
dataFieldName: "throughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
min: 400,
|
min: 400,
|
||||||
max: fetchMaxvalue,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner
|
uiType: UiType.Spinner
|
||||||
@@ -108,14 +92,10 @@ describe("SmartUiComponent", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("should render", async () => {
|
it("should render", async () => {
|
||||||
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} />);
|
const wrapper = shallow(
|
||||||
|
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} />
|
||||||
|
);
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(initializeCalled).toBeTruthy();
|
|
||||||
expect(fetchMaxCalled).toBeTruthy();
|
|
||||||
|
|
||||||
wrapper.setState({ isRefreshing: true });
|
|
||||||
wrapper.update();
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TextField } from "office-ui-fabric-react/lib/TextField";
|
|||||||
import { Text } from "office-ui-fabric-react/lib/Text";
|
import { Text } from "office-ui-fabric-react/lib/Text";
|
||||||
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 } from "office-ui-fabric-react";
|
||||||
import * as InputUtils from "./InputUtils";
|
import * as InputUtils from "./InputUtils";
|
||||||
import "./SmartUiComponent.less";
|
import "./SmartUiComponent.less";
|
||||||
|
|
||||||
@@ -26,51 +26,10 @@ export enum UiType {
|
|||||||
Slider = "Slider"
|
Slider = "Slider"
|
||||||
}
|
}
|
||||||
|
|
||||||
type numberPromise = () => Promise<number>;
|
|
||||||
type stringPromise = () => Promise<string>;
|
|
||||||
type choiceItemPromise = () => Promise<ChoiceItem[]>;
|
|
||||||
type infoPromise = () => Promise<Info>;
|
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
export type ChoiceItem = { label: string; key: string };
|
export type ChoiceItem = { label: string; key: string };
|
||||||
|
|
||||||
export type InputType = number | string | boolean | ChoiceItem;
|
export type InputType = number | string | boolean | ChoiceItem;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For now, this only supports integers
|
|
||||||
*/
|
|
||||||
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 Info {
|
export interface Info {
|
||||||
message: string;
|
message: string;
|
||||||
link?: {
|
link?: {
|
||||||
@@ -79,33 +38,63 @@ export interface Info {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
interface BaseInput {
|
||||||
|
label: string;
|
||||||
|
dataFieldName: string;
|
||||||
|
type: InputTypeValue;
|
||||||
|
placeholder?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Node {
|
/**
|
||||||
|
* For now, this only supports integers
|
||||||
|
*/
|
||||||
|
interface NumberInput extends BaseInput {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
defaultValue?: number;
|
||||||
|
uiType: UiType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BooleanInput extends BaseInput {
|
||||||
|
trueLabel: string;
|
||||||
|
falseLabel: string;
|
||||||
|
defaultValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StringInput extends BaseInput {
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChoiceInput extends BaseInput {
|
||||||
|
choices: ChoiceItem[];
|
||||||
|
defaultKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
||||||
|
|
||||||
|
interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
info?: (() => Promise<Info>) | Info;
|
info?: Info;
|
||||||
input?: AnyInput;
|
input?: AnyInput;
|
||||||
children?: Node[];
|
children?: Node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Descriptor {
|
export interface SmartUiDescriptor {
|
||||||
root: Node;
|
root: Node;
|
||||||
initialize?: () => Promise<Map<string, InputType>>;
|
|
||||||
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
|
||||||
inputNames?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************** Component implementation starts here ************************************* */
|
/************************** Component implementation starts here ************************************* */
|
||||||
|
|
||||||
export interface SmartUiComponentProps {
|
export interface SmartUiComponentProps {
|
||||||
descriptor: Descriptor;
|
descriptor: SmartUiDescriptor;
|
||||||
|
currentValues: Map<string, InputType>;
|
||||||
|
onInputChange: (input: AnyInput, newValue: InputType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmartUiComponentState {
|
interface SmartUiComponentState {
|
||||||
currentValues: Map<string, InputType>;
|
|
||||||
baselineValues: Map<string, InputType>;
|
|
||||||
errors: Map<string, string>;
|
errors: Map<string, string>;
|
||||||
isRefreshing: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
||||||
@@ -115,114 +104,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
fontSize: 12
|
fontSize: 12
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
this.initializeSmartUiComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: SmartUiComponentProps) {
|
constructor(props: SmartUiComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
baselineValues: new Map(),
|
errors: new Map()
|
||||||
currentValues: new Map(),
|
|
||||||
errors: new Map(),
|
|
||||||
isRefreshing: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeSmartUiComponent = async (): Promise<void> => {
|
|
||||||
this.setState({ isRefreshing: true });
|
|
||||||
await this.initializeNode(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: 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 initializeNode = async (currentNode: Node): Promise<void> => {
|
|
||||||
if (currentNode.info && currentNode.info instanceof Function) {
|
|
||||||
currentNode.info = await (currentNode.info as infoPromise)();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentNode.input) {
|
|
||||||
currentNode.input = await this.getModifiedInput(currentNode.input);
|
|
||||||
}
|
|
||||||
const promises = currentNode.children?.map(async (child: Node) => await this.initializeNode(child));
|
|
||||||
if (promises) {
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private getModifiedInput = async (input: AnyInput): Promise<AnyInput> => {
|
|
||||||
if (input.label instanceof Function) {
|
|
||||||
input.label = await (input.label as stringPromise)();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.placeholder instanceof Function) {
|
|
||||||
input.placeholder = await (input.placeholder as stringPromise)();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (input.type) {
|
|
||||||
case "string": {
|
|
||||||
return input as StringInput;
|
|
||||||
}
|
|
||||||
case "number": {
|
|
||||||
const numberInput = input as NumberInput;
|
|
||||||
if (numberInput.min instanceof Function) {
|
|
||||||
numberInput.min = await (numberInput.min as numberPromise)();
|
|
||||||
}
|
|
||||||
if (numberInput.max instanceof Function) {
|
|
||||||
numberInput.max = await (numberInput.max as numberPromise)();
|
|
||||||
}
|
|
||||||
if (numberInput.step instanceof Function) {
|
|
||||||
numberInput.step = await (numberInput.step as numberPromise)();
|
|
||||||
}
|
|
||||||
return numberInput;
|
|
||||||
}
|
|
||||||
case "boolean": {
|
|
||||||
const booleanInput = input as BooleanInput;
|
|
||||||
if (booleanInput.trueLabel instanceof Function) {
|
|
||||||
booleanInput.trueLabel = await (booleanInput.trueLabel as stringPromise)();
|
|
||||||
}
|
|
||||||
if (booleanInput.falseLabel instanceof Function) {
|
|
||||||
booleanInput.falseLabel = await (booleanInput.falseLabel as stringPromise)();
|
|
||||||
}
|
|
||||||
return booleanInput;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
const enumInput = input as ChoiceInput;
|
|
||||||
if (enumInput.choices instanceof Function) {
|
|
||||||
enumInput.choices = await (enumInput.choices as choiceItemPromise)();
|
|
||||||
}
|
|
||||||
return enumInput;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderInfo(info: Info): JSX.Element {
|
private renderInfo(info: Info): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MessageBar>
|
<MessageBar>
|
||||||
@@ -236,42 +124,28 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInputChange = (input: AnyInput, newValue: InputType) => {
|
|
||||||
if (input.onChange) {
|
|
||||||
const newValues = input.onChange(this.state.currentValues, newValue);
|
|
||||||
this.setState({ currentValues: newValues });
|
|
||||||
} else {
|
|
||||||
const dataFieldName = input.dataFieldName;
|
|
||||||
const { currentValues } = this.state;
|
|
||||||
currentValues.set(dataFieldName, newValue);
|
|
||||||
this.setState({ currentValues });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderTextInput(input: StringInput): JSX.Element {
|
private renderTextInput(input: StringInput): JSX.Element {
|
||||||
const value = this.state.currentValues.get(input.dataFieldName) as string;
|
const value = this.props.currentValues.get(input.dataFieldName) as string;
|
||||||
return (
|
return (
|
||||||
<div className="stringInputContainer">
|
<div className="stringInputContainer">
|
||||||
<div>
|
<TextField
|
||||||
<TextField
|
id={`${input.dataFieldName}-textBox-input`}
|
||||||
id={`${input.dataFieldName}-input`}
|
label={input.label}
|
||||||
label={input.label as string}
|
type="text"
|
||||||
type="text"
|
value={value}
|
||||||
value={value}
|
placeholder={input.placeholder}
|
||||||
placeholder={input.placeholder as string}
|
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||||
onChange={(_, newValue) => this.onInputChange(input, newValue)}
|
styles={{
|
||||||
styles={{
|
subComponentStyles: {
|
||||||
subComponentStyles: {
|
label: {
|
||||||
label: {
|
root: {
|
||||||
root: {
|
...SmartUiComponent.labelStyle,
|
||||||
...SmartUiComponent.labelStyle,
|
fontWeight: 600
|
||||||
fontWeight: 600
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -286,7 +160,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.onInputChange(input, newValue);
|
this.props.onInputChange(input, newValue);
|
||||||
this.clearError(dataFieldName);
|
this.clearError(dataFieldName);
|
||||||
return newValue.toString();
|
return newValue.toString();
|
||||||
} else {
|
} else {
|
||||||
@@ -301,7 +175,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
const newValue = InputUtils.onIncrementValue(value, step, max);
|
const newValue = InputUtils.onIncrementValue(value, step, max);
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.onInputChange(input, newValue);
|
this.props.onInputChange(input, newValue);
|
||||||
this.clearError(dataFieldName);
|
this.clearError(dataFieldName);
|
||||||
return newValue.toString();
|
return newValue.toString();
|
||||||
}
|
}
|
||||||
@@ -312,7 +186,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
const newValue = InputUtils.onDecrementValue(value, step, min);
|
const newValue = InputUtils.onDecrementValue(value, step, min);
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.onInputChange(input, newValue);
|
this.props.onInputChange(input, newValue);
|
||||||
this.clearError(dataFieldName);
|
this.clearError(dataFieldName);
|
||||||
return newValue.toString();
|
return newValue.toString();
|
||||||
}
|
}
|
||||||
@@ -322,19 +196,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
private renderNumberInput(input: NumberInput): JSX.Element {
|
private renderNumberInput(input: NumberInput): JSX.Element {
|
||||||
const { label, min, max, dataFieldName, step } = input;
|
const { label, min, max, dataFieldName, step } = input;
|
||||||
const props = {
|
const props = {
|
||||||
label: label as string,
|
label: label,
|
||||||
min: min as number,
|
min: min,
|
||||||
max: max as number,
|
max: max,
|
||||||
ariaLabel: label as string,
|
ariaLabel: label,
|
||||||
step: step as number
|
step: step
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = this.state.currentValues.get(dataFieldName) as number;
|
const value = this.props.currentValues.get(dataFieldName) as number;
|
||||||
if (input.uiType === UiType.Spinner) {
|
if (input.uiType === UiType.Spinner) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
{...props}
|
{...props}
|
||||||
|
id={`${input.dataFieldName}-spinner-input`}
|
||||||
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)}
|
||||||
@@ -354,18 +229,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
} else if (input.uiType === UiType.Slider) {
|
} else if (input.uiType === UiType.Slider) {
|
||||||
return (
|
return (
|
||||||
<Slider
|
<div id={`${input.dataFieldName}-slider-input`}>
|
||||||
{...props}
|
<Slider
|
||||||
value={value}
|
{...props}
|
||||||
onChange={newValue => this.onInputChange(input, newValue)}
|
value={value}
|
||||||
styles={{
|
onChange={newValue => this.props.onInputChange(input, newValue)}
|
||||||
titleLabel: {
|
styles={{
|
||||||
...SmartUiComponent.labelStyle,
|
titleLabel: {
|
||||||
fontWeight: 600
|
...SmartUiComponent.labelStyle,
|
||||||
},
|
fontWeight: 600
|
||||||
valueLabel: SmartUiComponent.labelStyle
|
},
|
||||||
}}
|
valueLabel: SmartUiComponent.labelStyle
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <>Unsupported number UI type {input.uiType}</>;
|
return <>Unsupported number UI type {input.uiType}</>;
|
||||||
@@ -373,9 +250,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
||||||
const { dataFieldName } = input;
|
const value = this.props.currentValues.get(input.dataFieldName) as boolean;
|
||||||
|
const selectedKey = value || input.defaultValue ? "true" : "false";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div id={`${input.dataFieldName}-radioSwitch-input`}>
|
||||||
<div className="inputLabelContainer">
|
<div className="inputLabelContainer">
|
||||||
<Text variant="small" nowrap className="inputLabel">
|
<Text variant="small" nowrap className="inputLabel">
|
||||||
{input.label}
|
{input.label}
|
||||||
@@ -384,23 +262,17 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
<RadioSwitchComponent
|
<RadioSwitchComponent
|
||||||
choices={[
|
choices={[
|
||||||
{
|
{
|
||||||
label: input.falseLabel as string,
|
label: input.falseLabel,
|
||||||
key: "false",
|
key: "false",
|
||||||
onSelect: () => this.onInputChange(input, false)
|
onSelect: () => this.props.onInputChange(input, false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: input.trueLabel as string,
|
label: input.trueLabel,
|
||||||
key: "true",
|
key: "true",
|
||||||
onSelect: () => this.onInputChange(input, true)
|
onSelect: () => this.props.onInputChange(input, true)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
selectedKey={
|
selectedKey={selectedKey}
|
||||||
(this.state.currentValues.has(dataFieldName)
|
|
||||||
? (this.state.currentValues.get(dataFieldName) as boolean)
|
|
||||||
: input.defaultValue)
|
|
||||||
? "true"
|
|
||||||
: "false"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -408,17 +280,15 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
|
|
||||||
private renderChoiceInput(input: ChoiceInput): JSX.Element {
|
private renderChoiceInput(input: ChoiceInput): JSX.Element {
|
||||||
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
|
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
|
||||||
|
const value = this.props.currentValues.get(dataFieldName) as string;
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label={label as string}
|
id={`${input.dataFieldName}-dropown-input`}
|
||||||
selectedKey={
|
label={label}
|
||||||
this.state.currentValues.has(dataFieldName)
|
selectedKey={value ? value : defaultKey}
|
||||||
? (this.state.currentValues.get(dataFieldName) as string)
|
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||||
: (defaultKey as string)
|
placeholder={placeholder}
|
||||||
}
|
options={choices.map(c => ({
|
||||||
onChange={(_, item: IDropdownOption) => this.onInputChange(input, item.key.toString())}
|
|
||||||
placeholder={placeholder as string}
|
|
||||||
options={(choices as ChoiceItem[]).map(c => ({
|
|
||||||
key: c.key,
|
key: c.key,
|
||||||
text: c.label
|
text: c.label
|
||||||
}))}
|
}))}
|
||||||
@@ -471,28 +341,10 @@ 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.isRefreshing ? (
|
return (
|
||||||
<div style={{ overflowX: "auto" }}>
|
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
{this.renderNode(this.props.descriptor.root)}
|
||||||
{this.renderNode(this.props.descriptor.root)}
|
</Stack>
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
|
||||||
<PrimaryButton
|
|
||||||
styles={{ root: { width: 100 } }}
|
|
||||||
text="submit"
|
|
||||||
onClick={async () => {
|
|
||||||
await this.props.descriptor.onSubmit(this.state.currentValues);
|
|
||||||
this.setDefaults();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PrimaryButton 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%" } }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +1,103 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SmartUiComponent should render 1`] = `
|
exports[`SmartUiComponent should render 1`] = `
|
||||||
<div
|
<Stack
|
||||||
style={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"overflowX": "auto",
|
"root": Object {
|
||||||
|
"padding": 10,
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 20,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
styles={
|
className="widgetRendererContainer"
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"padding": 10,
|
|
||||||
"width": 400,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 20,
|
"childrenGap": 15,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack
|
<StackItem>
|
||||||
className="widgetRendererContainer"
|
<StyledMessageBarBase>
|
||||||
tokens={
|
Start at $24/mo per database
|
||||||
Object {
|
<StyledLinkBase
|
||||||
"childrenGap": 15,
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
}
|
target="_blank"
|
||||||
}
|
>
|
||||||
|
More Details
|
||||||
|
</StyledLinkBase>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
<div
|
||||||
|
key="throughput"
|
||||||
>
|
>
|
||||||
<StackItem>
|
<Stack
|
||||||
<StyledMessageBarBase>
|
className="widgetRendererContainer"
|
||||||
Start at $24/mo per database
|
tokens={
|
||||||
<StyledLinkBase
|
Object {
|
||||||
href="https://aka.ms/azure-cosmos-db-pricing"
|
"childrenGap": 15,
|
||||||
target="_blank"
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<CustomizedSpinButton
|
||||||
|
ariaLabel="Throughput (input)"
|
||||||
|
decrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronDownSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={false}
|
||||||
|
id="throughput-spinner-input"
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput2"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
id="throughput2-slider-input"
|
||||||
>
|
>
|
||||||
More Details
|
|
||||||
</StyledLinkBase>
|
|
||||||
</StyledMessageBarBase>
|
|
||||||
</StackItem>
|
|
||||||
<div
|
|
||||||
key="throughput"
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
className="widgetRendererContainer"
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<div>
|
|
||||||
<CustomizedSpinButton
|
|
||||||
ariaLabel="Throughput (input)"
|
|
||||||
decrementButtonIcon={
|
|
||||||
Object {
|
|
||||||
"iconName": "ChevronDownSmall",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
key="throughput2"
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
className="widgetRendererContainer"
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<StyledSliderBase
|
<StyledSliderBase
|
||||||
ariaLabel="Throughput (Slider)"
|
ariaLabel="Throughput (Slider)"
|
||||||
label="Throughput (Slider)"
|
label="Throughput (Slider)"
|
||||||
@@ -126,215 +121,169 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</div>
|
||||||
</Stack>
|
</StackItem>
|
||||||
</div>
|
</Stack>
|
||||||
<div
|
</div>
|
||||||
key="throughput3"
|
<div
|
||||||
>
|
key="throughput3"
|
||||||
<Stack
|
>
|
||||||
className="widgetRendererContainer"
|
<Stack
|
||||||
tokens={
|
className="widgetRendererContainer"
|
||||||
Object {
|
tokens={
|
||||||
"childrenGap": 15,
|
Object {
|
||||||
}
|
"childrenGap": 15,
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
<StackItem>
|
|
||||||
<StyledMessageBarBase
|
|
||||||
messageBarType={1}
|
|
||||||
>
|
|
||||||
Error:
|
|
||||||
label, truelabel and falselabel are required for boolean input 'throughput3'
|
|
||||||
</StyledMessageBarBase>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
key="containerId"
|
|
||||||
>
|
>
|
||||||
<Stack
|
<StackItem>
|
||||||
className="widgetRendererContainer"
|
<StyledMessageBarBase
|
||||||
tokens={
|
messageBarType={1}
|
||||||
Object {
|
>
|
||||||
"childrenGap": 15,
|
Error:
|
||||||
}
|
label, truelabel and falselabel are required for boolean input 'throughput3'
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="containerId"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 15,
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
<StackItem>
|
|
||||||
<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>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
key="analyticalStore"
|
|
||||||
>
|
>
|
||||||
<Stack
|
<StackItem>
|
||||||
className="widgetRendererContainer"
|
<div
|
||||||
tokens={
|
className="stringInputContainer"
|
||||||
Object {
|
>
|
||||||
"childrenGap": 15,
|
<StyledTextFieldBase
|
||||||
}
|
id="containerId-textBox-input"
|
||||||
}
|
label="Container id"
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<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>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
key="database"
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
className="widgetRendererContainer"
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<StyledWithResponsiveMode
|
|
||||||
label="Database"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
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>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="analyticalStore"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 15,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
id="analyticalStore-radioSwitch-input"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="inputLabelContainer"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className="inputLabel"
|
||||||
|
nowrap={true}
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
Analytical Store
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<RadioSwitchComponent
|
||||||
|
choices={
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"key": "db1",
|
"key": "false",
|
||||||
"text": "Database 1",
|
"label": "Disabled",
|
||||||
|
"onSelect": [Function],
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "db2",
|
"key": "true",
|
||||||
"text": "Database 2",
|
"label": "Enabled",
|
||||||
},
|
"onSelect": [Function],
|
||||||
Object {
|
|
||||||
"key": "db3",
|
|
||||||
"text": "Database 3",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
selectedKey="db2"
|
selectedKey="true"
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</div>
|
||||||
</Stack>
|
</StackItem>
|
||||||
</div>
|
</Stack>
|
||||||
</Stack>
|
</div>
|
||||||
<Stack
|
<div
|
||||||
horizontal={true}
|
key="database"
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<CustomizedPrimaryButton
|
<Stack
|
||||||
onClick={[Function]}
|
className="widgetRendererContainer"
|
||||||
styles={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"childrenGap": 15,
|
||||||
"width": 100,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text="submit"
|
>
|
||||||
/>
|
<StackItem>
|
||||||
<CustomizedPrimaryButton
|
<StyledWithResponsiveMode
|
||||||
onClick={[Function]}
|
id="database-dropown-input"
|
||||||
styles={
|
label="Database"
|
||||||
Object {
|
onChange={[Function]}
|
||||||
"root": Object {
|
options={
|
||||||
"width": 100,
|
Array [
|
||||||
},
|
Object {
|
||||||
}
|
"key": "db1",
|
||||||
}
|
"text": "Database 1",
|
||||||
text="discard"
|
},
|
||||||
/>
|
Object {
|
||||||
</Stack>
|
"key": "db2",
|
||||||
|
"text": "Database 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db3",
|
||||||
|
"text": "Database 3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</Stack>
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SmartUiComponent should render 2`] = `
|
|
||||||
<StyledSpinnerBase
|
|
||||||
size={3}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"height": "100%",
|
|
||||||
"justifyContent": "center",
|
|
||||||
"textAlign": "center",
|
|
||||||
"width": "100%",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
|||||||
import refreshImg from "../images/refresh-cosmos.svg";
|
import refreshImg from "../images/refresh-cosmos.svg";
|
||||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
|
|
||||||
import { SelfServeType } from "./SelfServe/SelfServeUtils";
|
|
||||||
|
|
||||||
// TODO: Encapsulate and reuse all global variables as environment variables
|
// TODO: Encapsulate and reuse all global variables as environment variables
|
||||||
window.authType = AuthType.AAD;
|
window.authType = AuthType.AAD;
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import { Info } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|||||||
import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils";
|
import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils";
|
||||||
|
|
||||||
export const IsDisplayable = (): ClassDecorator => {
|
export const IsDisplayable = (): ClassDecorator => {
|
||||||
//eslint-disable-next-line @typescript-eslint/ban-types
|
return target => {
|
||||||
return (target: Function) => {
|
|
||||||
buildSmartUiDescriptor(target.name, target.prototype);
|
buildSmartUiDescriptor(target.name, target.prototype);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
||||||
//eslint-disable-next-line @typescript-eslint/ban-types
|
return target => {
|
||||||
return (target: Function) => {
|
|
||||||
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,15 +34,15 @@ export interface ChoiceInputOptions extends InputOptionsBase {
|
|||||||
type InputOptions = NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions;
|
type InputOptions = NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions;
|
||||||
|
|
||||||
function isNumberInputOptions(inputOptions: InputOptions): inputOptions is NumberInputOptions {
|
function isNumberInputOptions(inputOptions: InputOptions): inputOptions is NumberInputOptions {
|
||||||
return !!(inputOptions as NumberInputOptions).min;
|
return "min" in inputOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBooleanInputOptions(inputOptions: InputOptions): inputOptions is BooleanInputOptions {
|
function isBooleanInputOptions(inputOptions: InputOptions): inputOptions is BooleanInputOptions {
|
||||||
return !!(inputOptions as BooleanInputOptions).trueLabel;
|
return "trueLabel" in inputOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isChoiceInputOptions(inputOptions: InputOptions): inputOptions is ChoiceInputOptions {
|
function isChoiceInputOptions(inputOptions: InputOptions): inputOptions is ChoiceInputOptions {
|
||||||
return !!(inputOptions as ChoiceInputOptions).choices;
|
return "choices" in inputOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||||
@@ -77,32 +77,25 @@ export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecora
|
|||||||
|
|
||||||
export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
||||||
if (isNumberInputOptions(inputOptions)) {
|
if (isNumberInputOptions(inputOptions)) {
|
||||||
const numberInputOptions = inputOptions as NumberInputOptions;
|
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "label", value: numberInputOptions.label },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "min", value: numberInputOptions.min },
|
{ name: "min", value: inputOptions.min },
|
||||||
{ name: "max", value: numberInputOptions.max },
|
{ name: "max", value: inputOptions.max },
|
||||||
{ name: "step", value: numberInputOptions.step },
|
{ name: "step", value: inputOptions.step },
|
||||||
{ name: "uiType", value: numberInputOptions.uiType }
|
{ name: "uiType", value: inputOptions.uiType }
|
||||||
);
|
);
|
||||||
} else if (isBooleanInputOptions(inputOptions)) {
|
} else if (isBooleanInputOptions(inputOptions)) {
|
||||||
const booleanInputOptions = inputOptions as BooleanInputOptions;
|
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "label", value: booleanInputOptions.label },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "trueLabel", value: booleanInputOptions.trueLabel },
|
{ name: "trueLabel", value: inputOptions.trueLabel },
|
||||||
{ name: "falseLabel", value: booleanInputOptions.falseLabel }
|
{ name: "falseLabel", value: inputOptions.falseLabel }
|
||||||
);
|
);
|
||||||
} else if (isChoiceInputOptions(inputOptions)) {
|
} else if (isChoiceInputOptions(inputOptions)) {
|
||||||
const choiceInputOptions = inputOptions as ChoiceInputOptions;
|
return addToMap({ name: "label", value: inputOptions.label }, { name: "choices", value: inputOptions.choices });
|
||||||
return addToMap(
|
|
||||||
{ name: "label", value: choiceInputOptions.label },
|
|
||||||
{ name: "choices", value: choiceInputOptions.choices }
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const stringInputOptions = inputOptions as StringInputOptions;
|
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "label", value: stringInputOptions.label },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "placeholder", value: stringInputOptions.placeholder }
|
{ name: "placeholder", value: inputOptions.placeholder }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
104
src/SelfServe/SelfServeComponent.test.tsx
Normal file
104
src/SelfServe/SelfServeComponent.test.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { SelfServeDescriptor, SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
||||||
|
import { InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||||
|
|
||||||
|
describe("SelfServeComponent", () => {
|
||||||
|
const defaultValues = new Map<string, InputType>([
|
||||||
|
["throughput", "450"],
|
||||||
|
["analyticalStore", "false"],
|
||||||
|
["database", "db2"]
|
||||||
|
]);
|
||||||
|
const initializeMock = jest.fn(async () => defaultValues);
|
||||||
|
const onSubmitMock = jest.fn(async () => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
const exampleData: SelfServeDescriptor = {
|
||||||
|
initialize: initializeMock,
|
||||||
|
onSubmit: onSubmitMock,
|
||||||
|
inputNames: ["throughput", "containerId", "analyticalStore", "database"],
|
||||||
|
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,
|
||||||
|
uiType: UiType.Spinner
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: "object",
|
||||||
|
choices: [
|
||||||
|
{ label: "Database 1", key: "db1" },
|
||||||
|
{ label: "Database 2", key: "db2" },
|
||||||
|
{ label: "Database 3", key: "db3" }
|
||||||
|
],
|
||||||
|
defaultKey: "db2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyDefaultsSet = (currentValues: Map<string, InputType>): void => {
|
||||||
|
for (const key of currentValues.keys()) {
|
||||||
|
if (defaultValues.has(key)) {
|
||||||
|
expect(defaultValues.get(key)).toEqual(currentValues.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should render", async () => {
|
||||||
|
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
// initialize() should be called and defaults should be set when component is mounted
|
||||||
|
expect(initializeMock).toHaveBeenCalled();
|
||||||
|
const state = wrapper.state() as SelfServeComponentState;
|
||||||
|
verifyDefaultsSet(state.currentValues);
|
||||||
|
|
||||||
|
// onSubmit() must be called when submit button is clicked
|
||||||
|
const submitButton = wrapper.find("#submitButton");
|
||||||
|
submitButton.simulate("click");
|
||||||
|
expect(onSubmitMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
219
src/SelfServe/SelfServeComponent.tsx
Normal file
219
src/SelfServe/SelfServeComponent.tsx
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IStackTokens, PrimaryButton, Spinner, SpinnerSize, Stack } from "office-ui-fabric-react";
|
||||||
|
import {
|
||||||
|
ChoiceItem,
|
||||||
|
InputType,
|
||||||
|
InputTypeValue,
|
||||||
|
SmartUiComponent,
|
||||||
|
UiType,
|
||||||
|
SmartUiDescriptor,
|
||||||
|
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(),
|
||||||
|
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: currentValues, baselineValues: baselineValues, isRefreshing: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
public discard = (): void => {
|
||||||
|
console.log("discarding");
|
||||||
|
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 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%" } }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,26 +7,26 @@ import * as ko from "knockout";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import { Descriptor, SmartUiComponent } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { SelfServeDescriptor, SelfServeComponent } from "./SelfServeComponent";
|
||||||
import { SelfServeType } from "./SelfServeUtils";
|
import { SelfServeType } from "./SelfServeUtils";
|
||||||
|
|
||||||
export class SelfServeComponentAdapter implements ReactAdapter {
|
export class SelfServeComponentAdapter implements ReactAdapter {
|
||||||
public parameters: ko.Observable<Descriptor>;
|
public parameters: ko.Observable<SelfServeDescriptor>;
|
||||||
public container: Explorer;
|
public container: Explorer;
|
||||||
|
|
||||||
constructor(container: Explorer) {
|
constructor(container: Explorer) {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.parameters = ko.observable(undefined)
|
this.parameters = ko.observable(undefined);
|
||||||
this.container.selfServeType.subscribe(() => {
|
this.container.selfServeType.subscribe(() => {
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getDescriptor = async (selfServeType: SelfServeType): Promise<Descriptor> => {
|
public static getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
|
||||||
switch (selfServeType) {
|
switch (selfServeType) {
|
||||||
case SelfServeType.example: {
|
case SelfServeType.example: {
|
||||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||||
return new SelfServeExample.default().toSmartUiDescriptor();
|
return new SelfServeExample.default().toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -35,21 +35,17 @@ export class SelfServeComponentAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
if (this.container.selfServeType() === SelfServeType.invalid) {
|
if (this.container.selfServeType() === SelfServeType.invalid) {
|
||||||
return <h1>Invalid self serve type!</h1>
|
return <h1>Invalid self serve type!</h1>;
|
||||||
}
|
}
|
||||||
const smartUiDescriptor = this.parameters()
|
const smartUiDescriptor = this.parameters();
|
||||||
return smartUiDescriptor ? (
|
return smartUiDescriptor ? <SelfServeComponent descriptor={smartUiDescriptor} /> : <></>;
|
||||||
<SmartUiComponent descriptor={smartUiDescriptor} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private triggerRender() {
|
private triggerRender() {
|
||||||
window.requestAnimationFrame(async () => {
|
window.requestAnimationFrame(async () => {
|
||||||
const selfServeType = this.container.selfServeType();
|
const selfServeType = this.container.selfServeType();
|
||||||
const smartUiDescriptor = await SelfServeComponentAdapter.getDescriptor(selfServeType);
|
const smartUiDescriptor = await SelfServeComponentAdapter.getDescriptor(selfServeType);
|
||||||
this.parameters(smartUiDescriptor)
|
this.parameters(smartUiDescriptor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ describe("SelfServeUtils", () => {
|
|||||||
};
|
};
|
||||||
public initialize: () => Promise<Map<string, InputType>>;
|
public initialize: () => Promise<Map<string, InputType>>;
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSmartUiDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("onSubmit should be declared for self serve classes", () => {
|
it("onSubmit should be declared for self serve classes", () => {
|
||||||
@@ -24,7 +24,7 @@ describe("SelfServeUtils", () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSmartUiDescriptor()).toThrow("onSubmit() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSubmit() was not declared for the class 'Test'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("@SmartUi decorator must be present for self serve classes", () => {
|
it("@SmartUi decorator must be present for self serve classes", () => {
|
||||||
@@ -36,7 +36,9 @@ describe("SelfServeUtils", () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSmartUiDescriptor()).toThrow("@SmartUi decorator was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
||||||
|
"@SmartUi decorator was not declared for the class 'Test'"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updateContextWithDecorator", () => {
|
it("updateContextWithDecorator", () => {
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
|
import { ChoiceItem, Info, InputTypeValue, InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||||
import {
|
import {
|
||||||
ChoiceItem,
|
|
||||||
Node,
|
|
||||||
Info,
|
|
||||||
InputTypeValue,
|
|
||||||
Descriptor,
|
|
||||||
AnyInput,
|
|
||||||
NumberInput,
|
|
||||||
StringInput,
|
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
ChoiceInput,
|
ChoiceInput,
|
||||||
InputType
|
SelfServeDescriptor,
|
||||||
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
NumberInput,
|
||||||
|
StringInput,
|
||||||
|
Node,
|
||||||
|
AnyInput
|
||||||
|
} from "./SelfServeComponent";
|
||||||
|
|
||||||
export enum SelfServeType {
|
export enum SelfServeType {
|
||||||
// No self serve type passed, launch explorer
|
// No self serve type passed, launch explorer
|
||||||
@@ -26,9 +23,9 @@ export abstract class SelfServeBaseClass {
|
|||||||
public abstract onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
|
public abstract onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
|
||||||
public abstract initialize: () => Promise<Map<string, InputType>>;
|
public abstract initialize: () => Promise<Map<string, InputType>>;
|
||||||
|
|
||||||
public toSmartUiDescriptor(): Descriptor {
|
public toSelfServeDescriptor(): SelfServeDescriptor {
|
||||||
const className = this.constructor.name;
|
const className = this.constructor.name;
|
||||||
const smartUiDescriptor = Reflect.getMetadata(className, this) as Descriptor;
|
const smartUiDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
|
||||||
|
|
||||||
if (!this.initialize) {
|
if (!this.initialize) {
|
||||||
throw new Error(`initialize() was not declared for the class '${className}'`);
|
throw new Error(`initialize() was not declared for the class '${className}'`);
|
||||||
@@ -128,12 +125,12 @@ export const buildSmartUiDescriptor = (className: string, target: unknown): void
|
|||||||
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>): Descriptor => {
|
export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>): SelfServeDescriptor => {
|
||||||
const root = context.get("root");
|
const root = context.get("root");
|
||||||
context.delete("root");
|
context.delete("root");
|
||||||
const inputNames: string[] = [];
|
const inputNames: string[] = [];
|
||||||
|
|
||||||
const smartUiDescriptor: Descriptor = {
|
const smartUiDescriptor: SelfServeDescriptor = {
|
||||||
root: {
|
root: {
|
||||||
id: "root",
|
id: "root",
|
||||||
info: root?.info,
|
info: root?.info,
|
||||||
|
|||||||
168
src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap
Normal file
168
src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`SelfServeComponent should render 1`] = `
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflowX": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"padding": 10,
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SmartUiComponent
|
||||||
|
currentValues={
|
||||||
|
Map {
|
||||||
|
"throughput" => "450",
|
||||||
|
"analyticalStore" => "false",
|
||||||
|
"database" => "db2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptor={
|
||||||
|
Object {
|
||||||
|
"initialize": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"inputNames": Array [
|
||||||
|
"throughput",
|
||||||
|
"containerId",
|
||||||
|
"analyticalStore",
|
||||||
|
"database",
|
||||||
|
],
|
||||||
|
"onSubmit": [MockFunction],
|
||||||
|
"root": Object {
|
||||||
|
"children": Array [
|
||||||
|
Object {
|
||||||
|
"id": "throughput",
|
||||||
|
"info": undefined,
|
||||||
|
"input": Object {
|
||||||
|
"dataFieldName": "throughput",
|
||||||
|
"defaultValue": 400,
|
||||||
|
"label": "Throughput (input)",
|
||||||
|
"max": 500,
|
||||||
|
"min": 400,
|
||||||
|
"placeholder": undefined,
|
||||||
|
"step": 10,
|
||||||
|
"type": "number",
|
||||||
|
"uiType": "Spinner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "containerId",
|
||||||
|
"info": undefined,
|
||||||
|
"input": Object {
|
||||||
|
"dataFieldName": "containerId",
|
||||||
|
"label": "Container id",
|
||||||
|
"placeholder": undefined,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "analyticalStore",
|
||||||
|
"info": undefined,
|
||||||
|
"input": Object {
|
||||||
|
"dataFieldName": "analyticalStore",
|
||||||
|
"defaultValue": true,
|
||||||
|
"falseLabel": "Disabled",
|
||||||
|
"label": "Analytical Store",
|
||||||
|
"placeholder": undefined,
|
||||||
|
"trueLabel": "Enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "database",
|
||||||
|
"info": undefined,
|
||||||
|
"input": Object {
|
||||||
|
"choices": Array [
|
||||||
|
Object {
|
||||||
|
"key": "db1",
|
||||||
|
"label": "Database 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db2",
|
||||||
|
"label": "Database 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db3",
|
||||||
|
"label": "Database 3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"dataFieldName": "database",
|
||||||
|
"defaultKey": "db2",
|
||||||
|
"label": "Database",
|
||||||
|
"placeholder": undefined,
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"id": "root",
|
||||||
|
"info": Object {
|
||||||
|
"link": Object {
|
||||||
|
"href": "https://aka.ms/azure-cosmos-db-pricing",
|
||||||
|
"text": "More Details",
|
||||||
|
},
|
||||||
|
"message": "Start at $24/mo per database",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onInputChange={[Function]}
|
||||||
|
/>
|
||||||
|
<Stack
|
||||||
|
horizontal={true}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
id="submitButton"
|
||||||
|
onClick={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text="submit"
|
||||||
|
/>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
id="discardButton"
|
||||||
|
onClick={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text="discard"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -12,13 +12,11 @@ describe("Self Serve", () => {
|
|||||||
frame = await getTestExplorerFrame(
|
frame = await getTestExplorerFrame(
|
||||||
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]])
|
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]])
|
||||||
);
|
);
|
||||||
await frame.waitForSelector(".ms-Dropdown");
|
await frame.waitForSelector("#regions-dropown-input");
|
||||||
const dropdownLabel = await frame.$eval(".ms-Dropdown-label", element => element.textContent);
|
await frame.waitForSelector("#enableLogging-radioSwitch-input");
|
||||||
expect(dropdownLabel).toEqual("Regions");
|
await frame.waitForSelector("#accountName-textBox-input");
|
||||||
await frame.waitForSelector(".radioSwitchComponent");
|
await frame.waitForSelector("#dbThroughput-slider-input");
|
||||||
await frame.waitForSelector(".ms-TextField");
|
await frame.waitForSelector("#collectionThroughput-spinner-input");
|
||||||
await frame.waitForSelector(".ms-Slider ");
|
|
||||||
await frame.waitForSelector(".ms-spinButton-input");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const testName = (expect as any).getState().currentTestName;
|
const testName = (expect as any).getState().currentTestName;
|
||||||
|
|||||||
Reference in New Issue
Block a user