Compare commits

...

28 Commits

Author SHA1 Message Date
hardiknai-techm
8ed4465885 Panel should not use GenericRightPaneComponent 2021-05-17 16:30:28 +05:30
hardiknai-techm
af5d77d754 merge master 2021-05-17 06:48:15 +05:30
Hardikkumar Nai
a52a156005 Remove Old Add Database Pane Code (#784)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-14 12:05:00 -05:00
hardiknai-techm
ead93b9fa5 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into genericRightPaneComponent 2021-05-07 12:56:45 +05:30
hardiknai-techm
88491ba6a9 resolve master merge conflict 2021-05-06 18:59:00 +05:30
hardiknai-techm
fc83484b6c resolve merge conflict 2021-05-06 17:12:03 +05:30
hardiknai-techm
e91145234f resolve e2e test error 2021-05-04 18:34:27 +05:30
hardiknai-techm
7c4bc9e0c0 Merge branch 'remove_explorer.defaultExperience' of https://github.com/Azure/cosmos-explorer into genericRightPaneComponent 2021-05-04 08:22:23 +05:30
hardiknai-techm
40d71d3d7a resolve marge conflict 2021-05-04 08:13:34 +05:30
hardiknai-techm
0c3f8bd625 resolve merge conflict 2021-05-04 07:40:09 +05:30
hardiknai-techm
6bdf1c7f7c update snapshot test 2021-04-29 20:23:21 +05:30
hardiknai-techm
f048f21def resolve format issue 2021-04-29 08:46:59 +05:30
hardiknai-techm
38ffa6a003 merge master branch 2021-04-29 08:35:26 +05:30
hardiknai-techm
7902df4d16 Merge branch 'remove_explorer.defaultExperience' of https://github.com/Azure/cosmos-explorer into remove_explorer.defaultExperience 2021-04-29 08:15:03 +05:30
hardiknai-techm
e701dcc881 resolve master branch conflict 2021-04-29 08:13:06 +05:30
Steve Faulkner
3a6c7f9f94 Fixes 2021-04-28 18:59:13 -05:00
Steve Faulkner
7b5b752d9c Fix strict 2021-04-28 18:46:51 -05:00
Steve Faulkner
a0d22960ff Merge branch 'master' into move_graph_style_panel_to_react 2021-04-28 15:20:57 -05:00
Steve Faulkner
4c6650760b Merge branch 'master' into remove_explorer.defaultExperience 2021-04-27 19:51:06 -05:00
Steve Faulkner
0b1ac8f445 WIP 2021-04-25 15:57:00 -07:00
Steve Faulkner
96305f50f8 Merge branch 'master' into remove_explorer.defaultExperience 2021-04-25 15:54:03 -07:00
hardiknai-techm
3e011f939d resolve conflict master branch 2021-04-23 19:13:13 +05:30
hardiknai-techm
10961a2f9f marge master 2021-04-23 18:47:05 +05:30
hardiknai-techm
ba25eea41e update sanpshort test case 2021-04-16 15:59:48 +05:30
hardiknai-techm
e17fe25292 Some panel use GenenricRightPanel and Some panel use RightPanelForm 2021-04-16 15:53:44 +05:30
hardiknai-techm
9494c9cd55 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into genericRightPaneComponent 2021-04-16 14:55:40 +05:30
hardiknai-techm
8f0bb1add8 Remove Explorer.defaultExperience 2021-04-15 13:04:51 +05:30
hardiknai-techm
5c9ab15b3a remove genericRightPaneComponent and create RightPaneWrapper with form 2021-04-15 11:47:05 +05:30
46 changed files with 13757 additions and 19952 deletions

View File

@@ -13,6 +13,4 @@ ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
// Panes // Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());

View File

@@ -1,12 +1,12 @@
jest.mock("../../../../Juno/JunoClient"); jest.mock("../../../../Juno/JunoClient");
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from ".";
import { HttpStatusCodes } from "../../../../Common/Constants"; import { HttpStatusCodes } from "../../../../Common/Constants";
import { JunoClient } from "../../../../Juno/JunoClient"; import { JunoClient } from "../../../../Juno/JunoClient";
import { CodeOfConduct, CodeOfConductProps } from "./CodeOfConduct";
describe("CodeOfConductComponent", () => { describe("CodeOfConduct", () => {
let codeOfConductProps: CodeOfConductComponentProps; let codeOfConductProps: CodeOfConductProps;
beforeEach(() => { beforeEach(() => {
const junoClient = new JunoClient(); const junoClient = new JunoClient();
@@ -21,12 +21,12 @@ describe("CodeOfConductComponent", () => {
}); });
it("renders", () => { it("renders", () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />); const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("onAcceptedCodeOfConductCalled", async () => { it("onAcceptedCodeOfConductCalled", async () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />); const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
wrapper.find(".genericPaneSubmitBtn").first().simulate("click"); wrapper.find(".genericPaneSubmitBtn").first().simulate("click");
await Promise.resolve(); await Promise.resolve();
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled(); expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();

View File

@@ -6,15 +6,15 @@ import { JunoClient } from "../../../../Juno/JunoClient";
import { Action } from "../../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor";
export interface CodeOfConductComponentProps { export interface CodeOfConductProps {
junoClient: JunoClient; junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void; onAcceptCodeOfConduct: (result: boolean) => void;
} }
export const CodeOfConductComponent: FunctionComponent<CodeOfConductComponentProps> = ({ export const CodeOfConduct: FunctionComponent<CodeOfConductProps> = ({
junoClient, junoClient,
onAcceptCodeOfConduct, onAcceptCodeOfConduct,
}: CodeOfConductComponentProps) => { }: CodeOfConductProps) => {
const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
@@ -47,7 +47,7 @@ export const CodeOfConductComponent: FunctionComponent<CodeOfConductComponentPro
startKey startKey
); );
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct"); handleError(error, "CodeOfConduct/acceptCodeOfConduct", "Failed to accept code of conduct");
} }
}; };

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeOfConductComponent renders 1`] = ` exports[`CodeOfConduct renders 1`] = `
<Stack <Stack
tokens={ tokens={
Object { Object {

View File

@@ -1,123 +0,0 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "@fluentui/react";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
interface CodeOfConductComponentState {
readCodeOfConduct: boolean;
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private viewCodeOfConductTraced: boolean;
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
this.state = {
readCodeOfConduct: false,
};
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
}
private async acceptCodeOfConduct(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey);
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
}
}
private onChangeCheckbox = (): void => {
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
};
public render(): JSX.Element {
if (!this.viewCodeOfConductTraced) {
this.viewCodeOfConductTraced = true;
trace(Action.NotebooksGalleryViewCodeOfConduct);
}
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{this.descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.descriptionPara3}
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
</Text>
</Stack.Item>
<Stack.Item>
<Checkbox
styles={{
label: {
margin: 0,
padding: "2 0 2 0",
},
text: {
fontSize: 12,
},
}}
label="I have read and accept the code of conduct."
onChange={this.onChangeCheckbox}
/>
</Stack.Item>
<Stack.Item>
<PrimaryButton
ariaLabel="Continue"
title="Continue"
onClick={async () => await this.acceptCodeOfConduct()}
tabIndex={0}
className="genericPaneSubmitBtn"
text="Continue"
disabled={!this.state.readCodeOfConduct}
/>
</Stack.Item>
</Stack>
);
}
}

View File

@@ -30,7 +30,7 @@ import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { Dialog, DialogProps } from "../Dialog"; import { Dialog, DialogProps } from "../Dialog";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import { CodeOfConductComponent } from "./CodeOfConductComponent"; import { CodeOfConduct } from "./CodeOfConduct/CodeOfConduct";
import "./GalleryViewerComponent.less"; import "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
@@ -372,7 +372,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
{acceptedCodeOfConduct === false && ( {acceptedCodeOfConduct === false && (
<Overlay isDarkThemed> <Overlay isDarkThemed>
<div className="publicGalleryTabOverlayContent"> <div className="publicGalleryTabOverlayContent">
<CodeOfConductComponent <CodeOfConduct
junoClient={this.props.junoClient} junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => { onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result }); this.setState({ isCodeOfConductAccepted: result });

View File

@@ -35,49 +35,6 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
"addDatabaseText": [Function],
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -1016,49 +973,6 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
"addDatabaseText": [Function],
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -2010,49 +1924,6 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
"addDatabaseText": [Function],
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
@@ -2991,49 +2862,6 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
"addDatabaseText": [Function],
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],

View File

@@ -52,7 +52,6 @@ import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager"; import type { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
@@ -68,7 +67,7 @@ import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksP
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel"; import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
@@ -101,7 +100,6 @@ export interface ExplorerParams {
export default class Explorer { export default class Explorer {
public addCollectionText: ko.Observable<string>; public addCollectionText: ko.Observable<string>;
public addDatabaseText: ko.Observable<string>;
public collectionTitle: ko.Observable<string>; public collectionTitle: ko.Observable<string>;
public deleteCollectionText: ko.Observable<string>; public deleteCollectionText: ko.Observable<string>;
public deleteDatabaseText: ko.Observable<string>; public deleteDatabaseText: ko.Observable<string>;
@@ -149,7 +147,6 @@ export default class Explorer {
public tabsManager: TabsManager; public tabsManager: TabsManager;
// Contextual panes // Contextual panes
public addDatabasePane: AddDatabasePane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient; private gitHubClient: GitHubClient;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
@@ -208,7 +205,6 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
this.addCollectionText = ko.observable<string>("New Collection"); this.addCollectionText = ko.observable<string>("New Collection");
this.addDatabaseText = ko.observable<string>("New Database");
this.collectionTitle = ko.observable<string>("Collections"); this.collectionTitle = ko.observable<string>("Collections");
this.collectionTreeNodeAltText = ko.observable<string>("Collection"); this.collectionTreeNodeAltText = ko.observable<string>("Collection");
this.deleteCollectionText = ko.observable<string>("Delete Collection"); this.deleteCollectionText = ko.observable<string>("Delete Collection");
@@ -401,13 +397,6 @@ export default class Explorer {
} }
}); });
this.addDatabasePane = new AddDatabasePane({
id: "adddatabasepane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane", id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -423,7 +412,6 @@ export default class Explorer {
} }
}); });
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
document.addEventListener( document.addEventListener(
@@ -441,7 +429,6 @@ export default class Explorer {
switch (userContext.apiType) { switch (userContext.apiType) {
case "SQL": case "SQL":
this.addCollectionText("New Container"); this.addCollectionText("New Container");
this.addDatabaseText("New Database");
this.collectionTitle("SQL API"); this.collectionTitle("SQL API");
this.collectionTreeNodeAltText("Container"); this.collectionTreeNodeAltText("Container");
this.deleteCollectionText("Delete Container"); this.deleteCollectionText("Delete Container");
@@ -450,7 +437,6 @@ export default class Explorer {
break; break;
case "Mongo": case "Mongo":
this.addCollectionText("New Collection"); this.addCollectionText("New Collection");
this.addDatabaseText("New Database");
this.collectionTitle("Collections"); this.collectionTitle("Collections");
this.collectionTreeNodeAltText("Collection"); this.collectionTreeNodeAltText("Collection");
this.deleteCollectionText("Delete Collection"); this.deleteCollectionText("Delete Collection");
@@ -459,7 +445,6 @@ export default class Explorer {
break; break;
case "Gremlin": case "Gremlin":
this.addCollectionText("New Graph"); this.addCollectionText("New Graph");
this.addDatabaseText("New Database");
this.deleteCollectionText("Delete Graph"); this.deleteCollectionText("Delete Graph");
this.deleteDatabaseText("Delete Database"); this.deleteDatabaseText("Delete Database");
this.collectionTitle("Gremlin API"); this.collectionTitle("Gremlin API");
@@ -468,7 +453,6 @@ export default class Explorer {
break; break;
case "Tables": case "Tables":
this.addCollectionText("New Table"); this.addCollectionText("New Table");
this.addDatabaseText("New Database");
this.deleteCollectionText("Delete Table"); this.deleteCollectionText("Delete Table");
this.deleteDatabaseText("Delete Database"); this.deleteDatabaseText("Delete Database");
this.collectionTitle("Azure Table API"); this.collectionTitle("Azure Table API");
@@ -478,7 +462,6 @@ export default class Explorer {
break; break;
case "Cassandra": case "Cassandra":
this.addCollectionText("New Table"); this.addCollectionText("New Table");
this.addDatabaseText("New Keyspace");
this.deleteCollectionText("Delete Table"); this.deleteCollectionText("Delete Table");
this.deleteDatabaseText("Delete Keyspace"); this.deleteDatabaseText("Delete Keyspace");
this.collectionTitle("Cassandra API"); this.collectionTitle("Cassandra API");
@@ -1384,7 +1367,7 @@ export default class Explorer {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else { } else {
this.openSidePanel( this.openSidePanel(
"", "Rename Notebook",
<StringInputPane <StringInputPane
explorer={this} explorer={this}
closePanel={() => { closePanel={() => {
@@ -1415,7 +1398,7 @@ export default class Explorer {
} }
this.openSidePanel( this.openSidePanel(
"", "Create new directory",
<StringInputPane <StringInputPane
explorer={this} explorer={this}
closePanel={() => { closePanel={() => {
@@ -1907,7 +1890,7 @@ export default class Explorer {
"Delete " + getDatabaseName(), "Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel <DeleteDatabaseConfirmationPanel
explorer={this} explorer={this}
openNotificationConsole={this.expandConsole} openNotificationConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel} closePanel={this.closeSidePanel}
selectedDatabase={this.findSelectedDatabase()} selectedDatabase={this.findSelectedDatabase()}
/> />
@@ -1949,19 +1932,14 @@ export default class Explorer {
); );
} }
public openAddDatabasePane(): void { public openAddDatabasePane(): void {
if (userContext.features.enableKOPanel) { this.openSidePanel(
this.addDatabasePane.open(); "New " + getDatabaseName(),
document.getElementById("linkAddDatabase").focus(); <AddDatabasePanel
} else { explorer={this}
this.openSidePanel( openNotificationConsole={() => this.expandConsole()}
"Add " + getDatabaseName(), closePanel={this.closeSidePanel}
<AddDatabasePanel />
explorer={this} );
openNotificationConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel}
/>
);
}
} }
public openBrowseQueriesPanel(): void { public openBrowseQueriesPanel(): void {

View File

@@ -132,6 +132,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onLabelChange(event); onLabelChange(event);
}} }}
autoFocus
/> />
<div className="actionCol"></div> <div className="actionCol"></div>
</div> </div>

View File

@@ -33,7 +33,6 @@ export class CommandBarComponentAdapter implements ReactAdapter {
container.deleteCollectionText, container.deleteCollectionText,
container.deleteDatabaseText, container.deleteDatabaseText,
container.addCollectionText, container.addCollectionText,
container.addDatabaseText,
container.isDatabaseNodeOrNoneSelected, container.isDatabaseNodeOrNoneSelected,
container.isDatabaseNodeSelected, container.isDatabaseNodeSelected,
container.isNoneSelected, container.isNoneSelected,

View File

@@ -126,7 +126,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addDatabaseText = ko.observable("mockText");
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
@@ -221,7 +220,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => { beforeAll(() => {
mockExplorer = {} as Explorer; mockExplorer = {} as Explorer;
mockExplorer.addDatabaseText = ko.observable("mockText");
mockExplorer.addCollectionText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {

View File

@@ -22,6 +22,7 @@ import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext"; import { configContext, Platform } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { getDatabaseName } from "../../../Utils/APITypeUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { OpenFullScreen } from "../../OpenFullScreen"; import { OpenFullScreen } from "../../OpenFullScreen";
@@ -261,7 +262,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
} }
function createNewDatabase(container: Explorer): CommandButtonComponentProps { function createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = container.addDatabaseText(); const label = "New " + getDatabaseName();
return { return {
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
iconAlt: label, iconAlt: label,

View File

@@ -1,174 +0,0 @@
<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 -->
<div class="contextual-pane-in">
<form data-bind="submit: submit" style="height: 100%">
<div
class="paneContentContainer"
role="dialog"
aria-labelledby="databaseTitle"
data-bind="template: { name: 'add-database-inputs' }"
></div>
</form>
</div>
<!-- Add database form -- End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>
<script type="text/html" id="add-database-inputs">
<!-- Add database header - Start -->
<div class="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
tabindex="0"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add database header - End -->
<!-- Add database errors - Start -->
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: formErrors() && formErrors() !== ''">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails, event: { keypress: onMoreDetailsKeyPress }"
tabindex="0"
>
More details</a
>
</span>
</div>
</div>
<!-- Add database errors - End -->
<!-- upsell message - start -->
<div
class="infoBoxContainer"
aria-live="assertive"
data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()"
>
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo" /></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a
class="underlinedLink"
id="linkAddDatabase"
data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank"
href=""
tabindex="0"
></a>
</span>
</div>
</div>
<!-- upsell message - end -->
<!-- Add database inputs - Start -->
<div class="paneMainContent">
<div>
<p>
<span class="mandatoryStar">*</span>
<span data-bind="text: databaseIdLabel"></span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth" data-bind="text: databaseIdTooltipText"></span>
</span>
</p>
<input
id="database-id"
type="text"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size="40"
class="collid"
data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { 'aria-label': databaseIdLabel, 'placeholder': databaseIdPlaceHolder }"
autofocus
/>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="New database provision support">
<input
tabindex="0"
type="checkbox"
id="addDatabasePane-databaseSharedThroughput"
title="Provision shared throughput"
data-bind="checked: databaseCreateNewShared"
/>
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span
class="tooltiptext provisionDatabaseThroughput"
data-bind="text: databaseLevelThroughputTooltipText"
></span>
</span>
</div>
<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
}"
>
</throughput-input-autopilot-v3>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade --><a
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request"
>Contact support</a
>
for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.
</p>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" value="OK" class="btncreatecoll1" />
</div>
</div>
<!-- Add database inputs - End -->
</script>

View File

@@ -1,105 +0,0 @@
import * as Constants from "../../Common/Constants";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
const mockDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: false,
},
type: undefined,
};
const mockFreeTierDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: true,
},
type: undefined,
};
describe("Add Database Pane", () => {
describe("getSharedThroughputDefault()", () => {
it("should be true if subscription type is Benefits", () => {
updateUserContext({
subscriptionType: SubscriptionType.Benefits,
});
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be false if subscription type is EA", () => {
updateUserContext({
subscriptionType: SubscriptionType.EA,
});
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
});
it("should be true if subscription type is Free", () => {
updateUserContext({
subscriptionType: SubscriptionType.Free,
});
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is Internal", () => {
updateUserContext({
subscriptionType: SubscriptionType.Internal,
});
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is PAYG", () => {
updateUserContext({
subscriptionType: SubscriptionType.PAYG,
});
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should display free tier text in upsell messaging", () => {
updateUserContext({ databaseAccount: mockFreeTierDatabaseAccount });
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.isFreeTierAccount()).toBe(true);
expect(addDatabasePane.upsellMessage()).toContain("With free tier");
expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
expect(addDatabasePane.upsellAnchorText()).toBe("Learn more");
});
it("should display standard texr in upsell messaging", () => {
updateUserContext({ databaseAccount: mockDatabaseAccount });
const explorer = new Explorer();
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.isFreeTierAccount()).toBe(false);
expect(addDatabasePane.upsellMessage()).toContain("Start at");
expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.cosmosPricing);
expect(addDatabasePane.upsellAnchorText()).toBe("More details");
});
});
});

View File

@@ -1,452 +0,0 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import * as ViewModels from "../../Contracts/ViewModels";
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 { ContextualPaneBase } from "./ContextualPaneBase";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
public databaseIdLabel: ko.Computed<string>;
public databaseIdPlaceHolder: ko.Computed<string>;
public databaseId: ko.Observable<string>;
public databaseIdTooltipText: ko.Computed<string>;
public databaseLevelThroughputTooltipText: ko.Computed<string>;
public databaseCreateNewShared: ko.Observable<boolean>;
public formErrorsDetails: ko.Observable<string>;
public throughput: ViewModels.Editable<number>;
public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>;
public maxThroughputRUText: ko.PureComputed<string>;
public throughputRangeText: ko.Computed<string>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public requestUnitsUsageCost: ko.Computed<string>;
public canRequestSupport: ko.PureComputed<boolean>;
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
public upsellAnchorUrl: ko.PureComputed<string>;
public upsellAnchorText: ko.PureComputed<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public maxAutoPilotThroughputSet: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public freeTierExceedThroughputTooltip: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>;
public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title((this.container && this.container.addDatabaseText()) || "New Database");
this.databaseId = ko.observable<string>();
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
// TODO 388844: get defaults from parent frame
this.databaseCreateNewShared = ko.observable<boolean>(this.getSharedThroughputDefault());
this.databaseIdLabel = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Keyspace id" : "Database id"
);
this.databaseIdPlaceHolder = ko.computed<string>(() =>
userContext.apiType === "Cassandra" ? "Type a new keyspace id" : "Type a new database id"
);
this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${
isCassandraAccount ? "tables" : "collections"
}`;
});
this.databaseLevelThroughputTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
return `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
});
this.throughput = editable.observable<number>();
this.maxThroughputRU = ko.observable<number>();
this.minThroughputRU = ko.observable<number>();
this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false);
this.isAutoPilotSelected = ko.observable<boolean>(false);
this.maxAutoPilotThroughputSet = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (!autoPilot) {
return "";
}
return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */);
});
this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.requestUnitsUsageCost = ko.computed(() => {
const offerThroughput: number = this.throughput();
if (
offerThroughput < this.minThroughputRU() ||
(offerThroughput > this.maxThroughputRU() && !this.canExceedMaximumValue())
) {
return "";
}
const { databaseAccount: account } = userContext;
if (!account) {
return "";
}
const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
let estimatedSpendAcknowledge: string;
let estimatedSpend: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput,
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.maxAutoPilotThroughputSet(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.maxAutoPilotThroughputSet(),
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
}
// TODO: change throughputSpendAckText to be a computed value, instead of having this side effect
this.throughputSpendAckText(estimatedSpendAcknowledge);
return estimatedSpend;
});
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.isFreeTierAccount = ko.computed<boolean>(() => {
return userContext?.databaseAccount?.properties?.enableFreeTier;
});
this.showUpsellMessage = ko.pureComputed(() => {
if (this.container.isServerlessEnabled()) {
return false;
}
if (this.isFreeTierAccount()) {
return this.databaseCreateNewShared();
}
return true;
});
this.maxThroughputRUText = ko.pureComputed(() => {
return this.maxThroughputRU().toLocaleString();
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
const selectedThroughput: number = this.throughput();
const maxRU: number = this.maxThroughputRU && this.maxThroughputRU();
const isMaxRUGreaterThanDefault: boolean = maxRU > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
const isThroughputSetGreaterThanDefault: boolean =
selectedThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
if (this.canExceedMaximumValue()) {
return isThroughputSetGreaterThanDefault;
}
return isThroughputSetGreaterThanDefault && isMaxRUGreaterThanDefault;
});
this.databaseCreateNewShared.subscribe((useShared: boolean) => {
this._updateThroughputLimitByDatabase();
});
this.resetData();
this.freeTierExceedThroughputTooltip = ko.pureComputed<string>(() =>
this.isFreeTierAccount() && !this.container.isFirstResourceCreated()
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: ""
);
this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage(
userContext.portalEnv,
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
false
);
});
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`;
});
this.upsellAnchorUrl = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
});
this.upsellAnchorText = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? "Learn more" : "More details";
});
}
public onMoreDetailsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.showErrorDetails();
return false;
}
return true;
};
public open() {
super.open();
this.resetData();
const addDatabasePaneOpenMessage = {
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const focusElement = document.getElementById("database-id");
focusElement && focusElement.focus();
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
}
public submit() {
if (!this._isValid()) {
return;
}
const offerThroughput: number = this._computeOfferThroughput();
const addDatabasePaneStartMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
this.formErrors("");
this.isExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = {
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (this.isAutoPilotSelected()) {
createDatabaseParams.autoPilotMaxThroughput = this.maxAutoPilotThroughputSet();
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey);
},
(error: any) => {
this._onCreateDatabaseFailure(error, offerThroughput, startKey);
}
);
}
public resetData() {
this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
this._updateThroughputLimitByDatabase();
this.throughputSpendAck(false);
super.resetData();
}
public getSharedThroughputDefault(): boolean {
const { subscriptionType } = userContext;
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
return true;
}
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
const addDatabasePaneSuccessMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.traceSuccess(Action.CreateDatabase, addDatabasePaneSuccessMessage, startKey);
this.resetData();
}
private _onCreateDatabaseFailure(error: any, offerThroughput: number, startKey: number): void {
this.isExecuting(false);
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = {
database: ko.toJS({
id: this.databaseId(),
shared: this.databaseCreateNewShared(),
}),
offerThroughput: offerThroughput,
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}
private _getThroughput(): number {
const throughput: number = this.throughput();
return isNaN(throughput) ? 0 : Number(throughput);
}
private _computeOfferThroughput(): number {
if (!this.canConfigureThroughput()) {
return undefined;
}
if (this.isAutoPilotSelected()) {
return undefined;
}
return this._getThroughput();
}
private _isValid(): boolean {
// TODO add feature flag that disables validation for customers with custom accounts
if (this.isAutoPilotSelected()) {
const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
this.formErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
const throughput = this._getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (
this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
return true;
}
private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings {
if (this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) {
return {
maxThroughput: this.maxAutoPilotThroughputSet() * 1,
};
}
return undefined;
}
private _updateThroughputLimitByDatabase() {
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.throughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin);
}
}

View File

@@ -9,10 +9,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
interface Location { interface Location {
@@ -42,7 +39,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
}: CopyNotebookPanelProps) => { }: CopyNotebookPanelProps) => {
const [isExecuting, setIsExecuting] = useState<boolean>(); const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>(); const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
const [selectedLocation, setSelectedLocation] = useState<Location>(); const [selectedLocation, setSelectedLocation] = useState<Location>();
@@ -92,7 +88,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError(`Failed to copy ${name} to ${destination}`); setFormError(`Failed to copy ${name} to ${destination}`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally { } finally {
clearMessage && clearMessage(); clearMessage && clearMessage();
@@ -130,14 +125,10 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
setSelectedLocation(option?.data); setSelectedLocation(option?.data);
}; };
const genericPaneProps: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError, formError,
formErrorDetail,
id: "copynotebookpane",
isExecuting: isExecuting, isExecuting: isExecuting,
title: "Copy notebook",
submitButtonText: "OK", submitButtonText: "OK",
onClose: closePanel,
onSubmit: () => submit(), onSubmit: () => submit(),
expandConsole: () => container.expandConsole(), expandConsole: () => container.expandConsole(),
}; };
@@ -149,8 +140,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} /> <CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -130,8 +130,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: selectedCollectionId } }); .simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount(); wrapper.unmount();
@@ -151,8 +151,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: feedbackText } }); .simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(

View File

@@ -12,10 +12,7 @@ import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils"; import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
@@ -35,7 +32,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
}; };
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = "Delete " + collectionName;
const submit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
const collection = explorer.findSelectedCollection(); const collection = explorer.findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) { if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName; const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
@@ -100,19 +97,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
); );
} }
}; };
const genericPaneProps: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
formErrorDetail: formError,
id: "deleteCollectionpane",
isExecuting, isExecuting,
title: paneTitle,
submitButtonText: "OK", submitButtonText: "OK",
onClose: closePanel, onSubmit,
onSubmit: submit,
expandConsole: () => explorer.expandConsole(), expandConsole: () => explorer.expandConsole(),
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
@@ -150,6 +143,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
)} )}
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -98,8 +98,8 @@ describe("Delete Database Confirmation Pane", () => {
.find("#confirmDatabaseId") .find("#confirmDatabaseId")
.hostNodes() .hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } }); .simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists("button")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); wrapper.find("button").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId); expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
wrapper.unmount(); wrapper.unmount();
}); });

