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,54 @@
import { shallow } from "enzyme";
import React from "react";
import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent";
import { container, collection } from "../TestUtils";
import * as DataModels from "../../../../Contracts/DataModels";
describe("ConflictResolutionComponent", () => {
const baseProps: ConflictResolutionComponentProps = {
collection: collection,
container: container,
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom,
onConflictResolutionPolicyModeChange: () => {
return;
},
conflictResolutionPolicyPath: "",
conflictResolutionPolicyPathBaseline: "",
onConflictResolutionPolicyPathChange: () => {
return;
},
conflictResolutionPolicyProcedure: "",
conflictResolutionPolicyProcedureBaseline: "",
onConflictResolutionPolicyProcedureChange: () => {
return;
},
onConflictResolutionDirtyChange: () => {
return;
}
};
it("Sproc text field displayed", () => {
const wrapper = shallow(<ConflictResolutionComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#conflictResolutionCustomTextField")).toEqual(true);
expect(wrapper.exists("#conflictResolutionLwwTextField")).toEqual(false);
});
it("Path text field displayed", () => {
const props = { ...baseProps, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins };
const wrapper = shallow(<ConflictResolutionComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#conflictResolutionCustomTextField")).toEqual(false);
expect(wrapper.exists("#conflictResolutionLwwTextField")).toEqual(true);
});
it("conflict resolution policy dirty is set", () => {
let conflictRsolutionComponent = new ConflictResolutionComponent(baseProps);
expect(conflictRsolutionComponent.IsComponentDirty()).toEqual(false);
const newProps = { ...baseProps, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins };
conflictRsolutionComponent = new ConflictResolutionComponent(newProps);
expect(conflictRsolutionComponent.IsComponentDirty()).toEqual(true);
});
});

View File

@@ -0,0 +1,142 @@
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import Explorer from "../../../Explorer";
import {
getTextFieldStyles,
conflictResolutionLwwTooltip,
conflictResolutionCustomToolTip,
subComponentStackProps,
getChoiceGroupStyles
} from "../SettingsRenderUtils";
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
import { isDirty } from "../SettingsUtils";
export interface ConflictResolutionComponentProps {
collection: ViewModels.Collection;
container: Explorer;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
onConflictResolutionPolicyModeChange: (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
) => void;
conflictResolutionPolicyPath: string;
conflictResolutionPolicyPathBaseline: string;
onConflictResolutionPolicyPathChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
conflictResolutionPolicyProcedure: string;
conflictResolutionPolicyProcedureBaseline: string;
onConflictResolutionPolicyProcedureChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
onConflictResolutionDirtyChange: (isConflictResolutionDirty: boolean) => void;
}
export class ConflictResolutionComponent extends React.Component<ConflictResolutionComponentProps> {
private shouldCheckComponentIsDirty = true;
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
{
key: DataModels.ConflictResolutionMode.LastWriterWins,
text: "Last Write Wins (default)"
},
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" }
];
componentDidMount(): void {
this.onComponentUpdate();
}
componentDidUpdate(): void {
this.onComponentUpdate();
}
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
this.props.onConflictResolutionDirtyChange(this.IsComponentDirty());
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): boolean => {
if (
isDirty(this.props.conflictResolutionPolicyMode, this.props.conflictResolutionPolicyModeBaseline) ||
isDirty(this.props.conflictResolutionPolicyPath, this.props.conflictResolutionPolicyPathBaseline) ||
isDirty(this.props.conflictResolutionPolicyProcedure, this.props.conflictResolutionPolicyProcedureBaseline)
) {
return true;
}
return false;
};
private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup
label="Mode"
selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions}
onChange={this.props.onConflictResolutionPolicyModeChange}
styles={getChoiceGroupStyles(
this.props.conflictResolutionPolicyMode,
this.props.conflictResolutionPolicyModeBaseline
)}
/>
);
private onRenderLwwComponentTextField = (props: ITextFieldProps) => (
<ToolTipLabelComponent label={props.label} toolTipElement={conflictResolutionLwwTooltip} />
);
private getConflictResolutionLWWComponent = (): JSX.Element => (
<TextField
id="conflictResolutionLwwTextField"
label={"Conflict Resolver Property"}
onRenderLabel={this.onRenderLwwComponentTextField}
styles={getTextFieldStyles(
this.props.conflictResolutionPolicyPath,
this.props.conflictResolutionPolicyPathBaseline
)}
value={this.props.conflictResolutionPolicyPath}
onChange={this.props.onConflictResolutionPolicyPathChange}
/>
);
private onRenderCustomComponentTextField = (props: ITextFieldProps) => (
<ToolTipLabelComponent label={props.label} toolTipElement={conflictResolutionCustomToolTip} />
);
private getConflictResolutionCustomComponent = (): JSX.Element => (
<TextField
id="conflictResolutionCustomTextField"
label="Stored procedure"
onRenderLabel={this.onRenderCustomComponentTextField}
styles={getTextFieldStyles(
this.props.conflictResolutionPolicyProcedure,
this.props.conflictResolutionPolicyProcedureBaseline
)}
value={this.props.conflictResolutionPolicyProcedure}
onChange={this.props.onConflictResolutionPolicyProcedureChange}
/>
);
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.getConflictResolutionModeComponent()}
{this.props.conflictResolutionPolicyMode === DataModels.ConflictResolutionMode.LastWriterWins &&
this.getConflictResolutionLWWComponent()}
{this.props.conflictResolutionPolicyMode === DataModels.ConflictResolutionMode.Custom &&
this.getConflictResolutionCustomComponent()}
</Stack>
);
}
}

