Compare commits

..

52 Commits

Author SHA1 Message Date
hardiknai-techm
9acf9485cb host fix 2021-05-11 20:52:54 +05:30
hardiknai-techm
9751f0dd81 resolve merge conflict 2021-05-11 20:37:53 +05:30
hardiknai-techm
e0c8f32a7e Merge branch 'move_add_Database_Panel_to_react' of https://github.com/Azure/cosmos-explorer into move_add_Database_Panel_to_react 2021-05-10 23:41:51 +05:30
hardiknai-techm
46eda44fd7 snapshort update 2021-05-10 23:41:13 +05:30
Steve Faulkner
ab0dd728b9 Update snapshot 2021-05-10 13:09:13 -05:00
hardiknai-techm
c50543c341 address comment 2021-05-10 21:56:23 +05:30
hardiknai-techm
a675124bdc Merge branch 'master' of https://github.com/Azure/cosmos-explorer into move_add_Database_Panel_to_react 2021-05-10 17:56:04 +05:30
hardiknai-techm
05eae478d0 resolve master merge conflict 2021-05-10 17:55:26 +05:30
hardiknai-techm
6b0e972564 resolve merge conflict 2021-05-07 12:53:48 +05:30
hardiknai-techm
8075072f76 correct base on some suggestion 2021-05-07 12:51:53 +05:30
Steve Faulkner
0e0e6a2257 more 2021-05-05 19:31:07 -05:00
Steve Faulkner
4299b86c55 Revert "Pane cleanup"
This reverts commit b169fba45b.
2021-05-05 19:29:56 -05:00
Steve Faulkner
b169fba45b Pane cleanup 2021-05-05 19:26:23 -05:00
Steve Faulkner
2b576d6b8a Remove core-js 2021-05-05 19:22:53 -05:00
Steve Faulkner
0c009caea6 Fix test 2021-05-05 19:15:26 -05:00
Steve Faulkner
33048b2c98 Fixes 2021-05-05 19:00:23 -05:00
Steve Faulkner
3d4a24b2fd rename 2021-05-05 18:42:30 -05:00
Steve Faulkner
2ad0236c73 Fix 2021-05-05 18:41:37 -05:00
Steve Faulkner
c180d6a0cd Merge with mater 2021-05-05 18:31:54 -05:00
Steve Faulkner
dd747ee1ee Resolve conflict 2021-05-05 15:23:37 -05:00
hardiknai-techm
5485a0d3c6 resolve merge conflict 2021-05-04 13:03:01 +05:30
hardiknai-techm
7b6975761f resolve format and compile issue 2021-04-29 08:41:14 +05:30
hardiknai-techm
ed44c8640a Merge branch 'master' of https://github.com/Azure/cosmos-explorer into move_add_Database_Panel_to_react 2021-04-29 08:36:37 +05:30
hardiknai-techm
ade9049d80 update snapshrot test case 2021-04-28 20:36:04 +05:30
hardiknai-techm
6a3f50651e marge master conflict 2021-04-28 19:58:09 +05:30
hardiknai-techm
8de3edd0f8 remove div wrapper class 2021-04-24 15:49:10 +05:30
hardiknai-techm
9f0d5658c7 Merge branch 'genericRightPaneComponent' of https://github.com/Azure/cosmos-explorer into move_add_Database_Panel_to_react 2021-04-24 13:41:03 +05:30
hardiknai-techm
28bbba5522 addDatabasePane provide backward compatibility 2021-04-24 13:39:13 +05:30
hardiknai-techm
42552bad07 addDatabasePane provide backward compatibility 2021-04-24 13:32:00 +05:30
hardiknai-techm
0aeb16fb96 resolve master branch conflict 2021-04-24 11:10:38 +05:30
hardiknai-techm
3e011f939d resolve conflict master branch 2021-04-23 19:13:13 +05:30
hardiknai-techm
43aafde25f revert knockout component 2021-04-19 18:43:10 +05:30
hardiknai-techm
9aafadf346 resolve master merge conflict 2021-04-19 17:52:28 +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
5c9ab15b3a remove genericRightPaneComponent and create RightPaneWrapper with form 2021-04-15 11:47:05 +05:30
hardiknai-techm
57d8df6608 npm install perfomr due to error 2021-04-07 10:30:51 +05:30
hardiknai-techm
2dc33c0333 remove AddDatabasePane knowckout.js code 2021-04-07 05:39:29 +05:30
hardiknai-techm
dfa97936ed resolve master merge conflict 2021-04-07 05:33:29 +05:30
hardiknai-techm
6a12ba27d9 functional component convert and use fluent ui 2021-04-07 05:21:09 +05:30
hardiknai-techm
f9ffff3c11 functional component convert and use fluent ui 2021-04-07 05:17:31 +05:30
hardiknai-techm
538b2700ef added snapshort test 2021-04-05 15:44:22 +05:30
hardiknai-techm
f468a0c36e default auto scale selected 2021-04-05 11:28:21 +05:30
hardiknai-techm
fb7526a0d0 api call with string autoPilotMaxThroughput 2021-04-05 09:09:38 +05:30
hardiknai-techm
63afb5dd6c format and compile 2021-03-31 18:36:12 +05:30
hardiknai-techm
6a1f8ce822 format and compile 2021-03-31 18:24:25 +05:30
hardiknai-techm
3b09a19ce5 resolve merge master conflict 2021-03-31 17:08:50 +05:30
hardiknai-techm
299c765e7b resolve autoscale save option 2021-03-31 17:02:03 +05:30
hardiknai-techm
8d206c39b1 remove knockout component 2021-03-30 20:16:55 +05:30
hardiknai-techm
b68e601e02 Merge branch 'master' of https://github.com/Azure/cosmos-explorer into move_add_Database_Panel_to_react 2021-03-29 16:21:23 +05:30
Your Name
201310eea4 migrate AddDatabase component 2021-03-24 12:40:20 +05:30
127 changed files with 2779 additions and 2369 deletions

View File

@@ -84,8 +84,8 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
# src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
# src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
@@ -115,8 +115,7 @@ src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
# src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/GraphStylingPane.ts
src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts

View File

@@ -3,8 +3,8 @@ module.exports = {
browser: true,
es6: true,
},
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks", "jsx-a11y"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended"],
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
@@ -34,7 +34,6 @@ module.exports = {
},
],
rules: {
"jsx-a11y/anchor-is-valid": 1,
"no-console": ["error", { allow: ["error", "warn", "dir"] }],
curly: "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",

View File

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

View File

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

View File

@@ -2099,7 +2099,7 @@ a:link {
display: flex;
flex: 1 1 auto;
overflow-x: auto;
overflow-y: hidden;
overflow-y: auto;
height: 100%;
}
@@ -3085,7 +3085,3 @@ settings-pane {
padding-left: @SmallSpace;
}
}
.hiddenMain {
visibility: hidden;
height: 0px;
}

View File

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

101
package-lock.json generated
View File

@@ -2277,6 +2277,15 @@
}
}
},
"@fluentui/react-window-provider": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-1.0.2.tgz",
"integrity": "sha512-fGSgL3Vp/+6t1Ysfz21FWZmqsU+iFVxOigvHnm5uKVyyRPwtaabv/F6kQ2y5isLMI2YmJaUd2i0cDJKu8ggrvw==",
"requires": {
"@uifabric/set-version": "^7.0.24",
"tslib": "^1.10.0"
}
},
"@fluentui/set-version": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.1.0.tgz",
@@ -5255,12 +5264,6 @@
"@types/d3-selection": "*"
}
},
"@types/dom-to-image": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.2.tgz",
"integrity": "sha512-Yxbwmz/glNwRIXfBI8efG2bgIxrFAKV1MdfpqbUDq25ULMot7U7FYXPiso5G8DlBExSP+AakuG0mNus9yw4RZQ==",
"dev": true
},
"@types/dom4": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz",
@@ -6723,12 +6726,6 @@
"integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=",
"dev": true
},
"ast-types-flow": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
"integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
"dev": true
},
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
@@ -6790,12 +6787,6 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"axe-core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.2.0.tgz",
"integrity": "sha512-1uIESzroqpaTzt9uX48HO+6gfnKu3RwvWdCcWSrX4csMInJfCo1yvKPNXCwXFRpJqRW25tiASb6No0YH57PXqg==",
"dev": true
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
@@ -6811,12 +6802,6 @@
}
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
"integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==",
"dev": true
},
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -9105,12 +9090,6 @@
"d3-transition": "2"
}
},
"damerau-levenshtein": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz",
"integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -9697,11 +9676,6 @@
}
}
},
"dom-to-image": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz",
"integrity": "sha1-ilA2CAiMh7HCL5A0rgMuGJiVWGc="
},
"dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
@@ -10460,43 +10434,6 @@
"@typescript-eslint/experimental-utils": "^2.5.0"
}
},
"eslint-plugin-jsx-a11y": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz",
"integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==",
"dev": true,
"requires": {
"@babel/runtime": "^7.11.2",
"aria-query": "^4.2.2",
"array-includes": "^3.1.1",
"ast-types-flow": "^0.0.7",
"axe-core": "^4.0.2",
"axobject-query": "^2.2.0",
"damerau-levenshtein": "^1.0.6",
"emoji-regex": "^9.0.0",
"has": "^1.0.3",
"jsx-ast-utils": "^3.1.0",
"language-tags": "^1.0.5"
},
"dependencies": {
"emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"jsx-ast-utils": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz",
"integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==",
"dev": true,
"requires": {
"array-includes": "^3.1.2",
"object.assign": "^4.1.2"
}
}
}
},
"eslint-plugin-no-null": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-null/-/eslint-plugin-no-null-1.0.2.tgz",
@@ -18300,21 +18237,6 @@
"resolved": "https://registry.npmjs.org/labella/-/labella-1.1.4.tgz",
"integrity": "sha1-xsxaNA6N80DrM1YzaD6lm4KMMi0="
},
"language-subtag-registry": {
"version": "0.3.21",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
"integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==",
"dev": true
},
"language-tags": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
"integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=",
"dev": true,
"requires": {
"language-subtag-registry": "~0.3.2"
}
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
@@ -26005,11 +25927,6 @@
"version": "1.0.46",
"resolved": "https://registry.npmjs.org/zalgo-promise/-/zalgo-promise-1.0.46.tgz",
"integrity": "sha512-tzPpQRqaQQavxl17TY98nznvmr+judUg3My7ugsUcRDbdqisYOE2z79HNNDgXnyX3eA0mf2bMOJrqHptt00npg=="
},
"zustand": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.0.tgz",
"integrity": "sha512-fwZfax2c954pWB+RYXHXG0HWyuoUj8YiNykRjZC/w6L7ay9fPQ7M90mgDVP1KJsRqxLsDILBWSNxuzw5BkCpxA=="
}
}
}

View File

@@ -56,7 +56,6 @@
"datatables.net-dt": "1.10.19",
"date-fns": "1.29.0",
"dayjs": "1.8.19",
"dom-to-image": "2.6.0",
"dotenv": "8.2.0",
"eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0",
@@ -98,8 +97,7 @@
"swr": "0.4.0",
"terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1",
"utility-types": "3.10.0",
"zustand": "3.5.0"
"utility-types": "3.10.0"
},
"devDependencies": {
"@babel/core": "7.9.0",
@@ -111,7 +109,6 @@
"@types/codemirror": "0.0.56",
"@types/crossroads": "0.0.30",
"@types/d3": "5.9.2",
"@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.7",
"@types/enzyme-adapter-react-16": "1.0.6",
"@types/hasher": "0.0.31",
@@ -142,7 +139,6 @@
"enzyme-to-json": "3.6.1",
"eslint": "7.8.1",
"eslint-cli": "1.1.1",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.2",
"eslint-plugin-react-hooks": "4.2.0",

View File

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

View File

@@ -9,17 +9,11 @@ import postRobot from "post-robot";
import * as React from "react";
import * as ReactDOM from "react-dom";
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/default.css";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import "./CellOutputViewer.less";
import { TransformMedia } from "./TransformMedia";
export interface SnapshotResponse {
imageSrc: string;
requestId: string;
}
export interface CellOutputViewerProps {
id: string;
contentRef: ContentRef;
@@ -68,36 +62,6 @@ const onInit = async () => {
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

View File

@@ -1,41 +0,0 @@
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" aria-label="Expand Tree">
<span
className="leftarrowCollapsed"
onClick={toggleLeftPaneExpanded}
role="button"
tabIndex={0}
onKeyDown={toggleLeftPaneExpanded}
>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span
className="collectionCollapsed"
onClick={toggleLeftPaneExpanded}
role="button"
tabIndex={0}
onKeyDown={toggleLeftPaneExpanded}
>
<span data-bind="text: collectionTitle" />
</span>
</li>
</ul>
</div>
</div>
);
};

View File

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

View File

@@ -40,6 +40,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
<TextField
label={entityValueLabel && entityValueLabel}
id="entityTimeId"
autoFocus
type="time"
value={entityTimeValue}
onChange={onEntityTimeValueChange}
@@ -54,6 +55,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
label={entityValueLabel && entityValueLabel}
className="addEntityTextField"
id="entityValueId"
autoFocus
disabled={isEntityValueDisable}
type={entityValueType}
placeholder={entityValuePlaceholder}

View File

@@ -1,60 +0,0 @@
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"
onKeyDown={toggleLeftPaneExpanded}
>
<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

@@ -96,6 +96,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<TextField
label={entityPropertyLabel && entityPropertyLabel}
id="entityPropertyId"
autoFocus
disabled={isPropertyTypeDisable}
placeholder={entityPropertyPlaceHolder}
value={entityProperty}

View File

@@ -5,7 +5,7 @@ export interface TooltipProps {
children: string;
}
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
export const InfoTooltip: React.FunctionComponent = ({ children }: TooltipProps) => {
return (
<span>
<TooltipHost content={children}>

View File

@@ -47,7 +47,7 @@ export const Upload: FunctionComponent<UploadProps> = ({
props.onUpload(event);
}
};
const title = label + " to upload";
return (
<div>
<span className="renewUploadItemsHeader">{label}</span>
@@ -67,7 +67,7 @@ export const Upload: FunctionComponent<UploadProps> = ({
role="button"
/>
<a href="#" id="fileImportLinkNotebook" onClick={onImportLinkClick} onKeyPress={onImportLinkKeyPress}>
<Image className="fileImportImg" src={FolderIcon} alt={label} title={label} />
<Image className="fileImportImg" src={FolderIcon} alt={title} title={title} />
</a>
</Stack>
</div>

View File

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

View File

@@ -4,10 +4,26 @@ import * as ko from "knockout";
import "./ComponentRegisterer";
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", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true);
});
it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});
it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
});

