Compare commits

..

4 Commits

Author SHA1 Message Date
sunghyunkang1111
3385b62b60 Merge branch 'master' into loc_batch4 2026-03-06 16:04:36 -06:00
Sung-Hyun Kang
79a00080bd Update test snap 2026-03-06 15:18:20 -06:00
Sung-Hyun Kang
8bc875ae92 run prettier 2026-03-06 14:35:24 -06:00
Sung-Hyun Kang
47b8737ab1 Add localization strings batch 4 2026-03-06 13:37:25 -06:00
27 changed files with 628 additions and 274 deletions

View File

@@ -17,6 +17,8 @@ import {
} from "@fluentui/react";
import React, { FC, useEffect } from "react";
import create, { UseStore } from "zustand";
import { Keys } from "../../Localization/Keys.generated";
import { t } from "../../Localization/t";
export interface DialogState {
visible: boolean;
@@ -88,7 +90,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
isModal: true,
title,
subText,
primaryButtonText: "Close",
primaryButtonText: t(Keys.common.close),
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();

View File

@@ -44,6 +44,8 @@ import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
@@ -689,12 +691,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
const validationErrors = [];
if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
validationErrors.push("includedPaths is required");
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsRequired));
} else if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push("includedPaths must be an array");
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsMustBeArray));
}
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push("excludedPaths must be an array if provided");
validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
}
this.setState({
@@ -896,7 +898,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const buttons: CommandButtonComponentProps[] = [];
const isExecuting = this.props.settingsTab.isExecuting();
if (this.saveSettingsButton.isVisible()) {
const label = "Save";
const label = t(Keys.common.save);
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -909,7 +911,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
if (this.discardSettingsChangesButton.isVisible()) {
const label = "Discard";
const label = t(Keys.common.discard);
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -934,9 +936,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
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 + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
}
this.setState({ autoPilotThroughput: newThroughput, throughputError });
};
@@ -947,9 +950,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
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 + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
}
this.setState({ throughput: newThroughput, throughputError });
};
@@ -1560,7 +1564,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
>
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>This table shared throughput is configured at the keyspace</div>
<div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
)}
<div

View File