View File

@@ -0,0 +1,59 @@
import { shallow } from "enzyme";
import React from "react";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./IndexingPolicyComponent";
import * as DataModels from "../../../../Contracts/DataModels";
describe("IndexingPolicyComponent", () => {
const initialIndexingPolicyContent: DataModels.IndexingPolicy = {
automatic: false,
indexingMode: "",
includedPaths: [],
excludedPaths: []
};
const baseProps: IndexingPolicyComponentProps = {
shouldDiscardIndexingPolicy: false,
resetShouldDiscardIndexingPolicy: () => {
return;
},
indexingPolicyContent: initialIndexingPolicyContent,
indexingPolicyContentBaseline: initialIndexingPolicyContent,
onIndexingPolicyElementFocusChange: () => {
return;
},
onIndexingPolicyContentChange: () => {
return;
},
logIndexingPolicySuccessMessage: () => {
return;
},
onIndexingPolicyDirtyChange: () => {
return;
}
};
it("renders", () => {
const wrapper = shallow(<IndexingPolicyComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
});
it("indexing policy is reset", () => {
const wrapper = shallow(<IndexingPolicyComponent {...baseProps} />);
const indexingPolicyComponentInstance = wrapper.instance() as IndexingPolicyComponent;
const resetIndexingPolicyEditorMockFn = jest.fn();
indexingPolicyComponentInstance.resetIndexingPolicyEditor = resetIndexingPolicyEditorMockFn;
wrapper.setProps({ shouldDiscardIndexingPolicy: true });
wrapper.update();
expect(resetIndexingPolicyEditorMockFn.mock.calls.length).toEqual(1);
});
it("conflict resolution policy dirty is set", () => {
let indexingPolicyComponent = new IndexingPolicyComponent(baseProps);
expect(indexingPolicyComponent.IsComponentDirty()).toEqual(false);
const newProps = { ...baseProps, indexingPolicyContent: undefined as DataModels.IndexingPolicy };
indexingPolicyComponent = new IndexingPolicyComponent(newProps);
expect(indexingPolicyComponent.IsComponentDirty()).toEqual(true);
});
});

View File

@@ -0,0 +1,121 @@
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import * as monaco from "monaco-editor";
import { isDirty } from "../SettingsUtils";
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
export interface IndexingPolicyComponentProps {
shouldDiscardIndexingPolicy: boolean;
resetShouldDiscardIndexingPolicy: () => void;
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
onIndexingPolicyElementFocusChange: (indexingPolicyContentFocussed: boolean) => void;
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
logIndexingPolicySuccessMessage: () => void;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
}
interface IndexingPolicyComponentState {
indexingPolicyContentIsValid: boolean;
}
export class IndexingPolicyComponent extends React.Component<
IndexingPolicyComponentProps,
IndexingPolicyComponentState
> {
private shouldCheckComponentIsDirty = true;
private indexingPolicyDiv = React.createRef<HTMLDivElement>();
private indexingPolicyEditor: monaco.editor.IStandaloneCodeEditor;
constructor(props: IndexingPolicyComponentProps) {
super(props);
this.state = {
indexingPolicyContentIsValid: true
};
}
componentDidUpdate(): void {
if (this.props.shouldDiscardIndexingPolicy) {
this.resetIndexingPolicyEditor();
this.props.resetShouldDiscardIndexingPolicy();
}
this.onComponentUpdate();
}
componentDidMount(): void {
this.resetIndexingPolicyEditor();
this.onComponentUpdate();
}
public resetIndexingPolicyEditor = (): void => {
if (!this.indexingPolicyEditor) {
this.createIndexingPolicyEditor();
} else {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
indexingPolicyEditorModel.setValue(value);
}
this.onComponentUpdate();
};
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
this.props.onIndexingPolicyDirtyChange(this.IsComponentDirty());
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): boolean => {
if (
isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) &&
this.state.indexingPolicyContentIsValid
) {
return true;
}
return false;
};
private createIndexingPolicyEditor = (): void => {
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
value: value,
language: "json",
readOnly: false,
ariaLabel: "Indexing Policy"
});
if (this.indexingPolicyEditor) {
this.indexingPolicyEditor.onDidFocusEditorText(() => this.props.onIndexingPolicyElementFocusChange(true));
this.indexingPolicyEditor.onDidBlurEditorText(() => this.props.onIndexingPolicyElementFocusChange(false));
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logIndexingPolicySuccessMessage();
}
};
private onEditorContentChange = (): void => {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
try {
const newIndexingPolicyContent = JSON.parse(indexingPolicyEditorModel.getValue()) as DataModels.IndexingPolicy;
this.props.onIndexingPolicyContentChange(newIndexingPolicyContent);
this.setState({ indexingPolicyContentIsValid: true });
} catch (e) {
this.setState({ indexingPolicyContentIsValid: false });
}
};
public render(): JSX.Element {
return (
<Stack {...titleAndInputStackProps}>
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>
);
}
}