View File

@@ -2,10 +2,16 @@ import * as ko from "knockout";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
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 { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
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("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent());
@@ -15,4 +21,5 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());

View File

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

View File

@@ -32,9 +32,6 @@
.errorLink {
cursor: pointer;
}
.errorLinkColor {
color: @AccentMediumHigh;
}
}
.paneErrorIcon {

View File

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,6 @@
<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

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

View File

@@ -0,0 +1,186 @@
/**
* 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

@@ -0,0 +1,19 @@
<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

@@ -25,7 +25,6 @@ export class RadioSwitchComponent extends React.Component<RadioSwitchComponentPr
<div className="radioSwitchComponent">
{this.props.choices.map((choice: Choice) => (
<span
role="button"
tabIndex={0}
key={choice.key}
onClick={() => this.onSelect(choice)}

View File

@@ -122,7 +122,7 @@ export class IndexingPolicyComponent extends React.Component<
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" role="button" tabIndex={0} ref={this.indexingPolicyDiv}></div>
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>
);
}

View File

@@ -13,7 +13,6 @@ exports[`IndexingPolicyComponent renders 1`] = `
/>
<div
className="settingsV2IndexingPolicyEditor"
role="button"
tabIndex={0}
/>
</Stack>

View File

@@ -32,6 +32,115 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionText": [Function],
@@ -917,11 +1026,32 @@ exports[`SettingsComponent renders 1`] = `
},
},
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
@@ -988,6 +1118,7 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function],
"openedTabs": [Function],
},
"toggleLeftPaneExpandedKeyPress": [Function],
},
"databaseId": "test",
"defaultTtl": [Function],
@@ -1013,6 +1144,115 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionText": [Function],
@@ -1898,11 +2138,32 @@ exports[`SettingsComponent renders 1`] = `
},
},
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
@@ -1969,6 +2230,7 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function],
"openedTabs": [Function],
},
"toggleLeftPaneExpandedKeyPress": [Function],
}
}
isAutoPilotSelected={false}
@@ -2007,6 +2269,115 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionText": [Function],
@@ -2892,11 +3263,32 @@ exports[`SettingsComponent renders 1`] = `
},
},
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
@@ -2963,6 +3355,7 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function],
"openedTabs": [Function],
},
"toggleLeftPaneExpandedKeyPress": [Function],
},
"databaseId": "test",
"defaultTtl": [Function],
@@ -2988,6 +3381,115 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionText": [Function],
@@ -3873,11 +4375,32 @@ exports[`SettingsComponent renders 1`] = `
},
},
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
@@ -3944,6 +4467,7 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function],
"openedTabs": [Function],
},
"toggleLeftPaneExpandedKeyPress": [Function],
}
}
geospatialConfigType="Geometry"

View File

@@ -20,13 +20,17 @@ describe("ThroughputInput Pane", () => {
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)"
it("test Autoscale Mode select", () => {
wrapper.setProps({ isAutoscaleSelected: true });
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toBe(
"Estimate your required RU/s with capacity calculator."
);
expect(wrapper.find('[aria-label="maxRUDescription"]').at(0).text()).toContain("Max RU/s");
});
wrapper.find('[aria-label="Autoscale mode"]').simulate("change");
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
it("test Manual Mode select", () => {
wrapper.setProps({ isAutoscaleSelected: false });
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toContain("Estimate your required RU/s with");
expect(wrapper.find('[aria-label="capacityLink"]').at(0).text()).toContain("capacity calculator");
});
});

View File

@@ -17,6 +17,8 @@ export interface ThroughputInputProps {
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
isAutoscaleSelected?: boolean;
throughput?: number;
}
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
@@ -25,16 +27,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setThroughputValue,
setIsAutoscale,
isSharded,
isAutoscaleSelected = true,
throughput = AutoPilotUtils.minAutoPilotThroughput,
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>("");
setIsAutoscale(isAutoscaleSelected);
setThroughputValue(throughput);
const getThroughputLabelText = (): string => {
let throughputHeaderText: string;
if (isAutoscaleSelected) {
@@ -51,7 +49,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const onThroughputValueChange = (newInput: string): void => {
const newThroughput = parseInt(newInput);
setThroughput(newThroughput);
setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) {
setThroughputError("Unsharded collections support up to 10,000 RUs");
@@ -85,13 +82,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
}
@@ -101,7 +94,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text aria-label="Throughput header" variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{getThroughputLabelText()}
</Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>
@@ -113,6 +106,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
aria-label="Autoscale mode"
checked={isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
/>
@@ -123,6 +117,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
aria-label="Manual mode"
checked={!isAutoscaleSelected}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")}
/>

View File

@@ -25,7 +25,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
* 
</span>
<Text
aria-label="Throughput header"
key=".0:$.1"
style={
Object {
@@ -36,7 +35,6 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
variant="small"
>
<span
aria-label="Throughput header"
className="css-54"
style={
Object {
@@ -655,6 +653,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="throughputInputRadioBtn"
key=".0:$.0"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
@@ -670,6 +669,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
className="throughputInputRadioBtn"
key=".0:$.2"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>

View File

@@ -34,7 +34,6 @@ import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
@@ -46,7 +45,6 @@ import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/Gallery
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
@@ -61,6 +59,7 @@ import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfir
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
@@ -92,7 +91,7 @@ export interface ExplorerParams {
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
setNotificationConsoleData: (consoleData: ConsoleData) => void;
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
closeSidePanel: () => void;
closeDialog: () => void;
openDialog: (props: DialogProps) => void;
@@ -126,13 +125,14 @@ export default class Explorer {
// Panes
public contextPanes: ContextualPaneBase[];
public openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void;
public openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
public closeSidePanel: () => void;
// Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>;
private resourceTree: ResourceTreeAdapter;
@@ -150,6 +150,7 @@ export default class Explorer {
// Contextual panes
public addDatabasePane: AddDatabasePane;
public graphStylingPane: GraphStylingPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient;
public gitHubOAuthService: GitHubOAuthService;
@@ -161,6 +162,7 @@ export default class Explorer {
public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks
@@ -180,6 +182,7 @@ export default class Explorer {
public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = [];
private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager;
@@ -226,7 +229,6 @@ export default class Explorer {
});
}
});
this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSparkEnabledForAccount = ko.observable(false);
@@ -331,6 +333,7 @@ export default class Explorer {
}
return true;
});
this.isLeftPaneExpanded = ko.observable<boolean>(true);
this.selectedNode = ko.observable<ViewModels.TreeNode>();
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
// Make sure switching tabs restores tabs display
@@ -364,7 +367,7 @@ export default class Explorer {
return false;
}
return isCapabilityEnabled("EnableMongo");
return userContext.apiType === "Mongo";
});
this.isServerlessEnabled = ko.computed(
@@ -408,6 +411,13 @@ export default class Explorer {
container: this,
});
this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false),
@@ -423,6 +433,7 @@ export default class Explorer {
}
});
this._panes = [this.addDatabasePane, this.graphStylingPane, this.cassandraAddCollectionPane];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false);
@@ -662,8 +673,16 @@ export default class Explorer {
this.setIsNotificationConsoleExpanded(true);
}
public collapseConsole(): void {
this.setIsNotificationConsoleExpanded(false);
public toggleLeftPaneExpanded() {
this.isLeftPaneExpanded(!this.isLeftPaneExpanded());
if (this.isLeftPaneExpanded()) {
document.getElementById("expandToggleLeftPaneButton").focus();
this.splitter.expandLeft();
} else {
document.getElementById("collapseToggleLeftPaneButton").focus();
this.splitter.collapseLeft();
}
}
public refreshDatabaseForResourceToken(): Q.Promise<any> {
@@ -782,6 +801,14 @@ export default class Explorer {
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
public provideFeedbackEmail = () => {
window.open(Constants.Urls.feedbackEmail, "_blank");
@@ -1036,9 +1063,6 @@ export default class Explorer {
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true);
}
if (flights.indexOf(Constants.Flights.SchemaAnalyzer) !== -1) {
userContext.features.enableSchemaAnalyzer = true;
}
}
public findSelectedCollection(): ViewModels.Collection {
@@ -1047,6 +1071,10 @@ export default class Explorer {
: this.selectedNode().collection) as ViewModels.Collection;
}
public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
}
public isRunningOnNationalCloud(): boolean {
return (
userContext.portalEnv === "blackforest" ||
@@ -1242,18 +1270,10 @@ export default class Explorer {
public async publishNotebook(
name: string,
content: NotebookPaneContent,
notebookContentRef?: string,
onTakeSnapshot?: (request: SnapshotRequest) => void,
onClosePanel?: () => void
parentDomElement?: HTMLElement
): Promise<void> {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(
name,
content,
notebookContentRef,
onTakeSnapshot,
onClosePanel
);
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
this.isPublishNotebookPaneEnabled(true);
}
}
@@ -1840,6 +1860,7 @@ export default class Explorer {
public async handleOpenFileAction(path: string): Promise<void> {
if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this.closeAllPanes();
this._openSetupNotebooksPaneForQuickstart();
}
@@ -1957,7 +1978,7 @@ export default class Explorer {
"Add " + getDatabaseName(),
<AddDatabasePanel
explorer={this}
openNotificationConsole={() => this.expandConsole()}
openNotificationConsole={this.expandConsole}
closePanel={this.closeSidePanel}
/>
);

View File

@@ -1,7 +1,7 @@
import * as sinon from "sinon";
import { D3ForceGraph, LoadMoreDataAction, D3GraphNodeData } from "./D3ForceGraph";
import { D3Node, D3Link, GraphData } from "../GraphExplorerComponent/GraphData";
import GraphTab from "../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "../GraphExplorerComponent/GraphData";
import { D3ForceGraph, D3GraphNodeData, LoadMoreDataAction } from "./D3ForceGraph";
describe("D3ForceGraph", () => {
const v1Id = "v1";
@@ -68,7 +68,7 @@ describe("D3ForceGraph", () => {
beforeEach(() => {
forceGraph = new D3ForceGraph({
igraphConfig: GraphTab.createIGraphConfig(),
graphConfig: GraphTab.createGraphConfig(),
onHighlightedNode: sinon.spy(),
onLoadMoreData: (action: LoadMoreDataAction): void => {},
@@ -141,7 +141,6 @@ describe("D3ForceGraph", () => {
const mouseoverEvent = document.createEvent("Events");
mouseoverEvent.initEvent("mouseover", true, false);
$(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
expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true);
@@ -151,7 +150,7 @@ describe("D3ForceGraph", () => {
expect(onHighlightedNode.id).toEqual(v1Id);
};
forceGraph.updateGraph(newGraph, forceGraph.igraphConfig);
forceGraph.updateGraph(newGraph);
});
});
});

View File

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

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
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

@@ -0,0 +1,51 @@
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

@@ -1,67 +0,0 @@
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

@@ -0,0 +1,103 @@
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

@@ -1,131 +0,0 @@
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

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

View File

@@ -0,0 +1,74 @@
<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>

View File

@@ -120,7 +120,6 @@ export class NotificationConsoleComponent extends React.Component<
onClick={() => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
tabIndex={0}
role="button"
>
<div className="statusBar">
<span className="dataTypeIcons">
@@ -180,7 +179,7 @@ export class NotificationConsoleComponent extends React.Component<
onKeyDown={(event: React.KeyboardEvent<HTMLSpanElement>) => this.onClearNotificationsKeyPress(event)}
tabIndex={0}
>
<img src={ClearIcon} alt="clear notifications" />
<img src={ClearIcon} alt="clear notifications image" />
Clear Notifications
</span>
</div>

View File

@@ -9,7 +9,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
id="notificationConsoleHeader"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<div
@@ -151,7 +150,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
tabIndex={0}
>
<img
alt="clear notifications"
alt="clear notifications image"
src=""
/>
Clear Notifications
@@ -174,7 +173,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
id="notificationConsoleHeader"
onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
>
<div
@@ -318,7 +316,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
tabIndex={0}
>
<img
alt="clear notifications"
alt="clear notifications image"
src=""
/>
Clear Notifications

View File

@@ -1,108 +0,0 @@
import { FileType, IContent, IContentProvider, ServerConfig } from "@nteract/core";
import { Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { HttpStatusCodes } from "../../../../Common/Constants";
import { getErrorMessage } from "../../../../Common/ErrorHandlingUtils";
import * as Logger from "../../../../Common/Logger";
export interface InMemoryContentProviderParams {
[path: string]: { readonly: boolean; content: IContent<FileType> };
}
// Nteract relies on `errno` property to figure out the kind of failure
// That's why we need a custom wrapper around Error to include `errno` property
class InMemoryContentProviderError extends Error {
constructor(error: string, public errno: number = InMemoryContentProvider.SelfErrorCode) {
super(error);
}
}
export class InMemoryContentProvider implements IContentProvider {
public static readonly SelfErrorCode = 666;
constructor(private params: InMemoryContentProviderParams) {}
public remove(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "remove");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public get(_config: ServerConfig, uri: string): Observable<AjaxResponse> {
const item = this.params[uri];
if (item) {
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
}
return this.errorResponse(`${uri} not found`, "get");
}
public update(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "update");
}
public create(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "create");
}
public save<FT extends FileType>(
_config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars
uri: string,
model: Partial<IContent<FT>>
): Observable<AjaxResponse> {
const item = this.params[uri];
if (item) {
if (!item.readonly) {
Object.assign(item.content, model);
}
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
}
return this.errorResponse(`${uri} not found`, "save");
}
public listCheckpoints(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "listCheckpoints");
}
public createCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "createCheckpoint");
}
public deleteCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "deleteCheckpoint");
}
public restoreFromCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "restoreFromCheckpoint");
}
private errorResponse(message: string, functionName: string): Observable<AjaxResponse> {
const error = new InMemoryContentProviderError(message);
Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno);
return of(this.createErrorAjaxResponse(error));
}
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
return {
originalEvent: new Event("no-op"),
xhr: new XMLHttpRequest(),
request: {},
status,
response: content ? content : undefined,
responseText: content ? JSON.stringify(content) : undefined,
responseType: "json",
};
}
private createErrorAjaxResponse(error: InMemoryContentProviderError): AjaxResponse {
return {
originalEvent: new Event("no-op"),
xhr: new XMLHttpRequest(),
request: {},
status: error.errno,
response: error,
responseText: getErrorMessage(error),
responseType: "json",
};
}
}

View File

@@ -1,15 +0,0 @@
// memory://<path>
// Custom scheme for in memory content
export const ContentUriPattern = /memory:\/\/([^/]*)/;
export function fromContentUri(contentUri: string): undefined | string {
const matches = contentUri.match(ContentUriPattern);
if (matches && matches.length > 1) {
return matches[1];
}
return undefined;
}
export function toContentUri(path: string): string {
return `memory://${path}`;
}

View File

@@ -1,11 +1,14 @@
// Vendor modules
import { actions, createContentRef, createKernelRef, selectors } from "@nteract/core";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { NotebookClientV2 } from "../NotebookClientV2";
// Vendor modules
import { actions, createContentRef, createKernelRef, selectors } from "@nteract/core";
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { CdbAppState } from "./types";
export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem;
@@ -16,6 +19,7 @@ export interface NotebookComponentAdapterOptions {
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void;
public getNotebookParentElement: () => HTMLElement;
public parameters: any;
constructor(options: NotebookComponentAdapterOptions) {
@@ -42,6 +46,11 @@ export class NotebookComponentAdapter extends NotebookComponentBootstrapper impl
})
);
}
this.getNotebookParentElement = () => {
const cdbAppState = this.getStore().getState() as CdbAppState;
return cdbAppState.cdb.currentNotebookParentElements.get(this.contentRef);
};
}
protected renderExtraComponent = (): JSX.Element => {

View File

@@ -1,29 +1,35 @@
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
import * as React from "react";
import { NotebookComponent } from "./NotebookComponent";
import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookUtil } from "../NotebookUtil";
// Vendor modules
import {
actions,
AppState,
ContentRef,
createKernelRef,
DocumentRecordProps,
ContentRef,
KernelRef,
NotebookContentRecord,
selectors,
} from "@nteract/core";
import "@nteract/styles/editor-overrides.css";
import "@nteract/styles/global-variables.css";
import * as Immutable from "immutable";
import { Provider } from "react-redux";
import { CellType, CellId, ImmutableNotebook } from "@nteract/commutable";
import { Store, AnyAction } from "redux";
import "./NotebookComponent.less";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/lib/codemirror.css";
import * as Immutable from "immutable";
import * as React from "react";
import { Provider } from "react-redux";
import "@nteract/styles/editor-overrides.css";
import "@nteract/styles/global-variables.css";
import "react-table/react-table.css";
import { AnyAction, Store } from "redux";
import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil";
import * as CdbActions from "./actions";
import { NotebookComponent } from "./NotebookComponent";
import "./NotebookComponent.less";
import * as NteractUtil from "../NTeractUtil";
export interface NotebookComponentBootstrapperOptions {
notebookClient: NotebookClientV2;
@@ -31,7 +37,7 @@ export interface NotebookComponentBootstrapperOptions {
}
export class NotebookComponentBootstrapper {
public contentRef: ContentRef;
protected contentRef: ContentRef;
protected renderExtraComponent: () => JSX.Element;
private notebookClient: NotebookClientV2;

View File

@@ -1,17 +1,11 @@
import { FileType, IContent, IContentProvider, IGetParams, ServerConfig } from "@nteract/core";
import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "@nteract/core";
import { Observable } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider";
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
export class NotebookContentProvider implements IContentProvider {
constructor(
private inMemoryContentProvider: InMemoryContentProvider,
private gitHubContentProvider: GitHubContentProvider,
private jupyterContentProvider: IContentProvider
) {}
constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {}
public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).remove(serverConfig, path);
@@ -66,10 +60,6 @@ export class NotebookContentProvider implements IContentProvider {
}
private getContentProvider(path: string): IContentProvider {
if (InMemoryContentProviderUtils.fromContentUri(path)) {
return this.inMemoryContentProvider;
}
if (GitHubUtils.fromContentUri(path)) {
return this.gitHubContentProvider;
}

View File

@@ -1,7 +1,6 @@
import { CellId } from "@nteract/commutable";
import { ContentRef } from "@nteract/core";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { SnapshotFragment, SnapshotRequest } from "./types";
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
export interface CloseNotebookAction {
@@ -86,68 +85,21 @@ export const traceNotebookTelemetry = (payload: {
};
};
export const STORE_CELL_OUTPUT_SNAPSHOT = "STORE_CELL_OUTPUT_SNAPSHOT";
export interface StoreCellOutputSnapshotAction {
type: "STORE_CELL_OUTPUT_SNAPSHOT";
export const UPDATE_NOTEBOOK_PARENT_DOM_ELTS = "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
export interface UpdateNotebookParentDomEltAction {
type: "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
payload: {
cellId: string;
snapshot: SnapshotFragment;
contentRef: ContentRef;
parentElt: HTMLElement;
};
}
export const storeCellOutputSnapshot = (payload: {
cellId: string;
snapshot: SnapshotFragment;
}): StoreCellOutputSnapshotAction => {
export const UpdateNotebookParentDomElt = (payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
}): UpdateNotebookParentDomEltAction => {
return {
type: STORE_CELL_OUTPUT_SNAPSHOT,
payload,
};
};
export const STORE_NOTEBOOK_SNAPSHOT = "STORE_NOTEBOOK_SNAPSHOT";
export interface StoreNotebookSnapshotAction {
type: "STORE_NOTEBOOK_SNAPSHOT";
payload: {
imageSrc: string;
requestId: string;
};
}
export const storeNotebookSnapshot = (payload: {
imageSrc: string;
requestId: string;
}): StoreNotebookSnapshotAction => {
return {
type: STORE_NOTEBOOK_SNAPSHOT,
payload,
};
};
export const TAKE_NOTEBOOK_SNAPSHOT = "TAKE_NOTEBOOK_SNAPSHOT";
export interface TakeNotebookSnapshotAction {
type: "TAKE_NOTEBOOK_SNAPSHOT";
payload: SnapshotRequest;
}
export const takeNotebookSnapshot = (payload: SnapshotRequest): TakeNotebookSnapshotAction => {
return {
type: TAKE_NOTEBOOK_SNAPSHOT,
payload,
};
};
export const NOTEBOOK_SNAPSHOT_ERROR = "NOTEBOOK_SNAPSHOT_ERROR";
export interface NotebookSnapshotErrorAction {
type: "NOTEBOOK_SNAPSHOT_ERROR";
payload: {
error: string;
};
}
export const notebookSnapshotError = (payload: { error: string }): NotebookSnapshotErrorAction => {
return {
type: NOTEBOOK_SNAPSHOT_ERROR,
type: UPDATE_NOTEBOOK_PARENT_DOM_ELTS,
payload,
};
};

View File

@@ -70,32 +70,17 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
return state.set("hoveredCellId", typedAction.payload.cellId);
}
case cdbActions.STORE_CELL_OUTPUT_SNAPSHOT: {
const typedAction = action as cdbActions.StoreCellOutputSnapshotAction;
state.cellOutputSnapshots.set(typedAction.payload.cellId, typedAction.payload.snapshot);
// TODO Simpler datastructure to instantiate new Map?
return state.set("cellOutputSnapshots", new Map(state.cellOutputSnapshots));
}
case cdbActions.STORE_NOTEBOOK_SNAPSHOT: {
const typedAction = action as cdbActions.StoreNotebookSnapshotAction;
// Clear pending request
return state.set("notebookSnapshot", typedAction.payload).set("pendingSnapshotRequest", undefined);
}
case cdbActions.TAKE_NOTEBOOK_SNAPSHOT: {
const typedAction = action as cdbActions.TakeNotebookSnapshotAction;
// Clear previous snapshots
return state
.set("cellOutputSnapshots", new Map())
.set("notebookSnapshot", undefined)
.set("notebookSnapshotError", undefined)
.set("pendingSnapshotRequest", typedAction.payload);
}
case cdbActions.NOTEBOOK_SNAPSHOT_ERROR: {
const typedAction = action as cdbActions.NotebookSnapshotErrorAction;
return state.set("notebookSnapshotError", typedAction.payload.error);
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");
const contentRef = typedAction.payload.contentRef;
const parentElt = typedAction.payload.parentElt;
if (parentElt) {
parentEltsMap.set(contentRef, parentElt);
} else {
parentEltsMap.delete(contentRef);
}
return state.set("currentNotebookParentElements", parentEltsMap);
}
}
return state;

View File

@@ -1,40 +1,15 @@
import { CellId } from "@nteract/commutable";
import { AppState } from "@nteract/core";
import * as Immutable from "immutable";
import { AppState, ContentRef } from "@nteract/core";
import { Notebook } from "../../../Common/Constants";
export interface SnapshotFragment {
image: HTMLImageElement;
boundingClientRect: DOMRect;
requestId: string;
}
export type SnapshotRequest = NotebookSnapshotRequest | CellSnapshotRequest;
interface NotebookSnapshotRequestBase {
requestId: string;
aspectRatio: number;
notebookContentRef: string; // notebook redux contentRef
downloadFilename?: string; // Optional: will download as a file
}
interface NotebookSnapshotRequest extends NotebookSnapshotRequestBase {
type: "notebook";
}
interface CellSnapshotRequest extends NotebookSnapshotRequestBase {
type: "celloutput";
cellId: string;
}
import { CellId } from "@nteract/commutable";
export interface CdbRecordProps {
databaseAccountName: string | undefined;
defaultExperience: string | undefined;
kernelRestartDelayMs: number;
hoveredCellId: CellId | undefined;
cellOutputSnapshots: Map<string, SnapshotFragment>;
notebookSnapshot?: { imageSrc: string; requestId: string };
pendingSnapshotRequest?: SnapshotRequest;
notebookSnapshotError?: string;
currentNotebookParentElements: Map<ContentRef, HTMLElement>;
}
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
@@ -48,8 +23,5 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
defaultExperience: undefined,
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
hoveredCellId: undefined,
cellOutputSnapshots: new Map(),
notebookSnapshot: undefined,
pendingSnapshotRequest: undefined,
notebookSnapshotError: undefined,
currentNotebookParentElements: new Map<ContentRef, HTMLElement>(),
});

View File

@@ -22,14 +22,12 @@ import { getFullName } from "../../Utils/UserUtils";
import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
// import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { SnapshotRequest } from "./NotebookComponent/types";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
type NotebookPaneContent = string | ImmutableNotebook;
@@ -51,7 +49,6 @@ export default class NotebookManager {
public notebookClient: NotebookContainerClient;
public notebookContentClient: NotebookContentClient;
private inMemoryContentProvider: InMemoryContentProvider;
private gitHubContentProvider: GitHubContentProvider;
public gitHubOAuthService: GitHubOAuthService;
public gitHubClient: GitHubClient;
@@ -65,20 +62,12 @@ export default class NotebookManager {
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
this.gitHubClient = new GitHubClient(this.onGitHubClientError);
this.inMemoryContentProvider = new InMemoryContentProvider({
[SchemaAnalyzerNotebook.path]: {
readonly: true,
content: SchemaAnalyzerNotebook,
},
});
this.gitHubContentProvider = new GitHubContentProvider({
gitHubClient: this.gitHubClient,
promptForCommitMsg: this.promptForCommitMsg,
});
this.notebookContentProvider = new NotebookContentProvider(
this.inMemoryContentProvider,
this.gitHubContentProvider,
contents.JupyterContentProvider
);
@@ -123,13 +112,11 @@ export default class NotebookManager {
public async openPublishNotebookPane(
name: string,
content: NotebookPaneContent,
notebookContentRef: string,
onTakeSnapshot: (request: SnapshotRequest) => void,
onClosePanel: () => void
parentDomElement: HTMLElement
): Promise<void> {
const explorer = this.params.container;
explorer.openSidePanel(
"Publish Notebook",
"New Collection",
<PublishNotebookPane
explorer={this.params.container}
junoClient={this.junoClient}
@@ -138,10 +125,8 @@ export default class NotebookManager {
name={name}
author={getFullName()}
notebookContent={content}
notebookContentRef={notebookContentRef}
onTakeSnapshot={onTakeSnapshot}
/>,
onClosePanel
parentDomElement={parentDomElement}
/>
);
}

View File

@@ -1,6 +1,6 @@
import { CellId } from "@nteract/commutable";
import { CellType } from "@nteract/commutable/src";
import { actions, ContentRef, selectors } from "@nteract/core";
import { actions, ContentRef } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
@@ -12,8 +12,6 @@ import { Dispatch } from "redux";
import { userContext } from "../../../UserContext";
import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
@@ -34,18 +32,10 @@ export interface NotebookRendererBaseProps {
}
interface NotebookRendererDispatchProps {
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
notebookSnapshotError: (error: string) => void;
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
}
interface StateProps {
pendingSnapshotRequest: SnapshotRequest;
cellOutputSnapshots: Map<string, SnapshotFragment>;
notebookSnapshot: { imageSrc: string; requestId: string };
nbCodeCells: number;
}
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps & StateProps;
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
const Cell = () => (
@@ -70,37 +60,27 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
private notebookRendererRef = React.createRef<HTMLDivElement>();
constructor(props: NotebookRendererProps) {
super(props);
this.state = {
hoveredCellId: undefined,
};
}
componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any);
}
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
async componentDidUpdate(): Promise<void> {
// Take a snapshot if there's a pending request and all the outputs are also saved
if (
this.props.pendingSnapshotRequest &&
this.props.pendingSnapshotRequest.type === "notebook" &&
this.props.pendingSnapshotRequest.notebookContentRef === this.props.contentRef &&
(!this.props.notebookSnapshot ||
this.props.pendingSnapshotRequest.requestId !== this.props.notebookSnapshot.requestId) &&
this.props.cellOutputSnapshots.size === this.props.nbCodeCells
) {
try {
// Use Html2Canvas because it is much more reliable and fast than dom-to-file
const result = await NotebookUtil.takeScreenshotHtml2Canvas(
this.notebookRendererRef.current,
this.props.pendingSnapshotRequest.aspectRatio,
[...this.props.cellOutputSnapshots.values()],
this.props.pendingSnapshotRequest.downloadFilename
);
this.props.storeNotebookSnapshot(result.imageSrc, this.props.pendingSnapshotRequest.requestId);
} catch (error) {
this.props.notebookSnapshotError(error.message);
} finally {
this.setState({ processedSnapshotRequest: undefined });
}
}
componentDidUpdate() {
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentWillUnmount() {
this.props.updateNotebookParentDomElt(this.props.contentRef, undefined);
}
render(): JSX.Element {
@@ -176,40 +156,28 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
}
}
export const makeMapStateToProps = (
initialState: CdbAppState,
ownProps: NotebookRendererProps
): ((state: CdbAppState) => StateProps) => {
const mapStateToProps = (state: CdbAppState): StateProps => {
const { contentRef } = ownProps;
const model = selectors.model(state, { contentRef });
let nbCodeCells;
if (model && model.type === "notebook") {
nbCodeCells = NotebookUtil.findCodeCellWithDisplay(model.notebook).length;
}
const { pendingSnapshotRequest, cellOutputSnapshots, notebookSnapshot } = state.cdb;
return { pendingSnapshotRequest, cellOutputSnapshots, notebookSnapshot, nbCodeCells };
};
return mapStateToProps;
};
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) =>
dispatch(
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
})
),
storeNotebookSnapshot: (imageSrc: string, requestId: string) =>
dispatch(cdbActions.storeNotebookSnapshot({ imageSrc, requestId })),
notebookSnapshotError: (error: string) => dispatch(cdbActions.notebookSnapshotError({ error })),
);
},
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
return dispatch(
cdbActions.UpdateNotebookParentDomElt({
contentRef,
parentElt,
})
);
},
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(BaseNotebookRenderer);
export default connect(null, makeMapDispatchToProps)(BaseNotebookRenderer);

View File

@@ -53,7 +53,7 @@ export class PromptPure extends React.Component<Props> {
}
}
const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => {
const makeMapStateToProps = (state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => {
const mapStateToProps = (state: CdbAppState) => {
const { contentRef, id } = ownProps;
const model = selectors.model(state, { contentRef });

View File

@@ -1,5 +1,5 @@
import { ContextualMenuItemType, DirectionalHint, IconButton, IContextualMenuItem } from "@fluentui/react";
import { CellId, CellType, ImmutableCodeCell } from "@nteract/commutable";
import { CellId, CellType } from "@nteract/commutable";
import { actions, AppState, DocumentRecordProps } from "@nteract/core";
import * as selectors from "@nteract/selectors";
import { CellToolbarContext } from "@nteract/stateful-components";
@@ -10,8 +10,6 @@ import { connect } from "react-redux";
import { Dispatch } from "redux";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as cdbActions from "../NotebookComponent/actions";
import { SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
export interface ComponentProps {
contentRef: ContentRef;
@@ -28,14 +26,12 @@ interface DispatchProps {
clearOutputs: () => void;
deleteCell: () => void;
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
}
interface StateProps {
cellType: CellType;
cellIdAbove: CellId;
cellIdBelow: CellId;
hasCodeOutput: boolean;
}
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
@@ -62,29 +58,11 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
this.props.traceNotebookTelemetry(Action.NotebooksClearOutputsFromMenu, ActionModifiers.Mark);
},
},
{
key: "Divider",
itemType: ContextualMenuItemType.Divider,
},
]);
if (this.props.hasCodeOutput) {
items.push({
key: "Export output to image",
text: "Export output to image",
onClick: () => {
this.props.takeNotebookSnapshot({
requestId: new Date().getTime().toString(),
aspectRatio: undefined,
type: "celloutput",
cellId: this.props.id,
notebookContentRef: this.props.contentRef,
downloadFilename: `celloutput-${this.props.contentRef}_${this.props.id}.png`,
});
},
});
}
items.push({
key: "Divider",
itemType: ContextualMenuItemType.Divider,
});
}
items = items.concat([
@@ -205,13 +183,12 @@ const mapDispatchToProps = (
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
});
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState) => {
const cell = selectors.cell.cellFromState(state, { id: ownProps.id, contentRef: ownProps.contentRef });
const cellType = cell.cell_type;
const cellType = selectors.cell.cellFromState(state, { id: ownProps.id, contentRef: ownProps.contentRef })
.cell_type;
const model = selectors.model(state, { contentRef: ownProps.contentRef });
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);
const cellIndex = cellOrder.indexOf(ownProps.id);
@@ -222,7 +199,6 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
cellType,
cellIdAbove,
cellIdBelow,
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
};
};
return mapStateToProps;

View File

@@ -1,9 +1,10 @@
import { AppState, ContentRef, DocumentRecordProps, selectors } from "@nteract/core";
import { RecordOf } from "immutable";
import React from "react";
import { connect } from "react-redux";
import "./CellLabeler.less";
import { AppState, ContentRef, selectors, DocumentRecordProps } from "@nteract/core";
import { RecordOf } from "immutable";
interface ComponentProps {
id: string;
contentRef: ContentRef; // TODO: Make this per contentRef?
@@ -28,7 +29,7 @@ class CellLabeler extends React.Component<ComponentProps & StateProps> {
}
}
const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState) => {
const model = selectors.model(state, { contentRef: ownProps.contentRef });
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);

View File

@@ -7,9 +7,7 @@ import postRobot from "post-robot";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { CellOutputViewerProps, SnapshotResponse } from "../../../../CellOutputViewer/CellOutputViewer";
import * as cdbActions from "../../NotebookComponent/actions";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../../NotebookComponent/types";
import { CellOutputViewerProps } from "../../../../CellOutputViewer/CellOutputViewer";
// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx
// to add support for sandboxing using <iframe>
@@ -26,47 +24,27 @@ interface StateProps {
expanded: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputs: Immutable.List<any>;
pendingSnapshotRequest: SnapshotRequest;
}
interface DispatchProps {
onMetadataChange?: (metadata: JSONObject, mediaType: string, index?: number) => void;
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
storeSnapshotFragment: (cellId: string, snapshotFragment: SnapshotFragment) => void;
notebookSnapshotError: (error: string) => void;
}
type SandboxOutputsProps = ComponentProps & StateProps & DispatchProps;
export class SandboxOutputs extends React.Component<SandboxOutputsProps> {
export class SandboxOutputs extends React.PureComponent<ComponentProps & StateProps & DispatchProps> {
private childWindow: Window;
private nodeRef = React.createRef<HTMLDivElement>();
constructor(props: SandboxOutputsProps) {
super(props);
this.state = {
processedSnapshotRequest: undefined,
};
}
render(): JSX.Element {
// Using min-width to set the width of the iFrame, works around an issue in iOS that can prevent the iFrame from sizing correctly.
return this.props.outputs && this.props.outputs.size > 0 ? (
<div ref={this.nodeRef}>
<IframeResizer
checkOrigin={false}
loading="lazy"
heightCalculationMethod="taggedElement"
onLoad={(event) => this.handleFrameLoad(event)}
src="./cellOutputViewer.html"
style={{ height: "1px", width: "1px", minWidth: "100%", border: "none" }}
sandbox="allow-downloads allow-popups allow-forms allow-pointer-lock allow-scripts allow-popups-to-escape-sandbox"
/>
</div>
) : (
<></>
return (
<IframeResizer
checkOrigin={false}
loading="lazy"
heightCalculationMethod="taggedElement"
onLoad={(event) => this.handleFrameLoad(event)}
src="./cellOutputViewer.html"
style={{ height: "1px", width: "1px", minWidth: "100%", border: "none" }}
sandbox="allow-downloads allow-popups allow-forms allow-pointer-lock allow-scripts allow-popups-to-escape-sandbox"
/>
);
}
@@ -98,48 +76,8 @@ export class SandboxOutputs extends React.Component<SandboxOutputsProps> {
this.sendPropsToFrame();
}
async componentDidUpdate(prevProps: SandboxOutputsProps): Promise<void> {
componentDidUpdate(): void {
this.sendPropsToFrame();
if (
this.props.pendingSnapshotRequest &&
prevProps.pendingSnapshotRequest !== this.props.pendingSnapshotRequest &&
this.props.pendingSnapshotRequest.notebookContentRef === this.props.contentRef &&
this.nodeRef?.current
) {
const boundingClientRect = this.nodeRef.current.getBoundingClientRect();
try {
const { data } = (await postRobot.send(
this.childWindow,
"snapshotRequest",
this.props.pendingSnapshotRequest
)) as { data: SnapshotResponse };
if (this.props.pendingSnapshotRequest.type === "notebook") {
if (data.imageSrc === undefined) {
this.props.storeSnapshotFragment(this.props.id, {
image: undefined,
boundingClientRect: boundingClientRect,
requestId: data.requestId,
});
return;
}
const image = new Image();
image.src = data.imageSrc;
image.onload = () => {
this.props.storeSnapshotFragment(this.props.id, {
image,
boundingClientRect: boundingClientRect,
requestId: data.requestId,
});
};
} else if (this.props.pendingSnapshotRequest.type === "celloutput") {
this.props.storeNotebookSnapshot(data.imageSrc, this.props.pendingSnapshotRequest.requestId);
}
} catch (error) {
this.props.notebookSnapshotError(error.message);
}
}
}
}
@@ -147,7 +85,7 @@ export const makeMapStateToProps = (
initialState: AppState,
ownProps: ComponentProps
): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: CdbAppState): StateProps => {
const mapStateToProps = (state: AppState): StateProps => {
let outputs = Immutable.List();
let hidden = false;
let expanded = false;
@@ -164,17 +102,7 @@ export const makeMapStateToProps = (
}
}
// Determine whether to take a snapshot or not
let pendingSnapshotRequest = state.cdb.pendingSnapshotRequest;
if (
pendingSnapshotRequest &&
pendingSnapshotRequest.type === "celloutput" &&
pendingSnapshotRequest.cellId !== id
) {
pendingSnapshotRequest = undefined;
}
return { outputs, hidden, expanded, pendingSnapshotRequest };
return { outputs, hidden, expanded };
};
return mapStateToProps;
};
@@ -197,11 +125,6 @@ export const makeMapDispatchToProps = (
})
);
},
storeSnapshotFragment: (cellId: string, snapshot: SnapshotFragment) =>
dispatch(cdbActions.storeCellOutputSnapshot({ cellId, snapshot })),
storeNotebookSnapshot: (imageSrc: string, requestId: string) =>
dispatch(cdbActions.storeNotebookSnapshot({ imageSrc, requestId })),
notebookSnapshotError: (error: string) => dispatch(cdbActions.notebookSnapshotError({ error })),
};
};
return mapDispatchToProps;

View File

@@ -1,15 +1,15 @@
import { NotebookUtil } from "./NotebookUtil";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import {
CodeCellParams,
ImmutableNotebook,
MediaBundle,
CodeCellParams,
MarkdownCellParams,
makeCodeCell,
makeMarkdownCell,
makeNotebookRecord,
MarkdownCellParams,
MediaBundle,
} from "@nteract/commutable";
import { List, Map } from "immutable";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookUtil } from "./NotebookUtil";
const fileName = "file";
const notebookName = "file.ipynb";
@@ -131,7 +131,7 @@ describe("NotebookUtil", () => {
describe("findFirstCodeCellWithDisplay", () => {
it("works for Notebook file", () => {
const notebookObject = notebookRecord as ImmutableNotebook;
expect(NotebookUtil.findCodeCellWithDisplay(notebookObject)[0]).toEqual("1");
expect(NotebookUtil.findFirstCodeCellWithDisplay(notebookObject)).toEqual(1);
});
});
});

View File

@@ -1,11 +1,8 @@
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas";
import path from "path";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as StringUtils from "../../Utils/StringUtils";
import { SnapshotFragment } from "./NotebookComponent/types";
import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import * as StringUtils from "../../Utils/StringUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
// Must match rx-jupyter' FileType
export type FileType = "directory" | "file" | "notebook";
@@ -144,175 +141,23 @@ export class NotebookUtil {
return `${basePath}${newName}`;
}
public static hasCodeCellOutput(cell: ImmutableCodeCell): boolean {
return !!cell?.outputs?.find(
(output) =>
output.output_type === "display_data" ||
output.output_type === "execute_result" ||
output.output_type === "stream"
);
}
/**
* Find code cells with display
* @param notebookObject
* @returns array of cell ids
*/
public static findCodeCellWithDisplay(notebookObject: ImmutableNotebook): string[] {
return notebookObject.cellOrder.reduce((accumulator: string[], cellId) => {
const cell = notebookObject.cellMap.get(cellId);
if (cell?.cell_type === "code") {
if (NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell)) {
accumulator.push(cellId);
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
let codeCellIndex = 0;
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
const cellId = notebookObject.cellOrder.get(i);
if (cellId) {
const cell = notebookObject.cellMap.get(cellId);
if (cell?.cell_type === "code") {
const displayOutput = (cell as ImmutableCodeCell)?.outputs?.find(
(output) => output.output_type === "display_data" || output.output_type === "execute_result"
);
if (displayOutput) {
return codeCellIndex;
}
codeCellIndex++;
}
}
return accumulator;
}, []);
}
public static takeScreenshotHtml2Canvas = (
target: HTMLElement,
aspectRatio: number,
subSnapshots: SnapshotFragment[],
downloadFilename?: string
): Promise<{ imageSrc: string | undefined }> => {
return new Promise(async (resolve, reject) => {
try {
// target.scrollIntoView();
const canvas = await Html2Canvas(target, {
useCORS: true,
allowTaint: true,
scale: 1,
logging: false,
});
//redraw canvas to fit aspect ratio
const originalImageData = canvas.toDataURL();
const width = parseInt(canvas.style.width.split("px")[0]);
if (aspectRatio) {
canvas.height = width * aspectRatio;
}
if (originalImageData === "data:,") {
// Empty output
resolve({ imageSrc: undefined });
return;
}
const context = canvas.getContext("2d");
const image = new Image();
image.src = originalImageData;
image.onload = () => {
if (!context) {
reject(new Error("No context to draw on"));
return;
}
context.drawImage(image, 0, 0);
// draw sub images
if (subSnapshots) {
const parentRect = target.getBoundingClientRect();
subSnapshots.forEach((snapshot) => {
if (snapshot.image) {
context.drawImage(
snapshot.image,
snapshot.boundingClientRect.x - parentRect.x,
snapshot.boundingClientRect.y - parentRect.y
);
}
});
}
resolve({ imageSrc: canvas.toDataURL() });
if (downloadFilename) {
NotebookUtil.downloadFile(
downloadFilename,
canvas.toDataURL("image/png").replace("image/png", "image/octet-stream")
);
}
};
} catch (error) {
return reject(error);
}
});
};
public static takeScreenshotDomToImage = (
target: HTMLElement,
aspectRatio: number,
subSnapshots: SnapshotFragment[],
downloadFilename?: string
): Promise<{ imageSrc?: string }> => {
return new Promise(async (resolve, reject) => {
// target.scrollIntoView();
try {
const filter = (node: Node): boolean => {
const excludedList = ["IMG", "CANVAS"];
return !excludedList.includes((node as HTMLElement).tagName);
};
const originalImageData = await domtoimage.toPng(target, { filter });
if (originalImageData === "data:,") {
// Empty output
resolve({});
return;
}
const baseImage = new Image();
baseImage.src = originalImageData;
baseImage.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = baseImage.width;
canvas.height = aspectRatio !== undefined ? baseImage.width * aspectRatio : baseImage.width;
const context = canvas.getContext("2d");
if (!context) {
reject(new Error("No Canvas to draw on"));
return;
}
// White background otherwise image is transparent
context.fillStyle = "white";
context.fillRect(0, 0, baseImage.width, baseImage.height);
context.drawImage(baseImage, 0, 0);
// draw sub images
if (subSnapshots) {
const parentRect = target.getBoundingClientRect();
subSnapshots.forEach((snapshot) => {
if (snapshot.image) {
context.drawImage(
snapshot.image,
snapshot.boundingClientRect.x - parentRect.x,
snapshot.boundingClientRect.y - parentRect.y
);
}
});
}
resolve({ imageSrc: canvas.toDataURL() });
if (downloadFilename) {
NotebookUtil.downloadFile(
downloadFilename,
canvas.toDataURL("image/png").replace("image/png", "image/octet-stream")
);
}
};
} catch (error) {
reject(error);
}
});
};
private static downloadFile(filename: string, content: string): void {
const link = document.createElement("a");
link.href = content;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
throw new Error("Output does not exist for any of the cells.");
}
}

