Files
cosmos-explorer/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx
sakshigupta12feb 2c31ec2a8d Dark theme for Explorer (#2185)
* First dark theme commits for command bar

* Updated theme on sidebar

* Updated tabs, sidebar, splash screen

* settings theme changes

* Dark theme applied to Monaco editor

* Dark theme to stored procedures

* Fixed sidebar scroll

* Updated scroll issue in sidebar

* Command bar items fixed

* Fixed lint errors

* fixed lint errors

* settings side panel fixed

* Second last iteration for css

* Fixed all the issues of css

* Updated the theme icon for now on DE to change the theme from portal/DE itself

* Formatting issue resolved

* Remove CloudShellTerminalComponent changes - revert to master version

* Fixed test issue

* Fixed formatting issue

* Fix tests: update snapshots and revert xterm imports for compatibility

* Fix xterm imports in CloudShellTerminalComponent to use @xterm packages

* Fix Cloud Shell component imports for compatibility

* Update test snapshots

* Fix xterm package consistency across all CloudShell components

* Fix TypeScript compilation errors in CloudShell components and query Documents

- Standardized xterm package imports across CloudShell components to use legacy 'xterm' package
- Fixed Terminal type compatibility issues in CommonUtils.tsx
- Added type casting for enableQueryControl property in queryDocuments.ts to handle Azure Cosmos SDK interface limitations
- Applied code formatting to ensure consistency

* Update failing snapshot tests

- Updated TreeNodeComponent snapshot tests for loading states
- Updated ThroughputInputAutoPilotV3Component snapshots for number formatting changes (10,00,000 -> 1,000,000)
- All snapshot tests now pass

* Fixed test issue

* Fixed test issue

* Updated the buttons for theme

* Updated the Theme changes based on portal theme changes

* Updated review comments

* Updated the duplicate code and fixed the fabric react error

* Few places styling added and resolving few comments

* Fixed errors

* Fixed comments

* Fixed comments

* Fixed comments

* Fixed full text policy issue for mongoru accounts

* Resolved comments for class Name and few others

* Added css for homepage in ru accounts

* Final commit with all the feedback issues resolved

* Lint error resolved

* Updated the review comments and few Ui issues

* Resolved review comments and changed header bg and active state color

* Moved svg code to different file and imported

* css fixed for the hpome page boxed for ru account

* Lint errors

* Fixed boxes issue in ru accounts

* Handled the initial theme from the portal

* Updated snap

* Update snapshots for TreeNodeComponent and CreateCopyJobScreensProvider tests

* Fix duplicate DataExplorerRoot test id causing Playwright strict mode violation

* Fix locale-dependent number formatting in ThroughputInputAutoPilotV3Component

---------

Co-authored-by: Sakshi Gupta <sakshig+microsoft@microsoft.com>
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2025-12-16 12:21:58 +05:30

411 lines
16 KiB
TypeScript

import { Checkbox, DirectionalHint, Link, Separator, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import { getWorkloadType } from "Common/DatabaseAccountUtility";
import { CostEstimateText } from "Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText";
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 { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
import "./ThroughputInput.less";
export interface ThroughputInputProps {
isDatabase: boolean;
isSharded: boolean;
isFreeTier: boolean;
showFreeTierExceedThroughputTooltip: boolean;
isQuickstart?: boolean;
isGlobalSecondaryIndex?: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
}
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isDatabase,
isSharded,
isFreeTier,
showFreeTierExceedThroughputTooltip,
isQuickstart,
isGlobalSecondaryIndex,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
let defaultThroughput: number;
const workloadType: Constants.WorkloadType = getWorkloadType();
if (isFabricNative()) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
} else if (
isFreeTier ||
isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
} else if (workloadType === Constants.WorkloadType.Production) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput10K;
} else {
defaultThroughput = AutoPilotUtils.autoPilotThroughput4K;
}
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(defaultThroughput);
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;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
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;
}
});
});
totalThroughput *= numberOfRegions;
setTotalThroughputUsed(totalThroughput);
if (throughputCap && throughputCap !== -1 && 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 ${
totalThroughput + throughput * numberOfRegions
} RU/s. Change total throughput limit in cost management.`,
);
setIsThroughputCapExceeded(true);
}
}, []);
const checkThroughputCap = (newThroughput: number): boolean => {
if (throughputCap && throughputCap !== -1 && 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 * numberOfRegions
} 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) {
throughputHeaderText = AutoPilotUtils.getAutoPilotHeaderText().toLocaleLowerCase();
} else {
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
let maxRU: string;
if (userContext.isTryCosmosDBSubscription) {
maxRU = Constants.TryCosmosExperience.maxRU.toLocaleString();
} else if (!isSharded) {
maxRU = "10000";
} else {
maxRU = "unlimited";
}
throughputHeaderText = `throughput (${minRU} - ${maxRU} RU/s)`;
}
return `${isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
};
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");
return;
}
if (!checkThroughputCap(newThroughput)) {
return;
}
setThroughputError("");
};
const getAutoScaleTooltip = (): string => {
const collectionName = getCollectionName().toLocaleLowerCase();
return `Set the max RU/s to the highest RU/s you want your ${collectionName} to scale to. The ${collectionName} will scale between 10% of max RU/s to the max RU/s based on usage.`;
};
const getCostAcknowledgeText = (): string => {
const databaseAccount = userContext.databaseAccount;
if (!databaseAccount || !databaseAccount.properties) {
return "";
}
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
return PricingUtils.getEstimatedSpendAcknowledgeString(
throughput,
userContext.portalEnv,
numberOfRegions,
multimasterEnabled,
isAutoscaleSelected,
);
};
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
setThroughput(defaultThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(defaultThroughput);
setIsAutoscale(true);
checkThroughputCap(defaultThroughput);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
};
return (
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text
aria-label="Throughput header"
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
{getThroughputLabelText()}
</Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
</Stack>
{!isGlobalSecondaryIndex && (
<Stack horizontal verticalAlign="center">
<div role="radiogroup">
<input
id="Autoscale-input"
className="throughputInputRadioBtn"
aria-label={`${getThroughputLabelText()} Autoscale`}
aria-required={true}
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
Autoscale
</label>
<input
id="Manual-input"
className="throughputInputRadioBtn"
aria-label={`${getThroughputLabelText()} Manual`}
checked={!isAutoscaleSelected}
type="radio"
aria-required={true}
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
Manual
</label>
</div>
</Stack>
)}
{isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text style={{ marginTop: -2, fontSize: 12, color: "var(--colorNeutralForeground1)" }}>
Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10%
of that value.
</Text>
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
Minimum RU/s
</Text>
<InfoTooltip>The minimum RU/s your container will scale to</InfoTooltip>
</Stack>
<Text
style={{
fontFamily: "Segoe UI",
width: 70,
height: 27,
border: "none",
fontSize: 14,
backgroundColor: "transparent",
fontWeight: 400,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--colorNeutralForeground1)",
}}
>
{Math.round(throughput / 10).toString()}
</Text>
</Stack>
<Text
style={{
fontFamily: "Segoe UI",
fontSize: 12,
fontWeight: 400,
paddingBottom: 6,
color: "var(--colorNeutralForeground1)",
}}
>
x 10 =
</Text>
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
Maximum RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TextField
id="autoscaleRUValueField"
data-test="autoscaleRUInput"
type="number"
styles={{
fieldGroup: { width: 100, height: 27, flexShrink: 0 },
field: { fontSize: 14, fontWeight: 400, color: "var(--colorNeutralForeground1)" },
}}
onChange={(_event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
value={throughput.toString()}
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
required={true}
errorMessage={throughputError}
/>
</Stack>
</Stack>
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription" style={{ color: "var(--colorNeutralForeground1)" }}>
Estimate your required RU/s with&nbsp;
<Link
className="underlinedLink"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="Capacity calculator"
>
capacity calculator
</Link>
.
</Text>
</Stack>
<Separator className="panelSeparator" style={{ paddingTop: -8, paddingBottom: -8 }} />
</Stack>
)}
{!isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription" style={{ color: "var(--colorNeutralForeground1)" }}>
Estimate your required RU/s with&nbsp;
<Link
className="underlinedLink"
target="_blank"
href="https://cosmos.azure.com/capacitycalculator/"
aria-label="Capacity calculator"
>
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
aria-label="maxRUDescription"
>
{isDatabase ? "Database" : getCollectionName()} Required RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
showFreeTierExceedThroughputTooltip && throughput > SharedConstants.FreeTierLimits.RU
? `The first ${SharedConstants.FreeTierLimits.RU} RU/s in this account are free. Billing will apply to any throughput beyond ${SharedConstants.FreeTierLimits.RU} RU/s.`
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={throughput.toString()}
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} Required RU/s`}
required={true}
errorMessage={throughputError}
/>
</TooltipHost>
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
</Stack>
)}
{throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<Checkbox
checked={isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
setIsCostAcknowledged(isChecked);
onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
);
};