@@ -25,6 +25,8 @@ import {
import * as React from "react";
import { Urls } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { hoursInAMonth } from "../../../Shared/Constants";
import {
computeRUUsagePriceHourly,
@@ -338,10 +340,12 @@ export const getEstimatedSpendingElement = (
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
return (
<Stack>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.costEstimate.title)}
</Text>
{costElement}
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
How we calculate this
{t(Keys.controls.settings.costEstimate.howWeCalculate)}
</Text>
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
<span>
@@ -353,7 +357,8 @@ export const getEstimatedSpendingElement = (
</span>
<span>
{priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}/RU
{priceBreakdown.pricePerRu}
{t(Keys.controls.settings.costEstimate.perRu)}
</span>
</Stack>
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
@@ -365,18 +370,16 @@ export const getEstimatedSpendingElement = (
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
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.{" "}
{t(Keys.controls.settings.throughput.manualToAutoscaleDisclaimer)}{" "}
<Link href={Urls.autoscaleMigration}>Learn more</Link>
</Text>
);
export const ttlWarning: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
operation explicitly issued by a client application. For more information see,{" "}
{t(Keys.controls.settings.throughput.ttlWarningText)}{" "}
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
Time to Live (TTL) in Azure Cosmos DB
{t(Keys.controls.settings.throughput.ttlWarningLinkText)}
</Link>
.
</Text>
@@ -384,29 +387,28 @@ export const ttlWarning: JSX.Element = (
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
You have not saved the latest changes made to your{" "}
{t(Keys.controls.settings.throughput.unsavedEditorWarningPrefix)}{" "}
{editor === "indexPolicy"
? "indexing policy"
? t(Keys.controls.settings.throughput.unsavedIndexingPolicy)
: editor === "dataMasking"
? "data masking policy"
: "computed properties"}
. Please click save to confirm the changes.
? t(Keys.controls.settings.throughput.unsavedDataMaskingPolicy)
: t(Keys.controls.settings.throughput.unsavedComputedProperties)}
{t(Keys.controls.settings.throughput.unsavedEditorWarningSuffix)}
</Text>
);
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
time to complete.
{t(Keys.controls.settings.throughput.updateDelayedApplyWarning)}
</Text>
);
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
return (
<Text id="updateThroughputDelayedApplyWarningMessage">
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
with this value and wait until the scale-up is completed.
{t(Keys.controls.settings.throughput.scalingUpDelayMessage, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
</Text>
);
};
@@ -418,22 +420,26 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
return (
<>
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
There are three options you can choose from to proceed:
{t(Keys.controls.settings.throughput.exceedPreAllocatedMessage)}
</Text>
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
<li>
{t(Keys.controls.settings.throughput.instantScaleOption, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
</li>
{instantMaximumThroughput < maximumThroughput && (
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
<li>
{t(Keys.controls.settings.throughput.asyncScaleOption, { maximumThroughput: String(maximumThroughput) })}
</li>
)}
<li>
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
and the Azure Cosmos DB team will review.
{t(Keys.controls.settings.throughput.quotaMaxOption, { maximumThroughput: String(maximumThroughput) })}
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
target="_blank"
>
Learn more
{t(Keys.common.learnMore)}
</Link>
</li>
</ol>
@@ -444,23 +450,19 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
return (
<Text styles={infoAndToolTipTextStyle}>
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
limit, please refer to our service quote documentation.
{t(Keys.controls.settings.throughput.belowMinimumMessage, { minimum: String(minimum) })}
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
>
Learn more
{t(Keys.common.learnMore)}
</Link>
</Text>
);
};
export const saveThroughputWarningMessage: JSX.Element = (
<Text>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
<Text>{t(Keys.controls.settings.throughput.saveThroughputWarning)}</Text>
);
const getCurrentThroughput = (
@@ -472,23 +474,29 @@ const getCurrentThroughput = (
if (targetThroughput) {
if (throughput) {
return isAutoscale
? `, Current autoscale throughput: ${Math.round(
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
throughput / 10,
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
targetThroughput / 10,
)} - ${targetThroughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
)} - ${throughput} ${throughputUnit}, ${t(
Keys.controls.settings.throughput.targetAutoscaleThroughput,
)} ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}, ${t(
Keys.controls.settings.throughput.targetManualThroughput,
)} ${targetThroughput}`;
} else {
return isAutoscale
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
? `, ${t(Keys.controls.settings.throughput.targetAutoscaleThroughput)} ${Math.round(
targetThroughput / 10,
)} - ${targetThroughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.targetManualThroughput)} ${targetThroughput} ${throughputUnit}`;
}
}
if (!targetThroughput && throughput) {
return isAutoscale
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
throughput / 10,
)} - ${throughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}`;
}
return "";
@@ -503,10 +511,10 @@ export const getThroughputApplyDelayedMessage = (
requestedThroughput: number,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days
to complete. View the latest status in Notifications.
{t(Keys.controls.settings.throughput.applyDelayedMessage)}
<br />
Database: {databaseName}, Container: {collectionName}{" "}
{t(Keys.controls.settings.throughput.databaseLabel)} {databaseName},{" "}
{t(Keys.controls.settings.throughput.containerLabel)} {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
@@ -519,9 +527,13 @@ export const getThroughputApplyShortDelayMessage = (
collectionName: string,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete.
{t(Keys.controls.settings.throughput.applyShortDelayMessage)}
<br />
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</Text>
);
@@ -535,10 +547,13 @@ export const getThroughputApplyLongDelayMessage = (
requestedThroughput: number,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
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.
{t(Keys.controls.settings.throughput.applyLongDelayMessage)}
<br />
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
@@ -547,63 +562,49 @@ export const getToolTipContainer = (content: string | JSX.Element): JSX.Element
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
export const conflictResolutionLwwTooltip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based
conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the
winner for the conflicting versions of the document. Specify your own integer property if you want to override the
default timestamp based conflict resolution.
</Text>
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.conflictResolution.lwwTooltip)}</Text>
);
export const conflictResolutionCustomToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write
application defined logic to determine the winner of the conflicting versions of a document. The stored procedure
will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the
conflicts will be populated in the
{t(Keys.controls.settings.conflictResolution.customTooltip)}
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
{` conflicts feed`}
{t(Keys.controls.settings.conflictResolution.customTooltipConflictsFeed)}
</Link>
. You can update/re-register the stored procedure at any time.
{t(Keys.controls.settings.conflictResolution.customTooltipSuffix)}
</Text>
);
export const changeFeedPolicyToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default.
To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes.
Reads are unaffected.
</Text>
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.changeFeed.tooltip)}</Text>
);
export const mongoIndexingPolicyDisclaimer: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
{t(Keys.controls.settings.mongoIndexing.disclaimer)}
<Link
href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types"
target="_blank"
style={{ color: "var(--colorBrandForeground1)" }}
>
{` Compound indexes `}
{t(Keys.controls.settings.mongoIndexing.disclaimerCompoundIndexesLink)}
</Link>
are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo
shell.
{t(Keys.controls.settings.mongoIndexing.disclaimerSuffix)}
</Text>
);
export const mongoCompoundIndexNotSupportedMessage: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this
collection, use the Mongo Shell.
{t(Keys.controls.settings.mongoIndexing.compoundNotSupported)}
</Text>
);
export const mongoIndexingPolicyAADError: JSX.Element = (
<MessageBar messageBarType={MessageBarType.error}>
<Text>
To use the indexing policy editor, please login to the
{t(Keys.controls.settings.mongoIndexing.aadError)}
<Link target="_blank" href="https://portal.azure.com">
{"azure portal."}
{t(Keys.controls.settings.mongoIndexing.aadErrorLink)}
</Link>
</Text>
</MessageBar>
@@ -611,7 +612,7 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}>
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.mongoIndexing.refreshingProgress)}</Text>
<Spinner size={SpinnerSize.small} />
</Stack>
);
@@ -623,15 +624,18 @@ export const renderMongoIndexTransformationRefreshMessage = (
if (progress === 0) {
return (
<Text styles={infoAndToolTipTextStyle}>
{"You can make more indexing changes once the current index transformation is complete. "}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
{t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesZero)}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheck)}</Link>
</Text>
);
} else {
return (
<Text styles={infoAndToolTipTextStyle}>
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
{`${t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesProgress).replace(
"{{progress}}",
String(progress),
)} `}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheckProgress)}</Link>
</Text>
);
}

View File

@@ -4,6 +4,8 @@ import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/C
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { loadMonaco } from "Explorer/LazyMonaco";
import { monacoTheme, useThemeStore } from "hooks/useTheme";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import * as monaco from "monaco-editor";
import * as React from "react";
export interface ComputedPropertiesComponentProps {
@@ -107,7 +109,7 @@ export class ComputedPropertiesComponent extends React.Component<
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
value: value,
language: "json",
ariaLabel: "Computed properties",
ariaLabel: t(Keys.controls.settings.computedProperties.ariaLabel),
theme: monacoTheme(),
});
if (this.computedPropertiesEditor) {
@@ -151,9 +153,9 @@ export class ComputedPropertiesComponent extends React.Component<
)}
<Text style={{ marginLeft: "30px", marginBottom: "10px", color: "var(--colorNeutralForeground1)" }}>
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
{t(Keys.common.learnMore)} <FontIcon iconName="NavigateExternalInline" />
</Link>
&#160; about how to define computed properties and how to use them.
&#160; {t(Keys.controls.settings.computedProperties.learnMorePrefix)}
</Text>
<div
className="settingsV2Editor"

View File

@@ -2,6 +2,8 @@ import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } fr
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import {
conflictResolutionCustomToolTip,
conflictResolutionLwwTooltip,
@@ -32,9 +34,12 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
{
key: DataModels.ConflictResolutionMode.LastWriterWins,
text: "Last Write Wins (default)",
text: t(Keys.controls.settings.conflictResolution.lwwDefault),
},
{
key: DataModels.ConflictResolutionMode.Custom,
text: t(Keys.controls.settings.conflictResolution.customMergeProcedure),
},
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
];
componentDidMount(): void {
@@ -85,7 +90,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup
label="Mode"
label={t(Keys.controls.settings.conflictResolution.mode)}
selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions}
onChange={this.onConflictResolutionPolicyModeChange}
@@ -103,7 +108,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionLWWComponent = (): JSX.Element => (
<TextField
id="conflictResolutionLwwTextField"
label={"Conflict Resolver Property"}
label={t(Keys.controls.settings.conflictResolution.conflictResolverProperty)}
onRenderLabel={this.onRenderLwwComponentTextField}
styles={{
fieldGroup: {
@@ -158,7 +163,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
return (
<TextField
id="conflictResolutionCustomTextField"
label="Stored procedure"
label={t(Keys.controls.settings.conflictResolution.storedProcedure)}
onRenderLabel={this.onRenderCustomComponentTextField}
styles={{
fieldGroup: {

View File

@@ -7,6 +7,8 @@ import {
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import React from "react";
export interface ContainerPolicyComponentProps {
@@ -153,7 +155,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText="Vector Policy"
headerText={t(Keys.controls.settings.containerPolicy.vectorPolicy)}
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{vectorEmbeddings && (
@@ -175,7 +177,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText="Full Text Policy"
headerText={t(Keys.controls.settings.containerPolicy.fullTextPolicy)}
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{fullTextSearchPolicy ? (
@@ -218,7 +220,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
});
}}
>
Create new full text search policy
{t(Keys.controls.settings.containerPolicy.createFullTextPolicy)}
</DefaultButton>
)}
</Stack>

View File

@@ -2,6 +2,8 @@ import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import * as monaco from "monaco-editor";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty as isContentDirty, isDataMaskingEnabled } from "../SettingsUtils";
@@ -89,7 +91,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
value: value,
language: "json",
automaticLayout: true,
ariaLabel: "Data Masking Policy",
ariaLabel: t(Keys.controls.settings.dataMasking.ariaLabel),
fontSize: 13,
minimap: { enabled: false },
wordWrap: "off",
@@ -142,7 +144,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
)}
{this.props.validationErrors.length > 0 && (
<MessageBar messageBarType={MessageBarType.error}>
Validation failed: {this.props.validationErrors.join(", ")}
{t(Keys.controls.settings.dataMasking.validationFailed)} {this.props.validationErrors.join(", ")}
</MessageBar>
)}
<div className="settingsV2Editor" tabIndex={0} ref={this.dataMaskingDiv}></div>

View File

@@ -2,6 +2,8 @@ import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer";
import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
@@ -21,7 +23,9 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
{isSourceContainer && (
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
<Text styles={{ root: { fontWeight: 600 } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexesDefined)}
</Text>
)}
<Text>
<Link
@@ -31,7 +35,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "}
about how to define global secondary indexes and how to use them.
{t(Keys.controls.settings.globalSecondaryIndex.learnMoreSuffix)}
</Text>
</Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}

View File

@@ -9,6 +9,8 @@ import { useSidePanel } from "hooks/useSidePanel";
import * as monaco from "monaco-editor";
import React, { useEffect, useRef } from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
export interface GlobalSecondaryIndexSourceComponentProps {
collection: ViewModels.Collection;
@@ -67,7 +69,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue,
language: "json",
ariaLabel: "Global Secondary Index JSON",
ariaLabel: t(Keys.controls.settings.globalSecondaryIndex.jsonAriaLabel),
readOnly: true,
});
};
@@ -98,7 +100,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
}}
/>
<PrimaryButton
text="Add index"
text={t(Keys.controls.settings.globalSecondaryIndex.addIndex)}
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel

View File

@@ -1,6 +1,8 @@
import { Stack, Text } from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
export interface GlobalSecondaryIndexTargetComponentProps {
collection: ViewModels.Collection;
@@ -25,17 +27,21 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
<Text styles={textHeadingStyle}>{t(Keys.controls.settings.globalSecondaryIndex.settingsTitle)}</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.sourceContainer)}
</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
<Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexDefinition)}
</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack>

View File

@@ -3,6 +3,8 @@ import { monacoTheme, useThemeStore } from "hooks/useTheme";
import * as monaco from "monaco-editor";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils";
@@ -119,7 +121,7 @@ export class IndexingPolicyComponent extends React.Component<
value: value,
language: "json",
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: "Indexing Policy",
ariaLabel: t(Keys.controls.settings.indexingPolicy.ariaLabel),
theme: monacoTheme(),
});
if (this.indexingPolicyEditor) {

View File

@@ -1,6 +1,8 @@
import { MessageBar, MessageBarType } from "@fluentui/react";
import * as React from "react";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { Keys } from "../../../../../Localization/Keys.generated";
import { t } from "../../../../../Localization/t";
import {
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage,
@@ -46,7 +48,11 @@ export class IndexingPolicyRefreshComponent extends React.Component<
try {
await this.props.refreshIndexTransformationProgress();
} catch (error) {
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
handleError(
error,
"RefreshIndexTransformationProgress",
t(Keys.controls.settings.indexingPolicyRefresh.refreshFailed),
);
} finally {
this.setState({ isRefreshing: false });
}

View File

@@ -14,7 +14,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>

View File

@@ -9,6 +9,8 @@ import {
IDropdownOption,
ITextField,
} from "@fluentui/react";
import { Keys } from "../../../../../Localization/Keys.generated";
import { t } from "../../../../../Localization/t";
import {
addMongoIndexSubElementsTokens,
mongoErrorMessageStyles,
@@ -66,7 +68,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
<Stack {...mongoWarningStackProps}>
<Stack horizontal tokens={addMongoIndexSubElementsTokens}>
<TextField
ariaLabel={"Index Field Name " + this.props.position}
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexFieldName) + " " + this.props.position}
disabled={this.props.disabled}
styles={shortWidthTextFieldStyles}
componentRef={this.setRef}
@@ -76,17 +78,17 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
/>
<Dropdown
ariaLabel={"Index Type " + this.props.position}
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexType) + " " + this.props.position}
disabled={this.props.disabled}
styles={shortWidthDropDownStyles}
placeholder="Select an index type"
placeholder={t(Keys.controls.settings.mongoIndexing.selectIndexType)}
selectedKey={this.props.type}
options={this.indexTypes}
onChange={this.onTypeChange}
/>
<IconButton
ariaLabel={"Undo Button " + this.props.position}
ariaLabel={t(Keys.controls.settings.mongoIndexing.undoButton) + " " + this.props.position}
iconProps={{ iconName: "Undo" }}
disabled={!this.props.description && !this.props.type}
onClick={() => this.props.onDiscard()}

View File

@@ -15,6 +15,8 @@ import {
} from "@fluentui/react";
import * as React from "react";
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/cosmos/types";
import { Keys } from "../../../../../Localization/Keys.generated";
import { t } from "../../../../../Localization/t";
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
import {
addMongoIndexStackProps,
@@ -83,11 +85,25 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
};
private initialIndexesColumns: IColumn[] = [
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
{
key: "definition",
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
fieldName: "definition",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "type",
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
fieldName: "type",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "actionButton",
name: "Drop Index",
name: t(Keys.controls.settings.mongoIndexing.dropIndexColumn),
fieldName: "actionButton",
minWidth: 100,
maxWidth: 200,
@@ -96,11 +112,25 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
];
private indexesToBeDroppedColumns: IColumn[] = [
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
{
key: "definition",
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
fieldName: "definition",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "type",
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
fieldName: "type",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "actionButton",
name: "Add index back",
name: t(Keys.controls.settings.mongoIndexing.addIndexBackColumn),
fieldName: "actionButton",
minWidth: 100,
maxWidth: 200,
@@ -161,7 +191,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
return isCurrentIndex ? (
<IconButton
ariaLabel="Delete index Button"
ariaLabel={t(Keys.controls.settings.mongoIndexing.deleteIndexButton)}
iconProps={{ iconName: "Delete" }}
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
onClick={() => {
@@ -170,7 +200,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
/>
) : (
<IconButton
ariaLabel="Add back Index Button"
ariaLabel={t(Keys.controls.settings.mongoIndexing.addBackIndexButton)}
iconProps={{ iconName: "Add" }}
onClick={() => {
this.props.onRevertIndexDrop(arrayPosition);
@@ -258,7 +288,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return (
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}>
<CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.currentIndexes)}
isExpandedByDefault={true}
>
{
<>
<DetailsList
@@ -285,7 +318,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return (
<Stack styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}>
<CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.indexesToBeDropped)}
isExpandedByDefault={true}
>
{indexesToBeDropped.length > 0 && (
<DetailsList
styles={customDetailsListStyles}

View File

@@ -18,6 +18,8 @@ import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/da
import { Platform, configContext } from "ConfigContext";
import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import {
CosmosSqlDataTransferDataSourceSink,
DataTransferJobGetResults,
@@ -80,7 +82,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
};
const partitionKeyName = "Partition key";
const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
const partitionKeyValue = getPartitionKeyValue();
const textHeadingStyle = {
@@ -148,7 +150,13 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
const getProgressDescription = (): string => {
const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount;
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
const processedCountString =
totalCount > 0
? t(Keys.controls.settings.partitionKeyEditor.documentsProcessed, {
processedCount: String(processedCount),
totalCount: String(totalCount),
})
: "";
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
};
@@ -181,16 +189,28 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
<Stack tokens={{ childrenGap: 10 }}>
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>}
{!isReadOnly && (
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
)}
<Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text>
<Text styles={textSubHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
</Stack>
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
{isHierarchicalPartitionedContainer()
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
</Text>
</Stack>
</Stack>
@@ -204,33 +224,33 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles}
>
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
the source container for the entire duration of the partition key change process.
{t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline
style={{ color: "var(--colorBrandForeground1)" }}
>
Learn more
{t(Keys.common.learnMore)}
</Link>
</MessageBar>
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
To change the partition key, a new destination container must be created or an existing destination
container selected. Data will then be copied to the destination container.
{t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
</Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }}
text="Change"
text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)}
/>
)}
{portalDataTransferJob && (
<Stack>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
</Text>
<Stack
horizontal
tokens={{ childrenGap: 20 }}
@@ -251,7 +271,10 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
}}
></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
<DefaultButton
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
/>
)}
</Stack>
</Stack>

View File

@@ -4,6 +4,8 @@ import * as Constants from "../../../../Common/Constants";
import { Platform, configContext } from "../../../../ConfigContext";
import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
@@ -156,14 +158,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
const freeTierLimits = SharedConstants.FreeTierLimits;
return (
<Text>
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "}
{freeTierLimits.RU} RU/s.
{t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
Learn more.
{t(Keys.controls.settings.scale.freeTierLearnMore)}
</Link>
</Text>
);
@@ -188,12 +188,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}>
<Text>Throughput (RU/s)</Text>
<Text>{t(Keys.controls.settings.scale.throughputRuS)}</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>
<Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</Text>
</Stack>
)}
</Stack>

View File

@@ -12,6 +12,8 @@ import {
} from "@fluentui/react";
import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import { userContext } from "../../../../UserContext";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import {
@@ -85,9 +87,12 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
constructor(props: SubSettingsComponentProps) {
super(props);
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = "Unique keys";
this.uniqueKeyName = t(Keys.controls.settings.subSettings.uniqueKeys);
this.uniqueKeyValue = this.getUniqueKeyValue();
}
@@ -143,9 +148,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
};
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" },
{ key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" },
{ key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" },
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), ariaLabel: "ttl-off-option" },
{
key: TtlType.OnNoDefault,
text: t(Keys.controls.settings.subSettings.ttlOnNoDefault),
ariaLabel: "ttl-on-no-default-option",
},
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn), ariaLabel: "ttl-on-option" },
];
public getTtlValue = (value: string): TtlType => {
@@ -216,13 +225,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
}}
>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
To enable time-to-live (TTL) for your collection/documents,{" "}
{t(Keys.controls.settings.subSettings.mongoTtlMessage)}{" "}
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live"
target="_blank"
style={{ color: "var(--colorBrandForeground1)" }}
>
create a TTL index
{t(Keys.controls.settings.subSettings.mongoTtlLinkText)}
</Link>
.
</Text>
@@ -231,7 +240,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label="Time to Live"
label={t(Keys.controls.settings.subSettings.timeToLive)}
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
@@ -255,8 +264,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
max={Int32.Max}
value={this.props.displayedTtlSeconds}
onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)"
ariaLabel={`Time to live in seconds`}
suffix={t(Keys.controls.settings.subSettings.seconds)}
ariaLabel={t(Keys.controls.settings.subSettings.timeToLiveInSeconds)}
data-test="ttl-input"
/>
)}
@@ -264,16 +273,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" },
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), disabled: true },
{ key: TtlType.OnNoDefault, text: t(Keys.controls.settings.subSettings.ttlOnNoDefault) },
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
];
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="analyticalStorageTimeToLive"
label="Analytical Storage Time to Live"
label={t(Keys.controls.settings.subSettings.analyticalStorageTtl)}
selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions}
onChange={this.onAnalyticalStorageTtlSelectionChange}
@@ -294,7 +303,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
min={1}
max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix="second(s)"
suffix={t(Keys.controls.settings.subSettings.seconds)}
onChange={this.onAnalyticalStorageTtlSecondsChange}
/>
)}
@@ -302,14 +311,22 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
{
key: GeospatialConfigType.Geography,
text: t(Keys.controls.settings.subSettings.geography),
ariaLabel: "geography-option",
},
{
key: GeospatialConfigType.Geometry,
text: t(Keys.controls.settings.subSettings.geometry),
ariaLabel: "geometry-option",
},
];
private getGeoSpatialComponent = (): JSX.Element => (
<ChoiceGroup
id="geoSpatialConfig"
label="Geospatial Configuration"
label={t(Keys.controls.settings.subSettings.geospatialConfiguration)}
selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.onGeoSpatialConfigTypeChange}
@@ -318,8 +335,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: ChangeFeedPolicyState.Off, text: "Off" },
{ key: ChangeFeedPolicyState.On, text: "On" },
{ key: ChangeFeedPolicyState.Off, text: t(Keys.controls.settings.subSettings.ttlOff) },
{ key: ChangeFeedPolicyState.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
];
private getChangeFeedComponent = (): JSX.Element => {
@@ -328,7 +345,10 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
<ToolTipLabelComponent
label={t(Keys.controls.settings.changeFeed.label)}
toolTipElement={changeFeedPolicyToolTip}
/>
</Label>
<ChoiceGroup
id="changeFeedPolicy"
@@ -373,14 +393,20 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
)}
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
<Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.largePartitionKeyEnabled, {
partitionKeyName: this.partitionKeyName.toLowerCase(),
})}
</Text>
)}
{userContext.apiType === "SQL" &&
(this.isHierarchicalPartitionedContainer() ? (
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text>
<Text className={classNames.hintText}>{t(Keys.controls.settings.subSettings.hierarchicalPartitioned)}</Text>
) : (
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text>
<Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.nonHierarchicalPartitioned)}
</Text>
))}
</Stack>
);

View File

@@ -1,5 +1,7 @@
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
import { ThroughputBucket } from "Contracts/DataModels";
import { Keys } from "../../../../../Localization/Keys.generated";
import { t } from "../../../../../Localization/t";
import React, { FC, useEffect, useState } from "react";
import { isDirty } from "../../SettingsUtils";
@@ -65,7 +67,9 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
return (
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
{t(Keys.controls.settings.throughputBuckets.label)}
</Label>
<Stack>
{throughputBuckets?.map((bucket) => (
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
@@ -76,7 +80,9 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
value={bucket.maxThroughputPercentage}
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
showValue={false}
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
label={`${t(Keys.controls.settings.throughputBuckets.bucketLabel, { id: String(bucket.id) })}${
bucket.id === 1 ? t(Keys.controls.settings.throughputBuckets.dataExplorerQueryBucket) : ""
}`}
styles={{
root: { flex: 2, maxWidth: 400 },
titleLabel: {
@@ -99,8 +105,8 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
disabled={bucket.maxThroughputPercentage === 100}
/>
<Toggle
onText="Active"
offText="Inactive"
onText={t(Keys.controls.settings.throughputBuckets.active)}
offText={t(Keys.controls.settings.throughputBuckets.inactive)}
checked={bucket.maxThroughputPercentage !== 100}
onChange={(event, checked) => onToggle(bucket.id, checked)}
styles={{

View File

@@ -18,6 +18,8 @@ import {
} from "@fluentui/react";
import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels";
import { Keys } from "../../../../../Localization/Keys.generated";
import { t } from "../../../../../Localization/t";
import * as SharedConstants from "../../../../../Shared/Constants";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
@@ -97,8 +99,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private throughputInputMaxValue: number;
private autoPilotInputMaxValue: number;
private options: IChoiceGroupOption[] = [
{ key: "true", text: "Autoscale" },
{ key: "false", text: "Manual" },
{ key: "true", text: t(Keys.controls.settings.throughputInput.autoscale) },
{ key: "false", text: t(Keys.controls.settings.throughputInput.manual) },
];
// Style constants for theme-aware colors and layout
@@ -244,7 +246,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Updated cost per month
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text
@@ -253,7 +255,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
</Text>
<Text
style={{
@@ -261,7 +264,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
</Text>
</Stack>
</div>
@@ -274,7 +278,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Current cost per month
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
</Text>
<Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
<Text
@@ -283,7 +287,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
</Text>
<Text
style={{
@@ -291,7 +296,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
</Text>
</Stack>
</Stack>
@@ -326,7 +332,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Updated cost per month
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={this.settingsAndScaleStyle.root}>
@@ -349,7 +355,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Current cost per month
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={this.settingsAndScaleStyle.root}>
@@ -444,10 +450,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
this.setState({ spendAckChecked: checked });
private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
const capacity: string = this.props.isFixed
? t(Keys.controls.settings.throughputInput.fixed)
: t(Keys.controls.settings.throughputInput.unlimited);
return (
<Stack {...titleAndInputStackProps}>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>Storage capacity</Label>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.storageCapacity)}
</Label>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>{capacity}</Text>
</Stack>
);
@@ -558,10 +568,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
/>
<Stack horizontal>
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
<Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.instant)}
</Separator>
</Stack.Item>
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
<Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.fourToSixHrs)}
</Separator>
</Stack.Item>
</Stack>
</Stack>
@@ -641,7 +655,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
Minimum RU/s
{t(Keys.controls.settings.throughputInput.minimumRuS)}
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack>
@@ -675,7 +689,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: "var(--colorNeutralForeground1)",
}}
>
x 10 =
{t(Keys.controls.settings.throughputInput.x10Equals)}
</Text>
{/* Column 3: Maximum RU/s */}
@@ -685,7 +699,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
Maximum RU/s
{t(Keys.controls.settings.throughputInput.maximumRuS)}
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack>
@@ -726,7 +740,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
const errorMessage: string =
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
sanitizedValue % 1000
? t(Keys.controls.settings.throughput.throughputIncrementError)
: this.props.throughputError;
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
}}
validateOnLoad={false}
@@ -772,7 +788,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
)}
{this.props.isAutoPilotSelected ? (
<Text style={{ marginTop: "40px", color: "var(--colorNeutralForeground1)" }}>
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
{t(Keys.controls.settings.throughputInput.autoscaleDescription, {
resourceType: this.props.collectionName ? "container" : "database",
})}{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
{this.props.maxAutoPilotThroughput} RU/s
@@ -787,16 +805,19 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
styles={this.darkThemeMessageBarStyles}
style={{ marginTop: "40px" }}
>
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
{t(Keys.controls.settings.throughputInput.freeTierWarning, {
ru: String(SharedConstants.FreeTierLimits.RU),
})}
</MessageBar>
)}
</>
)}
{!this.overrideWithProvisionedThroughputSettings() && (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
Estimate your required RU/s with
{t(Keys.controls.settings.throughputInput.capacityCalculator)}
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
{t(Keys.controls.settings.throughputInput.capacityCalculatorLink)}{" "}
<FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
)}
@@ -809,9 +830,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && (
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
)}
{this.props.isFixed && <p>{t(Keys.controls.settings.throughputInput.fixedStorageNote)}</p>}
{this.props.collectionName && (
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
)}

View File

@@ -561,9 +561,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
@@ -581,9 +579,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
Based on usage, your
container
throughput will scale from
Based on usage, your container throughput will scale from
<b>
400
@@ -692,7 +688,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -705,7 +702,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</div>
@@ -739,7 +737,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -752,7 +751,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</Stack>
@@ -1169,9 +1169,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
@@ -1755,9 +1753,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"

View File

@@ -27,7 +27,8 @@ exports[`ComputedPropertiesComponent renders 1`] = `
iconName="NavigateExternalInline"
/>
</StyledLinkBase>
  about how to define computed properties and how to use them.
 
about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2Editor"

View File

@@ -33,8 +33,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
}
}
>
Change
partition key
Change partition key
</Text>
<Stack
horizontal={true}
@@ -61,8 +60,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
}
}
>
Current
partition key
Current partition key
</Text>
<Text
styles={
@@ -223,8 +221,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
}
}
>
Current
partition key
Current partition key
</Text>
<Text
styles={

View File

@@ -410,9 +410,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
<Text
className="hintText-115"
>
Large
partition key
has been enabled.
Large partition key has been enabled.
</Text>
<Text
className="hintText-115"
@@ -984,9 +982,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
<Text
className="hintText-115"
>
Large
partition key
has been enabled.
Large partition key has been enabled.
</Text>
<Text
className="hintText-115"
@@ -1522,9 +1518,7 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
<Text
className="hintText-115"
>
Large
partition key
has been enabled.
Large partition key has been enabled.
</Text>
<Text
className="hintText-115"
@@ -2157,9 +2151,7 @@ exports[`SubSettingsComponent renders 1`] = `
<Text
className="hintText-115"
>
Large
partition key
has been enabled.
Large partition key has been enabled.
</Text>
<Text
className="hintText-115"
@@ -2729,9 +2721,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
<Text
className="hintText-115"
>
Large
partition key
has been enabled.
Large partition key has been enabled.
</Text>
<Text
className="hintText-115"

View File

@@ -1,6 +1,8 @@
import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import { userContext } from "../../../UserContext";
import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils";
@@ -175,25 +177,27 @@ const getStringValue = (value: isDirtyTypes, type: string): string => {
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
switch (tab) {
case SettingsV2TabTypes.ScaleTab:
return "Scale";
return t(Keys.controls.settings.tabTitles.scale);
case SettingsV2TabTypes.ConflictResolutionTab:
return "Conflict Resolution";
return t(Keys.controls.settings.tabTitles.conflictResolution);
case SettingsV2TabTypes.SubSettingsTab:
return "Settings";
return t(Keys.controls.settings.tabTitles.settings);
case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy";
return t(Keys.controls.settings.tabTitles.indexingPolicy);
case SettingsV2TabTypes.PartitionKeyTab:
return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)";
return isFabricNative()
? t(Keys.controls.settings.tabTitles.partitionKeys)
: t(Keys.controls.settings.tabTitles.partitionKeysPreview);
case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties";
return t(Keys.controls.settings.tabTitles.computedProperties);
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Policies";
return t(Keys.controls.settings.tabTitles.containerPolicies);
case SettingsV2TabTypes.ThroughputBucketsTab:
return "Throughput Buckets";
return t(Keys.controls.settings.tabTitles.throughputBuckets);
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return "Global Secondary Index (Preview)";
return t(Keys.controls.settings.tabTitles.globalSecondaryIndexPreview);
case SettingsV2TabTypes.DataMaskingTab:
return "Masking Policy (preview)";
return t(Keys.controls.settings.tabTitles.maskingPolicyPreview);
default:
throw new Error(`Unknown tab ${tab}`);
}
@@ -203,19 +207,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
if (description && !type) {
return {
type: MongoNotificationType.Warning,
message: "Please select a type for each index.",
message: t(Keys.controls.settings.mongoNotifications.selectTypeWarning),
};
}
if (type && (!description || description.trim().length === 0)) {
return {
type: MongoNotificationType.Error,
message: "Please enter a field name.",
message: t(Keys.controls.settings.mongoNotifications.enterFieldNameError),
};
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
return {
type: MongoNotificationType.Error,
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder,
message: t(Keys.controls.settings.mongoNotifications.wildcardPathError) + MongoWildcardPlaceHolder,
};
}
@@ -249,28 +253,29 @@ export const isIndexTransforming = (indexTransformationProgress: number): boolea
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
const partitionKeyName =
apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
};
export const getPartitionKeyTooltipText = (apiType: string): string => {
if (apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
return t(Keys.controls.settings.partitionKey.shardKeyTooltip);
}
let tooltipText = `The ${getPartitionKeyName(
apiType,
true,
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
let tooltipText = `The ${getPartitionKeyName(apiType, true)} ${t(
Keys.controls.settings.partitionKey.partitionKeyTooltip,
)}`;
if (apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
tooltipText += t(Keys.controls.settings.partitionKey.sqlPartitionKeyTooltipSuffix);
}
return tooltipText;
};
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
return subtext;
return t(Keys.controls.settings.partitionKey.partitionKeySubtext);
}
return "";
};
@@ -278,18 +283,18 @@ export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: st
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
switch (apiType) {
case "Mongo":
return "e.g., categoryId";
return t(Keys.controls.settings.partitionKey.mongoPlaceholder);
case "Gremlin":
return "e.g., /address";
return t(Keys.controls.settings.partitionKey.gremlinPlaceholder);
case "SQL":
return `${
index === undefined
? "Required - first partition key e.g., /TenantId"
? t(Keys.controls.settings.partitionKey.sqlFirstPartitionKey)
: index === 0
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
? t(Keys.controls.settings.partitionKey.sqlSecondPartitionKey)
: t(Keys.controls.settings.partitionKey.sqlThirdPartitionKey)
}`;
default:
return "e.g., /address/zipCode";
return t(Keys.controls.settings.partitionKey.defaultPlaceholder);
}
};