View File

@@ -1,48 +0,0 @@
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import SchemaAnalyzer from "./SchemaAnalyzer";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzerUtils";
export class SchemaAnalyzerAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
this.getStore().dispatch(
actions.fetchContent({
filepath: SchemaAnalyzerNotebook.path,
params: {},
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<SchemaAnalyzer {...props} />;
</Provider>
);
}
}

View File

@@ -1,101 +0,0 @@
import {
DefaultButton,
Icon,
IRenderFunction,
ITextFieldProps,
PrimaryButton,
Stack,
TextField,
TooltipHost,
} from "@fluentui/react";
import * as React from "react";
type SchemaAnalyzerHeaderProps = {
isKernelIdle: boolean;
isKernelBusy: boolean;
onSampleSizeUpdated: (sampleSize: string) => void;
onAnalyzeButtonClick: (filter: string, sampleSize: string) => void;
};
export const DefaultFilter = "";
export const DefaultSampleSize = "1000";
const FilterPlaceholder = "{ field: 'value' }";
const SampleSizePlaceholder = "1000";
const MinSampleSize = 1;
const MaxSampleSize = 5000;
export const SchemaAnalyzerHeader = ({
isKernelIdle,
isKernelBusy,
onSampleSizeUpdated,
onAnalyzeButtonClick,
}: SchemaAnalyzerHeaderProps): JSX.Element => {
const [filter, setFilter] = React.useState<string>(DefaultFilter);
const [sampleSize, setSampleSize] = React.useState<string>(DefaultSampleSize);
return (
<Stack horizontal tokens={{ childrenGap: 10 }}>
<Stack.Item grow>
<TextField
value={filter}
onChange={(event, newValue) => setFilter(newValue)}
label="Filter"
placeholder={FilterPlaceholder}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item>
<TextField
value={sampleSize}
onChange={(event, newValue) => {
const num = Number(newValue);
if (!newValue || (num >= MinSampleSize && num <= MaxSampleSize)) {
setSampleSize(newValue);
onSampleSizeUpdated(newValue);
}
}}
label="Sample size"
onRenderLabel={onSampleSizeWrapDefaultLabelRenderer}
placeholder={SampleSizePlaceholder}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={() => {
const sampleSizeToUse = sampleSize || DefaultSampleSize;
setSampleSize(sampleSizeToUse);
onAnalyzeButtonClick(filter, sampleSizeToUse);
}}
disabled={!isKernelIdle}
styles={{ root: { width: 120 } }}
/>
</Stack.Item>
<Stack.Item align="end">
<DefaultButton
text="Reset"
disabled={!isKernelIdle}
onClick={() => {
setFilter(DefaultFilter);
setSampleSize(DefaultSampleSize);
}}
/>
</Stack.Item>
</Stack>
);
};
const onSampleSizeWrapDefaultLabelRenderer = (
props: ITextFieldProps,
defaultRender: IRenderFunction<ITextFieldProps>
): JSX.Element => {
return (
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<span>{defaultRender(props)}</span>
<TooltipHost content={`Number of documents to sample between ${MinSampleSize} and ${MaxSampleSize}`}>
<Icon iconName="Info" ariaLabel="Info" />
</TooltipHost>
</Stack>
);
};

