mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-19 12:59:12 +01:00
Added localizations 3rd batch (#2413)
* Added localizations 3rd batch * Fix unit tests
This commit is contained in:
@@ -42,6 +42,8 @@ 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";
|
||||
@@ -177,7 +179,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
linkText="Learn more"
|
||||
linkText={t(Keys.common.learnMore)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -274,17 +276,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
content={t(Keys.panes.addCollection.databaseTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -304,7 +306,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
@@ -317,7 +319,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -347,7 +349,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
label={t(Keys.panes.addCollection.shareThroughput, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -365,17 +369,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
content={t(Keys.panes.addCollection.shareThroughputTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -424,14 +428,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
content={t(Keys.panes.addCollection.collectionIdTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.collectionIdTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -445,10 +453,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||||
aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
|
||||
value={this.state.collectionId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ collectionId: event.target.value })
|
||||
@@ -462,7 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
{t(Keys.panes.addCollection.indexing)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -470,32 +478,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableIndexing}
|
||||
aria-label="Turn on indexing"
|
||||
aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
|
||||
aria-checked={this.state.enableIndexing}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onTurnOnIndexing.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Automatic</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableIndexing}
|
||||
aria-label="Turn off indexing"
|
||||
aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
|
||||
aria-checked={!this.state.enableIndexing}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onTurnOffIndexing.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
|
||||
</Stack>
|
||||
|
||||
<Text variant="small">
|
||||
{this.getFreeTierIndexingText()}{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -508,21 +516,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Sharding
|
||||
{t(Keys.panes.addCollection.sharding)}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
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."
|
||||
}
|
||||
content={t(Keys.panes.addCollection.shardingTooltip)}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
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."
|
||||
}
|
||||
ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -531,7 +535,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.isSharded}
|
||||
aria-label="Unsharded"
|
||||
aria-label={t(Keys.panes.addCollection.unsharded)}
|
||||
aria-checked={!this.state.isSharded}
|
||||
name="unsharded"
|
||||
type="radio"
|
||||
@@ -540,12 +544,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onUnshardedRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.isSharded}
|
||||
aria-label="Sharded"
|
||||
aria-label={t(Keys.panes.addCollection.sharded)}
|
||||
aria-checked={this.state.isSharded}
|
||||
name="sharded"
|
||||
type="radio"
|
||||
@@ -554,7 +558,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onShardedRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Sharded</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -679,15 +683,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
{t(Keys.panes.addCollection.addPartitionKey)}
|
||||
</DefaultButton>
|
||||
{this.state.subPartitionKeys.length > 0 && (
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
<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.{" "}
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
|
||||
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
@@ -700,7 +703,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
checked={this.state.enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -718,23 +723,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
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.`}
|
||||
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
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.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -769,8 +770,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
autoComplete="off"
|
||||
placeholder={
|
||||
userContext.apiType === "Mongo"
|
||||
? "Comma separated paths e.g. firstName,address.zipCode"
|
||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||
? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
|
||||
: t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
|
||||
}
|
||||
className="panelTextField"
|
||||
value={uniqueKey}
|
||||
@@ -802,7 +803,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, ""] })}
|
||||
>
|
||||
Add unique key
|
||||
{t(Keys.panes.addCollection.addUniqueKey)}
|
||||
</ActionButton>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -823,7 +824,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
@@ -832,13 +833,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">On</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
@@ -847,26 +848,28 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Azure Synapse Link is required for creating an analytical store{" "}
|
||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}{" "}
|
||||
<br />
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
<DefaultButton
|
||||
text="Enable"
|
||||
text={t(Keys.panes.addCollection.enable)}
|
||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
||||
style={{ height: 27, width: 80 }}
|
||||
styles={{ label: { fontSize: 12 } }}
|
||||
@@ -878,7 +881,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{this.shouldShowVectorSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Vector Policy"
|
||||
title={t(Keys.panes.addCollection.containerVectorPolicy)}
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
@@ -906,7 +909,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{this.shouldShowFullTextSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Full Text Search Policy"
|
||||
title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
@@ -935,7 +938,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
title={t(Keys.panes.addCollection.advanced)}
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
@@ -948,23 +951,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
{t(Keys.panes.addCollection.indexing)}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||
content={t(Keys.panes.addCollection.mongoIndexingTooltip)}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||
ariaLabel={t(Keys.panes.addCollection.mongoIndexingTooltip)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Checkbox
|
||||
label="Create a Wildcard Index on all fields"
|
||||
label={t(Keys.panes.addCollection.createWildcardIndex)}
|
||||
checked={this.state.createMongoWildCardIndex}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -986,7 +989,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Checkbox
|
||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||
label={t(Keys.panes.addCollection.legacySdkCheckbox)}
|
||||
checked={this.state.useHashV1}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -1003,11 +1006,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
/>
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
<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.{" "}
|
||||
<Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
|
||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -1018,7 +1019,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</div>
|
||||
|
||||
{!this.props.isCopyJobFlow && (
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||
)}
|
||||
|
||||
{this.state.isExecuting && (
|
||||
@@ -1044,7 +1045,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
progressTrack: { backgroundColor: "#A6A6A6" },
|
||||
progressBar: { background: "white" },
|
||||
}}
|
||||
label="Adding sample data set"
|
||||
label={t(Keys.panes.addCollection.addingSampleDataSet)}
|
||||
/>
|
||||
</TeachingBubble>
|
||||
)}
|
||||
@@ -1150,8 +1151,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
private getFreeTierIndexingText(): string {
|
||||
return this.state.enableIndexing
|
||||
? "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.";
|
||||
? t(Keys.panes.addCollection.indexingOnInfo)
|
||||
: t(Keys.panes.addCollection.indexingOffInfo);
|
||||
}
|
||||
|
||||
private getPartitionKeySubtext(): string {
|
||||
@@ -1249,14 +1250,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
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily spend.";
|
||||
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
|
||||
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
|
||||
this.setState({ errorMessage });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
|
||||
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
|
||||
this.setState({ errorMessage: t(Keys.panes.addCollection.unshardedMaxRuError) });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1270,12 +1271,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
if (this.shouldShowVectorSearchParameters()) {
|
||||
if (!this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.state.fullTextPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
|
||||
this.setState({ errorMessage: t(Keys.panes.addCollection.fullTextSearchPolicyError) });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,33 @@ 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 "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.";
|
||||
return t(Keys.panes.addCollectionUtility.shardKeyTooltip);
|
||||
}
|
||||
|
||||
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.`;
|
||||
let tooltipText = t(Keys.panes.addCollectionUtility.partitionKeyTooltip, {
|
||||
partitionKeyName: getPartitionKeyName(true),
|
||||
});
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||
tooltipText += t(Keys.panes.addCollectionUtility.partitionKeyTooltipSqlSuffix);
|
||||
}
|
||||
|
||||
return tooltipText;
|
||||
}
|
||||
|
||||
export function getPartitionKeyName(isLowerCase?: boolean): string {
|
||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
const partitionKeyName =
|
||||
userContext.apiType === "Mongo"
|
||||
? t(Keys.panes.addCollectionUtility.shardKeyLabel)
|
||||
: t(Keys.panes.addCollectionUtility.partitionKeyLabel);
|
||||
|
||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||
}
|
||||
@@ -32,19 +37,19 @@ export function getPartitionKeyName(isLowerCase?: boolean): string {
|
||||
export function getPartitionKeyPlaceHolder(index?: number): string {
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
return "e.g., categoryId";
|
||||
return t(Keys.panes.addCollectionUtility.shardKeyPlaceholder);
|
||||
case "Gremlin":
|
||||
return "e.g., /address";
|
||||
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderDefault);
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderFirst)
|
||||
: index === 0
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderSecond)
|
||||
: t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderThird)
|
||||
}`;
|
||||
default:
|
||||
return "e.g., /address/zipCode";
|
||||
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderGraph);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,13 +74,12 @@ export function isFreeTierAccount(): boolean {
|
||||
}
|
||||
|
||||
export function UniqueKeysHeader(): JSX.Element {
|
||||
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.";
|
||||
const tooltipContent = t(Keys.panes.addCollectionUtility.uniqueKeysTooltip);
|
||||
|
||||
return (
|
||||
<Stack horizontal style={{ marginBottom: -2 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Unique keys
|
||||
{t(Keys.panes.addCollectionUtility.uniqueKeysLabel)}
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||
@@ -99,12 +103,11 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
|
||||
}
|
||||
|
||||
export function AnalyticalStoreHeader(): JSX.Element {
|
||||
const tooltipContent =
|
||||
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
|
||||
const tooltipContent = t(Keys.panes.addCollectionUtility.analyticalStoreTooltip);
|
||||
return (
|
||||
<Stack horizontal style={{ marginBottom: -2 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical Store
|
||||
{t(Keys.panes.addCollectionUtility.analyticalStoreLabel)}
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||
@@ -116,14 +119,13 @@ export function AnalyticalStoreHeader(): JSX.Element {
|
||||
export function AnalyticalStorageContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
|
||||
the performance of transactional workloads.{" "}
|
||||
{t(Keys.panes.addCollectionUtility.analyticalStoreDescription)}{" "}
|
||||
<Link
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||
target="_blank"
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -155,10 +157,9 @@ export function scrollToSection(id: string): void {
|
||||
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||
queries.{" "}
|
||||
{t(Keys.panes.addCollectionUtility.vectorPolicyTooltip)}{" "}
|
||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -482,10 +482,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
Azure Synapse Link is required for creating an analytical store
|
||||
Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link for this Cosmos DB account.
|
||||
|
||||
container
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
<br />
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about Azure Synapse Link."
|
||||
@@ -608,7 +606,8 @@ 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"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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";
|
||||
@@ -40,15 +42,18 @@ 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 ? "Keyspace id" : "Database id";
|
||||
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
|
||||
const databaseIdLabel: string = isCassandraAccount
|
||||
? t(Keys.panes.addDatabase.keyspaceIdLabel)
|
||||
: t(Keys.panes.addDatabase.databaseIdLabel);
|
||||
const databaseIdPlaceHolder: string = t(Keys.panes.addDatabase.databaseIdPlaceholder, { databaseLabel });
|
||||
|
||||
const [databaseId, setDatabaseId] = useState<string>("");
|
||||
const databaseIdTooltipText = `A ${
|
||||
isCassandraAccount ? "keyspace" : "database"
|
||||
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
|
||||
const databaseIdTooltipText = t(Keys.panes.addDatabase.databaseTooltip, { databaseLabel, collectionsLabel });
|
||||
|
||||
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
|
||||
const databaseLevelThroughputTooltipText = t(Keys.panes.addDatabase.shareThroughputTooltip, {
|
||||
databaseLabel,
|
||||
collectionsLabel,
|
||||
});
|
||||
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
|
||||
getNewDatabaseSharedThroughputDefault(),
|
||||
);
|
||||
@@ -144,15 +149,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(
|
||||
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`,
|
||||
);
|
||||
setFormErrors(t(Keys.panes.addDatabase.greaterThanError, { minValue: AutoPilotUtils.autoPilotThroughput1K }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
|
||||
setFormErrors(
|
||||
t(Keys.panes.addDatabase.acknowledgeSpendError, { period: isAutoscaleSelected ? "monthly" : "daily" }),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -169,7 +174,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formErrors,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||
onSubmit,
|
||||
};
|
||||
@@ -187,7 +192,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
linkText="Learn more"
|
||||
linkText={t(Keys.common.learnMore)}
|
||||
/>
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
|
||||
@@ -40,6 +40,8 @@ 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";
|
||||
@@ -168,19 +170,19 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
}
|
||||
|
||||
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
const errorMessage: string = "Please acknowledge the estimated monthly spend.";
|
||||
const errorMessage: string = t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly);
|
||||
setErrorMessage(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (showVectorSearchParameters()) {
|
||||
if (!vectorPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container vector policy");
|
||||
setErrorMessage(t(Keys.panes.addCollection.vectorPolicyError));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fullTextPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container full text search policy");
|
||||
setErrorMessage(t(Keys.panes.addCollection.fullTextSearchPolicyError));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -307,7 +309,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Global secondary index container id
|
||||
{t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexId)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<input
|
||||
@@ -318,7 +320,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., indexbyEmailId`}
|
||||
placeholder={t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexIdPlaceholder)}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={globalSecondaryIndexId}
|
||||
@@ -336,7 +338,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||
target="blank"
|
||||
>
|
||||
Learn more about defining global secondary indexes.
|
||||
{t(Keys.panes.addGlobalSecondaryIndex.projectionQueryTooltip)}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
@@ -349,7 +351,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder={"SELECT c.email, c.accountId FROM c"}
|
||||
placeholder={t(Keys.panes.addGlobalSecondaryIndex.projectionQueryPlaceholder)}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={definition || ""}
|
||||
@@ -393,7 +395,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
||||
</Stack>
|
||||
</div>
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
||||
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={isThroughputCapExceeded} />
|
||||
{isExecuting && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,16 @@ 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";
|
||||
@@ -71,8 +73,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
const errorMessage =
|
||||
isNewKeySpaceAutoscale || isTableAutoscale
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily spend.";
|
||||
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
|
||||
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
|
||||
setFormError(errorMessage);
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +151,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||
onSubmit,
|
||||
};
|
||||
@@ -161,7 +163,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
|
||||
{t(Keys.panes.cassandraAddCollection.keyspaceLabel)}{" "}
|
||||
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.keyspaceTooltip)}</InfoTooltip>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -179,7 +182,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
setExistingKeyspaceId("");
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
@@ -193,7 +196,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
setIsKeyspaceShared(false);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
|
||||
</Stack>
|
||||
|
||||
{keyspaceCreateNew && (
|
||||
@@ -275,9 +278,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Enter CQL command to create the table.{" "}
|
||||
{t(Keys.panes.cassandraAddCollection.tableIdLabel)}{" "}
|
||||
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
|
||||
Learn More
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -295,7 +298,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter table Id"
|
||||
placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
|
||||
size={20}
|
||||
value={tableId}
|
||||
onChange={(e, newValue) => setTableId(newValue)}
|
||||
@@ -307,7 +310,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
multiline
|
||||
id="editor-area"
|
||||
rows={5}
|
||||
ariaLabel="Table schema"
|
||||
ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
|
||||
value={userTableQuery}
|
||||
onChange={(e, newValue) => setUserTableQuery(newValue)}
|
||||
/>
|
||||
@@ -318,17 +321,12 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<input
|
||||
type="checkbox"
|
||||
id="tableSharedThroughput"
|
||||
title="Provision dedicated throughput for this table"
|
||||
title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
|
||||
checked={dedicateTableThroughput}
|
||||
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
|
||||
/>
|
||||
<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>
|
||||
<span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
|
||||
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
|
||||
</Stack>
|
||||
)}
|
||||
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
|
||||
|
||||
@@ -26,6 +26,8 @@ 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";
|
||||
@@ -72,7 +74,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
await createDataTransferJob();
|
||||
await onClose();
|
||||
} catch (error) {
|
||||
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
|
||||
handleError(error, "ChangePartitionKey", t(Keys.panes.changePartitionKey.failedToStartError));
|
||||
}
|
||||
setIsExecuting(false);
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
@@ -133,17 +135,21 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK">
|
||||
<RightPaneForm
|
||||
formError={formError}
|
||||
isExecuting={isExecuting}
|
||||
onSubmit={submit}
|
||||
submitButtonText={t(Keys.common.ok)}
|
||||
>
|
||||
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
When changing a container’s partition key, you will need to create a destination container with the correct
|
||||
partition key. You may also select an existing destination container.
|
||||
{t(Keys.panes.changePartitionKey.description)}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
|
||||
target="_blank"
|
||||
underline
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
<Stack>
|
||||
@@ -218,14 +224,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
content={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
ariaLabel={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -239,10 +249,14 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
placeholder={t(Keys.panes.changePartitionKey.collectionIdPlaceholder, {
|
||||
collectionName: getCollectionName(),
|
||||
})}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||||
aria-label={t(Keys.panes.changePartitionKey.collectionIdAriaLabel, {
|
||||
collectionName: getCollectionName(),
|
||||
})}
|
||||
value={targetCollectionId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
|
||||
/>
|
||||
@@ -349,7 +363,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
{t(Keys.panes.addCollection.addPartitionKey)}
|
||||
</DefaultButton>
|
||||
{subPartitionKeys.length > 0 && (
|
||||
<Text
|
||||
@@ -357,11 +371,10 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
variant="small"
|
||||
style={{ color: "var(--colorNeutralForeground1)" }}
|
||||
>
|
||||
<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.{" "}
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
|
||||
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
@@ -377,14 +390,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
content={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
ariaLabel={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -400,7 +417,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
}}
|
||||
defaultSelectedKey={targetCollectionId}
|
||||
responsiveMode={999}
|
||||
ariaLabel="Existing Containers"
|
||||
ariaLabel={t(Keys.panes.changePartitionKey.existingContainers)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,8 @@ 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";
|
||||
@@ -82,14 +84,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
|
||||
const notebookContentItem = await copyNotebook(selectedLocation);
|
||||
if (!notebookContentItem) {
|
||||
throw new Error(`Failed to upload ${name}`);
|
||||
throw new Error(t(Keys.panes.copyNotebook.uploadFailedError, { name }));
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
|
||||
closeSidePanel();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(`Failed to copy ${name} to ${destination}`);
|
||||
setFormError(t(Keys.panes.copyNotebook.copyFailedError, { name, destination }));
|
||||
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
@@ -136,7 +138,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting: isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ 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";
|
||||
|
||||
@@ -96,8 +98,8 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
return options;
|
||||
};
|
||||
const dropDownProps: IDropdownProps = {
|
||||
label: "Location",
|
||||
ariaLabel: "Location",
|
||||
label: t(Keys.panes.copyNotebook.location),
|
||||
ariaLabel: t(Keys.panes.copyNotebook.locationAriaLabel),
|
||||
placeholder: "Select an option",
|
||||
onRenderTitle: onRenderDropDownTitle,
|
||||
onRenderOption: onRenderDropDownOption,
|
||||
@@ -109,7 +111,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
<div className="paneMainContent">
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
<Label htmlFor="notebookName">Name</Label>
|
||||
<Label htmlFor="notebookName">{t(Keys.panes.copyNotebook.name)}</Label>
|
||||
<Text id="notebookName">{name}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ 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";
|
||||
@@ -34,12 +36,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
|
||||
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (!collection || inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input id " + inputCollectionName + " does not match the selected " + collection.id();
|
||||
const errorMessage = t(Keys.panes.deleteCollection.inputMismatch, {
|
||||
input: inputCollectionName,
|
||||
selectedId: collection.id(),
|
||||
});
|
||||
setFormError(errorMessage);
|
||||
NotificationConsoleUtils.logConsoleError(
|
||||
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
|
||||
@@ -106,18 +111,23 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit,
|
||||
};
|
||||
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}?`;
|
||||
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
|
||||
collectionName: collectionName.toLowerCase(),
|
||||
});
|
||||
const reasonInfo =
|
||||
t(Keys.panes.deleteCollection.feedbackTitle) +
|
||||
" " +
|
||||
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
|
||||
<Text variant="small">{confirmContainer}</Text>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
@@ -133,10 +143,10 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
{shouldRecordFeedback() && (
|
||||
<div className="deleteCollectionFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
{t(Keys.panes.deleteCollection.feedbackTitle)}
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this {collectionName}?
|
||||
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteCollectionFeedbackInput"
|
||||
|
||||
@@ -34,9 +34,7 @@ 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
|
||||
|
||||
@@ -5,6 +5,8 @@ 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";
|
||||
@@ -38,11 +40,19 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
setFormError(
|
||||
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
|
||||
t(Keys.panes.deleteDatabase.inputMismatch, {
|
||||
databaseName: getDatabaseName(),
|
||||
input: databaseInput,
|
||||
selectedId: selectedDatabase.id(),
|
||||
}),
|
||||
);
|
||||
logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
|
||||
logConsoleError(
|
||||
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
|
||||
t(Keys.panes.deleteDatabase.inputMismatch, {
|
||||
databaseName: getDatabaseName(),
|
||||
input: databaseInput,
|
||||
selectedId: selectedDatabase.id(),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -114,18 +124,20 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
const errorProps: PanelInfoErrorProps = {
|
||||
messageType: "warning",
|
||||
showErrorDetails: false,
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
message: t(Keys.panes.deleteDatabase.warningMessage),
|
||||
};
|
||||
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()}?`;
|
||||
const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
|
||||
const reasonInfo =
|
||||
t(Keys.panes.deleteDatabase.feedbackTitle) +
|
||||
" " +
|
||||
t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() });
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||
@@ -148,10 +160,10 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
{isLastNonEmptyDatabase() && (
|
||||
<div className="deleteDatabaseFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
{t(Keys.panes.deleteDatabase.feedbackTitle)}
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this {getDatabaseName()}?
|
||||
{t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })}
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
|
||||
@@ -3,6 +3,8 @@ 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";
|
||||
@@ -45,8 +47,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
};
|
||||
|
||||
const setInvalidParamError = (invalidParam: string): void => {
|
||||
setFormError(`Invalid param specified: ${invalidParam}`);
|
||||
logConsoleError(`Invalid param specified: ${invalidParam} is not a valid literal value`);
|
||||
setFormError(t(Keys.panes.executeStoredProcedure.invalidParamError, { invalidParam }));
|
||||
logConsoleError(t(Keys.panes.executeStoredProcedure.invalidParamConsoleError, { invalidParam }));
|
||||
};
|
||||
|
||||
const submit = (): void => {
|
||||
@@ -96,7 +98,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "Execute",
|
||||
submitButtonText: t(Keys.common.execute),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
@@ -107,9 +109,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
inputParameters.push(
|
||||
<InputParameter
|
||||
key={paramKeyValue.text + i}
|
||||
dropdownLabel={i === 0 ? "Key" : ""}
|
||||
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
|
||||
inputLabel={i === 0 ? "Param" : ""}
|
||||
dropdownLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.key) : ""}
|
||||
inputParameterTitle={i === 0 ? t(Keys.panes.executeStoredProcedure.enterInputParameters) : ""}
|
||||
inputLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.param) : ""}
|
||||
isAddRemoveVisible={true}
|
||||
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
|
||||
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
|
||||
@@ -130,9 +132,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelMainContent">
|
||||
<InputParameter
|
||||
dropdownLabel="Key"
|
||||
inputParameterTitle="Partition key value"
|
||||
inputLabel="Value"
|
||||
dropdownLabel={t(Keys.panes.executeStoredProcedure.key)}
|
||||
inputParameterTitle={t(Keys.panes.executeStoredProcedure.partitionKeyValue)}
|
||||
inputLabel={t(Keys.panes.executeStoredProcedure.value)}
|
||||
isAddRemoveVisible={false}
|
||||
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
|
||||
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
|
||||
@@ -143,8 +145,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
/>
|
||||
{getInputParameterComponent()}
|
||||
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt={t(Keys.panes.executeStoredProcedure.addParam)} />
|
||||
<Text className="addNewParamStyle">{t(Keys.panes.executeStoredProcedure.addNewParam)}</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
|
||||
@@ -11,6 +11,8 @@ 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 = [
|
||||
@@ -74,7 +76,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EntityCancelIcon}
|
||||
alt="Delete param"
|
||||
alt={t(Keys.panes.executeStoredProcedure.deleteParam)}
|
||||
id="deleteparam"
|
||||
role="button"
|
||||
onClick={onDeleteParamKeyPress}
|
||||
@@ -84,7 +86,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={AddPropertyIcon}
|
||||
alt="Add param"
|
||||
alt={t(Keys.panes.executeStoredProcedure.addParam)}
|
||||
id="addparam"
|
||||
role="button"
|
||||
onClick={onAddNewParamKeyPress}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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";
|
||||
@@ -17,7 +19,7 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
|
||||
}: GraphStylingProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
|
||||
const buttonLabel = "Ok";
|
||||
const buttonLabel = t(Keys.common.ok);
|
||||
|
||||
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -5,6 +5,8 @@ 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";
|
||||
@@ -33,8 +35,8 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
const submit = async (): Promise<void> => {
|
||||
setFormError("");
|
||||
if (!selectedFiles || selectedFiles.length === 0) {
|
||||
setFormError("No file specified");
|
||||
logConsoleError("Could not load query -- No file specified. Please input a file.");
|
||||
setFormError(t(Keys.panes.loadQuery.noFileSpecifiedError));
|
||||
logConsoleError(t(Keys.panes.loadQuery.noFileSpecifiedError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
setLoadingFalse();
|
||||
} catch (error) {
|
||||
setLoadingFalse();
|
||||
setFormError("Failed to load query");
|
||||
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryError));
|
||||
logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
|
||||
}
|
||||
};
|
||||
@@ -71,7 +73,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
};
|
||||
|
||||
reader.onerror = (): void => {
|
||||
setFormError("Failed to load query");
|
||||
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryFromFileError, { fileName: file.name }));
|
||||
logConsoleError(`Failed to load query from file ${file.name}`);
|
||||
};
|
||||
return reader.readAsText(file);
|
||||
@@ -79,7 +81,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "Load",
|
||||
submitButtonText: t(Keys.common.load),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
@@ -90,7 +92,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
<Stack horizontal>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
label="Select a query document"
|
||||
label={t(Keys.panes.loadQuery.selectFilesToOpen)}
|
||||
value={selectedFileName}
|
||||
autoFocus
|
||||
readOnly
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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";
|
||||
@@ -41,7 +43,7 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError: errorMessage,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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";
|
||||
|
||||
@@ -20,13 +22,17 @@ 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="Infomation" />;
|
||||
let icon: JSX.Element = (
|
||||
<Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label={t(Keys.panes.panelInfo.information)} />
|
||||
);
|
||||
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="Infomation" />;
|
||||
icon = (
|
||||
<Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label={t(Keys.panes.panelInfo.information)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -43,7 +49,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
</Text>
|
||||
{showErrorDetails && (
|
||||
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
|
||||
More details
|
||||
{t(Keys.panes.panelInfo.moreDetails)}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -5,6 +5,8 @@ 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";
|
||||
@@ -91,7 +93,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
|
||||
let startKey: number;
|
||||
|
||||
if (!notebookName || !notebookDescription || !author || !imageSrc) {
|
||||
setFormError(`Failed to publish ${notebookName} to gallery`);
|
||||
setFormError(t(Keys.panes.publishNotebook.publishFailedError, { notebookName }));
|
||||
setFormErrorDetail("Name, description, author and cover image are required");
|
||||
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
|
||||
setIsExecuting(false);
|
||||
@@ -143,7 +145,11 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
|
||||
);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`);
|
||||
setFormError(
|
||||
t(Keys.panes.publishNotebook.publishFailedError, {
|
||||
notebookName: FileSystemUtil.stripExtension(notebookName, "ipynb"),
|
||||
}),
|
||||
);
|
||||
setFormErrorDetail(`${errorMessage}`);
|
||||
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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";
|
||||
@@ -57,13 +59,11 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
const maxImageSizeInMib = 1.5;
|
||||
|
||||
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 descriptionPara1 = t(Keys.panes.publishNotebook.publishDescription);
|
||||
|
||||
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
|
||||
notebookName,
|
||||
"ipynb",
|
||||
)}" to the gallery?`;
|
||||
const descriptionPara2 = t(Keys.panes.publishNotebook.publishPrompt, {
|
||||
name: FileSystemUtil.stripExtension(notebookName, "ipynb"),
|
||||
});
|
||||
|
||||
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
|
||||
if (onTakeSnapshot) {
|
||||
@@ -74,9 +74,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
}
|
||||
|
||||
const thumbnailSelectorProps: IDropdownProps = {
|
||||
label: "Cover image",
|
||||
label: t(Keys.panes.publishNotebook.coverImage),
|
||||
selectedKey: type,
|
||||
ariaLabel: "Cover image",
|
||||
ariaLabel: t(Keys.panes.publishNotebook.coverImage),
|
||||
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("Output does not exist for any of the cells."));
|
||||
firstOutputErrorHandler(new Error(t(Keys.panes.publishNotebook.outputDoesNotExist)));
|
||||
}
|
||||
}
|
||||
setType(options.text);
|
||||
@@ -107,8 +107,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
};
|
||||
|
||||
const thumbnailUrlProps: ITextFieldProps = {
|
||||
label: "Cover image url",
|
||||
ariaLabel: "Cover image url",
|
||||
label: t(Keys.panes.publishNotebook.coverImageUrl),
|
||||
ariaLabel: t(Keys.panes.publishNotebook.coverImageUrl),
|
||||
required: true,
|
||||
onChange: (event, newValue) => {
|
||||
setImageSrc(newValue);
|
||||
@@ -116,7 +116,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
};
|
||||
|
||||
const firstOutputErrorHandler = (error: Error) => {
|
||||
const formError = "Failed to capture first output";
|
||||
const formError = t(Keys.panes.publishNotebook.failedToCaptureOutput);
|
||||
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 = `Failed to convert ${file.name} to base64 format`;
|
||||
const formError = t(Keys.panes.publishNotebook.failedToConvertError, { fileName: file.name });
|
||||
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 = `Failed to upload ${file.name}`;
|
||||
const formError = t(Keys.panes.publishNotebook.failedToUploadError, { fileName: 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="Name"
|
||||
ariaLabel="Name"
|
||||
label={t(Keys.panes.publishNotebook.name)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.name)}
|
||||
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
|
||||
required
|
||||
onChange={(event, newValue) => {
|
||||
@@ -198,8 +198,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Description"
|
||||
ariaLabel="Description"
|
||||
label={t(Keys.panes.publishNotebook.description)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.description)}
|
||||
multiline
|
||||
rows={3}
|
||||
required
|
||||
@@ -211,9 +211,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Tags"
|
||||
ariaLabel="Tags"
|
||||
placeholder="Optional tag 1, Optional tag 2"
|
||||
label={t(Keys.panes.publishNotebook.tags)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.tags)}
|
||||
placeholder={t(Keys.panes.publishNotebook.tagsPlaceholder)}
|
||||
onChange={(event, newValue) => {
|
||||
setNotebookTags(newValue);
|
||||
}}
|
||||
@@ -227,7 +227,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>Preview</Text>
|
||||
<Text>{t(Keys.panes.publishNotebook.preview)}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<GalleryCardComponent
|
||||
|
||||
@@ -4,6 +4,8 @@ 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";
|
||||
@@ -28,27 +30,27 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [queryName, setQueryName] = useState<string>("");
|
||||
|
||||
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 setupSaveQueriesText = t(Keys.panes.saveQuery.setupCostMessage, { databaseName: SavedQueries.DatabaseName });
|
||||
const title = t(Keys.panes.saveQuery.panelTitle);
|
||||
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
setFormError("");
|
||||
if (!isSaveQueryEnabled()) {
|
||||
setFormError("Cannot save query");
|
||||
logConsoleError("Failed to save query: account not setup to save queries");
|
||||
logConsoleError(t(Keys.panes.saveQuery.accountNotSetupError));
|
||||
}
|
||||
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
if (!queryName || queryName.length === 0) {
|
||||
setFormError("No query name specified");
|
||||
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
|
||||
setFormError(t(Keys.panes.saveQuery.noQueryNameError));
|
||||
logConsoleError(t(Keys.panes.saveQuery.noQueryNameError));
|
||||
return;
|
||||
} else if (!query || query.length === 0) {
|
||||
setFormError("Invalid query content specified");
|
||||
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content.");
|
||||
setFormError(t(Keys.panes.saveQuery.invalidQueryContentError));
|
||||
logConsoleError(t(Keys.panes.saveQuery.invalidQueryContentError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,8 +82,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
} catch (error) {
|
||||
setLoadingFalse();
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError("Failed to save query");
|
||||
logConsoleError(`Failed to save query: ${errorMessage}`);
|
||||
setFormError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }));
|
||||
logConsoleError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }) + ": " + errorMessage);
|
||||
traceFailure(
|
||||
Action.SaveQuery,
|
||||
{
|
||||
@@ -126,8 +128,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
setFormError("Failed to setup a container for saved queries");
|
||||
logConsoleError(`Failed to setup a container for saved queries: ${errorMessage}`);
|
||||
setFormError(t(Keys.panes.saveQuery.failedToSetupContainerError));
|
||||
logConsoleError(t(Keys.panes.saveQuery.failedToSetupContainerError) + ": " + errorMessage);
|
||||
} finally {
|
||||
setLoadingFalse();
|
||||
}
|
||||
@@ -136,7 +138,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
|
||||
submitButtonText: isSaveQueryEnabled() ? t(Keys.common.save) : t(Keys.panes.saveQuery.completeSetup),
|
||||
onSubmit: () => {
|
||||
isSaveQueryEnabled() ? submit() : setupQueries();
|
||||
},
|
||||
@@ -160,7 +162,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
) : (
|
||||
<TextField
|
||||
id="saveQueryInput"
|
||||
label="Name"
|
||||
label={t(Keys.panes.saveQuery.name)}
|
||||
autoFocus
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
onChange={(event, newInput?: string) => {
|
||||
|
||||
@@ -24,6 +24,8 @@ 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,
|
||||
@@ -235,7 +237,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const regionOptions: IDropdownOption[] = [];
|
||||
regionOptions.push({
|
||||
key: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||
text: `Global (Default)`,
|
||||
text: t(Keys.panes.settings.globalDefault),
|
||||
data: {
|
||||
isGlobal: true,
|
||||
writeEnabled: true,
|
||||
@@ -246,7 +248,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
uniqueAccountRegions.add(loc.locationName);
|
||||
regionOptions.push({
|
||||
key: loc.documentEndpoint,
|
||||
text: `${loc.locationName} (Read/Write)`,
|
||||
text: `${loc.locationName} ${t(Keys.panes.settings.readWrite)}`,
|
||||
data: {
|
||||
isGlobal: false,
|
||||
writeEnabled: true,
|
||||
@@ -259,7 +261,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
uniqueAccountRegions.add(loc.locationName);
|
||||
regionOptions.push({
|
||||
key: loc.documentEndpoint,
|
||||
text: `${loc.locationName} (Read)`,
|
||||
text: `${loc.locationName} ${t(Keys.panes.settings.read)}`,
|
||||
data: {
|
||||
isGlobal: false,
|
||||
writeEnabled: false,
|
||||
@@ -317,13 +319,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
authError instanceof msalAuthError &&
|
||||
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
|
||||
) {
|
||||
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`,
|
||||
);
|
||||
logConsoleError(t(Keys.panes.settings.popupsDisabledError));
|
||||
} else {
|
||||
logConsoleError(
|
||||
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
|
||||
);
|
||||
logConsoleError(t(Keys.panes.settings.failedToAcquireTokenError));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -485,33 +483,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const genericPaneProps: RightPaneFormProps = {
|
||||
formError: "",
|
||||
isExecuting,
|
||||
submitButtonText: "Apply",
|
||||
submitButtonText: t(Keys.common.apply),
|
||||
onSubmit: () => handlerOnSubmit(),
|
||||
};
|
||||
const pageOptionList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
||||
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" },
|
||||
{ key: Constants.Queries.CustomPageOption, text: t(Keys.panes.settings.custom) },
|
||||
{ key: Constants.Queries.UnlimitedPageOption, text: t(Keys.panes.settings.unlimited) },
|
||||
];
|
||||
|
||||
const graphAutoOptionList: IChoiceGroupOption[] = [
|
||||
{ key: "false", text: "Graph" },
|
||||
{ key: "true", text: "JSON" },
|
||||
{ key: "false", text: t(Keys.panes.settings.graph) },
|
||||
{ key: "true", text: t(Keys.panes.settings.json) },
|
||||
];
|
||||
|
||||
const priorityLevelOptionList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.PriorityLevel.Low, text: "Low" },
|
||||
{ key: Constants.PriorityLevel.High, text: "High" },
|
||||
{ key: Constants.PriorityLevel.Low, text: t(Keys.panes.settings.low) },
|
||||
{ key: Constants.PriorityLevel.High, text: t(Keys.panes.settings.high) },
|
||||
];
|
||||
|
||||
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" },
|
||||
{ key: Constants.RBACOptions.setTrueRBACOption, text: "True" },
|
||||
{ key: Constants.RBACOptions.setFalseRBACOption, text: "False" },
|
||||
{ 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"]) },
|
||||
];
|
||||
|
||||
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
|
||||
{ key: SplitterDirection.Vertical, text: "Vertical" },
|
||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
||||
{ key: SplitterDirection.Vertical, text: t(Keys.tabs.query.vertical) },
|
||||
{ key: SplitterDirection.Horizontal, text: t(Keys.tabs.query.horizontal) },
|
||||
];
|
||||
|
||||
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
|
||||
@@ -724,13 +722,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowQueryPageOptions && (
|
||||
<AccordionItem value="1">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Page Options</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.pageOptions)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
|
||||
many query results per page.
|
||||
{t(Keys.panes.settings.pageOptionsDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="pageOptions"
|
||||
@@ -744,14 +741,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{isCustomPageOptionSelected() && (
|
||||
<div className="tabcontent">
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Query results per page{" "}
|
||||
{t(Keys.panes.settings.queryResultsPerPage)}{" "}
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Enter the number of query results that should be shown per page.
|
||||
{t(Keys.panes.settings.queryResultsPerPageTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<SpinButton
|
||||
ariaLabel="Custom query items per page"
|
||||
ariaLabel={t(Keys.panes.settings.customQueryItemsPerPage)}
|
||||
value={"" + customItemPerPage}
|
||||
onIncrement={(newValue) => {
|
||||
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
|
||||
@@ -761,8 +758,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
min={1}
|
||||
step={1}
|
||||
className="textfontclr"
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
|
||||
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
@@ -774,20 +771,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{showEnableEntraIdRbac && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.entraIdRbac)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
{t(Keys.panes.settings.entraIdRbacDescription)}
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
{t(Keys.common.learnMore)}{" "}
|
||||
</a>
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
@@ -804,17 +800,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
|
||||
<AccordionItem value="3">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Region Selection</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.regionSelection)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Changes region the Cosmos Client uses to access account.
|
||||
{t(Keys.panes.settings.regionSelectionDescription)}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Select Region</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.selectRegion)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Changes the account endpoint used to perform client operations.
|
||||
{t(Keys.panes.settings.selectRegionTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<Dropdown
|
||||
@@ -865,17 +861,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
<>
|
||||
<AccordionItem value="4">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Query Timeout</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.queryTimeout)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.queryTimeoutDescription)}
|
||||
</div>
|
||||
<Toggle
|
||||
styles={toggleStyles}
|
||||
label="Enable query timeout"
|
||||
label={t(Keys.panes.settings.enableQueryTimeout)}
|
||||
onChange={handleOnQueryTimeoutToggleChange}
|
||||
defaultChecked={queryTimeoutEnabled}
|
||||
/>
|
||||
@@ -883,18 +878,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{queryTimeoutEnabled && (
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<SpinButton
|
||||
label="Query timeout (ms)"
|
||||
label={t(Keys.panes.settings.queryTimeoutMs)}
|
||||
labelPosition={Position.top}
|
||||
defaultValue={(queryTimeout || 5000).toString()}
|
||||
min={100}
|
||||
step={1000}
|
||||
onChange={handleOnQueryTimeoutSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
|
||||
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<Toggle
|
||||
label="Automatically cancel query after timeout"
|
||||
label={t(Keys.panes.settings.automaticallyCancelQuery)}
|
||||
styles={toggleStyles}
|
||||
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
||||
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
||||
@@ -905,16 +900,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionItem>
|
||||
<AccordionItem value="5">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>RU Limit</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.ruLimit)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
If a query exceeds a configured RU limit, the query will be aborted.
|
||||
{t(Keys.panes.settings.ruLimitDescription)}
|
||||
</div>
|
||||
<Toggle
|
||||
styles={toggleStyles}
|
||||
label="Enable RU limit"
|
||||
label={t(Keys.panes.settings.enableRuLimit)}
|
||||
onChange={handleOnRUThresholdToggleChange}
|
||||
defaultChecked={ruThresholdEnabled}
|
||||
/>
|
||||
@@ -922,14 +917,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{ruThresholdEnabled && (
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<SpinButton
|
||||
label="RU Limit (RU)"
|
||||
label={t(Keys.panes.settings.ruLimitLabel)}
|
||||
labelPosition={Position.top}
|
||||
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||
min={1}
|
||||
step={1000}
|
||||
onChange={handleOnRUThresholdSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
|
||||
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
@@ -939,12 +934,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Default Query Results View</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.defaultQueryResults)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Select the default view to use when displaying query results.
|
||||
{t(Keys.panes.settings.defaultQueryResultsDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="defaultQueryResultsView"
|
||||
@@ -962,17 +957,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{showRetrySettings && (
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.retrySettings)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
{t(Keys.panes.settings.retrySettingsDescription)}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.maxRetryAttempts)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
{t(Keys.panes.settings.maxRetryAttemptsTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
@@ -981,18 +976,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
|
||||
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
|
||||
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}>Fixed retry interval (ms)</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.fixedRetryInterval)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
|
||||
as part of the response. Default value is 0 milliseconds.
|
||||
{t(Keys.panes.settings.fixedRetryIntervalTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
@@ -1001,18 +995,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
|
||||
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
|
||||
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}>Max wait time (s)</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.maxWaitTime)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
{t(Keys.panes.settings.maxWaitTimeTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
@@ -1021,8 +1014,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
|
||||
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
|
||||
onIncrement={(newValue) =>
|
||||
setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)
|
||||
}
|
||||
@@ -1039,24 +1032,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{!isEmulator && (
|
||||
<AccordionItem value="8">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enableContainerPagination)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
{t(Keys.panes.settings.enableContainerPaginationDescription)}
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
ariaLabel={t(Keys.panes.settings.enableContainerPagination)}
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
label={t(Keys.panes.settings.enableContainerPagination)}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable container pagination</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableContainerPagination)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1066,24 +1061,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<AccordionItem value="9">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable cross-partition query</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enableCrossPartitionQuery)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.enableCrossPartitionQueryDescription)}
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable cross partition query"
|
||||
ariaLabel={t(Keys.panes.settings.enableCrossPartitionQuery)}
|
||||
checked={crossPartitionQueryEnabled}
|
||||
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable cross-partition query</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableCrossPartitionQuery)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1093,19 +1089,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowEnhancedQueryControl && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enhanced query control</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enhancedQueryControl)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Query up to the max degree of parallelism.
|
||||
{t(Keys.panes.settings.maxDegreeOfParallelismQuery)}
|
||||
<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"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
{t(Keys.common.learnMore)}{" "}
|
||||
</a>
|
||||
</div>
|
||||
<Checkbox
|
||||
@@ -1113,11 +1109,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="EnableQueryControl"
|
||||
ariaLabel={t(Keys.panes.settings.enableQueryControl)}
|
||||
checked={queryControlEnabled}
|
||||
onChange={() => setQueryControlEnabled(!queryControlEnabled)}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable query control</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableQueryControl)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1127,14 +1125,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowParallelismOption && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Max degree of parallelism</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.maxDegreeOfParallelism)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.maxDegreeOfParallelismDescription)}
|
||||
</div>
|
||||
<SpinButton
|
||||
min={-1}
|
||||
@@ -1150,8 +1146,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
|
||||
}
|
||||
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
||||
ariaLabel="Max degree of parallelism"
|
||||
label="Max degree of parallelism"
|
||||
ariaLabel={t(Keys.panes.settings.maxDegreeOfParallelism)}
|
||||
label={t(Keys.panes.settings.maxDegreeOfParallelism)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
@@ -1161,14 +1157,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowPriorityLevelOption && (
|
||||
<AccordionItem value="11">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Priority Level</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.priorityLevel)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.priorityLevelDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="priorityLevel"
|
||||
@@ -1184,19 +1178,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowGraphAutoVizOption && (
|
||||
<AccordionItem value="12">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Display Gremlin query results as: </div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.displayGremlinQueryResults)} </div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Select Graph to automatically visualize the query results as a Graph or JSON to display the
|
||||
results as JSON.
|
||||
{t(Keys.panes.settings.displayGremlinQueryResultsDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
selectedKey={graphAutoVizDisabled}
|
||||
options={graphAutoOptionList}
|
||||
onChange={handleOnGremlinChange}
|
||||
aria-label="Graph Auto-visualization"
|
||||
aria-label={t(Keys.panes.settings.graphAutoVisualization)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
@@ -1205,25 +1198,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowCopilotSampleDBOption && (
|
||||
<AccordionItem value="13">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable sample database</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enableSampleDatabase)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.enableSampleDatabaseDescription)}
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable sample db for query exploration"
|
||||
ariaLabel={t(Keys.panes.settings.enableSampleDbAriaLabel)}
|
||||
checked={copilotSampleDBEnabled}
|
||||
onChange={handleSampleDatabaseChange}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable sample database</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableSampleDatabase)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1233,13 +1226,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowMongoGuidRepresentationOption && (
|
||||
<AccordionItem value="14">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Guid Representation</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.guidRepresentation)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.guidRepresentationDescription)}
|
||||
</div>
|
||||
<Dropdown
|
||||
aria-labelledby="mongoGuidRepresentation"
|
||||
@@ -1253,7 +1245,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
)}
|
||||
<AccordionItem value="15">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Advanced Settings</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.advancedSettings)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
@@ -1283,14 +1275,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
},
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Ignore partition key on document update"
|
||||
ariaLabel={t(Keys.panes.settings.ignorePartitionKey)}
|
||||
checked={ignorePartitionKeyOnDocumentUpdate}
|
||||
onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange}
|
||||
label="Ignore partition key on document update"
|
||||
label={t(Keys.panes.settings.ignorePartitionKey)}
|
||||
/>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
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.
|
||||
{t(Keys.panes.settings.ignorePartitionKeyTooltip)}
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -1320,9 +1311,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
}}
|
||||
onClick={() => {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Clear History",
|
||||
t(Keys.panes.settings.clearHistory),
|
||||
undefined,
|
||||
"Are you sure you want to proceed?",
|
||||
t(Keys.panes.settings.clearHistoryConfirm),
|
||||
() => {
|
||||
deleteAllStates();
|
||||
updateUserContext({
|
||||
@@ -1332,35 +1323,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
});
|
||||
useClientWriteEnabled.setState({ clientWriteEnabled: true });
|
||||
},
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
undefined,
|
||||
<>
|
||||
<span>
|
||||
This action will clear the all customizations for this account in this browser, including:
|
||||
</span>
|
||||
<span>{t(Keys.panes.settings.clearHistoryDescription)}</span>
|
||||
<ul className={styles.bulletList}>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Clear History
|
||||
{t(Keys.panes.settings.clearHistory)}
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settingsSection">
|
||||
<div className={`settingsSectionPart ${styles.settingsSectionContainer}`}>
|
||||
<div className="settingsSectionLabel">Explorer Version</div>
|
||||
<div className="settingsSectionLabel">{t(Keys.panes.settings.explorerVersion)}</div>
|
||||
<div>{explorerVersion}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<div className="settingsSectionLabel">Session ID</div>
|
||||
<div className="settingsSectionLabel">{t(Keys.panes.settings.sessionId)}</div>
|
||||
<div>{sessionId}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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="EnableQueryControl"
|
||||
ariaLabel="Enable query control"
|
||||
checked={false}
|
||||
className="padding"
|
||||
onChange={[Function]}
|
||||
@@ -1190,7 +1190,8 @@ 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>
|
||||
|
||||
@@ -11,6 +11,8 @@ 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";
|
||||
|
||||
@@ -113,13 +115,13 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
||||
<CosmosFluentProvider>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Text>Select which columns to display in your view of items in your container.</Text>
|
||||
<Text>{t(Keys.panes.tableColumnSelection.selectColumns)}</Text>
|
||||
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
|
||||
<SearchBox
|
||||
className={styles.searchBox}
|
||||
value={columnSearchText}
|
||||
onChange={onSearchChange}
|
||||
placeholder="Search fields"
|
||||
placeholder={t(Keys.panes.tableColumnSelection.searchFields)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -130,7 +132,9 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
||||
key={columnDefinition.id}
|
||||
label={{
|
||||
className: styles.checkboxLabel,
|
||||
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
|
||||
children: `${columnDefinition.label}${
|
||||
columnDefinition.isPartitionKey ? t(Keys.panes.tableColumnSelection.partitionKeySuffix) : ""
|
||||
}`,
|
||||
}}
|
||||
checked={selectedColumnIdsSet.has(columnDefinition.id)}
|
||||
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
|
||||
@@ -138,15 +142,15 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
||||
))}
|
||||
</div>
|
||||
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
|
||||
Reset
|
||||
{t(Keys.panes.tableColumnSelection.reset)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
|
||||
<Button appearance="primary" onClick={onSave}>
|
||||
Save
|
||||
{t(Keys.common.save)}
|
||||
</Button>
|
||||
<Button appearance="secondary" onClick={closeSidePanel}>
|
||||
Cancel
|
||||
{t(Keys.common.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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";
|
||||
@@ -100,8 +102,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(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,13 +111,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
(property === "PartitionKey" && containsAnyWhiteSpace(value) === true) ||
|
||||
(property === "RowKey" && containsAnyWhiteSpace(value) === true)
|
||||
) {
|
||||
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`);
|
||||
logConsoleError(t(Keys.panes.tables.whitespaceError, { property }));
|
||||
setFormError(t(Keys.panes.tables.whitespaceError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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";
|
||||
@@ -198,7 +200,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,8 +210,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
(property === "RowKey" && value === "") ||
|
||||
(property === "RowKey" && value === undefined)
|
||||
) {
|
||||
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -403,7 +405,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
)}
|
||||
</div>
|
||||
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
|
||||
Warning: Null fields will not be displayed for editing.
|
||||
{t(Keys.panes.tables.nullFieldsWarning)}
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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";
|
||||
@@ -35,7 +37,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
const props: RightPaneFormProps = {
|
||||
formError: "",
|
||||
isExecuting: false,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit,
|
||||
};
|
||||
|
||||
@@ -121,11 +123,11 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<Text>Select the columns that you want to query.</Text>
|
||||
<Text>{t(Keys.panes.tableQuerySelect.selectColumns)}</Text>
|
||||
<div className="column-select-view">
|
||||
<Checkbox
|
||||
id="availableCheckbox"
|
||||
label="Available Columns"
|
||||
label={t(Keys.panes.tableQuerySelect.availableColumns)}
|
||||
checked={isAvailableColumnChecked}
|
||||
onChange={availableColumnsCheckboxClick}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,8 @@ 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";
|
||||
@@ -63,8 +65,8 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
const onSubmit = () => {
|
||||
setFormError("");
|
||||
if (!files || files.length === 0) {
|
||||
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.");
|
||||
setFormError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
|
||||
logConsoleError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,7 +152,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
},
|
||||
{
|
||||
key: "fileName",
|
||||
name: "FILE NAME",
|
||||
name: t(Keys.panes.uploadItems.fileNameColumn),
|
||||
fieldName: "fileName",
|
||||
minWidth: 120,
|
||||
maxWidth: 140,
|
||||
@@ -169,7 +171,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
name: "STATUS",
|
||||
name: t(Keys.panes.uploadItems.statusColumn),
|
||||
fieldName: "numSucceeded",
|
||||
minWidth: 120,
|
||||
maxWidth: 140,
|
||||
@@ -178,7 +180,11 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
data: "string",
|
||||
isPadded: true,
|
||||
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||
const fieldContent = t(Keys.panes.uploadItems.uploadStatus, {
|
||||
numSucceeded: item.numSucceeded,
|
||||
numThrottled: item.numThrottled,
|
||||
numFailed: item.numFailed,
|
||||
});
|
||||
return (
|
||||
<TooltipHost
|
||||
content={fieldContent}
|
||||
@@ -197,12 +203,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
<div className="paneMainContent">
|
||||
<Upload
|
||||
key={reducer} // Force re-render on state change
|
||||
label="Select JSON Files"
|
||||
label={t(Keys.panes.uploadItems.selectJsonFiles)}
|
||||
onUpload={updateSelectedFiles}
|
||||
accept="application/json"
|
||||
multiple
|
||||
tabIndex={0}
|
||||
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."
|
||||
tooltip={t(Keys.panes.uploadItems.selectJsonFilesTooltip)}
|
||||
/>
|
||||
{uploadFileData?.length > 0 && (
|
||||
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
|
||||
|
||||
@@ -714,9 +714,7 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user