View File

@@ -0,0 +1,160 @@
import { shallow } from "enzyme";
import React from "react";
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
import { container, collection } from "../TestUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import Explorer from "../../../Explorer";
import * as Constants from "../../../../Common/Constants";
import { PlatformType } from "../../../../PlatformType";
import * as DataModels from "../../../../Contracts/DataModels";
import { throughputUnit } from "../SettingsRenderUtils";
import * as SharedConstants from "../../../../Shared/Constants";
import ko from "knockout";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
nonNationalCloudContainer.getPlatformType = () => PlatformType.Portal;
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
const targetThroughput = 6000;
const baseProps: ScaleComponentProps = {
collection: collection,
container: container,
isFixedContainer: false,
autoPilotTiersList: [],
onThroughputChange: () => {
return;
},
throughput: 1000,
throughputBaseline: 1000,
autoPilotThroughput: 4000,
autoPilotThroughputBaseline: 4000,
isAutoPilotSelected: false,
wasAutopilotOriginallySet: true,
onAutoPilotSelected: () => false,
onMaxAutoPilotThroughputChange: () => {
return;
},
onScaleSaveableChange: () => {
return;
},
onScaleDiscardableChange: () => {
return;
},
initialNotification: {
description: `Throughput update for ${targetThroughput} ${throughputUnit}`
} as DataModels.Notification
};
it("renders with correct intiial notification", () => {
let wrapper = shallow(<ScaleComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
const newCollection = { ...collection };
const maxThroughput = 5000;
const targetMaxThroughput = 50000;
newCollection.offer = ko.observable({
content: {
offerAutopilotSettings: {
maxThroughput: maxThroughput,
targetMaxThroughput: targetMaxThroughput
}
},
headers: { "x-ms-offer-replace-pending": true }
} as DataModels.OfferWithHeaders);
const newProps = {
...baseProps,
initialNotification: undefined as DataModels.Notification,
collection: newCollection
};
wrapper = shallow(<ScaleComponent {...newProps} />);
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
});
it("autoScale disabled", () => {
const scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
});
it("autoScale enabled", () => {
const newContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
newContainer.databaseAccount({
id: undefined,
name: undefined,
location: undefined,
type: undefined,
kind: "documentdb",
tags: undefined,
properties: {
documentEndpoint: undefined,
tableEndpoint: undefined,
gremlinEndpoint: undefined,
cassandraEndpoint: undefined,
capabilities: [
{
name: Constants.CapabilityNames.EnableAutoScale.toLowerCase(),
description: undefined
}
]
}
});
const props = { ...baseProps, container: newContainer };
const scaleComponent = new ScaleComponent(props);
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
});
it("getMaxRUThroughputInputLimit", () => {
const scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
});
it("getThroughputTitle", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - 40,000 RU/s)");
let newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
newProps = { ...baseProps, isAutoPilotSelected: true };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
});
it("canThroughputExceedMaximumValue", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(false);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
});
it("getThroughputWarningMessage", () => {
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
let scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
newProps.throughput = throughputBeyondMaxRus;
scaleComponent = new ScaleComponent(newProps);
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
});
});

