Migrate Add Database Panel to React (#597)

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
Hardikkumar Nai
2021-05-11 00:32:14 +05:30
committed by GitHub
parent 7efa8ca58f
commit 9d5c9d6296
36 changed files with 4010 additions and 2669 deletions

View File

@@ -213,6 +213,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={true}
isAutoscaleSelected={this.isNewDatabaseAutoscale}
throughput={this.newDatabaseThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
@@ -442,6 +444,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={false}
isAutoscaleSelected={this.isCollectionAutoscale}
throughput={this.collectionThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}

View File

@@ -1,4 +1,4 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" data-bind="attr: { id: id }">
<!-- Add database form -- Start -->
@@ -126,31 +126,31 @@
<div data-bind="visible: databaseCreateNewShared">
<throughput-input-autopilot-v3
params="{
step: 100,
value: throughput,
testId: 'sharedThroughputValue',
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckDatabase',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newDatabase-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newDatabase-databaseThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"
step: 100,
value: throughput,
testId: 'sharedThroughputValue',
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckDatabase',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newDatabase-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newDatabase-databaseThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}"
>
</throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport">

View File

@@ -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(<AddDatabasePanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -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<AddDatabasePaneProps> = ({
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<string>("");
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<boolean>(getSharedThroughputDefault);
const [formErrorsDetails, setFormErrorsDetails] = useState<string>();
const [formErrors, setFormErrors] = useState<string>("");
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(container.isAutoscaleDefaultEnabled());
const throughputDefaults = container.collectionCreationDefaults.throughput;
const [throughput, setThroughput] = useState<number>(
isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared
);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(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<boolean>(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<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
setDatabaseId(newValue || "");
},
[]
);
const props: RightPaneFormProps = {
expandConsole: container.expandConsole,
formError: formErrors,
formErrorDetail: formErrorsDetails,
isExecuting,
submitButtonText: "OK",
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="paneContentContainer" role="dialog" aria-labelledby="databaseTitle">
{showUpsellMessage && formErrors === "" && (
<PanelInfoErrorComponent
message={upsellMessage}
messageType="info"
showErrorDetails={false}
openNotificationConsole={openNotificationConsole}
link={upsellAnchorUrl}
linkText={upsellAnchorText}
/>
)}
<div className="paneMainContent">
<div>
<p>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</p>
<TextField
id="database-id"
type="text"
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}
value={databaseId}
onChange={handleonChangeDBId}
autoFocus
/>
<div
className="databaseProvision"
aria-label="New database provision support"
style={{ display: "block ruby" }}
>
<Checkbox
title="Provision shared throughput"
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>{" "}
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</div>
{databaseCreateNewShared && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isAutoscaleSelected={isAutoPilotSelected}
throughput={throughput}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)}
/>
{canRequestSupport() && (
<p>
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">
Contact support{" "}
</a>
for more than <span>{throughputDefaults.unlimitedmax?.toLocaleString()} </span> RU/s.
</p>
)}
</div>
)}
</div>
</div>
</div>
</RightPaneForm>
);
};

View File

