From 64f36e2d284ccae2478e1267c9a7b75ccb989920 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Sat, 30 Oct 2021 19:45:16 -0700 Subject: [PATCH] Add throughput cap error message (#1151) --- src/Contracts/DataModels.ts | 1 + .../Controls/Settings/SettingsComponent.tsx | 47 +++++++++++++- .../SettingsSubComponents/ScaleComponent.tsx | 2 + .../ThroughputInputAutoPilotV3Component.tsx | 3 + .../ThroughputInput/ThroughputInput.test.tsx | 1 + .../ThroughputInput/ThroughputInput.tsx | 65 ++++++++++++++++++- .../ThroughputInput.test.tsx.snap | 1 + src/Explorer/Panes/AddCollectionPanel.tsx | 10 ++- .../AddDatabasePanel/AddDatabasePanel.tsx | 3 + .../AddDatabasePanel.test.tsx.snap | 2 + .../CassandraAddCollectionPane.tsx | 4 ++ ...teCollectionConfirmationPane.test.tsx.snap | 6 ++ .../ExecuteSprocParamsPane.test.tsx.snap | 6 ++ src/Explorer/Panes/PanelFooterComponent.tsx | 10 ++- .../Panes/RightPaneForm/RightPaneForm.tsx | 6 +- .../__snapshots__/RightPaneForm.test.tsx.snap | 6 ++ .../StringInputPane.test.tsx.snap | 6 ++ .../TableQuerySelectPanel.test.tsx.snap | 6 ++ .../AddTableEntityPanel.test.tsx.snap | 6 ++ .../EditTableEntityPanel.test.tsx.snap | 6 ++ ...eteDatabaseConfirmationPanel.test.tsx.snap | 6 ++ src/Platform/Hosted/extractFeatures.ts | 2 + .../generatedClients/cosmos/sqlResources.ts | 2 +- src/hooks/useDatabaseAccounts.tsx | 5 +- 24 files changed, 200 insertions(+), 12 deletions(-) diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 25d73d189..99db16f11 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -26,6 +26,7 @@ export interface DatabaseAccountExtendedProperties { isVirtualNetworkFilterEnabled?: boolean; ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; + capacity?: { totalThroughputLimit: number }; } export interface DatabaseAccountResponseLocation { diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 0094806c6..880a50b99 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1,4 +1,5 @@ import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react"; +import { useDatabases } from "Explorer/useDatabases"; import * as React from "react"; import DiscardIcon from "../../../../images/discard.svg"; import SaveIcon from "../../../../images/save-cosmos.svg"; @@ -71,6 +72,7 @@ export interface SettingsComponentState { wasAutopilotOriginallySet: boolean; isScaleSaveable: boolean; isScaleDiscardable: boolean; + throughputError: string; timeToLive: TtlType; timeToLiveBaseline: TtlType; @@ -124,6 +126,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S private changeFeedPolicyVisible: boolean; private isFixedContainer: boolean; private shouldShowIndexingPolicyEditor: boolean; + private totalThroughputUsed: number; public mongoDBCollectionResource: MongoDBCollectionResource; constructor(props: SettingsComponentProps) { @@ -155,6 +158,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S wasAutopilotOriginallySet: false, isScaleSaveable: false, isScaleDiscardable: false, + throughputError: undefined, timeToLive: undefined, timeToLiveBaseline: undefined, @@ -208,6 +212,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S return true; }, }; + + this.totalThroughputUsed = 0; + (useDatabases.getState().databases || []).forEach((database) => { + if (database.offer()) { + const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput; + this.totalThroughputUsed += dbThroughput; + } + + (database.collections() || []).forEach((collection) => { + if (collection.offer()) { + const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput; + this.totalThroughputUsed += colThroughput; + } + }); + }); } componentDidMount(): void { @@ -254,6 +273,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S return false; } + if (this.state.throughputError) { + return false; + } + return ( this.state.isScaleSaveable || this.state.isSubSettingsSaveable || @@ -643,10 +666,27 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S return buttons; }; - private onMaxAutoPilotThroughputChange = (newThroughput: number): void => - this.setState({ autoPilotThroughput: newThroughput }); + private onMaxAutoPilotThroughputChange = (newThroughput: number): void => { + let throughputError = ""; + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + if (throughputCap && throughputCap - this.totalThroughputUsed < newThroughput - this.offer.autoscaleMaxThroughput) { + throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ + this.totalThroughputUsed + newThroughput + } RU/s. Change total throughput limit in cost management.`; + } + this.setState({ autoPilotThroughput: newThroughput, throughputError }); + }; - private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput }); + private onThroughputChange = (newThroughput: number): void => { + let throughputError = ""; + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + if (throughputCap && throughputCap - this.totalThroughputUsed < newThroughput - this.offer.manualThroughput) { + throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ + this.totalThroughputUsed + newThroughput + } RU/s. Change total throughput limit in cost management.`; + } + this.setState({ throughput: newThroughput, throughputError }); + }; private onAutoPilotSelected = (isAutoPilotSelected: boolean): void => this.setState({ isAutoPilotSelected: isAutoPilotSelected }); @@ -893,6 +933,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S onScaleSaveableChange: this.onScaleSaveableChange, onScaleDiscardableChange: this.onScaleDiscardableChange, initialNotification: this.props.settingsTab.pendingNotification(), + throughputError: this.state.throughputError, }; if (!this.isCollectionSettingsTab) { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 98914e33a..c1739171d 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -36,6 +36,7 @@ export interface ScaleComponentProps { onScaleSaveableChange: (isScaleSaveable: boolean) => void; onScaleDiscardableChange: (isScaleDiscardable: boolean) => void; initialNotification: DataModels.Notification; + throughputError?: string; } export class ScaleComponent extends React.Component<ScaleComponentProps> { @@ -189,6 +190,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> { onScaleDiscardableChange={this.props.onScaleDiscardableChange} getThroughputWarningMessage={this.getThroughputWarningMessage} usageSizeInKB={this.props.collection?.usageSizeInKB()} + throughputError={this.props.throughputError} /> ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 609d16b2d..c958b5a6a 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -75,6 +75,7 @@ export interface ThroughputInputAutoPilotV3Props { onScaleDiscardableChange: (isScaleDiscardable: boolean) => void; getThroughputWarningMessage: () => JSX.Element; usageSizeInKB: number; + throughputError?: string; } interface ThroughputInputAutoPilotV3State { @@ -540,6 +541,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()} onChange={this.onAutoPilotThroughputChange} min={minAutoPilotThroughput} + errorMessage={this.props.throughputError} /> {!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()} {this.minRUperGBSurvey()} @@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< } onChange={this.onThroughputChange} min={this.props.minimum} + errorMessage={this.props.throughputError} /> {this.state.exceedFreeTierThroughput && ( <MessageBar diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx index 6995f240a..9d1733da0 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.test.tsx @@ -7,6 +7,7 @@ const props = { isSharded: true, setThroughputValue: () => jest.fn(), setIsAutoscale: () => jest.fn(), + setIsThroughputCapExceeded: () => jest.fn(), onCostAcknowledgeChange: () => jest.fn(), }; describe("ThroughputInput Pane", () => { diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index de8b7757a..1b5972d36 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -1,5 +1,6 @@ import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; -import React, { FunctionComponent, useState } from "react"; +import { useDatabases } from "Explorer/useDatabases"; +import React, { FunctionComponent, useEffect, useState } from "react"; import * as Constants from "../../../Common/Constants"; import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; import * as SharedConstants from "../../../Shared/Constants"; @@ -16,6 +17,7 @@ export interface ThroughputInputProps { showFreeTierExceedThroughputTooltip: boolean; setThroughputValue: (throughput: number) => void; setIsAutoscale: (isAutoscale: boolean) => void; + setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void; onCostAcknowledgeChange: (isAcknowledged: boolean) => void; } @@ -24,6 +26,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({ showFreeTierExceedThroughputTooltip, setThroughputValue, setIsAutoscale, + setIsThroughputCapExceeded, isSharded, onCostAcknowledgeChange, }: ThroughputInputProps) => { @@ -31,10 +34,58 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({ const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput); const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false); const [throughputError, setThroughputError] = useState<string>(""); + const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0); setIsAutoscale(isAutoscaleSelected); setThroughputValue(throughput); + const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; + + useEffect(() => { + // throughput cap check for the initial state + let totalThroughput = 0; + (useDatabases.getState().databases || []).forEach((database) => { + if (database.offer()) { + const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput; + totalThroughput += dbThroughput; + } + + (database.collections() || []).forEach((collection) => { + if (collection.offer()) { + const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput; + totalThroughput += colThroughput; + } + }); + }); + setTotalThroughputUsed(totalThroughput); + + if (throughputCap && throughputCap - totalThroughput < throughput) { + setThroughputError( + `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ + totalThroughputUsed + throughput + } RU/s. Change total throughput limit in cost management.` + ); + + setIsThroughputCapExceeded(true); + } + }, []); + + const checkThroughputCap = (newThroughput: number): boolean => { + if (throughputCap && throughputCap - totalThroughputUsed < newThroughput) { + setThroughputError( + `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ + totalThroughputUsed + newThroughput + } RU/s. Change total throughput limit in cost management.` + ); + setIsThroughputCapExceeded(true); + return false; + } + + setThroughputError(""); + setIsThroughputCapExceeded(false); + return true; + }; + const getThroughputLabelText = (): string => { let throughputHeaderText: string; if (isAutoscaleSelected) { @@ -60,11 +111,17 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({ const newThroughput = parseInt(newInput); setThroughput(newThroughput); setThroughputValue(newThroughput); + if (!isSharded && newThroughput > 10000) { setThroughputError("Unsharded collections support up to 10,000 RUs"); - } else { - setThroughputError(""); + return; } + + if (!checkThroughputCap(newThroughput)) { + return; + } + + setThroughputError(""); }; const getAutoScaleTooltip = (): string => { @@ -96,11 +153,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({ setIsAutoScaleSelected(true); setThroughputValue(AutoPilotUtils.minAutoPilotThroughput); setIsAutoscale(true); + checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput); } else { setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400); setIsAutoScaleSelected(false); setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400); setIsAutoscale(false); + checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400); } }; diff --git a/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap b/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap index 0798c01ef..d3a8ecf23 100644 --- a/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap +++ b/src/Explorer/Controls/ThroughputInput/__snapshots__/ThroughputInput.test.tsx.snap @@ -6,6 +6,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = ` isSharded={true} onCostAcknowledgeChange={[Function]} setIsAutoscale={[Function]} + setIsThroughputCapExceeded={[Function]} setThroughputValue={[Function]} showFreeTierExceedThroughputTooltip={true} > diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index 028b3c426..a5d6651d5 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -92,6 +92,7 @@ export interface AddCollectionPanelState { errorMessage: string; showErrorDetails: boolean; isExecuting: boolean; + isThroughputCapExceeded: boolean; } export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> { @@ -122,6 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, errorMessage: "", showErrorDetails: false, isExecuting: false, + isThroughputCapExceeded: false, }; } @@ -249,6 +251,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, isSharded={this.state.isSharded} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} + setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => + this.setState({ isThroughputCapExceeded }) + } onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)} /> )} @@ -480,6 +485,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, isSharded={this.state.isSharded} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} + setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => + this.setState({ isThroughputCapExceeded }) + } onCostAcknowledgeChange={(isAcknowledged: boolean) => { this.isCostAcknowledged = isAcknowledged; }} @@ -676,7 +684,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, )} </div> - <PanelFooterComponent buttonLabel="OK" /> + <PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} /> {this.state.isExecuting && <PanelLoadingScreen />} </form> diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 3ee59bd30..0c3568ecf 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -50,6 +50,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({ ); const [formErrors, setFormErrors] = useState<string>(""); const [isExecuting, setIsExecuting] = useState<boolean>(false); + const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false); const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; @@ -166,6 +167,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({ formError: formErrors, isExecuting, submitButtonText: "OK", + isSubmitButtonDisabled: isThroughputCapExceeded, onSubmit, }; @@ -236,6 +238,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({ isSharded={databaseCreateNewShared} setThroughputValue={(newThroughput: number) => (throughput = newThroughput)} setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)} + setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} /> )} diff --git a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap index 9c4bbaa32..bf462a66c 100644 --- a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap +++ b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap @@ -4,6 +4,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = ` <RightPaneForm formError="" isExecuting={false} + isSubmitButtonDisabled={false} onSubmit={[Function]} submitButtonText="OK" > @@ -92,6 +93,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = ` isSharded={true} onCostAcknowledgeChange={[Function]} setIsAutoscale={[Function]} + setIsThroughputCapExceeded={[Function]} setThroughputValue={[Function]} /> </div> diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 64ee3fb3f..9aa4faf6b 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -43,6 +43,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(); const [formError, setFormError] = useState<string>(""); + const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false); const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier; const addCollectionPaneOpenMessage = { @@ -149,6 +150,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio formError, isExecuting, submitButtonText: "OK", + isSubmitButtonDisabled: isThroughputCapExceeded, onSubmit, }; @@ -262,6 +264,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio isSharded setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)} + setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} /> )} @@ -334,6 +337,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio isSharded={false} setThroughputValue={(throughput: number) => (tableThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)} + setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)} /> )} diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index de359f986..b31dc19d3 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -369,18 +369,21 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect </div> <PanelFooterComponent buttonLabel="OK" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" text="OK" type="submit" > <PrimaryButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" text="OK" theme={ @@ -660,6 +663,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect > <CustomizedDefaultButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -941,6 +945,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect > <DefaultButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1223,6 +1228,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect <BaseButton ariaLabel="OK" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap index 4e673b39c..3f163902a 100644 --- a/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap +++ b/src/Explorer/Panes/ExecuteSprocParamsPane/__snapshots__/ExecuteSprocParamsPane.test.tsx.snap @@ -5305,18 +5305,21 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = ` </div> <PanelFooterComponent buttonLabel="Execute" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="Execute" + disabled={false} id="sidePanelOkButton" text="Execute" type="submit" > <PrimaryButton ariaLabel="Execute" + disabled={false} id="sidePanelOkButton" text="Execute" theme={ @@ -5596,6 +5599,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = ` > <CustomizedDefaultButton ariaLabel="Execute" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -5877,6 +5881,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = ` > <DefaultButton ariaLabel="Execute" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -6159,6 +6164,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = ` <BaseButton ariaLabel="Execute" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/PanelFooterComponent.tsx b/src/Explorer/Panes/PanelFooterComponent.tsx index d007a5e36..c24ac5fb3 100644 --- a/src/Explorer/Panes/PanelFooterComponent.tsx +++ b/src/Explorer/Panes/PanelFooterComponent.tsx @@ -3,12 +3,20 @@ import React from "react"; export interface PanelFooterProps { buttonLabel: string; + isButtonDisabled?: boolean; } export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({ buttonLabel, + isButtonDisabled, }: PanelFooterProps): JSX.Element => ( <div className="panelFooter"> - <PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} /> + <PrimaryButton + type="submit" + id="sidePanelOkButton" + text={buttonLabel} + ariaLabel={buttonLabel} + disabled={!!isButtonDisabled} + /> </div> ); diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx index 26c356a0f..a444db02d 100644 --- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx +++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx @@ -9,6 +9,7 @@ export interface RightPaneFormProps { onSubmit: () => void; submitButtonText: string; isSubmitButtonHidden?: boolean; + isSubmitButtonDisabled?: boolean; children?: ReactNode; } @@ -18,6 +19,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({ onSubmit, submitButtonText, isSubmitButtonHidden = false, + isSubmitButtonDisabled = false, children, }: RightPaneFormProps) => { const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => { @@ -30,7 +32,9 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({ <form className="panelFormWrapper" onSubmit={handleOnSubmit}> {formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />} {children} - {!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />} + {!isSubmitButtonHidden && ( + <PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} /> + )} </form> {isExecuting && <PanelLoadingScreen />} </> diff --git a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap index 3438c3398..bafc521fc 100644 --- a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap +++ b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap @@ -14,18 +14,21 @@ exports[`Right Pane Form should render Default properly 1`] = ` > <PanelFooterComponent buttonLabel="Load" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="Load" + disabled={false} id="sidePanelOkButton" text="Load" type="submit" > <PrimaryButton ariaLabel="Load" + disabled={false} id="sidePanelOkButton" text="Load" theme={ @@ -305,6 +308,7 @@ exports[`Right Pane Form should render Default properly 1`] = ` > <CustomizedDefaultButton ariaLabel="Load" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -586,6 +590,7 @@ exports[`Right Pane Form should render Default properly 1`] = ` > <DefaultButton ariaLabel="Load" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -868,6 +873,7 @@ exports[`Right Pane Form should render Default properly 1`] = ` <BaseButton ariaLabel="Load" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index 0c1815afe..e83d2db32 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -675,18 +675,21 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` </div> <PanelFooterComponent buttonLabel="Create" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="Create" + disabled={false} id="sidePanelOkButton" text="Create" type="submit" > <PrimaryButton ariaLabel="Create" + disabled={false} id="sidePanelOkButton" text="Create" theme={ @@ -966,6 +969,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` > <CustomizedDefaultButton ariaLabel="Create" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1247,6 +1251,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` > <DefaultButton ariaLabel="Create" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1529,6 +1534,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` <BaseButton ariaLabel="Create" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap index 84159803b..ad7c2346c 100644 --- a/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/TableQuerySelectPanel/__snapshots__/TableQuerySelectPanel.test.tsx.snap @@ -1262,18 +1262,21 @@ exports[`Table query select Panel should render Default properly 1`] = ` </div> <PanelFooterComponent buttonLabel="OK" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" text="OK" type="submit" > <PrimaryButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" text="OK" theme={ @@ -1553,6 +1556,7 @@ exports[`Table query select Panel should render Default properly 1`] = ` > <CustomizedDefaultButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1834,6 +1838,7 @@ exports[`Table query select Panel should render Default properly 1`] = ` > <DefaultButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -2116,6 +2121,7 @@ exports[`Table query select Panel should render Default properly 1`] = ` <BaseButton ariaLabel="OK" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap index c78e43d5c..2714a4cd6 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/AddTableEntityPanel.test.tsx.snap @@ -356,18 +356,21 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` </div> <PanelFooterComponent buttonLabel="Add Entity" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="Add Entity" + disabled={false} id="sidePanelOkButton" text="Add Entity" type="submit" > <PrimaryButton ariaLabel="Add Entity" + disabled={false} id="sidePanelOkButton" text="Add Entity" theme={ @@ -647,6 +650,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` > <CustomizedDefaultButton ariaLabel="Add Entity" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -928,6 +932,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` > <DefaultButton ariaLabel="Add Entity" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1210,6 +1215,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = ` <BaseButton ariaLabel="Add Entity" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap index 018fe7118..ff6bc9ef3 100644 --- a/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap +++ b/src/Explorer/Panes/Tables/__snapshots__/EditTableEntityPanel.test.tsx.snap @@ -357,18 +357,21 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` </div> <PanelFooterComponent buttonLabel="Update" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="Update" + disabled={false} id="sidePanelOkButton" text="Update" type="submit" > <PrimaryButton ariaLabel="Update" + disabled={false} id="sidePanelOkButton" text="Update" theme={ @@ -648,6 +651,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` > <CustomizedDefaultButton ariaLabel="Update" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -929,6 +933,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` > <DefaultButton ariaLabel="Update" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1211,6 +1216,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = ` <BaseButton ariaLabel="Update" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 5613a797e..ac2eef412 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -1041,18 +1041,21 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` </div> <PanelFooterComponent buttonLabel="OK" + isButtonDisabled={false} > <div className="panelFooter" > <CustomizedPrimaryButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" text="OK" type="submit" > <PrimaryButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" text="OK" theme={ @@ -1332,6 +1335,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` > <CustomizedDefaultButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1613,6 +1617,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` > <DefaultButton ariaLabel="OK" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} @@ -1895,6 +1900,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = ` <BaseButton ariaLabel="OK" baseClassName="ms-Button" + disabled={false} id="sidePanelOkButton" onRenderDescription={[Function]} primary={true} diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 3bea00924..655b9430a 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -34,6 +34,7 @@ export type Features = { readonly mongoProxyEndpoint?: string; readonly mongoProxyAPIs?: string; readonly notebooksTemporarilyDown: boolean; + readonly enableThroughputCap: boolean; }; export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { @@ -86,6 +87,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"), phoenix: "true" === get("phoenix"), notebooksDownBanner: "true" === get("notebooksDownBanner"), + enableThroughputCap: "true" === get("enablethroughputcap"), }; } diff --git a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts index 40c68fb86..8c4a237ea 100644 --- a/src/Utils/arm/generatedClients/cosmos/sqlResources.ts +++ b/src/Utils/arm/generatedClients/cosmos/sqlResources.ts @@ -9,7 +9,7 @@ import { configContext } from "../../../../ConfigContext"; import { armRequest } from "../../request"; import * as Types from "./types"; -const apiVersion = "2021-04-15"; +const apiVersion = "2021-10-15"; /* Lists the SQL databases under an existing Azure Cosmos DB database account. */ export async function listSqlDatabases( diff --git a/src/hooks/useDatabaseAccounts.tsx b/src/hooks/useDatabaseAccounts.tsx index c120d6a71..1390ceaed 100644 --- a/src/hooks/useDatabaseAccounts.tsx +++ b/src/hooks/useDatabaseAccounts.tsx @@ -1,6 +1,7 @@ import useSWR from "swr"; import { configContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; +import { userContext } from "../UserContext"; interface AccountListResult { nextLink: string; @@ -14,8 +15,8 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: headers.append("Authorization", bearer); let accounts: Array<DatabaseAccount> = []; - - let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2021-06-15`; + const apiVersion = userContext.features.enableThroughputCap ? "2021-10-15" : "2021-06-15"; + let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=${apiVersion}`; while (nextLink) { const response: Response = await fetch(nextLink, { headers });