Compare commits

..

1 Commits

Author SHA1 Message Date
Asier Isayas
7d41990e13 add the aad endpoint to known authorities when setting up MSAL 2026-03-06 07:08:55 -08:00
31 changed files with 416 additions and 801 deletions

View File

@@ -42,8 +42,6 @@ import {
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { CollectionCreation } from "Shared/Constants";
@@ -179,7 +177,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText={t(Keys.common.learnMore)}
linkText="Learn more"
/>
)}
@@ -276,17 +274,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
@@ -306,7 +304,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
@@ -319,7 +317,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
@@ -349,9 +347,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={t(Keys.panes.addCollection.shareThroughput, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -369,17 +365,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
@@ -428,18 +424,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
@@ -453,10 +445,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ collectionId: event.target.value })
@@ -470,7 +462,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.indexing)}
Indexing
</Text>
</Stack>
@@ -478,32 +470,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.enableIndexing}
aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
aria-label="Turn on indexing"
aria-checked={this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
<span className="panelRadioBtnLabel">Automatic</span>
<input
className="panelRadioBtn"
checked={!this.state.enableIndexing}
aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
aria-label="Turn off indexing"
aria-checked={!this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
<span className="panelRadioBtnLabel">Off</span>
</Stack>
<Text variant="small">
{this.getFreeTierIndexingText()}{" "}
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
</Stack>
@@ -516,17 +508,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.sharding)}
Sharding
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.shardingTooltip)}
content={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
ariaLabel={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
/>
</TooltipHost>
</Stack>
@@ -535,7 +531,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={!this.state.isSharded}
aria-label={t(Keys.panes.addCollection.unsharded)}
aria-label="Unsharded"
aria-checked={!this.state.isSharded}
name="unsharded"
type="radio"
@@ -544,12 +540,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span>
<input
className="panelRadioBtn"
checked={this.state.isSharded}
aria-label={t(Keys.panes.addCollection.sharded)}
aria-label="Sharded"
aria-checked={this.state.isSharded}
name="sharded"
type="radio"
@@ -558,7 +554,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
<span className="panelRadioBtnLabel">Sharded</span>
</Stack>
</Stack>
)}
@@ -683,14 +679,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
>
{t(Keys.panes.addCollection.addPartitionKey)}
Add hierarchical partition key
</DefaultButton>
{this.state.subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
)}
@@ -703,9 +700,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
checked={this.state.enableDedicatedThroughput}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -723,19 +718,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
/>
</TooltipHost>
</Stack>
@@ -770,8 +769,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
: t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
value={uniqueKey}
@@ -803,7 +802,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
styles={{ root: { padding: 0 }, label: { fontSize: 12, color: "var(--colorNeutralForeground1)" } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
>
{t(Keys.panes.addCollection.addUniqueKey)}
Add unique key
</ActionButton>
</Stack>
)}
@@ -824,7 +823,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -833,13 +832,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -848,28 +847,26 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}{" "}
<br />
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
<DefaultButton
text={t(Keys.panes.addCollection.enable)}
text="Enable"
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
@@ -881,7 +878,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.containerVectorPolicy)}
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
@@ -909,7 +906,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowFullTextSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
@@ -938,7 +935,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.advanced)}
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
@@ -951,23 +948,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.indexing)}
Indexing
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.mongoIndexingTooltip)}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.mongoIndexingTooltip)}
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
/>
</TooltipHost>
</Stack>
<Checkbox
label={t(Keys.panes.addCollection.createWildcardIndex)}
label="Create a Wildcard Index on all fields"
checked={this.state.createMongoWildCardIndex}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -989,7 +986,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<Checkbox
label={t(Keys.panes.addCollection.legacySdkCheckbox)}
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={this.state.useHashV1}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -1006,9 +1003,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the
created container will use a legacy partitioning scheme that supports partition key values of size
only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
</Stack>
@@ -1019,7 +1018,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div>
{!this.props.isCopyJobFlow && (
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
)}
{this.state.isExecuting && (
@@ -1045,7 +1044,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
progressTrack: { backgroundColor: "#A6A6A6" },
progressBar: { background: "white" },
}}
label={t(Keys.panes.addCollection.addingSampleDataSet)}
label="Adding sample data set"
/>
</TeachingBubble>
)}
@@ -1151,8 +1150,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private getFreeTierIndexingText(): string {
return this.state.enableIndexing
? t(Keys.panes.addCollection.indexingOnInfo)
: t(Keys.panes.addCollection.indexingOffInfo);
? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
}
private getPartitionKeySubtext(): string {
@@ -1250,14 +1249,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
const errorMessage = this.isNewDatabaseAutoscale
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
this.setState({ errorMessage });
return false;
}
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
this.setState({ errorMessage: t(Keys.panes.addCollection.unshardedMaxRuError) });
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
return false;
}
@@ -1271,12 +1270,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
if (this.shouldShowVectorSearchParameters()) {
if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
}
if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: t(Keys.panes.addCollection.fullTextSearchPolicyError) });
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
return false;
}
}

View File