@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddDatabasePane Pane should render Default properly 1`] = `
<RightPaneForm
expandConsole={[Function]}
formError=""
isExecuting={false}
onSubmit={[Function]}
submitButtonText="OK"
>
<div
aria-labelledby="databaseTitle"
className="paneContentContainer"
role="dialog"
>
<PanelInfoErrorComponent
link="https://aka.ms/azure-cosmos-db-pricing"
linkText="More details"
message="Start at $24/mo per database, multiple containers included"
messageType="info"
openNotificationConsole={[Function]}
showErrorDetails={false}
/>
<div
className="paneMainContent"
>
<div>
<p>
<span
className="mandatoryStar"
>
*
</span>
<Text
variant="small"
>
Database id
</Text>
<InfoTooltip>
A database is a logical container of one or more collections
</InfoTooltip>
</p>
<StyledTextFieldBase
aria-label="Database id"
aria-required="true"
autoComplete="off"
autoFocus={true}
id="database-id"
onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Type a new database id"
size={40}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
type="text"
value=""
/>
<div
aria-label="New database provision support"
className="databaseProvision"
style={
Object {
"display": "block ruby",
}
}
>
<StyledCheckboxBase
checked={true}
label="Provision throughput"
onChange={[Function]}
styles={
Object {
"checkbox": Object {
"height": 12,
"width": 12,
},
"label": Object {
"alignItems": "center",
"padding": 0,
},
}
}
title="Provision shared throughput"
/>
<InfoTooltip>
Provisioned throughput at the database level will be shared across all collections within the database.
</InfoTooltip>
</div>
<div>
<ThroughputInput
isAutoscaleSelected={false}
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
throughput={400}
/>
</div>
</div>
</div>
</div>
</RightPaneForm>
`;

View File

@@ -17,7 +17,6 @@ export class AddDatabasePaneComponent {
};
}
}
export class AddCollectionPaneComponent {
constructor() {
return {

View File

@@ -9,12 +9,6 @@ export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
buttonLabel,
}: PanelFooterProps): JSX.Element => (
<div className="panelFooter">
<PrimaryButton
type="submit"
id="sidePanelOkButton"
text={buttonLabel}
ariaLabel={buttonLabel}
data-testid="submit"
/>
<PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} />
</div>
);

View File

@@ -22,19 +22,19 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
}: PanelInfoErrorProps): JSX.Element => {
let icon: JSX.Element;
if (messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" data-testid="errorIcon" />;
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
} else if (messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" data-testid="warningIcon" />;
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" aria-label="warning" />;
} else if (messageType === "info") {
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" data-testid="InfoIcon" />;
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
}
return (
formError && (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="start">
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small" data-testid="panelmessage">
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
{message}
{link && linkText && (
<Link target="_blank" href={link}>

View File

@@ -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(<RightPaneForm {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("should call close method click cancel icon", () => {
render(<RightPaneForm {...props} />);
fireEvent.click(screen.getByTestId("closePaneBtn"));
expect(onClose).toHaveBeenCalled();
});
it("should call submit method enter in form", () => {
render(<RightPaneForm {...props} />);
fireEvent.click(screen.getByTestId("submit"));
fireEvent.click(screen.getByLabelText("Load"));
expect(onSubmit).toHaveBeenCalled();
});
it("should call submit method click on submit button", () => {
render(<RightPaneForm {...props} />);
fireEvent.click(screen.getByTestId("submit"));
fireEvent.click(screen.getByLabelText("Load"));
expect(onSubmit).toHaveBeenCalled();
});
it("should render error in header", () => {
render(<RightPaneForm {...props} formError="file already Exist" />);
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");
});
});

View File

@@ -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<RightPaneFormProps> = ({
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<HTMLFormElement>) => {
event.preventDefault();
onSubmit();
};
const renderPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level={2}>
{title}
</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
data-testid="closePaneBtn"
onClick={onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
onClose();
event.stopPropagation();
}
};
const panelInfoErrorProps: PanelInfoErrorProps = {
messageType: "error",
@@ -78,19 +38,13 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
};
return (
<div tabIndex={-1} onKeyDown={onKeyDown}>
<div className="contextual-pane-out" onClick={onClose}></div>
<div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
<div className="panelContentWrapper">
{renderPanelHeader()}
<PanelInfoErrorComponent {...panelInfoErrorProps} />
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
</form>
</div>
{isExecuting && <PanelLoadingScreen />}
</div>
</div>
<>
<PanelInfoErrorComponent {...panelInfoErrorProps} />
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
</form>
{isExecuting && <PanelLoadingScreen />}
</>
);
};

View File

@@ -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<SettingsPaneProps> = ({
setGraphAutoVizDisabled(option.key);
};
const genericPaneProps: GenericRightPaneProps = {
const genericPaneProps: RightPaneFormProps = {
expandConsole,
formError: formErrors,
formErrorDetail: "",
id: "settingspane",
isExecuting,
title: "Setting",
submitButtonText: "Apply",
onClose: () => closePanel(),
onSubmit: () => handlerOnSubmit(undefined),
};
const pageOptionList: IChoiceGroupOption[] = [
@@ -130,17 +124,17 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
setPageOption(option.key);
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<RightPaneForm {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<div className="settingsSection">
<div className="settingsSectionPart pageOptionsPart">
<div className="settingsSectionLabel">
Page options
<Tooltip>
<InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
query results per page.
</Tooltip>
</InfoTooltip>
</div>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
</div>
@@ -149,7 +143,7 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="tabcontent">
<div className="settingsSectionLabel">
Query results per page
<Tooltip>Enter the number of query results that should be shown per page.</Tooltip>
<InfoTooltip>Enter the number of query results that should be shown per page.</InfoTooltip>
</div>
<SpinButton
@@ -176,10 +170,10 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable cross-partition query
<Tooltip>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is
not scoped to single partition key value.
</Tooltip>
</InfoTooltip>
</div>
<Checkbox
@@ -199,11 +193,11 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Max degree of parallelism
<Tooltip>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A
positive property value limits the number of concurrent operations to the set value. If it is set to
less than 0, the system automatically decides the number of concurrent operations to run.
</Tooltip>
</InfoTooltip>
</div>
<SpinButton
@@ -227,10 +221,10 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Display Gremlin query results as:&nbsp;
<Tooltip>
<InfoTooltip>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as
JSON.
</Tooltip>
</InfoTooltip>
</div>
<ChoiceGroup
@@ -249,6 +243,6 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
</div>
</div>
</div>
</GenericRightPaneComponent>
</RightPaneForm>
);
};

View File

@@ -1,16 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Pane should render Default properly 1`] = `
<GenericRightPaneComponent
<RightPaneForm
expandConsole={[Function]}
formError=""
formErrorDetail=""
id="settingspane"
isExecuting={false}
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Apply"
title="Setting"
>
<div
className="paneMainContent"
@@ -25,9 +22,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Page options
<Tooltip>
<InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</Tooltip>
</InfoTooltip>
</div>
<StyledChoiceGroup
onChange={[Function]}
@@ -56,9 +53,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Query results per page
<Tooltip>
<InfoTooltip>
Enter the number of query results that should be shown per page.
</Tooltip>
</InfoTooltip>
</div>
<StyledSpinButton
ariaLabel="Custom query items per page"
@@ -85,9 +82,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Enable cross-partition query
<Tooltip>
<InfoTooltip>
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</Tooltip>
</InfoTooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable cross partition query"
@@ -114,9 +111,9 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSectionLabel"
>
Max degree of parallelism
<Tooltip>
<InfoTooltip>
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
</Tooltip>
</InfoTooltip>
</div>
<StyledSpinButton
ariaLabel="Max degree of parallelism"
@@ -148,20 +145,17 @@ exports[`Settings Pane should render Default properly 1`] = `
</div>
</div>
</div>
</GenericRightPaneComponent>
</RightPaneForm>
`;
exports[`Settings Pane should render Gremlin properly 1`] = `
<GenericRightPaneComponent
<RightPaneForm
expandConsole={[Function]}
formError=""
formErrorDetail=""
id="settingspane"
isExecuting={false}
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Apply"
title="Setting"
>
<div
className="paneMainContent"
@@ -176,9 +170,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
className="settingsSectionLabel"
>
Display Gremlin query results as: 
<Tooltip>
<InfoTooltip>
Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.
</Tooltip>
</InfoTooltip>
</div>
<StyledChoiceGroup
aria-label="Graph Auto-visualization"
@@ -214,5 +208,5 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
</div>
</div>
</div>
</GenericRightPaneComponent>
</RightPaneForm>
`;

View File

@@ -15,9 +15,6 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
closePanel,
uploadFile,
}: UploadFilePanelProps) => {
const title = "Upload file to notebook server";
const submitButtonLabel = "Upload";
const selectFileInputLabel = "Select file to upload";
const extensions: string = undefined; //ex. ".ipynb"
const errorMessage = "Could not upload file";
const inProgressMessage = "Uploading file to notebook server";
@@ -39,11 +36,8 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
}
const file: File = files.item(0);
// const id: string = logConsoleProgress(
// `${inProgressMessage}: ${file.name}`
// );
logConsoleProgress(`${inProgressMessage}: ${file.name}`);
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
setIsExecuting(true);
@@ -61,7 +55,7 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
)
.finally(() => {
setIsExecuting(false);
// clearInProgressMessageWithId(id);
clearMessage();
});
};
@@ -92,18 +86,15 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
expandConsole,
formError: formErrors,
formErrorDetail: formErrorsDetails,
id: "uploadFilePane",
isExecuting: isExecuting,
title,
submitButtonText: submitButtonLabel,
onClose: closePanel,
submitButtonText: "Upload",
onSubmit: submit,
};
return (
<RightPaneForm {...genericPaneProps}>
<div className="paneMainContent">
<Upload label={selectFileInputLabel} accept={extensions} onUpload={updateSelectedFiles} />
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
</div>
</RightPaneForm>
);

View File

@@ -2,7 +2,6 @@ import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@flu
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { getErrorMessage } from "../../Tables/Utilities";
@@ -10,23 +9,9 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
export interface UploadItemsPaneProps {
explorer: Explorer;
closePanel: () => void;
}
const getTitle = (): string => {
if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") {
return "Upload Tables";
} else if (userContext.apiType === "Gremlin") {
return "Upload Graph";
} else {
return "Upload Items";
}
};
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
explorer,
closePanel,
}: UploadItemsPaneProps) => {
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explorer }: UploadItemsPaneProps) => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>("");
@@ -71,11 +56,8 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
expandConsole: () => explorer.expandConsole(),
formError,
formErrorDetail,
id: "uploaditemspane",
isExecuting: isExecuting,
title: getTitle(),
submitButtonText: "Upload",
onClose: closePanel,
onSubmit,
};

View File

@@ -5,11 +5,8 @@ exports[`Upload Items Pane should render Default properly 1`] = `
expandConsole={[Function]}
formError=""
formErrorDetail=""
id="uploaditemspane"
onClose={[Function]}
onSubmit={[Function]}
submitButtonText="Upload"
title="Upload Items"
>
<div
className="paneMainContent"