View File

@@ -1,39 +0,0 @@
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import * as React from "react";
type SchemaAnalyzerSplashScreenProps = {
isKernelIdle: boolean;
isKernelBusy: boolean;
onAnalyzeButtonClick: () => void;
};
export const SchemaAnalyzerSplashScreen = ({
isKernelIdle,
isKernelBusy,
onAnalyzeButtonClick,
}: SchemaAnalyzerSplashScreenProps): JSX.Element => {
return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item>
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
</Stack.Item>
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={() => onAnalyzeButtonClick()}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</Stack>
);
};

View File

@@ -1,44 +0,0 @@
import { Notebook } from "@nteract/commutable";
import { IContent } from "@nteract/types";
import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
const notebookName = "schema-analyzer-component-notebook.ipynb";
const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName);
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
export const SchemaAnalyzerNotebook: IContent<"notebook"> = {
name: notebookName,
path: notebookPath,
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};

View File

@@ -1,4 +1,4 @@
.schemaAnalyzer {
.schemaAnalyzerComponent {
width: 100%;
height: 100%;
overflow-y: auto;

View File

@@ -1,26 +1,22 @@
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { ImmutableExecuteResult, ImmutableOutput } from "@nteract/commutable";
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "@fluentui/react";
import { ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import Immutable from "immutable";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import loadTransform from "../NotebookComponent/loadTransform";
import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs";
import "./SchemaAnalyzer.less";
import { DefaultFilter, DefaultSampleSize, SchemaAnalyzerHeader } from "./SchemaAnalyzerHeader";
import { SchemaAnalyzerSplashScreen } from "./SchemaAnalyzerSplashScreen";
import "./SchemaAnalyzerComponent.less";
interface SchemaAnalyzerPureProps {
interface SchemaAnalyzerComponentPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
interface SchemaAnalyzerDispatchProps {
interface SchemaAnalyzerComponentDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
@@ -28,23 +24,25 @@ interface SchemaAnalyzerDispatchProps {
type OutputType = "rich" | "json";
interface SchemaAnalyzerState {
interface SchemaAnalyzerComponentState {
outputType: OutputType;
filter?: string;
isFiltering: boolean;
sampleSize: string;
}
type SchemaAnalyzerProps = SchemaAnalyzerPureProps & StateProps & SchemaAnalyzerDispatchProps;
type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps &
StateProps &
SchemaAnalyzerComponentDispatchProps;
export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaAnalyzerState> {
private clickAnalyzeTelemetryStartKey: number;
constructor(props: SchemaAnalyzerProps) {
export class SchemaAnalyzerComponent extends React.Component<
SchemaAnalyzerComponentProps,
SchemaAnalyzerComponentState
> {
constructor(props: SchemaAnalyzerComponentProps) {
super(props);
this.state = {
outputType: "rich",
isFiltering: false,
sampleSize: DefaultSampleSize,
};
}
@@ -52,59 +50,34 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
loadTransform(this.props);
}
private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => {
private onFilterTextFieldChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
filter: newValue,
});
};
private onAnalyzeButtonClick = () => {
const query = {
command: "listSchema",
database: this.props.databaseId,
collection: this.props.collectionId,
outputType: this.state.outputType,
filter,
sampleSize,
filter: this.state.filter,
};
this.setState({
isFiltering: true,
});
if (this.state.filter) {
this.setState({
isFiltering: true,
});
}
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
this.clickAnalyzeTelemetryStartKey = traceStart(Action.SchemaAnalyzerClickAnalyze, {
database: this.props.databaseId,
collection: this.props.collectionId,
sampleSize,
});
this.props.runCell(this.props.contentRef, this.props.firstCellId);
};
private traceClickAnalyzeComplete = (kernelStatus: string, outputs: Immutable.List<ImmutableOutput>) => {
/**
* CosmosMongoKernel always returns 1st output as "text/html"
* This output can be an error stack or information about how many documents were sampled
*/
let firstTextHtmlOutput: string;
if (outputs.size > 0 && outputs.get(0).output_type === "execute_result") {
const executeResult = outputs.get(0) as ImmutableExecuteResult;
firstTextHtmlOutput = executeResult.data["text/html"];
}
const data = {
database: this.props.databaseId,
collection: this.props.collectionId,
firstTextHtmlOutput,
sampleSize: this.state.sampleSize,
numOfOutputs: outputs.size,
kernelStatus,
};
// Only in cases where CosmosMongoKernel runs into an error we get a single output
if (outputs.size === 1) {
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
} else {
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
}
};
render(): JSX.Element {
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
if (!id) {
@@ -113,22 +86,31 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle";
const showSchemaOutput = isKernelIdle && outputs?.size > 0;
if (showSchemaOutput && this.clickAnalyzeTelemetryStartKey) {
this.traceClickAnalyzeComplete(kernelStatus, outputs);
this.clickAnalyzeTelemetryStartKey = undefined;
}
const showSchemaOutput = isKernelIdle && outputs.size > 0;
return (
<div className="schemaAnalyzer">
<Stack tokens={{ childrenGap: 20, padding: 20 }}>
<SchemaAnalyzerHeader
isKernelIdle={isKernelIdle}
isKernelBusy={isKernelBusy}
onSampleSizeUpdated={(sampleSize) => this.setState({ sampleSize })}
onAnalyzeButtonClick={this.onAnalyzeButtonClick}
/>
<div className="schemaAnalyzerComponent">
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item grow styles={{ root: { display: "contents" } }}>
<Stack horizontal tokens={{ childrenGap: 20 }} styles={{ root: { width: "100%" } }}>
<Stack.Item grow align="end">
<TextField
value={this.state.filter}
onChange={this.onFilterTextFieldChange}
label="Filter"
placeholder="{ field: 'value' }"
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={this.onAnalyzeButtonClick}
disabled={!isKernelIdle}
/>
</Stack.Item>
</Stack>
</Stack.Item>
{showSchemaOutput ? (
<SandboxOutputs
@@ -138,13 +120,32 @@ export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaA
outputClassName="schema-analyzer-cell-output"
/>
) : this.state.isFiltering ? (
<Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />
<Stack.Item>
{isKernelBusy && <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />}
</Stack.Item>
) : (
<SchemaAnalyzerSplashScreen
isKernelIdle={isKernelIdle}
isKernelBusy={isKernelBusy}
onAnalyzeButtonClick={this.onAnalyzeButtonClick}
/>
<>
<Stack.Item>
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
</Stack.Item>
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={this.onAnalyzeButtonClick}
disabled={kernelStatus !== "idle"}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</>
)}
</Stack>
</div>
@@ -228,4 +229,4 @@ const makeMapDispatchToProps = () => {
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzer);
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent);

View File

@@ -0,0 +1,88 @@
import { Notebook } from "@nteract/commutable";
import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import SchemaAnalyzerComponent from "./SchemaAnalyzerComponent";
export class SchemaAnalyzerComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
const model: IContent<"notebook"> = {
name: "schema-analyzer-component-notebook.ipynb",
path: "schema-analyzer-component-notebook.ipynb",
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: model.path,
model,
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<SchemaAnalyzerComponent {...props} />;
</Provider>
);
}
}

View File

@@ -17,6 +17,7 @@ describe("OpenActions", () => {
explorer.onNewCollectionClicked = jest.fn();
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
explorer.cassandraAddCollectionPane.open = jest.fn();
explorer.closeAllPanes = () => {};
database = {
id: ko.observable("db"),

View File

@@ -140,16 +140,19 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind.AddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
) {
explorer.closeAllPanes();
explorer.onNewCollectionClicked();
} else if (
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
) {
explorer.closeAllPanes();
explorer.cassandraAddCollectionPane.open();
} else if (
action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) {
explorer.closeAllPanes();
explorer.openSettingPane();
}
}

View File

@@ -25,7 +25,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils";
import { isCapabilityEnabled } from "../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -58,18 +58,16 @@ export interface AddCollectionPanelState {
errorMessage: string;
showErrorDetails: boolean;
isExecuting: boolean;
newDatabaseThroughput: number;
isNewDatabaseAutoscale: boolean;
collectionThroughput: number;
isCollectionAutoscale: boolean;
isCostAcknowledged: boolean;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
private newDatabaseThroughput: number;
private isNewDatabaseAutoscale: boolean;
private collectionThroughput: number;
private isCollectionAutoscale: boolean;
private isCostAcknowledged: boolean;
constructor(props: AddCollectionPanelProps) {
super(props);
this.state = {
createNewDatabase: userContext.apiType !== "Tables" && !this.props.databaseId,
newDatabaseId: "",
@@ -88,6 +86,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
errorMessage: "",
showErrorDetails: false,
isExecuting: false,
newDatabaseThroughput: undefined,
isNewDatabaseAutoscale: true,
collectionThroughput: undefined,
isCollectionAutoscale: true,
isCostAcknowledged: false,
};
}
@@ -139,6 +142,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
id="databaseCreateNew"
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
@@ -152,6 +156,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={!this.state.createNewDatabase}
name="databaseType"
type="radio"
role="radio"
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
@@ -173,13 +178,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40}
className="panelTextField"
aria-label="New database id"
autoFocus
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ newDatabaseId: event.target.value })
}
/>
{!isServerlessAccount() && (
{!this.isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
@@ -204,16 +210,20 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{!isServerlessAccount() && this.state.isSharedThroughputChecked && (
{!this.isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={true}
isAutoscaleSelected={this.state.isNewDatabaseAutoscale}
throughput={this.state.newDatabaseThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
setThroughputValue={(throughput: number) => this.setState({ newDatabaseThroughput: throughput })}
setIsAutoscale={(isAutoscale: boolean) => this.setState({ isNewDatabaseAutoscale: isAutoscale })}
onCostAcknowledgeChange={(isAcknowledge: boolean) =>
this.setState({ isCostAcknowledged: isAcknowledge })
}
/>
)}
</Stack>
@@ -284,6 +294,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-label="Turn on indexing"
aria-checked={this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)}
/>
@@ -295,6 +306,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-label="Turn off indexing"
aria-checked={!this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)}
/>
@@ -337,6 +349,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={!this.state.isSharded}
name="unsharded"
type="radio"
role="radio"
id="unshardedOption"
tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)}
@@ -350,6 +363,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={this.state.isSharded}
name="sharded"
type="radio"
role="radio"
id="shardedOption"
tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)}
@@ -389,7 +403,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (
userContext.apiType !== "Mongo" &&
!this.state.partitionKey &&
this.state.partitionKey === "" &&
!event.target.value.startsWith("/")
) {
this.setState({ partitionKey: "/" + event.target.value });
@@ -401,7 +415,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
{!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
@@ -435,11 +449,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
}
isDatabase={false}
isAutoscaleSelected={this.state.isCollectionAutoscale}
throughput={this.state.collectionThroughput}
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
setThroughputValue={(throughput: number) => this.setState({ collectionThroughput: throughput })}
setIsAutoscale={(isAutoscale: boolean) => this.setState({ isCollectionAutoscale: isAutoscale })}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
this.isCostAcknowledged = isAcknowledged;
this.setState({ isCostAcknowledged: isAcknowledged });
}}
/>
)}
@@ -473,6 +489,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
autoFocus
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -586,6 +603,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="enableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
@@ -600,6 +618,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
role="radio"
id="disableAnalyticalStoreBtn"
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
@@ -741,8 +760,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return userContext.databaseAccount?.properties?.enableFreeTier;
}
private isServerlessAccount(): boolean {
return userContext.databaseAccount.properties?.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableServerless
);
}
private getSharedThroughputDefault(): boolean {
return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
return userContext.subscriptionType !== SubscriptionType.EA && !this.isServerlessAccount();
}
private getFreeTierIndexingText(): string {
@@ -780,7 +805,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) {
if (this.isServerlessAccount()) {
return false;
}
@@ -810,7 +835,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (isServerlessAccount()) {
if (this.isServerlessAccount()) {
return false;
}
@@ -875,9 +900,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
const errorMessage = this.isNewDatabaseAutoscale
const throughput = this.state.createNewDatabase
? this.state.newDatabaseThroughput
: this.state.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.state.isCostAcknowledged) {
const errorMessage = this.state.isNewDatabaseAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
this.setState({ errorMessage });
@@ -901,10 +928,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getAnalyticalStorageTtl(): number {
if (!this.isSynapseLinkEnabled()) {
return undefined;
}
if (!this.shouldShowAnalyticalStoreOptions()) {
return undefined;
}
@@ -964,8 +987,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
},
collection: {
id: this.state.collectionId,
throughput: this.collectionThroughput,
isAutoscale: this.isCollectionAutoscale,
throughput: this.state.collectionThroughput,
isAutoscale: this.state.isCollectionAutoscale,
partitionKey,
uniqueKeyPolicy,
collectionWithDedicatedThroughput: this.state.enableDedicatedThroughput,
@@ -983,16 +1006,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
let offerThroughput: number;
let autoPilotMaxThroughput: number;
if (this.state.createNewDatabase) {
if (this.isNewDatabaseAutoscale) {
autoPilotMaxThroughput = this.newDatabaseThroughput;
if (this.state.isNewDatabaseAutoscale) {
autoPilotMaxThroughput = this.state.newDatabaseThroughput;
} else {
offerThroughput = this.newDatabaseThroughput;
offerThroughput = this.state.newDatabaseThroughput;
}
} else if (!databaseLevelThroughput) {
if (this.isCollectionAutoscale) {
autoPilotMaxThroughput = this.collectionThroughput;
if (this.state.isCollectionAutoscale) {
autoPilotMaxThroughput = this.state.collectionThroughput;
} else {
offerThroughput = this.collectionThroughput;
offerThroughput = this.state.collectionThroughput;
}
}

View File

@@ -1,9 +1,10 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { Checkbox, Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { configContext, Platform } from "../../../ConfigContext";
import * as DataModels from "../../../Contracts/DataModels";
import { SubscriptionType } from "../../../Contracts/SubscriptionType";
import * as SharedConstants from "../../../Shared/Constants";
@@ -11,8 +12,7 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../../Utils/PricingUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
@@ -29,10 +29,17 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
closePanel,
openNotificationConsole,
}: AddDatabasePaneProps) => {
let throughput: number;
let isAutoscaleSelected: boolean;
let isCostAcknowledged: boolean;
const { subscriptionType } = userContext;
const getSharedThroughputDefault = !(subscriptionType === SubscriptionType.EA || container.isServerlessEnabled());
const _isAutoPilotSelectedAndWhatTier = (): DataModels.AutoPilotCreationSettings => {
if (isAutoPilotSelected && maxAutoPilotThroughputSet) {
return {
maxThroughput: maxAutoPilotThroughputSet * 1,
};
}
return undefined;
};
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
@@ -45,13 +52,61 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
subscriptionType !== SubscriptionType.EA && !isServerlessAccount()
);
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(getSharedThroughputDefault);
const [formErrorsDetails, setFormErrorsDetails] = useState<string>();
const [formErrors, setFormErrors] = useState<string>("");
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(container.isAutoscaleDefaultEnabled());
const throughputDefaults = container.collectionCreationDefaults.throughput;
const [throughput, setThroughput] = useState<number>(
isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared
);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
const canRequestSupport = () => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = throughput;
return offerThroughput <= 100000;
}
return false;
};
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const upsellMessage: string = PricingUtils.getUpsellMessage(
userContext.portalEnv,
isFreeTierAccount,
container.isFirstResourceCreated(),
false
);
const upsellAnchorUrl: string = isFreeTierAccount ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
const upsellAnchorText: string = isFreeTierAccount ? "Learn more" : "More details";
const maxAutoPilotThroughputSet = AutoPilotUtils.minAutoPilotThroughput;
const canConfigureThroughput = !container.isServerlessEnabled();
const showUpsellMessage = () => {
if (container.isServerlessEnabled()) {
return false;
}
if (isFreeTierAccount) {
return databaseCreateNewShared;
}
return true;
};
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
useEffect(() => {
setDatabaseCreateNewShared(getSharedThroughputDefault);
}, [subscriptionType]);
const addDatabasePaneMessage = {
database: {
@@ -71,7 +126,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
subscriptionType: SubscriptionType[subscriptionType],
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
throughput,
throughput: throughput,
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
@@ -84,9 +139,11 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
return;
}
const offerThroughput: number = _computeOfferThroughput();
const addDatabasePaneStartMessage = {
...addDatabasePaneMessage,
throughput,
offerThroughput,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
setFormErrors("");
@@ -96,18 +153,18 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
};
if (isAutoscaleSelected) {
createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.throughput;
if (isAutoPilotSelected) {
createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.offerThroughput;
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.throughput;
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then(
() => {
_onCreateDatabaseSuccess(throughput, startKey);
_onCreateDatabaseSuccess(offerThroughput, startKey);
},
(error: string) => {
_onCreateDatabaseFailure(error, throughput, startKey);
_onCreateDatabaseFailure(error, offerThroughput, startKey);
}
);
};
@@ -127,6 +184,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
setIsExecuting(false);
const errorMessage = getErrorMessage(error);
setFormErrors(errorMessage);
setFormErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = {
...addDatabasePaneMessage,
offerThroughput,
@@ -136,19 +194,48 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
};
const _getThroughput = (): number => {
return isNaN(throughput) ? 0 : Number(throughput);
};
const _computeOfferThroughput = (): number => {
if (!canConfigureThroughput) {
return undefined;
}
return _getThroughput();
};
const _isValid = (): boolean => {
// TODO add feature flag that disables validation for customers with custom accounts
if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
if (isAutoPilotSelected) {
const autoPilot = _isAutoPilotSelectedAndWhatTier();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
}
const throughput = _getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
setFormErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
const autoscaleThroughput = maxAutoPilotThroughputSet * 1;
if (
isAutoPilotSelected &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!throughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
@@ -163,8 +250,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
);
const props: RightPaneFormProps = {
expandConsole: openNotificationConsole,
expandConsole: container.expandConsole,
formError: formErrors,
formErrorDetail: formErrorsDetails,
isExecuting,
submitButtonText: "OK",
onSubmit,
@@ -172,65 +260,81 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
return (
<RightPaneForm {...props}>
{!formErrors && isFreeTierAccount && (
<PanelInfoErrorComponent
message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)}
messageType="info"
showErrorDetails={false}
openNotificationConsole={openNotificationConsole}
link={Constants.Urls.freeTierInformation}
linkText="Learn more"
/>
)}
<div className="panelMainContent">
<div>
<Stack horizontal>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</Stack>
<TextField
id="database-id"
type="text"
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}
value={databaseId}
onChange={handleonChangeDBId}
style={{ fontSize: 12 }}
styles={{ root: { width: 300 } }}
<div className="paneContentContainer" role="dialog" aria-labelledby="databaseTitle">
{showUpsellMessage && formErrors === "" && (
<PanelInfoErrorComponent
message={upsellMessage}
messageType="info"
showErrorDetails={false}
openNotificationConsole={openNotificationConsole}
link={upsellAnchorUrl}
linkText={upsellAnchorText}
/>
)}
<div className="paneMainContent">
<div>
<p>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</p>
<Stack horizontal>
<Checkbox
title="Provision shared throughput"
styles={{
text: { fontSize: 12 },
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
<TextField
id="database-id"
type="text"
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}
value={databaseId}
onChange={handleonChangeDBId}
autoFocus
/>
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</Stack>
{!isServerlessAccount() && databaseCreateNewShared && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
<div
className="databaseProvision"
aria-label="New database provision support"
style={{ display: "block ruby" }}
>
<Checkbox
title="Provision shared throughput"
styles={{
checkbox: { width: 12, height: 12 },
label: { padding: 0, alignItems: "center" },
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>{" "}
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</div>
{databaseCreateNewShared && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isAutoscaleSelected={isAutoPilotSelected}
throughput={throughput}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)}
/>
{canRequestSupport() && (
<p>
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">
Contact support{" "}
</a>
for more than <span>{throughputDefaults.unlimitedmax?.toLocaleString()} </span> RU/s.
</p>
)}
</div>
)}
</div>
</div>
</div>
</RightPaneForm>

View File

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

View File

@@ -120,6 +120,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField
id="confirmCollectionId"
autoFocus
value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {

View File

@@ -33,7 +33,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<div
className="contextual-pane-out"
onClick={[Function]}
onKeyDown={[Function]}
/>
<div
className="contextual-pane"
@@ -1166,9 +1165,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
className="errorLink"
hidden={true}
onClick={[Function]}
onKeyDown={[Function]}
role="link"
tabIndex={0}
>
More details
</a>
@@ -1201,6 +1198,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</span>
</Text>
<StyledTextFieldBase
autoFocus={true}
id="confirmCollectionId"
onChange={[Function]}
styles={
@@ -1213,6 +1211,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
value=""
>
<TextFieldBase
autoFocus={true}
deferredValidationTime={200}
id="confirmCollectionId"
onChange={[Function]}
@@ -1505,6 +1504,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<input
aria-invalid={false}
autoFocus={true}
className="ms-TextField-field field-115"
id="confirmCollectionId"
onBlur={[Function]}
@@ -3664,7 +3664,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
hidden={true}
>
<img
alt="loader"
className="dataExplorerLoader"
src=""
/>

View File

@@ -1,5 +1,5 @@
import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
@@ -134,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
<Text variant="small">Confirm by typing the database id</Text>
<TextField
id="confirmDatabaseId"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {
setDatabaseInput(newInput);

View File

@@ -63,6 +63,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<TextField
label={inputLabel && inputLabel}
id="confirmCollectionId"
autoFocus
value={paramValue}
onChange={onParamValueChange}
/>

View File

@@ -24,7 +24,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<div
className="contextual-pane-out"
onClick={[Function]}
onKeyDown={[Function]}
/>
<div
className="contextual-pane"
@@ -1157,9 +1156,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="errorLink"
hidden={true}
onClick={[Function]}
onKeyDown={[Function]}
role="link"
tabIndex={0}
>
More details
</a>
@@ -2712,12 +2709,14 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</DropdownBase>
</Dropdown>
<StyledTextFieldBase
autoFocus={true}
id="confirmCollectionId"
key=".0:$.1"
label="Value"
onChange={[Function]}
>
<TextFieldBase
autoFocus={true}
deferredValidationTime={200}
id="confirmCollectionId"
label="Value"
@@ -3303,6 +3302,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<input
aria-invalid={false}
aria-labelledby="TextFieldLabel6"
autoFocus={true}
className="ms-TextField-field field-85"
id="confirmCollectionId"
onBlur={[Function]}
@@ -4864,6 +4864,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
</DropdownBase>
</Dropdown>
<StyledTextFieldBase
autoFocus={true}
id="confirmCollectionId"
key=".0:$.1"
label="Param"
@@ -4871,6 +4872,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
value=""
>
<TextFieldBase
autoFocus={true}
deferredValidationTime={200}
id="confirmCollectionId"
label="Param"
@@ -5457,6 +5459,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
<input
aria-invalid={false}
aria-labelledby="TextFieldLabel10"
autoFocus={true}
className="ms-TextField-field field-85"
id="confirmCollectionId"
onBlur={[Function]}
@@ -8224,7 +8227,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
hidden={true}
>
<img
alt="loader"
className="dataExplorerLoader"
src=""
/>

View File

@@ -1,4 +1,3 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { IconButton, PrimaryButton } from "@fluentui/react";
import React, { FunctionComponent, ReactNode } from "react";
import ErrorRedIcon from "../../../../images/error_red.svg";
@@ -68,14 +67,7 @@ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps>
<span className="formErrors" title={formError}>
{formError}
</span>
<a
className="errorLink"
role="link"
hidden={!formErrorDetail}
onClick={expandConsole}
tabIndex={0}
onKeyDown={expandConsole}
>
<a className="errorLink" role="link" hidden={!formErrorDetail} onClick={expandConsole}>
More details
</a>
</span>
@@ -105,7 +97,7 @@ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps>
const renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} alt="loader" />
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
@@ -119,7 +111,7 @@ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps>
return (
<div tabIndex={-1} onKeyDown={onKeyDown}>
<div className="contextual-pane-out" onClick={onClose} onKeyDown={onClose}></div>
<div className="contextual-pane-out" onClick={onClose}></div>
<div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
<div className="panelContentWrapper">
{renderPanelHeader()}

View File

@@ -21,6 +21,115 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionText": [Function],
@@ -906,11 +1015,32 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
},
},
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
@@ -977,6 +1107,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"activeTab": [Function],
"openedTabs": [Function],
},
"toggleLeftPaneExpandedKeyPress": [Function],
},
"getRepo": [Function],
"pinRepo": [Function],

View File

@@ -0,0 +1,59 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" data-bind="attr: { id: id }">
<!-- Graph Styling form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Graph Styling header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2">Graph Styling</span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Graph Styling header - End -->
<!-- Graph Styling errors - Start -->
<div
aria-live="assertive"
class="warningErrorContainer"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '' , click: showErrorDetails"
>
More details
</a>
</span>
</div>
</div>
<!-- Graph Styling errors - End -->
<!-- Add graph configuration - Start -->
<div class="paneMainContent">
<graph-style
id="graphStyleComponent"
params="{ config:graphConfigUIData, firstFieldHasFocus: firstFieldHasFocus }"
></graph-style>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
<!-- Add Graph configuration - End -->
</form>
</div>
<!-- Graph Styling form - End -->
</div>
</div>

View File

@@ -0,0 +1,68 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class GraphStylingPane extends ContextualPaneBase {
public graphConfigUIData: ViewModels.GraphConfigUiData;
private remoteConfig: ViewModels.GraphConfigUiData;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.graphConfigUIData = {
showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY),
nodeProperties: ko.observableArray([]),
nodePropertiesWithNone: ko.observableArray([]),
nodeCaptionChoice: ko.observable(null),
nodeColorKeyChoice: ko.observable(null),
nodeIconChoice: ko.observable(null),
nodeIconSet: ko.observable(null),
};
this.graphConfigUIData.nodeCaptionChoice.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeCaptionChoice(val);
}
});
this.graphConfigUIData.nodeColorKeyChoice.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeColorKeyChoice(val);
}
});
this.graphConfigUIData.nodeIconChoice.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeIconChoice(val);
}
});
this.graphConfigUIData.nodeIconSet.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeIconSet(val);
}
});
this.graphConfigUIData.showNeighborType.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.showNeighborType(val);
}
});
}
public setData(config: ViewModels.GraphConfigUiData): void {
// Update pane ko's with config's ko
this.graphConfigUIData.nodeIconChoice(config.nodeIconChoice());
this.graphConfigUIData.nodeIconSet(config.nodeIconSet());
this.graphConfigUIData.nodeProperties(config.nodeProperties());
this.graphConfigUIData.nodePropertiesWithNone(config.nodePropertiesWithNone());
this.graphConfigUIData.showNeighborType(config.showNeighborType());
// Make sure these two happen *after* setting the options of the dropdown,
// otherwise, the ko will not get set if the choice is not part of the options
this.graphConfigUIData.nodeCaptionChoice(config.nodeCaptionChoice());
this.graphConfigUIData.nodeColorKeyChoice(config.nodeColorKeyChoice());
this.remoteConfig = config;
}
public close() {
this.remoteConfig = null;
super.close();
}
}