View File

@@ -127,9 +127,13 @@ exports[`SettingsUtils functions render 1`] = `
>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
Database:
sampleDb
, Container:
,
Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
@@ -309,7 +313,7 @@ exports[`SettingsUtils functions render 1`] = `
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>

View File

@@ -723,5 +723,216 @@
"information": "Information",
"moreDetails": "More details"
}
},
"controls": {
"settings": {
"tabTitles": {
"scale": "Scale",
"conflictResolution": "Conflict Resolution",
"settings": "Settings",
"indexingPolicy": "Indexing Policy",
"partitionKeys": "Partition Keys",
"partitionKeysPreview": "Partition Keys (preview)",
"computedProperties": "Computed Properties",
"containerPolicies": "Container Policies",
"throughputBuckets": "Throughput Buckets",
"globalSecondaryIndexPreview": "Global Secondary Index (Preview)",
"maskingPolicyPreview": "Masking Policy (preview)"
},
"mongoNotifications": {
"selectTypeWarning": "Please select a type for each index.",
"enterFieldNameError": "Please enter a field name.",
"wildcardPathError": "Wildcard path is not present in the field name. Use a pattern like "
},
"partitionKey": {
"shardKey": "Shard key",
"partitionKey": "Partition key",
"shardKeyTooltip": "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It's critical to choose a field that will evenly distribute your data.",
"partitionKeyTooltip": "is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.",
"sqlPartitionKeyTooltipSuffix": " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.",
"partitionKeySubtext": "For small workloads, the item ID is a suitable choice for the partition key.",
"mongoPlaceholder": "e.g., categoryId",
"gremlinPlaceholder": "e.g., /address",
"sqlFirstPartitionKey": "Required - first partition key e.g., /TenantId",
"sqlSecondPartitionKey": "second partition key e.g., /UserId",
"sqlThirdPartitionKey": "third partition key e.g., /SessionId",
"defaultPlaceholder": "e.g., /address/zipCode"
},
"costEstimate": {
"title": "Cost estimate*",
"howWeCalculate": "How we calculate this",
"updatedCostPerMonth": "Updated cost per month",
"currentCostPerMonth": "Current cost per month",
"perRu": "/RU"
},
"throughput": {
"manualToAutoscaleDisclaimer": "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.",
"ttlWarningText": "The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application. For more information see,",
"ttlWarningLinkText": "Time to Live (TTL) in Azure Cosmos DB",
"unsavedIndexingPolicy": "indexing policy",
"unsavedDataMaskingPolicy": "data masking policy",
"unsavedComputedProperties": "computed properties",
"unsavedEditorWarningPrefix": "You have not saved the latest changes made to your",
"unsavedEditorWarningSuffix": ". Please click save to confirm the changes.",
"updateDelayedApplyWarning": "You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some time to complete.",
"scalingUpDelayMessage": "Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your number of physical partitions. You can increase your throughput to {{instantMaximumThroughput}} instantly or proceed with this value and wait until the scale-up is completed.",
"exceedPreAllocatedMessage": "Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected. There are three options you can choose from to proceed:",
"instantScaleOption": "You can instantly scale up to {{instantMaximumThroughput}} RU/s.",
"asyncScaleOption": "You can asynchronously scale up to any value under {{maximumThroughput}} RU/s in 4-6 hours.",
"quotaMaxOption": "Your current quota max is {{maximumThroughput}} RU/s. To go over this limit, you must request a quota increase and the Azure Cosmos DB team will review.",
"belowMinimumMessage": "You are not able to lower throughput below your current minimum of {{minimum}} RU/s. For more information on this limit, please refer to our service quote documentation.",
"saveThroughputWarning": "Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes",
"currentAutoscaleThroughput": "Current autoscale throughput:",
"targetAutoscaleThroughput": "Target autoscale throughput:",
"currentManualThroughput": "Current manual throughput:",
"targetManualThroughput": "Target manual throughput:",
"applyDelayedMessage": "The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days to complete. View the latest status in Notifications.",
"databaseLabel": "Database:",
"containerLabel": "Container:",
"applyShortDelayMessage": "A request to increase the throughput is currently in progress. This operation will take some time to complete.",
"applyLongDelayMessage": "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.",
"throughputCapError": "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 {{newTotalThroughput}} RU/s. Change total throughput limit in cost management.",
"throughputIncrementError": "Throughput value must be in increments of 1000"
},
"conflictResolution": {
"lwwDefault": "Last Write Wins (default)",
"customMergeProcedure": "Merge Procedure (custom)",
"mode": "Mode",
"conflictResolverProperty": "Conflict Resolver Property",
"storedProcedure": "Stored procedure",
"lwwTooltip": "Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the winner for the conflicting versions of the document. Specify your own integer property if you want to override the default timestamp based conflict resolution.",
"customTooltip": "Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write application defined logic to determine the winner of the conflicting versions of a document. The stored procedure will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the conflicts will be populated in the",
"customTooltipConflictsFeed": " conflicts feed",
"customTooltipSuffix": ". You can update/re-register the stored procedure at any time."
},
"changeFeed": {
"label": "Change feed log retention policy",
"tooltip": "Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default. To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes. Reads are unaffected."
},
"mongoIndexing": {
"disclaimer": "For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.",
"disclaimerCompoundIndexesLink": " Compound indexes ",
"disclaimerSuffix": "are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo shell.",
"compoundNotSupported": "Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this collection, use the Mongo Shell.",
"aadError": "To use the indexing policy editor, please login to the",
"aadErrorLink": "azure portal.",
"refreshingProgress": "Refreshing index transformation progress",
"canMakeMoreChangesZero": "You can make more indexing changes once the current index transformation is complete. ",
"refreshToCheck": "Refresh to check if it has completed.",
"canMakeMoreChangesProgress": "You can make more indexing changes once the current index transformation has completed. It is {{progress}}% complete. ",
"refreshToCheckProgress": "Refresh to check the progress.",
"definitionColumn": "Definition",
"typeColumn": "Type",
"dropIndexColumn": "Drop Index",
"addIndexBackColumn": "Add index back",
"deleteIndexButton": "Delete index Button",
"addBackIndexButton": "Add back Index Button",
"currentIndexes": "Current index(es)",
"indexesToBeDropped": "Index(es) to be dropped",
"indexFieldName": "Index Field Name",
"indexType": "Index Type",
"selectIndexType": "Select an index type",
"undoButton": "Undo Button"
},
"subSettings": {
"timeToLive": "Time to Live",
"ttlOff": "Off",
"ttlOnNoDefault": "On (no default)",
"ttlOn": "On",
"seconds": "second(s)",
"timeToLiveInSeconds": "Time to live in seconds",
"analyticalStorageTtl": "Analytical Storage Time to Live",
"geospatialConfiguration": "Geospatial Configuration",
"geography": "Geography",
"geometry": "Geometry",
"uniqueKeys": "Unique keys",
"mongoTtlMessage": "To enable time-to-live (TTL) for your collection/documents,",
"mongoTtlLinkText": "create a TTL index",
"partitionKeyTooltipTemplate": "This {{partitionKeyName}} is used to distribute data across multiple partitions for scalability. The value \"{{partitionKeyValue}}\" determines how documents are partitioned.",
"largePartitionKeyEnabled": "Large {{partitionKeyName}} has been enabled.",
"hierarchicalPartitioned": "Hierarchically partitioned container.",
"nonHierarchicalPartitioned": "Non-hierarchically partitioned container."
},
"scale": {
"freeTierInfo": "With free tier, you will get the first {{ru}} RU/s and {{storage}} GB of storage in this account for free. To keep your account free, keep the total RU/s across all resources in the account to {{ru}} RU/s.",
"freeTierLearnMore": "Learn more.",
"throughputRuS": "Throughput (RU/s)",
"autoScaleCustomSettings": "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.",
"keyspaceSharedThroughput": "This table shared throughput is configured at the keyspace"
},
"partitionKeyEditor": {
"changePartitionKey": "Change {{partitionKeyName}}",
"currentPartitionKey": "Current {{partitionKeyName}}",
"partitioning": "Partitioning",
"hierarchical": "Hierarchical",
"nonHierarchical": "Non-hierarchical",
"safeguardWarning": "To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the source container for the entire duration of the partition key change process.",
"changeDescription": "To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container.",
"changeButton": "Change",
"changeJob": "{{partitionKeyName}} change job",
"cancelButton": "Cancel",
"documentsProcessed": "({{processedCount}} of {{totalCount}} documents processed)"
},
"computedProperties": {
"ariaLabel": "Computed properties",
"learnMorePrefix": "about how to define computed properties and how to use them."
},
"indexingPolicy": {
"ariaLabel": "Indexing Policy"
},
"dataMasking": {
"ariaLabel": "Data Masking Policy",
"validationFailed": "Validation failed:",
"includedPathsRequired": "includedPaths is required",
"includedPathsMustBeArray": "includedPaths must be an array",
"excludedPathsMustBeArray": "excludedPaths must be an array if provided"
},
"containerPolicy": {
"vectorPolicy": "Vector Policy",
"fullTextPolicy": "Full Text Policy",
"createFullTextPolicy": "Create new full text search policy"
},
"globalSecondaryIndex": {
"indexesDefined": "This container has the following indexes defined for it.",
"learnMoreSuffix": "about how to define global secondary indexes and how to use them.",
"jsonAriaLabel": "Global Secondary Index JSON",
"addIndex": "Add index",
"settingsTitle": "Global Secondary Index Settings",
"sourceContainer": "Source container",
"indexDefinition": "Global secondary index definition"
},
"indexingPolicyRefresh": {
"refreshFailed": "Refreshing index transformation progress failed"
},
"throughputInput": {
"autoscale": "Autoscale",
"manual": "Manual",
"minimumRuS": "Minimum RU/s",
"maximumRuS": "Maximum RU/s",
"x10Equals": "x 10 =",
"storageCapacity": "Storage capacity",
"fixed": "Fixed",
"unlimited": "Unlimited",
"instant": "Instant",
"fourToSixHrs": "4-6 hrs",
"autoscaleDescription": "Based on usage, your {{resourceType}} throughput will scale from",
"freeTierWarning": "Billing will apply if you provision more than {{ru}} RU/s of manual throughput, or if the resource scales beyond {{ru}} RU/s with autoscale.",
"capacityCalculator": "Estimate your required RU/s with",
"capacityCalculatorLink": " capacity calculator",
"fixedStorageNote": "When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.",
"min": "min",
"max": "max"
},
"throughputBuckets": {
"label": "Throughput Buckets",
"bucketLabel": "Bucket {{id}}",
"dataExplorerQueryBucket": " (Data Explorer Query Bucket)",
"active": "Active",
"inactive": "Inactive"
}
},
"dialog": {
"defaultCloseButton": "Close"
}
}
}