Refactored Settings Tab (#161)

* added  SettingsV2 Tab

* lint changes

* foxed failing test

* Addressed PR comments

- removed dangerouslySetInnerHtml
- removed underscore dependency
- added AccessibleElement
- removed unnecesary exceptions to linting

* split render into separate functions

- removed sinon in test
- Added some enums to replace constant strings
- removed dangerously set inner html
- made autopilot input as StatefulValue

* add settingscomponent snapshot

* fixed linting errors

* fixed errors

* addressed PR comments

- Moved StatefulValue to new class
- Split render to more functions for throughputInputComponents

* Added sub components

- Added tests for SettingsRenderUtls
- Added empty test files for adding tests later

* Moved all inputs to fluent UI

- removed rupm
- added reusable styles

* Added Tabs

- Added ToolTipLabel component
- Removed toggleables for individual components
- Removed accessible elements
- Added IndexingPolicyComponent

* Added more tests

* Addressed PR comments

* Moved Label radio buttons to choicegroup

* fixed lint errors

* Removed StatefulValue

- Moved conflict res tab to the end
- Added styling for autpilot radiobuttons

* fixed linting errors

* fix bugs from merge to master

* fixed formatting issue

* Addressed PR comments

- Added unit tests for smaller methods within each component

* fixed linting errors

* removed redundant snapshots

* removed empty line

* made separate props objects for subcomponents

* Moved dirty checks to sub components

* Made indesing policy component height = 80% of view port

- modified auto pilot v3 messages
- Added Fluent UI tolltip
-

* Moved warning messages inline

* moved conflict res helpers out

* fixed bugs

* added stack style for message

* fixed tests

* Added tests

* fixed linting and format errors

* undid changes

* more edits

* fixed compile errors

* fixed compile errors

* fixed errors

* fixed bug with save and discard buttons

* fixed compile errors

* addressed PR comments
This commit is contained in:
Srinath Narayanan
2020-09-30 12:34:39 -07:00
committed by GitHub
parent 4ecdfe60eb
commit fc722e87be
47 changed files with 11504 additions and 59 deletions

View File

@@ -0,0 +1,97 @@
import { shallow } from "enzyme";
import React from "react";
import {
ThroughputInputAutoPilotV3Component,
ThroughputInputAutoPilotV3Props
} from "./ThroughputInputAutoPilotV3Component";
import * as DataModels from "../../../../../Contracts/DataModels";
describe("ThroughputInputAutoPilotV3Component", () => {
const baseProps: ThroughputInputAutoPilotV3Props = {
databaseAccount: {} as DataModels.DatabaseAccount,
serverId: undefined,
wasAutopilotOriginallySet: false,
throughput: 100,
throughputBaseline: 100,
onThroughputChange: undefined,
minimum: 10000,
maximum: 400,
step: 100,
isEnabled: true,
isEmulator: false,
spendAckChecked: false,
spendAckId: "spendAckId",
spendAckText: "spendAckText",
spendAckVisible: false,
showAsMandatory: true,
isFixed: false,
label: "label",
infoBubbleText: "infoBubbleText",
canExceedMaximumValue: true,
onAutoPilotSelected: undefined,
isAutoPilotSelected: false,
maxAutoPilotThroughput: 4000,
maxAutoPilotThroughputBaseline: 4000,
onMaxAutoPilotThroughputChange: undefined,
onScaleSaveableChange: () => {
return;
},
onScaleDiscardableChange: () => {
return;
},
getThroughputWarningMessage: () => undefined
};
it("throughput input visible", () => {
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...baseProps} />);
const throughputComponent = wrapper.instance() as ThroughputInputAutoPilotV3Component;
expect(throughputComponent.hasProvisioningTypeChanged()).toEqual(false);
expect(throughputComponent.overrideWithProvisionedThroughputSettings()).toEqual(false);
expect(throughputComponent.overrideWithAutoPilotSettings()).toEqual(false);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#throughputInput")).toEqual(true);
expect(wrapper.exists("#autopilotInput")).toEqual(false);
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false);
});
it("autopilot input visible", () => {
const newProps = { ...baseProps, isAutoPilotSelected: true };
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...newProps} />);
const throughputComponent = wrapper.instance() as ThroughputInputAutoPilotV3Component;
expect(throughputComponent.hasProvisioningTypeChanged()).toEqual(true);
expect(throughputComponent.overrideWithProvisionedThroughputSettings()).toEqual(true);
expect(throughputComponent.overrideWithAutoPilotSettings()).toEqual(false);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#autopilotInput")).toEqual(true);
expect(wrapper.exists("#throughputInput")).toEqual(false);
expect(wrapper.exists("#manualToAutoscaleDisclaimerElement")).toEqual(true);
wrapper.setProps({ wasAutopilotOriginallySet: true });
wrapper.update();
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
});
it("spendAck checkbox visible", () => {
const newProps = { ...baseProps, spendAckVisible: true };
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...newProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#spendAckCheckBox")).toEqual(true);
});
it("scale saveable and discardable are set", () => {
let throughputComponent = new ThroughputInputAutoPilotV3Component(baseProps);
let isComponentDirtyResult = throughputComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(false);
expect(isComponentDirtyResult.isDiscardable).toEqual(false);
const newProps = { ...baseProps, throughput: 1000000 };
throughputComponent = new ThroughputInputAutoPilotV3Component(newProps);
isComponentDirtyResult = throughputComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(true);
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
});
});