View File

@@ -1,5 +1,5 @@
import { useBoolean } from "@fluentui/react-hooks";
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
@@ -12,9 +12,8 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface DeleteDatabaseConfirmationPanelProps { interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -23,36 +22,19 @@ interface DeleteDatabaseConfirmationPanelProps {
selectedDatabase: Database; selectedDatabase: Database;
} }
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ( export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
props: DeleteDatabaseConfirmationPanelProps explorer,
): JSX.Element => { openNotificationConsole,
closePanel,
selectedDatabase,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>(""); const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>(""); const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const getPanelErrorProps = (): PanelInfoErrorProps => { const submit = async (): Promise<void> => {
if (formError) {
return {
messageType: "error",
message: formError,
showErrorDetails: true,
openNotificationConsole: props.openNotificationConsole,
};
}
return {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
};
const submit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
const { selectedDatabase, explorer } = props;
event.preventDefault();
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError("Input database name does not match the selected database"); setFormError("Input database name does not match the selected database");
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`); logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
@@ -69,7 +51,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
try { try {
await deleteDatabase(selectedDatabase.id()); await deleteDatabase(selectedDatabase.id());
props.closePanel(); closePanel();
explorer.refreshAllDatabases(); explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id()); explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined); explorer.selectedNode(undefined);
@@ -121,13 +103,27 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}; };
const shouldRecordFeedback = (): boolean => { const shouldRecordFeedback = (): boolean => {
const { explorer } = props;
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared()); return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
}; };
const props: RightPaneFormProps = {
formError,
isExecuting: isLoading,
submitButtonText: "OK",
onSubmit: () => submit(),
expandConsole: openNotificationConsole,
};
const errorProps: PanelInfoErrorProps = {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
return ( return (
<form className="panelFormWrapper" onSubmit={submit}> <RightPaneForm {...props}>
<PanelInfoErrorComponent {...getPanelErrorProps()} /> {!formError && <PanelInfoErrorComponent {...errorProps} />}
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
@@ -161,8 +157,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
</div> </div>
)} )}
</div> </div>
<PanelFooterComponent buttonLabel="OK" /> </RightPaneForm>
{isLoading && <PanelLoadingScreen />}
</form>
); );
}; };

View File

@@ -1,12 +1,10 @@
import { useBoolean } from "@fluentui/react-hooks";
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react"; import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Tree/StoredProcedure"; import StoredProcedure from "../../Tree/StoredProcedure";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { InputParameter } from "./InputParameter"; import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps { interface ExecuteSprocParamsPaneProps {
@@ -35,24 +33,11 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as "" const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" }); const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedKey(item); setSelectedKey(item);
}; };
const genericPaneProps: GenericRightPaneProps = {
expandConsole,
formError: formError,
formErrorDetail: formErrorsDetails,
id: "executesprocparamspane",
isExecuting: isLoading,
title: "Input parameters",
submitButtonText: "Execute",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const validateUnwrappedParams = (): boolean => { const validateUnwrappedParams = (): boolean => {
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues; const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
for (let i = 0; i < unwrappedParams.length; i++) { for (let i = 0; i < unwrappedParams.length; i++) {
@@ -66,7 +51,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
const setInvalidParamError = (invalidParam: string): void => { const setInvalidParamError = (invalidParam: string): void => {
setFormError(`Invalid param specified: ${invalidParam}`); setFormError(`Invalid param specified: ${invalidParam}`);
setFormErrorsDetails(`Invalid param specified: ${invalidParam} is not a valid literal value`); logConsoleError(`Invalid param specified: ${invalidParam} is not a valid literal value`);
}; };
const submit = (): void => { const submit = (): void => {
@@ -128,8 +113,16 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
setParamKeyValues(cloneParamKeyValue); setParamKeyValues(cloneParamKeyValue);
}; };
const props: RightPaneFormProps = {
expandConsole,
formError: formError,
isExecuting: isLoading,
submitButtonText: "Execute",
onSubmit: () => submit(),
};
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<InputParameter <InputParameter
@@ -169,6 +162,6 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
</Stack> </Stack>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -1,126 +0,0 @@
import { IconButton, PrimaryButton } from "@fluentui/react";
import React, { FunctionComponent, ReactNode } from "react";
import ErrorRedIcon from "../../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../../Common/Constants";
export interface GenericRightPaneProps {
expandConsole: () => void;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonHidden?: boolean;
children?: ReactNode;
}
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
expandConsole,
formError,
formErrorDetail,
id,
isExecuting,
onClose,
onSubmit,
submitButtonText,
title,
isSubmitButtonHidden,
children,
}: GenericRightPaneProps) => {
const getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
const panelHeight: number = getPanelHeight();
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"
onClick={onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
const renderErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={formError}>
{formError}
</span>
<a className="errorLink" role="link" hidden={!formErrorDetail} onClick={expandConsole}>
More details
</a>
</span>
</div>
</div>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={submitButtonText}
/>
</div>
</div>
);
};
const renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
onClose();
event.stopPropagation();
}
};
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()}
{renderErrorSection()}
{children}
{renderPanelFooter()}
</div>
{renderLoadingScreen()}
</div>
</div>
);
};

View File

@@ -24,49 +24,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
"addDatabaseText": [Function],
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],

View File

@@ -1,5 +1,5 @@
import { useBoolean } from "@fluentui/react-hooks";
import { IImageProps, Image, ImageFit, Stack, TextField } from "@fluentui/react"; import { IImageProps, Image, ImageFit, Stack, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import folderIcon from "../../../../images/folder_16x16.svg"; import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger"; import { logError } from "../../../Common/Logger";
@@ -8,10 +8,7 @@ import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
interface LoadQueryPaneProps { interface LoadQueryPaneProps {
explorer: Explorer; explorer: Explorer;
@@ -24,7 +21,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
}: LoadQueryPaneProps): JSX.Element => { }: LoadQueryPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [selectedFileName, setSelectedFileName] = useState<string>(""); const [selectedFileName, setSelectedFileName] = useState<string>("");
const [selectedFiles, setSelectedFiles] = useState<FileList>(); const [selectedFiles, setSelectedFiles] = useState<FileList>();
@@ -35,19 +31,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
className: "fileIcon", className: "fileIcon",
}; };
const title = "Load Query";
const genericPaneProps: GenericRightPaneProps = {
expandConsole: () => explorer.expandConsole(),
formError: formError,
formErrorDetail: formErrorsDetails,
id: "loadQueryPane",
isExecuting: isLoading,
title,
submitButtonText: "Load",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const onFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => { const onFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { files } = e.target; const { files } = e.target;
setSelectedFiles(files); setSelectedFiles(files);
@@ -56,10 +39,8 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
setFormErrorsDetails("");
if (!selectedFiles || selectedFiles.length === 0) { if (!selectedFiles || selectedFiles.length === 0) {
setFormError("No file specified"); setFormError("No file specified");
setFormErrorsDetails("No file specified. Please input a file.");
logConsoleError("Could not load query -- No file specified. Please input a file."); logConsoleError("Could not load query -- No file specified. Please input a file.");
return; return;
} }
@@ -75,7 +56,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
} catch (error) { } catch (error) {
setLoadingFalse(); setLoadingFalse();
setFormError("Failed to load query"); setFormError("Failed to load query");
setFormErrorsDetails(`Failed to load query: ${error}`);
logConsoleError(`Failed to load query from file ${file.name}: ${error}`); logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
} }
}; };
@@ -100,14 +80,20 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
reader.onerror = (): void => { reader.onerror = (): void => {
setFormError("Failed to load query"); setFormError("Failed to load query");
setFormErrorsDetails(`Failed to load query`);
logConsoleError(`Failed to load query from file ${file.name}`); logConsoleError(`Failed to load query from file ${file.name}`);
}; };
return reader.readAsText(file); return reader.readAsText(file);
}; };
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: "Load",
onSubmit: () => submit(),
expandConsole: () => explorer.expandConsole(),
};
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<Stack horizontal> <Stack horizontal>
@@ -132,6 +118,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
</Stack> </Stack>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Load Query Pane should render Default properly 1`] = ` exports[`Load Query Pane should render Default properly 1`] = `
<GenericRightPaneComponent <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
id="loadQueryPane"
isExecuting={false} isExecuting={false}
onClose={[Function]}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Load" submitButtonText="Load"
title="Load Query"
> >
<div <div
className="panelFormWrapper" className="panelFormWrapper"
@@ -58,5 +54,5 @@ exports[`Load Query Pane should render Default properly 1`] = `
</Stack> </Stack>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
`; `;

View File

@@ -1,4 +1,4 @@
import { shallow, ShallowWrapper } from "enzyme"; import { mount, shallow, ShallowWrapper } from "enzyme";
import React from "react"; import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -36,7 +36,7 @@ describe("New Vertex Panel", () => {
it("should call form submit method", () => { it("should call form submit method", () => {
const onSubmitSpy = jest.fn(); const onSubmitSpy = jest.fn();
const newWrapper = shallow( const newWrapper = mount(
<NewVertexPanel <NewVertexPanel
explorer={fakeExplorer} explorer={fakeExplorer}
partitionKeyPropertyProp={undefined} partitionKeyPropertyProp={undefined}
@@ -61,7 +61,7 @@ describe("New Vertex Panel", () => {
const result = onSubmitSpy(fakeNewVertexData, onErrorSpy, onSuccessSpy); const result = onSubmitSpy(fakeNewVertexData, onErrorSpy, onSuccessSpy);
const newWrapper = shallow( const newWrapper = mount(
<NewVertexPanel <NewVertexPanel
explorer={fakeExplorer} explorer={fakeExplorer}
partitionKeyPropertyProp={undefined} partitionKeyPropertyProp={undefined}

View File

@@ -3,9 +3,7 @@ import React, { FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent"; import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent";
import { PanelFooterComponent } from "../PanelFooterComponent"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
export interface INewVertexPanelProps { export interface INewVertexPanelProps {
explorer: Explorer; explorer: Explorer;
partitionKeyPropertyProp: string; partitionKeyPropertyProp: string;
@@ -21,14 +19,10 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
}: INewVertexPanelProps): JSX.Element => { }: INewVertexPanelProps): JSX.Element => {
let newVertexDataValue: ViewModels.NewVertexData; let newVertexDataValue: ViewModels.NewVertexData;
const [errorMessage, setErrorMessage] = useState<string>(""); const [errorMessage, setErrorMessage] = useState<string>("");
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const buttonLabel = "OK";
const submit = (event: React.MouseEvent<HTMLFormElement>) => { const submit = () => {
event.preventDefault();
setErrorMessage(undefined); setErrorMessage(undefined);
setShowErrorDetails(false);
if (onSubmit !== undefined) { if (onSubmit !== undefined) {
setLoadingTrue(); setLoadingTrue();
onSubmit(newVertexDataValue, onError, onSuccess); onSubmit(newVertexDataValue, onError, onSuccess);
@@ -37,7 +31,6 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const onError = (errorMsg: string) => { const onError = (errorMsg: string) => {
setErrorMessage(errorMsg); setErrorMessage(errorMsg);
setShowErrorDetails(true);
setLoadingFalse(); setLoadingFalse();
}; };
@@ -49,17 +42,16 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const onChange = (newVertexData: ViewModels.NewVertexData) => { const onChange = (newVertexData: ViewModels.NewVertexData) => {
newVertexDataValue = newVertexData; newVertexDataValue = newVertexData;
}; };
const props: RightPaneFormProps = {
formError: errorMessage,
isExecuting: isLoading,
submitButtonText: "OK",
onSubmit: () => submit(),
expandConsole: openNotificationConsole,
};
return ( return (
<form className="panelFormWrapper" onSubmit={(event: React.MouseEvent<HTMLFormElement>) => submit(event)}> <RightPaneForm {...props}>
{errorMessage && (
<PanelInfoErrorComponent
message={errorMessage}
messageType="error"
showErrorDetails={showErrorDetails}
openNotificationConsole={openNotificationConsole}
/>
)}
<div className="panelMainContent"> <div className="panelMainContent">
<NewVertexComponent <NewVertexComponent
newVertexDataProp={newVertexDataValue} newVertexDataProp={newVertexDataValue}
@@ -67,8 +59,6 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
onChangeProp={onChange} onChangeProp={onChange}
/> />
</div> </div>
<PanelFooterComponent buttonLabel={buttonLabel} /> </RightPaneForm>
{isLoading && <PanelLoadingScreen />}
</form>
); );
}; };

View File

@@ -1,9 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`New Vertex Panel should render default property 1`] = ` exports[`New Vertex Panel should render default property 1`] = `
<form <RightPaneForm
className="panelFormWrapper" expandConsole={[Function]}
formError=""
isExecuting={false}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="OK"
> >
<div <div
className="panelMainContent" className="panelMainContent"
@@ -13,8 +16,5 @@ exports[`New Vertex Panel should render default property 1`] = `
partitionKeyPropertyProp="" partitionKeyPropertyProp=""
/> />
</div> </div>
<PanelFooterComponent </RightPaneForm>
buttonLabel="OK"
/>
</form>
`; `;

View File

@@ -1,4 +1,3 @@
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html"; import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
export class PaneComponent { export class PaneComponent {
constructor(data: any) { constructor(data: any) {
@@ -6,15 +5,6 @@ export class PaneComponent {
} }
} }
export class AddDatabasePaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: AddDatabasePaneTemplate,
};
}
}
export class CassandraAddCollectionPaneComponent { export class CassandraAddCollectionPaneComponent {
constructor() { constructor() {
return { return {

View File

@@ -8,6 +8,7 @@ export interface PanelInfoErrorProps {
link?: string; link?: string;
linkText?: string; linkText?: string;
openNotificationConsole?: () => void; openNotificationConsole?: () => void;
formError?: boolean;
} }
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({

View File

@@ -7,15 +7,12 @@ import { JunoClient } from "../../../Juno/JunoClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfConductComponent"; import { CodeOfConduct } from "../../Controls/NotebookGallery/CodeOfConduct/CodeOfConduct";
import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types"; import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
export interface PublishNotebookPaneAProps { export interface PublishNotebookPaneAProps {
@@ -155,7 +152,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
clearPublishingMessage(); clearPublishingMessage();
setIsExecuting(false); setIsExecuting(false);
} }
closePanel(); closePanel();
}; };
@@ -170,15 +166,11 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
setFormErrorDetail(""); setFormErrorDetail("");
}; };
const props: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
formErrorDetail: formErrorDetail,
id: "publishnotebookpane",
isExecuting: isExecuting, isExecuting: isExecuting,
title: "Publish to gallery",
submitButtonText: "Publish", submitButtonText: "Publish",
onSubmit: () => submit(), onSubmit: () => submit(),
onClose: closePanel,
expandConsole: () => container.expandConsole(), expandConsole: () => container.expandConsole(),
isSubmitButtonHidden: !isCodeOfConductAccepted, isSubmitButtonHidden: !isCodeOfConductAccepted,
}; };
@@ -201,10 +193,10 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
onTakeSnapshot, onTakeSnapshot,
}; };
return ( return (
<GenericRightPaneComponent {...props}> <RightPaneForm {...props}>
{!isCodeOfConductAccepted ? ( {!isCodeOfConductAccepted ? (
<div style={{ padding: "25px", marginTop: "10px" }}> <div style={{ padding: "25px", marginTop: "10px" }}>
<CodeOfConductComponent <CodeOfConduct
junoClient={junoClient} junoClient={junoClient}
onAcceptCodeOfConduct={(isAccepted) => { onAcceptCodeOfConduct={(isAccepted) => {
setIsCodeOfConductAccepted(isAccepted); setIsCodeOfConductAccepted(isAccepted);
@@ -214,6 +206,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
) : ( ) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} /> <PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)} )}
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { useBoolean } from "@fluentui/react-hooks";
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Areas, SavedQueries } from "../../../Common/Constants"; import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
@@ -9,10 +9,7 @@ import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetr
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
interface SaveQueryPaneProps { interface SaveQueryPaneProps {
explorer: Explorer; explorer: Explorer;
@@ -25,32 +22,16 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
}: SaveQueryPaneProps): JSX.Element => { }: SaveQueryPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [queryName, setQueryName] = useState<string>(""); const [queryName, setQueryName] = useState<string>("");
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
const title = "Save Query"; const title = "Save Query";
const { canSaveQueries } = explorer; const { canSaveQueries } = explorer;
const genericPaneProps: GenericRightPaneProps = {
expandConsole: () => explorer.expandConsole(),
formError: formError,
formErrorDetail: formErrorsDetails,
id: "saveQueryPane",
isExecuting: isLoading,
title,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
onClose: () => closePanel(),
onSubmit: () => {
canSaveQueries() ? submit() : setupQueries();
},
};
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
setFormErrorsDetails("");
if (!canSaveQueries()) { if (!canSaveQueries()) {
setFormError("Cannot save query"); setFormError("Cannot save query");
setFormErrorsDetails("Failed to save query: account not set up to save queries");
logConsoleError("Failed to save query: account not setup to save queries"); logConsoleError("Failed to save query: account not setup to save queries");
} }
@@ -148,8 +129,17 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
} }
}; };
const props: RightPaneFormProps = {
expandConsole: () => explorer.expandConsole(),
formError: formError,
isExecuting: isLoading,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
onSubmit: () => {
canSaveQueries() ? submit() : setupQueries();
},
};
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
{!canSaveQueries() ? ( {!canSaveQueries() ? (
@@ -158,6 +148,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
<TextField <TextField
id="saveQueryInput" id="saveQueryInput"
label="Name" label="Name"
autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setQueryName(newInput); setQueryName(newInput);
@@ -166,6 +157,6 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
)} )}
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Save Query Pane should render Default properly 1`] = ` exports[`Save Query Pane should render Default properly 1`] = `
<GenericRightPaneComponent <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
id="saveQueryPane"
isExecuting={false} isExecuting={false}
onClose={[Function]}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Complete setup" submitButtonText="Complete setup"
title="Save Query"
> >
<div <div
className="panelFormWrapper" className="panelFormWrapper"
@@ -25,5 +21,5 @@ exports[`Save Query Pane should render Default properly 1`] = `
</Text> </Text>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
`; `;

