mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 06:56:38 +00:00
Added more selfserve changes (#443)
* exposed baselineValues * added getOnSaveNotification * disable UI when onSave is taking place * added optional polling * Added portal notifications * minor edits * added label for description * Added correlationids and polling of refresh * added label tooltip * removed ClassInfo decorator * Added dynamic decription * added info and warninf types for description * promise retry changes * compile errors fixed * merged sqlxEdits * undid sqlx changes * added completed notification * passed retryInterval in notif options * added polling on landing on the page * edits for error display * added link generation * addressed PR comments * modified test * fixed compilation error
This commit is contained in:
parent
c1b74266eb
commit
ecdc41ada9
9
src/Contracts/SelfServeContracts.ts
Normal file
9
src/Contracts/SelfServeContracts.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Messaging types used with SelfServe Component <-> Portal communication
|
||||||
|
* and Hosted <-> SelfServe Component communication
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum SelfServeMessageTypes {
|
||||||
|
TelemetryInfo = "TelemetryInfo",
|
||||||
|
Notification = "Notification",
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
|
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
|
||||||
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
|
import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
|
||||||
|
|
||||||
describe("SmartUiComponent", () => {
|
describe("SmartUiComponent", () => {
|
||||||
const exampleData: SmartUiDescriptor = {
|
const exampleData: SmartUiDescriptor = {
|
||||||
@ -18,10 +18,12 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "description",
|
id: "description",
|
||||||
input: {
|
input: {
|
||||||
|
labelTKey: undefined,
|
||||||
dataFieldName: "description",
|
dataFieldName: "description",
|
||||||
type: "string",
|
type: "string",
|
||||||
description: {
|
description: {
|
||||||
textTKey: "this is an example description text.",
|
textTKey: "this is an example description text.",
|
||||||
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
textTKey: "Click here for more information.",
|
textTKey: "Click here for more information.",
|
||||||
|
@ -6,12 +6,13 @@ 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 { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
||||||
import { Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
|
import { Label, 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 {
|
import {
|
||||||
ChoiceItem,
|
ChoiceItem,
|
||||||
Description,
|
Description,
|
||||||
|
DescriptionType,
|
||||||
Info,
|
Info,
|
||||||
InputType,
|
InputType,
|
||||||
InputTypeValue,
|
InputTypeValue,
|
||||||
@ -19,6 +20,7 @@ import {
|
|||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
} from "../../../SelfServe/SelfServeTypes";
|
} from "../../../SelfServe/SelfServeTypes";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
|
import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic UX renderer
|
* Generic UX renderer
|
||||||
@ -29,15 +31,14 @@ import { TFunction } from "i18next";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface BaseDisplay {
|
interface BaseDisplay {
|
||||||
|
labelTKey: string;
|
||||||
dataFieldName: string;
|
dataFieldName: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
type: InputTypeValue;
|
type: InputTypeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseInput extends BaseDisplay {
|
interface BaseInput extends BaseDisplay {
|
||||||
labelTKey: string;
|
|
||||||
placeholderTKey?: string;
|
placeholderTKey?: string;
|
||||||
errorMessage?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,7 +68,8 @@ interface ChoiceInput extends BaseInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DescriptionDisplay extends BaseDisplay {
|
interface DescriptionDisplay extends BaseDisplay {
|
||||||
description: Description;
|
description?: Description;
|
||||||
|
isDynamicDescription?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||||
@ -123,25 +125,28 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
|
|
||||||
private renderInfo(info: Info): JSX.Element {
|
private renderInfo(info: Info): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MessageBar styles={{ root: { width: 400 } }}>
|
info && (
|
||||||
|
<Text>
|
||||||
{this.props.getTranslation(info.messageTKey)}
|
{this.props.getTranslation(info.messageTKey)}
|
||||||
|
{` `}
|
||||||
{info.link && (
|
{info.link && (
|
||||||
<Link href={info.link.href} target="_blank">
|
<Link href={info.link.href} target="_blank">
|
||||||
{this.props.getTranslation(info.link.textTKey)}
|
{this.props.getTranslation(info.link.textTKey)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</MessageBar>
|
</Text>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderTextInput(input: StringInput): JSX.Element {
|
private renderTextInput(input: StringInput, labelId: string): JSX.Element {
|
||||||
const value = this.props.currentValues.get(input.dataFieldName)?.value 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;
|
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}-textField-input`}
|
id={`${input.dataFieldName}-textField-input`}
|
||||||
label={this.props.getTranslation(input.labelTKey)}
|
aria-labelledby={labelId}
|
||||||
type="text"
|
type="text"
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
placeholder={this.props.getTranslation(input.placeholderTKey)}
|
placeholder={this.props.getTranslation(input.placeholderTKey)}
|
||||||
@ -149,32 +154,36 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
root: { width: 400 },
|
root: { width: 400 },
|
||||||
subComponentStyles: {
|
|
||||||
label: {
|
|
||||||
root: {
|
|
||||||
...SmartUiComponent.labelStyle,
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDescription(input: DescriptionDisplay): JSX.Element {
|
private renderDescription(input: DescriptionDisplay, labelId: string): JSX.Element {
|
||||||
const description = input.description;
|
const dataFieldName = input.dataFieldName;
|
||||||
return (
|
const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
|
||||||
<Text id={`${input.dataFieldName}-text-display`}>
|
if (!description) {
|
||||||
{this.props.getTranslation(input.description.textTKey)}{" "}
|
return this.renderError("Description is not provided.");
|
||||||
|
}
|
||||||
|
const descriptionElement = (
|
||||||
|
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
|
||||||
|
{this.props.getTranslation(description.textTKey)}
|
||||||
|
{` `}
|
||||||
{description.link && (
|
{description.link && (
|
||||||
<Link target="_blank" href={input.description.link.href}>
|
<Link target="_blank" href={description.link.href}>
|
||||||
{this.props.getTranslation(input.description.link.textTKey)}
|
{this.props.getTranslation(description.link.textTKey)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (description.type === DescriptionType.Text) {
|
||||||
|
return descriptionElement;
|
||||||
|
}
|
||||||
|
const messageBarType =
|
||||||
|
description.type === DescriptionType.InfoMessageBar ? MessageBarType.info : MessageBarType.warning;
|
||||||
|
return <MessageBar messageBarType={messageBarType}>{descriptionElement}</MessageBar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearError(dataFieldName: string): void {
|
private clearError(dataFieldName: string): void {
|
||||||
@ -220,13 +229,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderNumberInput(input: NumberInput): JSX.Element {
|
private renderNumberInput(input: NumberInput, labelId: string): JSX.Element {
|
||||||
const { labelTKey, min, max, dataFieldName, step } = input;
|
const { labelTKey, min, max, dataFieldName, step } = input;
|
||||||
const props = {
|
const props = {
|
||||||
label: this.props.getTranslation(labelTKey),
|
|
||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
ariaLabel: labelTKey,
|
ariaLabel: this.props.getTranslation(labelTKey),
|
||||||
step: step,
|
step: step,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -243,13 +251,8 @@ 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}
|
||||||
|
aria-labelledby={labelId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
styles={{
|
|
||||||
label: {
|
|
||||||
...SmartUiComponent.labelStyle,
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{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>
|
||||||
@ -266,10 +269,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
root: { width: 400 },
|
root: { width: 400 },
|
||||||
titleLabel: {
|
|
||||||
...SmartUiComponent.labelStyle,
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
valueLabel: SmartUiComponent.labelStyle,
|
valueLabel: SmartUiComponent.labelStyle,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -280,13 +279,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
private renderBooleanInput(input: BooleanInput, labelId: string): JSX.Element {
|
||||||
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
|
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
|
||||||
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
|
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
|
||||||
return (
|
return (
|
||||||
<Toggle
|
<Toggle
|
||||||
id={`${input.dataFieldName}-toggle-input`}
|
id={`${input.dataFieldName}-toggle-input`}
|
||||||
label={this.props.getTranslation(input.labelTKey)}
|
aria-labelledby={labelId}
|
||||||
checked={value || false}
|
checked={value || false}
|
||||||
onText={this.props.getTranslation(input.trueLabelTKey)}
|
onText={this.props.getTranslation(input.trueLabelTKey)}
|
||||||
offText={this.props.getTranslation(input.falseLabelTKey)}
|
offText={this.props.getTranslation(input.falseLabelTKey)}
|
||||||
@ -297,8 +296,8 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderChoiceInput(input: ChoiceInput): JSX.Element {
|
private renderChoiceInput(input: ChoiceInput, labelId: string): JSX.Element {
|
||||||
const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = input;
|
const { defaultKey, dataFieldName, choices, placeholderTKey } = input;
|
||||||
const value = this.props.currentValues.get(dataFieldName)?.value as string;
|
const value = this.props.currentValues.get(dataFieldName)?.value as string;
|
||||||
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||||
let selectedKey = value ? value : defaultKey;
|
let selectedKey = value ? value : defaultKey;
|
||||||
@ -308,7 +307,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
id={`${input.dataFieldName}-dropdown-input`}
|
id={`${input.dataFieldName}-dropdown-input`}
|
||||||
label={this.props.getTranslation(labelTKey)}
|
aria-labelledby={labelId}
|
||||||
selectedKey={selectedKey}
|
selectedKey={selectedKey}
|
||||||
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||||
placeholder={this.props.getTranslation(placeholderTKey)}
|
placeholder={this.props.getTranslation(placeholderTKey)}
|
||||||
@ -319,40 +318,53 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}))}
|
}))}
|
||||||
styles={{
|
styles={{
|
||||||
root: { width: 400 },
|
root: { width: 400 },
|
||||||
label: {
|
|
||||||
...SmartUiComponent.labelStyle,
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
dropdown: SmartUiComponent.labelStyle,
|
dropdown: SmartUiComponent.labelStyle,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderError(input: AnyDisplay): JSX.Element {
|
private renderError(errorMessage: string): JSX.Element {
|
||||||
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
|
return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDisplay(input: AnyDisplay): JSX.Element {
|
private renderDisplayWithInfoBubble(input: AnyDisplay, info: Info): JSX.Element {
|
||||||
if (input.errorMessage) {
|
if (input.errorMessage) {
|
||||||
return this.renderError(input);
|
return this.renderError(input.errorMessage);
|
||||||
}
|
}
|
||||||
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
|
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
|
||||||
if (inputHidden) {
|
if (inputHidden) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
const labelId = `${input.dataFieldName}-label`;
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{input.labelTKey && (
|
||||||
|
<Label id={labelId}>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label={this.props.getTranslation(input.labelTKey)}
|
||||||
|
toolTipElement={this.renderInfo(info)}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
{this.renderDisplay(input, labelId)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDisplay(input: AnyDisplay, labelId: string): JSX.Element {
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "string":
|
case "string":
|
||||||
if ("description" in input) {
|
if ("description" in input || "isDynamicDescription" in input) {
|
||||||
return this.renderDescription(input as DescriptionDisplay);
|
return this.renderDescription(input as DescriptionDisplay, labelId);
|
||||||
}
|
}
|
||||||
return this.renderTextInput(input as StringInput);
|
return this.renderTextInput(input as StringInput, labelId);
|
||||||
case "number":
|
case "number":
|
||||||
return this.renderNumberInput(input as NumberInput);
|
return this.renderNumberInput(input as NumberInput, labelId);
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return this.renderBooleanInput(input as BooleanInput);
|
return this.renderBooleanInput(input as BooleanInput, labelId);
|
||||||
case "object":
|
case "object":
|
||||||
return this.renderChoiceInput(input as ChoiceInput);
|
return this.renderChoiceInput(input as ChoiceInput, labelId);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown input type: ${input.type}`);
|
throw new Error(`Unknown input type: ${input.type}`);
|
||||||
}
|
}
|
||||||
@ -363,10 +375,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
||||||
<Stack.Item>
|
<Stack.Item>{node.input && this.renderDisplayWithInfoBubble(node.input, node.info as Info)}</Stack.Item>
|
||||||
{node.info && this.renderInfo(node.info as Info)}
|
|
||||||
{node.input && this.renderDisplay(node.input)}
|
|
||||||
</Stack.Item>
|
|
||||||
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
|
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
@ -9,25 +9,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<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
|
<div
|
||||||
key="description"
|
key="description"
|
||||||
>
|
>
|
||||||
@ -40,7 +22,9 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
<Text
|
<Text
|
||||||
|
aria-labelledby="description-label"
|
||||||
id="description-text-display"
|
id="description-text-display"
|
||||||
>
|
>
|
||||||
this is an example description text.
|
this is an example description text.
|
||||||
@ -52,6 +36,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
Click here for more information.
|
Click here for more information.
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -67,6 +52,14 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="throughput-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Throughput (input)"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<Stack
|
<Stack
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
@ -82,6 +75,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CustomizedSpinButton
|
<CustomizedSpinButton
|
||||||
|
aria-labelledby="throughput-label"
|
||||||
ariaLabel="Throughput (input)"
|
ariaLabel="Throughput (input)"
|
||||||
decrementButtonIcon={
|
decrementButtonIcon={
|
||||||
Object {
|
Object {
|
||||||
@ -95,7 +89,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
"iconName": "ChevronUpSmall",
|
"iconName": "ChevronUpSmall",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
label="Throughput (input)"
|
label=""
|
||||||
labelPosition={0}
|
labelPosition={0}
|
||||||
max={500}
|
max={500}
|
||||||
min={400}
|
min={400}
|
||||||
@ -103,18 +97,9 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
onIncrement={[Function]}
|
onIncrement={[Function]}
|
||||||
onValidate={[Function]}
|
onValidate={[Function]}
|
||||||
step={10}
|
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>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -130,13 +115,20 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="throughput2-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Throughput (Slider)"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<div
|
<div
|
||||||
id="throughput2-slider-input"
|
id="throughput2-slider-input"
|
||||||
>
|
>
|
||||||
<StyledSliderBase
|
<StyledSliderBase
|
||||||
ariaLabel="Throughput (Slider)"
|
ariaLabel="Throughput (Slider)"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
label="Throughput (Slider)"
|
|
||||||
max={500}
|
max={500}
|
||||||
min={400}
|
min={400}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@ -146,12 +138,6 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
"root": Object {
|
"root": Object {
|
||||||
"width": 400,
|
"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 {
|
"valueLabel": 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",
|
||||||
@ -161,6 +147,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -197,35 +184,34 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="containerId-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Container id"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<div
|
<div
|
||||||
className="stringInputContainer"
|
className="stringInputContainer"
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
aria-labelledby="containerId-label"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
id="containerId-textField-input"
|
id="containerId-textField-input"
|
||||||
label="Container id"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"width": 400,
|
"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"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -241,11 +227,19 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="analyticalStore-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Analytical Store"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<StyledToggleBase
|
<StyledToggleBase
|
||||||
|
aria-labelledby="analyticalStore-label"
|
||||||
checked={false}
|
checked={false}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
id="analyticalStore-toggle-input"
|
id="analyticalStore-toggle-input"
|
||||||
label="Analytical Store"
|
|
||||||
offText="Disabled"
|
offText="Disabled"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onText="Enabled"
|
onText="Enabled"
|
||||||
@ -257,6 +251,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -272,10 +267,18 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="database-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Database"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<StyledWithResponsiveMode
|
<StyledWithResponsiveMode
|
||||||
|
aria-labelledby="database-label"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
id="database-dropdown-input"
|
id="database-dropdown-input"
|
||||||
label="Database"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
Array [
|
Array [
|
||||||
@ -301,18 +304,13 @@ exports[`SmartUiComponent disable all inputs 1`] = `
|
|||||||
"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,
|
||||||
},
|
},
|
||||||
"label": Object {
|
|
||||||
"color": "#393939",
|
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
|
||||||
"fontSize": 12,
|
|
||||||
"fontWeight": 600,
|
|
||||||
},
|
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -328,25 +326,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<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
|
<div
|
||||||
key="description"
|
key="description"
|
||||||
>
|
>
|
||||||
@ -359,7 +339,9 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
<Text
|
<Text
|
||||||
|
aria-labelledby="description-label"
|
||||||
id="description-text-display"
|
id="description-text-display"
|
||||||
>
|
>
|
||||||
this is an example description text.
|
this is an example description text.
|
||||||
@ -371,6 +353,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
Click here for more information.
|
Click here for more information.
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -386,6 +369,14 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="throughput-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Throughput (input)"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<Stack
|
<Stack
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
@ -401,6 +392,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CustomizedSpinButton
|
<CustomizedSpinButton
|
||||||
|
aria-labelledby="throughput-label"
|
||||||
ariaLabel="Throughput (input)"
|
ariaLabel="Throughput (input)"
|
||||||
decrementButtonIcon={
|
decrementButtonIcon={
|
||||||
Object {
|
Object {
|
||||||
@ -414,7 +406,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
"iconName": "ChevronUpSmall",
|
"iconName": "ChevronUpSmall",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
label="Throughput (input)"
|
label=""
|
||||||
labelPosition={0}
|
labelPosition={0}
|
||||||
max={500}
|
max={500}
|
||||||
min={400}
|
min={400}
|
||||||
@ -422,18 +414,9 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
onIncrement={[Function]}
|
onIncrement={[Function]}
|
||||||
onValidate={[Function]}
|
onValidate={[Function]}
|
||||||
step={10}
|
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>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -449,12 +432,19 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="throughput2-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Throughput (Slider)"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<div
|
<div
|
||||||
id="throughput2-slider-input"
|
id="throughput2-slider-input"
|
||||||
>
|
>
|
||||||
<StyledSliderBase
|
<StyledSliderBase
|
||||||
ariaLabel="Throughput (Slider)"
|
ariaLabel="Throughput (Slider)"
|
||||||
label="Throughput (Slider)"
|
|
||||||
max={500}
|
max={500}
|
||||||
min={400}
|
min={400}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
@ -464,12 +454,6 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
"root": Object {
|
"root": Object {
|
||||||
"width": 400,
|
"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 {
|
"valueLabel": 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",
|
||||||
@ -479,6 +463,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -515,34 +500,33 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="containerId-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Container id"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<div
|
<div
|
||||||
className="stringInputContainer"
|
className="stringInputContainer"
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
|
aria-labelledby="containerId-label"
|
||||||
id="containerId-textField-input"
|
id="containerId-textField-input"
|
||||||
label="Container id"
|
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"width": 400,
|
"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"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -558,10 +542,18 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack>
|
||||||
|
<StyledLabelBase
|
||||||
|
id="analyticalStore-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
|
label="Analytical Store"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
<StyledToggleBase
|
<StyledToggleBase
|
||||||
|
aria-labelledby="analyticalStore-label"
|
||||||
checked={false}
|
checked={false}
|
||||||
id="analyticalStore-toggle-input"
|
id="analyticalStore-toggle-input"
|
||||||
label="Analytical Store"
|
|
||||||
offText="Disabled"
|
offText="Disabled"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
onText="Enabled"
|
onText="Enabled"
|
||||||
@ -573,6 +565,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@ -588,9 +581,17 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledWithResponsiveMode
|
<Stack>
|
||||||
id="database-dropdown-input"
|
<StyledLabelBase
|
||||||
|
id="database-label"
|
||||||
|
>
|
||||||
|
<ToolTipLabelComponent
|
||||||
label="Database"
|
label="Database"
|
||||||
|
/>
|
||||||
|
</StyledLabelBase>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
aria-labelledby="database-label"
|
||||||
|
id="database-dropdown-input"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
Array [
|
Array [
|
||||||
@ -616,18 +617,13 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
|
|||||||
"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,
|
||||||
},
|
},
|
||||||
"label": Object {
|
|
||||||
"color": "#393939",
|
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
|
||||||
"fontSize": 12,
|
|
||||||
"fontWeight": 600,
|
|
||||||
},
|
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"width": 400,
|
"width": 400,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,9 +9,11 @@
|
|||||||
"North Central US": "North Central US",
|
"North Central US": "North Central US",
|
||||||
"West US": "West US",
|
"West US": "West US",
|
||||||
"East US 2": "East US 2",
|
"East US 2": "East US 2",
|
||||||
"ClassInfo": "This is a self serve class",
|
"Current Region": "Current Region",
|
||||||
"RegionDropdownInfo": "More regions can be added in the future.",
|
"RegionDropdownInfo": "More regions can be added in the future.",
|
||||||
"ValidationError": "Regions and AccountName should not be empty.",
|
"RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
|
||||||
|
"DbThroughputValidationError": "Please update throughput for database.",
|
||||||
|
"DescriptionLabel": "Description",
|
||||||
"DescriptionText": "This class sets collection and database throughput.",
|
"DescriptionText": "This class sets collection and database throughput.",
|
||||||
"DecriptionLinkText": "Click here for more information",
|
"DecriptionLinkText": "Click here for more information",
|
||||||
"Regions": "Regions",
|
"Regions": "Regions",
|
||||||
@ -22,10 +24,17 @@
|
|||||||
"Account Name": "Account Name",
|
"Account Name": "Account Name",
|
||||||
"AccountNamePlaceHolder": "Enter the account name",
|
"AccountNamePlaceHolder": "Enter the account name",
|
||||||
"Collection Throughput": "Collection Throughput",
|
"Collection Throughput": "Collection Throughput",
|
||||||
"Enable DB level throughput": "Enable DB level throughput",
|
"Enable DB level throughput": "Enable Database Level Throughput",
|
||||||
"Database Throughput": "Database Throughput",
|
"Database Throughput": "Database Throughput",
|
||||||
"RefreshMessage": "Self Serve Example successfully refreshing",
|
"UpdateInProgressMessage": "Data is being updated",
|
||||||
"SubmissionMessage": "Submitted successfully"
|
"UpdateCompletedMessageTitle":"Update succeeded",
|
||||||
|
"UpdateCompletedMessageText": "Data updation completed.",
|
||||||
|
"SubmissionMessageSuccessTitle": "Update started",
|
||||||
|
"SubmissionMessageForNewRegionText": "Data update started. Region changed.",
|
||||||
|
"SubmissionMessageForSameRegionText": "Data update started. Region not changed.",
|
||||||
|
"SubmissionMessageErrorTitle": "Data update failed",
|
||||||
|
"SubmissionMessageErrorText": "Data update failed because of errors.",
|
||||||
|
"OnSaveFailureMessage": "Data save operation not currently permitted."
|
||||||
},
|
},
|
||||||
"SqlX": {
|
"SqlX": {
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes";
|
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput, RefreshParams } from "./SelfServeTypes";
|
||||||
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
|
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
@ -33,7 +33,9 @@ export interface ChoiceInputOptions extends InputOptionsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DescriptionDisplayOptions {
|
export interface DescriptionDisplayOptions {
|
||||||
|
labelTKey?: string;
|
||||||
description?: (() => Promise<Description>) | Description;
|
description?: (() => Promise<Description>) | Description;
|
||||||
|
isDynamicDescription?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputOptions =
|
type InputOptions =
|
||||||
@ -56,7 +58,7 @@ const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is Choic
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
|
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
|
||||||
return "description" in inputOptions;
|
return "description" in inputOptions || "isDynamicDescription" in inputOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||||
@ -80,7 +82,11 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const OnChange = (
|
export const OnChange = (
|
||||||
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>
|
onChange: (
|
||||||
|
newValue: InputType,
|
||||||
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
) => Map<string, SmartUiInput>
|
||||||
): PropertyDecorator => {
|
): PropertyDecorator => {
|
||||||
return addToMap({ name: "onChange", value: onChange });
|
return addToMap({ name: "onChange", value: onChange });
|
||||||
};
|
};
|
||||||
@ -111,7 +117,11 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
|||||||
{ name: "choices", value: inputOptions.choices }
|
{ name: "choices", value: inputOptions.choices }
|
||||||
);
|
);
|
||||||
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
||||||
return addToMap({ name: "description", value: inputOptions.description });
|
return addToMap(
|
||||||
|
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||||
|
{ name: "description", value: inputOptions.description },
|
||||||
|
{ name: "isDynamicDescription", value: inputOptions.isDynamicDescription }
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||||
@ -126,8 +136,8 @@ export const IsDisplayable = (): ClassDecorator => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
|
||||||
return (target) => {
|
return (target) => {
|
||||||
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -64,13 +64,20 @@ export const initialize = async (): Promise<InitializeResponse> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
||||||
|
const refreshCountString = SessionStorageUtility.getEntry("refreshCount");
|
||||||
|
const refreshCount = refreshCountString ? parseInt(refreshCountString) : 0;
|
||||||
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const databaseAccountName = userContext.databaseAccount.name;
|
const databaseAccountName = userContext.databaseAccount.name;
|
||||||
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
|
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
|
||||||
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
||||||
|
|
||||||
|
const progressToBeSent = refreshCount % 5 === 0 ? isUpdateInProgress : true;
|
||||||
|
SessionStorageUtility.setEntry("refreshCount", (refreshCount + 1).toString());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isUpdateInProgress: isUpdateInProgress,
|
isUpdateInProgress: progressToBeSent,
|
||||||
notificationMessage: "RefreshMessage",
|
updateInProgressMessageTKey: "UpdateInProgressMessage",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators";
|
import { PropertyInfo, OnChange, Values, IsDisplayable, RefreshOptions } from "../Decorators";
|
||||||
import {
|
import {
|
||||||
ChoiceItem,
|
ChoiceItem,
|
||||||
|
Description,
|
||||||
|
DescriptionType,
|
||||||
Info,
|
Info,
|
||||||
InputType,
|
InputType,
|
||||||
NumberUiType,
|
NumberUiType,
|
||||||
|
OnSaveResult,
|
||||||
RefreshResult,
|
RefreshResult,
|
||||||
SelfServeBaseClass,
|
SelfServeBaseClass,
|
||||||
SelfServeNotification,
|
|
||||||
SelfServeNotificationType,
|
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
} from "../SelfServeTypes";
|
} from "../SelfServeTypes";
|
||||||
import {
|
import {
|
||||||
@ -27,16 +28,19 @@ const regionDropdownItems: ChoiceItem[] = [
|
|||||||
{ label: "East US 2", key: Regions.EastUS2 },
|
{ label: "East US 2", key: Regions.EastUS2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const selfServeExampleInfo: Info = {
|
|
||||||
messageTKey: "ClassInfo",
|
|
||||||
};
|
|
||||||
|
|
||||||
const regionDropdownInfo: Info = {
|
const regionDropdownInfo: Info = {
|
||||||
messageTKey: "RegionDropdownInfo",
|
messageTKey: "RegionDropdownInfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
|
const onRegionsChange = (newValue: InputType, currentState: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||||
currentState.set("regions", { value: newValue });
|
currentState.set("regions", { value: newValue });
|
||||||
|
|
||||||
|
const currentRegionText = `current region selected is ${newValue}`;
|
||||||
|
currentState.set("currentRegionText", {
|
||||||
|
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
|
|
||||||
const currentEnableLogging = currentState.get("enableLogging");
|
const currentEnableLogging = currentState.get("enableLogging");
|
||||||
if (newValue === Regions.NorthCentralUS) {
|
if (newValue === Regions.NorthCentralUS) {
|
||||||
currentState.set("enableLogging", { value: false, disabled: true });
|
currentState.set("enableLogging", { value: false, disabled: true });
|
||||||
@ -47,8 +51,8 @@ const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: Inpu
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onEnableDbLevelThroughputChange = (
|
const onEnableDbLevelThroughputChange = (
|
||||||
currentState: Map<string, SmartUiInput>,
|
newValue: InputType,
|
||||||
newValue: InputType
|
currentState: Map<string, SmartUiInput>
|
||||||
): Map<string, SmartUiInput> => {
|
): Map<string, SmartUiInput> => {
|
||||||
currentState.set("enableDbLevelThroughput", { value: newValue });
|
currentState.set("enableDbLevelThroughput", { value: newValue });
|
||||||
const currentDbThroughput = currentState.get("dbThroughput");
|
const currentDbThroughput = currentState.get("dbThroughput");
|
||||||
@ -57,9 +61,15 @@ const onEnableDbLevelThroughputChange = (
|
|||||||
return currentState;
|
return currentState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
const validate = (
|
||||||
|
currentvalues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
): void => {
|
||||||
|
if (currentvalues.get("dbThroughput") === baselineValues.get("dbThroughput")) {
|
||||||
|
throw new Error("DbThroughputValidationError");
|
||||||
|
}
|
||||||
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
||||||
throw new Error("ValidationError");
|
throw new Error("RegionsAndAccountNameValidationError");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,12 +96,12 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
|||||||
*/
|
*/
|
||||||
@IsDisplayable()
|
@IsDisplayable()
|
||||||
/*
|
/*
|
||||||
@ClassInfo()
|
@RefreshOptions()
|
||||||
- optional
|
- role: Passes the refresh options to be used by the self serve model.
|
||||||
- input: Info | () => Promise<Info>
|
- inputs:
|
||||||
- role: Display an Info bar as the first element of the UI.
|
retryIntervalInMs - The time interval between refresh attempts when an update in ongoing.
|
||||||
*/
|
*/
|
||||||
@ClassInfo(selfServeExampleInfo)
|
@RefreshOptions({ retryIntervalInMs: 2000 })
|
||||||
export default class SelfServeExample extends SelfServeBaseClass {
|
export default class SelfServeExample extends SelfServeBaseClass {
|
||||||
/*
|
/*
|
||||||
onRefresh()
|
onRefresh()
|
||||||
@ -109,18 +119,21 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
onSave()
|
onSave()
|
||||||
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
- input: (currentValues: Map<string, InputType>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Promise<string>
|
||||||
- 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 onSave 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. It uses the currentValues and baselineValues maps to perform custom validations
|
||||||
- returns: SelfServeNotification -
|
as well.
|
||||||
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)
|
- returns: The initialize, success and failure messages to be displayed in the Portal Notification blade after the operation is completed.
|
||||||
*/
|
*/
|
||||||
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
public onSave = async (
|
||||||
validate(currentValues);
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
): Promise<OnSaveResult> => {
|
||||||
|
validate(currentValues, baselineValues);
|
||||||
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
|
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
|
||||||
const enableLogging = currentValues.get("enableLogging")?.value as boolean;
|
const enableLogging = currentValues.get("enableLogging")?.value as boolean;
|
||||||
const accountName = currentValues.get("accountName")?.value as string;
|
const accountName = currentValues.get("accountName")?.value as string;
|
||||||
@ -128,8 +141,48 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
|
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
|
||||||
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
||||||
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
||||||
|
try {
|
||||||
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
||||||
return { message: "SubmissionMessage", type: SelfServeNotificationType.info };
|
if (currentValues.get("regions") === baselineValues.get("regions")) {
|
||||||
|
return {
|
||||||
|
operationStatusUrl: undefined,
|
||||||
|
portalNotification: {
|
||||||
|
initialize: {
|
||||||
|
titleTKey: "SubmissionMessageSuccessTitle",
|
||||||
|
messageTKey: "SubmissionMessageForSameRegionText",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
titleTKey: "UpdateCompletedMessageTitle",
|
||||||
|
messageTKey: "UpdateCompletedMessageText",
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
titleTKey: "SubmissionMessageErrorTitle",
|
||||||
|
messageTKey: "SubmissionMessageErrorText",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
operationStatusUrl: undefined,
|
||||||
|
portalNotification: {
|
||||||
|
initialize: {
|
||||||
|
titleTKey: "SubmissionMessageSuccessTitle",
|
||||||
|
messageTKey: "SubmissionMessageForNewRegionText",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
titleTKey: "UpdateCompletedMessageTitle",
|
||||||
|
messageTKey: "UpdateCompletedMessageText",
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
titleTKey: "SubmissionMessageErrorTitle",
|
||||||
|
messageTKey: "SubmissionMessageErrorText",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("OnSaveFailureMessage");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -150,6 +203,11 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||||
const initializeResponse = await initialize();
|
const initializeResponse = await initialize();
|
||||||
const defaults = new Map<string, SmartUiInput>();
|
const defaults = new Map<string, SmartUiInput>();
|
||||||
|
const currentRegionText = `current region selected is ${initializeResponse.regions}`;
|
||||||
|
defaults.set("currentRegionText", {
|
||||||
|
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
defaults.set("regions", { value: initializeResponse.regions });
|
defaults.set("regions", { value: initializeResponse.regions });
|
||||||
defaults.set("enableLogging", { value: initializeResponse.enableLogging });
|
defaults.set("enableLogging", { value: initializeResponse.enableLogging });
|
||||||
const accountName = initializeResponse.accountName;
|
const accountName = initializeResponse.accountName;
|
||||||
@ -172,15 +230,24 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
e) Text (with optional hyperlink) for descriptions
|
e) Text (with optional hyperlink) for descriptions
|
||||||
*/
|
*/
|
||||||
@Values({
|
@Values({
|
||||||
|
labelTKey: "DescriptionLabel",
|
||||||
description: {
|
description: {
|
||||||
textTKey: "DescriptionText",
|
textTKey: "DescriptionText",
|
||||||
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
href: "https://aka.ms/cosmos-create-account-portal",
|
||||||
textTKey: "DecriptionLinkText",
|
textTKey: "DecriptionLinkText",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
labelTKey: "Current Region",
|
||||||
|
isDynamicDescription: true,
|
||||||
|
})
|
||||||
|
currentRegionText: string;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@PropertyInfo()
|
@PropertyInfo()
|
||||||
- optional
|
- optional
|
||||||
@ -192,8 +259,8 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
/*
|
/*
|
||||||
@OnChange()
|
@OnChange()
|
||||||
- optional
|
- optional
|
||||||
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
- input: (currentValues: Map<string, InputType>, newValue: InputType, baselineValues: ReadonlyMap<string, SmartUiInput>) => Map<string, InputType>
|
||||||
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property,
|
- role: Takes a Map of current values, the newValue for this property and a ReadonlyMap of baselineValues as inputs. This is called when a property,
|
||||||
say prop1, changes its value in the UI. This can be used to
|
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.
|
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
|
b) Change the visibility for prop2 in the UI, based on prop1
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
||||||
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes";
|
import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
|
||||||
|
|
||||||
describe("SelfServeComponent", () => {
|
describe("SelfServeComponent", () => {
|
||||||
const defaultValues = new Map<string, SmartUiInput>([
|
const defaultValues = new Map<string, SmartUiInput>([
|
||||||
@ -17,13 +17,20 @@ describe("SelfServeComponent", () => {
|
|||||||
|
|
||||||
const initializeMock = jest.fn(async () => new Map(defaultValues));
|
const initializeMock = jest.fn(async () => new Map(defaultValues));
|
||||||
const onSaveMock = jest.fn(async () => {
|
const onSaveMock = jest.fn(async () => {
|
||||||
return { message: "submitted successfully", type: SelfServeNotificationType.info };
|
return {
|
||||||
|
operationStatusUrl: undefined,
|
||||||
|
} as OnSaveResult;
|
||||||
});
|
});
|
||||||
|
const refreshResult = {
|
||||||
|
isUpdateInProgress: false,
|
||||||
|
updateInProgressMessageTKey: "refresh performed successfully",
|
||||||
|
};
|
||||||
|
|
||||||
const onRefreshMock = jest.fn(async () => {
|
const onRefreshMock = jest.fn(async () => {
|
||||||
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" };
|
return { ...refreshResult };
|
||||||
});
|
});
|
||||||
const onRefreshIsUpdatingMock = jest.fn(async () => {
|
const onRefreshIsUpdatingMock = jest.fn(async () => {
|
||||||
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" };
|
return { ...refreshResult, isUpdateInProgress: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
const exampleData: SelfServeDescriptor = {
|
const exampleData: SelfServeDescriptor = {
|
||||||
@ -136,16 +143,15 @@ describe("SelfServeComponent", () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
state = wrapper.state() as SelfServeComponentState;
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
isEqual(state.baselineValues, updatedValues);
|
isEqual(state.baselineValues, updatedValues);
|
||||||
selfServeComponent.resetBaselineValues();
|
selfServeComponent.updateBaselineValues();
|
||||||
state = wrapper.state() as SelfServeComponentState;
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
isEqual(state.baselineValues, defaultValues);
|
isEqual(state.baselineValues, defaultValues);
|
||||||
isEqual(state.currentValues, state.baselineValues);
|
isEqual(state.currentValues, state.baselineValues);
|
||||||
|
|
||||||
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well
|
// clicking refresh calls onRefresh.
|
||||||
selfServeComponent.onRefreshClicked();
|
selfServeComponent.onRefreshClicked();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect(onRefreshMock).toHaveBeenCalledTimes(2);
|
expect(onRefreshMock).toHaveBeenCalledTimes(2);
|
||||||
expect(initializeMock).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
selfServeComponent.onSaveButtonClick();
|
selfServeComponent.onSaveButtonClick();
|
||||||
expect(onSaveMock).toHaveBeenCalledTimes(1);
|
expect(onSaveMock).toHaveBeenCalledTimes(1);
|
||||||
|
@ -15,20 +15,45 @@ import {
|
|||||||
InputType,
|
InputType,
|
||||||
RefreshResult,
|
RefreshResult,
|
||||||
SelfServeDescriptor,
|
SelfServeDescriptor,
|
||||||
SelfServeNotification,
|
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
DescriptionDisplay,
|
DescriptionDisplay,
|
||||||
StringInput,
|
StringInput,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
ChoiceInput,
|
ChoiceInput,
|
||||||
SelfServeNotificationType,
|
|
||||||
} from "./SelfServeTypes";
|
} from "./SelfServeTypes";
|
||||||
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||||
import { getMessageBarType } from "./SelfServeUtils";
|
|
||||||
import { Translation } from "react-i18next";
|
import { Translation } from "react-i18next";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import "../i18n";
|
import "../i18n";
|
||||||
|
import { sendMessage } from "../Common/MessageHandler";
|
||||||
|
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
|
||||||
|
import promiseRetry, { AbortError } from "p-retry";
|
||||||
|
|
||||||
|
interface SelfServeNotification {
|
||||||
|
message: string;
|
||||||
|
type: MessageBarType;
|
||||||
|
isCancellable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortalNotificationContent {
|
||||||
|
retryIntervalInMs: number;
|
||||||
|
operationStatusUrl: string;
|
||||||
|
portalNotification?: {
|
||||||
|
initialize: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
success: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
failure: {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface SelfServeComponentProps {
|
export interface SelfServeComponentProps {
|
||||||
descriptor: SelfServeDescriptor;
|
descriptor: SelfServeDescriptor;
|
||||||
@ -39,17 +64,26 @@ export interface SelfServeComponentState {
|
|||||||
currentValues: Map<string, SmartUiInput>;
|
currentValues: Map<string, SmartUiInput>;
|
||||||
baselineValues: Map<string, SmartUiInput>;
|
baselineValues: Map<string, SmartUiInput>;
|
||||||
isInitializing: boolean;
|
isInitializing: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
hasErrors: boolean;
|
hasErrors: boolean;
|
||||||
compileErrorMessage: string;
|
compileErrorMessage: string;
|
||||||
notification: SelfServeNotification;
|
|
||||||
refreshResult: RefreshResult;
|
refreshResult: RefreshResult;
|
||||||
|
notification: SelfServeNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
||||||
|
private static readonly defaultRetryIntervalInMs = 30000;
|
||||||
private smartUiGeneratorClassName: string;
|
private smartUiGeneratorClassName: string;
|
||||||
|
private retryIntervalInMs: number;
|
||||||
|
private retryOptions: promiseRetry.Options;
|
||||||
|
private translationFunction: TFunction;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
this.performRefresh();
|
this.performRefresh().then(() => {
|
||||||
|
if (this.state.refreshResult?.isUpdateInProgress) {
|
||||||
|
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.initializeSmartUiComponent();
|
this.initializeSmartUiComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,12 +94,18 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
currentValues: new Map(),
|
currentValues: new Map(),
|
||||||
baselineValues: new Map(),
|
baselineValues: new Map(),
|
||||||
isInitializing: true,
|
isInitializing: true,
|
||||||
|
isSaving: false,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
compileErrorMessage: undefined,
|
compileErrorMessage: undefined,
|
||||||
notification: undefined,
|
|
||||||
refreshResult: undefined,
|
refreshResult: undefined,
|
||||||
|
notification: undefined,
|
||||||
};
|
};
|
||||||
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
||||||
|
this.retryIntervalInMs = this.props.descriptor.refreshParams?.retryIntervalInMs;
|
||||||
|
if (!this.retryIntervalInMs) {
|
||||||
|
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
|
||||||
|
}
|
||||||
|
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
|
||||||
}
|
}
|
||||||
|
|
||||||
private onError = (hasErrors: boolean): void => {
|
private onError = (hasErrors: boolean): void => {
|
||||||
@ -109,7 +149,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
this.setState({ currentValues, baselineValues });
|
this.setState({ currentValues, baselineValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
public resetBaselineValues = (): void => {
|
public updateBaselineValues = (): void => {
|
||||||
const currentValues = this.state.currentValues;
|
const currentValues = this.state.currentValues;
|
||||||
let baselineValues = this.state.baselineValues;
|
let baselineValues = this.state.baselineValues;
|
||||||
for (const key of currentValues.keys()) {
|
for (const key of currentValues.keys()) {
|
||||||
@ -204,7 +244,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
|
|
||||||
private onInputChange = (input: AnyDisplay, 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(
|
||||||
|
newValue,
|
||||||
|
this.state.currentValues,
|
||||||
|
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
|
||||||
|
);
|
||||||
this.setState({ currentValues: newValues });
|
this.setState({ currentValues: newValues });
|
||||||
} else {
|
} else {
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
@ -215,29 +259,62 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public performSave = async (): Promise<void> => {
|
||||||
|
this.setState({ isSaving: true, notification: undefined });
|
||||||
|
try {
|
||||||
|
const onSaveResult = await this.props.descriptor.onSave(
|
||||||
|
this.state.currentValues,
|
||||||
|
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
|
||||||
|
);
|
||||||
|
if (onSaveResult.portalNotification) {
|
||||||
|
const requestInitializedPortalNotification = onSaveResult.portalNotification.initialize;
|
||||||
|
const requestSucceededPortalNotification = onSaveResult.portalNotification.success;
|
||||||
|
const requestFailedPortalNotification = onSaveResult.portalNotification.failure;
|
||||||
|
|
||||||
|
this.sendNotificationMessage({
|
||||||
|
retryIntervalInMs: this.retryIntervalInMs,
|
||||||
|
operationStatusUrl: onSaveResult.operationStatusUrl,
|
||||||
|
portalNotification: {
|
||||||
|
initialize: {
|
||||||
|
title: this.getTranslation(requestInitializedPortalNotification.titleTKey),
|
||||||
|
message: this.getTranslation(requestInitializedPortalNotification.messageTKey),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
title: this.getTranslation(requestSucceededPortalNotification.titleTKey),
|
||||||
|
message: this.getTranslation(requestSucceededPortalNotification.messageTKey),
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
title: this.getTranslation(requestFailedPortalNotification.titleTKey),
|
||||||
|
message: this.getTranslation(requestFailedPortalNotification.messageTKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({
|
||||||
|
notification: {
|
||||||
|
type: MessageBarType.error,
|
||||||
|
isCancellable: true,
|
||||||
|
message: this.getTranslation(error.message),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.setState({ isSaving: false });
|
||||||
|
}
|
||||||
|
await this.onRefreshClicked();
|
||||||
|
this.updateBaselineValues();
|
||||||
|
};
|
||||||
|
|
||||||
public onSaveButtonClick = (): void => {
|
public onSaveButtonClick = (): void => {
|
||||||
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues);
|
this.performSave();
|
||||||
onSavePromise.catch((error) => {
|
|
||||||
this.setState({
|
|
||||||
notification: {
|
|
||||||
message: `${error.message}`,
|
|
||||||
type: SelfServeNotificationType.error,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
onSavePromise.then((notification: SelfServeNotification) => {
|
|
||||||
this.setState({
|
|
||||||
notification: {
|
|
||||||
message: notification.message,
|
|
||||||
type: notification.type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.resetBaselineValues();
|
|
||||||
this.onRefreshClicked();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public isDiscardButtonDisabled = (): boolean => {
|
public isDiscardButtonDisabled = (): boolean => {
|
||||||
|
if (this.state.isSaving) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
for (const key of this.state.currentValues.keys()) {
|
for (const key of this.state.currentValues.keys()) {
|
||||||
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||||
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||||
@ -250,7 +327,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
};
|
};
|
||||||
|
|
||||||
public isSaveButtonDisabled = (): boolean => {
|
public isSaveButtonDisabled = (): boolean => {
|
||||||
if (this.state.hasErrors) {
|
if (this.state.hasErrors || this.state.isSaving) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (const key of this.state.currentValues.keys()) {
|
for (const key of this.state.currentValues.keys()) {
|
||||||
@ -264,38 +341,69 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
private performRefresh = async (): Promise<RefreshResult> => {
|
private performRefresh = async (): Promise<void> => {
|
||||||
const refreshResult = await this.props.descriptor.onRefresh();
|
const refreshResult = await this.props.descriptor.onRefresh();
|
||||||
this.setState({ refreshResult: { ...refreshResult } });
|
let updateInProgressNotification: SelfServeNotification;
|
||||||
return refreshResult;
|
if (this.state.refreshResult?.isUpdateInProgress && !refreshResult.isUpdateInProgress) {
|
||||||
|
await this.initializeSmartUiComponent();
|
||||||
|
}
|
||||||
|
if (refreshResult.isUpdateInProgress) {
|
||||||
|
updateInProgressNotification = {
|
||||||
|
type: MessageBarType.info,
|
||||||
|
isCancellable: false,
|
||||||
|
message: this.getTranslation(refreshResult.updateInProgressMessageTKey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
refreshResult: { ...refreshResult },
|
||||||
|
notification: updateInProgressNotification,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public onRefreshClicked = async (): Promise<void> => {
|
public onRefreshClicked = async (): Promise<void> => {
|
||||||
this.setState({ isInitializing: true });
|
this.setState({ isInitializing: true });
|
||||||
const refreshResult = await this.performRefresh();
|
await this.performRefresh();
|
||||||
if (!refreshResult.isUpdateInProgress) {
|
|
||||||
this.initializeSmartUiComponent();
|
|
||||||
}
|
|
||||||
this.setState({ isInitializing: false });
|
this.setState({ isInitializing: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
|
public pollRefresh = async (): Promise<void> => {
|
||||||
return translationFunction(`Common.${key}`);
|
try {
|
||||||
|
await this.performRefresh();
|
||||||
|
} catch (error) {
|
||||||
|
throw new AbortError(error);
|
||||||
|
}
|
||||||
|
const refreshResult = this.state.refreshResult;
|
||||||
|
if (refreshResult.isUpdateInProgress) {
|
||||||
|
throw new Error("update in progress. retrying ...");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
|
public getCommonTranslation = (key: string): string => {
|
||||||
|
return this.getTranslation(key, "Common");
|
||||||
|
};
|
||||||
|
|
||||||
|
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => {
|
||||||
|
const translationKey = `${prefix}.${messageKey}`;
|
||||||
|
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
|
||||||
|
if (translation === translationKey) {
|
||||||
|
return messageKey;
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: "save",
|
key: "save",
|
||||||
text: this.getCommonTranslation(translate, "Save"),
|
text: this.getCommonTranslation("Save"),
|
||||||
iconProps: { iconName: "Save" },
|
iconProps: { iconName: "Save" },
|
||||||
split: true,
|
split: true,
|
||||||
disabled: this.isSaveButtonDisabled(),
|
disabled: this.isSaveButtonDisabled(),
|
||||||
onClick: this.onSaveButtonClick,
|
onClick: () => this.onSaveButtonClick(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "discard",
|
key: "discard",
|
||||||
text: this.getCommonTranslation(translate, "Discard"),
|
text: this.getCommonTranslation("Discard"),
|
||||||
iconProps: { iconName: "Undo" },
|
iconProps: { iconName: "Undo" },
|
||||||
split: true,
|
split: true,
|
||||||
disabled: this.isDiscardButtonDisabled(),
|
disabled: this.isDiscardButtonDisabled(),
|
||||||
@ -305,7 +413,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "refresh",
|
key: "refresh",
|
||||||
text: this.getCommonTranslation(translate, "Refresh"),
|
text: this.getCommonTranslation("Refresh"),
|
||||||
disabled: this.state.isInitializing,
|
disabled: this.state.isInitializing,
|
||||||
iconProps: { iconName: "Refresh" },
|
iconProps: { iconName: "Refresh" },
|
||||||
split: true,
|
split: true,
|
||||||
@ -316,12 +424,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => {
|
private sendNotificationMessage = (portalNotificationContent: PortalNotificationContent): void => {
|
||||||
const translation = translationFunction(messageKey);
|
sendMessage({
|
||||||
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) {
|
type: SelfServeMessageTypes.Notification,
|
||||||
return messageKey;
|
data: { portalNotificationContent },
|
||||||
}
|
});
|
||||||
return translation;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
@ -332,14 +439,14 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
return (
|
return (
|
||||||
<Translation>
|
<Translation>
|
||||||
{(translate) => {
|
{(translate) => {
|
||||||
const getTranslation = (key: string): string => {
|
if (!this.translationFunction) {
|
||||||
return translate(`${this.smartUiGeneratorClassName}.${key}`);
|
this.translationFunction = translate;
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ overflowX: "auto" }}>
|
<div style={{ overflowX: "auto" }}>
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
||||||
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
|
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
|
||||||
{this.state.isInitializing ? (
|
{this.state.isInitializing ? (
|
||||||
<Spinner
|
<Spinner
|
||||||
size={SpinnerSize.large}
|
size={SpinnerSize.large}
|
||||||
@ -347,27 +454,25 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{this.state.refreshResult?.isUpdateInProgress && (
|
|
||||||
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
|
||||||
{getTranslation(this.state.refreshResult.notificationMessage)}
|
|
||||||
</MessageBar>
|
|
||||||
)}
|
|
||||||
{this.state.notification && (
|
{this.state.notification && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
messageBarType={getMessageBarType(this.state.notification.type)}
|
messageBarType={this.state.notification.type}
|
||||||
styles={{ root: { width: 400 } }}
|
onDismiss={
|
||||||
onDismiss={() => this.setState({ notification: undefined })}
|
this.state.notification.isCancellable
|
||||||
|
? () => this.setState({ notification: undefined })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
|
{this.state.notification.message}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
)}
|
)}
|
||||||
<SmartUiComponent
|
<SmartUiComponent
|
||||||
disabled={this.state.refreshResult?.isUpdateInProgress}
|
disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
|
||||||
descriptor={this.state.root as SmartUiDescriptor}
|
descriptor={this.state.root as SmartUiDescriptor}
|
||||||
currentValues={this.state.currentValues}
|
currentValues={this.state.currentValues}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onError={this.onError}
|
onError={this.onError}
|
||||||
getTranslation={getTranslation}
|
getTranslation={this.getTranslation}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -3,7 +3,11 @@ interface BaseInput {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
type: InputTypeValue;
|
type: InputTypeValue;
|
||||||
labelTKey?: (() => Promise<string>) | string;
|
labelTKey?: (() => Promise<string>) | string;
|
||||||
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
onChange?: (
|
||||||
|
newValue: InputType,
|
||||||
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
) => Map<string, SmartUiInput>;
|
||||||
placeholderTKey?: (() => Promise<string>) | string;
|
placeholderTKey?: (() => Promise<string>) | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,16 +48,23 @@ export interface Node {
|
|||||||
export interface SelfServeDescriptor {
|
export interface SelfServeDescriptor {
|
||||||
root: Node;
|
root: Node;
|
||||||
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||||
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
onSave?: (
|
||||||
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
) => Promise<OnSaveResult>;
|
||||||
inputNames?: string[];
|
inputNames?: string[];
|
||||||
onRefresh?: () => Promise<RefreshResult>;
|
onRefresh?: () => Promise<RefreshResult>;
|
||||||
|
refreshParams?: RefreshParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||||
|
|
||||||
export abstract class SelfServeBaseClass {
|
export abstract class SelfServeBaseClass {
|
||||||
public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
|
public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||||
public abstract onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
public abstract onSave: (
|
||||||
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
) => Promise<OnSaveResult>;
|
||||||
public abstract onRefresh: () => Promise<RefreshResult>;
|
public abstract onRefresh: () => Promise<RefreshResult>;
|
||||||
|
|
||||||
public toSelfServeDescriptor(): SelfServeDescriptor {
|
public toSelfServeDescriptor(): SelfServeDescriptor {
|
||||||
@ -70,7 +81,7 @@ export abstract class SelfServeBaseClass {
|
|||||||
throw new Error(`onRefresh() was not declared for the class '${className}'`);
|
throw new Error(`onRefresh() was not declared for the class '${className}'`);
|
||||||
}
|
}
|
||||||
if (!selfServeDescriptor?.root) {
|
if (!selfServeDescriptor?.root) {
|
||||||
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
throw new Error(`@IsDisplayable decorator was not declared for the class '${className}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
selfServeDescriptor.initialize = this.initialize;
|
selfServeDescriptor.initialize = this.initialize;
|
||||||
@ -89,7 +100,7 @@ export enum NumberUiType {
|
|||||||
|
|
||||||
export type ChoiceItem = { label: string; key: string };
|
export type ChoiceItem = { label: string; key: string };
|
||||||
|
|
||||||
export type InputType = number | string | boolean | ChoiceItem;
|
export type InputType = number | string | boolean | ChoiceItem | Description;
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
messageTKey: string;
|
messageTKey: string;
|
||||||
@ -99,8 +110,15 @@ export interface Info {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DescriptionType {
|
||||||
|
Text,
|
||||||
|
InfoMessageBar,
|
||||||
|
WarningMessageBar,
|
||||||
|
}
|
||||||
|
|
||||||
export interface Description {
|
export interface Description {
|
||||||
textTKey: string;
|
textTKey: string;
|
||||||
|
type: DescriptionType;
|
||||||
link?: {
|
link?: {
|
||||||
href: string;
|
href: string;
|
||||||
textTKey: string;
|
textTKey: string;
|
||||||
@ -113,18 +131,29 @@ export interface SmartUiInput {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SelfServeNotificationType {
|
export interface OnSaveResult {
|
||||||
info = "info",
|
operationStatusUrl: string;
|
||||||
warning = "warning",
|
portalNotification?: {
|
||||||
error = "error",
|
initialize: {
|
||||||
}
|
titleTKey: string;
|
||||||
|
messageTKey: string;
|
||||||
export interface SelfServeNotification {
|
};
|
||||||
message: string;
|
success: {
|
||||||
type: SelfServeNotificationType;
|
titleTKey: string;
|
||||||
|
messageTKey: string;
|
||||||
|
};
|
||||||
|
failure: {
|
||||||
|
titleTKey: string;
|
||||||
|
messageTKey: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshResult {
|
export interface RefreshResult {
|
||||||
isUpdateInProgress: boolean;
|
isUpdateInProgress: boolean;
|
||||||
notificationMessage: string;
|
updateInProgressMessageTKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshParams {
|
||||||
|
retryIntervalInMs: number;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { NumberUiType, RefreshResult, SelfServeBaseClass, SelfServeNotification, SmartUiInput } from "./SelfServeTypes";
|
import { NumberUiType, OnSaveResult, RefreshResult, SelfServeBaseClass, SmartUiInput } from "./SelfServeTypes";
|
||||||
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
|
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
|
||||||
|
|
||||||
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 initialize: () => Promise<Map<string, SmartUiInput>>;
|
public initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||||
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<OnSaveResult>;
|
||||||
public onRefresh: () => Promise<RefreshResult>;
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
}
|
}
|
||||||
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'");
|
||||||
@ -14,7 +14,7 @@ describe("SelfServeUtils", () => {
|
|||||||
it("onSave 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 initialize = jest.fn();
|
public initialize = jest.fn();
|
||||||
public onSave: () => Promise<SelfServeNotification>;
|
public onSave: () => Promise<OnSaveResult>;
|
||||||
public onRefresh: () => Promise<RefreshResult>;
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
|
||||||
@ -29,14 +29,14 @@ describe("SelfServeUtils", () => {
|
|||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("@SmartUi decorator must be present for self serve classes", () => {
|
it("@IsDisplayable decorator must be present for self serve classes", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public initialize = jest.fn();
|
public initialize = jest.fn();
|
||||||
public onSave = jest.fn();
|
public onSave = jest.fn();
|
||||||
public onRefresh = jest.fn();
|
public onRefresh = jest.fn();
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
||||||
"@SmartUi decorator was not declared for the class 'Test'"
|
"@IsDisplayable decorator was not declared for the class 'Test'"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { MessageBarType } from "office-ui-fabric-react";
|
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import {
|
import {
|
||||||
Node,
|
Node,
|
||||||
@ -15,8 +14,9 @@ import {
|
|||||||
SelfServeDescriptor,
|
SelfServeDescriptor,
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
StringInput,
|
StringInput,
|
||||||
SelfServeNotificationType,
|
RefreshParams,
|
||||||
} from "./SelfServeTypes";
|
} from "./SelfServeTypes";
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
export enum SelfServeType {
|
export enum SelfServeType {
|
||||||
// No self serve type passed, launch explorer
|
// No self serve type passed, launch explorer
|
||||||
@ -28,6 +28,14 @@ export enum SelfServeType {
|
|||||||
sqlx = "sqlx",
|
sqlx = "sqlx",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum BladeType {
|
||||||
|
SqlKeys = "keys",
|
||||||
|
MongoKeys = "mongoDbKeys",
|
||||||
|
CassandraKeys = "cassandraDbKeys",
|
||||||
|
GremlinKeys = "keys",
|
||||||
|
TableKeys = "tableKeys",
|
||||||
|
}
|
||||||
|
|
||||||
export interface DecoratorProperties {
|
export interface DecoratorProperties {
|
||||||
id: string;
|
id: string;
|
||||||
info?: (() => Promise<Info>) | Info;
|
info?: (() => Promise<Info>) | Info;
|
||||||
@ -44,9 +52,13 @@ export interface DecoratorProperties {
|
|||||||
uiType?: string;
|
uiType?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
description?: (() => Promise<Description>) | Description;
|
description?: (() => Promise<Description>) | Description;
|
||||||
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
isDynamicDescription?: boolean;
|
||||||
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<void>;
|
refreshParams?: RefreshParams;
|
||||||
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
onChange?: (
|
||||||
|
newValue: InputType,
|
||||||
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
) => Map<string, SmartUiInput>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
@ -83,7 +95,7 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
|
|||||||
descriptorValue: K
|
descriptorValue: K
|
||||||
): void => {
|
): void => {
|
||||||
if (!(context instanceof Map)) {
|
if (!(context instanceof Map)) {
|
||||||
throw new Error(`@SmartUi should be the first decorator for the class '${className}'.`);
|
throw new Error(`@IsDisplayable should be the first decorator for the class '${className}'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const propertyObject = context.get(propertyName) ?? { id: propertyName };
|
const propertyObject = context.get(propertyName) ?? { id: propertyName };
|
||||||
@ -108,16 +120,17 @@ export const mapToSmartUiDescriptor = (
|
|||||||
className: string,
|
className: string,
|
||||||
context: Map<string, DecoratorProperties>
|
context: Map<string, DecoratorProperties>
|
||||||
): SelfServeDescriptor => {
|
): SelfServeDescriptor => {
|
||||||
|
const inputNames: string[] = [];
|
||||||
const root = context.get("root");
|
const root = context.get("root");
|
||||||
context.delete("root");
|
context.delete("root");
|
||||||
const inputNames: string[] = [];
|
|
||||||
|
|
||||||
const smartUiDescriptor: SelfServeDescriptor = {
|
const smartUiDescriptor: SelfServeDescriptor = {
|
||||||
root: {
|
root: {
|
||||||
id: className,
|
id: className,
|
||||||
info: root?.info,
|
info: undefined,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
refreshParams: root?.refreshParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
while (context.size > 0) {
|
while (context.size > 0) {
|
||||||
@ -155,7 +168,10 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
|
|||||||
}
|
}
|
||||||
return value as NumberInput;
|
return value as NumberInput;
|
||||||
case "string":
|
case "string":
|
||||||
if (value.description) {
|
if (value.description || value.isDynamicDescription) {
|
||||||
|
if (value.description && value.isDynamicDescription) {
|
||||||
|
value.errorMessage = `dynamic descriptions should not have defaults set here.`;
|
||||||
|
}
|
||||||
return value as DescriptionDisplay;
|
return value as DescriptionDisplay;
|
||||||
}
|
}
|
||||||
if (!value.labelTKey) {
|
if (!value.labelTKey) {
|
||||||
@ -175,13 +191,9 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMessageBarType = (type: SelfServeNotificationType): MessageBarType => {
|
export const generateBladeLink = (blade: BladeType): string => {
|
||||||
switch (type) {
|
const subscriptionId = userContext.subscriptionId;
|
||||||
case SelfServeNotificationType.info:
|
const resourceGroupName = userContext.resourceGroup;
|
||||||
return MessageBarType.info;
|
const databaseAccountName = userContext.databaseAccount.name;
|
||||||
case SelfServeNotificationType.warning:
|
return `www.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
|
||||||
return MessageBarType.warning;
|
|
||||||
case SelfServeNotificationType.error:
|
|
||||||
return MessageBarType.error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { IsDisplayable, OnChange, Values } from "../Decorators";
|
import { IsDisplayable, OnChange, Values } from "../Decorators";
|
||||||
import {
|
import {
|
||||||
ChoiceItem,
|
ChoiceItem,
|
||||||
|
DescriptionType,
|
||||||
InputType,
|
InputType,
|
||||||
NumberUiType,
|
NumberUiType,
|
||||||
|
OnSaveResult,
|
||||||
RefreshResult,
|
RefreshResult,
|
||||||
SelfServeBaseClass,
|
SelfServeBaseClass,
|
||||||
SelfServeNotification,
|
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
} from "../SelfServeTypes";
|
} from "../SelfServeTypes";
|
||||||
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
|
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
|
||||||
|
|
||||||
const onEnableDedicatedGatewayChange = (
|
const onEnableDedicatedGatewayChange = (
|
||||||
currentState: Map<string, SmartUiInput>,
|
newValue: InputType,
|
||||||
newValue: InputType
|
currentState: Map<string, SmartUiInput>
|
||||||
): Map<string, SmartUiInput> => {
|
): Map<string, SmartUiInput> => {
|
||||||
const sku = currentState.get("sku");
|
const sku = currentState.get("sku");
|
||||||
const instances = currentState.get("instances");
|
const instances = currentState.get("instances");
|
||||||
@ -49,7 +50,7 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
return refreshDedicatedGatewayProvisioning();
|
return refreshDedicatedGatewayProvisioning();
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<OnSaveResult> => {
|
||||||
validate(currentValues);
|
validate(currentValues);
|
||||||
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
|
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
|
||||||
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
|
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
|
||||||
@ -63,6 +64,7 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
@Values({
|
@Values({
|
||||||
description: {
|
description: {
|
||||||
textTKey: "Provisioning dedicated gateways for SqlX accounts.",
|
textTKey: "Provisioning dedicated gateways for SqlX accounts.",
|
||||||
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
textTKey: "Learn more about dedicated gateway.",
|
textTKey: "Learn more about dedicated gateway.",
|
||||||
|
@ -47,15 +47,14 @@ interface Options {
|
|||||||
queryParams?: ARMQueryParams;
|
queryParams?: ARMQueryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
|
export async function armRequestWithoutPolling<T>({
|
||||||
export async function armRequest<T>({
|
|
||||||
host,
|
host,
|
||||||
path,
|
path,
|
||||||
apiVersion,
|
apiVersion,
|
||||||
method,
|
method,
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
queryParams,
|
queryParams,
|
||||||
}: Options): Promise<T> {
|
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
||||||
const url = new URL(path, host);
|
const url = new URL(path, host);
|
||||||
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||||
if (queryParams) {
|
if (queryParams) {
|
||||||
@ -92,13 +91,33 @@ export async function armRequest<T>({
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const operationStatusUrl = response.headers && response.headers.get("location");
|
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
||||||
|
const responseBody = (await response.json()) as T;
|
||||||
|
return { result: responseBody, operationStatusUrl: operationStatusUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
|
||||||
|
export async function armRequest<T>({
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
apiVersion,
|
||||||
|
method,
|
||||||
|
body: requestBody,
|
||||||
|
queryParams,
|
||||||
|
}: Options): Promise<T> {
|
||||||
|
const armRequestResult = await armRequestWithoutPolling<T>({
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
apiVersion,
|
||||||
|
method,
|
||||||
|
body: requestBody,
|
||||||
|
queryParams,
|
||||||
|
});
|
||||||
|
const operationStatusUrl = armRequestResult.operationStatusUrl;
|
||||||
if (operationStatusUrl) {
|
if (operationStatusUrl) {
|
||||||
return await promiseRetry(() => getOperationStatus(operationStatusUrl));
|
return await promiseRetry(() => getOperationStatus(operationStatusUrl));
|
||||||
}
|
}
|
||||||
|
return armRequestResult.result;
|
||||||
const responseBody = (await response.json()) as T;
|
|
||||||
return responseBody;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOperationStatus(operationStatusUrl: string) {
|
async function getOperationStatus(operationStatusUrl: string) {
|
||||||
|
@ -20,6 +20,7 @@ describe("Self Serve", () => {
|
|||||||
|
|
||||||
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
|
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
|
||||||
await frame.waitForSelector("#description-text-display");
|
await frame.waitForSelector("#description-text-display");
|
||||||
|
await frame.waitForSelector("#currentRegionText-text-display");
|
||||||
|
|
||||||
const regions = await frame.waitForSelector("#regions-dropdown-input");
|
const regions = await frame.waitForSelector("#regions-dropdown-input");
|
||||||
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
|
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
|
||||||
|
Loading…
Reference in New Issue
Block a user