Fix throughput input component and add database panel (#773)

This commit is contained in:
victor-meng 2021-05-12 11:56:24 -07:00 committed by GitHub
parent 0a6c7c0ff9
commit 2f6dbd83f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 177 additions and 299 deletions

View File

@ -19,18 +19,4 @@ describe("ThroughputInput Pane", () => {
it("should render Default properly", () => {
expect(wrapper).toMatchSnapshot();
});
it("test Autoscale Mode select", () => {
wrapper.setProps({ isAutoscaleSelected: true });
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toBe(
"Estimate your required RU/s with capacity calculator."
);
expect(wrapper.find('[aria-label="maxRUDescription"]').at(0).text()).toContain("Max RU/s");
});
it("test Manual Mode select", () => {
wrapper.setProps({ isAutoscaleSelected: false });
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toContain("Estimate your required RU/s with");
expect(wrapper.find('[aria-label="capacityLink"]').at(0).text()).toContain("capacity calculator");
});
});

View File

@ -17,8 +17,6 @@ export interface ThroughputInputProps {
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
isAutoscaleSelected?: boolean;
throughput?: number;
}
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
@ -27,12 +25,16 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setThroughputValue,
setIsAutoscale,
isSharded,
isAutoscaleSelected = true,
throughput = AutoPilotUtils.minAutoPilotThroughput,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
setIsAutoscale(isAutoscaleSelected);
setThroughputValue(throughput);
const getThroughputLabelText = (): string => {
let throughputHeaderText: string;
if (isAutoscaleSelected) {
@ -49,6 +51,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const onThroughputValueChange = (newInput: string): void => {
const newThroughput = parseInt(newInput);
setThroughput(newThroughput);
setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) {
setThroughputError("Unsharded collections support up to 10,000 RUs");
@ -82,9 +85,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
}

View File

@ -25,7 +25,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { isCapabilityEnabled } from "../../Utils/CapabilityUtils";
import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@ -182,7 +182,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
{!this.isServerlessAccount() && (
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
@ -207,14 +207,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{!this.isServerlessAccount() && this.state.isSharedThroughputChecked && (
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={true}
isAutoscaleSelected={this.isNewDatabaseAutoscale}
throughput={this.newDatabaseThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
@ -398,7 +396,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (
userContext.apiType !== "Mongo" &&
this.state.partitionKey === "" &&
!this.state.partitionKey &&
!event.target.value.startsWith("/")
) {
this.setState({ partitionKey: "/" + event.target.value });
@ -410,7 +408,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
@ -444,8 +442,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={false}
isAutoscaleSelected={this.isCollectionAutoscale}
throughput={this.collectionThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@ -755,14 +751,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return userContext.databaseAccount?.properties?.enableFreeTier;
}
private isServerlessAccount(): boolean {
return userContext.databaseAccount.properties?.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableServerless
);
}
private getSharedThroughputDefault(): boolean {
return userContext.subscriptionType !== SubscriptionType.EA && !this.isServerlessAccount();
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
}
private getFreeTierIndexingText(): string {
@ -800,7 +790,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowCollectionThroughputInput(): boolean {
if (this.isServerlessAccount()) {
if (isServerlessAccount()) {
return false;
}
@ -830,7 +820,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (this.isServerlessAccount()) {
if (isServerlessAccount()) {
return false;
}

View File

@ -1,10 +1,9 @@
import { Checkbox, Text, TextField } from "@fluentui/react";
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { configContext, Platform } from "../../../ConfigContext";
import * as DataModels from "../../../Contracts/DataModels";
import { SubscriptionType } from "../../../Contracts/SubscriptionType";
import * as SharedConstants from "../../../Shared/Constants";
@ -12,7 +11,8 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../../Utils/PricingUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
@ -29,17 +29,10 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
closePanel,
openNotificationConsole,
}: AddDatabasePaneProps) => {
let throughput: number;
let isAutoscaleSelected: boolean;
let isCostAcknowledged: boolean;
const { subscriptionType } = userContext;
const getSharedThroughputDefault = !(subscriptionType === SubscriptionType.EA || container.isServerlessEnabled());
const _isAutoPilotSelectedAndWhatTier = (): DataModels.AutoPilotCreationSettings => {
if (isAutoPilotSelected && maxAutoPilotThroughputSet) {
return {
maxThroughput: maxAutoPilotThroughputSet * 1,
};
}
return undefined;
};
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
@ -52,61 +45,14 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(getSharedThroughputDefault);
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
subscriptionType !== SubscriptionType.EA && !isServerlessAccount()
);
const [formErrorsDetails, setFormErrorsDetails] = useState<string>();
const [formErrors, setFormErrors] = useState<string>("");
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(container.isAutoscaleDefaultEnabled());
const throughputDefaults = container.collectionCreationDefaults.throughput;
const [throughput, setThroughput] = useState<number>(
isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared
);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
const canRequestSupport = () => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = throughput;
return offerThroughput <= 100000;
}
return false;
};
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const upsellMessage: string = PricingUtils.getUpsellMessage(
userContext.portalEnv,
isFreeTierAccount,
container.isFirstResourceCreated(),
false
);
const upsellAnchorUrl: string = isFreeTierAccount ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
const upsellAnchorText: string = isFreeTierAccount ? "Learn more" : "More details";
const maxAutoPilotThroughputSet = AutoPilotUtils.minAutoPilotThroughput;
const canConfigureThroughput = !container.isServerlessEnabled();
const showUpsellMessage = () => {
if (container.isServerlessEnabled()) {
return false;
}
if (isFreeTierAccount) {
return databaseCreateNewShared;
}
return true;
};
const [isExecuting, setIsExecuting] = useState<boolean>(false);
useEffect(() => {
setDatabaseCreateNewShared(getSharedThroughputDefault);
}, [subscriptionType]);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const addDatabasePaneMessage = {
database: {
@ -126,7 +72,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
subscriptionType: SubscriptionType[subscriptionType],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: throughput,
throughput,
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
@ -139,11 +85,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
return;
}
const offerThroughput: number = _computeOfferThroughput();
const addDatabasePaneStartMessage = {
...addDatabasePaneMessage,
offerThroughput,
throughput,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
setFormErrors("");
@ -153,18 +97,18 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (isAutoPilotSelected) {
createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.offerThroughput;
if (isAutoscaleSelected) {
createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.throughput;
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.throughput;
}
createDatabase(createDatabaseParams).then(
() => {
_onCreateDatabaseSuccess(offerThroughput, startKey);
_onCreateDatabaseSuccess(throughput, startKey);
},
(error: string) => {
_onCreateDatabaseFailure(error, offerThroughput, startKey);
_onCreateDatabaseFailure(error, throughput, startKey);
}
);
};
@ -194,48 +138,19 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
};
const _getThroughput = (): number => {
return isNaN(throughput) ? 0 : Number(throughput);
};
const _computeOfferThroughput = (): number => {
if (!canConfigureThroughput) {
return undefined;
}
return _getThroughput();
};
const _isValid = (): boolean => {
// TODO add feature flag that disables validation for customers with custom accounts
if (isAutoPilotSelected) {
const autoPilot = _isAutoPilotSelectedAndWhatTier();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
const throughput = _getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
setFormErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = maxAutoPilotThroughputSet * 1;
if (
isAutoPilotSelected &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!throughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
return false;
}
@ -250,7 +165,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
);
const props: RightPaneFormProps = {
expandConsole: container.expandConsole,
expandConsole: openNotificationConsole,
formError: formErrors,
formErrorDetail: formErrorsDetails,
isExecuting,
@ -260,24 +175,23 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
return (
<RightPaneForm {...props}>
<div className="paneContentContainer" role="dialog" aria-labelledby="databaseTitle">
{showUpsellMessage && formErrors === "" && (
{!formErrors && isFreeTierAccount && (
<PanelInfoErrorComponent
message={upsellMessage}
message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)}
messageType="info"
showErrorDetails={false}
openNotificationConsole={openNotificationConsole}
link={upsellAnchorUrl}
linkText={upsellAnchorText}
link={Constants.Urls.freeTierInformation}
linkText="Learn more"
/>
)}
<div className="paneMainContent">
<div className="panelMainContent">
<div>
<p>
<Stack horizontal>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</p>
</Stack>
<TextField
id="database-id"
@ -292,50 +206,36 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
value={databaseId}
onChange={handleonChangeDBId}
autoFocus
style={{ fontSize: 12 }}
styles={{ root: { width: 300 } }}
/>
<div
className="databaseProvision"
aria-label="New database provision support"
style={{ display: "block ruby" }}
>
<Stack horizontal>
<Checkbox
title="Provision shared throughput"
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>{" "}
/>
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</div>
{databaseCreateNewShared && (
<div>
</Stack>
{!isServerlessAccount() && databaseCreateNewShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isAutoscaleSelected={isAutoPilotSelected}
throughput={throughput}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
{canRequestSupport() && (
<p>
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">
Contact support{" "}
</a>
for more than <span>{throughputDefaults.unlimitedmax?.toLocaleString()} </span> RU/s.
</p>
)}
</div>
)}
</div>
</div>
</div>
</RightPaneForm>
);

View File

@ -9,23 +9,12 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
submitButtonText="OK"
>
<div
aria-labelledby="databaseTitle"
className="paneContentContainer"
role="dialog"
>
<PanelInfoErrorComponent
link="https://aka.ms/azure-cosmos-db-pricing"
linkText="More details"
message="Start at $24/mo per database, multiple containers included"
messageType="info"
openNotificationConsole={[Function]}
showErrorDetails={false}
/>
<div
className="paneMainContent"
className="panelMainContent"
>
<div>
<p>
<Stack
horizontal={true}
>
<span
className="mandatoryStar"
>
@ -39,7 +28,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
<InfoTooltip>
A database is a logical container of one or more collections
</InfoTooltip>
</p>
</Stack>
<StyledTextFieldBase
aria-label="Database id"
aria-required="true"
@ -50,18 +39,24 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Type a new database id"
size={40}
style={
Object {
"fontSize": 12,
}
}
styles={
Object {
"root": Object {
"width": 300,
},
}
}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
type="text"
value=""
/>
<div
aria-label="New database provision support"
className="databaseProvision"
style={
Object {
"display": "block ruby",
}
}
<Stack
horizontal={true}
>
<StyledCheckboxBase
checked={true}
@ -77,28 +72,25 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
"alignItems": "center",
"padding": 0,
},
"text": Object {
"fontSize": 12,
},
}
}
title="Provision shared throughput"
/>
<InfoTooltip>
Provisioned throughput at the database level will be shared across all collections within the database.
</InfoTooltip>
</div>
<div>
</Stack>
<ThroughputInput
isAutoscaleSelected={false}
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
throughput={400}
/>
</div>
</div>
</div>
</div>
</RightPaneForm>
`;

View File

@ -1,4 +1,7 @@
import * as Constants from "../Common/Constants";
import { userContext } from "../UserContext";
export const isCapabilityEnabled = (capabilityName: string): boolean =>
userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName);
export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless);

View File

@ -18,7 +18,7 @@ export const useSidePanel = (): SidePanelHooks => {
setHeaderText(headerText);
setPanelContent(panelContent);
setIsPanelOpen(true);
setOnCloseCallback({ callback: onClose });
!!onClose && setOnCloseCallback({ callback: onClose });
};
const closeSidePanel = (): void => {