View File

@@ -1,37 +0,0 @@
import React, { FunctionComponent } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { PanelFooterComponent } from "../PanelFooterComponent";
interface GraphStylingProps {
closePanel: () => void;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
getValues: (igraphConfig?: IGraphConfig) => void;
}
export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
closePanel,
igraphConfigUiData,
igraphConfig,
getValues,
}: GraphStylingProps): JSX.Element => {
const buttonLabel = "Ok";
const submit = () => {
closePanel();
};
return (
<form className="panelFormWrapper" onSubmit={submit}>
<div className="panelMainContent">
<GraphStyleComponent
igraphConfigUiData={igraphConfigUiData}
igraphConfig={igraphConfig}
getValues={getValues}
></GraphStyleComponent>
</div>
<PanelFooterComponent buttonLabel={buttonLabel} />
</form>
);
};

View File

@@ -1,5 +1,5 @@
import { IImageProps, Image, ImageFit, Stack, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { IImageProps, Image, ImageFit, Stack, TextField } from "@fluentui/react";
import React, { FunctionComponent, useState } from "react";
import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger";
@@ -115,6 +115,7 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
id="confirmCollectionId"
label="Select a query document"
value={selectedFileName}
autoFocus
readOnly
styles={{ fieldGroup: { width: 300 } }}
/>

View File

@@ -22,6 +22,7 @@ exports[`Load Query Pane should render Default properly 1`] = `
horizontal={true}
>
<StyledTextFieldBase
autoFocus={true}
id="confirmCollectionId"
label="Select a query document"
readOnly={true}

View File

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

View File

@@ -89,7 +89,6 @@
.paneErrorLink {
cursor: pointer;
font-size: @mediumFontSize;
color: @AccentMediumHigh;
}
}
}

View File

@@ -1,5 +1,5 @@
import { shallow } from "enzyme";
import React from "react";
import { shallow } from "enzyme";
import { PanelContainerComponent, PanelContainerProps } from "./PanelContainerComponent";
describe("PaneContainerComponent test", () => {

View File

@@ -8,6 +8,7 @@ export interface PanelInfoErrorProps {
link?: string;
linkText?: string;
openNotificationConsole?: () => void;
formError?: boolean;
}
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({
@@ -17,8 +18,9 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
link,
linkText,
openNotificationConsole,
formError = true,
}: PanelInfoErrorProps): JSX.Element => {
let icon: JSX.Element = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
let icon: JSX.Element;
if (messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
} else if (messageType === "warning") {
@@ -28,29 +30,25 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
}
return (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
{message}
{link && linkText && (
<Link target="_blank" href={link}>
{linkText}
</Link>
formError && (
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
{icon}
<span className="panelWarningErrorDetailsLinkContainer">
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
{message}
{link && linkText && (
<Link target="_blank" href={link}>
{linkText}
</Link>
)}
</Text>
{showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={openNotificationConsole}>
More details
</a>
)}
</Text>
{showErrorDetails && (
<a
className="paneErrorLink"
role="link"
onClick={openNotificationConsole}
tabIndex={0}
onKeyDown={openNotificationConsole}
>
More details
</a>
)}
</span>
</Stack>
</span>
</Stack>
)
);
};

View File

@@ -3,6 +3,6 @@ import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares
export const PanelLoadingScreen: React.FunctionComponent = () => (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer">
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} alt="Loader" />
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
</div>
);

View File

@@ -12,14 +12,13 @@ describe("PublishNotebookPaneComponent", () => {
notebookAuthor: "CosmosDB",
notebookCreatedDate: "2020-07-17T00:00:00Z",
notebookObject: undefined,
notebookContentRef: undefined,
notebookParentDomElement: undefined,
setNotebookName: undefined,
setNotebookDescription: undefined,
setNotebookTags: undefined,
setImageSrc: undefined,
onError: undefined,
clearFormError: undefined,
onTakeSnapshot: undefined,
};
const wrapper = shallow(<PublishNotebookPaneComponent {...props} />);

View File

@@ -2,7 +2,6 @@ import { ImmutableNotebook, toJS } from "@nteract/commutable";
import React, { FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { useNotebookSnapshotStore } from "../../../hooks/useNotebookSnapshotStore";
import { JunoClient } from "../../../Juno/JunoClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
@@ -11,7 +10,6 @@ import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfCon
import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
@@ -26,8 +24,7 @@ export interface PublishNotebookPaneAProps {
name: string;
author: string;
notebookContent: string | ImmutableNotebook;
notebookContentRef: string;
onTakeSnapshot: (request: SnapshotRequest) => void;
parentDomElement: HTMLElement;
}
export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> = ({
explorer: container,
@@ -36,8 +33,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
name,
author,
notebookContent,
notebookContentRef,
onTakeSnapshot,
parentDomElement,
}: PublishNotebookPaneAProps): JSX.Element => {
const [isCodeOfConductAccepted, setIsCodeOfConductAccepted] = useState<boolean>(false);
const [content, setContent] = useState<string>("");
@@ -49,7 +45,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
const [notebookDescription, setNotebookDescription] = useState<string>("");
const [notebookTags, setNotebookTags] = useState<string>("");
const [imageSrc, setImageSrc] = useState<string>();
const { snapshot: notebookSnapshot, error: notebookSnapshotError } = useNotebookSnapshotStore();
const CodeOfConductAccepted = async () => {
try {
@@ -79,14 +74,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
setContent(newContent);
}, []);
useEffect(() => {
setImageSrc(notebookSnapshot);
}, [notebookSnapshot]);
useEffect(() => {
setFormError(notebookSnapshotError);
}, [notebookSnapshotError]);
const submit = async (): Promise<void> => {
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${name} to gallery`);
setIsExecuting(true);
@@ -191,14 +178,13 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
notebookAuthor: author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: notebookObject,
notebookContentRef,
notebookParentDomElement: parentDomElement,
onError: createFormError,
clearFormError: clearFormError,
setNotebookName,
setNotebookDescription,
setNotebookTags,
setImageSrc,
onTakeSnapshot,
};
return (
<GenericRightPaneComponent {...props}>

View File

@@ -1,9 +1,9 @@
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "@fluentui/react";
import { ImmutableNotebook } from "@nteract/commutable";
import Html2Canvas from "html2canvas";
import React, { FunctionComponent, useState } from "react";
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import "./styled.less";
@@ -11,19 +11,17 @@ export interface PublishNotebookPaneProps {
notebookName: string;
notebookAuthor: string;
notebookTags: string;
imageSrc: string;
notebookDescription: string;
notebookCreatedDate: string;
notebookObject: ImmutableNotebook;
notebookContentRef: string;
imageSrc: string;
notebookParentDomElement?: HTMLElement;
onError: (formError: string, formErrorDetail: string, area: string) => void;
clearFormError: () => void;
setNotebookName: (newValue: string) => void;
setNotebookDescription: (newValue: string) => void;
setNotebookTags: (newValue: string) => void;
setImageSrc: (newValue: string) => void;
onTakeSnapshot: (request: SnapshotRequest) => void;
}
enum ImageTypes {
@@ -36,19 +34,18 @@ enum ImageTypes {
export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPaneProps> = ({
notebookName,
notebookTags,
imageSrc,
notebookDescription,
notebookAuthor,
notebookCreatedDate,
notebookObject,
notebookContentRef,
imageSrc,
notebookParentDomElement,
onError,
clearFormError,
setNotebookName,
setNotebookDescription,
setNotebookTags,
setImageSrc,
onTakeSnapshot,
}: PublishNotebookPaneProps) => {
const [type, setType] = useState<string>(ImageTypes.CustomImage);
const CARD_WIDTH = 256;
@@ -66,40 +63,25 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
)}" to the gallery?`;
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
if (onTakeSnapshot) {
options.push(ImageTypes.TakeScreenshot);
if (notebookObject) {
options.push(ImageTypes.UseFirstDisplayOutput);
}
}
const thumbnailSelectorProps: IDropdownProps = {
label: "Cover image",
selectedKey: type,
defaultSelectedKey: ImageTypes.CustomImage,
ariaLabel: "Cover image",
options: options.map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
setImageSrc("");
clearFormError();
if (options.text === ImageTypes.TakeScreenshot) {
onTakeSnapshot({
aspectRatio: cardHeightToWidthRatio,
requestId: new Date().getTime().toString(),
type: "notebook",
notebookContentRef,
});
try {
await takeScreenshot(notebookParentDomElement, screenshotErrorHandler);
} catch (error) {
screenshotErrorHandler(error);
}
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
const cellIds = NotebookUtil.findCodeCellWithDisplay(notebookObject);
if (cellIds.length > 0) {
onTakeSnapshot({
aspectRatio: cardHeightToWidthRatio,
requestId: new Date().getTime().toString(),
type: "celloutput",
cellId: cellIds[0],
notebookContentRef,
});
} else {
firstOutputErrorHandler(new Error("Output does not exist for any of the cells."));
try {
await takeScreenshot(findFirstOutput(), firstOutputErrorHandler);
} catch (error) {
firstOutputErrorHandler(error);
}
}
setType(options.text);
@@ -115,6 +97,13 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
},
};
const screenshotErrorHandler = (error: Error) => {
const formError = "Failed to take screen shot";
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/takeScreenshot";
onError(formError, formErrorDetail, area);
};
const firstOutputErrorHandler = (error: Error) => {
const formError = "Failed to capture first output";
const formErrorDetail = `${error}`;
@@ -122,6 +111,13 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
onError(formError, formErrorDetail, area);
};
if (notebookParentDomElement) {
options.push(ImageTypes.TakeScreenshot);
if (notebookObject) {
options.push(ImageTypes.UseFirstDisplayOutput);
}
}
const imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
const reader = new FileReader();
reader.readAsDataURL(file);
@@ -137,6 +133,36 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
};
};
const takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => {
const updateImageSrcWithScreenshot = (canvasUrl: string): void => {
setImageSrc(canvasUrl);
};
target.scrollIntoView();
Html2Canvas(target, {
useCORS: true,
allowTaint: true,
scale: 1,
logging: true,
})
.then((canvas) => {
//redraw canvas to fit Card Cover Image dimensions
const originalImageData = canvas.toDataURL();
const requiredHeight = parseInt(canvas.style.width.split("px")[0]) * cardHeightToWidthRatio;
canvas.height = requiredHeight;
const context = canvas.getContext("2d");
const image = new Image();
image.src = originalImageData;
image.onload = () => {
context.drawImage(image, 0, 0);
updateImageSrcWithScreenshot(canvas.toDataURL());
};
})
.catch((error) => {
onError(error);
});
};
const renderThumbnailSelectors = (type: string) => {
switch (type) {
case ImageTypes.Url:
@@ -172,6 +198,12 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
}
};
const findFirstOutput = (): HTMLElement => {
const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(notebookObject);
const cellOutputDomElements = notebookParentDomElement.querySelectorAll<HTMLElement>(".nteract-cell-outputs");
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
};
return (
<div className="publishNotebookPanelContent">
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>

View File

@@ -52,6 +52,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
<StackItem>
<Dropdown
ariaLabel="Cover image"
defaultSelectedKey="Custom Image"
label="Cover image"
onChange={[Function]}
options={
@@ -66,7 +67,6 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
},
]
}
selectedKey="Custom Image"
/>
</StackItem>
<StackItem>

