Compare commits

..

29 Commits

Author SHA1 Message Date
sunilyadav840
88ef4ea9ed updated snapshot 2021-05-14 14:24:31 +05:30
sunilyadav840
6a28bd4898 Merge branch 'configure-eslint-plugin-jsx-a11y' of https://github.com/Azure/cosmos-explorer into configure-eslint-plugin-jsx-a11y 2021-05-14 13:25:43 +05:30
sunilyadav840
2a2f55ff28 Merge branch 'master' 2021-05-14 13:25:03 +05:30
Steve Faulkner
f9e8b5eaaa Remove Unused Knockout Components (#783) 2021-05-13 18:03:29 -05:00
vaidankarswapnil
a6b82c8340 Migrate graph style panel to react (#619)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
2021-05-13 15:45:00 -05:00
Tanuj Mittal
404b1fc0f1 Prep Schema Analyzer for flighting (#760)
* Prepare for flighting Schema Analyzer

* Rename SchemaAnalyzerComponent -> SchemaAnalyzer

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

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

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

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

View File

@@ -54,7 +54,6 @@ src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts
src/Explorer/Controls/InputTypeahead/InputTypeahead.ts
src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts
src/Explorer/Controls/Notebook/NotebookAppMessageHandler.ts
src/Explorer/Controls/ThroughputInput/ThroughputInput.test.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponent.ts
src/Explorer/Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3.ts
src/Explorer/Controls/Toolbar/IToolbarAction.ts
@@ -85,8 +84,8 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
src/Explorer/Graph/GraphExplorerComponent/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
@@ -109,16 +108,15 @@ src/Explorer/Notebook/NotebookUtil.ts
src/Explorer/OpenActions.test.ts
src/Explorer/OpenActions.ts
src/Explorer/OpenActionsStubs.ts
src/Explorer/Panes/AddCollectionPane.test.ts
src/Explorer/Panes/AddCollectionPane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/BrowseQueriesPane.ts
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/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.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"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
plugins: ["@typescript-eslint", "no-null", "prefer-arrow", "react-hooks", "jsx-a11y"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended"],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
@@ -34,6 +34,7 @@ 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,6 +141,7 @@ 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: url("../../fonts/segoe-ui/west-european/normal/latest.woff");
src: local("Segoe UI"), url("../../fonts/segoe-ui/west-european/normal/latest.woff");
}
@DataExplorerFont: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;

View File

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

View File

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

5040
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"@azure/ms-rest-nodeauth": "3.0.7",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.10.1",
"@fluentui/react": "8.14.3",
"@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1",
@@ -43,7 +43,6 @@
"@testing-library/jest-dom": "5.11.9",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
@@ -57,6 +56,7 @@
"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,18 +98,20 @@
"swr": "0.4.0",
"terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1",
"utility-types": "3.10.0"
"utility-types": "3.10.0",
"zustand": "3.5.0"
},
"devDependencies": {
"@babel/core": "7.14.0",
"@babel/preset-env": "7.14.1",
"@babel/preset-react": "7.13.13",
"@babel/preset-typescript": "7.13.0",
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.9.0",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@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",
@@ -127,10 +129,10 @@
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36",
"@typescript-eslint/eslint-plugin": "4.22.1",
"@typescript-eslint/parser": "4.22.1",
"babel-jest": "26.6.3",
"babel-loader": "8.2.2",
"@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0",
"babel-jest": "24.9.0",
"babel-loader": "8.1.0",
"buffer": "5.1.0",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"create-file-webpack": "1.0.2",
@@ -138,8 +140,9 @@
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.5",
"enzyme-to-json": "3.6.1",
"eslint": "7.25.0",
"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",
@@ -152,7 +155,7 @@
"html-loader": "0.5.5",
"html-loader-jest": "0.2.1",
"html-webpack-plugin": "4.5.2",
"jest": "26.6.3",
"jest": "25.5.4",
"jest-canvas-mock": "2.1.0",
"jest-playwright-preset": "1.5.1",
"jest-trx-results-processor": "0.0.7",
@@ -169,10 +172,10 @@
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "9.1.2",
"tslint": "6.1.3",
"tslint-microsoft-contrib": "6.2.0",
"typescript": "4.3.0-beta",
"ts-loader": "6.2.2",
"tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0",
"typescript": "4.2.4",
"url-loader": "1.1.1",
"wait-on": "4.0.2",
"webpack": "4.46.0",

View File

@@ -1,10 +1,15 @@
.schema-analyzer-cell-outputs {
padding: 10px;
padding: 10px 2px;
}
// Mimic FluentUI8's DocumentCard style
.schema-analyzer-cell-output {
margin-bottom: 20px;
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;
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)
}

View File

@@ -9,11 +9,17 @@ 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;
@@ -62,6 +68,36 @@ 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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { Image, Stack, TextField } from "@fluentui/react";
import React, { ChangeEvent, FunctionComponent, KeyboardEvent, useRef, useState } from "react";
import FolderIcon from "../../../images/folder_16x16.svg";
import * as Constants from "../Constants";
import { Tooltip } from "../Tooltip/Tooltip";
import { InfoTooltip } from "../Tooltip/InfoTooltip";
interface UploadProps {
label: string;
@@ -47,11 +47,11 @@ export const Upload: FunctionComponent<UploadProps> = ({
props.onUpload(event);
}
};
const title = label + " to upload";
return (
<div>
<span className="renewUploadItemsHeader">{label}</span>
<Tooltip>{tooltip}</Tooltip>
{tooltip && <InfoTooltip>{tooltip}</InfoTooltip>}
<Stack horizontal>
<TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} />
<input
@@ -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={title} title={title} />
<Image className="fileImportImg" src={FolderIcon} alt={label} title={label} />
</a>
</Stack>
</div>

View File

@@ -206,17 +206,14 @@ export enum NeighborType {
BOTH,
}
/**
* 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>;
export interface IGraphConfigUiData {
showNeighborType: NeighborType;
nodeProperties: string[];
nodePropertiesWithNone: string[];
nodeCaptionChoice: string;
nodeColorKeyChoice: string;
nodeIconChoice: string;
nodeIconSet: string;
}
/**

View File

@@ -4,30 +4,10 @@ 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 registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
});
it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});
it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
});

View File

@@ -2,16 +2,10 @@ import * as ko from "knockout";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { 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());
@@ -19,7 +13,6 @@ ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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";
@@ -30,7 +30,7 @@ export class GalleryHeaderComponent extends React.Component {
);
};
public override render(): JSX.Element {
public render(): JSX.Element {
return (
<Stack
tokens={{ childrenGap: 10 }}
@@ -61,7 +61,7 @@ export class GalleryHeaderComponent extends React.Component {
<Stack.Item>
{this.renderHeaderItem(
GalleryHeaderComponent.galleryText,
undefined,
() => "",
GalleryHeaderComponent.headerItemTextProps
)}
</Stack.Item>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,11 +20,12 @@ export interface RadioSwitchComponentProps {
}
export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
public override render(): JSX.Element {
public render(): JSX.Element {
return (
<div className="radioSwitchComponent">
{this.props.choices.map((choice: Choice) => (
<span
role="button"
tabIndex={0}
key={choice.key}
onClick={() => this.onSelect(choice)}

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ export class IndexingPolicyComponent extends React.Component<
};
}
public override componentDidUpdate(): void {
componentDidUpdate(): void {
if (this.props.shouldDiscardIndexingPolicy) {
this.resetIndexingPolicyEditor();
this.props.resetShouldDiscardIndexingPolicy();
@@ -46,7 +46,7 @@ public override componentDidUpdate(): void {
this.onComponentUpdate();
}
public override componentDidMount(): void {
componentDidMount(): void {
this.resetIndexingPolicyEditor();
this.onComponentUpdate();
}
@@ -112,7 +112,7 @@ public override componentDidMount(): void {
}
};
public override render(): JSX.Element {
public render(): JSX.Element {
return (
<Stack {...titleAndInputStackProps}>
<IndexingPolicyRefreshComponent
@@ -122,7 +122,7 @@ public override componentDidMount(): void {
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
<div className="settingsV2IndexingPolicyEditor" role="button" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -31,9 +31,10 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName } from "../Utils/APITypeUtils";
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";
@@ -45,13 +46,14 @@ 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";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import AddDatabasePane from "./Panes/AddDatabasePane";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
@@ -59,7 +61,6 @@ 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";
@@ -91,7 +92,7 @@ export interface ExplorerParams {
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
setNotificationConsoleData: (consoleData: ConsoleData) => void;
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => void;
closeSidePanel: () => void;
closeDialog: () => void;
openDialog: (props: DialogProps) => void;
@@ -125,14 +126,13 @@ export default class Explorer {
// Panes
public contextPanes: ContextualPaneBase[];
public openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
public openSidePanel: (headerText: string, panelContent: JSX.Element, onClose?: () => void) => 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,8 +150,6 @@ export default class Explorer {
// Contextual panes
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient;
public gitHubOAuthService: GitHubOAuthService;
@@ -163,7 +161,6 @@ 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
@@ -183,7 +180,6 @@ 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;
@@ -230,6 +226,7 @@ export default class Explorer {
});
}
});
this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSparkEnabledForAccount = ko.observable(false);
@@ -334,7 +331,6 @@ 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
@@ -368,7 +364,7 @@ export default class Explorer {
return false;
}
return userContext.apiType === "Mongo";
return isCapabilityEnabled("EnableMongo");
});
this.isServerlessEnabled = ko.computed(
@@ -412,21 +408,6 @@ export default class Explorer {
container: this,
});
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => userContext.apiType === "Tables"),
id: "addcollectionpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false),
@@ -442,12 +423,6 @@ export default class Explorer {
}
});
this._panes = [
this.addDatabasePane,
this.addCollectionPane,
this.graphStylingPane,
this.cassandraAddCollectionPane,
];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false);
@@ -471,11 +446,6 @@ export default class Explorer {
this.collectionTreeNodeAltText("Container");
this.deleteCollectionText("Delete Container");
this.deleteDatabaseText("Delete Database");
this.addCollectionPane.title("Add Container");
this.addCollectionPane.collectionIdTitle("Container id");
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this container"
);
this.refreshTreeTitle("Refresh containers");
break;
case "Mongo":
@@ -485,11 +455,6 @@ export default class Explorer {
this.collectionTreeNodeAltText("Collection");
this.deleteCollectionText("Delete Collection");
this.deleteDatabaseText("Delete Database");
this.addCollectionPane.title("Add Collection");
this.addCollectionPane.collectionIdTitle("Collection id");
this.addCollectionPane.collectionWithThroughputInSharedTitle(
"Provision dedicated throughput for this collection"
);
this.refreshTreeTitle("Refresh collections");
break;
case "Gremlin":
@@ -499,9 +464,6 @@ export default class Explorer {
this.deleteDatabaseText("Delete Database");
this.collectionTitle("Gremlin API");
this.collectionTreeNodeAltText("Graph");
this.addCollectionPane.title("Add Graph");
this.addCollectionPane.collectionIdTitle("Graph id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this graph");
this.refreshTreeTitle("Refresh graphs");
break;
case "Tables":
@@ -511,9 +473,6 @@ export default class Explorer {
this.deleteDatabaseText("Delete Database");
this.collectionTitle("Azure Table API");
this.collectionTreeNodeAltText("Table");
this.addCollectionPane.title("Add Table");
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables");
this.tableDataClient = new TablesAPIDataClient();
break;
@@ -524,9 +483,6 @@ export default class Explorer {
this.deleteDatabaseText("Delete Keyspace");
this.collectionTitle("Cassandra API");
this.collectionTreeNodeAltText("Table");
this.addCollectionPane.title("Add Table");
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables");
this.tableDataClient = new CassandraAPIDataClient();
break;
@@ -706,16 +662,8 @@ export default class Explorer {
this.setIsNotificationConsoleExpanded(true);
}
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 collapseConsole(): void {
this.setIsNotificationConsoleExpanded(false);
}
public refreshDatabaseForResourceToken(): Q.Promise<any> {
@@ -834,14 +782,6 @@ 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");
@@ -1096,6 +1036,9 @@ 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 {
@@ -1104,10 +1047,6 @@ 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" ||
@@ -1303,10 +1242,18 @@ export default class Explorer {
public async publishNotebook(
name: string,
content: NotebookPaneContent,
parentDomElement?: HTMLElement
notebookContentRef?: string,
onTakeSnapshot?: (request: SnapshotRequest) => void,
onClosePanel?: () => void
): Promise<void> {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
await this.notebookManager.openPublishNotebookPane(
name,
content,
notebookContentRef,
onTakeSnapshot,
onClosePanel
);
this.isPublishNotebookPaneEnabled(true);
}
}
@@ -1845,9 +1792,6 @@ export default class Explorer {
public onNewCollectionClicked(databaseId?: string): void {
if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open();
} else if (userContext.features.enableKOPanel) {
this.addCollectionPane.open(this.selectedDatabaseId());
document.getElementById("linkAddCollection").focus();
} else {
this.openAddCollectionPanel(databaseId);
}
@@ -1896,7 +1840,6 @@ export default class Explorer {
public async handleOpenFileAction(path: string): Promise<void> {
if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this.closeAllPanes();
this._openSetupNotebooksPaneForQuickstart();
}
@@ -1961,7 +1904,7 @@ export default class Explorer {
public openDeleteDatabaseConfirmationPane(): void {
this.openSidePanel(
"Delete Database",
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel
explorer={this}
openNotificationConsole={this.expandConsole}
@@ -1972,12 +1915,12 @@ export default class Explorer {
}
public openUploadItemsPanePane(): void {
this.openSidePanel("Upload", <UploadItemsPane explorer={this} closePanel={this.closeSidePanel} />);
this.openSidePanel("Upload " + getUploadName(), <UploadItemsPane explorer={this} />);
}
public openSettingPane(): void {
this.openSidePanel(
"Settings",
"Setting",
<SettingsPane expandConsole={() => this.expandConsole()} closePanel={this.closeSidePanel} />
);
}
@@ -2005,6 +1948,21 @@ export default class Explorer {
/>
);
}
public openAddDatabasePane(): void {
if (userContext.features.enableKOPanel) {
this.addDatabasePane.open();
document.getElementById("linkAddDatabase").focus();
} else {
this.openSidePanel(
"Add " + getDatabaseName(),
<AddDatabasePanel
explorer={this}
openNotificationConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel}
/>
);
}
}
public openBrowseQueriesPanel(): void {
this.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this} closePanel={this.closeSidePanel} />);
@@ -2021,7 +1979,7 @@ export default class Explorer {
public openUploadFilePanel(parent?: NotebookContentItem): void {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.openSidePanel(
"Upload File",
"Upload file to notebook server",
<UploadFilePane
expandConsole={() => 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({
graphConfig: GraphTab.createGraphConfig(),
igraphConfig: GraphTab.createIGraphConfig(),
onHighlightedNode: sinon.spy(),
onLoadMoreData: (action: LoadMoreDataAction): void => {},
@@ -141,6 +141,7 @@ 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);
@@ -150,7 +151,7 @@ describe("D3ForceGraph", () => {
expect(onHighlightedNode.id).toEqual(v1Id);
};
forceGraph.updateGraph(newGraph);
forceGraph.updateGraph(newGraph, forceGraph.igraphConfig);
});
});
});

View File

@@ -13,7 +13,7 @@ import _ from "underscore";
import * as Constants from "../../../Common/Constants";
import { NeighborType } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
import { IGraphConfig } from "./../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer";
@@ -48,21 +48,22 @@ interface ZoomTransform extends Point2D {
export interface D3ForceGraphParameters {
// Graph to parent
graphConfig: GraphConfig;
onHighlightedNode: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph
onLoadMoreData: (action: LoadMoreDataAction) => void;
igraphConfig: IGraphConfig;
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>): void;
updateGraph(graphData: GraphData<D3Node, D3Link>, igraphConfigParam?: IGraphConfig): void;
enableHighlight(enable: boolean): void;
}
@@ -108,7 +109,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
@@ -119,9 +120,11 @@ 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;
@@ -151,7 +154,10 @@ export class D3ForceGraph implements GraphRenderer {
this.g.remove();
}
public updateGraph(newGraph: GraphData<D3Node, D3Link>): void {
public updateGraph(newGraph: GraphData<D3Node, D3Link>, igraphConfigParam?: IGraphConfig): void {
if (igraphConfigParam) {
this.igraphConfig = igraphConfigParam;
}
if (!newGraph || !this.simulation) {
return;
}
@@ -159,7 +165,8 @@ export class D3ForceGraph implements GraphRenderer {
this.graphDataWrapper = new GraphData<D3Node, D3Link>();
this.graphDataWrapper.setData(newGraph);
const key = this.params.graphConfig.nodeColorKey();
const key = this.igraphConfig.nodeColorKey;
if (key !== GraphExplorer.NONE_CHOICE) {
this.updateUniqueValues(key);
}
@@ -265,20 +272,7 @@ export class D3ForceGraph implements GraphRenderer {
});
});
// 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.redrawGraph();
this.instantiateSimulation();
} // initialize
@@ -371,7 +365,10 @@ 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) {
@@ -526,7 +523,10 @@ 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 nodes = this.simulation.nodes();
const nodes1 = this.simulation.nodes();
this.redrawGraph();
this.animateBigBang(nodes, newNodes);
this.animateBigBang(nodes1, 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.params.graphConfig.linkWidth())
.attr("stroke", this.params.graphConfig.linkColor());
.attr("stroke-width", this.igraphConfig.linkWidth)
.attr("stroke", this.igraphConfig.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.params.graphConfig.linkColor())
.attr("fill", this.igraphConfig.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.params.graphConfig.nodeSize());
.attr("r", this.igraphConfig.nodeSize);
var iconGroup = newNodes
.append("g")
@@ -733,22 +733,23 @@ export class D3ForceGraph implements GraphRenderer {
.attr("aria-label", (d: D3Node) => {
return this.retrieveNodeCaption(d);
})
.on("dblclick", function (_: MouseEvent, d: D3Node) {
.on("dblclick", function (this: Element, _: MouseEvent, d: D3Node) {
// https://stackoverflow.com/a/41945742 ('this' implicitly has type 'any' because it does not have a type annotation)
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
})
.on("click", function (_: MouseEvent, d: D3Node) {
.on("click", function (this: Element, _: MouseEvent, d: D3Node) {
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
})
.on("keypress", function (event: KeyboardEvent, d: D3Node) {
.on("keypress", function (this: Element, event: KeyboardEvent, d: D3Node) {
if (event.charCode === Constants.KeyCodes.Space || event.charCode === Constants.KeyCodes.Enter) {
event.stopPropagation();
// this is the <g> element
self.onNodeClicked(this.parentNode, d);
}
});
var nodeSize = this.params.graphConfig.nodeSize();
var nodeSize = this.igraphConfig.nodeSize;
var bgsize = nodeSize + 1;
iconGroup
@@ -758,7 +759,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("width", bgsize * 2)
.attr("height", bgsize * 2)
.attr("fill-opacity", (d: D3Node) => {
return this.params.graphConfig.nodeIconKey() ? 1 : 0;
return this.igraphConfig.nodeIconKey ? 1 : 0;
})
.attr("class", "icon-background");
@@ -766,14 +767,13 @@ export class D3ForceGraph implements GraphRenderer {
iconGroup
.append("svg:image")
.attr("xlink:href", (d: D3Node) => {
return D3ForceGraph.computeImageData(d, this.params.graphConfig);
return D3ForceGraph.computeImageData(d, this.igraphConfig);
})
.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 +807,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("x2", 0)
.attr("y2", gaugeYOffset)
.style("stroke-width", 1)
.style("stroke", this.params.graphConfig.linkColor());
.style("stroke", this.igraphConfig.linkColor);
parent
.append("use")
.attr("xlink:href", "#triangleRight")
@@ -876,7 +876,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("height", gaugeHeight)
.style("fill", "white")
.style("stroke-width", 1)
.style("stroke", this.params.graphConfig.linkColor());
.style("stroke", this.igraphConfig.linkColor);
parent
.append("rect")
.attr("x", (d: D3Node) => {
@@ -893,7 +893,7 @@ export class D3ForceGraph implements GraphRenderer {
: 0;
})
.attr("height", gaugeHeight)
.style("fill", this.params.graphConfig.nodeColor())
.style("fill", this.igraphConfig.nodeColor)
.attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden"));
parent
.append("text")
@@ -970,7 +970,7 @@ export class D3ForceGraph implements GraphRenderer {
const self = this;
nodeSelection.selectAll(".loadmore").remove();
var nodeSize = this.params.graphConfig.nodeSize();
var nodeSize = this.igraphConfig.nodeSize;
const rootSelectionG = nodeSelection
.filter((d: D3Node) => {
return !!d._isRoot && !!d._pagination;
@@ -994,7 +994,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.params.graphConfig.nodeColor());
this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.igraphConfig.nodeColor);
}
/**
@@ -1031,11 +1031,11 @@ export class D3ForceGraph implements GraphRenderer {
* @param d
*/
private getNodeColor(d: D3Node): string {
if (this.params.graphConfig.nodeColorKey()) {
const val = GraphData.getNodePropValue(d, this.params.graphConfig.nodeColorKey());
if (this.igraphConfig.nodeColorKey) {
const val = GraphData.getNodePropValue(d, this.igraphConfig.nodeColorKey);
return this.lookupColorFromKey(<string>val);
} else {
return this.params.graphConfig.nodeColor();
return this.igraphConfig.nodeColor;
}
}
@@ -1102,12 +1102,12 @@ export class D3ForceGraph implements GraphRenderer {
this.graphDataWrapper.getTargetsForId(nodeId)
);
}
})(this.params.graphConfig.showNeighborType());
})(this.igraphConfig.showNeighborType);
return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId;
});
this.g.selectAll(".link").classed("inactive", (l: D3Link) => {
switch (this.params.graphConfig.showNeighborType()) {
switch (this.igraphConfig.showNeighborType) {
case NeighborType.SOURCES_ONLY:
return (<D3Node>l.target).id !== nodeId;
case NeighborType.TARGETS_ONLY:
@@ -1151,7 +1151,7 @@ export class D3ForceGraph implements GraphRenderer {
}
private retrieveNodeCaption(d: D3Node) {
let key = this.params.graphConfig.nodeCaption();
let key = this.igraphConfig.nodeCaption;
let value: string = d.id || d.label;
if (key) {
value = <string>GraphData.getNodePropValue(d, key) || "";
@@ -1193,10 +1193,16 @@ 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.params.graphConfig.nodeSize() + 3;
var radius = this.igraphConfig.nodeSize + 3;
// End
const dx = target.x - d1.x;
@@ -1209,10 +1215,16 @@ 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.params.graphConfig.nodeSize() + 3;
var radius = this.igraphConfig.nodeSize + 3;
// Start
var dx = d1.x - source.x;
@@ -1244,13 +1256,13 @@ export class D3ForceGraph implements GraphRenderer {
return d._isRoot ? "node root" : "node";
});
this.applyConfig(this.params.graphConfig);
this.applyConfig(this.igraphConfig);
}
private static computeImageData(d: D3Node, config: GraphConfig): string {
let propValue = <string>GraphData.getNodePropValue(d, config.nodeIconKey()) || "";
private static computeImageData(d: D3Node, config: IGraphConfig): 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;
}
@@ -1260,48 +1272,46 @@ export class D3ForceGraph implements GraphRenderer {
/**
* Update graph according to configuration or use default
*/
private applyConfig(config: GraphConfig) {
if (config.nodeIconKey()) {
private applyConfig(config: IGraphConfig) {
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", this.params.graphConfig.nodeColor());
this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", config.nodeColor);
this.g.selectAll(".link").attr("stroke-width", config.linkWidth);
this.g.selectAll(".link").attr("stroke-width", config.linkWidth());
this.g.selectAll(".link").attr("stroke", config.linkColor());
this.g.selectAll(".link").attr("stroke", config.linkColor);
if (D3ForceGraph.useSvgMarkerEnd()) {
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

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { GraphConfig } from "../../Tabs/GraphTab";
import { IGraphConfig } 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,9 +58,10 @@ export interface GraphExplorerProps {
onLoadStartKeyChange: (newKey: number) => void;
resourceId: string;
/* TODO Figure out how to make this Knockout-free */
graphConfigUiData: ViewModels.GraphConfigUiData;
graphConfig?: GraphConfig;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
setIConfigUiData?: (data: string[]) => void;
}
export interface GraphHighlightedNodeData {
@@ -121,6 +122,10 @@ interface GraphExplorerState {
filterQueryError: string;
filterQueryWarning: string;
filterQueryStatus: FilterQueryStatus;
change: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
}
export interface EditedProperties {
@@ -218,6 +223,8 @@ 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 = {
@@ -237,6 +244,9 @@ 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
@@ -284,41 +294,27 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setGremlinParams();
}
/* 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();
});
/* *************************************** */
const selectedNode = this.state.highlightedNode;
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"]
@@ -408,7 +404,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.updateGraphData(this.originalGraphData, this.state.igraphConfig);
})
.then(
() => {
@@ -446,7 +442,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.updateGraphData(graphData, this.state.igraphConfig);
this.setState({ highlightedNode: null });
// Remove from root map
@@ -582,7 +578,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.edgeInfoCache.addVertex(vertex);
graphData.setAsRoot(vertex.id);
this.updateGraphData(graphData);
this.updateGraphData(graphData, this.state.igraphConfig);
};
vertex._outEdgeIds = vertex._outEdgeIds || [];
@@ -788,7 +784,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
graphData.getVertexById(edge.outV)._inEAllLoaded = false;
}
this.updateGraphData(graphData);
this.updateGraphData(graphData, this.state.igraphConfig);
},
(error: string) => {
GraphExplorer.reportToConsole(
@@ -809,7 +805,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
() => {
let graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData);
this.updateGraphData(graphData, this.state.igraphConfig);
},
(error: string) => {
GraphExplorer.reportToConsole(
@@ -858,7 +854,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (vertices.length === 0) {
// Clean graph
this.updateGraphData(new GraphData.GraphData());
this.updateGraphData(new GraphData.GraphData(), this.state.igraphConfig);
this.setState({ highlightedNode: null });
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result is empty");
}
@@ -940,7 +936,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let vertex = vertices[0];
const graphData = this.originalGraphData;
graphData.addVertex(vertex);
this.updateGraphData(graphData);
this.updateGraphData(graphData, this.state.igraphConfig);
this.collectNodeProperties(this.originalGraphData.vertices);
// Keep new vertex selected
@@ -965,7 +961,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}
/* ************* React life-cycle methods *********** */
public override render(): JSX.Element {
public render(): JSX.Element {
const currentTabIndex = ((resultDisplay: ResultDisplay): number => {
switch (resultDisplay) {
case ResultDisplay.Graph:
@@ -1022,10 +1018,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
);
}
public override componentWillUnmount(): void {
public componentWillUnmount(): void {
this.gremlinClient.destroy();
}
public override componentDidMount(): void {
public componentDidMount(): void {
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
@@ -1069,7 +1065,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}
}
public override componentDidUpdate(): void {
public componentDidUpdate(): void {
this.onIsPropertyPaneEditing(this.isPropertyPaneEditing());
this.onIsNewVertexDisabledChange(this.isNewVertexDisabled());
}
@@ -1121,8 +1117,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return rootMap[id];
})
);
if (this.props.graphConfigUiData.nodeProperties().indexOf(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY) !== -1) {
this.props.graphConfigUiData.nodeCaptionChoice(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY);
if (this.state.igraphConfigUiData.nodeProperties.indexOf(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY) !== -1) {
this.setState({
igraphConfigUiData: {
...this.state.igraphConfigUiData,
nodeCaptionChoice: GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY,
},
});
}
// Let react instantiate and render graph, before updating
@@ -1139,7 +1140,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
*/
public updateNodeIcons(nodeProp: string, iconSet: string): void {
if (nodeProp === GraphExplorer.NONE_CHOICE) {
this.props.graphConfig.nodeIconKey(null);
this.setState({
igraphConfig: {
...this.state.igraphConfig,
nodeIconKey: undefined,
},
});
return;
}
@@ -1163,8 +1169,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
});
// Update graph configuration
this.props.graphConfig.iconsMap(newIconsMap);
this.props.graphConfig.nodeIconKey(nodeProp);
this.setState({
igraphConfig: {
...this.state.igraphConfig,
iconsMap: newIconsMap,
nodeIconKey: nodeProp,
},
});
},
() => {
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to retrieve icons. iconSet:${iconSet}`);
@@ -1209,7 +1220,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}
private getPossibleRootNodes(): LeftPane.CaptionId[] {
const key = this.props.graphConfigUiData.nodeCaptionChoice();
const key = this.state.igraphConfig.nodeCaption;
return $.map(
this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => {
@@ -1320,7 +1331,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return "";
}
const nodeCaption = this.props.graphConfigUiData.nodeCaptionChoice();
const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
const node = this.originalGraphData.getVertexById(this.state.highlightedNode.id);
return GraphData.GraphData.getNodePropValue(node, nodeCaption) as string;
}
@@ -1410,7 +1421,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
const q = `SELECT c.id, c["${
this.props.graphConfigUiData.nodeCaptionChoice() || "id"
this.state.igraphConfigUiData.nodeCaptionChoice || "id"
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => {
@@ -1539,9 +1550,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
});
const values = Object.keys(props);
this.props.graphConfigUiData.nodeProperties(values);
// TODO This should move out of GraphExplorer
this.props.graphConfigUiData.nodePropertiesWithNone([GraphExplorer.NONE_CHOICE].concat(values));
this.setState({
igraphConfigUiData: {
...this.state.igraphConfigUiData,
nodeProperties: values,
},
});
this.props.setIConfigUiData(values);
}
/**
@@ -1566,9 +1582,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let sources: NeighborVertexBasicInfo[] = [];
let targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues();
let nodeCaption = this.props.graphConfigUiData.nodeCaptionChoice();
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
let sData: GraphHighlightedNodeData = {
id: data.id,
label: data.label,
@@ -1615,7 +1630,12 @@ 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,
});
});
}
@@ -1629,7 +1649,12 @@ 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,
});
});
}
@@ -1678,14 +1703,17 @@ 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>) {
private updateGraphData(
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
igraphConfig?: IGraphConfig
) {
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);
this.d3ForceGraph.updateGraph(gd, igraphConfig);
}
public onMiddlePaneInitialized(instance: D3ForceGraph.GraphRenderer): void {
@@ -1694,10 +1722,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private renderMiddlePane(): JSX.Element {
const forceGraphParams: D3ForceGraph.D3ForceGraphParameters = {
graphConfig: this.props.graphConfig,
igraphConfig: this.state.igraphConfig,
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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