View File

@@ -0,0 +1,235 @@
import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants";
import Explorer from "../../../Explorer";
import { PlatformType } from "../../../../PlatformType";
import {
getTextFieldStyles,
subComponentStackProps,
titleAndInputStackProps,
throughputUnit,
getThroughputApplyLongDelayMessage,
getThroughputApplyShortDelayMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage
} from "../SettingsRenderUtils";
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
export interface ScaleComponentProps {
collection: ViewModels.Collection;
container: Explorer;
isFixedContainer: boolean;
autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
onThroughputChange: (newThroughput: number) => void;
throughput: number;
throughputBaseline: number;
autoPilotThroughput: number;
autoPilotThroughputBaseline: number;
isAutoPilotSelected: boolean;
wasAutopilotOriginallySet: boolean;
onAutoPilotSelected: (isAutoPilotSelected: boolean) => void;
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
initialNotification: DataModels.Notification;
}
export class ScaleComponent extends React.Component<ScaleComponentProps> {
private isEmulator: boolean;
constructor(props: ScaleComponentProps) {
super(props);
this.isEmulator = this.props.container.isEmulator;
}
public isAutoScaleEnabled = (): boolean => {
const accountCapabilities: DataModels.Capability[] = this.props.container?.databaseAccount()?.properties
?.capabilities;
const enableAutoScaleCapability =
accountCapabilities &&
accountCapabilities.find((capability: DataModels.Capability) => {
return (
capability &&
capability.name &&
capability.name.toLowerCase() === Constants.CapabilityNames.EnableAutoScale.toLowerCase()
);
});
return !!enableAutoScaleCapability;
};
private getStorageCapacityTitle = (): JSX.Element => {
// Mongo container with system partition key still treat as "Fixed"
const isFixed =
!this.props.collection.partitionKey ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
const capacity: string = isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label>Storage capacity</Label>
<Text>{capacity}</Text>
</Stack>
);
};
public getMaxRUThroughputInputLimit = (): number => {
if (this.props.container?.getPlatformType() === PlatformType.Hosted && this.props.collection.partitionKey) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
return getMaxRUs(this.props.collection, this.props.container);
};
public getThroughputTitle = (): string => {
if (this.props.isAutoPilotSelected) {
return AutoPilotUtils.getAutoPilotHeaderText(false);
}
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
const maxThroughput: string =
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
? "unlimited"
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
};
public canThroughputExceedMaximumValue = (): boolean => {
const isPublicAzurePortal: boolean =
this.props.container.getPlatformType() === PlatformType.Portal &&
!this.props.container.isRunningOnNationalCloud();
const hasPartitionKey = !!this.props.collection.partitionKey;
return isPublicAzurePortal && hasPartitionKey;
};
public getInitialNotificationElement = (): JSX.Element => {
if (this.props.initialNotification) {
return this.getLongDelayMessage();
}
const offer = this.props.collection?.offer && this.props.collection.offer();
if (
offer &&
Object.keys(offer).find(value => {
return value === "headers";
}) &&
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
) {
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
const targetThroughput =
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
return undefined;
};
public getThroughputWarningMessage = (): JSX.Element => {
const throughputExceedsBackendLimits: boolean =
this.canThroughputExceedMaximumValue() &&
getMaxRUs(this.props.collection, this.props.container) <=
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage;
}
const throughputExceedsMaxValue: boolean =
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputDelayedApplyWarningMessage;
}
return undefined;
};
public getLongDelayMessage = (): JSX.Element => {
const matches: string[] = this.props.initialNotification?.description.match(
`Throughput update for (.*) ${throughputUnit}`
);
const throughput = this.props.throughputBaseline;
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
if (targetThroughput) {
return getThroughputApplyLongDelayMessage(
this.props.wasAutopilotOriginallySet,
throughput,
throughputUnit,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
return <></>;
};
private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()}
serverId={this.props.container.serverId()}
throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange}
minimum={getMinRUs(this.props.collection, this.props.container)}
maximum={this.getMaxRUThroughputInputLimit()}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
label={this.getThroughputTitle()}
isEmulator={this.isEmulator}
isFixed={this.props.isFixedContainer}
isAutoPilotSelected={this.props.isAutoPilotSelected}
onAutoPilotSelected={this.props.onAutoPilotSelected}
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
maxAutoPilotThroughput={this.props.autoPilotThroughput}
maxAutoPilotThroughputBaseline={this.props.autoPilotThroughputBaseline}
onMaxAutoPilotThroughputChange={this.props.onMaxAutoPilotThroughputChange}
spendAckChecked={false}
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
/>
);
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.getInitialNotificationElement() && (
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
)}
{!this.isAutoScaleEnabled() && (
<Stack {...subComponentStackProps}>
{this.getThroughputInputComponent()}
{this.getStorageCapacityTitle()}
</Stack>
)}
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}>
<Text>Throughput (RU/s)</Text>
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
<Text>
Your account has custom settings that prevents setting throughput at the container level. Please work with
your Cosmos DB engineering team point of contact to make changes.
</Text>
</Stack>
)}
</Stack>
);
}
}

