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