diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts index 2a7576610..5f860336b 100644 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ b/src/Explorer/ContextMenuButtonFactory.ts @@ -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(), }, ]; diff --git a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx index 9b96f36f4..134fcc7c3 100644 --- a/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx +++ b/src/Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent.tsx @@ -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 diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap index accc10398..945bd0644 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/__snapshots__/ThroughputInputAutoPilotV3Component.test.tsx.snap @@ -415,7 +415,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = ` - *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 @@ -689,7 +689,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = ` - *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 diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 474866fb9..2f4c53dd3 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -150,7 +150,7 @@ exports[`SettingsUtils functions render 1`] = ` - *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 diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.less b/src/Explorer/Controls/ThroughputInput/ThroughputInput.less index dec618d7b..0eed71bec 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.less +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.less @@ -11,10 +11,6 @@ padding: 0 @LargeSpace 0 @SmallSpace; } -.throughputInputSpacing { - margin-bottom: @SmallSpace; - - & > * { - margin-bottom: @SmallSpace; - } +.throughputInputSpacing > :not(:last-child) { + margin-bottom: @DefaultSpace; } diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 681b2d1c3..a28c336f8 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -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 { @@ -28,6 +31,7 @@ export class ThroughputInput extends React.Component - + {this.getThroughputLabelText()} - + @@ -74,7 +78,7 @@ export class ThroughputInput extends React.Component - Provision maximum RU/s required by this resource. Estimate your required RU/s with  + Estimate your required RU/s with  capacity calculator @@ -82,11 +86,11 @@ export class ThroughputInput extends React.Component - - Max RU/s + + {this.props.isDatabase ? "Database" : getCollectionName()} max RU/s - + @@ -101,11 +105,12 @@ export class ThroughputInput extends React.Component - Your {this.props.isDatabase ? "database" : "container"} throughput will automatically scale from{" "} + Your {this.props.isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will + automatically scale from{" "} {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 @@ -156,6 +162,7 @@ export class ThroughputInput extends React.Component SharedConstants.CollectionCreation.DefaultCollectionRUs100K && ( + 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 = (props: ? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier : PricingUtils.getPricePerRu(serverId) * multiplier; + const iconWithEstimatedCostDisclaimer: JSX.Element = ( + + + + ); + if (isAutoscale) { return ( - Estimated monthly cost ({currency}):{" "} + Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} {currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "} {currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "} @@ -287,7 +309,7 @@ const CostEstimateText: React.FunctionComponent = (props: return ( - Cost ({currency}):{" "} + Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} {currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "} {currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "} @@ -295,8 +317,6 @@ const CostEstimateText: React.FunctionComponent = (props: ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "} {currencySign + pricePerRu}/RU) -
- {PricingUtils.estimatedCostDisclaimer}
); }; diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 3c9ce8659..e044581f3 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -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, - + "Delete " + getCollectionName(), + ); } @@ -2106,14 +2101,15 @@ export default class Explorer { ); } - public async openAddCollectionPanel(): Promise { + public async openAddCollectionPanel(databaseId?: string): Promise { await this.loadDatabaseOffers(); this.openSidePanel( - "New Collection", + "New " + getCollectionName(), this.closeSidePanel()} openNotificationConsole={() => this.expandConsole()} + databaseId={databaseId} /> ); } diff --git a/src/Explorer/OpenActions.test.ts b/src/Explorer/OpenActions.test.ts index 47a3003af..e8113455f 100644 --- a/src/Explorer/OpenActions.test.ts +++ b/src/Explorer/OpenActions.test.ts @@ -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(); }); }); }); diff --git a/src/Explorer/OpenActions.ts b/src/Explorer/OpenActions.ts index 4742978cc..8a7c87f53 100644 --- a/src/Explorer/OpenActions.ts +++ b/src/Explorer/OpenActions.ts @@ -141,7 +141,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) { (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection] ) { explorer.closeAllPanes(); - explorer.addCollectionPane.open(); + explorer.onNewCollectionClicked(); } else if ( action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || (action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index ca03d9619..023305132 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -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" /> @@ -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" /> @@ -352,7 +350,6 @@ attr: { placeholder: partitionKeyPlaceholder, required: partitionKeyVisible(), - 'aria-label': partitionKeyName, pattern: partitionKeyPattern, title: partitionKeyTitle }" diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index e8e1eaf6b..8085d885a 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -476,7 +476,6 @@ export default class AddCollectionPane extends ContextualPaneBase { userContext.portalEnv, this.isFreeTierAccount(), this.container.isFirstResourceCreated(), - userContext.apiType, true ); }); diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index fdbb8c931..271de5d69 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -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 - Database id + Database {userContext.apiType === "Mongo" ? "name" : "id"} - +
@@ -140,7 +140,6 @@ export class AddCollectionPanel extends React.Component @@ -154,8 +153,6 @@ export class AddCollectionPanel extends React.Component @@ -166,8 +163,7 @@ export class AddCollectionPanel extends React.Component ) => @@ -188,7 +184,7 @@ export class AddCollectionPanel extends React.Component - + )} @@ -214,6 +212,7 @@ export class AddCollectionPanel extends React.Component (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, database: IDropdownOption) => this.setState({ selectedDatabaseId: database.key as string }) } + defaultSelectedKey={this.props.databaseId} + responsiveMode={999} /> )} + - {`${this.getCollectionName()} id`} + {`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`} - + ) => this.setState({ collectionId: event.target.value }) @@ -320,13 +321,15 @@ export class AddCollectionPanel extends React.Component - Sharding options + Sharding - + @@ -371,18 +374,15 @@ export class AddCollectionPanel extends React.Component - + ) => - this.setState({ partitionKey: event.target.value }) - } + onChange={(event: React.ChangeEvent) => { + if ( + userContext.apiType !== "Mongo" && + this.state.partitionKey === "" && + !event.target.value.startsWith("/") + ) { + this.setState({ partitionKey: "/" + event.target.value }); + } else { + this.setState({ partitionKey: event.target.value }); + } + }} /> )} @@ -402,7 +410,7 @@ export class AddCollectionPanel extends React.Component - + )} @@ -431,6 +441,7 @@ export class AddCollectionPanel extends React.Component (this.collectionThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} onCostAcknowledgeChange={(isAcknowledged: boolean) => { @@ -451,7 +462,7 @@ export class AddCollectionPanel extends React.Component - + @@ -504,134 +515,129 @@ export class AddCollectionPanel extends React.Component )} - - - {this.props.explorer.isEnableMongoCapabilityPresent() && ( - - - - - Indexing - - - - - + {userContext.apiType !== "Tables" && ( + { + TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); + this.scrollToAdvancedSection(); + }} + > + + {this.props.explorer.isEnableMongoCapabilityPresent() && ( + + + + + Indexing + + + + + + , isChecked: boolean) => + this.setState({ createMongoWildCardIndex: isChecked }) + } + /> + + )} + + {userContext.apiType === "SQL" && ( , isChecked: boolean) => - this.setState({ createMongoWildCardIndex: isChecked }) + this.setState({ useHashV2: isChecked }) } /> - - )} + )} - {userContext.apiType === "SQL" && ( - - - , isChecked: boolean) => - this.setState({ useHashV1: isChecked }) - } - /> - - My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2) - - - - - 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.{" "} - - Learn more - - - - )} - - {this.shouldShowAnalyticalStoreOptions() && ( - - - - Analytical store - - - - - - - - - On - - - Off - - - {!this.isSynapseLinkEnabled() && ( - - - Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link - for this Cosmos DB account.{" "} - - Learn more - + {this.shouldShowAnalyticalStoreOptions() && ( + + + + Analytical store - this.props.explorer.openEnableSynapseLinkDialog()} - style={{ height: 27, width: 80 }} - styles={{ label: { fontSize: 12 } }} - /> + + + - )} - - )} - - + + + + On + + + Off + + + {!this.isSynapseLinkEnabled() && ( + + + Azure Synapse Link is required for creating an analytical store{" "} + {getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account.{" "} + + Learn more + + + this.props.explorer.openEnableSynapseLinkDialog()} + style={{ height: 27, width: 80 }} + styles={{ label: { fontSize: 12 } }} + /> + + )} + + )} + + + )} @@ -648,24 +654,10 @@ export class AddCollectionPanel extends React.Component + Enable analytical store capability to perform near real-time analytics on your operational data, without + impacting the performance of transactional workloads.{" "} + + Learn more + +
+ ); + } + private shouldShowCollectionThroughputInput(): boolean { if (this.isServerlessAccount()) { return false; @@ -879,6 +899,11 @@ export class AddCollectionPanel extends React.Component 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): Promise { event.preventDefault(); @@ -923,7 +952,7 @@ export class AddCollectionPanel extends React.Component void; } export const DeleteCollectionConfirmationPane: FunctionComponent = ({ explorer, closePanel, - collectionName, }: DeleteCollectionConfirmationPaneProps) => { const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState(""); const [inputCollectionName, setInputCollectionName] = useState(""); @@ -34,6 +33,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent { return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared(); }; + const collectionName = getCollectionName().toLocaleLowerCase(); const paneTitle = "Delete " + collectionName; const submit = async (): Promise => { const collection = explorer.findSelectedCollection(); diff --git a/src/Explorer/Panes/PanelComponent.less b/src/Explorer/Panes/PanelComponent.less index 03ea9dd6d..2421fe68b 100644 --- a/src/Explorer/Panes/PanelComponent.less +++ b/src/Explorer/Panes/PanelComponent.less @@ -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; +} diff --git a/src/Explorer/Panes/PanelInfoErrorComponent.tsx b/src/Explorer/Panes/PanelInfoErrorComponent.tsx index 97282b12e..150fbcc25 100644 --- a/src/Explorer/Panes/PanelInfoErrorComponent.tsx +++ b/src/Explorer/Panes/PanelInfoErrorComponent.tsx @@ -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 + {icon} diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index 88da6e77a..022b8e7c4 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -525,7 +525,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
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) { diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index f14cfaf7e..5500118ce 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -115,6 +115,7 @@ export enum Action { NotebooksGalleryFavoritesCount, NotebooksGalleryPublishedCount, SelfServe, + ExpandAddCollectionPaneAdvancedSection, } export const ActionModifiers = { diff --git a/src/UserContext.ts b/src/UserContext.ts index 20596249e..45b1a805e 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -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; diff --git a/src/Utils/APITypeUtils.ts b/src/Utils/APITypeUtils.ts new file mode 100644 index 000000000..c321bd98b --- /dev/null +++ b/src/Utils/APITypeUtils.ts @@ -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; +}; diff --git a/src/Utils/PricingUtils.ts b/src/Utils/PricingUtils.ts index ea8e0011b..a3b75e2e7 100644 --- a/src/Utils/PricingUtils.ts +++ b/src/Utils/PricingUtils.ts @@ -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 ${AutoPilotUtils.getMinRUsBasedOnUserInput( maxAutoPilotThroughputSet )} RU/s (10% of max RU/s) - ${maxAutoPilotThroughputSet} RU/s based on usage.

After the first ${AutoPilotUtils.getStorageBasedOnUserInput( @@ -227,7 +228,7 @@ export function getEstimatedSpendHtml( `${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly
` + `(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` + `

` + - `${estimatedCostDisclaimer}

` + `*${estimatedCostDisclaimer}

` ); } @@ -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`; } } diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index d2f20358f..02ca87d52 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -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 diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 689bd3196..5bec9cdef 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -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")'); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index d679b89b6..33362c0e6 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -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")');