View File

@@ -0,0 +1,136 @@
import { shallow } from "enzyme";
import React from "react";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
import { container, collection } from "../TestUtils";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState } from "../SettingsUtils";
import ko from "knockout";
import Explorer from "../../../Explorer";
describe("SubSettingsComponent", () => {
container.isPreferredApiDocumentDB = ko.computed(() => true);
const baseProps: SubSettingsComponentProps = {
collection: collection,
container: container,
timeToLive: TtlType.On,
timeToLiveBaseline: TtlType.On,
onTtlChange: () => {
return;
},
timeToLiveSeconds: 1000,
timeToLiveSecondsBaseline: 1000,
onTimeToLiveSecondsChange: () => {
return;
},
geospatialConfigType: GeospatialConfigType.Geography,
geospatialConfigTypeBaseline: GeospatialConfigType.Geography,
onGeoSpatialConfigTypeChange: () => {
return;
},
isAnalyticalStorageEnabled: true,
analyticalStorageTtlSelection: TtlType.On,
analyticalStorageTtlSelectionBaseline: TtlType.On,
onAnalyticalStorageTtlSelectionChange: () => {
return;
},
analyticalStorageTtlSeconds: 2000,
analyticalStorageTtlSecondsBaseline: 2000,
onAnalyticalStorageTtlSecondsChange: () => {
return;
},
changeFeedPolicyVisible: true,
changeFeedPolicy: ChangeFeedPolicyState.On,
changeFeedPolicyBaseline: ChangeFeedPolicyState.On,
onChangeFeedPolicyChange: () => {
return;
},
onSubSettingsSaveableChange: () => {
return;
},
onSubSettingsDiscardableChange: () => {
return;
}
};
it("renders", () => {
const wrapper = shallow(<SubSettingsComponent {...baseProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#timeToLive")).toEqual(true);
expect(wrapper.exists("#timeToLiveSeconds")).toEqual(true);
expect(wrapper.exists("#geoSpatialConfig")).toEqual(true);
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(true);
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(true);
expect(wrapper.exists("#changeFeedPolicy")).toEqual(true);
});
it("timeToLiveSeconds hidden", () => {
const props = { ...baseProps, timeToLive: TtlType.Off };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#timeToLive")).toEqual(true);
expect(wrapper.exists("#timeToLiveSeconds")).toEqual(false);
});
it("analyticalTimeToLive hidden", () => {
const props = { ...baseProps, isAnalyticalStorageEnabled: false };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(false);
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(false);
});
it("analyticalTimeToLiveSeconds hidden", () => {
const props = { ...baseProps, analyticalStorageTtlSelection: TtlType.OnNoDefault };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(true);
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(false);
});
it("changeFeedPolicy hidden", () => {
const props = { ...baseProps, changeFeedPolicyVisible: false };
const wrapper = shallow(<SubSettingsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.exists("#changeFeedPolicy")).toEqual(false);
});
it("partitionKey is visible", () => {
const subSettingsComponent = new SubSettingsComponent(baseProps);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(true);
});
it("partitionKey not visible", () => {
const newContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
newContainer.isPreferredApiCassandra = ko.computed(() => true);
const props = { ...baseProps, container: newContainer };
const subSettingsComponent = new SubSettingsComponent(props);
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);
});
it("largePartitionKey is enabled", () => {
const subSettingsComponent = new SubSettingsComponent(baseProps);
expect(subSettingsComponent.isLargePartitionKeyEnabled()).toEqual(true);
});
it("sub settings saveable and discardable are set", () => {
let subSettingsComponent = new SubSettingsComponent(baseProps);
let isComponentDirtyResult = subSettingsComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(false);
expect(isComponentDirtyResult.isDiscardable).toEqual(false);
const newProps = { ...baseProps, timeToLive: TtlType.OnNoDefault };
subSettingsComponent = new SubSettingsComponent(newProps);
isComponentDirtyResult = subSettingsComponent.IsComponentDirty();
expect(isComponentDirtyResult.isSaveable).toEqual(true);
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
});
});

View File

@@ -0,0 +1,296 @@
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import {
GeospatialConfigType,
TtlType,
ChangeFeedPolicyState,
isDirty,
IsComponentDirtyResult
} from "../SettingsUtils";
import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
Label,
Text,
TextField,
Stack,
IChoiceGroupOption,
ChoiceGroup,
MessageBar,
MessageBarType
} from "office-ui-fabric-react";
import {
getTextFieldStyles,
changeFeedPolicyToolTip,
subComponentStackProps,
titleAndInputStackProps,
getChoiceGroupStyles,
ttlWarning,
messageBarStyles
} from "../SettingsRenderUtils";
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
export interface SubSettingsComponentProps {
collection: ViewModels.Collection;
container: Explorer;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
onTtlChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
onTimeToLiveSecondsChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
onGeoSpatialConfigTypeChange: (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
) => void;
isAnalyticalStorageEnabled: boolean;
analyticalStorageTtlSelection: TtlType;
analyticalStorageTtlSelectionBaseline: TtlType;
onAnalyticalStorageTtlSelectionChange: (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
) => void;
analyticalStorageTtlSeconds: number;
analyticalStorageTtlSecondsBaseline: number;
onAnalyticalStorageTtlSecondsChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
changeFeedPolicyVisible: boolean;
changeFeedPolicy: ChangeFeedPolicyState;
changeFeedPolicyBaseline: ChangeFeedPolicyState;
onChangeFeedPolicyChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
onSubSettingsSaveableChange: (isSubSettingsSaveable: boolean) => void;
onSubSettingsDiscardableChange: (isSubSettingsDiscardable: boolean) => void;
}
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
private shouldCheckComponentIsDirty = true;
private ttlVisible: boolean;
private geospatialVisible: boolean;
private partitionKeyValue: string;
private partitionKeyName: string;
constructor(props: SubSettingsComponentProps) {
super(props);
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key";
}
componentDidMount(): void {
this.onComponentUpdate();
}
componentDidUpdate(): void {
this.onComponentUpdate();
}
private onComponentUpdate = (): void => {
if (!this.shouldCheckComponentIsDirty) {
this.shouldCheckComponentIsDirty = true;
return;
}
const isComponentDirtyResult = this.IsComponentDirty();
this.props.onSubSettingsSaveableChange(isComponentDirtyResult.isSaveable);
this.props.onSubSettingsDiscardableChange(isComponentDirtyResult.isDiscardable);
this.shouldCheckComponentIsDirty = false;
};
public IsComponentDirty = (): IsComponentDirtyResult => {
if (
(this.props.timeToLive === TtlType.On && !this.props.timeToLiveSeconds) ||
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds)
) {
return { isSaveable: false, isDiscardable: true };
} else if (
isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) ||
(this.props.timeToLive === TtlType.On &&
isDirty(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)) ||
isDirty(this.props.analyticalStorageTtlSelection, this.props.analyticalStorageTtlSelectionBaseline) ||
(this.props.analyticalStorageTtlSelection === TtlType.On &&
isDirty(this.props.analyticalStorageTtlSeconds, this.props.analyticalStorageTtlSecondsBaseline)) ||
isDirty(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline) ||
isDirty(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)
) {
return { isSaveable: true, isDiscardable: true };
}
return { isSaveable: false, isDiscardable: false };
};
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off" },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" }
];
private getTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.props.onTtlChange}
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/>
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{ttlWarning}
</MessageBar>
)}
{this.props.timeToLive === TtlType.On && (
<TextField
id="timeToLiveSeconds"
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.props.onTimeToLiveSecondsChange}
suffix="second(s)"
/>
)}
</Stack>
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" }
];
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="analyticalStorageTimeToLive"
label="Analytical Storage Time to Live"
selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions}
onChange={this.props.onAnalyticalStorageTtlSelectionChange}
styles={getChoiceGroupStyles(
this.props.analyticalStorageTtlSelection,
this.props.analyticalStorageTtlSelectionBaseline
)}
/>
{this.props.analyticalStorageTtlSelection === TtlType.On && (
<TextField
id="analyticalStorageTimeToLiveSeconds"
styles={getTextFieldStyles(
this.props.analyticalStorageTtlSeconds,
this.props.analyticalStorageTtlSecondsBaseline
)}
type="number"
required
min={1}
max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix="second(s)"
onChange={this.props.onAnalyticalStorageTtlSecondsChange}
/>
)}
</Stack>
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography" },
{ key: GeospatialConfigType.Geometry, text: "Geometry" }
];
private getGeoSpatialComponent = (): JSX.Element => (
<ChoiceGroup
id="geoSpatialConfig"
label="Geospatial Configuration"
selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.props.onGeoSpatialConfigTypeChange}
styles={getChoiceGroupStyles(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline)}
/>
);
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: ChangeFeedPolicyState.Off, text: "Off" },
{ key: ChangeFeedPolicyState.On, text: "On" }
];
private getChangeFeedComponent = (): JSX.Element => {
const labelId = "settingsV2ChangeFeedLabelId";
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
</Label>
<ChoiceGroup
id="changeFeedPolicy"
selectedKey={this.props.changeFeedPolicy}
options={this.changeFeedChoiceGroupOptions}
onChange={this.props.onChangeFeedPolicyChange}
styles={getChoiceGroupStyles(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)}
aria-labelledby={labelId}
/>
</Stack>
);
};
private getPartitionKeyComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
{this.getPartitionKeyVisible() && (
<TextField
label={this.partitionKeyName}
disabled
styles={getTextFieldStyles(undefined, undefined)}
defaultValue={this.partitionKeyValue}
/>
)}
{this.isLargePartitionKeyEnabled() && <Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>}
</Stack>
);
public getPartitionKeyVisible = (): boolean => {
if (
this.props.container.isPreferredApiCassandra() ||
this.props.container.isPreferredApiTable() ||
!this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
) {
return false;
}
return true;
};
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
public render(): JSX.Element {
return (
<Stack {...subComponentStackProps}>
{this.ttlVisible && this.getTtlComponent()}
{this.geospatialVisible && this.getGeoSpatialComponent()}
{this.props.isAnalyticalStorageEnabled && this.getAnalyticalStorageTtlComponent()}
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
{this.getPartitionKeyComponent()}
</Stack>
);
}
}

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