View File

@@ -6,10 +6,7 @@ import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab"; import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface StringInputPanelProps { export interface StringInputPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -40,7 +37,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
}: StringInputPanelProps): JSX.Element => { }: StringInputPanelProps): JSX.Element => {
const [stringInput, setStringInput] = useState<string>(defaultInput); const [stringInput, setStringInput] = useState<string>(defaultInput);
const [formErrors, setFormErrors] = useState<string>(""); const [formErrors, setFormErrors] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
@@ -51,7 +47,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
return; return;
} else { } else {
setFormErrors(""); setFormErrors("");
setFormErrorsDetails("");
} }
const clearMessage = logConsoleProgress(`${inProgressMessage} ${stringInput}`); const clearMessage = logConsoleProgress(`${inProgressMessage} ${stringInput}`);
@@ -78,32 +73,26 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
error = JSON.stringify(reason); error = JSON.stringify(reason);
} }
// If it's an AjaxError (AjaxObservable), add more error
if (reason?.response?.message) { if (reason?.response?.message) {
error += `. ${reason.response.message}`; error += `. ${reason.response.message}`;
} }
setFormErrors(errorMessage); setFormErrors(errorMessage);
setFormErrorsDetails(`${errorMessage}: ${error}`);
logConsoleError(`${errorMessage} ${stringInput}: ${error}`); logConsoleError(`${errorMessage} ${stringInput}: ${error}`);
} finally { } finally {
setIsExecuting(false); setIsExecuting(false);
clearMessage(); clearMessage();
} }
}; };
const genericPaneProps: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError: formErrors, formError: formErrors,
formErrorDetail: formErrorsDetails,
id: "stringInputPane",
isExecuting: isExecuting, isExecuting: isExecuting,
title: paneTitle,
submitButtonText: submitButtonLabel, submitButtonText: submitButtonLabel,
onClose: closePanel,
onSubmit: submit, onSubmit: submit,
expandConsole: () => container.expandConsole(), expandConsole: () => container.expandConsole(),
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
<TextField <TextField
label={inputLabel} label={inputLabel}
@@ -117,6 +106,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
aria-label={inputLabel} aria-label={inputLabel}
/> />
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -3,7 +3,7 @@ import * as ko from "knockout";
import React from "react"; import React from "react";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel"; import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { TableQuerySelectPanel } from "./index"; import { TableQuerySelectPanel } from "./TableQuerySelectPanel";
describe("Table query select Panel", () => { describe("Table query select Panel", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;

View File

@@ -4,10 +4,7 @@ import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import * as Constants from "../../../Tables/Constants"; import * as Constants from "../../../Tables/Constants";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel"; import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { import { RightPaneForm, RightPaneFormProps } from "../../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../../GenericRightPaneComponent/GenericRightPaneComponent";
interface TableQuerySelectPanelProps { interface TableQuerySelectPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -31,24 +28,20 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
]); ]);
const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true); const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true);
const genericPaneProps: GenericRightPaneProps = { const onSubmit = (): void => {
formError: "",
formErrorDetail: "",
id: "querySelectPane",
isExecuting: false,
title: "Select Column",
submitButtonText: "OK",
onClose: () => closePanel(),
onSubmit: () => submit(),
expandConsole: () => explorer.expandConsole(),
};
const submit = (): void => {
queryViewModel.selectText(getParameters()); queryViewModel.selectText(getParameters());
queryViewModel.getSelectMessage(); queryViewModel.getSelectMessage();
closePanel(); closePanel();
}; };
const props: RightPaneFormProps = {
formError: "",
isExecuting: false,
submitButtonText: "OK",
onSubmit,
expandConsole: () => explorer.expandConsole(),
};
const handleClick = (isChecked: boolean, selectedColumn: string): void => { const handleClick = (isChecked: boolean, selectedColumn: string): void => {
const columns = columnOptions.map((column) => { const columns = columnOptions.map((column) => {
if (column.columnName === selectedColumn) { if (column.columnName === selectedColumn) {
@@ -128,7 +121,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<Text>Select the columns that you want to query.</Text> <Text>Select the columns that you want to query.</Text>
@@ -153,6 +146,6 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
</div> </div>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };

View File

@@ -78,7 +78,7 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
return uploadFile(file.name, fileContent); return uploadFile(file.name, fileContent);
}; };
const genericPaneProps: RightPaneFormProps = { const props: RightPaneFormProps = {
expandConsole, expandConsole,
formError: formErrors, formError: formErrors,
isExecuting: isExecuting, isExecuting: isExecuting,
@@ -87,7 +87,7 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
}; };
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} /> <Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
</div> </div>

View File

@@ -50,7 +50,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
setFiles(event.target.files); setFiles(event.target.files);
}; };
const genericPaneProps: RightPaneFormProps = { const props: RightPaneFormProps = {
expandConsole: () => explorer.expandConsole(), expandConsole: () => explorer.expandConsole(),
formError, formError,
isExecuting: isExecuting, isExecuting: isExecuting,
@@ -89,7 +89,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
}; };
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
<Upload <Upload
label="Select JSON Files" label="Select JSON Files"

View File

@@ -17,6 +17,7 @@ import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getDatabaseName } from "../../Utils/APITypeUtils";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher"; import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil"; import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -290,7 +291,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
} else { } else {
items.push({ items.push({
iconSrc: AddDatabaseIcon, iconSrc: AddDatabaseIcon,
title: this.container.addDatabaseText(), title: "New " + getDatabaseName(),
description: null, description: null,
onClick: () => this.container.openAddDatabasePane(), onClick: () => this.container.openAddDatabasePane(),
}); });

View File

@@ -154,7 +154,6 @@ const App: React.FunctionComponent = () => {
closePanel={closeSidePanel} closePanel={closeSidePanel}
isConsoleExpanded={isNotificationConsoleExpanded} isConsoleExpanded={isNotificationConsoleExpanded}
/> />
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' /> <div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
{showDialog && <Dialog {...dialogProps} />} {showDialog && <Dialog {...dialogProps} />}
</div> </div>