View File

@@ -1288,20 +1288,20 @@ 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-202"
>
<StyledIconBase
aria-label="warning"
className="panelWarningIcon"
data-testid="warningIcon"
iconName="WarningSolid"
key=".0:$.0"
>
<IconBase
aria-label="warning"
className="panelWarningIcon"
data-testid="warningIcon"
iconName="WarningSolid"
styles={[Function]}
theme={
@@ -1579,10 +1579,10 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
}
>
<i
aria-hidden={true}
aria-label="warning"
className="panelWarningIcon root-204"
data-icon-name="WarningSolid"
data-testid="warningIcon"
role="img"
>
</i>
@@ -1593,13 +1593,13 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
key=".0:$.1"
>
<Text
aria-label="message"
className="panelWarningErrorMessage"
data-testid="panelmessage"
variant="small"
>
<span
aria-label="message"
className="panelWarningErrorMessage css-205"
data-testid="panelmessage"
>
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
</span>
@@ -2303,14 +2303,12 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<CustomizedPrimaryButton
ariaLabel="OK"
data-testid="submit"
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
data-testid="submit"
id="sidePanelOkButton"
text="OK"
theme={
@@ -2590,7 +2588,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<CustomizedDefaultButton
ariaLabel="OK"
data-testid="submit"
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -2872,7 +2869,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
>
<DefaultButton
ariaLabel="OK"
data-testid="submit"
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -3155,7 +3151,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
data-testid="submit"
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -4012,7 +4007,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
aria-label="OK"
className="ms-Button ms-Button--primary root-218"
data-is-focusable={true}
data-testid="submit"
id="sidePanelOkButton"
onClick={[Function]}
onKeyDown={[Function]}