View File

@@ -0,0 +1,15 @@
import { shallow } from "enzyme";
import React from "react";
import { ToolTipLabelComponent, ToolTipLabelComponentProps } from "./ToolTipLabelComponent";
describe("ToolTipLabelComponent", () => {
const props: ToolTipLabelComponentProps = {
label: "sample tool tip label",
toolTipElement: <span>sample tool tip text</span>
};
it("renders", () => {
const wrapper = shallow(<ToolTipLabelComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,32 @@
import * as React from "react";
import { Stack, Text, IIconStyles, Icon, TooltipHost, DirectionalHint } from "office-ui-fabric-react";
import { toolTipLabelStackTokens } from "../SettingsRenderUtils";
export interface ToolTipLabelComponentProps {
label: string;
toolTipElement: JSX.Element;
}
const iconButtonStyles: Partial<IIconStyles> = { root: { marginBottom: -3 } };
export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponentProps> {
public render(): JSX.Element {
return (
<>
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>
{this.props.label && <Text style={{ fontWeight: 600 }}>{this.props.label}</Text>}
{this.props.toolTipElement && (
<TooltipHost
content={this.props.toolTipElement}
directionalHint={DirectionalHint.rightCenter}
calloutProps={{ gapSpace: 0 }}
styles={{ root: { display: "inline-block", float: "right" } }}
>
<Icon iconName="Info" ariaLabel="Info" styles={iconButtonStyles} />
</TooltipHost>
)}
</Stack>
</>
);
}
}

View File

@@ -0,0 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConflictResolutionComponent Path text field displayed 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StyledChoiceGroupBase
label="Mode"
onChange={[Function]}
options={
Array [
Object {
"key": "LastWriterWins",
"text": "Last Write Wins (default)",
},
Object {
"key": "Custom",
"text": "Merge Procedure (custom)",
},
]
}
selectedKey="LastWriterWins"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": undefined,
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
"fontSize": 14,
"padding": "2px 5px",
"whiteSpace": "nowrap",
},
},
},
],
}
}
/>
<StyledTextFieldBase
id="conflictResolutionLwwTextField"
label="Conflict Resolver Property"
onChange={[Function]}
onRenderLabel={[Function]}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
value=""
/>
</Stack>
`;
exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StyledChoiceGroupBase
label="Mode"
onChange={[Function]}
options={
Array [
Object {
"key": "LastWriterWins",
"text": "Last Write Wins (default)",
},
Object {
"key": "Custom",
"text": "Merge Procedure (custom)",
},
]
}
selectedKey="Custom"
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",
},
},
},
],
}
}
/>
<StyledTextFieldBase
id="conflictResolutionCustomTextField"
label="Stored procedure"
onChange={[Function]}
onRenderLabel={[Function]}
styles={
Object {
"fieldGroup": Object {
"borderColor": "",
"height": 25,
"selectors": Object {
":disabled": Object {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
value=""
/>
</Stack>
`;

