Compare commits

..

48 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
Steve Faulkner
f9e8b5eaaa Remove Unused Knockout Components (#783) 2021-05-13 18:03:29 -05:00
vaidankarswapnil
a6b82c8340 Migrate graph style panel to react (#619)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-13 15:45:00 -05:00
Tanuj Mittal
404b1fc0f1 Prep Schema Analyzer for flighting (#760)
* Prepare for flighting Schema Analyzer

* Rename SchemaAnalyzerComponent -> SchemaAnalyzer

* Only show Schema option if notebooks enabled
2021-05-13 10:34:09 -07:00
Sunil Kumar Yadav
d7c62ac7f1 Migrate Collapse/Open Resource Tree to React (#733)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-12 20:03:52 -05:00
Sunil Kumar Yadav
8e6d274b11 Add Files to TypeScript Strict (#776)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-12 19:56:48 -05:00
Sunil Kumar Yadav
2d506f0312 Add Files to TypeScript Strict Mode (#777) 2021-05-12 19:23:10 -05:00
victor-meng
d76aaca0dd Improve lazy load database/collection offer logic (#768) 2021-05-12 19:13:15 -05:00
victor-meng
14e58e5519 Batch of small fixes for RightPaneForm and AddDatabasePane components (#780) 2021-05-12 19:12:03 -05:00
victor-meng
2f6dbd83f3 Fix throughput input component and add database panel (#773) 2021-05-12 13:56:24 -05:00
Steve Faulkner
0a6c7c0ff9 Add Mongo 3.2 End to End Test (#779) 2021-05-12 13:41:44 -05:00
Steve Faulkner
66281447df Pass undefined analyticalTTL if Synapse is disabled (#778) 2021-05-12 11:49:25 -05:00
victor-meng
c5f76ac2a9 Fix isFixedCollectionWithSharedThroughputSupported flag (#774) 2021-05-12 09:16:13 -05:00
Laurent Nguyen
861042c27e Fix bug publish screenshot (#762)
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/762?feature.someFeatureFlagYouMightNeed=true)

The main change in this PR fixes the snapshot functionality in the Publish pane-related components. Because the code cell outputs are now rendered in their own iframes for security reasons, a single snapshot of the notebook is no longer possible: each cell output takes its own snapshot and the snapshots are collated on the main notebook snapshot.
- Move the snapshot functionality to notebook components: this removes the reference of the notebook DOM node that we must pass to the Publish pane via explorer.
- Add slice in the state and actions in notebook redux for notebook snapshot requests and result
- Add post robot message to take snapshots and receive results
- Add logic in `NotebookRenderer` to wait for all output snapshots done before taking the main one collating.
- Use `zustand` to share snapshot between Redux world and React world. This solves the issue of keeping the `PanelContainer` component generic, while being able to update its children (`PublishPanel` component) with the new snapshot.

Additional changes:
- Add `local()` in `@font-face` to check if font is already installed before downloading the font (must be done for Safari, but not Edge/Chrome)
- Add "Export output to image" menu item in notebook cell, since each cell output can take its own snapshot (which can be downloaded)
![image](https://user-images.githubusercontent.com/21954022/117454706-b5f16600-af46-11eb-8535-6bf99f3d9170.png)
2021-05-11 18:24:05 +00:00
victor-meng
4ed8fe9e7d Remove old add collection pane (#767) 2021-05-10 20:10:48 -05:00
Srinath Narayanan
4c506da7b9 Added metrics blade link and fixed SelfServe bugs (#764)
* Added metrics blade link

* fixed lint error
2021-05-10 17:36:50 -07:00
Hardikkumar Nai
a81b1a40a3 Use @fluentui/react DocumentCard (#715)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-10 14:17:37 -05:00
Hardikkumar Nai
9d5c9d6296 Migrate Add Database Panel to React (#597)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-10 14:02:14 -05:00
Steve Faulkner
7efa8ca58f Remove unused Switch Directory Pane (#766) 2021-05-09 22:37:18 -05:00
Hardikkumar Nai
487fbf2072 Remove genericRightPaneComponent and create RightPaneWrapper with form (#679)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-09 19:22:44 -05:00
Sunil Kumar Yadav
aa308b3e4d Enable TypeScript noImplicitThis (#761) 2021-05-07 10:25:19 -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
245 changed files with 21988 additions and 30047 deletions

View File

@@ -54,7 +54,6 @@ src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
src/Explorer/Controls/ThroughputInput/ThroughputInput.test.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
src/Explorer/Controls/Toolbar/IToolbarAction.ts src/Explorer/Controls/Toolbar/IToolbarAction.ts
@@ -85,8 +84,8 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts # src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts # src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
@@ -109,16 +108,15 @@ src/Explorer/Notebook/NotebookUtil.ts
src/Explorer/OpenActions.test.ts src/Explorer/OpenActions.test.ts
src/Explorer/OpenActions.ts src/Explorer/OpenActions.ts
src/Explorer/OpenActionsStubs.ts src/Explorer/OpenActionsStubs.ts
src/Explorer/Panes/AddCollectionPane.test.ts
src/Explorer/Panes/AddCollectionPane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/AddDatabasePane.ts src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/BrowseQueriesPane.ts src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/CassandraAddCollectionPane.ts src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts # src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SetupNotebooksPane.ts

View File

@@ -141,6 +141,7 @@ jobs:
- ./test/graph/container.spec.ts - ./test/graph/container.spec.ts
- ./test/sql/container.spec.ts - ./test/sql/container.spec.ts
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts - ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts

View File

@@ -4,7 +4,7 @@
@font-face { @font-face {
font-family: wf_segoe-ui_normal; font-family: wf_segoe-ui_normal;
src: url("../../fonts/segoe-ui/west-european/normal/latest.woff"); src: local("Segoe UI"), url("../../fonts/segoe-ui/west-european/normal/latest.woff");
} }
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; @DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;

View File

@@ -1757,7 +1757,7 @@ input::-webkit-calendar-picker-indicator {
cursor: pointer; cursor: pointer;
} }
.contextual-pane .paneMainContent { .paneMainContent {
flex: 1; flex: 1;
padding-left: 34px; padding-left: 34px;
padding-right: 34px; padding-right: 34px;
@@ -2099,7 +2099,7 @@ a:link {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
overflow-x: auto; overflow-x: auto;
overflow-y: auto; overflow-y: hidden;
height: 100%; height: 100%;
} }
@@ -3085,3 +3085,7 @@ settings-pane {
padding-left: @SmallSpace; padding-left: @SmallSpace;
} }
} }
.hiddenMain {
visibility: hidden;
height: 0px;
}

View File

@@ -3,6 +3,7 @@
.resourceTree { .resourceTree {
height: 100%; height: 100%;
width: 20%;
flex: 0 0 auto; flex: 0 0 auto;
.main { .main {
height: 100%; height: 100%;

4964
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"@azure/ms-rest-nodeauth": "3.0.7", "@azure/ms-rest-nodeauth": "3.0.7",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.10.1", "@fluentui/react": "8.14.3",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3", "@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1", "@microsoft/applicationinsights-web": "2.6.1",
@@ -43,7 +43,6 @@
"@testing-library/jest-dom": "5.11.9", "@testing-library/jest-dom": "5.11.9",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "file:./canvas", "canvas": "file:./canvas",
@@ -57,6 +56,7 @@
"datatables.net-dt": "1.10.19", "datatables.net-dt": "1.10.19",
"date-fns": "1.29.0", "date-fns": "1.29.0",
"dayjs": "1.8.19", "dayjs": "1.8.19",
"dom-to-image": "2.6.0",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"eslint-plugin-jest": "23.13.2", "eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
@@ -98,18 +98,20 @@
"swr": "0.4.0", "swr": "0.4.0",
"terser-webpack-plugin": "3.1.0", "terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1", "underscore": "1.9.1",
"utility-types": "3.10.0" "utility-types": "3.10.0",
"zustand": "3.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.14.0", "@babel/core": "7.9.0",
"@babel/preset-env": "7.14.1", "@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.13.13", "@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.13.0", "@babel/preset-typescript": "7.9.0",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7", "@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
"@types/crossroads": "0.0.30", "@types/crossroads": "0.0.30",
"@types/d3": "5.9.2", "@types/d3": "5.9.2",
"@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.7", "@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6", "@types/enzyme-adapter-react-16": "1.0.6",
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
@@ -127,10 +129,10 @@
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.22.1", "@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.1", "@typescript-eslint/parser": "4.22.0",
"babel-jest": "26.6.3", "babel-jest": "24.9.0",
"babel-loader": "8.2.2", "babel-loader": "8.1.0",
"buffer": "5.1.0", "buffer": "5.1.0",
"case-sensitive-paths-webpack-plugin": "2.3.0", "case-sensitive-paths-webpack-plugin": "2.3.0",
"create-file-webpack": "1.0.2", "create-file-webpack": "1.0.2",
@@ -138,7 +140,7 @@
"enzyme": "3.11.0", "enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.5", "enzyme-adapter-react-16": "1.15.5",
"enzyme-to-json": "3.6.1", "enzyme-to-json": "3.6.1",
"eslint": "7.25.0", "eslint": "7.8.1",
"eslint-cli": "1.1.1", "eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2", "eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2", "eslint-plugin-prefer-arrow": "1.2.2",
@@ -152,7 +154,7 @@
"html-loader": "0.5.5", "html-loader": "0.5.5",
"html-loader-jest": "0.2.1", "html-loader-jest": "0.2.1",
"html-webpack-plugin": "4.5.2", "html-webpack-plugin": "4.5.2",
"jest": "26.6.3", "jest": "25.5.4",
"jest-canvas-mock": "2.1.0", "jest-canvas-mock": "2.1.0",
"jest-playwright-preset": "1.5.1", "jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7", "jest-trx-results-processor": "0.0.7",
@@ -169,10 +171,10 @@
"rimraf": "3.0.0", "rimraf": "3.0.0",
"sinon": "3.2.1", "sinon": "3.2.1",
"style-loader": "0.23.0", "style-loader": "0.23.0",
"ts-loader": "9.1.2", "ts-loader": "6.2.2",
"tslint": "6.1.3", "tslint": "5.11.0",
"tslint-microsoft-contrib": "6.2.0", "tslint-microsoft-contrib": "6.0.0",
"typescript": "4.3.0-beta", "typescript": "4.2.4",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "4.46.0", "webpack": "4.46.0",

View File

@@ -1,10 +1,15 @@
.schema-analyzer-cell-outputs { .schema-analyzer-cell-outputs {
padding: 10px; padding: 10px 2px;
} }
// Mimic FluentUI8's DocumentCard style
.schema-analyzer-cell-output { .schema-analyzer-cell-output {
margin-bottom: 20px; margin-bottom: 20px;
padding: 10px; padding: 14px 20px;
border-radius: 2px; border: 1px solid rgb(237, 235, 233);
box-shadow: rgba(0, 0, 0, 13%) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 11%) 0px 0.3px 0.9px 0px; }
.schema-analyzer-cell-output:hover {
border-color: rgb(200, 198, 196);
box-shadow: inset 0 0 0 1px rgb(200, 198, 196)
} }

View File

@@ -9,11 +9,17 @@ import postRobot from "post-robot";
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work
import { SnapshotRequest } from "../Explorer/Notebook/NotebookComponent/types";
import "../Explorer/Notebook/NotebookRenderer/base.css"; import "../Explorer/Notebook/NotebookRenderer/base.css";
import "../Explorer/Notebook/NotebookRenderer/default.css"; import "../Explorer/Notebook/NotebookRenderer/default.css";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import "./CellOutputViewer.less"; import "./CellOutputViewer.less";
import { TransformMedia } from "./TransformMedia"; import { TransformMedia } from "./TransformMedia";
export interface SnapshotResponse {
imageSrc: string;
requestId: string;
}
export interface CellOutputViewerProps { export interface CellOutputViewerProps {
id: string; id: string;
contentRef: ContentRef; contentRef: ContentRef;
@@ -62,6 +68,36 @@ const onInit = async () => {
ReactDOM.render(outputs, document.getElementById("cellOutput")); ReactDOM.render(outputs, document.getElementById("cellOutput"));
} }
); );
postRobot.on(
"snapshotRequest",
{
window: window.parent,
domain: window.location.origin,
},
async (event): Promise<SnapshotResponse> => {
const topNode = document.getElementById("cellOutput");
if (!topNode) {
const errorMsg = "No top node to snapshot";
return Promise.reject(new Error(errorMsg));
}
// Typescript definition for event is wrong. So read props by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const snapshotRequest = (event as any).data as SnapshotRequest;
const result = await NotebookUtil.takeScreenshotDomToImage(
topNode,
snapshotRequest.aspectRatio,
undefined,
snapshotRequest.downloadFilename
);
return {
imageSrc: result.imageSrc,
requestId: snapshotRequest.requestId,
};
}
);
}; };
// Entry point // Entry point

View File

@@ -0,0 +1,35 @@
import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
export interface CollapsedResourceTreeProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
}
export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: CollapsedResourceTreeProps): JSX.Element => {
return (
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav">
<ul className="nav">
<li
className="resourceTreeCollapse"
id="collapseToggleLeftPaneButton"
role="button"
tabIndex={0}
aria-label="Expand Tree"
>
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
<span data-bind="text: collectionTitle" />
</span>
</li>
</ul>
</div>
</div>
);
};

View File

@@ -94,6 +94,7 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly SchemaAnalyzer = "schemaanalyzer";
} }
export class AfecFeatures { export class AfecFeatures {

View File

@@ -3,11 +3,11 @@ export class ObjectCache<T> extends Map<string, T> {
super(); super();
} }
public override get(key: string): T | undefined { public get(key: string): T | undefined {
return this.touch(key); return this.touch(key);
} }
public override set(key: string, value: T): this { public set(key: string, value: T): this {
if (this.size === this.limit) { if (this.size === this.limit) {
this.delete(this.keys().next().value); this.delete(this.keys().next().value);
} }

View File

@@ -0,0 +1,59 @@
import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
import { userContext } from "../UserContext";
export interface ResourceTreeProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
}
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: ResourceTreeProps): JSX.Element => {
return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */}
<div id="mainslide" className="flexContainer">
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol" data-bind="text: collectionTitle" />
<div className="float-right">
<span
className="padimgcolrefresh"
data-test="refreshTree"
role="button"
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label="Refresh tree"
title="Refresh tree"
>
<img className="refreshcol" src={refreshImg} alt="Refresh tree" />
</span>
<span
className="padimgcolrefresh1"
id="expandToggleLeftPaneButton"
role="button"
onClick={toggleLeftPaneExpanded}
tabIndex={0}
aria-label="Collapse Tree"
title="Collapse Tree"
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>
</div>
</div>
</div>
{userContext.authType === AuthType.ResourceToken ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
) : (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
)}
</div>
{/* Collections Window - End */}
</div>
);
};

View File

@@ -0,0 +1,16 @@
import { Icon, TooltipHost } from "@fluentui/react";
import * as React from "react";
export interface TooltipProps {
children: string;
}
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
return (
<span>
<TooltipHost content={children}>
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
</TooltipHost>
</span>
);
};

View File

@@ -1,24 +0,0 @@
import { ITooltipHostStyles, TooltipHost } from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import * as React from "react";
import InfoBubble from "../../../images/info-bubble.svg";
const calloutProps = { gapSpace: 0 };
const hostStyles: Partial<ITooltipHostStyles> = { root: { display: "inline-block" } };
export interface TooltipProps {
children: string;
}
export const Tooltip: React.FunctionComponent = ({ children }: TooltipProps) => {
const tooltipId = useId("tooltip");
return children ? (
<span>
<TooltipHost content={children} id={tooltipId} calloutProps={calloutProps} styles={hostStyles}>
<img className="infoImg" src={InfoBubble} alt="More information" />
</TooltipHost>
</span>
) : (
<></>
);
};

View File

@@ -2,7 +2,7 @@ import { Image, Stack, TextField } from "@fluentui/react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react"; import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg"; import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../Constants"; import * as Constants from "../Constants";
import { Tooltip } from "../Tooltip/Tooltip"; import { InfoTooltip } from "../Tooltip/InfoTooltip";
interface UploadProps { interface UploadProps {
label: string; label: string;
@@ -51,7 +51,7 @@ export const Upload: FunctionComponent<UploadProps> = ({
return ( return (
<div> <div>
<span className="renewUploadItemsHeader">{label}</span> <span className="renewUploadItemsHeader">{label}</span>
<Tooltip>{tooltip}</Tooltip> {tooltip && <InfoTooltip>{tooltip}</InfoTooltip>}
<Stack horizontal> <Stack horizontal>
<TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} /> <TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} />
<input <input

View File

@@ -206,17 +206,14 @@ export enum NeighborType {
BOTH, BOTH,
} }
/** export interface IGraphConfigUiData {
* Set of observable related to graph configuration by user showNeighborType: NeighborType;
*/ nodeProperties: string[];
export interface GraphConfigUiData { nodePropertiesWithNone: string[];
showNeighborType: ko.Observable<NeighborType>; nodeCaptionChoice: string;
nodeProperties: ko.ObservableArray<string>; nodeColorKeyChoice: string;
nodePropertiesWithNone: ko.ObservableArray<string>; nodeIconChoice: string;
nodeCaptionChoice: ko.Observable<string>; nodeIconSet: string;
nodeColorKeyChoice: ko.Observable<string>;
nodeIconChoice: ko.Observable<string>;
nodeIconSet: ko.Observable<string>;
} }
/** /**

View File

@@ -4,30 +4,10 @@ import * as ko from "knockout";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
describe("Component Registerer", () => { describe("Component Registerer", () => {
it("should register input-typeahead component", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true);
});
it("should register error-display component", () => {
expect(ko.components.isRegistered("error-display")).toBe(true);
});
it("should register graph-style component", () => {
expect(ko.components.isRegistered("graph-style")).toBe(true);
});
it("should register json-editor component", () => { it("should register json-editor component", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true); expect(ko.components.isRegistered("json-editor")).toBe(true);
}); });
it("should registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
});
it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});
it("should register dynamic-list component", () => { it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true); expect(ko.components.isRegistered("dynamic-list")).toBe(true);
}); });

View File

@@ -2,16 +2,10 @@ import * as ko from "knockout";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
@@ -19,7 +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("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());

View File

@@ -20,7 +20,7 @@ export class AccessibleElement extends React.Component<AccessibleElementProps> {
} }
}; };
public override render(): JSX.Element { public render(): JSX.Element {
const elementProps = { ...this.props }; const elementProps = { ...this.props };
delete elementProps.as; delete elementProps.as;
delete elementProps.onActivated; delete elementProps.onActivated;

View File

@@ -12,7 +12,7 @@ import TriangleRightIcon from "../../../../images/Triangle-right.svg";
export interface AccordionComponentProps {} export interface AccordionComponentProps {}
export class AccordionComponent extends React.Component<AccordionComponentProps> { export class AccordionComponent extends React.Component<AccordionComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return <div className="accordion">{this.props.children}</div>; return <div className="accordion">{this.props.children}</div>;
} }
} }
@@ -42,7 +42,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
}; };
} }
public override componentDidUpdate() { componentDidUpdate() {
if (this.props.isExpanded !== this.isExpanded) { if (this.props.isExpanded !== this.isExpanded) {
this.isExpanded = this.props.isExpanded; this.isExpanded = this.props.isExpanded;
this.setState({ this.setState({
@@ -51,7 +51,7 @@ public override componentDidUpdate() {
} }
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="accordionItemContainer"> <div className="accordionItemContainer">
<div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}> <div className="accordionItemHeader" onClick={this.onHeaderClick} onKeyPress={this.onHeaderKeyPress}>

View File

@@ -62,7 +62,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
this.props.onCreateNewSparkPoolClicked(item.key); this.props.onCreateNewSparkPoolClicked(item.key);
}; };
public override render() { public render() {
const { workspaces } = this.props; const { workspaces } = this.props;
let workspaceMenuItems: IContextualMenuItem[] = workspaces.map((workspace) => { let workspaceMenuItems: IContextualMenuItem[] = workspaces.map((workspace) => {
let sparkPoolsMenuProps: IContextualMenuProps = { let sparkPoolsMenuProps: IContextualMenuProps = {

View File

@@ -19,7 +19,7 @@ export interface CollapsiblePanelProps {
} }
export class CollapsiblePanel extends React.Component<CollapsiblePanelProps> { export class CollapsiblePanel extends React.Component<CollapsiblePanelProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className={`collapsiblePanel ${this.props.isCollapsed ? "paneCollapsed" : ""}`}> <div className={`collapsiblePanel ${this.props.isCollapsed ? "paneCollapsed" : ""}`}>
{!this.props.isCollapsed ? this.getExpandedFragment() : this.getCollapsedFragment()} {!this.props.isCollapsed ? this.getExpandedFragment() : this.getCollapsedFragment()}

View File

@@ -24,13 +24,13 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
this.setState({ isExpanded: !this.state.isExpanded }); this.setState({ isExpanded: !this.state.isExpanded });
}; };
public override componentDidUpdate(): void { public componentDidUpdate(): void {
if (this.state.isExpanded && this.props.onExpand) { if (this.state.isExpanded && this.props.onExpand) {
this.props.onExpand(); this.props.onExpand();
} }
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<> <>
<Stack <Stack

View File

@@ -129,7 +129,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
private dropdownElt: HTMLElement; private dropdownElt: HTMLElement;
private expandButtonElt: HTMLElement; private expandButtonElt: HTMLElement;
public override componentDidUpdate(): void { public componentDidUpdate(): void {
if (!this.dropdownElt || !this.expandButtonElt) { if (!this.dropdownElt || !this.expandButtonElt) {
return; return;
} }
@@ -243,7 +243,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
); );
} }
public override render(): JSX.Element { public render(): JSX.Element {
let mainClassName = "commandButtonComponent"; let mainClassName = "commandButtonComponent";
if (this.props.disabled) { if (this.props.disabled) {
mainClassName += " commandDisabled"; mainClassName += " commandDisabled";

View File

@@ -103,7 +103,7 @@ export const Dialog: FunctionComponent<DialogProps> = ({
text: secondaryButtonText, text: secondaryButtonText,
onClick: onSecondaryButtonClick, onClick: onSecondaryButtonClick,
} }
: undefined; : {};
return ( return (
<FluentDialog {...dialogProps}> <FluentDialog {...dialogProps}>

View File

@@ -16,7 +16,7 @@ export interface DefaultDirectoryDropdownProps {
export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDirectoryDropdownProps> { export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDirectoryDropdownProps> {
public static readonly lastVisitedKey: string = "lastVisited"; public static readonly lastVisitedKey: string = "lastVisited";
public override render(): JSX.Element { public render(): JSX.Element {
const lastVisitedOption: IDropdownOption = { const lastVisitedOption: IDropdownOption = {
key: DefaultDirectoryDropdownComponent.lastVisitedKey, key: DefaultDirectoryDropdownComponent.lastVisitedKey,
text: "Sign in to your last visited directory", text: "Sign in to your last visited directory",

View File

@@ -36,7 +36,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
}; };
} }
public override render(): JSX.Element { public render(): JSX.Element {
const { directories: originalItems, selectedDirectoryId } = this.props; const { directories: originalItems, selectedDirectoryId } = this.props;
const { filterText } = this.state; const { filterText } = this.state;
const filteredItems = const filteredItems =

View File

@@ -1959,7 +1959,7 @@ exports[`test render renders with filters 1`] = `
</div> </div>
</span> </span>
</button> </button>
<Component /> <FocusRects />
</BaseButton> </BaseButton>
</DefaultButton> </DefaultButton>
</CustomizedDefaultButton> </CustomizedDefaultButton>

View File

@@ -56,7 +56,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
public ariaLabel: string; public ariaLabel: string;
public buttonText: string; public buttonText: string;
public newItem: ko.Observable<string>; public newItem: ko.Observable<string>;
public override isTemplateReady: ko.Observable<boolean>; public isTemplateReady: ko.Observable<boolean>;
public listItems: ko.ObservableArray<DynamicListItem>; public listItems: ko.ObservableArray<DynamicListItem>;
public constructor(options: DynamicListParams) { public constructor(options: DynamicListParams) {

View File

@@ -26,7 +26,7 @@ export interface EditorParams extends JsonEditorParams {
*/ */
// TODO: Ideally, JsonEditorViewModel should extend EditorViewModel and not the other way around // TODO: Ideally, JsonEditorViewModel should extend EditorViewModel and not the other way around
class EditorViewModel extends JsonEditorViewModel { class EditorViewModel extends JsonEditorViewModel {
public override params: EditorParams; public params: EditorParams;
private static providerRegistered: string[] = []; private static providerRegistered: string[] = [];
public constructor(params: EditorParams) { public constructor(params: EditorParams) {
@@ -44,11 +44,11 @@ class EditorViewModel extends JsonEditorViewModel {
}); });
} }
protected override getEditorLanguage(): string { protected getEditorLanguage(): string {
return this.params.contentType; return this.params.contentType;
} }
protected override async registerCompletionItemProvider() { protected async registerCompletionItemProvider() {
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) { if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service"); const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service");
const monaco = await loadMonaco(); const monaco = await loadMonaco();
@@ -57,7 +57,7 @@ class EditorViewModel extends JsonEditorViewModel {
} }
} }
protected override async getErrorMarkers(input: string): Promise<monaco.editor.IMarkerData[]> { protected async getErrorMarkers(input: string): Promise<monaco.editor.IMarkerData[]> {
const { ErrorMarkProvider } = await import("@azure/cosmos-language-service"); const { ErrorMarkProvider } = await import("@azure/cosmos-language-service");
return ErrorMarkProvider.getErrorMark(input); return ErrorMarkProvider.getErrorMark(input);
} }

View File

@@ -21,20 +21,20 @@ export class EditorReact extends React.Component<EditorReactProps> {
super(props); super(props);
} }
public override componentDidMount(): void { public componentDidMount(): void {
this.createEditor(this.configureEditor.bind(this)); this.createEditor(this.configureEditor.bind(this));
} }
public override shouldComponentUpdate(): boolean { public shouldComponentUpdate(): boolean {
// Prevents component re-rendering // Prevents component re-rendering
return false; return false;
} }
public override componentWillUnmount(): void { public componentWillUnmount(): void {
this.selectionListener && this.selectionListener.dispose(); this.selectionListener && this.selectionListener.dispose();
} }
public override render(): JSX.Element { public render(): JSX.Element {
return <div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />; return <div className="jsonEditor" ref={(elt: HTMLElement) => this.setRef(elt)} />;
} }

View File

@@ -1,27 +0,0 @@
import template from "./error-display-component.html";
/**
* Helper class for ko component registration
* This component displays an error as designed in:
* https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29
* TODO: support "More details"
*/
export class ErrorDisplayComponent {
constructor() {
return {
viewModel: ErrorDisplayViewModel,
template,
};
}
}
/**
* Parameters for this component
*/
interface ErrorDisplayParams {
errorMsg: ko.Observable<string>; // Primary message
}
class ErrorDisplayViewModel {
public constructor(public params: ErrorDisplayParams) {}
}

View File

@@ -1,6 +0,0 @@
<div class="warningErrorContainer" data-bind="visible: !!params.errorMsg()">
<div class="warningErrorContent">
<span><img src="/error_red.svg" alt="Error" /></span>
<span class="settingErrorMsg warningErrorDetailsLinkContainer" data-bind="text: params.errorMsg()"></span>
</div>
</div>

View File

@@ -38,7 +38,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
}; };
} }
public override render(): JSX.Element { public render(): JSX.Element {
const textFieldProps: ITextFieldProps = { const textFieldProps: ITextFieldProps = {
placeholder: AddRepoComponent.TextFieldPlaceholder, placeholder: AddRepoComponent.TextFieldPlaceholder,
autoFocus: true, autoFocus: true,

View File

@@ -49,7 +49,7 @@ export class AuthorizeAccessComponent extends React.Component<
}; };
} }
public override render(): JSX.Element { public render(): JSX.Element {
const choiceGroupProps: IChoiceGroupProps = { const choiceGroupProps: IChoiceGroupProps = {
options: [ options: [
{ {

View File

@@ -29,7 +29,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
private static readonly OKButtonText = "OK"; private static readonly OKButtonText = "OK";
private static readonly CancelButtonText = "Cancel"; private static readonly CancelButtonText = "Cancel";
public override render(): JSX.Element { public render(): JSX.Element {
const content: JSX.Element = this.props.showAuthorizeAccess ? ( const content: JSX.Element = this.props.showAuthorizeAccess ? (
<AuthorizeAccessComponent {...this.props.authorizeAccessProps} /> <AuthorizeAccessComponent {...this.props.authorizeAccessProps} />
) : ( ) : (

View File

@@ -67,7 +67,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
private static readonly DefaultBranchName = "master"; private static readonly DefaultBranchName = "master";
private static readonly FooterIndex = -1; private static readonly FooterIndex = -1;
public override render(): JSX.Element { public render(): JSX.Element {
const pinnedReposListProps: IDetailsListProps = { const pinnedReposListProps: IDetailsListProps = {
styles: { styles: {
contentWrapper: { contentWrapper: {

View File

@@ -1,5 +1,5 @@
import { CommandButton, FontIcon, FontWeights, ITextProps, Separator, Stack, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "@fluentui/react";
export class GalleryHeaderComponent extends React.Component { export class GalleryHeaderComponent extends React.Component {
private static readonly azureText = "Microsoft Azure"; private static readonly azureText = "Microsoft Azure";
@@ -30,7 +30,7 @@ export class GalleryHeaderComponent extends React.Component {
); );
}; };
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack <Stack
tokens={{ childrenGap: 10 }} tokens={{ childrenGap: 10 }}
@@ -61,7 +61,7 @@ export class GalleryHeaderComponent extends React.Component {
<Stack.Item> <Stack.Item>
{this.renderHeaderItem( {this.renderHeaderItem(
GalleryHeaderComponent.galleryText, GalleryHeaderComponent.galleryText,
undefined, () => "",
GalleryHeaderComponent.headerItemTextProps GalleryHeaderComponent.headerItemTextProps
)} )}
</Stack.Item> </Stack.Item>

View File

@@ -1,186 +0,0 @@
/**
* How to use this component:
*
* In your html markup, use:
* <input-typeahead params="{
choices:choices,
selection:selection,
inputValue:inputValue,
placeholder:'Enter source',
typeaheadOverrideOptions:typeaheadOverrideOptions
}"></input-typeahead>
* The parameters are documented below.
*
* Notes:
* - dynamic:true by default, this allows choices to change after initialization.
* To turn it off, use:
* typeaheadOverrideOptions: { dynamic:false }
*
*/
import "jquery-typeahead";
import template from "./input-typeahead.html";
/**
* Helper class for ko component registration
*/
export class InputTypeaheadComponent {
constructor() {
return {
viewModel: InputTypeaheadViewModel,
template,
};
}
}
export interface Item {
caption: string;
value: any;
}
/**
* Parameters for this component
*/
interface InputTypeaheadParams {
/**
* List of choices available in the dropdown.
*/
choices: ko.ObservableArray<Item>;
/**
* Gets updated when user clicks on the choice in the dropdown
*/
selection?: ko.Observable<Item>;
/**
* The current string value of <input>
*/
inputValue?: ko.Observable<string>;
/**
* Define what text you want as the input placeholder
*/
placeholder: string;
/**
* Override default jquery-typeahead options
* WARNING: do not override input, source or callback to avoid breaking the components behavior.
*/
typeaheadOverrideOptions?: any;
/**
* This function gets called when pressing ENTER on the input box
*/
submitFct?: (inputValue: string | null, selection: Item | null) => void;
/**
* Typehead comes with a Search button that we normally remove.
* If you want to use it, turn this on
*/
showSearchButton?: boolean;
}
interface OnClickItem {
matchedKey: string;
value: any;
caption: string;
group: string;
}
interface Cache {
inputValue: string | null;
selection: Item | null;
}
class InputTypeaheadViewModel {
private static instanceCount = 0; // Generate unique id for each component's typeahead instance
private instanceNumber: number;
private params: InputTypeaheadParams;
private cache: Cache;
public constructor(params: InputTypeaheadParams) {
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
this.params = params;
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
this.cache = {
inputValue: null,
selection: null,
};
}
/**
* Must execute once ko is rendered, so that it can find the input element by id
*/
private initializeTypeahead() {
let params = this.params;
let cache = this.cache;
let options: any = {
input: `#${this.getComponentId()}`, //'.input-typeahead',
order: "asc",
minLength: 0,
searchOnFocus: true,
source: {
display: "caption",
data: () => {
return this.params.choices();
},
},
callback: {
onClick: (_node: unknown, _a: unknown, item: OnClickItem) => {
cache.selection = item;
if (params.selection) {
params.selection(item);
}
},
onResult(_node: unknown, query: any) {
cache.inputValue = query;
if (params.inputValue) {
params.inputValue(query);
}
},
},
template: (_query: string, item: any) => {
// Don't display id if caption *IS* the id
return item.caption === item.value
? "<span>{{caption}}</span>"
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
},
dynamic: true,
};
// Override options
if (params.typeaheadOverrideOptions) {
for (let p in params.typeaheadOverrideOptions) {
options[p] = params.typeaheadOverrideOptions[p];
}
}
($ as any).typeahead(options);
}
/**
* Get this component id
* @return unique id per instance
*/
private getComponentId(): string {
return `input-typeahead${this.instanceNumber}`;
}
/**
* Executed once ko is done rendering bindings
* Use ko's "template: afterRender" callback to do that without actually using any template.
* Another way is to call it within setTimeout() in constructor.
*/
public afterRender(): void {
this.initializeTypeahead();
}
public submit(): void {
if (this.params.submitFct) {
this.params.submitFct(this.cache.inputValue, this.cache.selection);
}
}
}

View File

@@ -113,7 +113,7 @@ export class InputTypeaheadComponent extends React.Component<
* @param prevState * @param prevState
* @param snapshot * @param snapshot
*/ */
public override componentDidUpdate( public componentDidUpdate(
prevProps: InputTypeaheadComponentProps, prevProps: InputTypeaheadComponentProps,
prevState: InputTypeaheadComponentState, prevState: InputTypeaheadComponentState,
snapshot: any snapshot: any
@@ -127,11 +127,11 @@ export class InputTypeaheadComponent extends React.Component<
/** /**
* Executed once react is done building the DOM for this component * Executed once react is done building the DOM for this component
*/ */
public override componentDidMount(): void { public componentDidMount(): void {
this.initializeTypeahead(); this.initializeTypeahead();
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<span className="input-typeahead-container"> <span className="input-typeahead-container">
<div <div

View File

@@ -1,19 +0,0 @@
<span class="input-typeahead-container">
<form class="input-typehead" data-bind="submit:submit">
<div class="typeahead__container">
<div class="typeahead__field">
<span class="typeahead__query">
<input
name="q"
type="search"
autocomplete="off"
data-bind="attr: { placeholder: params.placeholder, id:getComponentId() }, value:params.inputValue, template: { afterRender:afterRender() }"
/>
</span>
<span class="typeahead__button" data-bind="visible:params.showSearchButton">
<button type="submit"><span class="typeahead__search-icon"></span></button>
</span>
</div>
</div>
</form>
</span>

View File

@@ -19,7 +19,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
super(props); super(props);
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="notebookTerminalContainer"> <div className="notebookTerminalContainer">
<iframe <iframe

View File

@@ -1,20 +1,23 @@
import { import {
BaseButton, BaseButton,
Button, Button,
FontWeights, DocumentCard,
DocumentCardActivity,
DocumentCardDetails,
DocumentCardPreview,
DocumentCardTitle,
Icon, Icon,
IconButton, IconButton,
Image, IDocumentCardPreviewProps,
IDocumentCardStyles,
ImageFit, ImageFit,
Link, Link,
Persona,
Separator, Separator,
Spinner, Spinner,
SpinnerSize, SpinnerSize,
Text, Text,
TooltipHost, TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import { Card } from "@uifabric/react-cards";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
import { IGalleryItem } from "../../../../Juno/JunoClient"; import { IGalleryItem } from "../../../../Juno/JunoClient";
@@ -48,7 +51,6 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
const CARD_WIDTH = 256; const CARD_WIDTH = 256;
const cardImageHeight = 144; const cardImageHeight = 144;
const cardDescriptionMaxChars = 80; const cardDescriptionMaxChars = 80;
const cardItemGapBig = 10;
const cardItemGapSmall = 8; const cardItemGapSmall = 8;
const cardDeleteSpinnerHeight = 360; const cardDeleteSpinnerHeight = 360;
const smallTextLineHeight = 18; const smallTextLineHeight = 18;
@@ -64,9 +66,9 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
const dateString = new Date(data.created).toLocaleString("default", options); const dateString = new Date(data.created).toLocaleString("default", options);
const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb"); const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
const renderTruncatedDescription = (): string => { const renderTruncated = (text: string, totalLength: number): string => {
let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars); let truncatedDescription = text.substr(0, totalLength);
if (data.description.length > cardDescriptionMaxChars) { if (text.length > totalLength) {
truncatedDescription = `${truncatedDescription} ...`; truncatedDescription = `${truncatedDescription} ...`;
} }
return truncatedDescription; return truncatedDescription;
@@ -120,42 +122,35 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
event.preventDefault(); event.preventDefault();
activate(); activate();
}; };
const DocumentCardActivityPeople = [{ name: data.author, profileImageSrc: data.isSample && CosmosDBLogo }];
const previewProps: IDocumentCardPreviewProps = {
previewImages: [
{
previewImageSrc: data.thumbnailUrl,
imageFit: ImageFit.cover,
width: CARD_WIDTH,
height: cardImageHeight,
},
],
};
const cardStyles: IDocumentCardStyles = {
root: { display: "inline-block", marginRight: 20, width: CARD_WIDTH },
};
return ( return (
<Card <DocumentCard aria-label={cardTitle} styles={cardStyles} onClick={onClick}>
style={{ background: "white" }}
aria-label={cardTitle}
data-is-focusable="true"
tokens={{ width: CARD_WIDTH, childrenGap: 0 }}
onClick={(event) => handlerOnClick(event, onClick)}
>
{isDeletingPublishedNotebook && ( {isDeletingPublishedNotebook && (
<Card.Item tokens={{ padding: cardItemGapBig }}> <Spinner
<Spinner size={SpinnerSize.large}
size={SpinnerSize.large} label={`Deleting '${cardTitle}'`}
label={`Deleting '${cardTitle}'`} styles={{ root: { height: cardDeleteSpinnerHeight } }}
styles={{ root: { height: cardDeleteSpinnerHeight } }} />
/>
</Card.Item>
)} )}
{!isDeletingPublishedNotebook && ( {!isDeletingPublishedNotebook && (
<> <>
<Card.Item tokens={{ padding: cardItemGapBig }}> <DocumentCardActivity activity={dateString} people={DocumentCardActivityPeople} />
<Persona imageUrl={data.isSample && CosmosDBLogo} text={data.author} secondaryText={dateString} /> <DocumentCardPreview {...previewProps} />
</Card.Item> <DocumentCardDetails>
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight, padding: "2px 16px" } }}>
<Card.Item>
<Image
src={data.thumbnailUrl}
width={CARD_WIDTH}
height={cardImageHeight}
imageFit={ImageFit.cover}
alt={`${cardTitle} cover image`}
/>
</Card.Item>
<Card.Section styles={{ root: { padding: cardItemGapBig } }}>
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight } }}>
{data.tags ? ( {data.tags ? (
data.tags.map((tag, index, array) => ( data.tags.map((tag, index, array) => (
<span key={tag}> <span key={tag}>
@@ -167,43 +162,22 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
<br /> <br />
)} )}
</Text> </Text>
<DocumentCardTitle title={renderTruncated(cardTitle, 20)} shouldTruncate />
<Text <DocumentCardTitle
styles={{ title={renderTruncated(data.description, cardDescriptionMaxChars)}
root: { showAsSecondaryTitle
fontWeight: FontWeights.semibold, />
paddingTop: cardItemGapSmall, <span style={{ padding: "8px 16px" }}>
paddingBottom: cardItemGapSmall,
},
}}
nowrap
>
{cardTitle}
</Text>
<Text variant="small" styles={{ root: { height: smallTextLineHeight * 2 } }}>
{renderTruncatedDescription()}
</Text>
<span>
{data.views !== undefined && generateIconText("RedEye", data.views.toString())} {data.views !== undefined && generateIconText("RedEye", data.views.toString())}
{data.downloads !== undefined && generateIconText("Download", data.downloads.toString())} {data.downloads !== undefined && generateIconText("Download", data.downloads.toString())}
{data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())} {data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())}
</span> </span>
</Card.Section> </DocumentCardDetails>
{cardButtonsVisible && ( {cardButtonsVisible && (
<Card.Section <DocumentCardDetails>
styles={{
root: {
marginLeft: cardItemGapBig,
marginRight: cardItemGapBig,
},
}}
>
<Separator styles={{ root: { padding: 0, height: 1 } }} /> <Separator styles={{ root: { padding: 0, height: 1 } }} />
<span> <span style={{ padding: "0px 16px" }}>
{isFavorite !== undefined && {isFavorite !== undefined &&
generateIconButtonWithTooltip( generateIconButtonWithTooltip(
isFavorite ? "HeartFill" : "Heart", isFavorite ? "HeartFill" : "Heart",
@@ -222,10 +196,10 @@ export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps>
) )
)} )}
</span> </span>
</Card.Section> </DocumentCardDetails>
)} )}
</> </>
)} )}
</Card> </DocumentCard>
); );
}; };

View File

@@ -1,59 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GalleryCardComponent renders 1`] = ` exports[`GalleryCardComponent renders 1`] = `
<Card <StyledDocumentCardBase
aria-label="name" aria-label="name"
data-is-focusable="true" styles={
onClick={[Function]}
style={
Object { Object {
"background": "white", "root": Object {
} "display": "inline-block",
} "marginRight": 20,
tokens={ "width": 256,
Object { },
"childrenGap": 0,
"width": 256,
} }
} }
> >
<CardItem <StyledDocumentCardActivityBase
tokens={ activity="Invalid Date"
Object { people={
"padding": 10, Array [
} Object {
} "name": "author",
> "profileImageSrc": false,
<StyledPersonaBase
imageUrl={false}
secondaryText="Invalid Date"
text="author"
/>
</CardItem>
<CardItem>
<Image
alt="name cover image"
height={144}
imageFit={2}
src="thumbnailUrl"
width={256}
/>
</CardItem>
<CardSection
styles={
Object {
"root": Object {
"padding": 10,
}, },
} ]
} }
> />
<StyledDocumentCardPreviewBase
previewImages={
Array [
Object {
"height": 144,
"imageFit": 2,
"previewImageSrc": "thumbnailUrl",
"width": 256,
},
]
}
/>
<StyledDocumentCardDetailsBase>
<Text <Text
nowrap={true} nowrap={true}
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"height": 18, "height": 18,
"padding": "2px 16px",
}, },
} }
} }
@@ -69,33 +59,21 @@ exports[`GalleryCardComponent renders 1`] = `
</StyledLinkBase> </StyledLinkBase>
</span> </span>
</Text> </Text>
<Text <StyledDocumentCardTitleBase
nowrap={true} shouldTruncate={true}
styles={ title="name"
/>
<StyledDocumentCardTitleBase
showAsSecondaryTitle={true}
title="description"
/>
<span
style={
Object { Object {
"root": Object { "padding": "8px 16px",
"fontWeight": 600,
"paddingBottom": 8,
"paddingTop": 8,
},
} }
} }
> >
name
</Text>
<Text
styles={
Object {
"root": Object {
"height": 36,
},
}
}
variant="small"
>
description
</Text>
<span>
<Text <Text
styles={ styles={
Object { Object {
@@ -169,17 +147,8 @@ exports[`GalleryCardComponent renders 1`] = `
0 0
</Text> </Text>
</span> </span>
</CardSection> </StyledDocumentCardDetailsBase>
<CardSection <StyledDocumentCardDetailsBase>
styles={
Object {
"root": Object {
"marginLeft": 10,
"marginRight": 10,
},
}
}
>
<Separator <Separator
styles={ styles={
Object { Object {
@@ -190,7 +159,13 @@ exports[`GalleryCardComponent renders 1`] = `
} }
} }
/> />
<span> <span
style={
Object {
"padding": "0px 16px",
}
}
>
<StyledTooltipHostBase <StyledTooltipHostBase
calloutProps={ calloutProps={
Object { Object {
@@ -276,6 +251,6 @@ exports[`GalleryCardComponent renders 1`] = `
/> />
</StyledTooltipHostBase> </StyledTooltipHostBase>
</span> </span>
</CardSection> </StyledDocumentCardDetailsBase>
</Card> </StyledDocumentCardBase>
`; `;

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

@@ -42,7 +42,7 @@ export class GalleryAndNotebookViewerComponent extends React.Component<
}; };
} }
public override render(): JSX.Element { public render(): JSX.Element {
if (this.state.notebookUrl) { if (this.state.notebookUrl) {
const props: NotebookViewerComponentProps = { const props: NotebookViewerComponentProps = {
container: this.props.container, container: this.props.container,

View File

@@ -30,11 +30,10 @@ 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";
const CARD_WIDTH = 256;
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
junoClient: JunoClient; junoClient: JunoClient;
@@ -87,7 +86,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
public static readonly PublishedTitle = "My published work"; public static readonly PublishedTitle = "My published work";
private static readonly rowsPerPage = 5; private static readonly rowsPerPage = 5;
private static readonly CARD_WIDTH = 256;
private static readonly mostViewedText = "Most viewed"; private static readonly mostViewedText = "Most viewed";
private static readonly mostDownloadedText = "Most downloaded"; private static readonly mostDownloadedText = "Most downloaded";
private static readonly mostFavoritedText = "Most favorited"; private static readonly mostFavoritedText = "Most favorited";
@@ -149,7 +148,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state this.loadFavoriteNotebooks(this.state.searchText, this.state.sortBy, false); // Need this to show correct favorite button state
} }
public override render(): JSX.Element { public render(): JSX.Element {
this.traceViewGallery(); this.traceViewGallery();
const tabs: GalleryTabInfo[] = [ const tabs: GalleryTabInfo[] = [
@@ -373,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 });
@@ -644,7 +643,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => { private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
if (itemIndex === 0) { if (itemIndex === 0) {
this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount; this.columnCount = Math.floor(visibleRect.width / GalleryViewerComponent.CARD_WIDTH) || this.columnCount;
this.rowCount = GalleryViewerComponent.rowsPerPage; this.rowCount = GalleryViewerComponent.rowsPerPage;
} }
@@ -672,7 +671,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
return ( return (
<div style={{ float: "left", padding: 10 }}> <div style={{ float: "left", padding: 5 }}>
<GalleryCardComponent {...props} /> <GalleryCardComponent {...props} />
</div> </div>
); );

View File

@@ -38,7 +38,7 @@ export class InfoComponent extends React.Component<InfoComponentProps> {
); );
}; };
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}> <HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<div className="infoPanelMain"> <div className="infoPanelMain">

View File

@@ -41,7 +41,7 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
); );
}; };
public override render(): JSX.Element { public render(): JSX.Element {
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: "numeric", year: "numeric",
month: "short", month: "short",

View File

@@ -132,7 +132,7 @@ export class NotebookViewerComponent
} }
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="notebookViewerContainer"> <div className="notebookViewerContainer">
{this.props.backNavigationText !== undefined ? ( {this.props.backNavigationText !== undefined ? (

View File

@@ -62,7 +62,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
this.selection.setItems(this.state.filteredResults); this.selection.setItems(this.state.filteredResults);
} }
public override componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void { public componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void {
this.selection.setItems( this.selection.setItems(
this.state.filteredResults, this.state.filteredResults,
!_.isEqual(prevState.filteredResults, this.state.filteredResults) !_.isEqual(prevState.filteredResults, this.state.filteredResults)
@@ -79,11 +79,11 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
} }
// fetched saved queries when panel open // fetched saved queries when panel open
public override componentDidMount() { public componentDidMount() {
this.fetchSavedQueries(); this.fetchSavedQueries();
} }
public override render(): JSX.Element { public render(): JSX.Element {
if (this.state.queries.length === 0) { if (this.state.queries.length === 0) {
return this.renderBannerComponent(); return this.renderBannerComponent();
} }

View File

@@ -20,7 +20,7 @@ export interface RadioSwitchComponentProps {
} }
export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> { export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="radioSwitchComponent"> <div className="radioSwitchComponent">
{this.props.choices.map((choice: Choice) => ( {this.props.choices.map((choice: Choice) => (

View File

@@ -17,7 +17,7 @@ export abstract class ResizeSensorComponent<P, S> extends React.Component<P, S>
protected abstract onDimensionsChanged(width: number, height: number): void; protected abstract onDimensionsChanged(width: number, height: number): void;
protected abstract getSensorTarget(): HTMLElement; protected abstract getSensorTarget(): HTMLElement;
public override componentDidUpdate(): void { public componentDidUpdate(): void {
if (this.isSensing) { if (this.isSensing) {
return; return;
} }
@@ -37,7 +37,7 @@ export abstract class ResizeSensorComponent<P, S> extends React.Component<P, S>
} }
} }
public override componentWillUnmount(): void { public componentWillUnmount(): void {
if (!!this.resizeSensor) { if (!!this.resizeSensor) {
this.resizeSensor.detach(); this.resizeSensor.detach();
} }

View File

@@ -213,7 +213,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}; };
} }
public override componentDidMount(): void { componentDidMount(): void {
if (this.isCollectionSettingsTab) { if (this.isCollectionSettingsTab) {
this.refreshIndexTransformationProgress(); this.refreshIndexTransformationProgress();
this.loadMongoIndexes(); this.loadMongoIndexes();
@@ -226,7 +226,7 @@ public override componentDidMount(): void {
} }
} }
public override componentDidUpdate(): void { componentDidUpdate(): void {
if (this.props.settingsTab.isActive()) { if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons()); this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
} }
@@ -879,7 +879,7 @@ public override componentDidUpdate(): void {
return mongoIndexingPolicyAADError; return mongoIndexingPolicyAADError;
}; };
public override render(): JSX.Element { public render(): JSX.Element {
const scaleComponentProps: ScaleComponentProps = { const scaleComponentProps: ScaleComponentProps = {
collection: this.collection, collection: this.collection,
database: this.database, database: this.database,

View File

@@ -26,7 +26,7 @@ import {
} from "./SettingsRenderUtils"; } from "./SettingsRenderUtils";
class SettingsRenderUtilsTestComponent extends React.Component { class SettingsRenderUtilsTestComponent extends React.Component {
public override render(): JSX.Element { public render(): JSX.Element {
const estimatedSpendingColumns: IColumn[] = [ const estimatedSpendingColumns: IColumn[] = [
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true }, { key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true }, { key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },

View File

@@ -40,11 +40,11 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" }, { key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
]; ];
public override componentDidMount(): void { componentDidMount(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
public override componentDidUpdate(): void { componentDidUpdate(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
@@ -135,7 +135,7 @@ public override componentDidUpdate(): void {
/> />
); );
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{this.getConflictResolutionModeComponent()} {this.getConflictResolutionModeComponent()}

View File

@@ -38,7 +38,7 @@ export class IndexingPolicyComponent extends React.Component<
}; };
} }
public override componentDidUpdate(): void { componentDidUpdate(): void {
if (this.props.shouldDiscardIndexingPolicy) { if (this.props.shouldDiscardIndexingPolicy) {
this.resetIndexingPolicyEditor(); this.resetIndexingPolicyEditor();
this.props.resetShouldDiscardIndexingPolicy(); this.props.resetShouldDiscardIndexingPolicy();
@@ -46,7 +46,7 @@ public override componentDidUpdate(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
public override componentDidMount(): void { componentDidMount(): void {
this.resetIndexingPolicyEditor(); this.resetIndexingPolicyEditor();
this.onComponentUpdate(); this.onComponentUpdate();
} }
@@ -112,7 +112,7 @@ public override componentDidMount(): void {
} }
}; };
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<IndexingPolicyRefreshComponent <IndexingPolicyRefreshComponent

View File

@@ -52,7 +52,7 @@ export class IndexingPolicyRefreshComponent extends React.Component<
} }
}; };
public override render(): JSX.Element { public render(): JSX.Element {
return this.renderIndexTransformationWarning() ? ( return this.renderIndexTransformationWarning() ? (
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar>
) : ( ) : (

View File

@@ -61,7 +61,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
this.descriptionTextField.focus(); this.descriptionTextField.focus();
}; };
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...mongoWarningStackProps}> <Stack {...mongoWarningStackProps}>
<Stack horizontal tokens={addMongoIndexSubElementsTokens}> <Stack horizontal tokens={addMongoIndexSubElementsTokens}>

View File

@@ -89,14 +89,14 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
}, },
]; ];
public override componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void { componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void {
if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) { if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) {
this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus(); this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus();
} }
this.onComponentUpdate(); this.onComponentUpdate();
} }
public override componentDidMount(): void { componentDidMount(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
@@ -311,7 +311,7 @@ public override componentDidMount(): void {
); );
}; };
public override render(): JSX.Element { public render(): JSX.Element {
if (this.props.mongoIndexes) { if (this.props.mongoIndexes) {
if (this.hasCompoundIndex()) { if (this.hasCompoundIndex()) {
return mongoCompoundIndexNotSupportedMessage; return mongoCompoundIndexNotSupportedMessage;

View File

@@ -216,7 +216,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
); );
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{this.isFreeTierAccount() && ( {this.isFreeTierAccount() && (

View File

@@ -72,11 +72,11 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key"; this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
} }
public override componentDidMount(): void { componentDidMount(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
public override componentDidUpdate(): void { componentDidUpdate(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
@@ -323,7 +323,7 @@ public override componentDidUpdate(): void {
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2; public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
{userContext.apiType !== "Cassandra" && this.getTtlComponent()} {userContext.apiType !== "Cassandra" && this.getTtlComponent()}

View File

@@ -96,11 +96,11 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
{ key: "false", text: "Manual" }, { key: "false", text: "Manual" },
]; ];
public override componentDidMount(): void { componentDidMount(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
public override componentDidUpdate(): void { componentDidUpdate(): void {
this.onComponentUpdate(); this.onComponentUpdate();
} }
@@ -627,7 +627,7 @@ public override componentDidUpdate(): void {
); );
}; };
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...checkBoxAndInputStackProps}> <Stack {...checkBoxAndInputStackProps}>
{this.renderWarningMessage()} {this.renderWarningMessage()}

View File

@@ -252,7 +252,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
> >
capacity calculator capacity calculator
<Component <FontIcon
iconName="NavigateExternalInline" iconName="NavigateExternalInline"
/> />
</StyledLinkBase> </StyledLinkBase>
@@ -526,7 +526,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
> >
capacity calculator capacity calculator
<Component <FontIcon
iconName="NavigateExternalInline" iconName="NavigateExternalInline"
/> />
</StyledLinkBase> </StyledLinkBase>

View File

@@ -10,7 +10,7 @@ export interface ToolTipLabelComponentProps {
const iconButtonStyles: Partial<IIconStyles> = { root: { marginBottom: -3 } }; const iconButtonStyles: Partial<IIconStyles> = { root: { marginBottom: -3 } };
export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponentProps> { export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<> <>
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}> <Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>

View File

@@ -115,7 +115,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
fontSize: 12, fontSize: 12,
}; };
public override componentDidUpdate(): void { componentDidUpdate(): void {
if (!this.shouldCheckErrors) { if (!this.shouldCheckErrors) {
this.shouldCheckErrors = true; this.shouldCheckErrors = true;
return; return;
@@ -407,7 +407,7 @@ public override componentDidUpdate(): void {
); );
} }
public override render(): JSX.Element { render(): JSX.Element {
return this.renderNode(this.props.descriptor.root); return this.renderNode(this.props.descriptor.root);
} }
} }

View File

@@ -68,7 +68,7 @@ export class TabComponent extends React.Component<TabComponentProps> {
}); });
} }
public override render(): JSX.Element { public render(): JSX.Element {
const currentTabContent = this.props.tabs[this.props.currentTabIndex].content; const currentTabContent = this.props.tabs[this.props.currentTabIndex].content;
let className = "tabComponentContent"; let className = "tabComponentContent";
if (currentTabContent.className) { if (currentTabContent.className) {

View File

@@ -0,0 +1,13 @@
import { shallow } from "enzyme";
import React from "react";
import { CostEstimateText } from "./CostEstimateText";
const props = {
requestUnits: 5,
isAutoscale: false,
};
describe("CostEstimateText Pane", () => {
it("should render Default properly", () => {
const wrapper = shallow(<CostEstimateText {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,77 @@
import { Text } from "@fluentui/react";
import React, { FunctionComponent } from "react";
import { InfoTooltip } from "../../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import {
calculateEstimateNumber,
computeRUUsagePriceHourly,
getAutoscalePricePerRu,
getCurrencySign,
getMultimasterMultiplier,
getPriceCurrency,
getPricePerRu,
} from "../../../../Utils/PricingUtils";
interface CostEstimateTextProps {
requestUnits: number;
isAutoscale: boolean;
}
export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
requestUnits,
isAutoscale,
}: CostEstimateTextProps) => {
const { databaseAccount } = userContext;
if (!databaseAccount?.properties) {
return <></>;
}
const serverId: string = userContext.portalEnv;
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
const hourlyPrice: number = computeRUUsagePriceHourly({
serverId,
requestUnits,
numberOfRegions,
multimasterEnabled,
isAutoscale,
});
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * SharedConstants.hoursInAMonth;
const currency: string = getPriceCurrency(serverId);
const currencySign: string = getCurrencySign(serverId);
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
: getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
);
}
return (
<Text variant="small">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "}
{currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "}
{currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
{currencySign + pricePerRu}/RU)
</Text>
);
};

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CostEstimateText Pane should render Default properly 1`] = `<Fragment />`;

View File

@@ -0,0 +1,32 @@
import { mount, ReactWrapper } from "enzyme";
import React from "react";
import { ThroughputInput } from "./ThroughputInput";
const props = {
isDatabase: false,
showFreeTierExceedThroughputTooltip: true,
isSharded: false,
setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(),
onCostAcknowledgeChange: () => jest.fn(),
};
describe("ThroughputInput Pane", () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(<ThroughputInput {...props} />);
});
it("should render Default properly", () => {
expect(wrapper).toMatchSnapshot();
});
it("should switch mode properly", () => {
wrapper.find('[aria-label="Manual mode"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
"Container throughput (400 - unlimited RU/s)"
);
wrapper.find('[aria-label="Autoscale mode"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
});
});

View File

@@ -1,11 +1,14 @@
import { Checkbox, DirectionalHint, Icon, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import React from "react"; import React, { FunctionComponent, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../Shared/Constants"; import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils"; import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils"; import * as PricingUtils from "../../../Utils/PricingUtils";
import { CostEstimateText } from "./CostEstimateText/CostEstimateText";
import "./ThroughputInput.less";
export interface ThroughputInputProps { export interface ThroughputInputProps {
isDatabase: boolean; isDatabase: boolean;
@@ -16,176 +19,25 @@ export interface ThroughputInputProps {
onCostAcknowledgeChange: (isAcknowledged: boolean) => void; onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
} }
export interface ThroughputInputState { export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isAutoscaleSelected: boolean; isDatabase,
throughput: number; showFreeTierExceedThroughputTooltip,
isCostAcknowledged: boolean; setThroughputValue,
throughputError: string; setIsAutoscale,
} isSharded,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
export class ThroughputInput extends React.Component<ThroughputInputProps, ThroughputInputState> { setIsAutoscale(isAutoscaleSelected);
constructor(props: ThroughputInputProps) { setThroughputValue(throughput);
super(props);
this.state = { const getThroughputLabelText = (): string => {
isAutoscaleSelected: true,
throughput: AutoPilotUtils.minAutoPilotThroughput,
isCostAcknowledged: false,
throughputError: undefined,
};
this.props.setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
this.props.setIsAutoscale(true);
}
public override render(): JSX.Element {
return (
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{this.getThroughputLabelText()}
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={PricingUtils.getRuToolTipText()}>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
checked={this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onAutoscaleRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!this.state.isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onManualRadioBtnChange.bind(this)}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
</Stack>
{this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{this.props.isDatabase ? "Database" : getCollectionName()} max RU/s
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={this.getAutoScaleTooltip()}>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
</Stack>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
errorMessage={this.state.throughputError}
/>
<Text variant="small">
Your {this.props.isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will
automatically scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.state.throughput)} RU/s (10% of max RU/s) -{" "}
{this.state.throughput} RU/s
</b>{" "}
based on usage.
</Text>
</Stack>
)}
{!this.state.isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
capacity calculator
</Link>
.
</Text>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
this.props.showFreeTierExceedThroughputTooltip &&
this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => this.onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={this.state.throughput.toString()}
aria-label="Max request units per second"
required={true}
errorMessage={this.state.throughputError}
/>
</TooltipHost>
</Stack>
)}
<CostEstimateText requestUnits={this.state.throughput} isAutoscale={this.state.isAutoscaleSelected} />
{this.state.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<span className="mandatoryStar">*&nbsp;</span>
<Checkbox
checked={this.state.isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
this.setState({ isCostAcknowledged: isChecked });
this.props.onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{this.getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
);
}
private getThroughputLabelText(): string {
let throughputHeaderText: string; let throughputHeaderText: string;
if (this.state.isAutoscaleSelected) { if (isAutoscaleSelected) {
throughputHeaderText = AutoPilotUtils.getAutoPilotHeaderText().toLocaleLowerCase(); throughputHeaderText = AutoPilotUtils.getAutoPilotHeaderText().toLocaleLowerCase();
} else { } else {
const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString(); const minRU: string = SharedConstants.CollectionCreation.DefaultCollectionRUs400.toLocaleString();
@@ -194,29 +46,27 @@ public override render(): JSX.Element {
: "unlimited"; : "unlimited";
throughputHeaderText = `throughput (${minRU} - ${maxRU} RU/s)`; throughputHeaderText = `throughput (${minRU} - ${maxRU} RU/s)`;
} }
return `${isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`;
};
return `${this.props.isDatabase ? "Database" : getCollectionName()} ${throughputHeaderText}`; const onThroughputValueChange = (newInput: string): void => {
}
private onThroughputValueChange(newInput: string): void {
const newThroughput = parseInt(newInput); const newThroughput = parseInt(newInput);
this.setState({ throughput: newThroughput }); setThroughput(newThroughput);
this.props.setThroughputValue(newThroughput); setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) {
if (!this.props.isSharded && newThroughput > 10000) { setThroughputError("Unsharded collections support up to 10,000 RUs");
this.setState({ throughputError: "Unsharded collections support up to 10,000 RUs" });
} else { } else {
this.setState({ throughputError: undefined }); setThroughputError("");
} }
} };
private getAutoScaleTooltip(): string { const getAutoScaleTooltip = (): string => {
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
return `Set the max RU/s to the highest RU/s you want your ${collectionName} to scale to. The ${collectionName} will scale between 10% of max RU/s to the max RU/s based on usage.`; return `Set the max RU/s to the highest RU/s you want your ${collectionName} to scale to. The ${collectionName} will scale between 10% of max RU/s to the max RU/s based on usage.`;
} };
private getCostAcknowledgeText(): string { const getCostAcknowledgeText = (): string => {
const { databaseAccount } = userContext; const databaseAccount = userContext.databaseAccount;
if (!databaseAccount || !databaseAccount.properties) { if (!databaseAccount || !databaseAccount.properties) {
return ""; return "";
} }
@@ -225,98 +75,163 @@ public override render(): JSX.Element {
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations; const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
return PricingUtils.getEstimatedSpendAcknowledgeString( return PricingUtils.getEstimatedSpendAcknowledgeString(
this.state.throughput, throughput,
userContext.portalEnv, userContext.portalEnv,
numberOfRegions, numberOfRegions,
multimasterEnabled, multimasterEnabled,
this.state.isAutoscaleSelected isAutoscaleSelected
); );
} };
private onAutoscaleRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void { const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (event.target.checked && !this.state.isAutoscaleSelected) { if (mode === "Autoscale") {
this.setState({ isAutoscaleSelected: true, throughput: AutoPilotUtils.minAutoPilotThroughput }); setThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.props.setIsAutoscale(true); setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
} }
} };
private onManualRadioBtnChange(event: React.ChangeEvent<HTMLInputElement>): void {
if (event.target.checked && this.state.isAutoscaleSelected) {
this.setState({
isAutoscaleSelected: false,
throughput: SharedConstants.CollectionCreation.DefaultCollectionRUs400,
});
this.props.setIsAutoscale(false);
this.props.setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
}
}
interface CostEstimateTextProps {
requestUnits: number;
isAutoscale: boolean;
}
const CostEstimateText: React.FunctionComponent<CostEstimateTextProps> = (props: CostEstimateTextProps) => {
const { requestUnits, isAutoscale } = props;
const { databaseAccount } = userContext;
if (!databaseAccount?.properties) {
return <></>;
}
const serverId: string = userContext.portalEnv;
const numberOfRegions: number = databaseAccount.properties.readLocations?.length || 1;
const multimasterEnabled: boolean = databaseAccount.properties.enableMultipleWriteLocations;
const hourlyPrice: number = PricingUtils.computeRUUsagePriceHourly({
serverId,
requestUnits,
numberOfRegions,
multimasterEnabled,
isAutoscale,
});
const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * SharedConstants.hoursInAMonth;
const currency: string = PricingUtils.getPriceCurrency(serverId);
const currencySign: string = PricingUtils.getCurrencySign(serverId);
const multiplier = PricingUtils.getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
const pricePerRu = isAutoscale
? PricingUtils.getAutoscalePricePerRu(serverId, multiplier) * multiplier
: PricingUtils.getPricePerRu(serverId) * multiplier;
const iconWithEstimatedCostDisclaimer: JSX.Element = (
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={PricingUtils.estimatedCostDisclaimer}
styles={{ root: { verticalAlign: "bottom" } }}
>
<Icon iconName="Info" className="panelInfoIcon" />
</TooltipHost>
);
if (isAutoscale) {
return (
<Text variant="small">
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
<b>
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice / 10)} -{" "}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)}{" "}
</b>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
RU/s, {currencySign + pricePerRu}/RU)
</Text>
);
}
return ( return (
<Text variant="small"> <div className="throughputInputContainer throughputInputSpacing">
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} <Stack horizontal>
<b> <span className="mandatoryStar">*&nbsp;</span>
{currencySign + PricingUtils.calculateEstimateNumber(hourlyPrice)} hourly /{" "} <Text aria-label="Throughput header" variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{currencySign + PricingUtils.calculateEstimateNumber(dailyPrice)} daily /{" "} {getThroughputLabelText()}
{currencySign + PricingUtils.calculateEstimateNumber(monthlyPrice)} monthly{" "} </Text>
</b> <InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "} </Stack>
{currencySign + pricePerRu}/RU)
</Text> <Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Autoscale mode"
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
<span className="throughputInputRadioBtnLabel">Autoscale</span>
<input
className="throughputInputRadioBtn"
aria-label="Manual mode"
checked={!isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>
<span className="throughputInputRadioBtnLabel">Manual</span>
</Stack>
{isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with{" "}
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="ruDescription">
capacity calculator
</Link>
.
</Text>
<Stack horizontal>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
{isDatabase ? "Database" : getCollectionName()} Max RU/s
</Text>
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
</Stack>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}
errorMessage={throughputError}
/>
<Text variant="small">
Your {isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will automatically scale
from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(throughput)} RU/s (10% of max RU/s) - {throughput} RU/s
</b>{" "}
based on usage.
</Text>
</Stack>
)}
{!isAutoscaleSelected && (
<Stack className="throughputInputSpacing">
<Text variant="small" aria-label="ruDescription">
Estimate your required RU/s with&nbsp;
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="capacityLink">
capacity calculator
</Link>
.
</Text>
<TooltipHost
directionalHint={DirectionalHint.topLeftEdge}
content={
showFreeTierExceedThroughputTooltip &&
throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
: undefined
}
>
<TextField
type="number"
styles={{
fieldGroup: { width: 300, height: 27 },
field: { fontSize: 12 },
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={100}
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}
errorMessage={throughputError}
/>
</TooltipHost>
</Stack>
)}
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
{throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
<Stack horizontal verticalAlign="start">
<Checkbox
checked={isCostAcknowledged}
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, margin: "4px 4px 0 0" },
}}
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) => {
setIsCostAcknowledged(isChecked);
onCostAcknowledgeChange(isChecked);
}}
/>
<Text variant="small" style={{ lineHeight: "20px" }}>
{getCostAcknowledgeText()}
</Text>
</Stack>
)}
</div>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@ export interface TreeComponentProps {
} }
export class TreeComponent extends React.Component<TreeComponentProps> { export class TreeComponent extends React.Component<TreeComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div style={this.props.style} className={`treeComponent ${this.props.className}`}> <div style={this.props.style} className={`treeComponent ${this.props.className}`}>
<TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} /> <TreeNodeComponent paddingLeft={0} node={this.props.rootNode} generation={0} />
@@ -93,7 +93,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
}; };
} }
public override componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) { componentDidUpdate(prevProps: TreeNodeComponentProps, prevState: TreeNodeComponentState) {
// Only call when expand has actually changed // Only call when expand has actually changed
if (this.state.isExpanded !== prevState.isExpanded) { if (this.state.isExpanded !== prevState.isExpanded) {
if (this.state.isExpanded) { if (this.state.isExpanded) {
@@ -110,7 +110,7 @@ public override componentDidUpdate(prevProps: TreeNodeComponentProps, prevState:
} }
} }
public override render(): JSX.Element { public render(): JSX.Element {
return this.renderNode(this.props.node, this.props.generation); return this.renderNode(this.props.node, this.props.generation);
} }

View File

@@ -31,9 +31,10 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager"; import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
@@ -45,13 +46,13 @@ import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/Gallery
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil"; import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager"; 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 AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane"; 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";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
@@ -59,7 +60,6 @@ import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfir
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
@@ -67,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";
@@ -91,7 +91,7 @@ export interface ExplorerParams {
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void; setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
setNotificationConsoleData: (consoleData: ConsoleData) => void; setNotificationConsoleData: (consoleData: ConsoleData) => void;
setInProgressConsoleDataIdToBeDeleted: (id: string) => void; setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element) => void; openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void;
closeSidePanel: () => void; closeSidePanel: () => void;
closeDialog: () => void; closeDialog: () => void;
openDialog: (props: DialogProps) => void; openDialog: (props: DialogProps) => void;
@@ -100,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>;
@@ -125,14 +124,13 @@ export default class Explorer {
// Panes // Panes
public contextPanes: ContextualPaneBase[]; public contextPanes: ContextualPaneBase[];
public openSidePanel: (headerText: string, panelContent: JSX.Element) => void; public openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void;
public closeSidePanel: () => void; public closeSidePanel: () => void;
// Resource Tree // Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>; public databases: ko.ObservableArray<ViewModels.Database>;
public selectedDatabaseId: ko.Computed<string>; public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
@@ -149,9 +147,6 @@ export default class Explorer {
public tabsManager: TabsManager; public tabsManager: TabsManager;
// Contextual panes // Contextual panes
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient; private gitHubClient: GitHubClient;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
@@ -163,7 +158,6 @@ export default class Explorer {
public isMongoIndexingEnabled: ko.Observable<boolean>; public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>; public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>; public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks // Notebooks
@@ -183,7 +177,6 @@ export default class Explorer {
public openDialog: ExplorerParams["openDialog"]; public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"]; public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = [];
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>; private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager; private _arcadiaManager: ArcadiaResourceManager;
@@ -212,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");
@@ -230,6 +222,7 @@ export default class Explorer {
}); });
} }
}); });
this.isNotebooksEnabledForAccount = ko.observable(false); this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSparkEnabledForAccount = ko.observable(false); this.isSparkEnabledForAccount = ko.observable(false);
@@ -334,7 +327,6 @@ export default class Explorer {
} }
return true; return true;
}); });
this.isLeftPaneExpanded = ko.observable<boolean>(true);
this.selectedNode = ko.observable<ViewModels.TreeNode>(); this.selectedNode = ko.observable<ViewModels.TreeNode>();
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
// Make sure switching tabs restores tabs display // Make sure switching tabs restores tabs display
@@ -368,7 +360,7 @@ export default class Explorer {
return false; return false;
} }
return userContext.apiType === "Mongo"; return isCapabilityEnabled("EnableMongo");
}); });
this.isServerlessEnabled = ko.computed( this.isServerlessEnabled = ko.computed(
@@ -405,28 +397,6 @@ export default class Explorer {
} }
}); });
this.addDatabasePane = new AddDatabasePane({
id: "adddatabasepane",
visible: ko.observable<boolean>(false),
container: this,
});
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => userContext.apiType === "Tables"),
id: "addcollectionpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane",
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),
@@ -442,13 +412,6 @@ export default class Explorer {
} }
}); });
this._panes = [
this.addDatabasePane,
this.addCollectionPane,
this.graphStylingPane,
this.cassandraAddCollectionPane,
];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
document.addEventListener( document.addEventListener(
@@ -466,67 +429,43 @@ 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");
this.deleteDatabaseText("Delete Database"); this.deleteDatabaseText("Delete Database");
this.addCollectionPane.title("Add Container");
this.addCollectionPane.collectionIdTitle("Container id");
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this container"
);
this.refreshTreeTitle("Refresh containers"); this.refreshTreeTitle("Refresh containers");
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");
this.deleteDatabaseText("Delete Database"); this.deleteDatabaseText("Delete Database");
this.addCollectionPane.title("Add Collection");
this.addCollectionPane.collectionIdTitle("Collection id");
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this collection"
);
this.refreshTreeTitle("Refresh collections"); this.refreshTreeTitle("Refresh collections");
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");
this.collectionTreeNodeAltText("Graph"); this.collectionTreeNodeAltText("Graph");
this.addCollectionPane.title("Add Graph");
this.addCollectionPane.collectionIdTitle("Graph id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.refreshTreeTitle("Refresh graphs"); this.refreshTreeTitle("Refresh graphs");
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");
this.collectionTreeNodeAltText("Table"); this.collectionTreeNodeAltText("Table");
this.addCollectionPane.title("Add Table");
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.tableDataClient = new TablesAPIDataClient(); this.tableDataClient = new TablesAPIDataClient();
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");
this.collectionTreeNodeAltText("Table"); this.collectionTreeNodeAltText("Table");
this.addCollectionPane.title("Add Table");
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
@@ -706,16 +645,8 @@ export default class Explorer {
this.setIsNotificationConsoleExpanded(true); this.setIsNotificationConsoleExpanded(true);
} }
public toggleLeftPaneExpanded() { public collapseConsole(): void {
this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); this.setIsNotificationConsoleExpanded(false);
if (this.isLeftPaneExpanded()) {
document.getElementById("expandToggleLeftPaneButton").focus();
this.splitter.expandLeft();
} else {
document.getElementById("collapseToggleLeftPaneButton").focus();
this.splitter.collapseLeft();
}
} }
public refreshDatabaseForResourceToken(): Q.Promise<any> { public refreshDatabaseForResourceToken(): Q.Promise<any> {
@@ -834,14 +765,6 @@ export default class Explorer {
this.refreshNotebookList(); this.refreshNotebookList();
}; };
public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.toggleLeftPaneExpanded();
return false;
}
return true;
};
// Facade // Facade
public provideFeedbackEmail = () => { public provideFeedbackEmail = () => {
window.open(Constants.Urls.feedbackEmail, "_blank"); window.open(Constants.Urls.feedbackEmail, "_blank");
@@ -1096,6 +1019,9 @@ export default class Explorer {
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true); this.isMongoIndexingEnabled(true);
} }
if (flights.indexOf(Constants.Flights.SchemaAnalyzer) !== -1) {
userContext.features.enableSchemaAnalyzer = true;
}
} }
public findSelectedCollection(): ViewModels.Collection { public findSelectedCollection(): ViewModels.Collection {
@@ -1104,10 +1030,6 @@ export default class Explorer {
: this.selectedNode().collection) as ViewModels.Collection; : this.selectedNode().collection) as ViewModels.Collection;
} }
public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
}
public isRunningOnNationalCloud(): boolean { public isRunningOnNationalCloud(): boolean {
return ( return (
userContext.portalEnv === "blackforest" || userContext.portalEnv === "blackforest" ||
@@ -1303,10 +1225,18 @@ export default class Explorer {
public async publishNotebook( public async publishNotebook(
name: string, name: string,
content: NotebookPaneContent, content: NotebookPaneContent,
parentDomElement?: HTMLElement notebookContentRef?: string,
onTakeSnapshot?: (request: SnapshotRequest) => void,
onClosePanel?: () => void
): Promise<void> { ): Promise<void> {
if (this.notebookManager) { if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement); await this.notebookManager.openPublishNotebookPane(
name,
content,
notebookContentRef,
onTakeSnapshot,
onClosePanel
);
this.isPublishNotebookPaneEnabled(true); this.isPublishNotebookPaneEnabled(true);
} }
} }
@@ -1437,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={() => {
@@ -1468,7 +1398,7 @@ export default class Explorer {
} }
this.openSidePanel( this.openSidePanel(
"", "Create new directory",
<StringInputPane <StringInputPane
explorer={this} explorer={this}
closePanel={() => { closePanel={() => {
@@ -1845,9 +1775,6 @@ export default class Explorer {
public onNewCollectionClicked(databaseId?: string): void { public onNewCollectionClicked(databaseId?: string): void {
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open(); this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableKOPanel) {
this.addCollectionPane.open(this.selectedDatabaseId());
document.getElementById("linkAddCollection").focus();
} else { } else {
this.openAddCollectionPanel(databaseId); this.openAddCollectionPanel(databaseId);
} }
@@ -1896,7 +1823,6 @@ export default class Explorer {
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this.closeAllPanes();
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
@@ -1961,10 +1887,10 @@ export default class Explorer {
public openDeleteDatabaseConfirmationPane(): void { public openDeleteDatabaseConfirmationPane(): void {
this.openSidePanel( this.openSidePanel(
"Delete Database", "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()}
/> />
@@ -1972,12 +1898,12 @@ export default class Explorer {
} }
public openUploadItemsPanePane(): void { public openUploadItemsPanePane(): void {
this.openSidePanel("Upload", <UploadItemsPane explorer={this} closePanel={this.closeSidePanel} />); this.openSidePanel("Upload " + getUploadName(), <UploadItemsPane explorer={this} />);
} }
public openSettingPane(): void { public openSettingPane(): void {
this.openSidePanel( this.openSidePanel(
"Settings", "Setting",
<SettingsPane expandConsole={() => this.expandConsole()} closePanel={this.closeSidePanel} /> <SettingsPane expandConsole={() => this.expandConsole()} closePanel={this.closeSidePanel} />
); );
} }
@@ -2005,6 +1931,16 @@ export default class Explorer {
/> />
); );
} }
public openAddDatabasePane(): void {
this.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel
explorer={this}
openNotificationConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel}
/>
);
}
public openBrowseQueriesPanel(): void { public openBrowseQueriesPanel(): void {
this.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} closePanel={this.closeSidePanel} />); this.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} closePanel={this.closeSidePanel} />);
@@ -2021,7 +1957,7 @@ export default class Explorer {
public openUploadFilePanel(parent?: NotebookContentItem): void { public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot; parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel( this.openSidePanel(
"Upload File", "Upload file to notebook server",
<UploadFilePane <UploadFilePane
expandConsole={() => this.expandConsole()} expandConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel} closePanel={this.closeSidePanel}

View File

@@ -1,7 +1,7 @@
import * as sinon from "sinon"; import * as sinon from "sinon";
import { D3ForceGraph, LoadMoreDataAction, D3GraphNodeData } from "./D3ForceGraph";
import { D3Node, D3Link, GraphData } from "../GraphExplorerComponent/GraphData";
import GraphTab from "../../Tabs/GraphTab"; import GraphTab from "../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "../GraphExplorerComponent/GraphData";
import { D3ForceGraph, D3GraphNodeData, LoadMoreDataAction } from "./D3ForceGraph";
describe("D3ForceGraph", () => { describe("D3ForceGraph", () => {
const v1Id = "v1"; const v1Id = "v1";
@@ -68,7 +68,7 @@ describe("D3ForceGraph", () => {
beforeEach(() => { beforeEach(() => {
forceGraph = new D3ForceGraph({ forceGraph = new D3ForceGraph({
graphConfig: GraphTab.createGraphConfig(), igraphConfig: GraphTab.createIGraphConfig(),
onHighlightedNode: sinon.spy(), onHighlightedNode: sinon.spy(),
onLoadMoreData: (action: LoadMoreDataAction): void => {}, onLoadMoreData: (action: LoadMoreDataAction): void => {},
@@ -141,6 +141,7 @@ describe("D3ForceGraph", () => {
const mouseoverEvent = document.createEvent("Events"); const mouseoverEvent = document.createEvent("Events");
mouseoverEvent.initEvent("mouseover", true, false); mouseoverEvent.initEvent("mouseover", true, false);
$(rootNode).find(".node")[0].dispatchEvent(mouseoverEvent); // [0] is v1 vertex $(rootNode).find(".node")[0].dispatchEvent(mouseoverEvent); // [0] is v1 vertex
expect($(rootNode).find(".node")[0]).toBe(1);
// onHighlightedNode is always called once to clear the selection // onHighlightedNode is always called once to clear the selection
expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true); expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true);
@@ -150,7 +151,7 @@ describe("D3ForceGraph", () => {
expect(onHighlightedNode.id).toEqual(v1Id); expect(onHighlightedNode.id).toEqual(v1Id);
}; };
forceGraph.updateGraph(newGraph); forceGraph.updateGraph(newGraph, forceGraph.igraphConfig);
}); });
}); });
}); });

View File

@@ -13,7 +13,7 @@ import _ from "underscore";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { NeighborType } from "../../../Contracts/ViewModels"; import { NeighborType } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab"; import { IGraphConfig } from "./../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "./GraphData"; import { D3Link, D3Node, GraphData } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer"; import { GraphExplorer } from "./GraphExplorer";
@@ -48,21 +48,22 @@ interface ZoomTransform extends Point2D {
export interface D3ForceGraphParameters { export interface D3ForceGraphParameters {
// Graph to parent // Graph to parent
graphConfig: GraphConfig;
onHighlightedNode: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph igraphConfig: IGraphConfig;
onLoadMoreData: (action: LoadMoreDataAction) => void; onHighlightedNode?: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph
onLoadMoreData?: (action: LoadMoreDataAction) => void;
// parent to graph // parent to graph
onInitialized: (instance: GraphRenderer) => void; onInitialized?: (instance: GraphRenderer) => void;
// For unit testing purposes // For unit testing purposes
onGraphUpdated: (timestamp: number) => void; onGraphUpdated?: (timestamp: number) => void;
} }
export interface GraphRenderer { export interface GraphRenderer {
selectNode(id: string): void; selectNode(id: string): void;
resetZoom(): void; resetZoom(): void;
updateGraph(graphData: GraphData<D3Node, D3Link>): void; updateGraph(graphData: GraphData<D3Node, D3Link>, igraphConfigParam?: IGraphConfig): void;
enableHighlight(enable: boolean): void; enableHighlight(enable: boolean): void;
} }
@@ -108,7 +109,7 @@ export class D3ForceGraph implements GraphRenderer {
private viewCenter: Point2D; private viewCenter: Point2D;
// Map a property to a graph node attribute (such as color) // Map a property to a graph node attribute (such as color)
private uniqueValues: (string | number)[]; // keep track of unique values private uniqueValues: (string | number)[] = []; // keep track of unique values
private graphDataWrapper: GraphData<D3Node, D3Link>; private graphDataWrapper: GraphData<D3Node, D3Link>;
// Communication with outside // Communication with outside
@@ -119,9 +120,11 @@ export class D3ForceGraph implements GraphRenderer {
// outside -> Graph // outside -> Graph
private idToSelect: ko.Observable<string>; // Programmatically select node by id outside graph private idToSelect: ko.Observable<string>; // Programmatically select node by id outside graph
private isHighlightDisabled: boolean; private isHighlightDisabled: boolean;
public igraphConfig: IGraphConfig;
public constructor(params: D3ForceGraphParameters) { public constructor(params: D3ForceGraphParameters) {
this.params = params; this.params = params;
this.igraphConfig = this.params.igraphConfig;
this.idToSelect = ko.observable(null); this.idToSelect = ko.observable(null);
this.errorMsgs = ko.observableArray([]); this.errorMsgs = ko.observableArray([]);
this.graphDataWrapper = null; this.graphDataWrapper = null;
@@ -151,7 +154,10 @@ export class D3ForceGraph implements GraphRenderer {
this.g.remove(); this.g.remove();
} }
public updateGraph(newGraph: GraphData<D3Node, D3Link>): void { public updateGraph(newGraph: GraphData<D3Node, D3Link>, igraphConfigParam?: IGraphConfig): void {
if (igraphConfigParam) {
this.igraphConfig = igraphConfigParam;
}
if (!newGraph || !this.simulation) { if (!newGraph || !this.simulation) {
return; return;
} }
@@ -159,7 +165,8 @@ export class D3ForceGraph implements GraphRenderer {
this.graphDataWrapper = new GraphData<D3Node, D3Link>(); this.graphDataWrapper = new GraphData<D3Node, D3Link>();
this.graphDataWrapper.setData(newGraph); this.graphDataWrapper.setData(newGraph);
const key = this.params.graphConfig.nodeColorKey(); const key = this.igraphConfig.nodeColorKey;
if (key !== GraphExplorer.NONE_CHOICE) { if (key !== GraphExplorer.NONE_CHOICE) {
this.updateUniqueValues(key); this.updateUniqueValues(key);
} }
@@ -265,20 +272,7 @@ export class D3ForceGraph implements GraphRenderer {
}); });
}); });
// Redraw if any of these configs change this.redrawGraph();
this.params.graphConfig.nodeColor.subscribe(this.redrawGraph.bind(this));
this.params.graphConfig.nodeColorKey.subscribe((key: string) => {
// Compute colormap
this.uniqueValues = [];
this.updateUniqueValues(key);
this.redrawGraph();
});
this.params.graphConfig.linkColor.subscribe(() => this.redrawGraph());
this.params.graphConfig.showNeighborType.subscribe(() => this.redrawGraph());
this.params.graphConfig.nodeCaption.subscribe(() => this.redrawGraph());
this.params.graphConfig.nodeSize.subscribe(() => this.redrawGraph());
this.params.graphConfig.linkWidth.subscribe(() => this.redrawGraph());
this.params.graphConfig.nodeIconKey.subscribe(() => this.redrawGraph());
this.instantiateSimulation(); this.instantiateSimulation();
} // initialize } // initialize
@@ -371,7 +365,10 @@ export class D3ForceGraph implements GraphRenderer {
*/ */
private shiftGraph(targetPosition: Point2D): Q.Promise<Point2D> { private shiftGraph(targetPosition: Point2D): Q.Promise<Point2D> {
const deferred: Q.Deferred<Point2D> = Q.defer<Point2D>(); const deferred: Q.Deferred<Point2D> = Q.defer<Point2D>();
const offset = { x: this.width / 2 - targetPosition.x, y: this.height / 2 - targetPosition.y }; const offset = {
x: this.width / 2 - targetPosition.x,
y: this.height / 2 - targetPosition.y,
};
this.viewCenter = targetPosition; this.viewCenter = targetPosition;
if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) { if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) {
@@ -526,7 +523,10 @@ export class D3ForceGraph implements GraphRenderer {
.transition() .transition()
.duration(D3ForceGraph.TRANSITION_STEP3_MS) .duration(D3ForceGraph.TRANSITION_STEP3_MS)
.attrTween("transform", (d: D3Node) => { .attrTween("transform", (d: D3Node) => {
const finalPos = nodeFinalPositionMap.get(d.id) || { x: viewCenter.x, y: viewCenter.y }; const finalPos = nodeFinalPositionMap.get(d.id) || {
x: viewCenter.x,
y: viewCenter.y,
};
const ix = interpolateNumber(viewCenter.x, finalPos.x); const ix = interpolateNumber(viewCenter.x, finalPos.x);
const iy = interpolateNumber(viewCenter.y, finalPos.y); const iy = interpolateNumber(viewCenter.y, finalPos.y);
return (t: number) => { return (t: number) => {
@@ -626,10 +626,10 @@ export class D3ForceGraph implements GraphRenderer {
this.addNewLinks(); this.addNewLinks();
const nodes = this.simulation.nodes(); const nodes1 = this.simulation.nodes();
this.redrawGraph(); this.redrawGraph();
this.animateBigBang(nodes, newNodes); this.animateBigBang(nodes1, newNodes);
this.simulation.alpha(1).restart(); this.simulation.alpha(1).restart();
this.params.onGraphUpdated(new Date().getTime()); this.params.onGraphUpdated(new Date().getTime());
@@ -657,8 +657,8 @@ export class D3ForceGraph implements GraphRenderer {
.append("path") .append("path")
.attr("class", "link") .attr("class", "link")
.attr("fill", "none") .attr("fill", "none")
.attr("stroke-width", this.params.graphConfig.linkWidth()) .attr("stroke-width", this.igraphConfig.linkWidth)
.attr("stroke", this.params.graphConfig.linkColor()); .attr("stroke", this.igraphConfig.linkColor);
if (D3ForceGraph.useSvgMarkerEnd()) { if (D3ForceGraph.useSvgMarkerEnd()) {
line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`); line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`);
@@ -668,7 +668,7 @@ export class D3ForceGraph implements GraphRenderer {
.append("use") .append("use")
.attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`) .attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`)
.attr("class", "markerEnd link") .attr("class", "markerEnd link")
.attr("fill", this.params.graphConfig.linkColor()) .attr("fill", this.igraphConfig.linkColor)
.classed(`${this.getArrowHeadSymbolId()}`, true); .classed(`${this.getArrowHeadSymbolId()}`, true);
} }
@@ -724,7 +724,7 @@ export class D3ForceGraph implements GraphRenderer {
.append("circle") .append("circle")
.attr("fill", this.getNodeColor.bind(this)) .attr("fill", this.getNodeColor.bind(this))
.attr("class", "main") .attr("class", "main")
.attr("r", this.params.graphConfig.nodeSize()); .attr("r", this.igraphConfig.nodeSize);
var iconGroup = newNodes var iconGroup = newNodes
.append("g") .append("g")
@@ -733,22 +733,23 @@ export class D3ForceGraph implements GraphRenderer {
.attr("aria-label", (d: D3Node) => { .attr("aria-label", (d: D3Node) => {
return this.retrieveNodeCaption(d); return this.retrieveNodeCaption(d);
}) })
.on("dblclick", function (_: MouseEvent, d: D3Node) { .on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
// this is the <g> element // this is the <g> element
self.onNodeClicked(this.parentNode, d); self.onNodeClicked(this.parentNode, d);
}) })
.on("click", function (_: MouseEvent, d: D3Node) { .on("click", function (this: Element, _: MouseEvent, d: D3Node) {
// this is the <g> element // this is the <g> element
self.onNodeClicked(this.parentNode, d); self.onNodeClicked(this.parentNode, d);
}) })
.on("keypress", function (event: KeyboardEvent, d: D3Node) { .on("keypress", function (this: Element, event: KeyboardEvent, d: D3Node) {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) { if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation(); event.stopPropagation();
// this is the <g> element // this is the <g> element
self.onNodeClicked(this.parentNode, d); self.onNodeClicked(this.parentNode, d);
} }
}); });
var nodeSize = this.params.graphConfig.nodeSize(); var nodeSize = this.igraphConfig.nodeSize;
var bgsize = nodeSize + 1; var bgsize = nodeSize + 1;
iconGroup iconGroup
@@ -758,7 +759,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("width", bgsize * 2) .attr("width", bgsize * 2)
.attr("height", bgsize * 2) .attr("height", bgsize * 2)
.attr("fill-opacity", (d: D3Node) => { .attr("fill-opacity", (d: D3Node) => {
return this.params.graphConfig.nodeIconKey() ? 1 : 0; return this.igraphConfig.nodeIconKey ? 1 : 0;
}) })
.attr("class", "icon-background"); .attr("class", "icon-background");
@@ -766,14 +767,13 @@ export class D3ForceGraph implements GraphRenderer {
iconGroup iconGroup
.append("svg:image") .append("svg:image")
.attr("xlink:href", (d: D3Node) => { .attr("xlink:href", (d: D3Node) => {
return D3ForceGraph.computeImageData(d, this.params.graphConfig); return D3ForceGraph.computeImageData(d, this.igraphConfig);
}) })
.attr("x", -nodeSize) .attr("x", -nodeSize)
.attr("y", -nodeSize) .attr("y", -nodeSize)
.attr("height", nodeSize * 2) .attr("height", nodeSize * 2)
.attr("width", nodeSize * 2) .attr("width", nodeSize * 2)
.attr("class", "icon"); .attr("class", "icon");
newNodes newNodes
.append("text") .append("text")
.attr("class", "caption") .attr("class", "caption")
@@ -807,7 +807,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("x2", 0) .attr("x2", 0)
.attr("y2", gaugeYOffset) .attr("y2", gaugeYOffset)
.style("stroke-width", 1) .style("stroke-width", 1)
.style("stroke", this.params.graphConfig.linkColor()); .style("stroke", this.igraphConfig.linkColor);
parent parent
.append("use") .append("use")
.attr("xlink:href", "#triangleRight") .attr("xlink:href", "#triangleRight")
@@ -876,7 +876,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("height", gaugeHeight) .attr("height", gaugeHeight)
.style("fill", "white") .style("fill", "white")
.style("stroke-width", 1) .style("stroke-width", 1)
.style("stroke", this.params.graphConfig.linkColor()); .style("stroke", this.igraphConfig.linkColor);
parent parent
.append("rect") .append("rect")
.attr("x", (d: D3Node) => { .attr("x", (d: D3Node) => {
@@ -893,7 +893,7 @@ export class D3ForceGraph implements GraphRenderer {
: 0; : 0;
}) })
.attr("height", gaugeHeight) .attr("height", gaugeHeight)
.style("fill", this.params.graphConfig.nodeColor()) .style("fill", this.igraphConfig.nodeColor)
.attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden"));
parent parent
.append("text") .append("text")
@@ -970,7 +970,7 @@ export class D3ForceGraph implements GraphRenderer {
const self = this; const self = this;
nodeSelection.selectAll(".loadmore").remove(); nodeSelection.selectAll(".loadmore").remove();
var nodeSize = this.params.graphConfig.nodeSize(); var nodeSize = this.igraphConfig.nodeSize;
const rootSelectionG = nodeSelection const rootSelectionG = nodeSelection
.filter((d: D3Node) => { .filter((d: D3Node) => {
return !!d._isRoot && !!d._pagination; return !!d._isRoot && !!d._pagination;
@@ -994,7 +994,7 @@ export class D3ForceGraph implements GraphRenderer {
this.createLoadMoreControl(missingNeighborNonRootG, nodeSize); this.createLoadMoreControl(missingNeighborNonRootG, nodeSize);
// Don't color icons individually, just the definitions // Don't color icons individually, just the definitions
this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.igraphConfig.nodeColor);
} }
/** /**
@@ -1031,11 +1031,11 @@ export class D3ForceGraph implements GraphRenderer {
* @param d * @param d
*/ */
private getNodeColor(d: D3Node): string { private getNodeColor(d: D3Node): string {
if (this.params.graphConfig.nodeColorKey()) { if (this.igraphConfig.nodeColorKey) {
const val = GraphData.getNodePropValue(d, this.params.graphConfig.nodeColorKey()); const val = GraphData.getNodePropValue(d, this.igraphConfig.nodeColorKey);
return this.lookupColorFromKey(<string>val); return this.lookupColorFromKey(<string>val);
} else { } else {
return this.params.graphConfig.nodeColor(); return this.igraphConfig.nodeColor;
} }
} }
@@ -1102,12 +1102,12 @@ export class D3ForceGraph implements GraphRenderer {
this.graphDataWrapper.getTargetsForId(nodeId) this.graphDataWrapper.getTargetsForId(nodeId)
); );
} }
})(this.params.graphConfig.showNeighborType()); })(this.igraphConfig.showNeighborType);
return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId; return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId;
}); });
this.g.selectAll(".link").classed("inactive", (l: D3Link) => { this.g.selectAll(".link").classed("inactive", (l: D3Link) => {
switch (this.params.graphConfig.showNeighborType()) { switch (this.igraphConfig.showNeighborType) {
case NeighborType.SOURCES_ONLY: case NeighborType.SOURCES_ONLY:
return (<D3Node>l.target).id !== nodeId; return (<D3Node>l.target).id !== nodeId;
case NeighborType.TARGETS_ONLY: case NeighborType.TARGETS_ONLY:
@@ -1151,7 +1151,7 @@ export class D3ForceGraph implements GraphRenderer {
} }
private retrieveNodeCaption(d: D3Node) { private retrieveNodeCaption(d: D3Node) {
let key = this.params.graphConfig.nodeCaption(); let key = this.igraphConfig.nodeCaption;
let value: string = d.id || d.label; let value: string = d.id || d.label;
if (key) { if (key) {
value = <string>GraphData.getNodePropValue(d, key) || ""; value = <string>GraphData.getNodePropValue(d, key) || "";
@@ -1193,10 +1193,16 @@ export class D3ForceGraph implements GraphRenderer {
} }
private positionLinkEnd(l: D3Link) { private positionLinkEnd(l: D3Link) {
const source: Point2D = { x: (<D3Node>l.source).x, y: (<D3Node>l.source).y }; const source: Point2D = {
const target: Point2D = { x: (<D3Node>l.target).x, y: (<D3Node>l.target).y }; x: (<D3Node>l.source).x,
y: (<D3Node>l.source).y,
};
const target: Point2D = {
x: (<D3Node>l.target).x,
y: (<D3Node>l.target).y,
};
const d1 = D3ForceGraph.calculateControlPoint(source, target); const d1 = D3ForceGraph.calculateControlPoint(source, target);
var radius = this.params.graphConfig.nodeSize() + 3; var radius = this.igraphConfig.nodeSize + 3;
// End // End
const dx = target.x - d1.x; const dx = target.x - d1.x;
@@ -1209,10 +1215,16 @@ export class D3ForceGraph implements GraphRenderer {
} }
private positionLink(l: D3Link) { private positionLink(l: D3Link) {
const source: Point2D = { x: (<D3Node>l.source).x, y: (<D3Node>l.source).y }; const source: Point2D = {
const target: Point2D = { x: (<D3Node>l.target).x, y: (<D3Node>l.target).y }; x: (<D3Node>l.source).x,
y: (<D3Node>l.source).y,
};
const target: Point2D = {
x: (<D3Node>l.target).x,
y: (<D3Node>l.target).y,
};
const d1 = D3ForceGraph.calculateControlPoint(source, target); const d1 = D3ForceGraph.calculateControlPoint(source, target);
var radius = this.params.graphConfig.nodeSize() + 3; var radius = this.igraphConfig.nodeSize + 3;
// Start // Start
var dx = d1.x - source.x; var dx = d1.x - source.x;
@@ -1244,13 +1256,13 @@ export class D3ForceGraph implements GraphRenderer {
return d._isRoot ? "node root" : "node"; return d._isRoot ? "node root" : "node";
}); });
this.applyConfig(this.params.graphConfig); this.applyConfig(this.igraphConfig);
} }
private static computeImageData(d: D3Node, config: GraphConfig): string { private static computeImageData(d: D3Node, config: IGraphConfig): string {
let propValue = <string>GraphData.getNodePropValue(d, config.nodeIconKey()) || ""; let propValue = <string>GraphData.getNodePropValue(d, config.nodeIconKey) || "";
// Trim leading and trailing spaces to make comparison more forgiving. // Trim leading and trailing spaces to make comparison more forgiving.
let value = config.iconsMap()[propValue.trim()]; let value = config.iconsMap[propValue.trim()];
if (!value) { if (!value) {
return undefined; return undefined;
} }
@@ -1260,48 +1272,46 @@ export class D3ForceGraph implements GraphRenderer {
/** /**
* Update graph according to configuration or use default * Update graph according to configuration or use default
*/ */
private applyConfig(config: GraphConfig) { private applyConfig(config: IGraphConfig) {
if (config.nodeIconKey()) { if (config.nodeIconKey) {
this.g this.g
.selectAll(".node .icon") .selectAll(".node .icon")
.attr("xlink:href", (d: D3Node) => { .attr("xlink:href", (d: D3Node) => {
return D3ForceGraph.computeImageData(d, config); return D3ForceGraph.computeImageData(d, config);
}) })
.attr("x", -config.nodeSize()) .attr("x", -config.nodeSize)
.attr("y", -config.nodeSize()) .attr("y", -config.nodeSize)
.attr("height", config.nodeSize() * 2) .attr("height", config.nodeSize * 2)
.attr("width", config.nodeSize() * 2) .attr("width", config.nodeSize * 2)
.attr("class", "icon"); .attr("class", "icon");
} else { } else {
// clear icons // clear icons
this.g.selectAll(".node .icon").attr("xlink:href", undefined); this.g.selectAll(".node .icon").attr("xlink:href", undefined);
} }
this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => { this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => {
return config.nodeIconKey() ? 1 : 0; return config.nodeIconKey ? 1 : 0;
}); });
this.g.selectAll(".node text.caption").text((d: D3Node) => { this.g.selectAll(".node text.caption").text((d: D3Node) => {
return this.retrieveNodeCaption(d); return this.retrieveNodeCaption(d);
}); });
this.g.selectAll(".node circle.main").attr("r", config.nodeSize()); this.g.selectAll(".node circle.main").attr("r", config.nodeSize);
this.g.selectAll(".node text.caption").attr("dx", config.nodeSize() + 2); this.g.selectAll(".node text.caption").attr("dx", config.nodeSize + 2);
this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this)); this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this));
// Can't color nodes individually if using defs // Can't color nodes individually if using defs
this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", config.nodeColor);
this.g.selectAll(".link").attr("stroke-width", config.linkWidth);
this.g.selectAll(".link").attr("stroke-width", config.linkWidth()); this.g.selectAll(".link").attr("stroke", config.linkColor);
this.g.selectAll(".link").attr("stroke", config.linkColor());
if (D3ForceGraph.useSvgMarkerEnd()) { if (D3ForceGraph.useSvgMarkerEnd()) {
this.svg this.svg
.select(`#${this.getArrowHeadSymbolId()}-marker`) .select(`#${this.getArrowHeadSymbolId()}-marker`)
.attr("fill", config.linkColor()) .attr("fill", config.linkColor)
.attr("stroke", config.linkColor()); .attr("stroke", config.linkColor);
} else { } else {
this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor()); this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor);
} }
// Reset highlight // Reset highlight

View File

@@ -34,7 +34,7 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
this.addNewEdgeToNeighbor = this.props.isSource ? this.addNewEdgeToSource : this.addNewEdgeToTarget; this.addNewEdgeToNeighbor = this.props.isSource ? this.addNewEdgeToSource : this.addNewEdgeToTarget;
} }
public override componentDidMount(): void { public componentDidMount(): void {
// Show empty text boxes by default if no neighbor for convenience // Show empty text boxes by default if no neighbor for convenience
if (this.props.editedNeighbors.currentNeighbors.length === 0) { if (this.props.editedNeighbors.currentNeighbors.length === 0) {
if (this.props.isSource) { if (this.props.isSource) {
@@ -45,7 +45,7 @@ export class EditorNeighborsComponent extends React.Component<EditorNeighborsCom
} }
} }
public override render(): JSX.Element { public render(): JSX.Element {
const neighborTitle = this.props.isSource const neighborTitle = this.props.isSource
? EditorNeighborsComponent.SOURCE_TITLE ? EditorNeighborsComponent.SOURCE_TITLE
: EditorNeighborsComponent.TARGET_TITLE; : EditorNeighborsComponent.TARGET_TITLE;

View File

@@ -20,7 +20,7 @@ export class EditorNodePropertiesComponent extends React.Component<EditorNodePro
public static readonly VERTEX_PROPERTY_TYPES = ["string", "number", "boolean" /* 'null' */]; // TODO Enable null when fully supported by backend public static readonly VERTEX_PROPERTY_TYPES = ["string", "number", "boolean" /* 'null' */]; // TODO Enable null when fully supported by backend
private static readonly DEFAULT_PROPERTY_TYPE = "string"; private static readonly DEFAULT_PROPERTY_TYPE = "string";
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<table className="propertyTable"> <table className="propertyTable">
<tbody> <tbody>

View File

@@ -1,20 +1,20 @@
jest.mock("../../../Common/dataAccess/queryDocuments"); jest.mock("../../../Common/dataAccess/queryDocuments");
jest.mock("../../../Common/dataAccess/queryDocumentsPage"); jest.mock("../../../Common/dataAccess/queryDocumentsPage");
import React from "react";
import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import * as Q from "q"; import * as Q from "q";
import React from "react";
import * as sinon from "sinon";
import "../../../../externals/jquery.typeahead.min"; import "../../../../externals/jquery.typeahead.min";
import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeData } from "./GraphExplorer";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import GraphTab from "../../Tabs/GraphTab";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
import { GraphAccessor, GraphExplorer, GraphExplorerProps, GraphHighlightedNodeData } from "./GraphExplorer";
describe("Check whether query result is vertex array", () => { describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => { it("should reject null as vertex array", () => {
@@ -146,8 +146,8 @@ describe("GraphExplorer", () => {
const gremlinRU = 789.12; const gremlinRU = 789.12;
const createMockProps = (): GraphExplorerProps => { const createMockProps = (): GraphExplorerProps => {
const graphConfig = GraphTab.createGraphConfig(); const igraphConfig = GraphTab.createIGraphConfig();
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig); const igraphConfigUi = GraphTab.createIGraphConfigUiData(igraphConfig);
return { return {
onGraphAccessorCreated: (instance: GraphAccessor): void => {}, onGraphAccessorCreated: (instance: GraphAccessor): void => {},
@@ -170,8 +170,9 @@ describe("GraphExplorer", () => {
resourceId: "resourceId", resourceId: "resourceId",
/* TODO Figure out how to make this Knockout-free */ /* TODO Figure out how to make this Knockout-free */
graphConfigUiData: graphConfigUi, igraphConfigUiData: igraphConfigUi,
graphConfig: graphConfig, igraphConfig: igraphConfig,
setIConfigUiData: (data: string[]): void => {},
}; };
}; };

View File

@@ -19,7 +19,7 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent"; import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { GraphConfig } from "../../Tabs/GraphTab"; import { IGraphConfig } from "../../Tabs/GraphTab";
import { ArraysByKeyCache } from "./ArraysByKeyCache"; import { ArraysByKeyCache } from "./ArraysByKeyCache";
import * as D3ForceGraph from "./D3ForceGraph"; import * as D3ForceGraph from "./D3ForceGraph";
import { EdgeInfoCache } from "./EdgeInfoCache"; import { EdgeInfoCache } from "./EdgeInfoCache";
@@ -31,10 +31,10 @@ import * as LeftPane from "./LeftPaneComponent";
import { MiddlePaneComponent } from "./MiddlePaneComponent"; import { MiddlePaneComponent } from "./MiddlePaneComponent";
import * as NodeProperties from "./NodePropertiesComponent"; import * as NodeProperties from "./NodePropertiesComponent";
import { QueryContainerComponent } from "./QueryContainerComponent"; import { QueryContainerComponent } from "./QueryContainerComponent";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
addVertex: (v: ViewModels.NewVertexData) => Q.Promise<void>; addVertex: (v: ViewModels.NewVertexData) => Q.Promise<void>;
shareIGraphConfig: (igraphConfig: IGraphConfig) => void;
} }
export interface GraphExplorerProps { export interface GraphExplorerProps {
@@ -58,9 +58,10 @@ export interface GraphExplorerProps {
onLoadStartKeyChange: (newKey: number) => void; onLoadStartKeyChange: (newKey: number) => void;
resourceId: string; resourceId: string;
/* TODO Figure out how to make this Knockout-free */ igraphConfigUiData: ViewModels.IGraphConfigUiData;
graphConfigUiData: ViewModels.GraphConfigUiData; igraphConfig: IGraphConfig;
graphConfig?: GraphConfig;
setIConfigUiData?: (data: string[]) => void;
} }
export interface GraphHighlightedNodeData { export interface GraphHighlightedNodeData {
@@ -121,6 +122,10 @@ interface GraphExplorerState {
filterQueryError: string; filterQueryError: string;
filterQueryWarning: string; filterQueryWarning: string;
filterQueryStatus: FilterQueryStatus; filterQueryStatus: FilterQueryStatus;
change: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
} }
export interface EditedProperties { export interface EditedProperties {
@@ -218,6 +223,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private lastReportedIsPropertyEditing: boolean; private lastReportedIsPropertyEditing: boolean;
private lastReportedIsNewVertexDisabled: boolean; private lastReportedIsNewVertexDisabled: boolean;
public getNodeProperties: string[];
public igraphConfigUi: ViewModels.IGraphConfigUiData;
public constructor(props: GraphExplorerProps) { public constructor(props: GraphExplorerProps) {
super(props); super(props);
this.state = { this.state = {
@@ -237,6 +244,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
filterQueryError: null, filterQueryError: null,
filterQueryWarning: null, filterQueryWarning: null,
filterQueryStatus: FilterQueryStatus.NoResult, filterQueryStatus: FilterQueryStatus.NoResult,
change: null,
igraphConfigUiData: this.props.igraphConfigUiData,
igraphConfig: this.props.igraphConfig,
}; };
// Not part of React state // Not part of React state
@@ -284,41 +294,27 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setGremlinParams(); this.setGremlinParams();
} }
/* TODO Make this Knockout-free ! */ const selectedNode = this.state.highlightedNode;
this.props.graphConfigUiData.nodeCaptionChoice.subscribe((key) => {
this.props.graphConfig.nodeCaption(key);
const selectedNode = this.state.highlightedNode;
if (selectedNode) {
this.updatePropertiesPane(selectedNode.id);
}
this.render();
});
this.props.graphConfigUiData.nodeColorKeyChoice.subscribe((val) => {
this.props.graphConfig.nodeColorKey(val === GraphExplorer.NONE_CHOICE ? null : val);
this.render();
});
this.props.graphConfigUiData.showNeighborType.subscribe((val) => {
this.props.graphConfig.showNeighborType(val);
this.render();
});
this.props.graphConfigUiData.nodeIconChoice.subscribe((val) => {
this.updateNodeIcons(val, this.props.graphConfigUiData.nodeIconSet());
this.render();
});
this.props.graphConfigUiData.nodeIconSet.subscribe((val) => {
this.updateNodeIcons(this.props.graphConfigUiData.nodeIconChoice(), val);
this.render();
});
/* *************************************** */
props.onGraphAccessorCreated({ props.onGraphAccessorCreated({
applyFilter: this.submitQuery.bind(this), applyFilter: this.submitQuery.bind(this),
addVertex: this.addVertex.bind(this), addVertex: this.addVertex.bind(this),
shareIGraphConfig: this.shareIGraphConfig.bind(this),
}); });
} // constructor } // constructor
public shareIGraphConfig(igraphConfig: IGraphConfig) {
this.setState({
igraphConfig: { ...igraphConfig },
});
const selectedNode = this.state.highlightedNode;
if (selectedNode) {
this.updatePropertiesPane(selectedNode.id);
this.setResultDisplay(GraphExplorer.TAB_INDEX_GRAPH);
}
}
/** /**
* If pk is a string, return ["pk", "id"] * If pk is a string, return ["pk", "id"]
* else return [pk, "id"] * else return [pk, "id"]
@@ -408,7 +404,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Update graph (in case property is being shown) // Update graph (in case property is being shown)
this.updateInMemoryGraph(result.data); this.updateInMemoryGraph(result.data);
this.updateGraphData(this.originalGraphData); this.updateGraphData(this.originalGraphData, this.state.igraphConfig);
}) })
.then( .then(
() => { () => {
@@ -446,7 +442,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remove vertex from local cache // Remove vertex from local cache
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.removeVertex(id, false); graphData.removeVertex(id, false);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
this.setState({ highlightedNode: null }); this.setState({ highlightedNode: null });
// Remove from root map // Remove from root map
@@ -582,7 +578,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.edgeInfoCache.addVertex(vertex); this.edgeInfoCache.addVertex(vertex);
graphData.setAsRoot(vertex.id); graphData.setAsRoot(vertex.id);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
}; };
vertex._outEdgeIds = vertex._outEdgeIds || []; vertex._outEdgeIds = vertex._outEdgeIds || [];
@@ -788,7 +784,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
graphData.getVertexById(edge.outV)._inEAllLoaded = false; graphData.getVertexById(edge.outV)._inEAllLoaded = false;
} }
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
(error: string) => { (error: string) => {
GraphExplorer.reportToConsole( GraphExplorer.reportToConsole(
@@ -809,7 +805,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
() => { () => {
let graphData = this.originalGraphData; let graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false); graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
(error: string) => { (error: string) => {
GraphExplorer.reportToConsole( GraphExplorer.reportToConsole(
@@ -858,7 +854,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (vertices.length === 0) { if (vertices.length === 0) {
// Clean graph // Clean graph
this.updateGraphData(new GraphData.GraphData()); this.updateGraphData(new GraphData.GraphData(), this.state.igraphConfig);
this.setState({ highlightedNode: null }); this.setState({ highlightedNode: null });
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result is empty"); GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result is empty");
} }
@@ -940,7 +936,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let vertex = vertices[0]; let vertex = vertices[0];
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addVertex(vertex); graphData.addVertex(vertex);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
this.collectNodeProperties(this.originalGraphData.vertices); this.collectNodeProperties(this.originalGraphData.vertices);
// Keep new vertex selected // Keep new vertex selected
@@ -965,7 +961,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
} }
/* ************* React life-cycle methods *********** */ /* ************* React life-cycle methods *********** */
public override render(): JSX.Element { public render(): JSX.Element {
const currentTabIndex = ((resultDisplay: ResultDisplay): number => { const currentTabIndex = ((resultDisplay: ResultDisplay): number => {
switch (resultDisplay) { switch (resultDisplay) {
case ResultDisplay.Graph: case ResultDisplay.Graph:
@@ -1022,10 +1018,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
); );
} }
public override componentWillUnmount(): void { public componentWillUnmount(): void {
this.gremlinClient.destroy(); this.gremlinClient.destroy();
} }
public override componentDidMount(): void { public componentDidMount(): void {
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) { if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.Tab, Action.Tab,
@@ -1069,7 +1065,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
} }
} }
public override componentDidUpdate(): void { public componentDidUpdate(): void {
this.onIsPropertyPaneEditing(this.isPropertyPaneEditing()); this.onIsPropertyPaneEditing(this.isPropertyPaneEditing());
this.onIsNewVertexDisabledChange(this.isNewVertexDisabled()); this.onIsNewVertexDisabledChange(this.isNewVertexDisabled());
} }
@@ -1121,8 +1117,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return rootMap[id]; return rootMap[id];
}) })
); );
if (this.props.graphConfigUiData.nodeProperties().indexOf(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY) !== -1) { if (this.state.igraphConfigUiData.nodeProperties.indexOf(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY) !== -1) {
this.props.graphConfigUiData.nodeCaptionChoice(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY); this.setState({
igraphConfigUiData: {
...this.state.igraphConfigUiData,
nodeCaptionChoice: GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY,
},
});
} }
// Let react instantiate and render graph, before updating // Let react instantiate and render graph, before updating
@@ -1139,7 +1140,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
*/ */
public updateNodeIcons(nodeProp: string, iconSet: string): void { public updateNodeIcons(nodeProp: string, iconSet: string): void {
if (nodeProp === GraphExplorer.NONE_CHOICE) { if (nodeProp === GraphExplorer.NONE_CHOICE) {
this.props.graphConfig.nodeIconKey(null); this.setState({
igraphConfig: {
...this.state.igraphConfig,
nodeIconKey: undefined,
},
});
return; return;
} }
@@ -1163,8 +1169,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
// Update graph configuration // Update graph configuration
this.props.graphConfig.iconsMap(newIconsMap); this.setState({
this.props.graphConfig.nodeIconKey(nodeProp); igraphConfig: {
...this.state.igraphConfig,
iconsMap: newIconsMap,
nodeIconKey: nodeProp,
},
});
}, },
() => { () => {
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to retrieve icons. iconSet:${iconSet}`); GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to retrieve icons. iconSet:${iconSet}`);
@@ -1209,7 +1220,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
} }
private getPossibleRootNodes(): LeftPane.CaptionId[] { private getPossibleRootNodes(): LeftPane.CaptionId[] {
const key = this.props.graphConfigUiData.nodeCaptionChoice(); const key = this.state.igraphConfig.nodeCaption;
return $.map( return $.map(
this.state.rootMap, this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => { (value: any, index: number): LeftPane.CaptionId => {
@@ -1320,7 +1331,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return ""; return "";
} }
const nodeCaption = this.props.graphConfigUiData.nodeCaptionChoice(); const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
const node = this.originalGraphData.getVertexById(this.state.highlightedNode.id); const node = this.originalGraphData.getVertexById(this.state.highlightedNode.id);
return GraphData.GraphData.getNodePropValue(node, nodeCaption) as string; return GraphData.GraphData.getNodePropValue(node, nodeCaption) as string;
} }
@@ -1410,7 +1421,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
const q = `SELECT c.id, c["${ const q = `SELECT c.id, c["${
this.props.graphConfigUiData.nodeCaptionChoice() || "id" this.state.igraphConfigUiData.nodeCaptionChoice || "id"
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; }"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then( return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
@@ -1539,9 +1550,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
const values = Object.keys(props); const values = Object.keys(props);
this.props.graphConfigUiData.nodeProperties(values); this.setState({
// TODO This should move out of GraphExplorer igraphConfigUiData: {
this.props.graphConfigUiData.nodePropertiesWithNone([GraphExplorer.NONE_CHOICE].concat(values)); ...this.state.igraphConfigUiData,
nodeProperties: values,
},
});
this.props.setIConfigUiData(values);
} }
/** /**
@@ -1566,9 +1582,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let sources: NeighborVertexBasicInfo[] = []; let sources: NeighborVertexBasicInfo[] = [];
let targets: NeighborVertexBasicInfo[] = []; let targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues(); this.props.onResetDefaultGraphConfigValues();
let nodeCaption = this.props.graphConfigUiData.nodeCaptionChoice(); let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets); this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
let sData: GraphHighlightedNodeData = { let sData: GraphHighlightedNodeData = {
id: data.id, id: data.id,
label: data.label, label: data.label,
@@ -1615,7 +1630,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
sources.push({ name: caption, id: neighborId, edgeId: edge.id, edgeLabel: p }); sources.push({
name: caption,
id: neighborId,
edgeId: edge.id,
edgeLabel: p,
});
}); });
} }
@@ -1629,7 +1649,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
targets.push({ name: caption, id: neighborId, edgeId: edge.id, edgeLabel: p }); targets.push({
name: caption,
id: neighborId,
edgeId: edge.id,
edgeLabel: p,
});
}); });
} }
@@ -1678,14 +1703,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Clone object and keep the original untouched (by d3) * Clone object and keep the original untouched (by d3)
*/ */
private updateGraphData(graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>) { private updateGraphData(
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
igraphConfig?: IGraphConfig
) {
this.originalGraphData = graphData; this.originalGraphData = graphData;
let gd = JSON.parse(JSON.stringify(this.originalGraphData)); let gd = JSON.parse(JSON.stringify(this.originalGraphData));
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet."); console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
return; return;
} }
this.d3ForceGraph.updateGraph(gd); this.d3ForceGraph.updateGraph(gd, igraphConfig);
} }
public onMiddlePaneInitialized(instance: D3ForceGraph.GraphRenderer): void { public onMiddlePaneInitialized(instance: D3ForceGraph.GraphRenderer): void {
@@ -1694,10 +1722,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private renderMiddlePane(): JSX.Element { private renderMiddlePane(): JSX.Element {
const forceGraphParams: D3ForceGraph.D3ForceGraphParameters = { const forceGraphParams: D3ForceGraph.D3ForceGraphParameters = {
graphConfig: this.props.graphConfig, igraphConfig: this.state.igraphConfig,
onHighlightedNode: this.onHighlightedNode.bind(this), onHighlightedNode: this.onHighlightedNode.bind(this),
onLoadMoreData: this.onLoadMoreData.bind(this), onLoadMoreData: this.onLoadMoreData.bind(this),
onInitialized: (instance: D3ForceGraph.GraphRenderer): void => this.onMiddlePaneInitialized(instance), onInitialized: (instance: D3ForceGraph.GraphRenderer): void => {
this.onMiddlePaneInitialized(instance);
},
onGraphUpdated: this.onGraphUpdated.bind(this), onGraphUpdated: this.onGraphUpdated.bind(this),
}; };

View File

@@ -0,0 +1,74 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { GraphAccessor, GraphExplorer } from "./GraphExplorer";
interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
onGraphAccessorCreated: (instance: GraphAccessor) => void;
onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void;
onIsValidQuery: (isValidQuery: boolean) => void;
onIsPropertyEditing: (isEditing: boolean) => void;
onIsGraphDisplayed: (isDisplayed: boolean) => void;
onResetDefaultGraphConfigValues: () => void;
collectionPartitionKeyProperty: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void;
resourceId: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
setIConfigUiData?: (data: string[]) => void;
}
interface IGraphExplorerProps {
isChanged: boolean;
}
interface IGraphExplorerStates {
isChangedState: boolean;
}
export interface GraphExplorerAdapter
extends ReactAdapter,
React.Component<IGraphExplorerProps, IGraphExplorerStates> {}
export class GraphExplorerAdapter implements ReactAdapter {
public params: Parameter;
public parameters = {};
public isNewVertexDisabled: boolean;
public constructor(params: Parameter, props?: IGraphExplorerProps) {
this.params = params;
}
public renderComponent(): JSX.Element {
return (
<GraphExplorer
onIsNewVertexDisabledChange={this.params.onIsNewVertexDisabledChange}
onGraphAccessorCreated={this.params.onGraphAccessorCreated}
onIsFilterQueryLoadingChange={this.params.onIsFilterQueryLoading}
onIsValidQueryChange={this.params.onIsValidQuery}
onIsPropertyEditing={this.params.onIsPropertyEditing}
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
graphBackendEndpoint={this.params.graphBackendEndpoint}
databaseId={this.params.databaseId}
collectionId={this.params.collectionId}
masterKey={this.params.masterKey}
onLoadStartKey={this.params.onLoadStartKey}
onLoadStartKeyChange={this.params.onLoadStartKeyChange}
resourceId={this.params.resourceId}
igraphConfigUiData={this.params.igraphConfigUiData}
igraphConfig={this.params.igraphConfig}
setIConfigUiData={this.params.setIConfigUiData}
/>
);
}
}

View File

@@ -18,20 +18,20 @@ export class GraphVizComponent extends React.Component<GraphVizComponentProps> {
this.forceGraph = new D3ForceGraph(this.props.forceGraphParams); this.forceGraph = new D3ForceGraph(this.props.forceGraphParams);
} }
public override componentDidMount(): void { public componentDidMount(): void {
this.forceGraph.init(this.rootNode); this.forceGraph.init(this.rootNode);
} }
public override shouldComponentUpdate(): boolean { public shouldComponentUpdate(): boolean {
// Prevents component re-rendering // Prevents component re-rendering
return false; return false;
} }
public override componentWillUnmount(): void { public componentWillUnmount(): void {
this.forceGraph.destroy(); this.forceGraph.destroy();
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<svg id="maingraph" ref={(elt: Element) => this.setRef(elt)}> <svg id="maingraph" ref={(elt: Element) => this.setRef(elt)}>
<title>Main Graph</title> <title>Main Graph</title>

View File

@@ -18,7 +18,7 @@ interface LeftPaneComponentProps {
} }
export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> { export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="leftPane"> <div className="leftPane">
<div className="paneTitle leftPaneResults">Results</div> <div className="paneTitle leftPaneResults">Results</div>

View File

@@ -12,7 +12,7 @@ interface MiddlePaneComponentProps {
} }
export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProps> { export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="middlePane"> <div className="middlePane">
<div className="graphTitle"> <div className="graphTitle">

View File

@@ -104,7 +104,7 @@ export class NodePropertiesComponent extends React.Component<
return null; return null;
} }
public override render(): JSX.Element { public render(): JSX.Element {
if (!this.props.node) { if (!this.props.node) {
return <span />; return <span />;
} else { } else {

View File

@@ -25,7 +25,7 @@ export class QueryContainerComponent extends React.Component<
}; };
} }
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="queryContainer"> <div className="queryContainer">
<InputTypeaheadComponent.InputTypeaheadComponent <InputTypeaheadComponent.InputTypeaheadComponent

View File

@@ -20,7 +20,7 @@ export class ReadOnlyNeighborsComponent extends React.Component<ReadOnlyNeighbor
private static readonly NO_TARGETS_LABEL = "No targets found"; private static readonly NO_TARGETS_LABEL = "No targets found";
private static readonly TARGET_TITLE = "Target"; private static readonly TARGET_TITLE = "Target";
public override render(): JSX.Element { public render(): JSX.Element {
const neighbors = this.props.isSource ? this.props.node.sources : this.props.node.targets; const neighbors = this.props.isSource ? this.props.node.sources : this.props.node.targets;
const noNeighborsLabel = this.props.isSource const noNeighborsLabel = this.props.isSource
? ReadOnlyNeighborsComponent.NO_SOURCES_LABEL ? ReadOnlyNeighborsComponent.NO_SOURCES_LABEL

View File

@@ -12,7 +12,7 @@ export interface ReadOnlyNodePropertiesComponentProps {
} }
export class ReadOnlyNodePropertiesComponent extends React.Component<ReadOnlyNodePropertiesComponentProps> { export class ReadOnlyNodePropertiesComponent extends React.Component<ReadOnlyNodePropertiesComponentProps> {
public override render(): JSX.Element { public render(): JSX.Element {
return ( return (
<table className="roPropertyTable propertyTable"> <table className="roPropertyTable propertyTable">
<tbody> <tbody>

View File

@@ -1,51 +0,0 @@
import * as ko from "knockout";
import { GraphStyleComponent, GraphStyleParams } from "./GraphStyleComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
function buildComponent(buttonOptions: any) {
document.body.innerHTML = GraphStyleComponent.template as any;
const vm = new GraphStyleComponent.viewModel(buttonOptions);
ko.applyBindings(vm);
}
describe("Graph Style Component", () => {
let buildParams = (config: ViewModels.GraphConfigUiData): GraphStyleParams => {
return {
config: config,
};
};
afterEach(() => {
ko.cleanNode(document);
});
describe("Rendering", () => {
it("should display proper list of choices passed in component parameters", () => {
const PROP2 = "prop2";
const PROPC = "prop3";
const params = buildParams({
nodeCaptionChoice: ko.observable(null),
nodeIconChoice: ko.observable(null),
nodeColorKeyChoice: ko.observable(null),
nodeIconSet: ko.observable(null),
nodeProperties: ko.observableArray(["prop1", PROP2]),
nodePropertiesWithNone: ko.observableArray(["propa", "propb", PROPC]),
showNeighborType: ko.observable(null),
});
buildComponent(params);
var e: any = document.querySelector(".graphStyle #nodeCaptionChoices");
expect(e.options.length).toBe(2);
expect(e.options[1].value).toBe(PROP2);
e = document.querySelector(".graphStyle #nodeColorKeyChoices");
expect(e.options.length).toBe(3);
expect(e.options[2].value).toBe(PROPC);
e = document.querySelector(".graphStyle #nodeIconChoices");
expect(e.options.length).toBe(3);
expect(e.options[2].value).toBe(PROPC);
});
});
});

View File

@@ -0,0 +1,67 @@
import { render, screen } from "@testing-library/react";
import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { GraphStyleComponent, GraphStyleProps } from "./GraphStyleComponent";
describe("Graph Style Component", () => {
let fakeGraphConfig: IGraphConfig;
let fakeGraphConfigUiData: ViewModels.IGraphConfigUiData;
let props: GraphStyleProps;
beforeEach(() => {
fakeGraphConfig = {
nodeColor: "orange",
nodeColorKey: "node2",
linkColor: "orange",
showNeighborType: 0,
nodeCaption: "node1",
nodeSize: 10,
linkWidth: 1,
nodeIconKey: undefined,
iconsMap: {},
};
fakeGraphConfigUiData = {
nodeCaptionChoice: "node1",
nodeIconChoice: undefined,
nodeColorKeyChoice: "node2",
nodeIconSet: undefined,
nodeProperties: ["node1", "node2", "node3"],
nodePropertiesWithNone: ["none", "node1", "node2", "node3"],
showNeighborType: undefined,
};
props = {
igraphConfig: fakeGraphConfig,
igraphConfigUiData: fakeGraphConfigUiData,
getValues: (): void => undefined,
};
render(<GraphStyleComponent {...props} />);
});
it("should render default property", () => {
const { asFragment } = render(<GraphStyleComponent {...props} />);
expect(asFragment).toMatchSnapshot();
});
it("should render node properties dropdown list ", () => {
const dropDownList = screen.getByText("Show vertex (node) as");
expect(dropDownList).toBeDefined();
});
it("should render Map this property to node color dropdown list", () => {
const nodeColorDropdownList = screen.getByText("Map this property to node color");
expect(nodeColorDropdownList).toBeDefined();
});
it("should render show neighbor options", () => {
const nodeShowNeighborOptions = screen.getByText("Show");
expect(nodeShowNeighborOptions).toBeDefined();
});
it("should call handleOnChange method", () => {
const handleOnChange = jest.fn();
const nodeCaptionDropdownList = screen.getByText("Show vertex (node) as");
nodeCaptionDropdownList.onchange = handleOnChange();
expect(handleOnChange).toHaveBeenCalled();
});
});

View File

@@ -1,103 +0,0 @@
import * as Constants from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
/**
* Parameters for this component
*/
export interface GraphStyleParams {
config: ViewModels.GraphConfigUiData;
firstFieldHasFocus?: ko.Observable<boolean>;
/**
* Callback triggered when the template is bound to the component (for testing purposes)
*/
onTemplateReady?: () => void;
}
class GraphStyleViewModel extends WaitsForTemplateViewModel {
private params: GraphStyleParams;
public constructor(params: GraphStyleParams) {
super();
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && params.onTemplateReady) {
params.onTemplateReady();
}
});
this.params = params;
}
public onAllNeighborsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.params.config.showNeighborType(ViewModels.NeighborType.BOTH);
event.stopPropagation();
return false;
}
return true;
};
public onSourcesKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.params.config.showNeighborType(ViewModels.NeighborType.SOURCES_ONLY);
event.stopPropagation();
return false;
}
return true;
};
public onTargetsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.params.config.showNeighborType(ViewModels.NeighborType.TARGETS_ONLY);
event.stopPropagation();
return false;
}
return true;
};
}
const template = `
<div id="graphStyle" class="graphStyle" data-bind="setTemplateReady: true, with:params.config">
<div class="seconddivpadding">
<p>Show vertex (node) as</p>
<select id="nodeCaptionChoices" class="formTree paneselect" required data-bind="options:nodeProperties,
value:nodeCaptionChoice, hasFocus: $parent.params.firstFieldHasFocus"></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node color</p>
<select id="nodeColorKeyChoices" class="formTree paneselect" required data-bind="options:nodePropertiesWithNone,
value:nodeColorKeyChoice"></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node icon</p>
<select id="nodeIconChoices" class="formTree paneselect" required data-bind="options:nodePropertiesWithNone,
value:nodeIconChoice"></select>
<input type="text" data-bind="value:nodeIconSet" placeholder="Icon set: blank for collection id" class="nodeIconSet" autocomplete="off" />
</div>
<p class="seconddivpadding">Show</p>
<div class="tabs">
<div class="tab">
<input type="radio" id="tab11" name="graphneighbortype" class="radio" data-bind="checkedValue:2, checked:showNeighborType" />
<label for="tab11" tabindex="0" data-bind="event: { keypress: $parent.onAllNeighborsKeyPress }">All neighbors</label>
</div>
<div class="tab">
<input type="radio" id="tab12" name="graphneighbortype" class="radio" data-bind="checkedValue:0, checked:showNeighborType" />
<label for="tab12" tabindex="0" data-bind="event: { keypress: $parent.onSourcesKeyPress }">Sources</label>
</div>
<div class="tab">
<input type="radio" id="tab13" name="graphneighbortype" class="radio" data-bind="checkedValue:1, checked:showNeighborType" />
<label for="tab13" tabindex="0" data-bind="event: { keypress: $parent.onTargetsKeyPress }">Targets</label>
</div>
</div>
</div>`;
export const GraphStyleComponent = {
viewModel: GraphStyleViewModel,
template,
};

View File

@@ -0,0 +1,131 @@
import { ChoiceGroup, Dropdown, IChoiceGroupOption, IDropdownOption, IDropdownStyles, Stack } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { IGraphConfigUiData, NeighborType } from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
const IGraphConfigType = {
NODE_CAPTION: "NODE_CAPTION",
NODE_COLOR: "NODE_COLOR",
NODE_ICON: "NODE_ICON",
SHOW_NEIGHBOR_TYPE: "SHOW_NEIGHBOR_TYPE",
};
export interface GraphStyleProps {
igraphConfig: IGraphConfig;
igraphConfigUiData: IGraphConfigUiData;
getValues: (igraphConfig?: IGraphConfig) => void;
}
export const GraphStyleComponent: FunctionComponent<GraphStyleProps> = ({
igraphConfig,
igraphConfigUiData,
getValues,
}: GraphStyleProps): JSX.Element => {
const [igraphConfigState, setIGraphConfig] = useState<IGraphConfig>(igraphConfig);
const [selected, setSelected] = useState<boolean>(false);
const nodePropertiesOptions = igraphConfigUiData.nodeProperties.map((nodeProperty) => ({
key: nodeProperty,
text: nodeProperty,
}));
const nodePropertiesWithNoneOptions = igraphConfigUiData.nodePropertiesWithNone.map((nodePropertyWithNone) => ({
key: nodePropertyWithNone,
text: nodePropertyWithNone,
}));
const showNeighborTypeOptions: IChoiceGroupOption[] = [
{ key: NeighborType.BOTH.toString(), text: "All neighbors" },
{ key: NeighborType.SOURCES_ONLY.toString(), text: "Sources" },
{ key: NeighborType.TARGETS_ONLY.toString(), text: "Targets" },
];
const dropdownStyles: Partial<IDropdownStyles> = {
dropdown: { height: 32, marginRight: 10 },
};
const choiceButtonStyles = {
flexContainer: [
{
selectors: {
".ms-ChoiceField-wrapper label": {
fontSize: 14,
paddingTop: 0,
},
},
},
],
};
useEffect(() => {
if (selected) {
getValues(igraphConfigState);
}
//eslint-disable-next-line
}, [igraphConfigState]);
const handleOnChange = (val: string, igraphConfigType: string) => {
switch (igraphConfigType) {
case IGraphConfigType.NODE_CAPTION:
setSelected(true);
setIGraphConfig({
...igraphConfigState,
nodeCaption: val,
});
break;
case IGraphConfigType.NODE_COLOR:
setSelected(true);
setIGraphConfig({
...igraphConfigState,
nodeColorKey: val,
});
break;
case IGraphConfigType.SHOW_NEIGHBOR_TYPE:
setSelected(true);
setIGraphConfig({
...igraphConfigState,
showNeighborType: parseInt(val),
});
break;
}
};
return (
<Stack>
<div id="graphStyle" className="graphStyle">
<div className="seconddivpadding">
<Dropdown
label="Show vertex (node) as"
options={nodePropertiesOptions}
required
selectedKey={igraphConfigState.nodeCaption}
styles={dropdownStyles}
onChange={(_, options: IDropdownOption) =>
handleOnChange(options.key.toString(), IGraphConfigType.NODE_CAPTION)
}
/>
</div>
<div className="seconddivpadding">
<Dropdown
label="Map this property to node color"
options={nodePropertiesWithNoneOptions}
required
selectedKey={igraphConfigState.nodeColorKey}
styles={dropdownStyles}
onChange={(_, options: IDropdownOption) =>
handleOnChange(options.key.toString(), IGraphConfigType.NODE_COLOR)
}
/>
</div>
<div className="seconddivpadding">
<ChoiceGroup
label="Show"
styles={choiceButtonStyles}
options={showNeighborTypeOptions}
selectedKey={igraphConfigState.showNeighborType.toString()}
onChange={(_, options: IChoiceGroupOption) =>
handleOnChange(options.key.toString(), IGraphConfigType.SHOW_NEIGHBOR_TYPE)
}
/>
</div>
</div>
</Stack>
);
};

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Graph Style Component should render default property 1`] = `[Function]`;

View File

@@ -1,74 +0,0 @@
<div id="graphStyle" class="graphStyle" data-bind="setTemplateReady: true, with:params.config">
<div class="seconddivpadding">
<p>Show vertex (node) as</p>
<select
id="nodeCaptionChoices"
class="formTree paneselect"
required
data-bind="options:nodeProperties,
value:nodeCaptionChoice, hasFocus: $parent.params.firstFieldHasFocus"
></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node color</p>
<select
id="nodeColorKeyChoices"
class="formTree paneselect"
required
data-bind="options:nodePropertiesWithNone,
value:nodeColorKeyChoice"
></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node icon</p>
<select
id="nodeIconChoices"
class="formTree paneselect"
required
data-bind="options:nodePropertiesWithNone,
value:nodeIconChoice"
></select>
<input
type="text"
data-bind="value:nodeIconSet"
placeholder="Icon set: blank for collection id"
class="nodeIconSet"
autocomplete="off"
/>
</div>
<p class="seconddivpadding">Show</p>
<div class="tabs">
<div class="tab">
<input
type="radio"
id="tab11"
name="graphneighbortype"
class="radio"
data-bind="checkedValue:2, checked:showNeighborType"
/>
<label for="tab11">All neighbors</label>
</div>
<div class="tab">
<input
type="radio"
id="tab12"
name="graphneighbortype"
class="radio"
data-bind="checkedValue:0, checked:showNeighborType"
/>
<label for="tab12">Sources</label>
</div>
<div class="tab">
<input
type="radio"
id="tab13"
name="graphneighbortype"
class="radio"
data-bind="checkedValue:1, checked:showNeighborType"
/>
<label for="tab13">Targets</label>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More