@@ -3,33 +3,28 @@ import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
return t(Keys.panes.addCollectionUtility.shardKeyTooltip);
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.";
}
let tooltipText = t(Keys.panes.addCollectionUtility.partitionKeyTooltip, {
partitionKeyName: getPartitionKeyName(true),
});
let tooltipText = `The ${getPartitionKeyName(
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.`;
if (userContext.apiType === "SQL") {
tooltipText += t(Keys.panes.addCollectionUtility.partitionKeyTooltipSqlSuffix);
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.panes.addCollectionUtility.shardKeyLabel)
: t(Keys.panes.addCollectionUtility.partitionKeyLabel);
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
@@ -37,19 +32,19 @@ export function getPartitionKeyName(isLowerCase?: boolean): string {
export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return t(Keys.panes.addCollectionUtility.shardKeyPlaceholder);
return "e.g., categoryId";
case "Gremlin":
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderDefault);
return "e.g., /address";
case "SQL":
return `${
index === undefined
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderFirst)
? "Required - first partition key e.g., /TenantId"
: index === 0
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderSecond)
: t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderThird)
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderGraph);
return "e.g., /address/zipCode";
}
}
@@ -74,12 +69,13 @@ export function isFreeTierAccount(): boolean {
}
export function UniqueKeysHeader(): JSX.Element {
const tooltipContent = t(Keys.panes.addCollectionUtility.uniqueKeysTooltip);
const tooltipContent =
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollectionUtility.uniqueKeysLabel)}
Unique keys
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -103,11 +99,12 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
}
export function AnalyticalStoreHeader(): JSX.Element {
const tooltipContent = t(Keys.panes.addCollectionUtility.analyticalStoreTooltip);
const tooltipContent =
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollectionUtility.analyticalStoreLabel)}
Analytical Store
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -119,13 +116,14 @@ export function AnalyticalStoreHeader(): JSX.Element {
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
{t(Keys.panes.addCollectionUtility.analyticalStoreDescription)}{" "}
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);
@@ -157,9 +155,10 @@ export function scrollToSection(id: string): void {
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
{t(Keys.panes.addCollectionUtility.vectorPolicyTooltip)}{" "}
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);

View File

@@ -482,8 +482,10 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
}
variant="small"
>
Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link for this Cosmos DB account.
Azure Synapse Link is required for creating an analytical store
container
. Enable Synapse Link for this Cosmos DB account.
<br />
<StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
@@ -606,8 +608,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="removeIcon"
iconName="InfoSolid"
/>
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
<StyledLinkBase
href="https://aka.ms/cosmos-large-pk"

View File

@@ -1,7 +1,5 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
@@ -42,18 +40,15 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
const databaseIdLabel: string = isCassandraAccount
? t(Keys.panes.addDatabase.keyspaceIdLabel)
: t(Keys.panes.addDatabase.databaseIdLabel);
const databaseIdPlaceHolder: string = t(Keys.panes.addDatabase.databaseIdPlaceholder, { databaseLabel });
const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id";
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
const [databaseId, setDatabaseId] = useState<string>("");
const databaseIdTooltipText = t(Keys.panes.addDatabase.databaseTooltip, { databaseLabel, collectionsLabel });
const databaseIdTooltipText = `A ${
isCassandraAccount ? "keyspace" : "database"
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = t(Keys.panes.addDatabase.shareThroughputTooltip, {
databaseLabel,
collectionsLabel,
});
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
getNewDatabaseSharedThroughputDefault(),
);
@@ -149,15 +144,15 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
// TODO add feature flag that disables validation for customers with custom accounts
if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
setFormErrors(t(Keys.panes.addDatabase.greaterThanError, { minValue: AutoPilotUtils.autoPilotThroughput1K }));
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`,
);
return false;
}
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(
t(Keys.panes.addDatabase.acknowledgeSpendError, { period: isAutoscaleSelected ? "monthly" : "daily" }),
);
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
return false;
}
@@ -174,7 +169,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const props: RightPaneFormProps = {
formError: formErrors,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -192,7 +187,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText={t(Keys.common.learnMore)}
linkText="Learn more"
/>
)}
<div className="panelMainContent">

View File

@@ -40,8 +40,6 @@ import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -170,19 +168,19 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
}
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage: string = t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly);
const errorMessage: string = "Please acknowledge the estimated monthly spend.";
setErrorMessage(errorMessage);
return false;
}
if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) {
setErrorMessage(t(Keys.panes.addCollection.vectorPolicyError));
setErrorMessage("Please fix errors in container vector policy");
return false;
}
if (!fullTextPolicyValidated) {
setErrorMessage(t(Keys.panes.addCollection.fullTextSearchPolicyError));
setErrorMessage("Please fix errors in container full text search policy");
return false;
}
}
@@ -309,7 +307,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexId)}
Global secondary index container id
</Text>
</Stack>
<input
@@ -320,7 +318,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexIdPlaceholder)}
placeholder={`e.g., indexbyEmailId`}
size={40}
className="panelTextField"
value={globalSecondaryIndexId}
@@ -338,7 +336,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank"
>
{t(Keys.panes.addGlobalSecondaryIndex.projectionQueryTooltip)}
Learn more about defining global secondary indexes.
</Link>
}
>
@@ -351,7 +349,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
aria-required
required
autoComplete="off"
placeholder={t(Keys.panes.addGlobalSecondaryIndex.projectionQueryPlaceholder)}
placeholder={"SELECT c.email, c.accountId FROM c"}
size={40}
className="panelTextField"
value={definition || ""}
@@ -395,7 +393,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack>
</div>
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={isThroughputCapExceeded} />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />}
</form>
);

View File

@@ -2,16 +2,14 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -73,8 +71,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage =
isNewKeySpaceAutoscale || isTableAutoscale
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setFormError(errorMessage);
return;
}
@@ -151,7 +149,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -163,8 +161,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.keyspaceLabel)}{" "}
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.keyspaceTooltip)}</InfoTooltip>
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
</Text>
</Stack>
@@ -182,7 +179,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setExistingKeyspaceId("");
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
@@ -196,7 +193,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setIsKeyspaceShared(false);
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
<span className="panelRadioBtnLabel">Use existing</span>
</Stack>
{keyspaceCreateNew && (
@@ -278,9 +275,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.tableIdLabel)}{" "}
Enter CQL command to create the table.{" "}
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
{t(Keys.common.learnMore)}
Learn More
</Link>
</Text>
</Stack>
@@ -298,7 +295,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
placeholder="Enter table Id"
size={20}
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
@@ -310,7 +307,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
multiline
id="editor-area"
rows={5}
ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
ariaLabel="Table schema"
value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)}
/>
@@ -321,12 +318,17 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<input
type="checkbox"
id="tableSharedThroughput"
title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
title="Provision dedicated throughput for this table"
checked={dedicateTableThroughput}
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
/>
<span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
<span>Provision dedicated throughput for this table</span>
<InfoTooltip>
You can optionally provision dedicated throughput for a table within a keyspace that has throughput
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.
</InfoTooltip>
</Stack>
)}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (

View File

@@ -26,8 +26,6 @@ import {
import Explorer from "Explorer/Explorer";
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
import { useDatabases } from "Explorer/useDatabases";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
@@ -74,7 +72,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
await createDataTransferJob();
await onClose();
} catch (error) {
handleError(error, "ChangePartitionKey", t(Keys.panes.changePartitionKey.failedToStartError));
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
}
setIsExecuting(false);
useSidePanel.getState().closeSidePanel();
@@ -135,21 +133,17 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
};
return (
<RightPaneForm
formError={formError}
isExecuting={isExecuting}
onSubmit={submit}
submitButtonText={t(Keys.common.ok)}
>
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.changePartitionKey.description)}&nbsp;
When changing a containers partition key, you will need to create a destination container with the correct
partition key. You may also select an existing destination container.&nbsp;
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
target="_blank"
underline
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
<Stack>
@@ -224,18 +218,14 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
@@ -249,14 +239,10 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.changePartitionKey.collectionIdPlaceholder, {
collectionName: getCollectionName(),
})}
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
aria-label={t(Keys.panes.changePartitionKey.collectionIdAriaLabel, {
collectionName: getCollectionName(),
})}
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={targetCollectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
/>
@@ -363,7 +349,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
>
{t(Keys.panes.addCollection.addPartitionKey)}
Add hierarchical partition key
</DefaultButton>
{subPartitionKeys.length > 0 && (
<Text
@@ -371,10 +357,11 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
variant="small"
style={{ color: "var(--colorNeutralForeground1)" }}
>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
)}
@@ -390,18 +377,14 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
@@ -417,7 +400,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}}
defaultSelectedKey={targetCollectionId}
responsiveMode={999}
ariaLabel={t(Keys.panes.changePartitionKey.existingContainers)}
ariaLabel="Existing Containers"
/>
</Stack>
)}

View File

@@ -4,8 +4,6 @@ import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -84,14 +82,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const notebookContentItem = await copyNotebook(selectedLocation);
if (!notebookContentItem) {
throw new Error(t(Keys.panes.copyNotebook.uploadFailedError, { name }));
throw new Error(`Failed to upload ${name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
closeSidePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(t(Keys.panes.copyNotebook.copyFailedError, { name, destination }));
setFormError(`Failed to copy ${name} to ${destination}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally {
clearMessage && clearMessage();
@@ -138,7 +136,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const props: RightPaneFormProps = {
formError,
isExecuting: isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit: () => submit(),
};

View File

@@ -12,8 +12,6 @@ import {
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook";
@@ -98,8 +96,8 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
return options;
};
const dropDownProps: IDropdownProps = {
label: t(Keys.panes.copyNotebook.location),
ariaLabel: t(Keys.panes.copyNotebook.locationAriaLabel),
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: onRenderDropDownTitle,
onRenderOption: onRenderDropDownOption,
@@ -111,7 +109,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">{t(Keys.panes.copyNotebook.name)}</Label>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{name}</Text>
</Stack.Item>

View File

@@ -4,8 +4,6 @@ import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import { Collection } from "Contracts/ViewModels";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
@@ -36,15 +34,12 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
const paneTitle = "Delete " + collectionName;
const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = t(Keys.panes.deleteCollection.inputMismatch, {
input: inputCollectionName,
selectedId: collection.id(),
});
const errorMessage = "Input id " + inputCollectionName + " does not match the selected " + collection.id();
setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
@@ -111,23 +106,18 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const props: RightPaneFormProps = {
formError: formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit,
};
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
collectionName: collectionName.toLowerCase(),
});
const reasonInfo =
t(Keys.panes.deleteCollection.feedbackTitle) +
" " +
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">{confirmContainer}</Text>
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField
id="confirmCollectionId"
autoFocus
@@ -143,10 +133,10 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
{shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block>
{t(Keys.panes.deleteCollection.feedbackTitle)}
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
What is the reason why you are deleting this {collectionName}?
</Text>
<TextField
id="deleteCollectionFeedbackInput"

View File

@@ -34,7 +34,9 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<span
className="css-109"
>
Confirm by typing the container id
Confirm by typing the
container
id
</span>
</Text>
<StyledTextFieldBase

View File

@@ -5,8 +5,6 @@ import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
import { Collection, Database } from "Contracts/ViewModels";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
@@ -40,19 +38,11 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError(
t(Keys.panes.deleteDatabase.inputMismatch, {
databaseName: getDatabaseName(),
input: databaseInput,
selectedId: selectedDatabase.id(),
}),
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
);
logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
logConsoleError(
t(Keys.panes.deleteDatabase.inputMismatch, {
databaseName: getDatabaseName(),
input: databaseInput,
selectedId: selectedDatabase.id(),
}),
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
);
return;
}
@@ -124,20 +114,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const props: RightPaneFormProps = {
formError,
isExecuting: isLoading,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit: () => submit(),
};
const errorProps: PanelInfoErrorProps = {
messageType: "warning",
showErrorDetails: false,
message: t(Keys.panes.deleteDatabase.warningMessage),
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
const reasonInfo =
t(Keys.panes.deleteDatabase.feedbackTitle) +
" " +
t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() });
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`;
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
return (
<RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />}
@@ -160,10 +148,10 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
{isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback">
<Text variant="small" block>
{t(Keys.panes.deleteDatabase.feedbackTitle)}
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
{t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })}
What is the reason why you are deleting this {getDatabaseName()}?
</Text>
<TextField
id="deleteDatabaseFeedbackInput"

View File

@@ -3,8 +3,6 @@ import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useRef, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Tree/StoredProcedure";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -47,8 +45,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
};
const setInvalidParamError = (invalidParam: string): void => {
setFormError(t(Keys.panes.executeStoredProcedure.invalidParamError, { invalidParam }));
logConsoleError(t(Keys.panes.executeStoredProcedure.invalidParamConsoleError, { invalidParam }));
setFormError(`Invalid param specified: ${invalidParam}`);
logConsoleError(`Invalid param specified: ${invalidParam} is not a valid literal value`);
};
const submit = (): void => {
@@ -98,7 +96,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: t(Keys.common.execute),
submitButtonText: "Execute",
onSubmit: () => submit(),
};
@@ -109,9 +107,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
inputParameters.push(
<InputParameter
key={paramKeyValue.text + i}
dropdownLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.key) : ""}
inputParameterTitle={i === 0 ? t(Keys.panes.executeStoredProcedure.enterInputParameters) : ""}
inputLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.param) : ""}
dropdownLabel={i === 0 ? "Key" : ""}
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
inputLabel={i === 0 ? "Param" : ""}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
@@ -132,9 +130,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
<RightPaneForm {...props}>
<div className="panelMainContent">
<InputParameter
dropdownLabel={t(Keys.panes.executeStoredProcedure.key)}
inputParameterTitle={t(Keys.panes.executeStoredProcedure.partitionKeyValue)}
inputLabel={t(Keys.panes.executeStoredProcedure.value)}
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
@@ -145,8 +143,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
/>
{getInputParameterComponent()}
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt={t(Keys.panes.executeStoredProcedure.addParam)} />
<Text className="addNewParamStyle">{t(Keys.panes.executeStoredProcedure.addNewParam)}</Text>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
</RightPaneForm>