View File

@@ -0,0 +1,342 @@
import React from "react";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import {
getTextFieldStyles,
getToolTipContainer,
spendAckCheckBoxStyle,
titleAndInputStackProps,
checkBoxAndInputStackProps,
getChoiceGroupStyles,
messageBarStyles,
getEstimatedSpendElement,
getEstimatedAutoscaleSpendElement,
getAutoPilotV3SpendElement,
manualToAutoscaleDisclaimerElement
} from "../../SettingsRenderUtils";
import {
Text,
TextField,
ChoiceGroup,
IChoiceGroupOption,
Checkbox,
Stack,
Label,
Link,
MessageBar,
MessageBarType
} from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
serverId: string;
throughput: number;
throughputBaseline: number;
onThroughputChange: (newThroughput: number) => void;
minimum: number;
maximum: number;
step?: number;
isEnabled?: boolean;
spendAckChecked?: boolean;
spendAckId?: string;
spendAckText?: string;
spendAckVisible?: boolean;
showAsMandatory?: boolean;
isFixed: boolean;
isEmulator: boolean;
label: string;
infoBubbleText?: string;
canExceedMaximumValue?: boolean;
onAutoPilotSelected: (isAutoPilotSelected: boolean) => void;
isAutoPilotSelected: boolean;
wasAutopilotOriginallySet: boolean;
maxAutoPilotThroughput: number;
maxAutoPilotThroughputBaseline: number;
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
}
interface ThroughputInputAutoPilotV3State {
spendAckChecked: boolean;
}
export class ThroughputInputAutoPilotV3Component extends React.Component<
ThroughputInputAutoPilotV3Props,
ThroughputInputAutoPilotV3State
> {
private shouldCheckComponentIsDirty = true;
private static readonly defaultStep = 100;
private static readonly zeroThroughput = 0;
private step: number;
private choiceGroupFixedStyle = getChoiceGroupStyles(undefined, undefined);
private options: IChoiceGroupOption[] = [
{ key: "true", text: "Autoscale" },
{ key: "false", text: "Manual" }
];
componentDidMount(): void {
this.onComponentUpdate();
}
componentDidUpdate(): void {
this.onComponentUpdate();
}
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
const isComponentDirtyResult = this.IsComponentDirty();
this.props.onScaleSaveableChange(isComponentDirtyResult.isSaveable);
this.props.onScaleDiscardableChange(isComponentDirtyResult.isDiscardable);
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): IsComponentDirtyResult => {
let isSaveable = false;
let isDiscardable = false;
if (this.props.isEnabled) {
if (this.hasProvisioningTypeChanged()) {
isSaveable = true;
isDiscardable = true;
} else if (this.props.isAutoPilotSelected) {
if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) {
isDiscardable = true;
if (AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)) {
isSaveable = true;
}
}
} else {
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
isDiscardable = true;
isSaveable = true;
if (
!this.props.throughput ||
this.props.throughput < this.props.minimum ||
(this.props.throughput > this.props.maximum && (this.props.isEmulator || this.props.isFixed)) ||
(this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
!this.props.canExceedMaximumValue)
) {
isSaveable = false;
}
}
}
}
return { isSaveable, isDiscardable };
};
public constructor(props: ThroughputInputAutoPilotV3Props) {
super(props);
this.state = {
spendAckChecked: this.props.spendAckChecked
};
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
}
public hasProvisioningTypeChanged = (): boolean =>
this.props.wasAutopilotOriginallySet !== this.props.isAutoPilotSelected;
public overrideWithAutoPilotSettings = (): boolean =>
this.hasProvisioningTypeChanged() && this.props.wasAutopilotOriginallySet;
public overrideWithProvisionedThroughputSettings = (): boolean =>
this.hasProvisioningTypeChanged() && !this.props.wasAutopilotOriginallySet;
private getRequestUnitsUsageCost = (): JSX.Element => {
const account = this.props.databaseAccount;
if (!account) {
return <></>;
}
const serverId: string = this.props.serverId;
const offerThroughput: number = this.props.throughput;
const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
let estimatedSpend: JSX.Element;
if (!this.props.isAutoPilotSelected) {
estimatedSpend = getEstimatedSpendElement(
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
serverId,
regions,
multimaster,
false
);
} else {
estimatedSpend = getEstimatedAutoscaleSpendElement(
this.props.maxAutoPilotThroughput,
serverId,
regions,
multimaster
);
}
return estimatedSpend;
};
private getAutoPilotUsageCost = (): JSX.Element => {
if (!this.props.maxAutoPilotThroughput) {
return <></>;
}
return getAutoPilotV3SpendElement(
this.props.maxAutoPilotThroughput,
false /* isDatabaseThroughput */,
!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>
);
};
private onAutoPilotThroughputChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
let newThroughput = parseInt(newValue);
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
this.props.onMaxAutoPilotThroughputChange(newThroughput);
};
private onThroughputChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
let newThroughput = parseInt(newValue);
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else {
this.props.onThroughputChange(newThroughput);
}
};
private onChoiceGroupChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void => this.props.onAutoPilotSelected(option.key === "true");
private renderThroughputModeChoices = (): JSX.Element => {
const labelId = "settingsV2RadioButtonLabelId";
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent
label={this.props.label}
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
/>
</Label>
{this.overrideWithProvisionedThroughputSettings() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{manualToAutoscaleDisclaimerElement}
</MessageBar>
)}
<ChoiceGroup
selectedKey={this.props.isAutoPilotSelected.toString()}
options={this.options}
onChange={this.onChoiceGroupChange}
required={this.props.showAsMandatory}
ariaLabelledBy={labelId}
styles={this.choiceGroupFixedStyle}
/>
</Stack>
);
};
private onSpendAckChecked = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void =>
this.setState({ spendAckChecked: checked });
private renderAutoPilotInput = (): JSX.Element => (
<>
<Text>
Provision maximum RU/s required by this resource. Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`}
</Link>
</Text>
<TextField
label="Max RU/s"
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={this.step}
min={AutoPilotUtils.minAutoPilotThroughput}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={spendAckCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
/>
)}
</>
);
private renderThroughputInput = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<TextField
required
type="number"
id="throughputInput"
key="provisioned throughput input"
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
disabled={this.overrideWithAutoPilotSettings()}
step={this.step}
min={this.props.minimum}
max={this.props.canExceedMaximumValue ? undefined : this.props.maximum}
value={
this.overrideWithAutoPilotSettings()
? this.props.maxAutoPilotThroughputBaseline?.toString()
: this.props.throughput?.toString()
}
onChange={this.onThroughputChange}
/>
{this.props.getThroughputWarningMessage() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
styles={spendAckCheckBoxStyle}
label={this.props.spendAckText}
checked={this.state.spendAckChecked}
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && <p>Choose unlimited storage capacity for more than 10,000 RU/s.</p>}
</Stack>
);
public render(): JSX.Element {
return (
<Stack {...checkBoxAndInputStackProps}>
{!this.props.isFixed && this.renderThroughputModeChoices()}
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
</Stack>
);
}
}

View File

@@ -0,0 +1,434 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
>
<ToolTipLabelComponent
label="label"
toolTipElement={
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
infoBubbleText
</Text>
}
/>
</StyledLabelBase>
<StyledMessageBarBase
messageBarType={5}
styles={
Object {
"root": Object {
"marginTop": "5px",
},
}
}
>
<Text
id="manualToAutoscaleDisclaimerElement"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
<a
href="https://aka.ms/cosmos-autoscale-migration"
>
Learn more
</a>
</Text>
</StyledMessageBarBase>
<StyledChoiceGroupBase
ariaLabelledBy="settingsV2RadioButtonLabelId"
onChange={[Function]}
options={
Array [
Object {
"key": "true",
"text": "Autoscale",
},
Object {
"key": "false",
"text": "Manual",
},
]
}
required={true}
selectedKey="true"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
</Stack>
<Text>
Provision maximum RU/s required by this resource. Estimate your required RU/s with
<StyledLinkBase
href="https://cosmos.azure.com/capacitycalculator/"
target="_blank"
>
capacity calculator
</StyledLinkBase>
</Text>
<StyledTextFieldBase
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
label="Max RU/s"
min={4000}
onChange={[Function]}
required={true}
step={100}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
type="number"
value=""
/>
</Stack>
`;
exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
>
<ToolTipLabelComponent
label="label"
toolTipElement={
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
infoBubbleText
</Text>
}
/>
</StyledLabelBase>
<StyledChoiceGroupBase
ariaLabelledBy="settingsV2RadioButtonLabelId"
onChange={[Function]}
options={
Array [
Object {
"key": "true",
"text": "Autoscale",
},
Object {
"key": "false",
"text": "Manual",
},
]
}
required={true}
selectedKey="false"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
</Stack>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
required={true}
step={100}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
type="number"
value="100"
/>
<Text
id="throughputSpendElement"
>
Estimated cost (
USD
):
<b>
$
0.0080
hourly
/
$
0.19
daily
/
$
5.84
monthly
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
<StyledCheckboxBase
checked={false}
id="spendAckCheckBox"
label="spendAckText"
onChange={[Function]}
styles={
Object {
"label": Object {
"margin": 0,
"padding": "2 0 2 0",
},
"text": Object {
"fontSize": 12,
},
}
}
/>
</Stack>
</Stack>
`;
exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack>
<StyledLabelBase
id="settingsV2RadioButtonLabelId"
>
<ToolTipLabelComponent
label="label"
toolTipElement={
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
infoBubbleText
</Text>
}
/>
</StyledLabelBase>
<StyledChoiceGroupBase
ariaLabelledBy="settingsV2RadioButtonLabelId"
onChange={[Function]}
options={
Array [
Object {
"key": "true",
"text": "Autoscale",
},
Object {
"key": "false",
"text": "Manual",
},
]
}
required={true}
selectedKey="false"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
</Stack>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledTextFieldBase
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
required={true}
step={100}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
type="number"
value="100"
/>
<Text
id="throughputSpendElement"
>
Estimated cost (
USD
):
<b>
$
0.0080
hourly
/
$
0.19
daily
/
$
5.84
monthly
</b>
(
regions:
1
,
100
RU/s,
$
0.00008
/RU)
</Text>
</Stack>
</Stack>
`;