diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.test.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.test.tsx
new file mode 100644
index 000000000..4206eb723
--- /dev/null
+++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.test.tsx
@@ -0,0 +1,17 @@
+import { shallow } from "enzyme";
+import React from "react";
+import Explorer from "../../Explorer";
+import { AddDatabasePanel } from "./AddDatabasePanel";
+
+const props = {
+ explorer: new Explorer(),
+ closePanel: (): void => undefined,
+ openNotificationConsole: (): void => undefined,
+};
+
+describe("AddDatabasePane Pane", () => {
+ it("should render Default properly", () => {
+ const wrapper = shallow( );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx
new file mode 100644
index 000000000..c525f927d
--- /dev/null
+++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx
@@ -0,0 +1,342 @@
+import { Checkbox, Text, TextField } from "@fluentui/react";
+import React, { FunctionComponent, useEffect, useState } from "react";
+import * as Constants from "../../../Common/Constants";
+import { createDatabase } from "../../../Common/dataAccess/createDatabase";
+import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
+import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
+import { configContext, Platform } from "../../../ConfigContext";
+import * as DataModels from "../../../Contracts/DataModels";
+import { SubscriptionType } from "../../../Contracts/SubscriptionType";
+import * as SharedConstants from "../../../Shared/Constants";
+import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
+import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
+import { userContext } from "../../../UserContext";
+import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
+import * as PricingUtils from "../../../Utils/PricingUtils";
+import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
+import Explorer from "../../Explorer";
+import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
+import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
+
+export interface AddDatabasePaneProps {
+ explorer: Explorer;
+ closePanel: () => void;
+ openNotificationConsole: () => void;
+}
+
+export const AddDatabasePanel: FunctionComponent = ({
+ explorer: container,
+ closePanel,
+ openNotificationConsole,
+}: AddDatabasePaneProps) => {
+ const { subscriptionType } = userContext;
+ const getSharedThroughputDefault = !(subscriptionType === SubscriptionType.EA || container.isServerlessEnabled());
+ const _isAutoPilotSelectedAndWhatTier = (): DataModels.AutoPilotCreationSettings => {
+ if (isAutoPilotSelected && maxAutoPilotThroughputSet) {
+ return {
+ maxThroughput: maxAutoPilotThroughputSet * 1,
+ };
+ }
+ return undefined;
+ };
+
+ const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
+ const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
+ const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
+ const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id";
+ const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
+
+ const [databaseId, setDatabaseId] = useState("");
+ const databaseIdTooltipText = `A ${
+ isCassandraAccount ? "keyspace" : "database"
+ } is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
+
+ const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
+ const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState(getSharedThroughputDefault);
+ const [formErrorsDetails, setFormErrorsDetails] = useState();
+ const [formErrors, setFormErrors] = useState("");
+
+ const [isAutoPilotSelected, setIsAutoPilotSelected] = useState(container.isAutoscaleDefaultEnabled());
+
+ const throughputDefaults = container.collectionCreationDefaults.throughput;
+ const [throughput, setThroughput] = useState(
+ isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared
+ );
+
+ const [throughputSpendAck, setThroughputSpendAck] = useState(false);
+
+ const canRequestSupport = () => {
+ if (
+ configContext.platform !== Platform.Emulator &&
+ !userContext.isTryCosmosDBSubscription &&
+ configContext.platform !== Platform.Portal
+ ) {
+ const offerThroughput: number = throughput;
+ return offerThroughput <= 100000;
+ }
+
+ return false;
+ };
+ const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
+ const upsellMessage: string = PricingUtils.getUpsellMessage(
+ userContext.portalEnv,
+ isFreeTierAccount,
+ container.isFirstResourceCreated(),
+ false
+ );
+
+ const upsellAnchorUrl: string = isFreeTierAccount ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
+
+ const upsellAnchorText: string = isFreeTierAccount ? "Learn more" : "More details";
+ const maxAutoPilotThroughputSet = AutoPilotUtils.minAutoPilotThroughput;
+
+ const canConfigureThroughput = !container.isServerlessEnabled();
+ const showUpsellMessage = () => {
+ if (container.isServerlessEnabled()) {
+ return false;
+ }
+
+ if (isFreeTierAccount) {
+ return databaseCreateNewShared;
+ }
+
+ return true;
+ };
+ const [isExecuting, setIsExecuting] = useState(false);
+
+ useEffect(() => {
+ setDatabaseCreateNewShared(getSharedThroughputDefault);
+ }, [subscriptionType]);
+
+ const addDatabasePaneMessage = {
+ database: {
+ id: databaseId,
+ shared: databaseCreateNewShared,
+ },
+ subscriptionType: SubscriptionType[subscriptionType],
+ subscriptionQuotaId: userContext.quotaId,
+ defaultsCheck: {
+ flight: userContext.addCollectionFlight,
+ },
+ dataExplorerArea: Constants.Areas.ContextualPane,
+ };
+
+ useEffect(() => {
+ const addDatabasePaneOpenMessage = {
+ subscriptionType: SubscriptionType[subscriptionType],
+ subscriptionQuotaId: userContext.quotaId,
+ defaultsCheck: {
+ throughput: throughput,
+ flight: userContext.addCollectionFlight,
+ },
+ dataExplorerArea: Constants.Areas.ContextualPane,
+ };
+ TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
+ }, []);
+
+ const onSubmit = () => {
+ if (!_isValid()) {
+ return;
+ }
+
+ const offerThroughput: number = _computeOfferThroughput();
+
+ const addDatabasePaneStartMessage = {
+ ...addDatabasePaneMessage,
+ offerThroughput,
+ };
+ const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
+ setFormErrors("");
+ setIsExecuting(true);
+
+ const createDatabaseParams: DataModels.CreateDatabaseParams = {
+ databaseId: addDatabasePaneStartMessage.database.id,
+ databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
+ };
+ if (isAutoPilotSelected) {
+ createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.offerThroughput;
+ } else {
+ createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
+ }
+
+ createDatabase(createDatabaseParams).then(
+ () => {
+ _onCreateDatabaseSuccess(offerThroughput, startKey);
+ },
+ (error: string) => {
+ _onCreateDatabaseFailure(error, offerThroughput, startKey);
+ }
+ );
+ };
+
+ const _onCreateDatabaseSuccess = (offerThroughput: number, startKey: number): void => {
+ setIsExecuting(false);
+ closePanel();
+ container.refreshAllDatabases();
+ const addDatabasePaneSuccessMessage = {
+ ...addDatabasePaneMessage,
+ offerThroughput,
+ };
+ TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
+ };
+
+ const _onCreateDatabaseFailure = (error: string, offerThroughput: number, startKey: number): void => {
+ setIsExecuting(false);
+ const errorMessage = getErrorMessage(error);
+ setFormErrors(errorMessage);
+ setFormErrorsDetails(errorMessage);
+ const addDatabasePaneFailedMessage = {
+ ...addDatabasePaneMessage,
+ offerThroughput,
+ error: errorMessage,
+ errorStack: getErrorStack(error),
+ };
+ TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
+ };
+
+ const _getThroughput = (): number => {
+ return isNaN(throughput) ? 0 : Number(throughput);
+ };
+
+ const _computeOfferThroughput = (): number => {
+ if (!canConfigureThroughput) {
+ return undefined;
+ }
+
+ return _getThroughput();
+ };
+
+ const _isValid = (): boolean => {
+ // TODO add feature flag that disables validation for customers with custom accounts
+ if (isAutoPilotSelected) {
+ const autoPilot = _isAutoPilotSelectedAndWhatTier();
+ if (
+ !autoPilot ||
+ !autoPilot.maxThroughput ||
+ !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
+ ) {
+ setFormErrors(
+ `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
+ );
+ return false;
+ }
+ }
+ const throughput = _getThroughput();
+
+ if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
+ setFormErrors(`Please acknowledge the estimated daily spend.`);
+ return false;
+ }
+
+ const autoscaleThroughput = maxAutoPilotThroughputSet * 1;
+
+ if (
+ isAutoPilotSelected &&
+ autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
+ !throughputSpendAck
+ ) {
+ setFormErrors(`Please acknowledge the estimated monthly spend.`);
+ return false;
+ }
+
+ return true;
+ };
+
+ const handleonChangeDBId = React.useCallback(
+ (event: React.FormEvent, newValue?: string) => {
+ setDatabaseId(newValue || "");
+ },
+ []
+ );
+
+ const props: RightPaneFormProps = {
+ expandConsole: container.expandConsole,
+ formError: formErrors,
+ formErrorDetail: formErrorsDetails,
+ isExecuting,
+ submitButtonText: "OK",
+ onSubmit,
+ };
+
+ return (
+
+
+ {showUpsellMessage && formErrors === "" && (
+
+ )}
+
+
+
+ *
+ {databaseIdLabel}
+ {databaseIdTooltipText}
+
+
+
+
+
+ setDatabaseCreateNewShared(!databaseCreateNewShared)}
+ />{" "}
+ {databaseLevelThroughputTooltipText}
+
+ {databaseCreateNewShared && (
+
+
setThroughput(throughput)}
+ setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
+ onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)}
+ />
+
+ {canRequestSupport() && (
+
+
+ Contact support{" "}
+
+ for more than {throughputDefaults.unlimitedmax?.toLocaleString()} RU/s.
+
+ )}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap
new file mode 100644
index 000000000..bdc4f1526
--- /dev/null
+++ b/src/Explorer/Panes/AddDatabasePanel/__snapshots__/AddDatabasePanel.test.tsx.snap
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AddDatabasePane Pane should render Default properly 1`] = `
+
+
+
+
+
+
+
+ *
+
+
+ Database id
+
+
+ A database is a logical container of one or more collections
+
+
+
+
+
+
+
+ Provisioned throughput at the database level will be shared across all collections within the database.
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/Explorer/Panes/PaneComponents.ts b/src/Explorer/Panes/PaneComponents.ts
index 9d730887d..34ff76ed3 100644
--- a/src/Explorer/Panes/PaneComponents.ts
+++ b/src/Explorer/Panes/PaneComponents.ts
@@ -17,7 +17,6 @@ export class AddDatabasePaneComponent {
};
}
}
-
export class AddCollectionPaneComponent {
constructor() {
return {
diff --git a/src/Explorer/Panes/PanelFooterComponent.tsx b/src/Explorer/Panes/PanelFooterComponent.tsx
index ba74b0dd7..d007a5e36 100644
--- a/src/Explorer/Panes/PanelFooterComponent.tsx
+++ b/src/Explorer/Panes/PanelFooterComponent.tsx
@@ -9,12 +9,6 @@ export const PanelFooterComponent: React.FunctionComponent = (
buttonLabel,
}: PanelFooterProps): JSX.Element => (
);
diff --git a/src/Explorer/Panes/PanelInfoErrorComponent.tsx b/src/Explorer/Panes/PanelInfoErrorComponent.tsx
index 8e15596ed..2ae836f64 100644
--- a/src/Explorer/Panes/PanelInfoErrorComponent.tsx
+++ b/src/Explorer/Panes/PanelInfoErrorComponent.tsx
@@ -22,19 +22,19 @@ export const PanelInfoErrorComponent: React.FunctionComponent {
let icon: JSX.Element;
if (messageType === "error") {
- icon = ;
+ icon = ;
} else if (messageType === "warning") {
- icon = ;
+ icon = ;
} else if (messageType === "info") {
- icon = ;
+ icon = ;
}
return (
formError && (
-
+
{icon}
-
+
{message}
{link && linkText && (
diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx
index 79bcf43d0..a2f3423e1 100644
--- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx
+++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.test.tsx
@@ -3,48 +3,38 @@ import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { RightPaneForm } from "./RightPaneForm";
-const onClose = jest.fn();
const onSubmit = jest.fn();
const expandConsole = jest.fn();
const props = {
- closePanel: (): void => undefined,
expandConsole,
formError: "",
formErrorDetail: "",
- id: "loadQueryPane",
isExecuting: false,
- title: "Load Query Pane",
submitButtonText: "Load",
- onClose,
onSubmit,
};
-describe("Load Query Pane", () => {
+describe("Right Pane Form", () => {
let wrapper: ReactWrapper;
it("should render Default properly", () => {
wrapper = mount( );
expect(wrapper).toMatchSnapshot();
});
- it("should call close method click cancel icon", () => {
- render( );
- fireEvent.click(screen.getByTestId("closePaneBtn"));
- expect(onClose).toHaveBeenCalled();
- });
it("should call submit method enter in form", () => {
render( );
- fireEvent.click(screen.getByTestId("submit"));
+ fireEvent.click(screen.getByLabelText("Load"));
expect(onSubmit).toHaveBeenCalled();
});
it("should call submit method click on submit button", () => {
render( );
- fireEvent.click(screen.getByTestId("submit"));
+ fireEvent.click(screen.getByLabelText("Load"));
expect(onSubmit).toHaveBeenCalled();
});
it("should render error in header", () => {
render( );
- expect(screen.getByTestId("errorIcon")).toBeDefined();
- expect(screen.getByTestId("panelmessage").innerHTML).toEqual("file already Exist");
+ expect(screen.getByLabelText("error")).toBeDefined();
+ expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist");
});
});
diff --git a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx
index 40120ff76..774399b23 100644
--- a/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx
+++ b/src/Explorer/Panes/RightPaneForm/RightPaneForm.tsx
@@ -1,6 +1,4 @@
-import { IconButton } from "@fluentui/react";
import React, { FunctionComponent, ReactNode } from "react";
-import { KeyCodes } from "../../../Common/Constants";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
@@ -9,12 +7,9 @@ export interface RightPaneFormProps {
expandConsole: () => void;
formError: string;
formErrorDetail: string;
- id: string;
isExecuting: boolean;
- onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
- title: string;
isSubmitButtonHidden?: boolean;
children?: ReactNode;
}
@@ -23,51 +18,16 @@ export const RightPaneForm: FunctionComponent = ({
expandConsole,
formError,
formErrorDetail,
- id,
isExecuting,
- onClose,
onSubmit,
submitButtonText,
- title,
isSubmitButtonHidden = false,
children,
}: RightPaneFormProps) => {
- const getPanelHeight = (): number => {
- const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
- return window.innerHeight - $(notificationConsoleElement).height();
- };
-
- const panelHeight: number = getPanelHeight();
-
const handleOnSubmit = (event: React.FormEvent) => {
event.preventDefault();
onSubmit();
};
- const renderPanelHeader = (): JSX.Element => {
- return (
-
-
- {title}
-
-
-
- );
- };
-
- const onKeyDown = (event: React.KeyboardEvent): void => {
- if (event.keyCode === KeyCodes.Escape) {
- onClose();
- event.stopPropagation();
- }
- };
const panelInfoErrorProps: PanelInfoErrorProps = {
messageType: "error",
@@ -78,19 +38,13 @@ export const RightPaneForm: FunctionComponent = ({
};
return (
-
-
-
-
- {renderPanelHeader()}
-
-
-
- {isExecuting &&
}
-
-
+ <>
+
+
+ {isExecuting && }
+ >
);
};
diff --git a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap
index f11ff705e..90b9189f3 100644
--- a/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap
+++ b/src/Explorer/Panes/RightPaneForm/__snapshots__/RightPaneForm.test.tsx.snap
@@ -1,73 +1,322 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Load Query Pane should render Default properly 1`] = `
+exports[`Right Pane Form should render Default properly 1`] = `
-
+
+
+
`;
diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx
index 85331ec65..88fb10cf0 100644
--- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx
+++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx
@@ -1,16 +1,13 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import * as Constants from "../../../Common/Constants";
-import { Tooltip } from "../../../Common/Tooltip/Tooltip";
+import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { configContext } from "../../../ConfigContext";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import * as StringUtility from "../../../Shared/StringUtility";
import { userContext } from "../../../UserContext";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
-import {
- GenericRightPaneComponent,
- GenericRightPaneProps,
-} from "../GenericRightPaneComponent/GenericRightPaneComponent";
+import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface SettingsPaneProps {
expandConsole: () => void;
@@ -105,15 +102,12 @@ export const SettingsPane: FunctionComponent