View File

@@ -11,8 +11,6 @@ import {
import React, { FunctionComponent } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import EntityCancelIcon from "../../../../images/Entity_cancel.svg";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
const options = [
@@ -76,7 +74,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Image
{...imageProps}
src={EntityCancelIcon}
alt={t(Keys.panes.executeStoredProcedure.deleteParam)}
alt="Delete param"
id="deleteparam"
role="button"
onClick={onDeleteParamKeyPress}
@@ -86,7 +84,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Image
{...imageProps}
src={AddPropertyIcon}
alt={t(Keys.panes.executeStoredProcedure.addParam)}
alt="Add param"
id="addparam"
role="button"
onClick={onAddNewParamKeyPress}

View File

@@ -1,7 +1,5 @@
import React, { FunctionComponent } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent";
import { IGraphConfig } from "../../Tabs/GraphTab";
@@ -19,7 +17,7 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
}: GraphStylingProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const buttonLabel = t(Keys.common.ok);
const buttonLabel = "Ok";
const submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

View File

@@ -5,8 +5,6 @@ import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger";
import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { useSelectedNode } from "../../useSelectedNode";
@@ -35,8 +33,8 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const submit = async (): Promise<void> => {
setFormError("");
if (!selectedFiles || selectedFiles.length === 0) {
setFormError(t(Keys.panes.loadQuery.noFileSpecifiedError));
logConsoleError(t(Keys.panes.loadQuery.noFileSpecifiedError));
setFormError("No file specified");
logConsoleError("Could not load query -- No file specified. Please input a file.");
return;
}
@@ -50,7 +48,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
setLoadingFalse();
} catch (error) {
setLoadingFalse();
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryError));
setFormError("Failed to load query");
logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
}
};
@@ -73,7 +71,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
};
reader.onerror = (): void => {
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryFromFileError, { fileName: file.name }));
setFormError("Failed to load query");
logConsoleError(`Failed to load query from file ${file.name}`);
};
return reader.readAsText(file);
@@ -81,7 +79,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: t(Keys.common.load),
submitButtonText: "Load",
onSubmit: () => submit(),
};
@@ -92,7 +90,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
<Stack horizontal>
<TextField
id="confirmCollectionId"
label={t(Keys.panes.loadQuery.selectFilesToOpen)}
label="Select a query document"
value={selectedFileName}
autoFocus
readOnly

View File

@@ -1,8 +1,6 @@
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -43,7 +41,7 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const props: RightPaneFormProps = {
formError: errorMessage,
isExecuting: isLoading,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit: () => submit(),
};

View File

