mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 06:56:38 +00:00
Added more Self Serve functionalities (#401)
* added recursion and inition decorators * working version * added todo comment and removed console.log * Added Recursive add * removed type requirement * proper resolution of promises * added custom element and base class * Made selfServe standalone page * Added custom renderer as async type * Added overall defaults * added inital open from data explorer * removed landingpage * added feature for self serve type * renamed sqlx->example and added invalid type * Added comments for Example * removed unnecessary changes * Resolved PR comments Added tests Moved onSubmt and initialize inside base class Moved testExplorer to separate folder made fields of SelfServe Class non static * fixed lint errors * fixed compilation errors * Removed reactbinding changes * renamed dropdown -> choice * Added SelfServeComponent * Addressed PR comments * added toggle, visibility, text display,commandbar * added sqlx example * added onRefrssh * formatting changes * rmoved radioswitch display * updated smartui tests * Added more tests * onSubmit -> onSave * Resolved PR comments
This commit is contained in:
parent
b0b973b21a
commit
49bf8c60db
@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent";
|
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
|
||||||
|
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
|
||||||
|
|
||||||
describe("SmartUiComponent", () => {
|
describe("SmartUiComponent", () => {
|
||||||
const exampleData: SmartUiDescriptor = {
|
const exampleData: SmartUiDescriptor = {
|
||||||
@ -14,6 +15,20 @@ describe("SmartUiComponent", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: "description",
|
||||||
|
input: {
|
||||||
|
dataFieldName: "description",
|
||||||
|
type: "string",
|
||||||
|
description: {
|
||||||
|
text: "this is an example description text.",
|
||||||
|
link: {
|
||||||
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
|
text: "Click here for more information.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "throughput",
|
id: "throughput",
|
||||||
input: {
|
input: {
|
||||||
@ -24,7 +39,7 @@ describe("SmartUiComponent", () => {
|
|||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -37,7 +52,7 @@ describe("SmartUiComponent", () => {
|
|||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Slider,
|
uiType: NumberUiType.Slider,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50,7 +65,7 @@ describe("SmartUiComponent", () => {
|
|||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'",
|
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -91,11 +106,58 @@ describe("SmartUiComponent", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should render", async () => {
|
it("should render and honor input's hidden, disabled state", async () => {
|
||||||
|
const currentValues = new Map<string, SmartUiInput>();
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} />
|
<SmartUiComponent
|
||||||
|
disabled={false}
|
||||||
|
descriptor={exampleData}
|
||||||
|
currentValues={currentValues}
|
||||||
|
onInputChange={jest.fn()}
|
||||||
|
onError={() => {
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
expect(wrapper.exists("#containerId-textField-input")).toBeTruthy();
|
||||||
|
|
||||||
|
currentValues.set("containerId", { value: "container1", hidden: true });
|
||||||
|
wrapper.setProps({ currentValues });
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.exists("#containerId-textField-input")).toBeFalsy();
|
||||||
|
|
||||||
|
currentValues.set("containerId", { value: "container1", hidden: false, disabled: true });
|
||||||
|
wrapper.setProps({ currentValues });
|
||||||
|
wrapper.update();
|
||||||
|
const containerIdTextField = wrapper.find("#containerId-textField-input");
|
||||||
|
expect(containerIdTextField.props().disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disable all inputs", async () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<SmartUiComponent
|
||||||
|
disabled={true}
|
||||||
|
descriptor={exampleData}
|
||||||
|
currentValues={new Map()}
|
||||||
|
onInputChange={jest.fn()}
|
||||||
|
onError={() => {
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
const throughputSpinner = wrapper.find("#throughput-spinner-input");
|
||||||
|
expect(throughputSpinner.props().disabled).toBeTruthy();
|
||||||
|
const throughput2Slider = wrapper.find("#throughput2-slider-input").childAt(0);
|
||||||
|
expect(throughput2Slider.props().disabled).toBeTruthy();
|
||||||
|
const containerIdTextField = wrapper.find("#containerId-textField-input");
|
||||||
|
expect(containerIdTextField.props().disabled).toBeTruthy();
|
||||||
|
const analyticalStoreToggle = wrapper.find("#analyticalStore-toggle-input");
|
||||||
|
expect(analyticalStoreToggle.props().disabled).toBeTruthy();
|
||||||
|
const databaseDropdown = wrapper.find("#database-dropdown-input");
|
||||||
|
expect(databaseDropdown.props().disabled).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,11 +5,19 @@ import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
|
|||||||
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
import { TextField } from "office-ui-fabric-react/lib/TextField";
|
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 { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
||||||
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
import { Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
|
||||||
import * as InputUtils from "./InputUtils";
|
import * as InputUtils from "./InputUtils";
|
||||||
import "./SmartUiComponent.less";
|
import "./SmartUiComponent.less";
|
||||||
|
import {
|
||||||
|
ChoiceItem,
|
||||||
|
Description,
|
||||||
|
Info,
|
||||||
|
InputType,
|
||||||
|
InputTypeValue,
|
||||||
|
NumberUiType,
|
||||||
|
SmartUiInput,
|
||||||
|
} from "../../../SelfServe/SelfServeTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic UX renderer
|
* Generic UX renderer
|
||||||
@ -19,29 +27,14 @@ import "./SmartUiComponent.less";
|
|||||||
* - a descriptor of the UX.
|
* - a descriptor of the UX.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type InputTypeValue = "number" | "string" | "boolean" | "object";
|
interface BaseDisplay {
|
||||||
|
|
||||||
export enum UiType {
|
|
||||||
Spinner = "Spinner",
|
|
||||||
Slider = "Slider",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChoiceItem = { label: string; key: string };
|
|
||||||
|
|
||||||
export type InputType = number | string | boolean | ChoiceItem;
|
|
||||||
|
|
||||||
export interface Info {
|
|
||||||
message: string;
|
|
||||||
link?: {
|
|
||||||
href: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseInput {
|
|
||||||
label: string;
|
|
||||||
dataFieldName: string;
|
dataFieldName: string;
|
||||||
|
errorMessage?: string;
|
||||||
type: InputTypeValue;
|
type: InputTypeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseInput extends BaseDisplay {
|
||||||
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
@ -54,7 +47,7 @@ interface NumberInput extends BaseInput {
|
|||||||
max: number;
|
max: number;
|
||||||
step: number;
|
step: number;
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
uiType: UiType;
|
uiType: NumberUiType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BooleanInput extends BaseInput {
|
interface BooleanInput extends BaseInput {
|
||||||
@ -72,12 +65,16 @@ interface ChoiceInput extends BaseInput {
|
|||||||
defaultKey?: string;
|
defaultKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
interface DescriptionDisplay extends BaseDisplay {
|
||||||
|
description: Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
info?: Info;
|
info?: Info;
|
||||||
input?: AnyInput;
|
input?: AnyDisplay;
|
||||||
children?: Node[];
|
children?: Node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,11 +83,12 @@ export interface SmartUiDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/************************** Component implementation starts here ************************************* */
|
/************************** Component implementation starts here ************************************* */
|
||||||
|
|
||||||
export interface SmartUiComponentProps {
|
export interface SmartUiComponentProps {
|
||||||
descriptor: SmartUiDescriptor;
|
descriptor: SmartUiDescriptor;
|
||||||
currentValues: Map<string, InputType>;
|
currentValues: Map<string, SmartUiInput>;
|
||||||
onInputChange: (input: AnyInput, newValue: InputType) => void;
|
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
|
||||||
|
onError: (hasError: boolean) => void;
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmartUiComponentState {
|
interface SmartUiComponentState {
|
||||||
@ -98,12 +96,22 @@ interface SmartUiComponentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
||||||
|
private shouldCheckErrors = true;
|
||||||
private static readonly labelStyle = {
|
private static readonly labelStyle = {
|
||||||
color: "#393939",
|
color: "#393939",
|
||||||
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidUpdate(): void {
|
||||||
|
if (!this.shouldCheckErrors) {
|
||||||
|
this.shouldCheckErrors = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onError(this.state.errors.size > 0);
|
||||||
|
this.shouldCheckErrors = false;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: SmartUiComponentProps) {
|
constructor(props: SmartUiComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -113,7 +121,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
|
|
||||||
private renderInfo(info: Info): JSX.Element {
|
private renderInfo(info: Info): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MessageBar>
|
<MessageBar styles={{ root: { width: 400 } }}>
|
||||||
{info.message}
|
{info.message}
|
||||||
{info.link && (
|
{info.link && (
|
||||||
<Link href={info.link.href} target="_blank">
|
<Link href={info.link.href} target="_blank">
|
||||||
@ -125,17 +133,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderTextInput(input: StringInput): JSX.Element {
|
private renderTextInput(input: StringInput): JSX.Element {
|
||||||
const value = this.props.currentValues.get(input.dataFieldName) as string;
|
const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
|
||||||
|
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
|
||||||
return (
|
return (
|
||||||
<div className="stringInputContainer">
|
<div className="stringInputContainer">
|
||||||
<TextField
|
<TextField
|
||||||
id={`${input.dataFieldName}-textBox-input`}
|
id={`${input.dataFieldName}-textField-input`}
|
||||||
label={input.label}
|
label={input.label}
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value || ""}
|
||||||
placeholder={input.placeholder}
|
placeholder={input.placeholder}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
|
root: { width: 400 },
|
||||||
subComponentStyles: {
|
subComponentStyles: {
|
||||||
label: {
|
label: {
|
||||||
root: {
|
root: {
|
||||||
@ -150,13 +161,27 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderDescription(input: DescriptionDisplay): JSX.Element {
|
||||||
|
const description = input.description;
|
||||||
|
return (
|
||||||
|
<Text id={`${input.dataFieldName}-text-display`}>
|
||||||
|
{input.description.text}{" "}
|
||||||
|
{description.link && (
|
||||||
|
<Link target="_blank" href={input.description.link.href}>
|
||||||
|
{input.description.link.text}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private clearError(dataFieldName: string): void {
|
private clearError(dataFieldName: string): void {
|
||||||
const { errors } = this.state;
|
const { errors } = this.state;
|
||||||
errors.delete(dataFieldName);
|
errors.delete(dataFieldName);
|
||||||
this.setState({ errors });
|
this.setState({ errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onValidate = (input: AnyInput, value: string, min: number, max: number): string => {
|
private onValidate = (input: NumberInput, value: string, min: number, max: number): string => {
|
||||||
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) {
|
||||||
@ -165,13 +190,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return newValue.toString();
|
return newValue.toString();
|
||||||
} else {
|
} else {
|
||||||
const { errors } = this.state;
|
const { errors } = this.state;
|
||||||
errors.set(dataFieldName, `Invalid value ${value}: must be between ${min} and ${max}`);
|
errors.set(dataFieldName, `Invalid value '${value}'. It must be between ${min} and ${max}`);
|
||||||
this.setState({ errors });
|
this.setState({ errors });
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => {
|
private onIncrement = (input: NumberInput, value: string, step: number, max: number): string => {
|
||||||
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) {
|
||||||
@ -182,7 +207,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => {
|
private onDecrement = (input: NumberInput, value: string, step: number, min: number): string => {
|
||||||
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) {
|
||||||
@ -203,10 +228,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
step: step,
|
step: step,
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = this.props.currentValues.get(dataFieldName) as number;
|
const value = this.props.currentValues.get(dataFieldName)?.value as number;
|
||||||
if (input.uiType === UiType.Spinner) {
|
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||||
|
if (input.uiType === NumberUiType.Spinner) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
{...props}
|
{...props}
|
||||||
id={`${input.dataFieldName}-spinner-input`}
|
id={`${input.dataFieldName}-spinner-input`}
|
||||||
@ -215,6 +241,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
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)}
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
|
disabled={disabled}
|
||||||
styles={{
|
styles={{
|
||||||
label: {
|
label: {
|
||||||
...SmartUiComponent.labelStyle,
|
...SmartUiComponent.labelStyle,
|
||||||
@ -225,16 +252,18 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
{this.state.errors.has(dataFieldName) && (
|
{this.state.errors.has(dataFieldName) && (
|
||||||
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
||||||
)}
|
)}
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else if (input.uiType === UiType.Slider) {
|
} else if (input.uiType === NumberUiType.Slider) {
|
||||||
return (
|
return (
|
||||||
<div id={`${input.dataFieldName}-slider-input`}>
|
<div id={`${input.dataFieldName}-slider-input`}>
|
||||||
<Slider
|
<Slider
|
||||||
{...props}
|
{...props}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
|
root: { width: 400 },
|
||||||
titleLabel: {
|
titleLabel: {
|
||||||
...SmartUiComponent.labelStyle,
|
...SmartUiComponent.labelStyle,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -250,49 +279,44 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
||||||
const value = this.props.currentValues.get(input.dataFieldName) as boolean;
|
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
|
||||||
const selectedKey = value || input.defaultValue ? "true" : "false";
|
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
|
||||||
return (
|
return (
|
||||||
<div id={`${input.dataFieldName}-radioSwitch-input`}>
|
<Toggle
|
||||||
<div className="inputLabelContainer">
|
id={`${input.dataFieldName}-toggle-input`}
|
||||||
<Text variant="small" nowrap className="inputLabel">
|
label={input.label}
|
||||||
{input.label}
|
checked={value || false}
|
||||||
</Text>
|
onText={input.trueLabel}
|
||||||
</div>
|
offText={input.falseLabel}
|
||||||
<RadioSwitchComponent
|
disabled={disabled}
|
||||||
choices={[
|
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
|
||||||
{
|
styles={{ root: { width: 400 } }}
|
||||||
label: input.falseLabel,
|
/>
|
||||||
key: "false",
|
|
||||||
onSelect: () => this.props.onInputChange(input, false),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: input.trueLabel,
|
|
||||||
key: "true",
|
|
||||||
onSelect: () => this.props.onInputChange(input, true),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
selectedKey={selectedKey}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
const value = this.props.currentValues.get(dataFieldName)?.value as string;
|
||||||
|
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||||
|
let selectedKey = value ? value : defaultKey;
|
||||||
|
if (!selectedKey) {
|
||||||
|
selectedKey = "";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
id={`${input.dataFieldName}-dropown-input`}
|
id={`${input.dataFieldName}-dropdown-input`}
|
||||||
label={label}
|
label={label}
|
||||||
selectedKey={value ? value : defaultKey}
|
selectedKey={selectedKey}
|
||||||
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
options={choices.map((c) => ({
|
options={choices.map((c) => ({
|
||||||
key: c.key,
|
key: c.key,
|
||||||
text: c.label,
|
text: c.label,
|
||||||
}))}
|
}))}
|
||||||
styles={{
|
styles={{
|
||||||
|
root: { width: 400 },
|
||||||
label: {
|
label: {
|
||||||
...SmartUiComponent.labelStyle,
|
...SmartUiComponent.labelStyle,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -303,16 +327,23 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderError(input: AnyInput): JSX.Element {
|
private renderError(input: AnyDisplay): JSX.Element {
|
||||||
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
|
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderInput(input: AnyInput): JSX.Element {
|
private renderDisplay(input: AnyDisplay): JSX.Element {
|
||||||
if (input.errorMessage) {
|
if (input.errorMessage) {
|
||||||
return this.renderError(input);
|
return this.renderError(input);
|
||||||
}
|
}
|
||||||
|
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
|
||||||
|
if (inputHidden) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "string":
|
case "string":
|
||||||
|
if ("description" in input) {
|
||||||
|
return this.renderDescription(input as DescriptionDisplay);
|
||||||
|
}
|
||||||
return this.renderTextInput(input as StringInput);
|
return this.renderTextInput(input as StringInput);
|
||||||
case "number":
|
case "number":
|
||||||
return this.renderNumberInput(input as NumberInput);
|
return this.renderNumberInput(input as NumberInput);
|
||||||
@ -326,13 +357,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderNode(node: Node): JSX.Element {
|
private renderNode(node: Node): JSX.Element {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 15 };
|
const containerStackTokens: IStackTokens = { childrenGap: 10 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
{node.info && this.renderInfo(node.info as Info)}
|
{node.info && this.renderInfo(node.info as Info)}
|
||||||
{node.input && this.renderInput(node.input)}
|
{node.input && this.renderDisplay(node.input)}
|
||||||
</Stack.Item>
|
</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>
|
||||||
@ -340,11 +371,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
return this.renderNode(this.props.descriptor.root);
|
||||||
return (
|
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
|
||||||
{this.renderNode(this.props.descriptor.root)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,405 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SmartUiComponent should render 1`] = `
|
exports[`SmartUiComponent disable all inputs 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
styles={
|
className="widgetRendererContainer"
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"padding": 10,
|
|
||||||
"width": 400,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 20,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack
|
<StackItem>
|
||||||
className="widgetRendererContainer"
|
<StyledMessageBarBase
|
||||||
tokens={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"root": Object {
|
||||||
}
|
"width": 400,
|
||||||
}
|
},
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<StyledMessageBarBase>
|
|
||||||
Start at $24/mo per database
|
|
||||||
<StyledLinkBase
|
|
||||||
href="https://aka.ms/azure-cosmos-db-pricing"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
More Details
|
|
||||||
</StyledLinkBase>
|
|
||||||
</StyledMessageBarBase>
|
|
||||||
</StackItem>
|
|
||||||
<div
|
|
||||||
key="throughput"
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
className="widgetRendererContainer"
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 15,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Start at $24/mo per database
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<StackItem>
|
More Details
|
||||||
|
</StyledLinkBase>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
<div
|
||||||
|
key="description"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
id="description-text-display"
|
||||||
|
>
|
||||||
|
this is an example description text.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Click here for more information.
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomizedSpinButton
|
||||||
|
ariaLabel="Throughput (input)"
|
||||||
|
decrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronDownSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={true}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput2"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
id="throughput2-slider-input"
|
||||||
|
>
|
||||||
|
<StyledSliderBase
|
||||||
|
ariaLabel="Throughput (Slider)"
|
||||||
|
disabled={true}
|
||||||
|
label="Throughput (Slider)"
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onChange={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
"titleLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
"valueLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput3"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={1}
|
||||||
|
>
|
||||||
|
Error:
|
||||||
|
label, truelabel and falselabel are required for boolean input 'throughput3'
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="containerId"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
className="stringInputContainer"
|
||||||
|
>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={true}
|
||||||
|
id="containerId-textField-input"
|
||||||
|
label="Container id"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="analyticalStore"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledToggleBase
|
||||||
|
checked={false}
|
||||||
|
disabled={true}
|
||||||
|
id="analyticalStore-toggle-input"
|
||||||
|
label="Analytical Store"
|
||||||
|
offText="Disabled"
|
||||||
|
onChange={[Function]}
|
||||||
|
onText="Enabled"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="database"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
disabled={true}
|
||||||
|
id="database-dropdown-input"
|
||||||
|
label="Database"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "db1",
|
||||||
|
"text": "Database 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SmartUiComponent should render and honor input's hidden, disabled state 1`] = `
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Start at $24/mo per database
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
More Details
|
||||||
|
</StyledLinkBase>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
<div
|
||||||
|
key="description"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
id="description-text-display"
|
||||||
|
>
|
||||||
|
this is an example description text.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Click here for more information.
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<CustomizedSpinButton
|
<CustomizedSpinButton
|
||||||
ariaLabel="Throughput (input)"
|
ariaLabel="Throughput (input)"
|
||||||
decrementButtonIcon={
|
decrementButtonIcon={
|
||||||
@ -80,210 +433,203 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</Stack>
|
||||||
</Stack>
|
</StackItem>
|
||||||
</div>
|
</Stack>
|
||||||
<div
|
</div>
|
||||||
key="throughput2"
|
<div
|
||||||
>
|
key="throughput2"
|
||||||
<Stack
|
>
|
||||||
className="widgetRendererContainer"
|
<Stack
|
||||||
tokens={
|
className="widgetRendererContainer"
|
||||||
Object {
|
tokens={
|
||||||
"childrenGap": 15,
|
Object {
|
||||||
}
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
<StackItem>
|
|
||||||
<div
|
|
||||||
id="throughput2-slider-input"
|
|
||||||
>
|
|
||||||
<StyledSliderBase
|
|
||||||
ariaLabel="Throughput (Slider)"
|
|
||||||
label="Throughput (Slider)"
|
|
||||||
max={500}
|
|
||||||
min={400}
|
|
||||||
onChange={[Function]}
|
|
||||||
step={10}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"titleLabel": Object {
|
|
||||||
"color": "#393939",
|
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
|
||||||
"fontSize": 12,
|
|
||||||
"fontWeight": 600,
|
|
||||||
},
|
|
||||||
"valueLabel": Object {
|
|
||||||
"color": "#393939",
|
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
key="throughput3"
|
|
||||||
>
|
>
|
||||||
<Stack
|
<StackItem>
|
||||||
className="widgetRendererContainer"
|
<div
|
||||||
tokens={
|
id="throughput2-slider-input"
|
||||||
Object {
|
>
|
||||||
"childrenGap": 15,
|
<StyledSliderBase
|
||||||
}
|
ariaLabel="Throughput (Slider)"
|
||||||
}
|
label="Throughput (Slider)"
|
||||||
>
|
max={500}
|
||||||
<StackItem>
|
min={400}
|
||||||
<StyledMessageBarBase
|
|
||||||
messageBarType={1}
|
|
||||||
>
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<StyledTextFieldBase
|
|
||||||
id="containerId-textBox-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>
|
|
||||||
</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 [
|
|
||||||
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
|
|
||||||
id="database-dropown-input"
|
|
||||||
label="Database"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
step={10}
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"key": "db1",
|
|
||||||
"text": "Database 1",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"key": "db2",
|
|
||||||
"text": "Database 2",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"key": "db3",
|
|
||||||
"text": "Database 3",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
selectedKey="db2"
|
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"dropdown": Object {
|
"root": Object {
|
||||||
"color": "#393939",
|
"width": 400,
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
|
||||||
"fontSize": 12,
|
|
||||||
},
|
},
|
||||||
"label": Object {
|
"titleLabel": Object {
|
||||||
"color": "#393939",
|
"color": "#393939",
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
"fontSize": 12,
|
"fontSize": 12,
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
},
|
},
|
||||||
|
"valueLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</div>
|
||||||
</Stack>
|
</StackItem>
|
||||||
</div>
|
</Stack>
|
||||||
</Stack>
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput3"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={1}
|
||||||
|
>
|
||||||
|
Error:
|
||||||
|
label, truelabel and falselabel are required for boolean input 'throughput3'
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="containerId"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
className="stringInputContainer"
|
||||||
|
>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
id="containerId-textField-input"
|
||||||
|
label="Container id"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="analyticalStore"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledToggleBase
|
||||||
|
checked={false}
|
||||||
|
id="analyticalStore-toggle-input"
|
||||||
|
label="Analytical Store"
|
||||||
|
offText="Disabled"
|
||||||
|
onChange={[Function]}
|
||||||
|
onText="Enabled"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="database"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
id="database-dropdown-input"
|
||||||
|
label="Database"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "db1",
|
||||||
|
"text": "Database 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import { Info } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
||||||
import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils";
|
|
||||||
|
|
||||||
export const IsDisplayable = (): ClassDecorator => {
|
|
||||||
return (target) => {
|
|
||||||
buildSmartUiDescriptor(target.name, target.prototype);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
|
||||||
return (target) => {
|
|
||||||
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,10 +1,10 @@
|
|||||||
import { ChoiceItem, Info, InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes";
|
||||||
import { addPropertyToMap, CommonInputTypes } from "./SelfServeUtils";
|
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
interface Decorator {
|
interface Decorator {
|
||||||
name: keyof CommonInputTypes;
|
name: keyof DecoratorProperties;
|
||||||
value: ValueOf<CommonInputTypes>;
|
value: ValueOf<DecoratorProperties>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputOptionsBase {
|
interface InputOptionsBase {
|
||||||
@ -15,7 +15,7 @@ export interface NumberInputOptions extends InputOptionsBase {
|
|||||||
min: (() => Promise<number>) | number;
|
min: (() => Promise<number>) | number;
|
||||||
max: (() => Promise<number>) | number;
|
max: (() => Promise<number>) | number;
|
||||||
step: (() => Promise<number>) | number;
|
step: (() => Promise<number>) | number;
|
||||||
uiType: UiType;
|
uiType: NumberUiType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StringInputOptions extends InputOptionsBase {
|
export interface StringInputOptions extends InputOptionsBase {
|
||||||
@ -29,9 +29,19 @@ export interface BooleanInputOptions extends InputOptionsBase {
|
|||||||
|
|
||||||
export interface ChoiceInputOptions extends InputOptionsBase {
|
export interface ChoiceInputOptions extends InputOptionsBase {
|
||||||
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||||
|
placeholder?: (() => Promise<string>) | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputOptions = NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions;
|
export interface DescriptionDisplayOptions {
|
||||||
|
description?: (() => Promise<Description>) | Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputOptions =
|
||||||
|
| NumberInputOptions
|
||||||
|
| StringInputOptions
|
||||||
|
| BooleanInputOptions
|
||||||
|
| ChoiceInputOptions
|
||||||
|
| DescriptionDisplayOptions;
|
||||||
|
|
||||||
const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is NumberInputOptions => {
|
const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is NumberInputOptions => {
|
||||||
return "min" in inputOptions;
|
return "min" in inputOptions;
|
||||||
@ -45,6 +55,10 @@ const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is Choic
|
|||||||
return "choices" in inputOptions;
|
return "choices" in inputOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
|
||||||
|
return "description" in inputOptions;
|
||||||
|
};
|
||||||
|
|
||||||
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||||
return (target, property) => {
|
return (target, property) => {
|
||||||
let className = target.constructor.name;
|
let className = target.constructor.name;
|
||||||
@ -66,7 +80,7 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const OnChange = (
|
export const OnChange = (
|
||||||
onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>
|
||||||
): PropertyDecorator => {
|
): PropertyDecorator => {
|
||||||
return addToMap({ name: "onChange", value: onChange });
|
return addToMap({ name: "onChange", value: onChange });
|
||||||
};
|
};
|
||||||
@ -91,7 +105,13 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
|||||||
{ name: "falseLabel", value: inputOptions.falseLabel }
|
{ name: "falseLabel", value: inputOptions.falseLabel }
|
||||||
);
|
);
|
||||||
} else if (isChoiceInputOptions(inputOptions)) {
|
} else if (isChoiceInputOptions(inputOptions)) {
|
||||||
return addToMap({ name: "label", value: inputOptions.label }, { name: "choices", value: inputOptions.choices });
|
return addToMap(
|
||||||
|
{ name: "label", value: inputOptions.label },
|
||||||
|
{ name: "placeholder", value: inputOptions.placeholder },
|
||||||
|
{ name: "choices", value: inputOptions.choices }
|
||||||
|
);
|
||||||
|
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
||||||
|
return addToMap({ name: "description", value: inputOptions.description });
|
||||||
} else {
|
} else {
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "label", value: inputOptions.label },
|
{ name: "label", value: inputOptions.label },
|
||||||
@ -99,3 +119,15 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IsDisplayable = (): ClassDecorator => {
|
||||||
|
return (target) => {
|
||||||
|
buildSmartUiDescriptor(target.name, target.prototype);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
||||||
|
return (target) => {
|
||||||
|
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
||||||
|
};
|
||||||
|
};
|
64
src/SelfServe/Example/SelfServeExample.rp.ts
Normal file
64
src/SelfServe/Example/SelfServeExample.rp.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { get } from "../../Utils/arm/generatedClients/2020-04-01/databaseAccounts";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { SessionStorageUtility } from "../../Shared/StorageUtility";
|
||||||
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
|
export enum Regions {
|
||||||
|
NorthCentralUS = "NorthCentralUS",
|
||||||
|
WestUS = "WestUS",
|
||||||
|
EastUS2 = "EastUS2",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializeResponse {
|
||||||
|
regions: Regions;
|
||||||
|
enableLogging: boolean;
|
||||||
|
accountName: string;
|
||||||
|
collectionThroughput: number;
|
||||||
|
dbThroughput: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMaxThroughput = async (): Promise<number> => {
|
||||||
|
return 10000;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
regions: Regions,
|
||||||
|
enableLogging: boolean,
|
||||||
|
accountName: string,
|
||||||
|
collectionThroughput: number,
|
||||||
|
dbThoughput: number
|
||||||
|
): Promise<void> => {
|
||||||
|
SessionStorageUtility.setEntry("regions", regions);
|
||||||
|
SessionStorageUtility.setEntry("enableLogging", enableLogging?.toString());
|
||||||
|
SessionStorageUtility.setEntry("accountName", accountName);
|
||||||
|
SessionStorageUtility.setEntry("collectionThroughput", collectionThroughput?.toString());
|
||||||
|
SessionStorageUtility.setEntry("dbThroughput", dbThoughput?.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialize = async (): Promise<InitializeResponse> => {
|
||||||
|
const regions = Regions[SessionStorageUtility.getEntry("regions") as keyof typeof Regions];
|
||||||
|
const enableLogging = SessionStorageUtility.getEntry("enableLogging") === "true";
|
||||||
|
const accountName = SessionStorageUtility.getEntry("accountName");
|
||||||
|
let collectionThroughput = parseInt(SessionStorageUtility.getEntry("collectionThroughput"));
|
||||||
|
collectionThroughput = isNaN(collectionThroughput) ? undefined : collectionThroughput;
|
||||||
|
let dbThroughput = parseInt(SessionStorageUtility.getEntry("dbThroughput"));
|
||||||
|
dbThroughput = isNaN(dbThroughput) ? undefined : dbThroughput;
|
||||||
|
return {
|
||||||
|
regions: regions,
|
||||||
|
enableLogging: enableLogging,
|
||||||
|
accountName: accountName,
|
||||||
|
collectionThroughput: collectionThroughput,
|
||||||
|
dbThroughput: dbThroughput,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const databaseAccountName = userContext.databaseAccount.name;
|
||||||
|
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
|
||||||
|
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
||||||
|
return {
|
||||||
|
isUpdateInProgress: isUpdateInProgress,
|
||||||
|
notificationMessage: "Self Serve Example successfully refreshing",
|
||||||
|
};
|
||||||
|
};
|
@ -1,37 +1,57 @@
|
|||||||
import { PropertyInfo, OnChange, Values } from "../PropertyDecorators";
|
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators";
|
||||||
import { ClassInfo, IsDisplayable } from "../ClassDecorators";
|
import {
|
||||||
import { SelfServeBaseClass } from "../SelfServeUtils";
|
ChoiceItem,
|
||||||
import { ChoiceItem, Info, InputType, UiType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
|
Info,
|
||||||
import { SessionStorageUtility } from "../../Shared/StorageUtility";
|
InputType,
|
||||||
|
NumberUiType,
|
||||||
|
RefreshResult,
|
||||||
|
SelfServeBaseClass,
|
||||||
|
SelfServeNotification,
|
||||||
|
SelfServeNotificationType,
|
||||||
|
SmartUiInput,
|
||||||
|
} from "../SelfServeTypes";
|
||||||
|
import { onRefreshSelfServeExample, getMaxThroughput, Regions, update, initialize } from "./SelfServeExample.rp";
|
||||||
|
|
||||||
export enum Regions {
|
const regionDropdownItems: ChoiceItem[] = [
|
||||||
NorthCentralUS = "NCUS",
|
|
||||||
WestUS = "WUS",
|
|
||||||
EastUS2 = "EUS2",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const regionDropdownItems: ChoiceItem[] = [
|
|
||||||
{ label: "North Central US", key: Regions.NorthCentralUS },
|
{ label: "North Central US", key: Regions.NorthCentralUS },
|
||||||
{ label: "West US", key: Regions.WestUS },
|
{ label: "West US", key: Regions.WestUS },
|
||||||
{ label: "East US 2", key: Regions.EastUS2 },
|
{ label: "East US 2", key: Regions.EastUS2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const selfServeExampleInfo: Info = {
|
const selfServeExampleInfo: Info = {
|
||||||
message: "This is a self serve class",
|
message: "This is a self serve class",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const regionDropdownInfo: Info = {
|
const regionDropdownInfo: Info = {
|
||||||
message: "More regions can be added in the future.",
|
message: "More regions can be added in the future.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDbThroughputChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
|
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
|
||||||
currentState.set("dbThroughput", newValue);
|
currentState.set("regions", { value: newValue });
|
||||||
currentState.set("collectionThroughput", newValue);
|
const currentEnableLogging = currentState.get("enableLogging");
|
||||||
|
if (newValue === Regions.NorthCentralUS) {
|
||||||
|
currentState.set("enableLogging", { value: false, disabled: true });
|
||||||
|
} else {
|
||||||
|
currentState.set("enableLogging", { value: currentEnableLogging.value, disabled: false });
|
||||||
|
}
|
||||||
return currentState;
|
return currentState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeMaxThroughput = async (): Promise<number> => {
|
const onEnableDbLevelThroughputChange = (
|
||||||
return 10000;
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
newValue: InputType
|
||||||
|
): Map<string, SmartUiInput> => {
|
||||||
|
currentState.set("enableDbLevelThroughput", { value: newValue });
|
||||||
|
const currentDbThroughput = currentState.get("dbThroughput");
|
||||||
|
const isDbThroughputHidden = newValue === undefined || !(newValue as boolean);
|
||||||
|
currentState.set("dbThroughput", { value: currentDbThroughput.value, hidden: isDbThroughputHidden });
|
||||||
|
return currentState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
||||||
|
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
||||||
|
throw new Error("Regions and AccountName should not be empty.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -40,8 +60,9 @@ const initializeMaxThroughput = async (): Promise<number> => {
|
|||||||
Each self serve class
|
Each self serve class
|
||||||
- Needs to extends the SelfServeBase class.
|
- Needs to extends the SelfServeBase class.
|
||||||
- Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
|
- Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
|
||||||
- Needs to define an onSubmit() function, a callback for when the submit button is clicked.
|
- Needs to define an onSave() function, a callback for when the submit button is clicked.
|
||||||
- Needs to define an initialize() function, to set default values for the inputs.
|
- Needs to define an initialize() function, to set default values for the inputs.
|
||||||
|
- Needs to define an onRefresh() function, a callback for when the refresh button is clicked.
|
||||||
|
|
||||||
You can test this self serve UI by using the featureflag '?feature.selfServeType=example'
|
You can test this self serve UI by using the featureflag '?feature.selfServeType=example'
|
||||||
and plumb in similar feature flags for your own self serve class.
|
and plumb in similar feature flags for your own self serve class.
|
||||||
@ -61,25 +82,46 @@ const initializeMaxThroughput = async (): Promise<number> => {
|
|||||||
@ClassInfo(selfServeExampleInfo)
|
@ClassInfo(selfServeExampleInfo)
|
||||||
export default class SelfServeExample extends SelfServeBaseClass {
|
export default class SelfServeExample extends SelfServeBaseClass {
|
||||||
/*
|
/*
|
||||||
onSubmit()
|
onRefresh()
|
||||||
|
- role : Callback that is triggerrd when the refresh button is clicked. You should perform the your rest API
|
||||||
|
call to check if the update action is completed.
|
||||||
|
- returns:
|
||||||
|
RefreshResult -
|
||||||
|
isComponentUpdating: Indicated if the state is still being updated
|
||||||
|
notificationMessage: Notification message to be shown in case the component is still being updated
|
||||||
|
i.e, isComponentUpdating is true
|
||||||
|
*/
|
||||||
|
public onRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
return onRefreshSelfServeExample();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
onSave()
|
||||||
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
||||||
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
|
- 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.
|
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 this example, the onSave callback simply sets the value for keys corresponding to the field name
|
||||||
in the SessionStorage.
|
in the SessionStorage.
|
||||||
|
- returns: SelfServeNotification -
|
||||||
|
message: The message to be displayed in the message bar after the onSave is completed
|
||||||
|
type: The type of message bar to be used (info, warning, error)
|
||||||
*/
|
*/
|
||||||
public onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
|
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||||
SessionStorageUtility.setEntry("regions", currentValues.get("regions")?.toString());
|
validate(currentValues);
|
||||||
SessionStorageUtility.setEntry("enableLogging", currentValues.get("enableLogging")?.toString());
|
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
|
||||||
SessionStorageUtility.setEntry("accountName", currentValues.get("accountName")?.toString());
|
const enableLogging = currentValues.get("enableLogging")?.value as boolean;
|
||||||
SessionStorageUtility.setEntry("dbThroughput", currentValues.get("dbThroughput")?.toString());
|
const accountName = currentValues.get("accountName")?.value as string;
|
||||||
SessionStorageUtility.setEntry("collectionThroughput", currentValues.get("collectionThroughput")?.toString());
|
const collectionThroughput = currentValues.get("collectionThroughput")?.value as number;
|
||||||
|
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
|
||||||
|
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
||||||
|
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
||||||
|
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
||||||
|
return { message: "submitted successfully", type: SelfServeNotificationType.info };
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
initialize()
|
initialize()
|
||||||
- input: () => Promise<Map<string, InputType>>
|
|
||||||
- role: Set default values for the properties of this class.
|
- role: Set default values for the properties of this class.
|
||||||
|
|
||||||
The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput),
|
The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput),
|
||||||
@ -87,24 +129,46 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
defaults can be set by setting values in a Map corresponding to the field's name.
|
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 initialize function, to fetch the initial values for
|
Typically, you can make rest calls in the async initialize function, to fetch the initial values for
|
||||||
these fields. This is called after the onSubmit callback, to reinitialize the defaults.
|
these fields. This is called after the onSave callback, to reinitialize the defaults.
|
||||||
|
|
||||||
In this example, the initialize function simply reads the SessionStorage to fetch the default values
|
In this example, the initialize function simply reads the SessionStorage to fetch the default values
|
||||||
for these fields. These are then set when the changes are submitted.
|
for these fields. These are then set when the changes are submitted.
|
||||||
|
- returns: () => Promise<Map<string, InputType>>
|
||||||
*/
|
*/
|
||||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||||
const defaults = new Map<string, InputType>();
|
const initializeResponse = await initialize();
|
||||||
defaults.set("regions", SessionStorageUtility.getEntry("regions"));
|
const defaults = new Map<string, SmartUiInput>();
|
||||||
defaults.set("enableLogging", SessionStorageUtility.getEntry("enableLogging") === "true");
|
defaults.set("regions", { value: initializeResponse.regions });
|
||||||
const stringInput = SessionStorageUtility.getEntry("accountName");
|
defaults.set("enableLogging", { value: initializeResponse.enableLogging });
|
||||||
defaults.set("accountName", stringInput ? stringInput : "");
|
const accountName = initializeResponse.accountName;
|
||||||
const numberSliderInput = parseInt(SessionStorageUtility.getEntry("dbThroughput"));
|
defaults.set("accountName", { value: accountName ? accountName : "" });
|
||||||
defaults.set("dbThroughput", isNaN(numberSliderInput) ? 1 : numberSliderInput);
|
defaults.set("collectionThroughput", { value: initializeResponse.collectionThroughput });
|
||||||
const numberSpinnerInput = parseInt(SessionStorageUtility.getEntry("collectionThroughput"));
|
const enableDbLevelThroughput = !!initializeResponse.dbThroughput;
|
||||||
defaults.set("collectionThroughput", isNaN(numberSpinnerInput) ? 1 : numberSpinnerInput);
|
defaults.set("enableDbLevelThroughput", { value: enableDbLevelThroughput });
|
||||||
|
defaults.set("dbThroughput", { value: initializeResponse.dbThroughput, hidden: !enableDbLevelThroughput });
|
||||||
return defaults;
|
return defaults;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
@Values() :
|
||||||
|
- input: NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions | DescriptionDisplay
|
||||||
|
- role: Specifies the required options to display the property as
|
||||||
|
a) TextBox for text input
|
||||||
|
b) Spinner/Slider for number input
|
||||||
|
c) Radio buton/Toggle for boolean input
|
||||||
|
d) Dropdown for choice input
|
||||||
|
e) Text (with optional hyperlink) for descriptions
|
||||||
|
*/
|
||||||
|
@Values({
|
||||||
|
description: {
|
||||||
|
text: "This class sets collection and database throughput.",
|
||||||
|
link: {
|
||||||
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
|
text: "Click here for more information",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
/*
|
/*
|
||||||
@PropertyInfo()
|
@PropertyInfo()
|
||||||
- optional
|
- optional
|
||||||
@ -114,11 +178,22 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
@PropertyInfo(regionDropdownInfo)
|
@PropertyInfo(regionDropdownInfo)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@Values() :
|
@OnChange()
|
||||||
- input: NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions
|
- optional
|
||||||
- role: Specifies the required options to display the property as TextBox, Number Spinner/Slider, Radio buton or Dropdown.
|
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
||||||
|
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property,
|
||||||
|
say prop1, changes its value in the UI. This can be used to
|
||||||
|
a) Change the value (and reflect it in the UI) for prop2 based on prop1.
|
||||||
|
b) Change the visibility for prop2 in the UI, based on prop1
|
||||||
|
|
||||||
|
The new Map of propertyName -> value is returned.
|
||||||
|
|
||||||
|
In this example, the onRegionsChange function sets the enableLogging property to false (and disables
|
||||||
|
the corresponsing toggle UI) when "regions" is set to "North Central US", and enables the toggle for
|
||||||
|
any other value of "regions"
|
||||||
*/
|
*/
|
||||||
@Values({ label: "Regions", choices: regionDropdownItems })
|
@OnChange(onRegionsChange)
|
||||||
|
@Values({ label: "Regions", choices: regionDropdownItems, placeholder: "Select a region" })
|
||||||
regions: ChoiceItem;
|
regions: ChoiceItem;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
@ -134,34 +209,33 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
})
|
})
|
||||||
accountName: string;
|
accountName: string;
|
||||||
|
|
||||||
/*
|
|
||||||
@OnChange()
|
|
||||||
- optional
|
|
||||||
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
|
||||||
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property
|
|
||||||
changes its value in the UI. This can be used to change other input values based on some other input.
|
|
||||||
|
|
||||||
The new Map of propertyName -> value is returned.
|
|
||||||
|
|
||||||
In this example, the onDbThroughputChange function sets the collectionThroughput to the same value as the dbThroughput
|
|
||||||
when the slider in moved in the UI.
|
|
||||||
*/
|
|
||||||
@OnChange(onDbThroughputChange)
|
|
||||||
@Values({
|
|
||||||
label: "Database Throughput",
|
|
||||||
min: 400,
|
|
||||||
max: initializeMaxThroughput,
|
|
||||||
step: 100,
|
|
||||||
uiType: UiType.Slider,
|
|
||||||
})
|
|
||||||
dbThroughput: number;
|
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
label: "Collection Throughput",
|
label: "Collection Throughput",
|
||||||
min: 400,
|
min: 400,
|
||||||
max: initializeMaxThroughput,
|
max: getMaxThroughput,
|
||||||
step: 100,
|
step: 100,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
})
|
})
|
||||||
collectionThroughput: number;
|
collectionThroughput: number;
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this example, the onEnableDbLevelThroughputChange function makes the dbThroughput property visible when
|
||||||
|
enableDbLevelThroughput, a boolean, is set to true and hides dbThroughput property when it is set to false.
|
||||||
|
*/
|
||||||
|
@OnChange(onEnableDbLevelThroughputChange)
|
||||||
|
@Values({
|
||||||
|
label: "Enable DB level throughput",
|
||||||
|
trueLabel: "Enable",
|
||||||
|
falseLabel: "Disable",
|
||||||
|
})
|
||||||
|
enableDbLevelThroughput: boolean;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
label: "Database Throughput",
|
||||||
|
min: 400,
|
||||||
|
max: getMaxThroughput,
|
||||||
|
step: 100,
|
||||||
|
uiType: NumberUiType.Slider,
|
||||||
|
})
|
||||||
|
dbThroughput: number;
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SelfServeDescriptor, SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
||||||
import { InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes";
|
||||||
|
|
||||||
describe("SelfServeComponent", () => {
|
describe("SelfServeComponent", () => {
|
||||||
const defaultValues = new Map<string, InputType>([
|
const defaultValues = new Map<string, SmartUiInput>([
|
||||||
["throughput", "450"],
|
["throughput", { value: 450 }],
|
||||||
["analyticalStore", "false"],
|
["analyticalStore", { value: false }],
|
||||||
["database", "db2"],
|
["database", { value: "db2" }],
|
||||||
]);
|
]);
|
||||||
const initializeMock = jest.fn(async () => defaultValues);
|
const updatedValues = new Map<string, SmartUiInput>([
|
||||||
const onSubmitMock = jest.fn(async () => {
|
["throughput", { value: 460 }],
|
||||||
return;
|
["analyticalStore", { value: true }],
|
||||||
|
["database", { value: "db2" }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const initializeMock = jest.fn(async () => new Map(defaultValues));
|
||||||
|
const onSaveMock = jest.fn(async () => {
|
||||||
|
return { message: "submitted successfully", type: SelfServeNotificationType.info };
|
||||||
|
});
|
||||||
|
const onRefreshMock = jest.fn(async () => {
|
||||||
|
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" };
|
||||||
|
});
|
||||||
|
const onRefreshIsUpdatingMock = jest.fn(async () => {
|
||||||
|
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" };
|
||||||
});
|
});
|
||||||
|
|
||||||
const exampleData: SelfServeDescriptor = {
|
const exampleData: SelfServeDescriptor = {
|
||||||
initialize: initializeMock,
|
initialize: initializeMock,
|
||||||
onSubmit: onSubmitMock,
|
onSave: onSaveMock,
|
||||||
inputNames: ["throughput", "containerId", "analyticalStore", "database"],
|
onRefresh: onRefreshMock,
|
||||||
|
inputNames: ["throughput", "analyticalStore", "database"],
|
||||||
root: {
|
root: {
|
||||||
id: "root",
|
id: "root",
|
||||||
info: {
|
info: {
|
||||||
@ -38,7 +51,7 @@ describe("SelfServeComponent", () => {
|
|||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -78,27 +91,109 @@ describe("SelfServeComponent", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyDefaultsSet = (currentValues: Map<string, InputType>): void => {
|
const isEqual = (source: Map<string, SmartUiInput>, target: Map<string, SmartUiInput>): void => {
|
||||||
for (const key of currentValues.keys()) {
|
expect(target.size).toEqual(source.size);
|
||||||
if (defaultValues.has(key)) {
|
for (const key of source.keys()) {
|
||||||
expect(defaultValues.get(key)).toEqual(currentValues.get(key));
|
expect(target.get(key)).toEqual(source.get(key));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should render", async () => {
|
it("should render and honor save, discard, refresh actions", async () => {
|
||||||
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
// initialize() should be called and defaults should be set when component is mounted
|
// initialize() and onRefresh() should be called and defaults should be set when component is mounted
|
||||||
expect(initializeMock).toHaveBeenCalled();
|
expect(initializeMock).toHaveBeenCalledTimes(1);
|
||||||
const state = wrapper.state() as SelfServeComponentState;
|
expect(onRefreshMock).toHaveBeenCalledTimes(1);
|
||||||
verifyDefaultsSet(state.currentValues);
|
let state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.currentValues, defaultValues);
|
||||||
|
|
||||||
// onSubmit() must be called when submit button is clicked
|
// when currentValues and baselineValues differ, save and discard should not be disabled
|
||||||
const submitButton = wrapper.find("#submitButton");
|
wrapper.setState({ currentValues: updatedValues });
|
||||||
submitButton.simulate("click");
|
wrapper.update();
|
||||||
expect(onSubmitMock).toHaveBeenCalled();
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.currentValues, updatedValues);
|
||||||
|
const selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
expect(selfServeComponent.isSaveButtonDisabled()).toBeFalsy();
|
||||||
|
expect(selfServeComponent.isDiscardButtonDisabled()).toBeFalsy();
|
||||||
|
|
||||||
|
// when errors exist, save is disabled but discard is enabled
|
||||||
|
wrapper.setState({ hasErrors: true });
|
||||||
|
wrapper.update();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
expect(selfServeComponent.isSaveButtonDisabled()).toBeTruthy();
|
||||||
|
expect(selfServeComponent.isDiscardButtonDisabled()).toBeFalsy();
|
||||||
|
|
||||||
|
// discard resets currentValues to baselineValues
|
||||||
|
selfServeComponent.discard();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.currentValues, defaultValues);
|
||||||
|
isEqual(state.currentValues, state.baselineValues);
|
||||||
|
|
||||||
|
// resetBaselineValues sets baselineValues to currentValues
|
||||||
|
wrapper.setState({ baselineValues: updatedValues });
|
||||||
|
wrapper.update();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.baselineValues, updatedValues);
|
||||||
|
selfServeComponent.resetBaselineValues();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.baselineValues, defaultValues);
|
||||||
|
isEqual(state.currentValues, state.baselineValues);
|
||||||
|
|
||||||
|
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well
|
||||||
|
selfServeComponent.onRefreshClicked();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(onRefreshMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(initializeMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
selfServeComponent.onSaveButtonClick();
|
||||||
|
expect(onSaveMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getResolvedValue", async () => {
|
||||||
|
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
|
||||||
|
const numberResult = 1;
|
||||||
|
const numberPromise = async (): Promise<number> => {
|
||||||
|
return numberResult;
|
||||||
|
};
|
||||||
|
expect(await selfServeComponent.getResolvedValue(numberResult)).toEqual(numberResult);
|
||||||
|
expect(await selfServeComponent.getResolvedValue(numberPromise)).toEqual(numberResult);
|
||||||
|
|
||||||
|
const stringResult = "result";
|
||||||
|
const stringPromise = async (): Promise<string> => {
|
||||||
|
return stringResult;
|
||||||
|
};
|
||||||
|
expect(await selfServeComponent.getResolvedValue(stringResult)).toEqual(stringResult);
|
||||||
|
expect(await selfServeComponent.getResolvedValue(stringPromise)).toEqual(stringResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("message bar and spinner snapshots", async () => {
|
||||||
|
const newDescriptor = { ...exampleData, onRefresh: onRefreshIsUpdatingMock };
|
||||||
|
let wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
let selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
selfServeComponent.onSaveButtonClick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
newDescriptor.onRefresh = onRefreshMock;
|
||||||
|
wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
selfServeComponent.onSaveButtonClick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
wrapper.setState({ isInitializing: true });
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
wrapper.setState({ compileErrorMessage: "sample error message" });
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,62 +1,31 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { IStackTokens, PrimaryButton, Spinner, SpinnerSize, Stack } from "office-ui-fabric-react";
|
|
||||||
import {
|
import {
|
||||||
ChoiceItem,
|
CommandBar,
|
||||||
|
ICommandBarItemProps,
|
||||||
|
IStackTokens,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
|
Spinner,
|
||||||
|
SpinnerSize,
|
||||||
|
Stack,
|
||||||
|
} from "office-ui-fabric-react";
|
||||||
|
import {
|
||||||
|
AnyDisplay,
|
||||||
|
Node,
|
||||||
InputType,
|
InputType,
|
||||||
InputTypeValue,
|
RefreshResult,
|
||||||
SmartUiComponent,
|
SelfServeDescriptor,
|
||||||
UiType,
|
SelfServeNotification,
|
||||||
SmartUiDescriptor,
|
SmartUiInput,
|
||||||
Info,
|
DescriptionDisplay,
|
||||||
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
StringInput,
|
||||||
|
NumberInput,
|
||||||
export interface BaseInput {
|
BooleanInput,
|
||||||
label: (() => Promise<string>) | string;
|
ChoiceInput,
|
||||||
dataFieldName: string;
|
SelfServeNotificationType,
|
||||||
type: InputTypeValue;
|
} from "./SelfServeTypes";
|
||||||
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||||
placeholder?: (() => Promise<string>) | string;
|
import { getMessageBarType } from "./SelfServeUtils";
|
||||||
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 {
|
export interface SelfServeComponentProps {
|
||||||
descriptor: SelfServeDescriptor;
|
descriptor: SelfServeDescriptor;
|
||||||
@ -64,13 +33,18 @@ export interface SelfServeComponentProps {
|
|||||||
|
|
||||||
export interface SelfServeComponentState {
|
export interface SelfServeComponentState {
|
||||||
root: SelfServeDescriptor;
|
root: SelfServeDescriptor;
|
||||||
currentValues: Map<string, InputType>;
|
currentValues: Map<string, SmartUiInput>;
|
||||||
baselineValues: Map<string, InputType>;
|
baselineValues: Map<string, SmartUiInput>;
|
||||||
isRefreshing: boolean;
|
isInitializing: boolean;
|
||||||
|
hasErrors: boolean;
|
||||||
|
compileErrorMessage: string;
|
||||||
|
notification: SelfServeNotification;
|
||||||
|
refreshResult: RefreshResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
this.performRefresh();
|
||||||
this.initializeSmartUiComponent();
|
this.initializeSmartUiComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,62 +54,108 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
root: this.props.descriptor,
|
root: this.props.descriptor,
|
||||||
currentValues: new Map(),
|
currentValues: new Map(),
|
||||||
baselineValues: new Map(),
|
baselineValues: new Map(),
|
||||||
isRefreshing: false,
|
isInitializing: true,
|
||||||
|
hasErrors: false,
|
||||||
|
compileErrorMessage: undefined,
|
||||||
|
notification: undefined,
|
||||||
|
refreshResult: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onError = (hasErrors: boolean): void => {
|
||||||
|
this.setState({ hasErrors });
|
||||||
|
};
|
||||||
|
|
||||||
private initializeSmartUiComponent = async (): Promise<void> => {
|
private initializeSmartUiComponent = async (): Promise<void> => {
|
||||||
this.setState({ isRefreshing: true });
|
this.setState({ isInitializing: true });
|
||||||
await this.initializeSmartUiNode(this.props.descriptor.root);
|
|
||||||
await this.setDefaults();
|
await this.setDefaults();
|
||||||
this.setState({ isRefreshing: false });
|
const { currentValues, baselineValues } = this.state;
|
||||||
|
await this.initializeSmartUiNode(this.props.descriptor.root, currentValues, baselineValues);
|
||||||
|
this.setState({ isInitializing: false, currentValues, baselineValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
private setDefaults = async (): Promise<void> => {
|
private setDefaults = async (): Promise<void> => {
|
||||||
this.setState({ isRefreshing: true });
|
|
||||||
let { currentValues, baselineValues } = this.state;
|
let { currentValues, baselineValues } = this.state;
|
||||||
|
|
||||||
const initialValues = await this.props.descriptor.initialize();
|
const initialValues = await this.props.descriptor.initialize();
|
||||||
for (const key of initialValues.keys()) {
|
this.props.descriptor.inputNames.map((inputName) => {
|
||||||
if (this.props.descriptor.inputNames.indexOf(key) === -1) {
|
let initialValue = initialValues.get(inputName);
|
||||||
this.setState({ isRefreshing: false });
|
if (!initialValue) {
|
||||||
throw new Error(`${key} is not an input property of this class.`);
|
initialValue = { value: undefined, hidden: false };
|
||||||
|
}
|
||||||
|
currentValues = currentValues.set(inputName, initialValue);
|
||||||
|
baselineValues = baselineValues.set(inputName, initialValue);
|
||||||
|
initialValues.delete(inputName);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initialValues.size > 0) {
|
||||||
|
const keys = [];
|
||||||
|
for (const key of initialValues.keys()) {
|
||||||
|
keys.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentValues = currentValues.set(key, initialValues.get(key));
|
this.setState({
|
||||||
baselineValues = baselineValues.set(key, initialValues.get(key));
|
compileErrorMessage: `The following fields have default values set but are not input properties of this class: ${keys.join(
|
||||||
|
", "
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.setState({ currentValues, baselineValues, isRefreshing: false });
|
this.setState({ currentValues, baselineValues });
|
||||||
|
};
|
||||||
|
|
||||||
|
public resetBaselineValues = (): void => {
|
||||||
|
const currentValues = this.state.currentValues;
|
||||||
|
let baselineValues = this.state.baselineValues;
|
||||||
|
for (const key of currentValues.keys()) {
|
||||||
|
const currentValue = currentValues.get(key);
|
||||||
|
baselineValues = baselineValues.set(key, { ...currentValue });
|
||||||
|
}
|
||||||
|
this.setState({ baselineValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
public discard = (): void => {
|
public discard = (): void => {
|
||||||
let { currentValues } = this.state;
|
let { currentValues } = this.state;
|
||||||
const { baselineValues } = this.state;
|
const { baselineValues } = this.state;
|
||||||
for (const key of baselineValues.keys()) {
|
for (const key of currentValues.keys()) {
|
||||||
currentValues = currentValues.set(key, baselineValues.get(key));
|
const baselineValue = baselineValues.get(key);
|
||||||
|
currentValues = currentValues.set(key, { ...baselineValue });
|
||||||
}
|
}
|
||||||
this.setState({ currentValues });
|
this.setState({ currentValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeSmartUiNode = async (currentNode: Node): Promise<void> => {
|
private initializeSmartUiNode = async (
|
||||||
|
currentNode: Node,
|
||||||
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: Map<string, SmartUiInput>
|
||||||
|
): Promise<void> => {
|
||||||
currentNode.info = await this.getResolvedValue(currentNode.info);
|
currentNode.info = await this.getResolvedValue(currentNode.info);
|
||||||
|
|
||||||
if (currentNode.input) {
|
if (currentNode.input) {
|
||||||
currentNode.input = await this.getResolvedInput(currentNode.input);
|
currentNode.input = await this.getResolvedInput(currentNode.input, currentValues, baselineValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = currentNode.children?.map(async (child: Node) => await this.initializeSmartUiNode(child));
|
const promises = currentNode.children?.map(
|
||||||
|
async (child: Node) => await this.initializeSmartUiNode(child, currentValues, baselineValues)
|
||||||
|
);
|
||||||
if (promises) {
|
if (promises) {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private getResolvedInput = async (input: AnyInput): Promise<AnyInput> => {
|
private getResolvedInput = async (
|
||||||
|
input: AnyDisplay,
|
||||||
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: Map<string, SmartUiInput>
|
||||||
|
): Promise<AnyDisplay> => {
|
||||||
input.label = await this.getResolvedValue(input.label);
|
input.label = await this.getResolvedValue(input.label);
|
||||||
input.placeholder = await this.getResolvedValue(input.placeholder);
|
input.placeholder = await this.getResolvedValue(input.placeholder);
|
||||||
|
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "string": {
|
case "string": {
|
||||||
|
if ("description" in input) {
|
||||||
|
const descriptionDisplay = input as DescriptionDisplay;
|
||||||
|
descriptionDisplay.description = await this.getResolvedValue(descriptionDisplay.description);
|
||||||
|
}
|
||||||
return input as StringInput;
|
return input as StringInput;
|
||||||
}
|
}
|
||||||
case "number": {
|
case "number": {
|
||||||
@ -143,6 +163,16 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
numberInput.min = await this.getResolvedValue(numberInput.min);
|
numberInput.min = await this.getResolvedValue(numberInput.min);
|
||||||
numberInput.max = await this.getResolvedValue(numberInput.max);
|
numberInput.max = await this.getResolvedValue(numberInput.max);
|
||||||
numberInput.step = await this.getResolvedValue(numberInput.step);
|
numberInput.step = await this.getResolvedValue(numberInput.step);
|
||||||
|
|
||||||
|
const dataFieldName = numberInput.dataFieldName;
|
||||||
|
const defaultValue = currentValues.get(dataFieldName)?.value;
|
||||||
|
|
||||||
|
if (!defaultValue) {
|
||||||
|
const newDefaultValue = { value: numberInput.min, hidden: currentValues.get(dataFieldName)?.hidden };
|
||||||
|
currentValues.set(dataFieldName, newDefaultValue);
|
||||||
|
baselineValues.set(dataFieldName, newDefaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
return numberInput;
|
return numberInput;
|
||||||
}
|
}
|
||||||
case "boolean": {
|
case "boolean": {
|
||||||
@ -166,53 +196,157 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInputChange = (input: AnyInput, newValue: InputType) => {
|
private onInputChange = (input: AnyDisplay, newValue: InputType) => {
|
||||||
if (input.onChange) {
|
if (input.onChange) {
|
||||||
const newValues = input.onChange(this.state.currentValues, newValue);
|
const newValues = input.onChange(this.state.currentValues, newValue);
|
||||||
this.setState({ currentValues: newValues });
|
this.setState({ currentValues: newValues });
|
||||||
} else {
|
} else {
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
const { currentValues } = this.state;
|
const { currentValues } = this.state;
|
||||||
currentValues.set(dataFieldName, newValue);
|
const currentInputValue = currentValues.get(dataFieldName);
|
||||||
|
currentValues.set(dataFieldName, { value: newValue, hidden: currentInputValue?.hidden });
|
||||||
this.setState({ currentValues });
|
this.setState({ currentValues });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public onSaveButtonClick = (): void => {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues);
|
||||||
return !this.state.isRefreshing ? (
|
onSavePromise.catch((error) => {
|
||||||
<div style={{ overflowX: "auto" }}>
|
this.setState({
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
notification: {
|
||||||
<SmartUiComponent
|
message: `Error: ${error.message}`,
|
||||||
descriptor={this.state.root as SmartUiDescriptor}
|
type: SelfServeNotificationType.error,
|
||||||
currentValues={this.state.currentValues}
|
},
|
||||||
onInputChange={this.onInputChange}
|
});
|
||||||
/>
|
});
|
||||||
|
onSavePromise.then((notification: SelfServeNotification) => {
|
||||||
|
this.setState({
|
||||||
|
notification: {
|
||||||
|
message: notification.message,
|
||||||
|
type: notification.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.resetBaselineValues();
|
||||||
|
this.onRefreshClicked();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
public isDiscardButtonDisabled = (): boolean => {
|
||||||
<PrimaryButton
|
for (const key of this.state.currentValues.keys()) {
|
||||||
id="submitButton"
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||||
styles={{ root: { width: 100 } }}
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||||
text="submit"
|
|
||||||
onClick={async () => {
|
if (currentValue !== baselineValue) {
|
||||||
await this.props.descriptor.onSubmit(this.state.currentValues);
|
return false;
|
||||||
this.setDefaults();
|
}
|
||||||
}}
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
public isSaveButtonDisabled = (): boolean => {
|
||||||
|
if (this.state.hasErrors) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const key of this.state.currentValues.keys()) {
|
||||||
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||||
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||||
|
|
||||||
|
if (currentValue !== baselineValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
private performRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
const refreshResult = await this.props.descriptor.onRefresh();
|
||||||
|
this.setState({ refreshResult: { ...refreshResult } });
|
||||||
|
return refreshResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
public onRefreshClicked = async (): Promise<void> => {
|
||||||
|
this.setState({ isInitializing: true });
|
||||||
|
const refreshResult = await this.performRefresh();
|
||||||
|
if (!refreshResult.isUpdateInProgress) {
|
||||||
|
this.initializeSmartUiComponent();
|
||||||
|
}
|
||||||
|
this.setState({ isInitializing: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "save",
|
||||||
|
text: "Save",
|
||||||
|
iconProps: { iconName: "Save" },
|
||||||
|
split: true,
|
||||||
|
disabled: this.isSaveButtonDisabled(),
|
||||||
|
onClick: this.onSaveButtonClick,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "discard",
|
||||||
|
text: "Discard",
|
||||||
|
iconProps: { iconName: "Undo" },
|
||||||
|
split: true,
|
||||||
|
disabled: this.isDiscardButtonDisabled(),
|
||||||
|
onClick: () => {
|
||||||
|
this.discard();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "refresh",
|
||||||
|
text: "Refresh",
|
||||||
|
disabled: this.state.isInitializing,
|
||||||
|
iconProps: { iconName: "Refresh" },
|
||||||
|
split: true,
|
||||||
|
onClick: () => {
|
||||||
|
this.onRefreshClicked();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const containerStackTokens: IStackTokens = { childrenGap: 5 };
|
||||||
|
if (this.state.compileErrorMessage) {
|
||||||
|
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ overflowX: "auto" }}>
|
||||||
|
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
||||||
|
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
|
||||||
|
{this.state.isInitializing ? (
|
||||||
|
<Spinner
|
||||||
|
size={SpinnerSize.large}
|
||||||
|
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
) : (
|
||||||
id="discardButton"
|
<>
|
||||||
styles={{ root: { width: 100 } }}
|
{this.state.refreshResult?.isUpdateInProgress && (
|
||||||
text="discard"
|
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
||||||
onClick={() => this.discard()}
|
{this.state.refreshResult.notificationMessage}
|
||||||
/>
|
</MessageBar>
|
||||||
</Stack>
|
)}
|
||||||
|
{this.state.notification && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={getMessageBarType(this.state.notification.type)}
|
||||||
|
styles={{ root: { width: 400 } }}
|
||||||
|
onDismiss={() => this.setState({ notification: undefined })}
|
||||||
|
>
|
||||||
|
{this.state.notification.message}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
<SmartUiComponent
|
||||||
|
disabled={this.state.refreshResult?.isUpdateInProgress}
|
||||||
|
descriptor={this.state.root as SmartUiDescriptor}
|
||||||
|
currentValues={this.state.currentValues}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onError={this.onError}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<Spinner
|
|
||||||
size={SpinnerSize.large}
|
|
||||||
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ 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 { SelfServeDescriptor, SelfServeComponent } from "./SelfServeComponent";
|
import { SelfServeComponent } from "./SelfServeComponent";
|
||||||
|
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||||
import { SelfServeType } from "./SelfServeUtils";
|
import { SelfServeType } from "./SelfServeUtils";
|
||||||
|
|
||||||
export class SelfServeComponentAdapter implements ReactAdapter {
|
export class SelfServeComponentAdapter implements ReactAdapter {
|
||||||
@ -28,6 +29,10 @@ export class SelfServeComponentAdapter implements ReactAdapter {
|
|||||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||||
return new SelfServeExample.default().toSelfServeDescriptor();
|
return new SelfServeExample.default().toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
|
case SelfServeType.sqlx: {
|
||||||
|
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||||
|
return new SqlX.default().toSelfServeDescriptor();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
130
src/SelfServe/SelfServeTypes.ts
Normal file
130
src/SelfServe/SelfServeTypes.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
interface BaseInput {
|
||||||
|
dataFieldName: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
type: InputTypeValue;
|
||||||
|
label?: (() => Promise<string>) | string;
|
||||||
|
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||||
|
placeholder?: (() => Promise<string>) | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumberInput extends BaseInput {
|
||||||
|
min: (() => Promise<number>) | number;
|
||||||
|
max: (() => Promise<number>) | number;
|
||||||
|
step: (() => Promise<number>) | number;
|
||||||
|
defaultValue?: number;
|
||||||
|
uiType: NumberUiType;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 DescriptionDisplay extends BaseInput {
|
||||||
|
description: (() => Promise<Description>) | Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: string;
|
||||||
|
info?: (() => Promise<Info>) | Info;
|
||||||
|
input?: AnyDisplay;
|
||||||
|
children?: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelfServeDescriptor {
|
||||||
|
root: Node;
|
||||||
|
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||||
|
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||||
|
inputNames?: string[];
|
||||||
|
onRefresh?: () => Promise<RefreshResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||||
|
|
||||||
|
export abstract class SelfServeBaseClass {
|
||||||
|
public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||||
|
public abstract onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||||
|
public abstract onRefresh: () => Promise<RefreshResult>;
|
||||||
|
|
||||||
|
public toSelfServeDescriptor(): SelfServeDescriptor {
|
||||||
|
const className = this.constructor.name;
|
||||||
|
const selfServeDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
|
||||||
|
|
||||||
|
if (!this.initialize) {
|
||||||
|
throw new Error(`initialize() was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
if (!this.onSave) {
|
||||||
|
throw new Error(`onSave() was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
if (!this.onRefresh) {
|
||||||
|
throw new Error(`onRefresh() was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
if (!selfServeDescriptor?.root) {
|
||||||
|
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
selfServeDescriptor.initialize = this.initialize;
|
||||||
|
selfServeDescriptor.onSave = this.onSave;
|
||||||
|
selfServeDescriptor.onRefresh = this.onRefresh;
|
||||||
|
return selfServeDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputTypeValue = "number" | "string" | "boolean" | "object";
|
||||||
|
|
||||||
|
export enum NumberUiType {
|
||||||
|
Spinner = "Spinner",
|
||||||
|
Slider = "Slider",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChoiceItem = { label: string; key: string };
|
||||||
|
|
||||||
|
export type InputType = number | string | boolean | ChoiceItem;
|
||||||
|
|
||||||
|
export interface Info {
|
||||||
|
message: string;
|
||||||
|
link?: {
|
||||||
|
href: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Description {
|
||||||
|
text: string;
|
||||||
|
link?: {
|
||||||
|
href: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmartUiInput {
|
||||||
|
value: InputType;
|
||||||
|
hidden?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SelfServeNotificationType {
|
||||||
|
info = "info",
|
||||||
|
warning = "warning",
|
||||||
|
error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelfServeNotification {
|
||||||
|
message: string;
|
||||||
|
type: SelfServeNotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshResult {
|
||||||
|
isUpdateInProgress: boolean;
|
||||||
|
notificationMessage: string;
|
||||||
|
}
|
@ -1,40 +1,39 @@
|
|||||||
import {
|
import { NumberUiType, RefreshResult, SelfServeBaseClass, SelfServeNotification, SmartUiInput } from "./SelfServeTypes";
|
||||||
CommonInputTypes,
|
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
|
||||||
mapToSmartUiDescriptor,
|
|
||||||
SelfServeBaseClass,
|
|
||||||
updateContextWithDecorator,
|
|
||||||
} from "./SelfServeUtils";
|
|
||||||
import { InputType, UiType } from "./../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
||||||
|
|
||||||
describe("SelfServeUtils", () => {
|
describe("SelfServeUtils", () => {
|
||||||
it("initialize should be declared for self serve classes", () => {
|
it("initialize should be declared for self serve classes", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public onSubmit = async (): Promise<void> => {
|
public initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||||
return;
|
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||||
};
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
public initialize: () => Promise<Map<string, InputType>>;
|
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).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("onSave should be declared for self serve classes", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public onSubmit: () => Promise<void>;
|
public initialize = jest.fn();
|
||||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
public onSave: () => Promise<SelfServeNotification>;
|
||||||
return undefined;
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSubmit() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onRefresh should be declared for self serve classes", () => {
|
||||||
|
class Test extends SelfServeBaseClass {
|
||||||
|
public initialize = jest.fn();
|
||||||
|
public onSave = jest.fn();
|
||||||
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
|
}
|
||||||
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() 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", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public onSubmit = async (): Promise<void> => {
|
public initialize = jest.fn();
|
||||||
return;
|
public onSave = jest.fn();
|
||||||
};
|
public onRefresh = jest.fn();
|
||||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
||||||
"@SmartUi decorator was not declared for the class 'Test'"
|
"@SmartUi decorator was not declared for the class 'Test'"
|
||||||
@ -42,7 +41,7 @@ describe("SelfServeUtils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateContextWithDecorator", () => {
|
it("updateContextWithDecorator", () => {
|
||||||
const context = new Map<string, CommonInputTypes>();
|
const context = new Map<string, DecoratorProperties>();
|
||||||
updateContextWithDecorator(context, "dbThroughput", "testClass", "max", 1);
|
updateContextWithDecorator(context, "dbThroughput", "testClass", "max", 1);
|
||||||
updateContextWithDecorator(context, "dbThroughput", "testClass", "min", 2);
|
updateContextWithDecorator(context, "dbThroughput", "testClass", "min", 2);
|
||||||
updateContextWithDecorator(context, "collThroughput", "testClass", "max", 5);
|
updateContextWithDecorator(context, "collThroughput", "testClass", "max", 5);
|
||||||
@ -52,7 +51,7 @@ describe("SelfServeUtils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("mapToSmartUiDescriptor", () => {
|
it("mapToSmartUiDescriptor", () => {
|
||||||
const context: Map<string, CommonInputTypes> = new Map([
|
const context: Map<string, DecoratorProperties> = new Map([
|
||||||
[
|
[
|
||||||
"dbThroughput",
|
"dbThroughput",
|
||||||
{
|
{
|
||||||
@ -63,7 +62,7 @@ describe("SelfServeUtils", () => {
|
|||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
uiType: UiType.Slider,
|
uiType: NumberUiType.Slider,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -76,7 +75,7 @@ describe("SelfServeUtils", () => {
|
|||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -89,7 +88,7 @@ describe("SelfServeUtils", () => {
|
|||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
errorMessage: "label, truelabel and falselabel are required for boolean input",
|
errorMessage: "label, truelabel and falselabel are required for boolean input",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
|
import { MessageBarType } from "office-ui-fabric-react";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { ChoiceItem, Info, InputTypeValue, InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
||||||
import {
|
import {
|
||||||
|
Node,
|
||||||
|
AnyDisplay,
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
ChoiceInput,
|
ChoiceInput,
|
||||||
SelfServeDescriptor,
|
ChoiceItem,
|
||||||
|
Description,
|
||||||
|
DescriptionDisplay,
|
||||||
|
Info,
|
||||||
|
InputType,
|
||||||
|
InputTypeValue,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
|
SelfServeDescriptor,
|
||||||
|
SmartUiInput,
|
||||||
StringInput,
|
StringInput,
|
||||||
Node,
|
SelfServeNotificationType,
|
||||||
AnyInput,
|
} from "./SelfServeTypes";
|
||||||
} from "./SelfServeComponent";
|
|
||||||
|
|
||||||
export enum SelfServeType {
|
export enum SelfServeType {
|
||||||
// No self serve type passed, launch explorer
|
// No self serve type passed, launch explorer
|
||||||
@ -17,33 +25,10 @@ export enum SelfServeType {
|
|||||||
invalid = "invalid",
|
invalid = "invalid",
|
||||||
// Add your self serve types here
|
// Add your self serve types here
|
||||||
example = "example",
|
example = "example",
|
||||||
|
sqlx = "sqlx",
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class SelfServeBaseClass {
|
export interface DecoratorProperties {
|
||||||
public abstract onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
|
|
||||||
public abstract initialize: () => Promise<Map<string, InputType>>;
|
|
||||||
|
|
||||||
public toSelfServeDescriptor(): SelfServeDescriptor {
|
|
||||||
const className = this.constructor.name;
|
|
||||||
const smartUiDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
|
|
||||||
|
|
||||||
if (!this.initialize) {
|
|
||||||
throw new Error(`initialize() was not declared for the class '${className}'`);
|
|
||||||
}
|
|
||||||
if (!this.onSubmit) {
|
|
||||||
throw new Error(`onSubmit() was not declared for the class '${className}'`);
|
|
||||||
}
|
|
||||||
if (!smartUiDescriptor?.root) {
|
|
||||||
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
smartUiDescriptor.initialize = this.initialize;
|
|
||||||
smartUiDescriptor.onSubmit = this.onSubmit;
|
|
||||||
return smartUiDescriptor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommonInputTypes {
|
|
||||||
id: string;
|
id: string;
|
||||||
info?: (() => Promise<Info>) | Info;
|
info?: (() => Promise<Info>) | Info;
|
||||||
type?: InputTypeValue;
|
type?: InputTypeValue;
|
||||||
@ -58,41 +43,43 @@ export interface CommonInputTypes {
|
|||||||
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||||
uiType?: string;
|
uiType?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
description?: (() => Promise<Description>) | Description;
|
||||||
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||||
initialize?: () => Promise<Map<string, InputType>>;
|
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<void>;
|
||||||
|
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
name: T,
|
name: T,
|
||||||
value: K,
|
value: K,
|
||||||
fieldObject: CommonInputTypes
|
fieldObject: DecoratorProperties
|
||||||
): void => {
|
): void => {
|
||||||
fieldObject[name] = value;
|
fieldObject[name] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValue = <T extends keyof CommonInputTypes>(name: T, fieldObject: CommonInputTypes): unknown => {
|
const getValue = <T extends keyof DecoratorProperties>(name: T, fieldObject: DecoratorProperties): unknown => {
|
||||||
return fieldObject[name];
|
return fieldObject[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addPropertyToMap = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
export const addPropertyToMap = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
target: unknown,
|
target: unknown,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
className: string,
|
className: string,
|
||||||
descriptorName: keyof CommonInputTypes,
|
descriptorName: keyof DecoratorProperties,
|
||||||
descriptorValue: K
|
descriptorValue: K
|
||||||
): void => {
|
): void => {
|
||||||
const context =
|
const context =
|
||||||
(Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>) ?? new Map<string, CommonInputTypes>();
|
(Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>) ??
|
||||||
|
new Map<string, DecoratorProperties>();
|
||||||
updateContextWithDecorator(context, propertyName, className, descriptorName, descriptorValue);
|
updateContextWithDecorator(context, propertyName, className, descriptorName, descriptorValue);
|
||||||
Reflect.defineMetadata(className, context, target);
|
Reflect.defineMetadata(className, context, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateContextWithDecorator = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
export const updateContextWithDecorator = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
context: Map<string, CommonInputTypes>,
|
context: Map<string, DecoratorProperties>,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
className: string,
|
className: string,
|
||||||
descriptorName: keyof CommonInputTypes,
|
descriptorName: keyof DecoratorProperties,
|
||||||
descriptorValue: K
|
descriptorValue: K
|
||||||
): void => {
|
): void => {
|
||||||
if (!(context instanceof Map)) {
|
if (!(context instanceof Map)) {
|
||||||
@ -112,12 +99,12 @@ export const updateContextWithDecorator = <T extends keyof CommonInputTypes, K e
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
|
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
|
||||||
const context = Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>;
|
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
|
||||||
const smartUiDescriptor = mapToSmartUiDescriptor(context);
|
const smartUiDescriptor = mapToSmartUiDescriptor(context);
|
||||||
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>): SelfServeDescriptor => {
|
export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => {
|
||||||
const root = context.get("root");
|
const root = context.get("root");
|
||||||
context.delete("root");
|
context.delete("root");
|
||||||
const inputNames: string[] = [];
|
const inputNames: string[] = [];
|
||||||
@ -140,7 +127,7 @@ export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>):
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addToDescriptor = (
|
const addToDescriptor = (
|
||||||
context: Map<string, CommonInputTypes>,
|
context: Map<string, DecoratorProperties>,
|
||||||
root: Node,
|
root: Node,
|
||||||
key: string,
|
key: string,
|
||||||
inputNames: string[]
|
inputNames: string[]
|
||||||
@ -157,7 +144,7 @@ const addToDescriptor = (
|
|||||||
root.children.push(element);
|
root.children.push(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInput = (value: CommonInputTypes): AnyInput => {
|
const getInput = (value: DecoratorProperties): AnyDisplay => {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case "number":
|
case "number":
|
||||||
if (!value.label || !value.step || !value.uiType || !value.min || !value.max) {
|
if (!value.label || !value.step || !value.uiType || !value.min || !value.max) {
|
||||||
@ -165,6 +152,9 @@ const getInput = (value: CommonInputTypes): AnyInput => {
|
|||||||
}
|
}
|
||||||
return value as NumberInput;
|
return value as NumberInput;
|
||||||
case "string":
|
case "string":
|
||||||
|
if (value.description) {
|
||||||
|
return value as DescriptionDisplay;
|
||||||
|
}
|
||||||
if (!value.label) {
|
if (!value.label) {
|
||||||
value.errorMessage = `label is required for string input '${value.id}'.`;
|
value.errorMessage = `label is required for string input '${value.id}'.`;
|
||||||
}
|
}
|
||||||
@ -181,3 +171,14 @@ const getInput = (value: CommonInputTypes): AnyInput => {
|
|||||||
return value as ChoiceInput;
|
return value as ChoiceInput;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMessageBarType = (type: SelfServeNotificationType): MessageBarType => {
|
||||||
|
switch (type) {
|
||||||
|
case SelfServeNotificationType.info:
|
||||||
|
return MessageBarType.info;
|
||||||
|
case SelfServeNotificationType.warning:
|
||||||
|
return MessageBarType.warning;
|
||||||
|
case SelfServeNotificationType.error:
|
||||||
|
return MessageBarType.error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
33
src/SelfServe/SqlX/SqlX.rp.ts
Normal file
33
src/SelfServe/SqlX/SqlX.rp.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
|
|
||||||
|
export interface DedicatedGatewayResponse {
|
||||||
|
sku: string;
|
||||||
|
instances: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRegionSpecificMinInstances = async (): Promise<number> => {
|
||||||
|
// TODO: write RP call to get min number of instances needed for this region
|
||||||
|
throw new Error("getRegionSpecificMinInstances not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRegionSpecificMaxInstances = async (): Promise<number> => {
|
||||||
|
// TODO: write RP call to get max number of instances needed for this region
|
||||||
|
throw new Error("getRegionSpecificMaxInstances not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDedicatedGatewayProvisioning = async (sku: string, instances: number): Promise<void> => {
|
||||||
|
// TODO: write RP call to update dedicated gateway provisioning
|
||||||
|
throw new Error(
|
||||||
|
`updateDedicatedGatewayProvisioning not implemented. Parameters- sku: ${sku}, instances:${instances}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeDedicatedGatewayProvisioning = async (): Promise<DedicatedGatewayResponse> => {
|
||||||
|
// TODO: write RP call to initialize UI for dedicated gateway provisioning
|
||||||
|
throw new Error("initializeDedicatedGatewayProvisioning not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResult> => {
|
||||||
|
// TODO: write RP call to check if dedicated gateway update has gone through
|
||||||
|
throw new Error("refreshDedicatedGatewayProvisioning not implemented");
|
||||||
|
};
|
97
src/SelfServe/SqlX/SqlX.tsx
Normal file
97
src/SelfServe/SqlX/SqlX.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { IsDisplayable, OnChange, Values } from "../Decorators";
|
||||||
|
import {
|
||||||
|
ChoiceItem,
|
||||||
|
InputType,
|
||||||
|
NumberUiType,
|
||||||
|
RefreshResult,
|
||||||
|
SelfServeBaseClass,
|
||||||
|
SelfServeNotification,
|
||||||
|
SmartUiInput,
|
||||||
|
} from "../SelfServeTypes";
|
||||||
|
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
|
||||||
|
|
||||||
|
const onEnableDedicatedGatewayChange = (
|
||||||
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
newValue: InputType
|
||||||
|
): Map<string, SmartUiInput> => {
|
||||||
|
const sku = currentState.get("sku");
|
||||||
|
const instances = currentState.get("instances");
|
||||||
|
const isSkuHidden = newValue === undefined || !(newValue as boolean);
|
||||||
|
currentState.set("enableDedicatedGateway", { value: newValue });
|
||||||
|
currentState.set("sku", { value: sku.value, hidden: isSkuHidden });
|
||||||
|
currentState.set("instances", { value: instances.value, hidden: isSkuHidden });
|
||||||
|
return currentState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSkus = async (): Promise<ChoiceItem[]> => {
|
||||||
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getSkus not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstancesMin = async (): Promise<number> => {
|
||||||
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getInstancesMin not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstancesMax = async (): Promise<number> => {
|
||||||
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getInstancesMax not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (currentValues: Map<string, SmartUiInput>): void => {
|
||||||
|
// TODO: add cusom validation logic to be called before Saving the data.
|
||||||
|
throw new Error(`validate not implemented. No. of properties to validate: ${currentValues.size}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
@IsDisplayable()
|
||||||
|
export default class SqlX extends SelfServeBaseClass {
|
||||||
|
public onRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
return refreshDedicatedGatewayProvisioning();
|
||||||
|
};
|
||||||
|
|
||||||
|
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||||
|
validate(currentValues);
|
||||||
|
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
|
||||||
|
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||||
|
// TODO: get initialization data from initializeDedicatedGatewayProvisioning() RP call.
|
||||||
|
throw new Error("onSave not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
description: {
|
||||||
|
text: "Provisioning dedicated gateways for SqlX accounts.",
|
||||||
|
link: {
|
||||||
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
|
text: "Learn more about dedicated gateway.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@OnChange(onEnableDedicatedGatewayChange)
|
||||||
|
@Values({
|
||||||
|
label: "Dedicated Gateway",
|
||||||
|
trueLabel: "Enable",
|
||||||
|
falseLabel: "Disable",
|
||||||
|
})
|
||||||
|
enableDedicatedGateway: boolean;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
label: "SKUs",
|
||||||
|
choices: getSkus,
|
||||||
|
placeholder: "Select SKUs",
|
||||||
|
})
|
||||||
|
sku: ChoiceItem;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
label: "Number of instances",
|
||||||
|
min: getInstancesMin,
|
||||||
|
max: getInstancesMax,
|
||||||
|
step: 1,
|
||||||
|
uiType: NumberUiType.Spinner,
|
||||||
|
})
|
||||||
|
instances: number;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SelfServeComponent should render 1`] = `
|
exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
|
||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
@ -13,22 +13,95 @@ exports[`SelfServeComponent should render 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"padding": 10,
|
"padding": 10,
|
||||||
"width": 400,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 20,
|
"childrenGap": 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<StyledCommandBarBase
|
||||||
|
items={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Save",
|
||||||
|
},
|
||||||
|
"key": "save",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Save",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Undo",
|
||||||
|
},
|
||||||
|
"key": "discard",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Discard",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": false,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Refresh",
|
||||||
|
},
|
||||||
|
"key": "refresh",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Refresh",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"paddingLeft": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={0}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
refresh performed successfully
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={0}
|
||||||
|
onDismiss={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
submitted successfully
|
||||||
|
</StyledMessageBarBase>
|
||||||
<SmartUiComponent
|
<SmartUiComponent
|
||||||
currentValues={
|
currentValues={
|
||||||
Map {
|
Map {
|
||||||
"throughput" => "450",
|
"throughput" => Object {
|
||||||
"analyticalStore" => "false",
|
"value": 450,
|
||||||
"database" => "db2",
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
descriptor={
|
descriptor={
|
||||||
@ -36,21 +109,95 @@ exports[`SelfServeComponent should render 1`] = `
|
|||||||
"initialize": [MockFunction] {
|
"initialize": [MockFunction] {
|
||||||
"calls": Array [
|
"calls": Array [
|
||||||
Array [],
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
],
|
],
|
||||||
"results": Array [
|
"results": Array [
|
||||||
Object {
|
Object {
|
||||||
"type": "return",
|
"type": "return",
|
||||||
"value": Promise {},
|
"value": Promise {},
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"inputNames": Array [
|
"inputNames": Array [
|
||||||
"throughput",
|
"throughput",
|
||||||
"containerId",
|
|
||||||
"analyticalStore",
|
"analyticalStore",
|
||||||
"database",
|
"database",
|
||||||
],
|
],
|
||||||
"onSubmit": [MockFunction],
|
"onRefresh": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"onSave": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"children": Array [
|
"children": Array [
|
||||||
Object {
|
Object {
|
||||||
@ -128,41 +275,612 @@ exports[`SelfServeComponent should render 1`] = `
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
disabled={true}
|
||||||
|
onError={[Function]}
|
||||||
|
onInputChange={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflowX": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"padding": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledCommandBarBase
|
||||||
|
items={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Save",
|
||||||
|
},
|
||||||
|
"key": "save",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Save",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Undo",
|
||||||
|
},
|
||||||
|
"key": "discard",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Discard",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": false,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Refresh",
|
||||||
|
},
|
||||||
|
"key": "refresh",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Refresh",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"paddingLeft": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={0}
|
||||||
|
onDismiss={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
submitted successfully
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
<SmartUiComponent
|
||||||
|
currentValues={
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptor={
|
||||||
|
Object {
|
||||||
|
"initialize": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"inputNames": Array [
|
||||||
|
"throughput",
|
||||||
|
"analyticalStore",
|
||||||
|
"database",
|
||||||
|
],
|
||||||
|
"onRefresh": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
Array [],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"onSave": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={false}
|
||||||
|
onError={[Function]}
|
||||||
|
onInputChange={[Function]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflowX": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"padding": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledCommandBarBase
|
||||||
|
items={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Save",
|
||||||
|
},
|
||||||
|
"key": "save",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Save",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Undo",
|
||||||
|
},
|
||||||
|
"key": "discard",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Discard",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Refresh",
|
||||||
|
},
|
||||||
|
"key": "refresh",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Refresh",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"paddingLeft": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledSpinnerBase
|
||||||
|
size={3}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"height": "100%",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"textAlign": "center",
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SelfServeComponent message bar and spinner snapshots 4`] = `
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={1}
|
||||||
|
>
|
||||||
|
sample error message
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = `
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"overflowX": "auto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"padding": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledCommandBarBase
|
||||||
|
items={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Save",
|
||||||
|
},
|
||||||
|
"key": "save",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Save",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": true,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Undo",
|
||||||
|
},
|
||||||
|
"key": "discard",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Discard",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"disabled": false,
|
||||||
|
"iconProps": Object {
|
||||||
|
"iconName": "Refresh",
|
||||||
|
},
|
||||||
|
"key": "refresh",
|
||||||
|
"onClick": [Function],
|
||||||
|
"split": true,
|
||||||
|
"text": "Refresh",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"paddingLeft": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SmartUiComponent
|
||||||
|
currentValues={
|
||||||
|
Map {
|
||||||
|
"throughput" => Object {
|
||||||
|
"value": 450,
|
||||||
|
},
|
||||||
|
"analyticalStore" => Object {
|
||||||
|
"value": false,
|
||||||
|
},
|
||||||
|
"database" => Object {
|
||||||
|
"value": "db2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptor={
|
||||||
|
Object {
|
||||||
|
"initialize": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"inputNames": Array [
|
||||||
|
"throughput",
|
||||||
|
"analyticalStore",
|
||||||
|
"database",
|
||||||
|
],
|
||||||
|
"onRefresh": [MockFunction] {
|
||||||
|
"calls": Array [
|
||||||
|
Array [],
|
||||||
|
],
|
||||||
|
"results": Array [
|
||||||
|
Object {
|
||||||
|
"type": "return",
|
||||||
|
"value": Promise {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"onSave": [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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={false}
|
||||||
|
onError={[Function]}
|
||||||
onInputChange={[Function]}
|
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>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -12,10 +12,27 @@ 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("#regions-dropown-input");
|
|
||||||
await frame.waitForSelector("#enableLogging-radioSwitch-input");
|
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
|
||||||
await frame.waitForSelector("#accountName-textBox-input");
|
await frame.waitForSelector("#description-text-display");
|
||||||
|
|
||||||
|
const regions = await frame.waitForSelector("#regions-dropdown-input");
|
||||||
|
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
|
||||||
|
expect(disabledLoggingToggle).toHaveLength(0);
|
||||||
|
await regions.click();
|
||||||
|
const regionsDropdownElement1 = await frame.waitForSelector("#regions-dropdown-input-list0");
|
||||||
|
await regionsDropdownElement1.click();
|
||||||
|
disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
|
||||||
|
expect(disabledLoggingToggle).toHaveLength(1);
|
||||||
|
|
||||||
|
await frame.waitForSelector("#accountName-textField-input");
|
||||||
|
|
||||||
|
const enableDbLevelThroughput = await frame.waitForSelector("#enableDbLevelThroughput-toggle-input");
|
||||||
|
const dbThroughput = await frame.$$("#dbThroughput-slider-input");
|
||||||
|
expect(dbThroughput).toHaveLength(0);
|
||||||
|
await enableDbLevelThroughput.click();
|
||||||
await frame.waitForSelector("#dbThroughput-slider-input");
|
await frame.waitForSelector("#dbThroughput-slider-input");
|
||||||
|
|
||||||
await frame.waitForSelector("#collectionThroughput-spinner-input");
|
await frame.waitForSelector("#collectionThroughput-spinner-input");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
Loading…
Reference in New Issue
Block a user