add collection panel improvements (#630)
Co-authored-by: Jordi Bunster <jbunster@microsoft.com>
This commit is contained in:
parent
9878bf0d5e
commit
4efacace16
|
@ -29,11 +29,11 @@ export interface DatabaseContextMenuButtonParams {
|
||||||
* New resource tree (in ReactJS)
|
* New resource tree (in ReactJS)
|
||||||
*/
|
*/
|
||||||
export class ResourceTreeContextMenuButtonFactory {
|
export class ResourceTreeContextMenuButtonFactory {
|
||||||
public static createDatabaseContextMenu(container: Explorer): TreeNodeMenuItem[] {
|
public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] {
|
||||||
const items: TreeNodeMenuItem[] = [
|
const items: TreeNodeMenuItem[] = [
|
||||||
{
|
{
|
||||||
iconSrc: AddCollectionIcon,
|
iconSrc: AddCollectionIcon,
|
||||||
onClick: () => container.onNewCollectionClicked(),
|
onClick: () => container.onNewCollectionClicked(databaseId),
|
||||||
label: container.addCollectionText(),
|
label: container.addCollectionText(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
export interface CollapsibleSectionProps {
|
export interface CollapsibleSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
isExpandedByDefault: boolean;
|
isExpandedByDefault: boolean;
|
||||||
|
onExpand?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
|
@ -23,6 +24,12 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
this.setState({ isExpanded: !this.state.isExpanded });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public componentDidUpdate(): void {
|
||||||
|
if (this.state.isExpanded && this.props.onExpand) {
|
||||||
|
this.props.onExpand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -415,7 +415,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
<em>
|
<em>
|
||||||
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||||
</em>
|
</em>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -689,7 +689,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
<em>
|
<em>
|
||||||
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||||
</em>
|
</em>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -150,7 +150,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
<em>
|
<em>
|
||||||
*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||||
</em>
|
</em>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -11,10 +11,6 @@
|
||||||
padding: 0 @LargeSpace 0 @SmallSpace;
|
padding: 0 @LargeSpace 0 @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.throughputInputSpacing {
|
.throughputInputSpacing > :not(:last-child) {
|
||||||
margin-bottom: @SmallSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin-bottom: @SmallSpace;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ import React from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
|
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||||
|
|
||||||
export interface ThroughputInputProps {
|
export interface ThroughputInputProps {
|
||||||
isDatabase: boolean;
|
isDatabase: boolean;
|
||||||
|
isSharded: boolean;
|
||||||
showFreeTierExceedThroughputTooltip: boolean;
|
showFreeTierExceedThroughputTooltip: boolean;
|
||||||
setThroughputValue: (throughput: number) => void;
|
setThroughputValue: (throughput: number) => void;
|
||||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||||
|
@ -18,6 +20,7 @@ export interface ThroughputInputState {
|
||||||
isAutoscaleSelected: boolean;
|
isAutoscaleSelected: boolean;
|
||||||
throughput: number;
|
throughput: number;
|
||||||
isCostAcknowledged: boolean;
|
isCostAcknowledged: boolean;
|
||||||
|
throughputError: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> {
|
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> {
|
||||||
|
@ -28,6 +31,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
isAutoscaleSelected: true,
|
isAutoscaleSelected: true,
|
||||||
throughput: AutoPilotUtils.minAutoPilotThroughput,
|
throughput: AutoPilotUtils.minAutoPilotThroughput,
|
||||||
isCostAcknowledged: false,
|
isCostAcknowledged: false,
|
||||||
|
throughputError: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||||
|
@ -39,11 +43,11 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
<div className="throughputInputContainer throughputInputSpacing">
|
<div className="throughputInputContainer throughputInputSpacing">
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text variant="small" style={{ lineHeight: "20px" }}>
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
{this.getThroughputLabelText()}
|
{this.getThroughputLabelText()}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
|
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -74,7 +78,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
{this.state.isAutoscaleSelected && (
|
{this.state.isAutoscaleSelected && (
|
||||||
<Stack className="throughputInputSpacing">
|
<Stack className="throughputInputSpacing">
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Provision maximum RU/s required by this resource. Estimate your required RU/s with
|
Estimate your required RU/s with
|
||||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||||
capacity calculator
|
capacity calculator
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -82,11 +86,11 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Text variant="small" style={{ lineHeight: "20px" }}>
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
Max RU/s
|
{this.props.isDatabase ? "Database" : getCollectionName()} max RU/s
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
|
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -101,11 +105,12 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||||
value={this.state.throughput.toString()}
|
value={this.state.throughput.toString()}
|
||||||
aria-label="Max request units per second"
|
aria-label="Max request units per second"
|
||||||
required={true}
|
errorMessage={this.state.throughputError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Your {this.props.isDatabase ? "database" : "container"} throughput will automatically scale from{" "}
|
Your {this.props.isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will
|
||||||
|
automatically scale from{" "}
|
||||||
<b>
|
<b>
|
||||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
|
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
|
||||||
{this.state.throughput} RU/s
|
{this.state.throughput} RU/s
|
||||||
|
@ -147,6 +152,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
value={this.state.throughput.toString()}
|
value={this.state.throughput.toString()}
|
||||||
aria-label="Max request units per second"
|
aria-label="Max request units per second"
|
||||||
required={true}
|
required={true}
|
||||||
|
errorMessage={this.state.throughputError}
|
||||||
/>
|
/>
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -156,6 +162,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
|
|
||||||
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
|
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
|
||||||
<Stack horizontal verticalAlign="start">
|
<Stack horizontal verticalAlign="start">
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={this.state.isCostAcknowledged}
|
checked={this.state.isCostAcknowledged}
|
||||||
styles={{
|
styles={{
|
||||||
|
@ -177,30 +184,35 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||||
}
|
}
|
||||||
|
|
||||||
private getThroughputLabelText(): string {
|
private getThroughputLabelText(): string {
|
||||||
|
let throughputHeaderText: string;
|
||||||
if (this.state.isAutoscaleSelected) {
|
if (this.state.isAutoscaleSelected) {
|
||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
throughputHeaderText = AutoPilotUtils.getAutoPilotHeaderText().toLocaleLowerCase();
|
||||||
|
} else {
|
||||||
|
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
|
||||||
|
const maxRU: string = userContext.isTryCosmosDBSubscription
|
||||||
|
? Constants.TryCosmosExperience.maxRU.toLocaleString()
|
||||||
|
: "unlimited";
|
||||||
|
throughputHeaderText = `throughput (${minRU} - ${maxRU} RU/s)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
|
return `${this.props.isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
|
||||||
const maxRU: string = userContext.isTryCosmosDBSubscription
|
|
||||||
? Constants.TryCosmosExperience.maxRU.toLocaleString()
|
|
||||||
: "unlimited";
|
|
||||||
return this.state.isAutoscaleSelected
|
|
||||||
? AutoPilotUtils.getAutoPilotHeaderText()
|
|
||||||
: `Throughput (${minRU} - ${maxRU} RU/s)`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onThroughputValueChange(newInput: string): void {
|
private onThroughputValueChange(newInput: string): void {
|
||||||
const newThroughput = parseInt(newInput);
|
const newThroughput = parseInt(newInput);
|
||||||
this.setState({ throughput: newThroughput });
|
this.setState({ throughput: newThroughput });
|
||||||
this.props.setThroughputValue(newThroughput);
|
this.props.setThroughputValue(newThroughput);
|
||||||
|
|
||||||
|
if (!this.props.isSharded && newThroughput > 10000) {
|
||||||
|
this.setState({ throughputError: "Unsharded collections support up to 10,000 RUs" });
|
||||||
|
} else {
|
||||||
|
this.setState({ throughputError: undefined });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAutoScaleTooltip(): string {
|
private getAutoScaleTooltip(): string {
|
||||||
return `After the first ${AutoPilotUtils.getStorageBasedOnUserInput(
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
this.state.throughput
|
return `Set the max RU/s to the highest RU/s you want your ${collectionName} to scale to. The ${collectionName} will scale between 10% of max RU/s to the max RU/s based on usage.`;
|
||||||
)} GB of data stored, the max
|
|
||||||
RU/s will be automatically upgraded based on the new storage value.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCostAcknowledgeText(): string {
|
private getCostAcknowledgeText(): string {
|
||||||
|
@ -271,10 +283,20 @@ const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props:
|
||||||
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
||||||
: PricingUtils.getPricePerRu(serverId) * multiplier;
|
: PricingUtils.getPricePerRu(serverId) * multiplier;
|
||||||
|
|
||||||
|
const iconWithEstimatedCostDisclaimer: JSX.Element = (
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={PricingUtils.estimatedCostDisclaimer}
|
||||||
|
styles={{ root: { verticalAlign: "bottom" } }}
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
|
</TooltipHost>
|
||||||
|
);
|
||||||
|
|
||||||
if (isAutoscale) {
|
if (isAutoscale) {
|
||||||
return (
|
return (
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Estimated monthly cost ({currency}):{" "}
|
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||||
<b>
|
<b>
|
||||||
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
|
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
|
||||||
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
|
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
|
||||||
|
@ -287,7 +309,7 @@ const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props:
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text variant="small">
|
<Text variant="small">
|
||||||
Cost ({currency}):{" "}
|
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||||
<b>
|
<b>
|
||||||
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "}
|
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "}
|
||||||
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "}
|
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "}
|
||||||
|
@ -295,8 +317,6 @@ const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props:
|
||||||
</b>
|
</b>
|
||||||
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
|
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
|
||||||
{currencySign + pricePerRu}/RU)
|
{currencySign + pricePerRu}/RU)
|
||||||
<br />
|
|
||||||
<em>{PricingUtils.estimatedCostDisclaimer}</em>
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,12 +30,12 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
|
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { getCollectionName } from "../Utils/APITypeUtils";
|
||||||
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
import { stringToBlob } from "../Utils/BlobUtils";
|
||||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PricingUtils from "../Utils/PricingUtils";
|
|
||||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||||
|
@ -1950,14 +1950,14 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewCollectionClicked(): void {
|
public onNewCollectionClicked(databaseId?: string): void {
|
||||||
if (userContext.apiType === "Cassandra") {
|
if (userContext.apiType === "Cassandra") {
|
||||||
this.cassandraAddCollectionPane.open();
|
this.cassandraAddCollectionPane.open();
|
||||||
} else if (userContext.features.enableReactPane) {
|
} else if (userContext.features.enableKOPanel) {
|
||||||
this.openAddCollectionPanel();
|
|
||||||
} else {
|
|
||||||
this.addCollectionPane.open(this.selectedDatabaseId());
|
this.addCollectionPane.open(this.selectedDatabaseId());
|
||||||
document.getElementById("linkAddCollection").focus();
|
document.getElementById("linkAddCollection").focus();
|
||||||
|
} else {
|
||||||
|
this.openAddCollectionPanel(databaseId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2061,14 +2061,9 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public openDeleteCollectionConfirmationPane(): void {
|
public openDeleteCollectionConfirmationPane(): void {
|
||||||
let collectionName = PricingUtils.getCollectionName(userContext.apiType);
|
|
||||||
this.openSidePanel(
|
this.openSidePanel(
|
||||||
"Delete " + collectionName,
|
"Delete " + getCollectionName(),
|
||||||
<DeleteCollectionConfirmationPane
|
<DeleteCollectionConfirmationPane explorer={this} closePanel={this.closeSidePanel} />
|
||||||
explorer={this}
|
|
||||||
collectionName={collectionName}
|
|
||||||
closePanel={this.closeSidePanel}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2106,14 +2101,15 @@ export default class Explorer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openAddCollectionPanel(): Promise<void> {
|
public async openAddCollectionPanel(databaseId?: string): Promise<void> {
|
||||||
await this.loadDatabaseOffers();
|
await this.loadDatabaseOffers();
|
||||||
this.openSidePanel(
|
this.openSidePanel(
|
||||||
"New Collection",
|
"New " + getCollectionName(),
|
||||||
<AddCollectionPanel
|
<AddCollectionPanel
|
||||||
explorer={this}
|
explorer={this}
|
||||||
closePanel={() => this.closeSidePanel()}
|
closePanel={() => this.closeSidePanel()}
|
||||||
openNotificationConsole={() => this.expandConsole()}
|
openNotificationConsole={() => this.expandConsole()}
|
||||||
|
databaseId={databaseId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { ActionContracts } from "../Contracts/ExplorerContracts";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import Explorer from "./Explorer";
|
import Explorer from "./Explorer";
|
||||||
import { handleOpenAction } from "./OpenActions";
|
import { handleOpenAction } from "./OpenActions";
|
||||||
import AddCollectionPane from "./Panes/AddCollectionPane";
|
|
||||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||||
|
|
||||||
describe("OpenActions", () => {
|
describe("OpenActions", () => {
|
||||||
|
@ -15,8 +14,7 @@ describe("OpenActions", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
explorer = {} as Explorer;
|
explorer = {} as Explorer;
|
||||||
explorer.addCollectionPane = {} as AddCollectionPane;
|
explorer.onNewCollectionClicked = jest.fn();
|
||||||
explorer.addCollectionPane.open = jest.fn();
|
|
||||||
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
|
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
|
||||||
explorer.cassandraAddCollectionPane.open = jest.fn();
|
explorer.cassandraAddCollectionPane.open = jest.fn();
|
||||||
explorer.closeAllPanes = () => {};
|
explorer.closeAllPanes = () => {};
|
||||||
|
@ -90,24 +88,24 @@ describe("OpenActions", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("AddCollection pane kind", () => {
|
describe("AddCollection pane kind", () => {
|
||||||
it("string value should call addCollectionPane.open", () => {
|
it("string value should call explorer.onNewCollectionClicked", () => {
|
||||||
const action = {
|
const action = {
|
||||||
actionType: "OpenPane",
|
actionType: "OpenPane",
|
||||||
paneKind: "AddCollection",
|
paneKind: "AddCollection",
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionHandled = handleOpenAction(action, [], explorer);
|
const actionHandled = handleOpenAction(action, [], explorer);
|
||||||
expect(explorer.addCollectionPane.open).toHaveBeenCalled();
|
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("enum value should call addCollectionPane.open", () => {
|
it("enum value should call explorer.onNewCollectionClicked", () => {
|
||||||
const action = {
|
const action = {
|
||||||
actionType: "OpenPane",
|
actionType: "OpenPane",
|
||||||
paneKind: ActionContracts.PaneKind.AddCollection,
|
paneKind: ActionContracts.PaneKind.AddCollection,
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionHandled = handleOpenAction(action, [], explorer);
|
const actionHandled = handleOpenAction(action, [], explorer);
|
||||||
expect(explorer.addCollectionPane.open).toHaveBeenCalled();
|
expect(explorer.onNewCollectionClicked).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -141,7 +141,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
|
||||||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
|
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
|
||||||
) {
|
) {
|
||||||
explorer.closeAllPanes();
|
explorer.closeAllPanes();
|
||||||
explorer.addCollectionPane.open();
|
explorer.onNewCollectionClicked();
|
||||||
} else if (
|
} else if (
|
||||||
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
||||||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
||||||
|
|
|
@ -143,7 +143,6 @@
|
||||||
size="40"
|
size="40"
|
||||||
class="collid"
|
class="collid"
|
||||||
data-bind="visible: databaseCreateNew, textInput: databaseId, hasFocus: firstFieldHasFocus"
|
data-bind="visible: databaseCreateNew, textInput: databaseId, hasFocus: firstFieldHasFocus"
|
||||||
aria-label="Database id"
|
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -161,7 +160,6 @@
|
||||||
size="40"
|
size="40"
|
||||||
class="collid"
|
class="collid"
|
||||||
data-bind="visible: !databaseCreateNew(), textInput: databaseId, hasFocus: firstFieldHasFocus"
|
data-bind="visible: !databaseCreateNew(), textInput: databaseId, hasFocus: firstFieldHasFocus"
|
||||||
aria-label="Database id"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
|
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
|
||||||
|
@ -246,7 +244,7 @@
|
||||||
placeholder="e.g., Container1"
|
placeholder="e.g., Container1"
|
||||||
size="40"
|
size="40"
|
||||||
class="textfontclr collid"
|
class="textfontclr collid"
|
||||||
data-bind="value: collectionId, attr: { 'aria-label': collectionIdTitle }"
|
data-bind="value: collectionId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -352,7 +350,6 @@
|
||||||
attr: {
|
attr: {
|
||||||
placeholder: partitionKeyPlaceholder,
|
placeholder: partitionKeyPlaceholder,
|
||||||
required: partitionKeyVisible(),
|
required: partitionKeyVisible(),
|
||||||
'aria-label': partitionKeyName,
|
|
||||||
pattern: partitionKeyPattern,
|
pattern: partitionKeyPattern,
|
||||||
title: partitionKeyTitle
|
title: partitionKeyTitle
|
||||||
}"
|
}"
|
||||||
|
|
|
@ -476,7 +476,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||||
userContext.portalEnv,
|
userContext.portalEnv,
|
||||||
this.isFreeTierAccount(),
|
this.isFreeTierAccount(),
|
||||||
this.container.isFirstResourceCreated(),
|
this.container.isFirstResourceCreated(),
|
||||||
userContext.apiType,
|
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
IconButton,
|
IconButton,
|
||||||
IDropdownOption,
|
IDropdownOption,
|
||||||
Link,
|
Link,
|
||||||
|
Separator,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TooltipHost,
|
TooltipHost,
|
||||||
|
@ -23,6 +24,7 @@ import { CollectionCreation, IndexingPolicies } from "../../Shared/Constants";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||||
import { getUpsellMessage } from "../../Utils/PricingUtils";
|
import { getUpsellMessage } from "../../Utils/PricingUtils";
|
||||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||||
|
@ -35,6 +37,7 @@ export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
closePanel: () => void;
|
closePanel: () => void;
|
||||||
openNotificationConsole: () => void;
|
openNotificationConsole: () => void;
|
||||||
|
databaseId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddCollectionPanelState {
|
export interface AddCollectionPanelState {
|
||||||
|
@ -48,7 +51,7 @@ export interface AddCollectionPanelState {
|
||||||
partitionKey: string;
|
partitionKey: string;
|
||||||
enableDedicatedThroughput: boolean;
|
enableDedicatedThroughput: boolean;
|
||||||
createMongoWildCardIndex: boolean;
|
createMongoWildCardIndex: boolean;
|
||||||
useHashV1: boolean;
|
useHashV2: boolean;
|
||||||
enableAnalyticalStore: boolean;
|
enableAnalyticalStore: boolean;
|
||||||
uniqueKeys: string[];
|
uniqueKeys: string[];
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
@ -67,17 +70,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
createNewDatabase: userContext.apiType !== "Tables",
|
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
|
||||||
newDatabaseId: "",
|
newDatabaseId: "",
|
||||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||||
selectedDatabaseId: userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : undefined,
|
selectedDatabaseId:
|
||||||
|
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||||
collectionId: "",
|
collectionId: "",
|
||||||
enableIndexing: true,
|
enableIndexing: true,
|
||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: "",
|
partitionKey: "",
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: false,
|
||||||
createMongoWildCardIndex: true,
|
createMongoWildCardIndex: true,
|
||||||
useHashV1: false,
|
useHashV2: false,
|
||||||
enableAnalyticalStore: false,
|
enableAnalyticalStore: false,
|
||||||
uniqueKeys: [],
|
uniqueKeys: [],
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
|
@ -100,13 +104,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
|
|
||||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||||
<PanelInfoErrorComponent
|
<PanelInfoErrorComponent
|
||||||
message={getUpsellMessage(
|
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)}
|
||||||
userContext.portalEnv,
|
|
||||||
true,
|
|
||||||
this.props.explorer.isFirstResourceCreated(),
|
|
||||||
userContext.apiType,
|
|
||||||
true
|
|
||||||
)}
|
|
||||||
messageType="info"
|
messageType="info"
|
||||||
showErrorDetails={false}
|
showErrorDetails={false}
|
||||||
openNotificationConsole={this.props.openNotificationConsole}
|
openNotificationConsole={this.props.openNotificationConsole}
|
||||||
|
@ -120,13 +118,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
Database id
|
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="A database is analogous to a namespace. It is the unit of management for a set of containers."
|
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||||
|
true
|
||||||
|
).toLocaleLowerCase()}.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -140,7 +140,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
type="radio"
|
type="radio"
|
||||||
role="radio"
|
role="radio"
|
||||||
id="databaseCreateNew"
|
id="databaseCreateNew"
|
||||||
data-test="addCollection-createNewDatabase"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
@ -154,8 +153,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
name="databaseType"
|
name="databaseType"
|
||||||
type="radio"
|
type="radio"
|
||||||
role="radio"
|
role="radio"
|
||||||
id="databaseUseExisting"
|
|
||||||
data-test="addCollection-existingDatabase"
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
@ -166,8 +163,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<input
|
<input
|
||||||
name="newDatabaseId"
|
name="newDatabaseId"
|
||||||
id="databaseId"
|
id="newDatabaseId"
|
||||||
data-test="addCollection-newDatabaseId"
|
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -177,7 +173,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
placeholder="Type a new database id"
|
placeholder="Type a new database id"
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
aria-label="Database id"
|
aria-label="New database id"
|
||||||
autoFocus
|
autoFocus
|
||||||
value={this.state.newDatabaseId}
|
value={this.state.newDatabaseId}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
@ -188,7 +184,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
{!this.isServerlessAccount() && (
|
{!this.isServerlessAccount() && (
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Provision database throughput"
|
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||||
checked={this.state.isSharedThroughputChecked}
|
checked={this.state.isSharedThroughputChecked}
|
||||||
styles={{
|
styles={{
|
||||||
text: { fontSize: 12 },
|
text: { fontSize: 12 },
|
||||||
|
@ -201,9 +197,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
/>
|
/>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="Provisioned throughput at the database level will be shared across all containers within the database."
|
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||||
|
true
|
||||||
|
).toLocaleLowerCase()} within the database.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
@ -214,6 +212,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||||
}
|
}
|
||||||
isDatabase={true}
|
isDatabase={true}
|
||||||
|
isSharded={this.state.isSharded}
|
||||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||||
|
@ -230,38 +229,40 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||||
this.setState({ selectedDatabaseId: database.key as string })
|
this.setState({ selectedDatabaseId: database.key as string })
|
||||||
}
|
}
|
||||||
|
defaultSelectedKey={this.props.databaseId}
|
||||||
|
responsiveMode={999}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Separator className="panelSeparator" />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
{`${this.getCollectionName()} id`}
|
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
name="collectionId"
|
name="collectionId"
|
||||||
id="containerId"
|
id="collectionId"
|
||||||
data-test="addCollection-collectionId"
|
|
||||||
type="text"
|
type="text"
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
pattern="[^/?#\\]*[^/?# \\]"
|
pattern="[^/?#\\]*[^/?# \\]"
|
||||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||||
placeholder={`e.g., ${this.getCollectionName()}1`}
|
placeholder={`e.g., ${getCollectionName()}1`}
|
||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
aria-label={`${this.getCollectionName()} id`}
|
aria-label={`${getCollectionName()} id`}
|
||||||
value={this.state.collectionId}
|
value={this.state.collectionId}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
this.setState({ collectionId: event.target.value })
|
this.setState({ collectionId: event.target.value })
|
||||||
|
@ -320,13 +321,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
<Stack horizontal>
|
<Stack horizontal>
|
||||||
<span className="mandatoryStar">* </span>
|
<span className="mandatoryStar">* </span>
|
||||||
<Text className="panelTextBold" variant="small">
|
<Text className="panelTextBold" variant="small">
|
||||||
Sharding options
|
Sharding
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
content={
|
||||||
|
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -371,18 +374,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content={`The ${this.getPartitionKeyName()} is used to automatically partition data among
|
content={this.getPartitionKeyTooltipText()}
|
||||||
multiple servers for scalability. Choose a JSON property name that has a wide range of values and is
|
|
||||||
likely to have evenly distributed access patterns.`}
|
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="addCollection-partitionKeyValue"
|
id="addCollection-partitionKeyValue"
|
||||||
data-test="addCollection-partitionKeyValue"
|
|
||||||
aria-required
|
aria-required
|
||||||
required
|
required
|
||||||
size={40}
|
size={40}
|
||||||
|
@ -392,9 +392,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||||
value={this.state.partitionKey}
|
value={this.state.partitionKey}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({ partitionKey: event.target.value })
|
if (
|
||||||
}
|
userContext.apiType !== "Mongo" &&
|
||||||
|
this.state.partitionKey === "" &&
|
||||||
|
!event.target.value.startsWith("/")
|
||||||
|
) {
|
||||||
|
this.setState({ partitionKey: "/" + event.target.value });
|
||||||
|
} else {
|
||||||
|
this.setState({ partitionKey: event.target.value });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
@ -402,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
{!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
|
{!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
|
||||||
<Stack horizontal verticalAlign="center">
|
<Stack horizontal verticalAlign="center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={`Provision dedicated throughput for this ${this.getCollectionName()}`}
|
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||||
checked={this.state.enableDedicatedThroughput}
|
checked={this.state.enableDedicatedThroughput}
|
||||||
styles={{
|
styles={{
|
||||||
text: { fontSize: 12 },
|
text: { fontSize: 12 },
|
||||||
|
@ -415,12 +423,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
/>
|
/>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
content="You can optionally provision dedicated throughput for a container within a database that has throughput
|
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 containers in the database and
|
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
|
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."
|
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
@ -431,6 +441,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||||
}
|
}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
|
isSharded={this.state.isSharded}
|
||||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||||
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
|
||||||
|
@ -451,7 +462,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
||||||
per partition key."
|
per partition key."
|
||||||
>
|
>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
</TooltipHost>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
@ -504,134 +515,129 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CollapsibleSectionComponent title="Advanced" isExpandedByDefault={false}>
|
{userContext.apiType !== "Tables" && (
|
||||||
<Stack className="panelGroupSpacing">
|
<CollapsibleSectionComponent
|
||||||
{this.props.explorer.isEnableMongoCapabilityPresent() && (
|
title="Advanced"
|
||||||
<Stack>
|
isExpandedByDefault={false}
|
||||||
<Stack horizontal>
|
onExpand={() => {
|
||||||
<span className="mandatoryStar">* </span>
|
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||||
<Text className="panelTextBold" variant="small">
|
this.scrollToAdvancedSection();
|
||||||
Indexing
|
}}
|
||||||
</Text>
|
>
|
||||||
<TooltipHost
|
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
{this.props.explorer.isEnableMongoCapabilityPresent() && (
|
||||||
content="By default, only the field _id is indexed. Creating a wildcard index on all fields will quickly optimize
|
<Stack className="panelGroupSpacing">
|
||||||
query performance and is recommended during development."
|
<Stack horizontal>
|
||||||
>
|
<span className="mandatoryStar">* </span>
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
<Text className="panelTextBold" variant="small">
|
||||||
</TooltipHost>
|
Indexing
|
||||||
</Stack>
|
</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."
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
|
</TooltipHost>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Create a Wildcard Index on all fields"
|
||||||
|
checked={this.state.createMongoWildCardIndex}
|
||||||
|
styles={{
|
||||||
|
text: { fontSize: 12 },
|
||||||
|
checkbox: { width: 12, height: 12 },
|
||||||
|
label: { padding: 0, alignItems: "center" },
|
||||||
|
}}
|
||||||
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||||
|
this.setState({ createMongoWildCardIndex: isChecked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{userContext.apiType === "SQL" && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Create a Wildcard Index on all fields"
|
label="My partition key is larger than 100 bytes"
|
||||||
checked={this.state.createMongoWildCardIndex}
|
checked={this.state.useHashV2}
|
||||||
styles={{
|
styles={{
|
||||||
text: { fontSize: 12 },
|
text: { fontSize: 12 },
|
||||||
checkbox: { width: 12, height: 12 },
|
checkbox: { width: 12, height: 12 },
|
||||||
label: { padding: 0, alignItems: "center" },
|
label: { padding: 0, alignItems: "center" },
|
||||||
}}
|
}}
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||||
this.setState({ createMongoWildCardIndex: isChecked })
|
this.setState({ useHashV2: isChecked })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{userContext.apiType === "SQL" && (
|
{this.shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<Stack horizontal verticalAlign="start">
|
<Stack horizontal>
|
||||||
<Checkbox
|
<Text className="panelTextBold" variant="small">
|
||||||
checked={this.state.useHashV1}
|
Analytical store
|
||||||
styles={{
|
|
||||||
checkbox: { width: 12, height: 12 },
|
|
||||||
label: { padding: 0, margin: "4px 4px 0 0" },
|
|
||||||
}}
|
|
||||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
|
||||||
this.setState({ useHashV1: isChecked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Text variant="small" style={{ lineHeight: "20px" }}>
|
|
||||||
My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text variant="small">
|
|
||||||
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme
|
|
||||||
that supports partition key values of size up to 100 bytes.{" "}
|
|
||||||
<Link target="_blank" href="https://aka.ms/cosmosdb/pkv2">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.shouldShowAnalyticalStoreOptions() && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Stack horizontal>
|
|
||||||
<Text className="panelTextBold" variant="small">
|
|
||||||
Analytical store
|
|
||||||
</Text>
|
|
||||||
<TooltipHost
|
|
||||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
||||||
content="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads. Learn more"
|
|
||||||
>
|
|
||||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center">
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={this.state.enableAnalyticalStore}
|
|
||||||
disabled={!this.isSynapseLinkEnabled()}
|
|
||||||
aria-label="Enable analytical store"
|
|
||||||
aria-checked={this.state.enableAnalyticalStore}
|
|
||||||
name="analyticalStore"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="enableAnalyticalStoreBtn"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">On</span>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="panelRadioBtn"
|
|
||||||
checked={!this.state.enableAnalyticalStore}
|
|
||||||
disabled={!this.isSynapseLinkEnabled()}
|
|
||||||
aria-label="Disable analytical store"
|
|
||||||
aria-checked={!this.state.enableAnalyticalStore}
|
|
||||||
name="analyticalStore"
|
|
||||||
type="radio"
|
|
||||||
role="radio"
|
|
||||||
id="disableAnalyticalStoreBtn"
|
|
||||||
tabIndex={0}
|
|
||||||
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<span className="panelRadioBtnLabel">Off</span>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{!this.isSynapseLinkEnabled() && (
|
|
||||||
<Stack className="panelGroupSpacing">
|
|
||||||
<Text variant="small">
|
|
||||||
Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link
|
|
||||||
for this Cosmos DB account.{" "}
|
|
||||||
<Link href="https://aka.ms/cosmosdb-synapselink" target="_blank">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</Text>
|
</Text>
|
||||||
<DefaultButton
|
<TooltipHost
|
||||||
text="Enable"
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
content={this.getAnalyticalStorageTooltipContent()}
|
||||||
style={{ height: 27, width: 80 }}
|
>
|
||||||
styles={{ label: { fontSize: 12 } }}
|
<Icon iconName="Info" className="panelInfoIcon" />
|
||||||
/>
|
</TooltipHost>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
|
||||||
</Stack>
|
<Stack horizontal verticalAlign="center">
|
||||||
)}
|
<input
|
||||||
</Stack>
|
className="panelRadioBtn"
|
||||||
</CollapsibleSectionComponent>
|
checked={this.state.enableAnalyticalStore}
|
||||||
|
disabled={!this.isSynapseLinkEnabled()}
|
||||||
|
aria-label="Enable analytical store"
|
||||||
|
aria-checked={this.state.enableAnalyticalStore}
|
||||||
|
name="analyticalStore"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
id="enableAnalyticalStoreBtn"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">On</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="panelRadioBtn"
|
||||||
|
checked={!this.state.enableAnalyticalStore}
|
||||||
|
disabled={!this.isSynapseLinkEnabled()}
|
||||||
|
aria-label="Disable analytical store"
|
||||||
|
aria-checked={!this.state.enableAnalyticalStore}
|
||||||
|
name="analyticalStore"
|
||||||
|
type="radio"
|
||||||
|
role="radio"
|
||||||
|
id="disableAnalyticalStoreBtn"
|
||||||
|
tabIndex={0}
|
||||||
|
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||||
|
/>
|
||||||
|
<span className="panelRadioBtnLabel">Off</span>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{!this.isSynapseLinkEnabled() && (
|
||||||
|
<Stack className="panelGroupSpacing">
|
||||||
|
<Text variant="small">
|
||||||
|
Azure Synapse Link is required for creating an analytical store{" "}
|
||||||
|
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account.{" "}
|
||||||
|
<Link href="https://aka.ms/cosmosdb-synapselink" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
<DefaultButton
|
||||||
|
text="Enable"
|
||||||
|
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
||||||
|
style={{ height: 27, width: 80 }}
|
||||||
|
styles={{ label: { fontSize: 12 } }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PanelFooterComponent buttonLabel="OK" />
|
<PanelFooterComponent buttonLabel="OK" />
|
||||||
|
@ -648,24 +654,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCollectionName(): string {
|
private getPartitionKeyName(isLowerCase?: boolean): string {
|
||||||
switch (userContext.apiType) {
|
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||||
case "SQL":
|
|
||||||
return "Container";
|
|
||||||
case "Mongo":
|
|
||||||
return "Collection";
|
|
||||||
case "Cassandra":
|
|
||||||
case "Tables":
|
|
||||||
return "Table";
|
|
||||||
case "Gremlin":
|
|
||||||
return "Graph";
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported default experience type: ${userContext.apiType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartitionKeyName(): string {
|
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||||
return userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPartitionKeyPlaceHolder(): string {
|
private getPartitionKeyPlaceHolder(): string {
|
||||||
|
@ -774,6 +766,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let tooltipText = `The ${this.getPartitionKeyName(
|
||||||
|
true
|
||||||
|
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||||
|
|
||||||
|
if (userContext.apiType === "SQL") {
|
||||||
|
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return tooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAnalyticalStorageTooltipContent(): 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.{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/analytical-store-overview">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (this.isServerlessAccount()) {
|
if (this.isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -879,6 +899,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
|
||||||
|
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userContext.apiType === "Gremlin" &&
|
userContext.apiType === "Gremlin" &&
|
||||||
(this.state.partitionKey === "/id" || this.state.partitionKey === "/label")
|
(this.state.partitionKey === "/id" || this.state.partitionKey === "/label")
|
||||||
|
@ -905,6 +930,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
return Constants.AnalyticalStorageTtl.Disabled;
|
return Constants.AnalyticalStorageTtl.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private scrollToAdvancedSection(): void {
|
||||||
|
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
private async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
private async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -923,7 +952,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
|
||||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||||
? {
|
? {
|
||||||
paths: [partitionKeyString],
|
paths: [partitionKeyString],
|
||||||
|
|
|
@ -238,7 +238,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
userContext.portalEnv,
|
userContext.portalEnv,
|
||||||
this.isFreeTierAccount(),
|
this.isFreeTierAccount(),
|
||||||
this.container.isFirstResourceCreated(),
|
this.container.isFirstResourceCreated(),
|
||||||
userContext.apiType,
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtili
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
|
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import {
|
import {
|
||||||
|
@ -17,14 +18,12 @@ import {
|
||||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||||
export interface DeleteCollectionConfirmationPaneProps {
|
export interface DeleteCollectionConfirmationPaneProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
collectionName: string;
|
|
||||||
closePanel: () => void;
|
closePanel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||||
explorer,
|
explorer,
|
||||||
closePanel,
|
closePanel,
|
||||||
collectionName,
|
|
||||||
}: DeleteCollectionConfirmationPaneProps) => {
|
}: DeleteCollectionConfirmationPaneProps) => {
|
||||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||||
const [inputCollectionName, setInputCollectionName] = useState<string>("");
|
const [inputCollectionName, setInputCollectionName] = useState<string>("");
|
||||||
|
@ -34,6 +33,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||||
const shouldRecordFeedback = (): boolean => {
|
const shouldRecordFeedback = (): boolean => {
|
||||||
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
|
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
|
||||||
};
|
};
|
||||||
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
const paneTitle = "Delete " + collectionName;
|
const paneTitle = "Delete " + collectionName;
|
||||||
const submit = async (): Promise<void> => {
|
const submit = async (): Promise<void> => {
|
||||||
const collection = explorer.findSelectedCollection();
|
const collection = explorer.findSelectedCollection();
|
||||||
|
|
|
@ -11,11 +11,11 @@
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
& > * {
|
& > :not(.collapsibleSection) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
|
|
||||||
& > * {
|
& > :not(:last-child) {
|
||||||
margin-bottom: @SmallSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
width: @mediumFontSize;
|
width: @mediumFontSize;
|
||||||
margin: auto 0 auto @SmallSpace;
|
margin: auto 0 auto @SmallSpace;
|
||||||
color: @InfoIconColor;
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -49,10 +48,6 @@
|
||||||
font-size: @mediumFontSize;
|
font-size: @mediumFontSize;
|
||||||
padding: 0 @LargeSpace 0 @SmallSpace;
|
padding: 0 @LargeSpace 0 @SmallSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsibleSection {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +94,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelFooter {
|
.panelFooter {
|
||||||
padding: 20px 34px;
|
padding: 16px 34px;
|
||||||
border-top: solid 1px #bbbbbb;
|
border-top: solid 1px #bbbbbb;
|
||||||
|
|
||||||
& button {
|
& button {
|
||||||
|
@ -123,8 +118,8 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelGroupSpacing > * {
|
.panelGroupSpacing > :not(:last-child) {
|
||||||
margin-bottom: @SmallSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
.fileUpload {
|
.fileUpload {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
@ -170,3 +165,6 @@
|
||||||
.column-select-view {
|
.column-select-view {
|
||||||
margin: 20px 0px 0px 0px;
|
margin: 20px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
|
.panelSeparator::before {
|
||||||
|
background-color: #edebe9;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
|
||||||
import { Icon, Link, Stack, Text } from "office-ui-fabric-react";
|
import { Icon, Link, Stack, Text } from "office-ui-fabric-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export interface PanelInfoErrorProps {
|
export interface PanelInfoErrorProps {
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -23,7 +23,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="start">
|
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||||
{icon}
|
{icon}
|
||||||
<span className="panelWarningErrorDetailsLinkContainer">
|
<span className="panelWarningErrorDetailsLinkContainer">
|
||||||
<Text className="panelWarningErrorMessage" variant="small">
|
<Text className="panelWarningErrorMessage" variant="small">
|
||||||
|
|
|
@ -525,7 +525,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||||
<Stack
|
<Stack
|
||||||
className="panelInfoErrorContainer"
|
className="panelInfoErrorContainer"
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
verticalAlign="start"
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="ms-Stack panelInfoErrorContainer css-140"
|
className="ms-Stack panelInfoErrorContainer css-140"
|
||||||
|
|
|
@ -205,9 +205,8 @@ export default class Database implements ViewModels.Database {
|
||||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openAddCollection(database: Database, event: MouseEvent) {
|
public openAddCollection(database: Database) {
|
||||||
database.container.addCollectionPane.databaseId(database.id());
|
database.container.openAddCollectionPanel(database.id());
|
||||||
database.container.addCollectionPane.open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCollectionWithId(collectionId: string): ViewModels.Collection {
|
public findCollectionWithId(collectionId: string): ViewModels.Collection {
|
||||||
|
|
|
@ -197,7 +197,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
className: "databaseHeader",
|
className: "databaseHeader",
|
||||||
children: [],
|
children: [],
|
||||||
isSelected: () => this.isDataNodeSelected(database.id()),
|
isSelected: () => this.isDataNodeSelected(database.id()),
|
||||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container),
|
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()),
|
||||||
onClick: async (isExpanded) => {
|
onClick: async (isExpanded) => {
|
||||||
// Rewritten version of expandCollapseDatabase():
|
// Rewritten version of expandCollapseDatabase():
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
|
|
|
@ -115,6 +115,7 @@ export enum Action {
|
||||||
NotebooksGalleryFavoritesCount,
|
NotebooksGalleryFavoritesCount,
|
||||||
NotebooksGalleryPublishedCount,
|
NotebooksGalleryPublishedCount,
|
||||||
SelfServe,
|
SelfServe,
|
||||||
|
ExpandAddCollectionPaneAdvancedSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|
|
@ -19,7 +19,7 @@ interface UserContext {
|
||||||
readonly quotaId?: string;
|
readonly quotaId?: string;
|
||||||
// API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext
|
// API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext
|
||||||
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
|
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
|
||||||
apiType?: ApiType;
|
apiType: ApiType;
|
||||||
readonly isTryCosmosDBSubscription?: boolean;
|
readonly isTryCosmosDBSubscription?: boolean;
|
||||||
readonly portalEnv?: PortalEnv;
|
readonly portalEnv?: PortalEnv;
|
||||||
readonly features: Features;
|
readonly features: Features;
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
|
export const getCollectionName = (isPlural?: boolean): string => {
|
||||||
|
let collectionName: string;
|
||||||
|
let unknownApiType: never;
|
||||||
|
switch (userContext.apiType) {
|
||||||
|
case "SQL":
|
||||||
|
collectionName = "Container";
|
||||||
|
break;
|
||||||
|
case "Mongo":
|
||||||
|
collectionName = "Collection";
|
||||||
|
break;
|
||||||
|
case "Cassandra":
|
||||||
|
case "Tables":
|
||||||
|
collectionName = "Table";
|
||||||
|
break;
|
||||||
|
case "Gremlin":
|
||||||
|
collectionName = "Graph";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
unknownApiType = userContext.apiType;
|
||||||
|
throw new Error(`Unknown API type: ${unknownApiType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlural) {
|
||||||
|
collectionName += "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionName;
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import * as Constants from "../Shared/Constants";
|
import * as Constants from "../Shared/Constants";
|
||||||
|
import { getCollectionName } from "../Utils/APITypeUtils";
|
||||||
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
||||||
|
|
||||||
interface ComputeRUUsagePriceHourlyArgs {
|
interface ComputeRUUsagePriceHourlyArgs {
|
||||||
|
@ -10,7 +11,7 @@ interface ComputeRUUsagePriceHourlyArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const estimatedCostDisclaimer =
|
export const estimatedCostDisclaimer =
|
||||||
"*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account";
|
"This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anything that is not a number should return 0
|
* Anything that is not a number should return 0
|
||||||
|
@ -161,7 +162,7 @@ export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDat
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource: string = isDatabaseThroughput ? "database" : "container";
|
const resource: string = isDatabaseThroughput ? "database" : getCollectionName().toLocaleLowerCase();
|
||||||
return `Your ${resource} throughput will automatically scale from <b>${AutoPilotUtils.getMinRUsBasedOnUserInput(
|
return `Your ${resource} throughput will automatically scale from <b>${AutoPilotUtils.getMinRUsBasedOnUserInput(
|
||||||
maxAutoPilotThroughputSet
|
maxAutoPilotThroughputSet
|
||||||
)} RU/s (10% of max RU/s) - ${maxAutoPilotThroughputSet} RU/s</b> based on usage. <br /><br />After the first ${AutoPilotUtils.getStorageBasedOnUserInput(
|
)} RU/s (10% of max RU/s) - ${maxAutoPilotThroughputSet} RU/s</b> based on usage. <br /><br />After the first ${AutoPilotUtils.getStorageBasedOnUserInput(
|
||||||
|
@ -227,7 +228,7 @@ export function getEstimatedSpendHtml(
|
||||||
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
|
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
|
||||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
|
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
|
||||||
`<p style='padding: 10px 0px 0px 0px;'>` +
|
`<p style='padding: 10px 0px 0px 0px;'>` +
|
||||||
`<em>${estimatedCostDisclaimer}</em></p>`
|
`<em>*${estimatedCostDisclaimer}</em></p>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,11 +262,10 @@ export function getUpsellMessage(
|
||||||
serverId = "default",
|
serverId = "default",
|
||||||
isFreeTier = false,
|
isFreeTier = false,
|
||||||
isFirstResourceCreated = false,
|
isFirstResourceCreated = false,
|
||||||
defaultExperience: string,
|
|
||||||
isCollection: boolean
|
isCollection: boolean
|
||||||
): string {
|
): string {
|
||||||
if (isFreeTier) {
|
if (isFreeTier) {
|
||||||
const collectionName = getCollectionName(defaultExperience);
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
const resourceType = isCollection ? collectionName : "database";
|
const resourceType = isCollection ? collectionName : "database";
|
||||||
return isFirstResourceCreated
|
return isFirstResourceCreated
|
||||||
? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.`
|
? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.`
|
||||||
|
@ -277,22 +277,8 @@ export function getUpsellMessage(
|
||||||
price = Constants.OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice;
|
price = Constants.OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple containers included`;
|
return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple ${getCollectionName(
|
||||||
}
|
true
|
||||||
}
|
).toLocaleLowerCase()} included`;
|
||||||
|
|
||||||
export function getCollectionName(defaultExperience: string): string {
|
|
||||||
switch (defaultExperience) {
|
|
||||||
case "SQL":
|
|
||||||
return "container";
|
|
||||||
case "Mongo":
|
|
||||||
return "collection";
|
|
||||||
case "Tables":
|
|
||||||
case "Cassandra":
|
|
||||||
return "table";
|
|
||||||
case "Gremlin":
|
|
||||||
return "graph";
|
|
||||||
default:
|
|
||||||
throw Error("unknown API type");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,15 +16,10 @@ test("Mongo CRUD", async () => {
|
||||||
|
|
||||||
// Create new database and collection
|
// Create new database and collection
|
||||||
await explorer.click('[data-test="New Collection"]');
|
await explorer.click('[data-test="New Collection"]');
|
||||||
await explorer.click('[data-test="addCollection-newDatabaseId"]');
|
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||||
await explorer.fill('[data-test="addCollection-newDatabaseId"]', databaseId);
|
await explorer.fill('[aria-label="Collection id"]', containerId);
|
||||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
await explorer.fill('[aria-label="Shard key"]', "/pk");
|
||||||
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
|
await explorer.click("#sidePanelOkButton");
|
||||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
|
||||||
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
|
|
||||||
await explorer.click('[data-test="addCollection-partitionKeyValue"]');
|
|
||||||
await explorer.fill('[data-test="addCollection-partitionKeyValue"]', "/pk");
|
|
||||||
await explorer.click('[data-test="addCollection-createCollection"]');
|
|
||||||
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
|
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
|
||||||
await safeClick(explorer, `.nodeItem >> text=${containerId}`);
|
await safeClick(explorer, `.nodeItem >> text=${containerId}`);
|
||||||
// Create indexing policy
|
// Create indexing policy
|
||||||
|
|
|
@ -15,15 +15,10 @@ test("SQL CRUD", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await explorer.click('[data-test="New Container"]');
|
await explorer.click('[data-test="New Container"]');
|
||||||
await explorer.click('[data-test="addCollection-newDatabaseId"]');
|
await explorer.fill('[aria-label="New database id"]', databaseId);
|
||||||
await explorer.fill('[data-test="addCollection-newDatabaseId"]', databaseId);
|
await explorer.fill('[aria-label="Container id"]', containerId);
|
||||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
await explorer.fill('[aria-label="Partition key"]', "/pk");
|
||||||
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
|
await explorer.click("#sidePanelOkButton");
|
||||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
|
||||||
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
|
|
||||||
await explorer.click('[data-test="addCollection-partitionKeyValue"]');
|
|
||||||
await explorer.fill('[data-test="addCollection-partitionKeyValue"]', "/pk");
|
|
||||||
await explorer.click('[data-test="addCollection-createCollection"]');
|
|
||||||
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
|
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
|
||||||
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
|
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
|
||||||
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")');
|
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")');
|
||||||
|
|
|
@ -15,9 +15,8 @@ test("Tables CRUD", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await explorer.click('[data-test="New Table"]');
|
await explorer.click('[data-test="New Table"]');
|
||||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
await explorer.fill('[aria-label="Table id"]', tableId);
|
||||||
await explorer.fill('[data-test="addCollection-collectionId"]', tableId);
|
await explorer.click("#sidePanelOkButton");
|
||||||
await explorer.click('[data-test="addCollection-createCollection"]');
|
|
||||||
await safeClick(explorer, `[data-test="TablesDB"]`);
|
await safeClick(explorer, `[data-test="TablesDB"]`);
|
||||||
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
|
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
|
||||||
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
|
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
|
||||||
|
|
Loading…
Reference in New Issue