@@ -1,6 +1,4 @@
import { Icon, Link, Stack, Text } from "@fluentui/react";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import React from "react";
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
@@ -22,17 +20,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
}: PanelInfoErrorProps): JSX.Element => {
const expandConsole = useNotificationConsole((state) => state.expandConsole);
let icon: JSX.Element = (
<Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label={t(Keys.panes.panelInfo.information)} />
);
let icon: JSX.Element = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
if (messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
} else if (messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" aria-label="warning" />;
} else if (messageType === "info") {
icon = (
<Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label={t(Keys.panes.panelInfo.information)} />
);
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
}
return (
@@ -49,7 +43,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
</Text>
{showErrorDetails && (
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
{t(Keys.panes.panelInfo.moreDetails)}
More details
</a>
)}
</span>

View File

@@ -5,8 +5,6 @@ import { getErrorMessage, getErrorStack, handleError } from "../../../Common/Err
import { useNotebookSnapshotStore } from "../../../hooks/useNotebookSnapshotStore";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
@@ -93,7 +91,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
let startKey: number;
if (!notebookName || !notebookDescription || !author || !imageSrc) {
setFormError(t(Keys.panes.publishNotebook.publishFailedError, { notebookName }));
setFormError(`Failed to publish ${notebookName} to gallery`);
setFormErrorDetail("Name, description, author and cover image are required");
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
setIsExecuting(false);
@@ -145,11 +143,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
);
const errorMessage = getErrorMessage(error);
setFormError(
t(Keys.panes.publishNotebook.publishFailedError, {
notebookName: FileSystemUtil.stripExtension(notebookName, "ipynb"),
}),
);
setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
return;

View File

@@ -1,8 +1,6 @@
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "@fluentui/react";
import { ImmutableNotebook } from "@nteract/commutable";
import React, { FunctionComponent, useState } from "react";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
@@ -59,11 +57,13 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
const maxImageSizeInMib = 1.5;
const descriptionPara1 = t(Keys.panes.publishNotebook.publishDescription);
const descriptionPara1 =
"When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.";
const descriptionPara2 = t(Keys.panes.publishNotebook.publishPrompt, {
name: FileSystemUtil.stripExtension(notebookName, "ipynb"),
});
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
notebookName,
"ipynb",
)}" to the gallery?`;
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
if (onTakeSnapshot) {
@@ -74,9 +74,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
}
const thumbnailSelectorProps: IDropdownProps = {
label: t(Keys.panes.publishNotebook.coverImage),
label: "Cover image",
selectedKey: type,
ariaLabel: t(Keys.panes.publishNotebook.coverImage),
ariaLabel: "Cover image",
options: options.map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
setImageSrc("");
@@ -99,7 +99,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
notebookContentRef,
});
} else {
firstOutputErrorHandler(new Error(t(Keys.panes.publishNotebook.outputDoesNotExist)));
firstOutputErrorHandler(new Error("Output does not exist for any of the cells."));
}
}
setType(options.text);
@@ -107,8 +107,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
};
const thumbnailUrlProps: ITextFieldProps = {
label: t(Keys.panes.publishNotebook.coverImageUrl),
ariaLabel: t(Keys.panes.publishNotebook.coverImageUrl),
label: "Cover image url",
ariaLabel: "Cover image url",
required: true,
onChange: (event, newValue) => {
setImageSrc(newValue);
@@ -116,7 +116,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
};
const firstOutputErrorHandler = (error: Error) => {
const formError = t(Keys.panes.publishNotebook.failedToCaptureOutput);
const formError = "Failed to capture first output";
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/UseFirstOutput";
onError(formError, formErrorDetail, area);
@@ -130,7 +130,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
};
reader.onerror = (error) => {
const formError = t(Keys.panes.publishNotebook.failedToConvertError, { fileName: file.name });
const formError = `Failed to convert ${file.name} to base64 format`;
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/selectImageFile";
onError(formError, formErrorDetail, area);
@@ -151,7 +151,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
const file = event.target.files[0];
if (file.size / 1024 ** 2 > maxImageSizeInMib) {
event.target.value = "";
const formError = t(Keys.panes.publishNotebook.failedToUploadError, { fileName: file.name });
const formError = `Failed to upload ${file.name}`;
const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`;
const area = "PublishNotebookPaneComponent/selectImageFile";
@@ -185,8 +185,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>
<TextField
label={t(Keys.panes.publishNotebook.name)}
ariaLabel={t(Keys.panes.publishNotebook.name)}
label="Name"
ariaLabel="Name"
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
required
onChange={(event, newValue) => {
@@ -198,8 +198,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>
<TextField
label={t(Keys.panes.publishNotebook.description)}
ariaLabel={t(Keys.panes.publishNotebook.description)}
label="Description"
ariaLabel="Description"
multiline
rows={3}
required
@@ -211,9 +211,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>
<TextField
label={t(Keys.panes.publishNotebook.tags)}
ariaLabel={t(Keys.panes.publishNotebook.tags)}
placeholder={t(Keys.panes.publishNotebook.tagsPlaceholder)}
label="Tags"
ariaLabel="Tags"
placeholder="Optional tag 1, Optional tag 2"
onChange={(event, newValue) => {
setNotebookTags(newValue);
}}
@@ -227,7 +227,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
<Stack.Item>
<Text>{t(Keys.panes.publishNotebook.preview)}</Text>
<Text>Preview</Text>
</Stack.Item>
<Stack.Item>
<GalleryCardComponent

View File

@@ -4,8 +4,6 @@ import React, { FunctionComponent, useState } from "react";
import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
@@ -30,27 +28,27 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const [formError, setFormError] = useState<string>("");
const [queryName, setQueryName] = useState<string>("");
const setupSaveQueriesText = t(Keys.panes.saveQuery.setupCostMessage, { databaseName: SavedQueries.DatabaseName });
const title = t(Keys.panes.saveQuery.panelTitle);
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
const title = "Save Query";
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const submit = async (): Promise<void> => {
setFormError("");
if (!isSaveQueryEnabled()) {
setFormError("Cannot save query");
logConsoleError(t(Keys.panes.saveQuery.accountNotSetupError));
logConsoleError("Failed to save query: account not setup to save queries");
}
const queryTab = useTabs.getState().activeTab as NewQueryTab;
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) {
setFormError(t(Keys.panes.saveQuery.noQueryNameError));
logConsoleError(t(Keys.panes.saveQuery.noQueryNameError));
setFormError("No query name specified");
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
return;
} else if (!query || query.length === 0) {
setFormError(t(Keys.panes.saveQuery.invalidQueryContentError));
logConsoleError(t(Keys.panes.saveQuery.invalidQueryContentError));
setFormError("Invalid query content specified");
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content.");
return;
}
@@ -82,8 +80,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
} catch (error) {
setLoadingFalse();
const errorMessage = getErrorMessage(error);
setFormError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }));
logConsoleError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }) + ": " + errorMessage);
setFormError("Failed to save query");
logConsoleError(`Failed to save query: ${errorMessage}`);
traceFailure(
Action.SaveQuery,
{
@@ -128,8 +126,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
},
startKey,
);
setFormError(t(Keys.panes.saveQuery.failedToSetupContainerError));
logConsoleError(t(Keys.panes.saveQuery.failedToSetupContainerError) + ": " + errorMessage);
setFormError("Failed to setup a container for saved queries");
logConsoleError(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally {
setLoadingFalse();
}
@@ -138,7 +136,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: isSaveQueryEnabled() ? t(Keys.common.save) : t(Keys.panes.saveQuery.completeSetup),
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
onSubmit: () => {
isSaveQueryEnabled() ? submit() : setupQueries();
},
@@ -162,7 +160,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
) : (
<TextField
id="saveQueryInput"
label={t(Keys.panes.saveQuery.name)}
label="Name"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {

View File

@@ -24,8 +24,6 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import {
AppStateComponentNames,
@@ -237,7 +235,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const regionOptions: IDropdownOption[] = [];
regionOptions.push({
key: userContext?.databaseAccount?.properties?.documentEndpoint,
text: t(Keys.panes.settings.globalDefault),
text: `Global (Default)`,
data: {
isGlobal: true,
writeEnabled: true,
@@ -248,7 +246,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} ${t(Keys.panes.settings.readWrite)}`,
text: `${loc.locationName} (Read/Write)`,
data: {
isGlobal: false,
writeEnabled: true,
@@ -261,7 +259,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} ${t(Keys.panes.settings.read)}`,
text: `${loc.locationName} (Read)`,
data: {
isGlobal: false,
writeEnabled: false,
@@ -319,9 +317,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
authError instanceof msalAuthError &&
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
) {
logConsoleError(t(Keys.panes.settings.popupsDisabledError));
logConsoleError(
`We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`,
);
} else {
logConsoleError(t(Keys.panes.settings.failedToAcquireTokenError));
logConsoleError(
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
);
}
}
} else {
@@ -483,33 +485,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const genericPaneProps: RightPaneFormProps = {
formError: "",
isExecuting,
submitButtonText: t(Keys.common.apply),
submitButtonText: "Apply",
onSubmit: () => handlerOnSubmit(),
};
const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: t(Keys.panes.settings.custom) },
{ key: Constants.Queries.UnlimitedPageOption, text: t(Keys.panes.settings.unlimited) },
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" },
];
const graphAutoOptionList: IChoiceGroupOption[] = [
{ key: "false", text: t(Keys.panes.settings.graph) },
{ key: "true", text: t(Keys.panes.settings.json) },
{ key: "false", text: "Graph" },
{ key: "true", text: "JSON" },
];
const priorityLevelOptionList: IChoiceGroupOption[] = [
{ key: Constants.PriorityLevel.Low, text: t(Keys.panes.settings.low) },
{ key: Constants.PriorityLevel.High, text: t(Keys.panes.settings.high) },
{ key: Constants.PriorityLevel.Low, text: "Low" },
{ key: Constants.PriorityLevel.High, text: "High" },
];
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: t(Keys.panes.settings.automatic) },
{ key: Constants.RBACOptions.setTrueRBACOption, text: t(Keys.panes.settings["true"]) },
{ key: Constants.RBACOptions.setFalseRBACOption, text: t(Keys.panes.settings["false"]) },
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" },
{ key: Constants.RBACOptions.setTrueRBACOption, text: "True" },
{ key: Constants.RBACOptions.setFalseRBACOption, text: "False" },
];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: t(Keys.tabs.query.vertical) },
{ key: SplitterDirection.Horizontal, text: t(Keys.tabs.query.horizontal) },
{ key: SplitterDirection.Vertical, text: "Vertical" },
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
@@ -722,12 +724,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowQueryPageOptions && (
<AccordionItem value="1">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.pageOptions)}</div>
<div className={styles.header}>Page Options</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.pageOptionsDescription)}
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
many query results per page.
</div>
<ChoiceGroup
ariaLabelledBy="pageOptions"
@@ -741,14 +744,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{isCustomPageOptionSelected() && (
<div className="tabcontent">
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.queryResultsPerPage)}{" "}
Query results per page{" "}
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.queryResultsPerPageTooltip)}
Enter the number of query results that should be shown per page.
</InfoTooltip>
</div>
<SpinButton
ariaLabel={t(Keys.panes.settings.customQueryItemsPerPage)}
ariaLabel="Custom query items per page"
value={"" + customItemPerPage}
onIncrement={(newValue) => {
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
@@ -758,8 +761,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
min={1}
step={1}
className="textfontclr"
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
styles={spinButtonStyles}
/>
</div>
@@ -771,19 +774,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{showEnableEntraIdRbac && (
<AccordionItem value="2">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.entraIdRbac)}</div>
<div className={styles.header}>Enable Entra ID RBAC</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.entraIdRbacDescription)}
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
ID RBAC.
<a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
target="_blank"
rel="noopener noreferrer"
>
{" "}
{t(Keys.common.learnMore)}{" "}
Learn more{" "}
</a>
</div>
<ChoiceGroup
@@ -800,17 +804,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
<AccordionItem value="3">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.regionSelection)}</div>
<div className={styles.header}>Region Selection</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.regionSelectionDescription)}
Changes region the Cosmos Client uses to access account.
</div>
<div>
<span className={styles.subHeader}>{t(Keys.panes.settings.selectRegion)}</span>
<span className={styles.subHeader}>Select Region</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.selectRegionTooltip)}
Changes the account endpoint used to perform client operations.
</InfoTooltip>
</div>
<Dropdown
@@ -861,16 +865,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<>
<AccordionItem value="4">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.queryTimeout)}</div>
<div className={styles.header}>Query Timeout</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.queryTimeoutDescription)}
When a query reaches a specified time limit, a popup with an option to cancel the query will
show unless automatic cancellation has been enabled.
</div>
<Toggle
styles={toggleStyles}
label={t(Keys.panes.settings.enableQueryTimeout)}
label="Enable query timeout"
onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled}
/>
@@ -878,18 +883,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{queryTimeoutEnabled && (
<div className={styles.settingsSectionContainer}>
<SpinButton
label={t(Keys.panes.settings.queryTimeoutMs)}
label="Query timeout (ms)"
labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()}
min={100}
step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
<Toggle
label={t(Keys.panes.settings.automaticallyCancelQuery)}
label="Automatically cancel query after timeout"
styles={toggleStyles}
onChange={handleOnAutomaticallyCancelQueryToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout}
@@ -900,16 +905,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
<AccordionItem value="5">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.ruLimit)}</div>
<div className={styles.header}>RU Limit</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.ruLimitDescription)}
If a query exceeds a configured RU limit, the query will be aborted.
</div>
<Toggle
styles={toggleStyles}
label={t(Keys.panes.settings.enableRuLimit)}
label="Enable RU limit"
onChange={handleOnRUThresholdToggleChange}
defaultChecked={ruThresholdEnabled}
/>
@@ -917,14 +922,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ruThresholdEnabled && (
<div className={styles.settingsSectionContainer}>
<SpinButton
label={t(Keys.panes.settings.ruLimitLabel)}
label="RU Limit (RU)"
labelPosition={Position.top}
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
min={1}
step={1000}
onChange={handleOnRUThresholdSpinButtonChange}
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
</div>
@@ -934,12 +939,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<AccordionItem value="6">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.defaultQueryResults)}</div>
<div className={styles.header}>Default Query Results View</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.defaultQueryResultsDescription)}
Select the default view to use when displaying query results.
</div>
<ChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
@@ -957,17 +962,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{showRetrySettings && (
<AccordionItem value="7">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.retrySettings)}</div>
<div className={styles.header}>Retry Settings</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.retrySettingsDescription)}
Retry policy associated with throttled requests during CosmosDB queries.
</div>
<div>
<span className={styles.subHeader}>{t(Keys.panes.settings.maxRetryAttempts)}</span>
<span className={styles.subHeader}>Max retry attempts</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.maxRetryAttemptsTooltip)}
Max number of retries to be performed for a request. Default value 9.
</InfoTooltip>
</div>
<SpinButton
@@ -976,17 +981,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1}
value={"" + retryAttempts}
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
styles={spinButtonStyles}
/>
<div>
<span className={styles.subHeader}>{t(Keys.panes.settings.fixedRetryInterval)}</span>
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.fixedRetryIntervalTooltip)}
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
as part of the response. Default value is 0 milliseconds.
</InfoTooltip>
</div>
<SpinButton
@@ -995,17 +1001,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1000}
value={"" + retryInterval}
onChange={handleOnRetryIntervalSpinButtonChange}
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
styles={spinButtonStyles}
/>
<div>
<span className={styles.subHeader}>{t(Keys.panes.settings.maxWaitTime)}</span>
<span className={styles.subHeader}>Max wait time (s)</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.maxWaitTimeTooltip)}
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
seconds.
</InfoTooltip>
</div>
<SpinButton
@@ -1014,8 +1021,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1}
value={"" + MaxWaitTimeInSeconds}
onChange={handleOnMaxWaitTimeSpinButtonChange}
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) =>
setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)
}
@@ -1032,26 +1039,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{!isEmulator && (
<AccordionItem value="8">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableContainerPagination)}</div>
<div className={styles.header}>Enable container pagination</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableContainerPaginationDescription)}
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableContainerPagination)}
ariaLabel="Enable container pagination"
checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
label={t(Keys.panes.settings.enableContainerPagination)}
label="Enable container pagination"
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableContainerPagination)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable container pagination</span>
)}
/>
</div>
@@ -1061,25 +1066,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowCrossPartitionOption && (
<AccordionItem value="9">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableCrossPartitionQuery)}</div>
<div className={styles.header}>Enable cross-partition query</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableCrossPartitionQueryDescription)}
Send more than one request while executing a query. More than one request is necessary if the
query is not scoped to single partition key value.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableCrossPartitionQuery)}
ariaLabel="Enable cross partition query"
checked={crossPartitionQueryEnabled}
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableCrossPartitionQuery)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable cross-partition query</span>
)}
/>
</div>
@@ -1089,19 +1093,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowEnhancedQueryControl && (
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enhancedQueryControl)}</div>
<div className={styles.header}>Enhanced query control</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.maxDegreeOfParallelismQuery)}
Query up to the max degree of parallelism.
<a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control"
target="_blank"
rel="noopener noreferrer"
>
{" "}
{t(Keys.common.learnMore)}{" "}
Learn more{" "}
</a>
</div>
<Checkbox
@@ -1109,13 +1113,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableQueryControl)}
ariaLabel="EnableQueryControl"
checked={queryControlEnabled}
onChange={() => setQueryControlEnabled(!queryControlEnabled)}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableQueryControl)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable query control</span>
)}
/>
</div>
@@ -1125,12 +1127,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowParallelismOption && (
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.maxDegreeOfParallelism)}</div>
<div className={styles.header}>Max degree of parallelism</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.maxDegreeOfParallelismDescription)}
Gets or sets the number of concurrent operations run client side during parallel query execution.
A positive property value limits the number of concurrent operations to the set value. If it is
set to less than 0, the system automatically decides the number of concurrent operations to run.
</div>
<SpinButton
min={-1}
@@ -1146,8 +1150,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
}
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
ariaLabel={t(Keys.panes.settings.maxDegreeOfParallelism)}
label={t(Keys.panes.settings.maxDegreeOfParallelism)}
ariaLabel="Max degree of parallelism"
label="Max degree of parallelism"
styles={spinButtonStyles}
/>
</div>
@@ -1157,12 +1161,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowPriorityLevelOption && (
<AccordionItem value="11">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.priorityLevel)}</div>
<div className={styles.header}>Priority Level</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.priorityLevelDescription)}
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
Execution. If &quot;None&quot; is selected, Data Explorer will not specify priority level, and the
server-side default priority level will be used.
</div>
<ChoiceGroup
ariaLabelledBy="priorityLevel"
@@ -1178,18 +1184,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowGraphAutoVizOption && (
<AccordionItem value="12">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.displayGremlinQueryResults)}&nbsp;</div>
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.displayGremlinQueryResultsDescription)}
Select Graph to automatically visualize the query results as a Graph or JSON to display the
results as JSON.
</div>
<ChoiceGroup
selectedKey={graphAutoVizDisabled}
options={graphAutoOptionList}
onChange={handleOnGremlinChange}
aria-label={t(Keys.panes.settings.graphAutoVisualization)}
aria-label="Graph Auto-visualization"
/>
</div>
</AccordionPanel>
@@ -1198,25 +1205,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowCopilotSampleDBOption && (
<AccordionItem value="13">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableSampleDatabase)}</div>
<div className={styles.header}>Enable sample database</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableSampleDatabaseDescription)}
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
and maintained by Microsoft at no cost to you.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableSampleDbAriaLabel)}
ariaLabel="Enable sample db for query exploration"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableSampleDatabase)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable sample database</span>
)}
/>
</div>
@@ -1226,12 +1233,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowMongoGuidRepresentationOption && (
<AccordionItem value="14">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.guidRepresentation)}</div>
<div className={styles.header}>Guid Representation</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.guidRepresentationDescription)}
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and
deserialized when stored in BSON documents. This will apply to all document operations.
</div>
<Dropdown
aria-labelledby="mongoGuidRepresentation"
@@ -1245,7 +1253,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)}
<AccordionItem value="15">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.advancedSettings)}</div>
<div className={styles.header}>Advanced Settings</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
@@ -1275,13 +1283,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
},
}}
className="padding"
ariaLabel={t(Keys.panes.settings.ignorePartitionKey)}
ariaLabel="Ignore partition key on document update"
checked={ignorePartitionKeyOnDocumentUpdate}
onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange}
label={t(Keys.panes.settings.ignorePartitionKey)}
label="Ignore partition key on document update"
/>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.ignorePartitionKeyTooltip)}
If checked, the partition key value will not be used to locate the document during update
operations. Only use this if document updates are failing due to an abnormal partition key.
</InfoTooltip>
</Stack>
</div>
@@ -1311,9 +1320,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}}
onClick={() => {
useDialog.getState().showOkCancelModalDialog(
t(Keys.panes.settings.clearHistory),
"Clear History",
undefined,
t(Keys.panes.settings.clearHistoryConfirm),
"Are you sure you want to proceed?",
() => {
deleteAllStates();
updateUserContext({
@@ -1323,33 +1332,35 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
},
t(Keys.common.cancel),
"Cancel",
undefined,
<>
<span>{t(Keys.panes.settings.clearHistoryDescription)}</span>
<span>
This action will clear the all customizations for this account in this browser, including:
</span>
<ul className={styles.bulletList}>
<li>{t(Keys.panes.settings.clearHistoryTabLayout)}</li>
<li>{t(Keys.panes.settings.clearHistoryTableColumns)}</li>
<li>{t(Keys.panes.settings.clearHistoryFilters)}</li>
<li>{t(Keys.panes.settings.clearHistoryRegion)}</li>
<li>Reset your customized tab layout, including the splitter positions</li>
<li>Erase your table column preferences, including any custom columns</li>
<li>Clear your filter history</li>
<li>Reset region selection to global</li>
</ul>
</>,
);
}}
>
{t(Keys.panes.settings.clearHistory)}
Clear History
</DefaultButton>
</div>
</div>
<div className="settingsSection">
<div className={`settingsSectionPart ${styles.settingsSectionContainer}`}>
<div className="settingsSectionLabel">{t(Keys.panes.settings.explorerVersion)}</div>
<div className="settingsSectionLabel">Explorer Version</div>
<div>{explorerVersion}</div>
</div>
</div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">{t(Keys.panes.settings.sessionId)}</div>
<div className="settingsSectionLabel">Session ID</div>
<div>{sessionId}</div>
</div>
</div>

View File

@@ -660,7 +660,7 @@ exports[`Settings Pane should render Default properly 1`] = `
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</div>
<StyledCheckboxBase
ariaLabel="Enable cross-partition query"
ariaLabel="Enable cross partition query"
checked={false}
className="padding"
onChange={[Function]}
@@ -705,7 +705,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</a>
</div>
<StyledCheckboxBase
ariaLabel="Enable query control"
ariaLabel="EnableQueryControl"
checked={false}
className="padding"
onChange={[Function]}
@@ -1190,8 +1190,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
<div
className="___j7dlp70_0000000 fq02s40 f19n0e5"
>
Display Gremlin query results as:
 
Display Gremlin query results as: 
</div>
</AccordionHeader>
<AccordionPanel>

View File

@@ -11,8 +11,6 @@ import {
import { configContext } from "ConfigContext";
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import React from "react";
import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -115,13 +113,13 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
<CosmosFluentProvider>
<div className="panelFormWrapper">
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
<Text>{t(Keys.panes.tableColumnSelection.selectColumns)}</Text>
<Text>Select which columns to display in your view of items in your container.</Text>
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
<SearchBox
className={styles.searchBox}
value={columnSearchText}
onChange={onSearchChange}
placeholder={t(Keys.panes.tableColumnSelection.searchFields)}
placeholder="Search fields"
/>
</div>
@@ -132,9 +130,7 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
key={columnDefinition.id}
label={{
className: styles.checkboxLabel,
children: `${columnDefinition.label}${
columnDefinition.isPartitionKey ? t(Keys.panes.tableColumnSelection.partitionKeySuffix) : ""
}`,
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
}}
checked={selectedColumnIdsSet.has(columnDefinition.id)}
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
@@ -142,15 +138,15 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
))}
</div>
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
{t(Keys.panes.tableColumnSelection.reset)}
Reset
</Button>
</div>
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
<Button appearance="primary" onClick={onSave}>
{t(Keys.common.save)}
Save
</Button>
<Button appearance="secondary" onClick={closeSidePanel}>
{t(Keys.common.cancel)}
Cancel
</Button>
</div>
</div>

View File

@@ -1,7 +1,5 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
@@ -102,8 +100,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
for (let i = 0; i < entities.length; i++) {
const { property, type, value } = entities[i];
if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) {
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
return;
}
@@ -111,13 +109,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
(property === "PartitionKey" && containsAnyWhiteSpace(value) === true) ||
(property === "RowKey" && containsAnyWhiteSpace(value) === true)
) {
logConsoleError(t(Keys.panes.tables.whitespaceError, { property }));
setFormError(t(Keys.panes.tables.whitespaceError, { property }));
logConsoleError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
setFormError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
return;
}
if (!type) {
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}