View File

@@ -9,6 +9,7 @@ const expandConsole = jest.fn();
const props = {
expandConsole,
formError: "",
formErrorDetail: "",
isExecuting: false,
submitButtonText: "Load",
onSubmit,

View File

@@ -1,11 +1,12 @@
import React, { FunctionComponent, ReactNode } from "react";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
export interface RightPaneFormProps {
expandConsole: () => void;
formError: string;
formErrorDetail: string;
isExecuting: boolean;
onSubmit: () => void;
submitButtonText: string;
@@ -16,6 +17,7 @@ export interface RightPaneFormProps {
export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
expandConsole,
formError,
formErrorDetail,
isExecuting,
onSubmit,
submitButtonText,
@@ -27,17 +29,18 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
onSubmit();
};
const panelInfoErrorProps: PanelInfoErrorProps = {
messageType: "error",
message: formError,
formError: formError !== "",
showErrorDetails: formErrorDetail !== "",
openNotificationConsole: expandConsole,
};
return (
<>
<PanelInfoErrorComponent {...panelInfoErrorProps} />
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{formError && (
<PanelInfoErrorComponent
messageType="error"
message={formError}
showErrorDetails={true}
openNotificationConsole={expandConsole}
/>
)}
{children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
</form>

View File

@@ -4,10 +4,18 @@ exports[`Right Pane Form should render Default properly 1`] = `
<RightPaneForm
expandConsole={[MockFunction]}
formError=""
formErrorDetail=""
isExecuting={false}
onSubmit={[MockFunction]}
submitButtonText="Load"
>
<PanelInfoErrorComponent
formError={false}
message=""
messageType="error"
openNotificationConsole={[MockFunction]}
showErrorDetails={false}
/>
<form
className="panelFormWrapper"
onSubmit={[Function]}

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