mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-24 22:46:40 +00:00
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)
|
||||
*/
|
||||
export class ResourceTreeContextMenuButtonFactory {
|
||||
public static createDatabaseContextMenu(container: Explorer): TreeNodeMenuItem[] {
|
||||
public static createDatabaseContextMenu(container: Explorer, databaseId: string): TreeNodeMenuItem[] {
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked(),
|
||||
onClick: () => container.onNewCollectionClicked(databaseId),
|
||||
label: container.addCollectionText(),
|
||||
},
|
||||
];
|
||||
|
@ -5,6 +5,7 @@ import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
export interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
isExpandedByDefault: boolean;
|
||||
onExpand?: () => void;
|
||||
}
|
||||
|
||||
export interface CollapsibleSectionState {
|
||||
@ -23,6 +24,12 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
};
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
if (this.state.isExpanded && this.props.onExpand) {
|
||||
this.props.onExpand();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
|
@ -415,7 +415,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
</Text>
|
||||
<Text>
|
||||
<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>
|
||||
</Text>
|
||||
</Stack>
|
||||
@ -689,7 +689,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
</Text>
|
||||
<Text>
|
||||
<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>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
@ -150,7 +150,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
</Text>
|
||||
<Text>
|
||||
<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>
|
||||
</Text>
|
||||
</Stack>
|
||||
|
@ -11,10 +11,6 @@
|
||||
padding: 0 @LargeSpace 0 @SmallSpace;
|
||||
}
|
||||
|
||||
.throughputInputSpacing {
|
||||
margin-bottom: @SmallSpace;
|
||||
|
||||
& > * {
|
||||
margin-bottom: @SmallSpace;
|
||||
}
|
||||
.throughputInputSpacing > :not(:last-child) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ import React from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
|
||||
export interface ThroughputInputProps {
|
||||
isDatabase: boolean;
|
||||
isSharded: boolean;
|
||||
showFreeTierExceedThroughputTooltip: boolean;
|
||||
setThroughputValue: (throughput: number) => void;
|
||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||
@ -18,6 +20,7 @@ export interface ThroughputInputState {
|
||||
isAutoscaleSelected: boolean;
|
||||
throughput: number;
|
||||
isCostAcknowledged: boolean;
|
||||
throughputError: string;
|
||||
}
|
||||
|
||||
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> {
|
||||
@ -28,6 +31,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
isAutoscaleSelected: true,
|
||||
throughput: AutoPilotUtils.minAutoPilotThroughput,
|
||||
isCostAcknowledged: false,
|
||||
throughputError: undefined,
|
||||
};
|
||||
|
||||
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||
@ -39,11 +43,11 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
<div className="throughputInputContainer throughputInputSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small" style={{ lineHeight: "20px" }}>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
{this.getThroughputLabelText()}
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
|
||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -74,7 +78,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
{this.state.isAutoscaleSelected && (
|
||||
<Stack className="throughputInputSpacing">
|
||||
<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/">
|
||||
capacity calculator
|
||||
</Link>
|
||||
@ -82,11 +86,11 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
</Text>
|
||||
|
||||
<Stack horizontal>
|
||||
<Text variant="small" style={{ lineHeight: "20px" }}>
|
||||
Max RU/s
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
{this.props.isDatabase ? "Database" : getCollectionName()} max RU/s
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
|
||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -101,11 +105,12 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
value={this.state.throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
required={true}
|
||||
errorMessage={this.state.throughputError}
|
||||
/>
|
||||
|
||||
<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>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
|
||||
{this.state.throughput} RU/s
|
||||
@ -147,6 +152,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
value={this.state.throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
required={true}
|
||||
errorMessage={this.state.throughputError}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@ -156,6 +162,7 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
|
||||
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
|
||||
<Stack horizontal verticalAlign="start">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Checkbox
|
||||
checked={this.state.isCostAcknowledged}
|
||||
styles={{
|
||||
@ -177,30 +184,35 @@ export class ThroughputInput extends React.Component<ThroughputInputProps, Throu
|
||||
}
|
||||
|
||||
private getThroughputLabelText(): string {
|
||||
let throughputHeaderText: string;
|
||||
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();
|
||||
const maxRU: string = userContext.isTryCosmosDBSubscription
|
||||
? Constants.TryCosmosExperience.maxRU.toLocaleString()
|
||||
: "unlimited";
|
||||
return this.state.isAutoscaleSelected
|
||||
? AutoPilotUtils.getAutoPilotHeaderText()
|
||||
: `Throughput (${minRU} - ${maxRU} RU/s)`;
|
||||
return `${this.props.isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
|
||||
}
|
||||
|
||||
private onThroughputValueChange(newInput: string): void {
|
||||
const newThroughput = parseInt(newInput);
|
||||
this.setState({ throughput: 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 {
|
||||
return `After the first ${AutoPilotUtils.getStorageBasedOnUserInput(
|
||||
this.state.throughput
|
||||
)} GB of data stored, the max
|
||||
RU/s will be automatically upgraded based on the new storage value.`;
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
return `Set the max RU/s to the highest RU/s you want your ${collectionName} to scale to. The ${collectionName} will scale between 10% of max RU/s to the max RU/s based on usage.`;
|
||||
}
|
||||
|
||||
private getCostAcknowledgeText(): string {
|
||||
@ -271,10 +283,20 @@ const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props:
|
||||
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * 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) {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Estimated monthly cost ({currency}):{" "}
|
||||
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
|
||||
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
@ -287,7 +309,7 @@ const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props:
|
||||
|
||||
return (
|
||||
<Text variant="small">
|
||||
Cost ({currency}):{" "}
|
||||
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "}
|
||||
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "}
|
||||
@ -295,8 +317,6 @@ const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props:
|
||||
</b>
|
||||
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
|
||||
{currencySign + pricePerRu}/RU)
|
||||
<br />
|
||||
<em>{PricingUtils.estimatedCostDisclaimer}</em>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
@ -30,12 +30,12 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName } from "../Utils/APITypeUtils";
|
||||
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PricingUtils from "../Utils/PricingUtils";
|
||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
|
||||
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") {
|
||||
this.cassandraAddCollectionPane.open();
|
||||
} else if (userContext.features.enableReactPane) {
|
||||
this.openAddCollectionPanel();
|
||||
} else {
|
||||
} else if (userContext.features.enableKOPanel) {
|
||||
this.addCollectionPane.open(this.selectedDatabaseId());
|
||||
document.getElementById("linkAddCollection").focus();
|
||||
} else {
|
||||
this.openAddCollectionPanel(databaseId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2061,14 +2061,9 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public openDeleteCollectionConfirmationPane(): void {
|
||||
let collectionName = PricingUtils.getCollectionName(userContext.apiType);
|
||||
this.openSidePanel(
|
||||
"Delete " + collectionName,
|
||||
<DeleteCollectionConfirmationPane
|
||||
explorer={this}
|
||||
collectionName={collectionName}
|
||||
closePanel={this.closeSidePanel}
|
||||
/>
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane explorer={this} 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();
|
||||
this.openSidePanel(
|
||||
"New Collection",
|
||||
"New " + getCollectionName(),
|
||||
<AddCollectionPanel
|
||||
explorer={this}
|
||||
closePanel={() => this.closeSidePanel()}
|
||||
openNotificationConsole={() => this.expandConsole()}
|
||||
databaseId={databaseId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { ActionContracts } from "../Contracts/ExplorerContracts";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "./Explorer";
|
||||
import { handleOpenAction } from "./OpenActions";
|
||||
import AddCollectionPane from "./Panes/AddCollectionPane";
|
||||
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
|
||||
describe("OpenActions", () => {
|
||||
@ -15,8 +14,7 @@ describe("OpenActions", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = {} as Explorer;
|
||||
explorer.addCollectionPane = {} as AddCollectionPane;
|
||||
explorer.addCollectionPane.open = jest.fn();
|
||||
explorer.onNewCollectionClicked = jest.fn();
|
||||
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
|
||||
explorer.cassandraAddCollectionPane.open = jest.fn();
|
||||
explorer.closeAllPanes = () => {};
|
||||
@ -90,24 +88,24 @@ describe("OpenActions", () => {
|
||||
});
|
||||
|
||||
describe("AddCollection pane kind", () => {
|
||||
it("string value should call addCollectionPane.open", () => {
|
||||
it("string value should call explorer.onNewCollectionClicked", () => {
|
||||
const action = {
|
||||
actionType: "OpenPane",
|
||||
paneKind: "AddCollection",
|
||||
};
|
||||
|
||||
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 = {
|
||||
actionType: "OpenPane",
|
||||
paneKind: ActionContracts.PaneKind.AddCollection,
|
||||
};
|
||||
|
||||
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]
|
||||
) {
|
||||
explorer.closeAllPanes();
|
||||
explorer.addCollectionPane.open();
|
||||
explorer.onNewCollectionClicked();
|
||||
} else if (
|
||||
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
||||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
||||
|
@ -143,7 +143,6 @@
|
||||
size="40"
|
||||
class="collid"
|
||||
data-bind="visible: databaseCreateNew, textInput: databaseId, hasFocus: firstFieldHasFocus"
|
||||
aria-label="Database id"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
@ -161,7 +160,6 @@
|
||||
size="40"
|
||||
class="collid"
|
||||
data-bind="visible: !databaseCreateNew(), textInput: databaseId, hasFocus: firstFieldHasFocus"
|
||||
aria-label="Database id"
|
||||
/>
|
||||
|
||||
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
|
||||
@ -246,7 +244,7 @@
|
||||
placeholder="e.g., Container1"
|
||||
size="40"
|
||||
class="textfontclr collid"
|
||||
data-bind="value: collectionId, attr: { 'aria-label': collectionIdTitle }"
|
||||
data-bind="value: collectionId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -352,7 +350,6 @@
|
||||
attr: {
|
||||
placeholder: partitionKeyPlaceholder,
|
||||
required: partitionKeyVisible(),
|
||||
'aria-label': partitionKeyName,
|
||||
pattern: partitionKeyPattern,
|
||||
title: partitionKeyTitle
|
||||
}"
|
||||
|
@ -476,7 +476,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
userContext.portalEnv,
|
||||
this.isFreeTierAccount(),
|
||||
this.container.isFirstResourceCreated(),
|
||||
userContext.apiType,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
IconButton,
|
||||
IDropdownOption,
|
||||
Link,
|
||||
Separator,
|
||||
Stack,
|
||||
Text,
|
||||
TooltipHost,
|
||||
@ -23,6 +24,7 @@ import { CollectionCreation, IndexingPolicies } from "../../Shared/Constants";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getCollectionName } from "../../Utils/APITypeUtils";
|
||||
import { getUpsellMessage } from "../../Utils/PricingUtils";
|
||||
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
|
||||
@ -35,6 +37,7 @@ export interface AddCollectionPanelProps {
|
||||
explorer: Explorer;
|
||||
closePanel: () => void;
|
||||
openNotificationConsole: () => void;
|
||||
databaseId?: string;
|
||||
}
|
||||
|
||||
export interface AddCollectionPanelState {
|
||||
@ -48,7 +51,7 @@ export interface AddCollectionPanelState {
|
||||
partitionKey: string;
|
||||
enableDedicatedThroughput: boolean;
|
||||
createMongoWildCardIndex: boolean;
|
||||
useHashV1: boolean;
|
||||
useHashV2: boolean;
|
||||
enableAnalyticalStore: boolean;
|
||||
uniqueKeys: string[];
|
||||
errorMessage: string;
|
||||
@ -67,17 +70,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
createNewDatabase: userContext.apiType !== "Tables",
|
||||
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
|
||||
newDatabaseId: "",
|
||||
isSharedThroughputChecked: this.getSharedThroughputDefault(),
|
||||
selectedDatabaseId: userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : undefined,
|
||||
selectedDatabaseId:
|
||||
userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : this.props.databaseId,
|
||||
collectionId: "",
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: "",
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex: true,
|
||||
useHashV1: false,
|
||||
useHashV2: false,
|
||||
enableAnalyticalStore: false,
|
||||
uniqueKeys: [],
|
||||
errorMessage: "",
|
||||
@ -100,13 +104,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{!this.state.errorMessage && this.isFreeTierAccount() && (
|
||||
<PanelInfoErrorComponent
|
||||
message={getUpsellMessage(
|
||||
userContext.portalEnv,
|
||||
true,
|
||||
this.props.explorer.isFirstResourceCreated(),
|
||||
userContext.apiType,
|
||||
true
|
||||
)}
|
||||
message={getUpsellMessage(userContext.portalEnv, true, this.props.explorer.isFirstResourceCreated(), true)}
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
openNotificationConsole={this.props.openNotificationConsole}
|
||||
@ -120,13 +118,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Database id
|
||||
Database {userContext.apiType === "Mongo" ? "name" : "id"}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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>
|
||||
</Stack>
|
||||
|
||||
@ -140,7 +140,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
data-test="addCollection-createNewDatabase"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
@ -154,8 +153,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseUseExisting"
|
||||
data-test="addCollection-existingDatabase"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
@ -166,8 +163,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack className="panelGroupSpacing">
|
||||
<input
|
||||
name="newDatabaseId"
|
||||
id="databaseId"
|
||||
data-test="addCollection-newDatabaseId"
|
||||
id="newDatabaseId"
|
||||
aria-required
|
||||
required
|
||||
type="text"
|
||||
@ -177,7 +173,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
placeholder="Type a new database id"
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label="Database id"
|
||||
aria-label="New database id"
|
||||
autoFocus
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@ -188,7 +184,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{!this.isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label="Provision database throughput"
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
@ -201,9 +197,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
<TooltipHost
|
||||
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>
|
||||
</Stack>
|
||||
)}
|
||||
@ -214,6 +212,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||
}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
|
||||
@ -230,38 +229,40 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
|
||||
this.setState({ selectedDatabaseId: database.key as string })
|
||||
}
|
||||
defaultSelectedKey={this.props.databaseId}
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{`${this.getCollectionName()} id`}
|
||||
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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>
|
||||
</Stack>
|
||||
|
||||
<input
|
||||
name="collectionId"
|
||||
id="containerId"
|
||||
data-test="addCollection-collectionId"
|
||||
id="collectionId"
|
||||
type="text"
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
title="May not end with space nor contain characters '\' '/' '#' '?'"
|
||||
placeholder={`e.g., ${this.getCollectionName()}1`}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label={`${this.getCollectionName()} id`}
|
||||
aria-label={`${getCollectionName()} id`}
|
||||
value={this.state.collectionId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ collectionId: event.target.value })
|
||||
@ -320,13 +321,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Sharding options
|
||||
Sharding
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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>
|
||||
</Stack>
|
||||
|
||||
@ -371,18 +374,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`The ${this.getPartitionKeyName()} is used to automatically partition data among
|
||||
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.`}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
id="addCollection-partitionKeyValue"
|
||||
data-test="addCollection-partitionKeyValue"
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
@ -392,9 +392,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
|
||||
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
|
||||
value={this.state.partitionKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ partitionKey: event.target.value })
|
||||
}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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>
|
||||
)}
|
||||
@ -402,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={`Provision dedicated throughput for this ${this.getCollectionName()}`}
|
||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||
checked={this.state.enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
@ -415,12 +423,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content="You can optionally provision dedicated throughput for a container within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other containers in the database and
|
||||
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."
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
>
|
||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@ -431,6 +441,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
|
||||
}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||
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
|
||||
per partition key."
|
||||
>
|
||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@ -504,134 +515,129 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<CollapsibleSectionComponent title="Advanced" isExpandedByDefault={false}>
|
||||
<Stack className="panelGroupSpacing">
|
||||
{this.props.explorer.isEnableMongoCapabilityPresent() && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content="By default, only the field _id is indexed. Creating a wildcard index on all fields will quickly optimize
|
||||
query performance and is recommended during development."
|
||||
>
|
||||
<Icon iconName="InfoSolid" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
{userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
this.scrollToAdvancedSection();
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
||||
{this.props.explorer.isEnableMongoCapabilityPresent() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
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."
|
||||
>
|
||||
<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
|
||||
label="Create a Wildcard Index on all fields"
|
||||
checked={this.state.createMongoWildCardIndex}
|
||||
label="My partition key is larger than 100 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
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 })
|
||||
this.setState({ useHashV2: isChecked })
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal verticalAlign="start">
|
||||
<Checkbox
|
||||
checked={this.state.useHashV1}
|
||||
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>
|
||||
{this.shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical store
|
||||
</Text>
|
||||
<DefaultButton
|
||||
text="Enable"
|
||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
||||
style={{ height: 27, width: 80 }}
|
||||
styles={{ label: { fontSize: 12 } }}
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
|
||||
<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{" "}
|
||||
{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>
|
||||
|
||||
<PanelFooterComponent buttonLabel="OK" />
|
||||
@ -648,24 +654,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}));
|
||||
}
|
||||
|
||||
private getCollectionName(): string {
|
||||
switch (userContext.apiType) {
|
||||
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(isLowerCase?: boolean): string {
|
||||
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
|
||||
private getPartitionKeyName(): string {
|
||||
return userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||
}
|
||||
|
||||
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.";
|
||||
}
|
||||
|
||||
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 {
|
||||
if (this.isServerlessAccount()) {
|
||||
return false;
|
||||
@ -879,6 +899,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
|
||||
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
userContext.apiType === "Gremlin" &&
|
||||
(this.state.partitionKey === "/id" || this.state.partitionKey === "/label")
|
||||
@ -905,6 +930,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return Constants.AnalyticalStorageTtl.Disabled;
|
||||
}
|
||||
|
||||
private scrollToAdvancedSection(): void {
|
||||
document.getElementById("collapsibleSectionContent")?.scrollIntoView();
|
||||
}
|
||||
|
||||
private async submit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
@ -923,7 +952,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
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
|
||||
? {
|
||||
paths: [partitionKeyString],
|
||||
|
@ -238,7 +238,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
userContext.portalEnv,
|
||||
this.isFreeTierAccount(),
|
||||
this.container.isFirstResourceCreated(),
|
||||
userContext.apiType,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtili
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import {
|
||||
@ -17,14 +18,12 @@ import {
|
||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
export interface DeleteCollectionConfirmationPaneProps {
|
||||
explorer: Explorer;
|
||||
collectionName: string;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||
explorer,
|
||||
closePanel,
|
||||
collectionName,
|
||||
}: DeleteCollectionConfirmationPaneProps) => {
|
||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||
const [inputCollectionName, setInputCollectionName] = useState<string>("");
|
||||
@ -34,6 +33,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
const shouldRecordFeedback = (): boolean => {
|
||||
return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared();
|
||||
};
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const submit = async (): Promise<void> => {
|
||||
const collection = explorer.findSelectedCollection();
|
||||
|
@ -11,11 +11,11 @@
|
||||
margin: 20px 0;
|
||||
overflow: auto;
|
||||
|
||||
& > * {
|
||||
& > :not(.collapsibleSection) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
|
||||
& > * {
|
||||
margin-bottom: @SmallSpace;
|
||||
& > :not(:last-child) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@
|
||||
font-size: @mediumFontSize;
|
||||
width: @mediumFontSize;
|
||||
margin: auto 0 auto @SmallSpace;
|
||||
color: @InfoIconColor;
|
||||
cursor: default;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@ -49,10 +48,6 @@
|
||||
font-size: @mediumFontSize;
|
||||
padding: 0 @LargeSpace 0 @SmallSpace;
|
||||
}
|
||||
|
||||
.collapsibleSection {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +94,7 @@
|
||||
}
|
||||
|
||||
.panelFooter {
|
||||
padding: 20px 34px;
|
||||
padding: 16px 34px;
|
||||
border-top: solid 1px #bbbbbb;
|
||||
|
||||
& button {
|
||||
@ -123,8 +118,8 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panelGroupSpacing > * {
|
||||
margin-bottom: @SmallSpace;
|
||||
.panelGroupSpacing > :not(:last-child) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
.fileUpload {
|
||||
display: none !important;
|
||||
@ -170,3 +165,6 @@
|
||||
.column-select-view {
|
||||
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 React from "react";
|
||||
|
||||
export interface PanelInfoErrorProps {
|
||||
message: string;
|
||||
@ -23,7 +23,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="start">
|
||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||
{icon}
|
||||
<span className="panelWarningErrorDetailsLinkContainer">
|
||||
<Text className="panelWarningErrorMessage" variant="small">
|
||||
|
@ -525,7 +525,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
<Stack
|
||||
className="panelInfoErrorContainer"
|
||||
horizontal={true}
|
||||
verticalAlign="start"
|
||||
verticalAlign="center"
|
||||
>
|
||||
<div
|
||||
className="ms-Stack panelInfoErrorContainer css-140"
|
||||
|
@ -205,9 +205,8 @@ export default class Database implements ViewModels.Database {
|
||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||
}
|
||||
|
||||
public openAddCollection(database: Database, event: MouseEvent) {
|
||||
database.container.addCollectionPane.databaseId(database.id());
|
||||
database.container.addCollectionPane.open();
|
||||
public openAddCollection(database: Database) {
|
||||
database.container.openAddCollectionPanel(database.id());
|
||||
}
|
||||
|
||||
public findCollectionWithId(collectionId: string): ViewModels.Collection {
|
||||
|
@ -197,7 +197,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
className: "databaseHeader",
|
||||
children: [],
|
||||
isSelected: () => this.isDataNodeSelected(database.id()),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container),
|
||||
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database.id()),
|
||||
onClick: async (isExpanded) => {
|
||||
// Rewritten version of expandCollapseDatabase():
|
||||
if (isExpanded) {
|
||||
|
@ -115,6 +115,7 @@ export enum Action {
|
||||
NotebooksGalleryFavoritesCount,
|
||||
NotebooksGalleryPublishedCount,
|
||||
SelfServe,
|
||||
ExpandAddCollectionPaneAdvancedSection,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
@ -19,7 +19,7 @@ interface UserContext {
|
||||
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
|
||||
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
|
||||
apiType?: ApiType;
|
||||
apiType: ApiType;
|
||||
readonly isTryCosmosDBSubscription?: boolean;
|
||||
readonly portalEnv?: PortalEnv;
|
||||
readonly features: Features;
|
||||
|
30
src/Utils/APITypeUtils.ts
Normal file
30
src/Utils/APITypeUtils.ts
Normal file
@ -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 { getCollectionName } from "../Utils/APITypeUtils";
|
||||
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
||||
|
||||
interface ComputeRUUsagePriceHourlyArgs {
|
||||
@ -10,7 +11,7 @@ interface ComputeRUUsagePriceHourlyArgs {
|
||||
}
|
||||
|
||||
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
|
||||
@ -161,7 +162,7 @@ export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDat
|
||||
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(
|
||||
maxAutoPilotThroughputSet
|
||||
)} 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> ` +
|
||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
|
||||
`<p style='padding: 10px 0px 0px 0px;'>` +
|
||||
`<em>${estimatedCostDisclaimer}</em></p>`
|
||||
`<em>*${estimatedCostDisclaimer}</em></p>`
|
||||
);
|
||||
}
|
||||
|
||||
@ -261,11 +262,10 @@ export function getUpsellMessage(
|
||||
serverId = "default",
|
||||
isFreeTier = false,
|
||||
isFirstResourceCreated = false,
|
||||
defaultExperience: string,
|
||||
isCollection: boolean
|
||||
): string {
|
||||
if (isFreeTier) {
|
||||
const collectionName = getCollectionName(defaultExperience);
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const resourceType = isCollection ? collectionName : "database";
|
||||
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.`
|
||||
@ -277,22 +277,8 @@ export function getUpsellMessage(
|
||||
price = Constants.OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice;
|
||||
}
|
||||
|
||||
return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple containers 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");
|
||||
return `Start at ${getCurrencySign(serverId)}${price}/mo per database, multiple ${getCollectionName(
|
||||
true
|
||||
).toLocaleLowerCase()} included`;
|
||||
}
|
||||
}
|
||||
|
@ -16,15 +16,10 @@ test("Mongo CRUD", async () => {
|
||||
|
||||
// Create new database and collection
|
||||
await explorer.click('[data-test="New Collection"]');
|
||||
await explorer.click('[data-test="addCollection-newDatabaseId"]');
|
||||
await explorer.fill('[data-test="addCollection-newDatabaseId"]', databaseId);
|
||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
||||
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
|
||||
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 explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
await explorer.fill('[aria-label="Collection id"]', containerId);
|
||||
await explorer.fill('[aria-label="Shard key"]', "/pk");
|
||||
await explorer.click("#sidePanelOkButton");
|
||||
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
|
||||
await safeClick(explorer, `.nodeItem >> text=${containerId}`);
|
||||
// Create indexing policy
|
||||
|
@ -15,15 +15,10 @@ test("SQL CRUD", async () => {
|
||||
});
|
||||
|
||||
await explorer.click('[data-test="New Container"]');
|
||||
await explorer.click('[data-test="addCollection-newDatabaseId"]');
|
||||
await explorer.fill('[data-test="addCollection-newDatabaseId"]', databaseId);
|
||||
await explorer.click('[data-test="addCollection-collectionId"]');
|
||||
await explorer.fill('[data-test="addCollection-collectionId"]', containerId);
|
||||
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 explorer.fill('[aria-label="New database id"]', databaseId);
|
||||
await explorer.fill('[aria-label="Container id"]', containerId);
|
||||
await explorer.fill('[aria-label="Partition key"]', "/pk");
|
||||
await explorer.click("#sidePanelOkButton");
|
||||
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
|
||||
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
|
||||
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="addCollection-collectionId"]');
|
||||
await explorer.fill('[data-test="addCollection-collectionId"]', tableId);
|
||||
await explorer.click('[data-test="addCollection-createCollection"]');
|
||||
await explorer.fill('[aria-label="Table id"]', tableId);
|
||||
await explorer.click("#sidePanelOkButton");
|
||||
await safeClick(explorer, `[data-test="TablesDB"]`);
|
||||
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
|
||||
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
|
||||
|
Loading…
Reference in New Issue
Block a user