View File

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IndexingPolicyComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<div
className="settingsV2IndexingPolicyEditor"
tabIndex={0}
/>
</Stack>
`;

View File

@@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScaleComponent renders with correct intiial notification 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StyledMessageBarBase
messageBarType={5}
>
<Text
id="throughputApplyLongDelayMessage"
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
test
, Container:
test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text>
</StyledMessageBarBase>
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={false}
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
isFixed={false}
label="Throughput (6,000 - 40,000 RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={40000}
minimum={6000}
onAutoPilotSelected={[Function]}
onMaxAutoPilotThroughputChange={[Function]}
onScaleDiscardableChange={[Function]}
onScaleSaveableChange={[Function]}
onThroughputChange={[Function]}
spendAckChecked={false}
throughput={1000}
throughputBaseline={1000}
wasAutopilotOriginallySet={true}
/>
<Stack
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledLabelBase>
Storage capacity
</StyledLabelBase>
<Text>
Unlimited
</Text>
</Stack>
</Stack>
</Stack>
`;

View File

@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ToolTipLabelComponent renders 1`] = `
<Fragment>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 6,
}
}
verticalAlign="center"
>
<Text
style={
Object {
"fontWeight": 600,
}
}
>
sample tool tip label
</Text>
<StyledTooltipHostBase
calloutProps={
Object {
"gapSpace": 0,
}
}
content={
<span>
sample tool tip text
</span>
}
directionalHint={12}
styles={
Object {
"root": Object {
"display": "inline-block",
"float": "right",
},
}
}
>
<Memo(StyledIconBase)
ariaLabel="Info"
iconName="Info"
styles={
Object {
"root": Object {
"marginBottom": -3,
},
}
}
/>
</StyledTooltipHostBase>
</Stack>
</Fragment>
`;