View File

@@ -1,7 +1,5 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
@@ -200,7 +198,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
}
if (!type) {
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
@@ -210,8 +208,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
(property === "RowKey" && value === "") ||
(property === "RowKey" && value === undefined)
) {
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
return;
}
}
@@ -405,7 +403,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
)}
</div>
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
{t(Keys.panes.tables.nullFieldsWarning)}
Warning: Null fields will not be displayed for editing.
</div>
</RightPaneForm>
);

View File

@@ -1,6 +1,4 @@
import { Checkbox, Text } from "@fluentui/react";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import React, { FunctionComponent, useEffect, useState } from "react";
import { userContext } from "../../../../UserContext";
import { useSidePanel } from "../../../../hooks/useSidePanel";
@@ -37,7 +35,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
const props: RightPaneFormProps = {
formError: "",
isExecuting: false,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit,
};
@@ -123,11 +121,11 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<Text>{t(Keys.panes.tableQuerySelect.selectColumns)}</Text>
<Text>Select the columns that you want to query.</Text>
<div className="column-select-view">
<Checkbox
id="availableCheckbox"
label={t(Keys.panes.tableQuerySelect.availableColumns)}
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
/>

View File

@@ -13,8 +13,6 @@ import {
} from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { ChangeEvent, FunctionComponent, useReducer, useState } from "react";
import { getErrorMessage } from "../../Tables/Utilities";
@@ -65,8 +63,8 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
const onSubmit = () => {
setFormError("");
if (!files || files.length === 0) {
setFormError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
logConsoleError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
setFormError("No files were specified. Please input at least one file.");
logConsoleError("Could not upload items -- No files were specified. Please input at least one file.");
return;
}
@@ -152,7 +150,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
},
{
key: "fileName",
name: t(Keys.panes.uploadItems.fileNameColumn),
name: "FILE NAME",
fieldName: "fileName",
minWidth: 120,
maxWidth: 140,
@@ -171,7 +169,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
},
{
key: "status",
name: t(Keys.panes.uploadItems.statusColumn),
name: "STATUS",
fieldName: "numSucceeded",
minWidth: 120,
maxWidth: 140,
@@ -180,11 +178,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
data: "string",
isPadded: true,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = t(Keys.panes.uploadItems.uploadStatus, {
numSucceeded: item.numSucceeded,
numThrottled: item.numThrottled,
numFailed: item.numFailed,
});
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
return (
<TooltipHost
content={fieldContent}
@@ -203,12 +197,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
<div className="paneMainContent">
<Upload
key={reducer} // Force re-render on state change
label={t(Keys.panes.uploadItems.selectJsonFiles)}
label="Select JSON Files"
onUpload={updateSelectedFiles}
accept="application/json"
multiple
tabIndex={0}
tooltip={t(Keys.panes.uploadItems.selectJsonFilesTooltip)}
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
/>
{uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer" data-test="file-upload-status">

View File

@@ -714,7 +714,9 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span
className="css-126"
>
What is the reason why you are deleting this Database?
What is the reason why you are deleting this
Database
?
</span>
</Text>
<StyledTextFieldBase

View File

@@ -29,9 +29,6 @@
"upload": "Upload",
"connect": "Connect",
"remove": "Remove",
"load": "Load",
"publish": "Publish",
"browse": "Browse",
"increaseValueBy1": "Increase value by 1",
"decreaseValueBy1": "Decrease value by 1"
},
@@ -415,313 +412,5 @@
"mongoShell": {
"title": "Mongo Shell"
}
},
"panes": {
"deleteDatabase": {
"panelTitle": "Delete {{databaseName}}",
"warningMessage": "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
"confirmPrompt": "Confirm by typing the {{databaseName}} id (name)",
"inputMismatch": "Input {{databaseName}} name \"{{input}}\" does not match the selected {{databaseName}} \"{{selectedId}}\"",
"feedbackTitle": "Help us improve Azure Cosmos DB!",
"feedbackReason": "What is the reason why you are deleting this {{databaseName}}?"
},
"deleteCollection": {
"panelTitle": "Delete {{collectionName}}",
"confirmPrompt": "Confirm by typing the {{collectionName}} id",
"inputMismatch": "Input id {{input}} does not match the selected {{selectedId}}",
"feedbackTitle": "Help us improve Azure Cosmos DB!",
"feedbackReason": "What is the reason why you are deleting this {{collectionName}}?"
},
"addDatabase": {
"databaseLabel": "Database {{suffix}}",
"databaseIdLabel": "Database id",
"keyspaceIdLabel": "Keyspace id",
"databaseIdPlaceholder": "Type a new {{databaseLabel}} id",
"databaseTooltip": "A {{databaseLabel}} is a logical container of one or more {{collectionsLabel}}",
"shareThroughput": "Share throughput across {{collectionsLabel}}",
"shareThroughputTooltip": "Provisioned throughput at the {{databaseLabel}} level will be shared across all {{collectionsLabel}} within the {{databaseLabel}}.",
"greaterThanError": "Please enter a value greater than {{minValue}} for autopilot throughput",
"acknowledgeSpendError": "Please acknowledge the estimated {{period}} spend."
},
"addCollection": {
"createNew": "Create new",
"useExisting": "Use existing",
"databaseTooltip": "A database is analogous to a namespace. It is the unit of management for a set of {{collectionName}}.",
"shareThroughput": "Share throughput across {{collectionName}}",
"shareThroughputTooltip": "Throughput configured at the database level will be shared across all {{collectionName}} within the database.",
"collectionIdLabel": "{{collectionName}} id",
"collectionIdTooltip": "Unique identifier for the {{collectionName}} and used for id-based routing through REST and all SDKs.",
"collectionIdPlaceholder": "e.g., {{collectionName}}1",
"collectionIdAriaLabel": "{{collectionName}} id, Example {{collectionName}}1",
"existingDatabaseAriaLabel": "Choose existing {{databaseName}} id",
"existingDatabasePlaceholder": "Choose existing {{databaseName}} id",
"indexing": "Indexing",
"turnOnIndexing": "Turn on indexing",
"automatic": "Automatic",
"turnOffIndexing": "Turn off indexing",
"off": "Off",
"sharding": "Sharding",
"shardingTooltip": "Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data.",
"unsharded": "Unsharded",
"unshardedLabel": "Unsharded (20GB limit)",
"sharded": "Sharded",
"addPartitionKey": "Add hierarchical partition key",
"hierarchicalPartitionKeyInfo": "This feature allows you to partition your data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview JavaScript V3 SDK.",
"provisionDedicatedThroughput": "Provision dedicated throughput for this {{collectionName}}",
"provisionDedicatedThroughputTooltip": "You can optionally provision dedicated throughput for a {{collectionName}} within a database that has throughput provisioned. This dedicated throughput amount will not be shared with other {{collectionNamePlural}} in the database and does not count towards the throughput you provisioned for the database. This throughput amount will be billed in addition to the throughput amount you provisioned at the database level.",
"uniqueKeysPlaceholderMongo": "Comma separated paths e.g. firstName,address.zipCode",
"uniqueKeysPlaceholderSql": "Comma separated paths e.g. /firstName,/address/zipCode",
"addUniqueKey": "Add unique key",
"enableAnalyticalStore": "Enable analytical store",
"disableAnalyticalStore": "Disable analytical store",
"on": "On",
"analyticalStoreSynapseLinkRequired": "Azure Synapse Link is required for creating an analytical store {{collectionName}}. Enable Synapse Link for this Cosmos DB account.",
"enable": "Enable",
"containerVectorPolicy": "Container Vector Policy",
"containerFullTextSearchPolicy": "Container Full Text Search Policy",
"advanced": "Advanced",
"mongoIndexingTooltip": "The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development.",
"createWildcardIndex": "Create a Wildcard Index on all fields",
"legacySdkCheckbox": "My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)",
"legacySdkInfo": "To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.",
"indexingOnInfo": "All properties in your documents will be indexed by default for flexible and efficient queries.",
"indexingOffInfo": "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.",
"indexingOffWarning": "By creating this container with indexing turned off, you will not be able to make any indexing policy changes. Indexing changes are only allowed on a container with a indexing policy.",
"acknowledgeSpendErrorMonthly": "Please acknowledge the estimated monthly spend.",
"acknowledgeSpendErrorDaily": "Please acknowledge the estimated daily spend.",
"unshardedMaxRuError": "Unsharded collections support up to 10,000 RUs",
"acknowledgeShareThroughputError": "Please acknowledge the estimated cost of this dedicated throughput.",
"vectorPolicyError": "Please fix errors in container vector policy",
"fullTextSearchPolicyError": "Please fix errors in container full text search policy",
"addingSampleDataSet": "Adding sample data set"
},
"addCollectionUtility": {
"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": "The {{partitionKeyName}} 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.",
"partitionKeyTooltipSqlSuffix": " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.",
"shardKeyLabel": "Shard key",
"partitionKeyLabel": "Partition key",
"shardKeyPlaceholder": "e.g., categoryId",
"partitionKeyPlaceholderDefault": "e.g., /address",
"partitionKeyPlaceholderFirst": "Required - first partition key e.g., /TenantId",
"partitionKeyPlaceholderSecond": "second partition key e.g., /UserId",
"partitionKeyPlaceholderThird": "third partition key e.g., /SessionId",
"partitionKeyPlaceholderGraph": "e.g., /address/zipCode",
"uniqueKeysTooltip": "Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.",
"uniqueKeysLabel": "Unique keys",
"analyticalStoreLabel": "Analytical Store",
"analyticalStoreTooltip": "Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.",
"analyticalStoreDescription": "Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.",
"vectorPolicyTooltip": "Describe any properties in your data that contain vectors, so that they can be made available for similarity queries."
},
"settings": {
"pageOptions": "Page Options",
"pageOptionsDescription": "Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.",
"queryResultsPerPage": "Query results per page",
"queryResultsPerPageTooltip": "Enter the number of query results that should be shown per page.",
"customQueryItemsPerPage": "Custom query items per page",
"custom": "Custom",
"unlimited": "Unlimited",
"entraIdRbac": "Enable Entra ID RBAC",
"entraIdRbacDescription": "Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID RBAC.",
"true": "True",
"false": "False",
"regionSelection": "Region Selection",
"regionSelectionDescription": "Changes region the Cosmos Client uses to access account.",
"selectRegion": "Select Region",
"selectRegionTooltip": "Changes the account endpoint used to perform client operations.",
"globalDefault": "Global (Default)",
"readWrite": "(Read/Write)",
"read": "(Read)",
"queryTimeout": "Query Timeout",
"queryTimeoutDescription": "When a query reaches a specified time limit, a popup with an option to cancel the query will show unless automatic cancellation has been enabled.",
"enableQueryTimeout": "Enable query timeout",
"queryTimeoutMs": "Query timeout (ms)",
"automaticallyCancelQuery": "Automatically cancel query after timeout",
"ruLimit": "RU Limit",
"ruLimitDescription": "If a query exceeds a configured RU limit, the query will be aborted.",
"enableRuLimit": "Enable RU limit",
"ruLimitLabel": "RU Limit (RU)",
"defaultQueryResults": "Default Query Results View",
"defaultQueryResultsDescription": "Select the default view to use when displaying query results.",
"retrySettings": "Retry Settings",
"retrySettingsDescription": "Retry policy associated with throttled requests during CosmosDB queries.",
"maxRetryAttempts": "Max retry attempts",
"maxRetryAttemptsTooltip": "Max number of retries to be performed for a request. Default value 9.",
"fixedRetryInterval": "Fixed retry interval (ms)",
"fixedRetryIntervalTooltip": "Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. Default value is 0 milliseconds.",
"maxWaitTime": "Max wait time (s)",
"maxWaitTimeTooltip": "Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.",
"enableContainerPagination": "Enable container pagination",
"enableContainerPaginationDescription": "Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.",
"enableCrossPartitionQuery": "Enable cross-partition query",
"enableCrossPartitionQueryDescription": "Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.",
"maxDegreeOfParallelism": "Max degree of parallelism",
"maxDegreeOfParallelismDescription": "Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.",
"maxDegreeOfParallelismQuery": "Query up to the max degree of parallelism.",
"priorityLevel": "Priority Level",
"priorityLevelDescription": "Sets the priority level for data-plane requests from Data Explorer when using Priority-Based Execution. If \"None\" is selected, Data Explorer will not specify priority level, and the server-side default priority level will be used.",
"displayGremlinQueryResults": "Display Gremlin query results as:",
"displayGremlinQueryResultsDescription": "Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.",
"graph": "Graph",
"json": "JSON",
"graphAutoVisualization": "Graph Auto-visualization",
"enableSampleDatabase": "Enable sample database",
"enableSampleDatabaseDescription": "This is a sample database and collection with synthetic product data you can use to explore using NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by, and maintained by Microsoft at no cost to you.",
"enableSampleDbAriaLabel": "Enable sample db for query exploration",
"guidRepresentation": "Guid Representation",
"guidRepresentationDescription": "GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and deserialized when stored in BSON documents. This will apply to all document operations.",
"advancedSettings": "Advanced Settings",
"ignorePartitionKey": "Ignore partition key on document update",
"ignorePartitionKeyTooltip": "If checked, the partition key value will not be used to locate the document during update operations. Only use this if document updates are failing due to an abnormal partition key.",
"clearHistory": "Clear History",
"clearHistoryConfirm": "Are you sure you want to proceed?",
"clearHistoryDescription": "This action will clear the all customizations for this account in this browser, including:",
"clearHistoryTabLayout": "Reset your customized tab layout, including the splitter positions",
"clearHistoryTableColumns": "Erase your table column preferences, including any custom columns",
"clearHistoryFilters": "Clear your filter history",
"clearHistoryRegion": "Reset region selection to global",
"increaseValueBy1000": "Increase value by 1000",
"decreaseValueBy1000": "Decrease value by 1000",
"none": "None",
"low": "Low",
"high": "High",
"automatic": "Automatic",
"enhancedQueryControl": "Enhanced query control",
"enableQueryControl": "Enable query control",
"explorerVersion": "Explorer Version",
"accountId": "Account ID",
"sessionId": "Session ID",
"popupsDisabledError": "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on \"Login for Entra ID\" button",
"failedToAcquireTokenError": "Failed to acquire authorization token automatically. Please click on \"Login for Entra ID\" button to enable Entra ID RBAC operations"
},
"saveQuery": {
"panelTitle": "Save Query",
"setupCostMessage": "For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called \u201c{{databaseName}}\u201d. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.",
"completeSetup": "Complete setup",
"noQueryNameError": "No query name specified",
"invalidQueryContentError": "Invalid query content specified",
"failedToSaveQueryError": "Failed to save query {{queryName}}",
"failedToSetupContainerError": "Failed to setup a container for saved queries",
"accountNotSetupError": "Failed to save query: account not setup to save queries",
"name": "Name"
},
"loadQuery": {
"noFileSpecifiedError": "No file specified",
"failedToLoadQueryError": "Failed to load query",
"failedToLoadQueryFromFileError": "Failed to load query from file {{fileName}}",
"selectFilesToOpen": "Select a query document",
"browseFiles": "Browse"
},
"executeStoredProcedure": {
"enterInputParameters": "Enter input parameters (if any)",
"key": "Key",
"param": "Param",
"partitionKeyValue": "Partition key value",
"value": "Value",
"addNewParam": "Add New Param",
"addParam": "Add param",
"deleteParam": "Delete param",
"invalidParamError": "Invalid param specified: {{invalidParam}}",
"invalidParamConsoleError": "Invalid param specified: {{invalidParam}} is not a valid literal value",
"stringType": "String",
"customType": "Custom"
},
"uploadItems": {
"noFilesSpecifiedError": "No files were specified. Please input at least one file.",
"selectJsonFiles": "Select JSON Files",
"selectJsonFilesTooltip": "Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets.",
"fileNameColumn": "FILE NAME",
"statusColumn": "STATUS",
"uploadStatus": "{{numSucceeded}} created, {{numThrottled}} throttled, {{numFailed}} errors",
"uploadedFiles": "Uploaded files"
},
"copyNotebook": {
"copyFailedError": "Failed to copy {{name}} to {{destination}}",
"uploadFailedError": "Failed to upload {{name}}",
"location": "Location",
"locationAriaLabel": "Location",
"selectLocation": "Select a notebook location to copy",
"name": "Name"
},
"publishNotebook": {
"publishFailedError": "Failed to publish {{notebookName}} to gallery",
"publishDescription": "When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.",
"publishPrompt": "Would you like to publish and share \"{{name}}\" to the gallery?",
"coverImage": "Cover image",
"coverImageUrl": "Cover image url",
"name": "Name",
"description": "Description",
"tags": "Tags",
"tagsPlaceholder": "Optional tag 1, Optional tag 2",
"preview": "Preview",
"urlType": "URL",
"customImage": "Custom Image",
"takeScreenshot": "Take Screenshot",
"useFirstDisplayOutput": "Use First Display Output",
"failedToCaptureOutput": "Failed to capture first output",
"outputDoesNotExist": "Output does not exist for any of the cells.",
"failedToConvertError": "Failed to convert {{fileName}} to base64 format",
"failedToUploadError": "Failed to upload {{fileName}}"
},
"changePartitionKey": {
"failedToStartError": "Failed to start data transfer job",
"suboptimalPartitionKeyError": "Warning: The system has detected that your collection may be using a suboptimal partition key",
"description": "When changing a container\u2019s partition key, you will need to create a destination container with the correct partition key. You may also select an existing destination container.",
"sourceContainerId": "Source {{collectionName}} id",
"destinationContainerId": "Destination {{collectionName}} id",
"collectionIdTooltip": "Unique identifier for the {{collectionName}} and used for id-based routing through REST and all SDKs.",
"collectionIdPlaceholder": "e.g., {{collectionName}}1",
"collectionIdAriaLabel": "{{collectionName}} id, Example {{collectionName}}1",
"existingContainers": "Existing Containers",
"partitionKeyWarning": "The destination container must not already exist. Data Explorer will create a new destination container for you."
},
"cassandraAddCollection": {
"keyspaceLabel": "Keyspace name",
"keyspaceTooltip": "Select an existing keyspace or enter a new keyspace id.",
"tableIdLabel": "Enter CQL command to create the table.",
"enterTableId": "Enter table Id",
"tableSchemaAriaLabel": "Table schema",
"provisionDedicatedThroughput": "Provision dedicated throughput for this table",
"provisionDedicatedThroughputTooltip": "You can optionally provision dedicated throughput for a table within a keyspace that has throughput provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and does not count towards the throughput you provisioned for the keyspace. This throughput amount will be billed in addition to the throughput amount you provisioned at the keyspace level."
},
"tables": {
"addProperty": "Add Property",
"addRow": "Add Row",
"addEntity": "Add Entity",
"back": "back",
"nullFieldsWarning": "Warning: Null fields will not be displayed for editing.",
"propertyEmptyError": "{{property}} cannot be empty. Please input a value for {{property}}",
"whitespaceError": "{{property}} cannot have whitespace. Please input a value for {{property}} without whitespace",
"propertyTypeEmptyError": "Property type cannot be empty. Please select a type from the dropdown for property {{property}}"
},
"tableQuerySelect": {
"selectColumns": "Select the columns that you want to query.",
"availableColumns": "Available Columns"
},
"tableColumnSelection": {
"selectColumns": "Select which columns to display in your view of items in your container.",
"searchFields": "Search fields",
"reset": "Reset",
"partitionKeySuffix": " (partition key)"
},
"newVertex": {
"addProperty": "Add Property"
},
"addGlobalSecondaryIndex": {
"globalSecondaryIndexId": "Global secondary index container id",
"globalSecondaryIndexIdPlaceholder": "e.g., indexbyEmailId",
"projectionQuery": "Projection query",
"projectionQueryPlaceholder": "SELECT c.email, c.accountId FROM c",
"projectionQueryTooltip": "Learn more about defining global secondary indexes.",
"disabledTitle": "A global secondary index is already being created. Please wait for it to complete before creating another one."
},
"stringInput": {
"inputMismatchError": "Input {{input}} does not match the selected {{selectedId}}"
},
"panelInfo": {
"information": "Information",
"moreDetails": "More details"
}
}
}

View File

@@ -58,6 +58,7 @@ export async function getMsalInstance() {
auth: {
authority: `${configContext.AAD_ENDPOINT}organizations`,
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
knownAuthorities: [configContext.AAD_ENDPOINT],
},
};