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:
Srinath Narayanan 2021-01-26 09:44:14 -08:00 committed by GitHub
parent b0b973b21a
commit 49bf8c60db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2479 additions and 660 deletions

View File

@ -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();
}); });
}); });

View File

@ -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>
);
} }
} }

View File

@ -1,31 +1,24 @@
// 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
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StackItem>
<StyledMessageBarBase
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"padding": 10,
"width": 400, "width": 400,
}, },
} }
} }
tokens={
Object {
"childrenGap": 20,
}
}
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
> >
<StackItem>
<StyledMessageBarBase>
Start at $24/mo per database Start at $24/mo per database
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing" href="https://aka.ms/azure-cosmos-db-pricing"
@ -35,6 +28,33 @@ exports[`SmartUiComponent should render 1`] = `
</StyledLinkBase> </StyledLinkBase>
</StyledMessageBarBase> </StyledMessageBarBase>
</StackItem> </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 <div
key="throughput" key="throughput"
> >
@ -42,11 +62,344 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <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,6 +433,7 @@ exports[`SmartUiComponent should render 1`] = `
} }
} }
/> />
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -90,7 +444,7 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
@ -107,6 +461,9 @@ exports[`SmartUiComponent should render 1`] = `
step={10} step={10}
styles={ styles={
Object { Object {
"root": Object {
"width": 400,
},
"titleLabel": 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",
@ -132,7 +489,7 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
@ -153,7 +510,7 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
@ -162,11 +519,14 @@ exports[`SmartUiComponent should render 1`] = `
className="stringInputContainer" className="stringInputContainer"
> >
<StyledTextFieldBase <StyledTextFieldBase
id="containerId-textBox-input" id="containerId-textField-input"
label="Container id" label="Container id"
onChange={[Function]} onChange={[Function]}
styles={ styles={
Object { Object {
"root": Object {
"width": 400,
},
"subComponentStyles": Object { "subComponentStyles": Object {
"label": Object { "label": Object {
"root": Object { "root": Object {
@ -180,6 +540,7 @@ exports[`SmartUiComponent should render 1`] = `
} }
} }
type="text" type="text"
value=""
/> />
</div> </div>
</StackItem> </StackItem>
@ -192,43 +553,26 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <StackItem>
<div <StyledToggleBase
id="analyticalStore-radioSwitch-input" checked={false}
> id="analyticalStore-toggle-input"
<div label="Analytical Store"
className="inputLabelContainer" offText="Disabled"
> onChange={[Function]}
<Text onText="Enabled"
className="inputLabel" styles={
nowrap={true}
variant="small"
>
Analytical Store
</Text>
</div>
<RadioSwitchComponent
choices={
Array [
Object { Object {
"key": "false", "root": Object {
"label": "Disabled", "width": 400,
"onSelect": [Function],
}, },
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
} }
selectedKey="true" }
/> />
</div>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -239,13 +583,13 @@ exports[`SmartUiComponent should render 1`] = `
className="widgetRendererContainer" className="widgetRendererContainer"
tokens={ tokens={
Object { Object {
"childrenGap": 15, "childrenGap": 10,
} }
} }
> >
<StackItem> <StackItem>
<StyledWithResponsiveMode <StyledWithResponsiveMode
id="database-dropown-input" id="database-dropdown-input"
label="Database" label="Database"
onChange={[Function]} onChange={[Function]}
options={ options={
@ -278,12 +622,14 @@ exports[`SmartUiComponent should render 1`] = `
"fontSize": 12, "fontSize": 12,
"fontWeight": 600, "fontWeight": 600,
}, },
"root": Object {
"width": 400,
},
} }
} }
/> />
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
</Stack>
</Stack> </Stack>
`; `;

View File

@ -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);
};
};

View File

@ -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);
};
};

View 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",
};
};

View File

@ -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;
} }

View File

@ -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();
}); });
}); });

View File

@ -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();
this.props.descriptor.inputNames.map((inputName) => {
let initialValue = initialValues.get(inputName);
if (!initialValue) {
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()) { for (const key of initialValues.keys()) {
if (this.props.descriptor.inputNames.indexOf(key) === -1) { keys.push(key);
this.setState({ isRefreshing: false });
throw new Error(`${key} is not an input property of this class.`);
} }
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;
<PrimaryButton };
id="discardButton"
styles={{ root: { width: 100 } }} public isSaveButtonDisabled = (): boolean => {
text="discard" if (this.state.hasErrors) {
onClick={() => this.discard()} return true;
/> }
</Stack> for (const key of this.state.currentValues.keys()) {
</Stack> const currentValue = JSON.stringify(this.state.currentValues.get(key));
</div> 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 <Spinner
size={SpinnerSize.large} size={SpinnerSize.large}
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }} styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
/> />
) : (
<>
{this.state.refreshResult?.isUpdateInProgress && (
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
{this.state.refreshResult.notificationMessage}
</MessageBar>
)}
{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>
</div>
); );
} }
} }

View File

@ -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;
} }

View 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;
}

View File

@ -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",
}, },
], ],

View File

@ -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;
}
};

View 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");
};

View 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;
}

View File

@ -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>
`; `;

View File

@ -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