Migrate graph style panel to react (#619)

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
vaidankarswapnil
2021-05-14 02:15:00 +05:30
committed by GitHub
parent 404b1fc0f1
commit a6b82c8340
28 changed files with 596 additions and 1516 deletions

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")
@@ -749,7 +749,7 @@ export class D3ForceGraph implements GraphRenderer {
self.onNodeClicked(this.parentNode, d);
}
});
var nodeSize = this.params.graphConfig.nodeSize();
var nodeSize = this.igraphConfig.nodeSize;
var bgsize = nodeSize + 1;
iconGroup
@@ -759,7 +759,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("width", bgsize * 2)
.attr("height", bgsize * 2)
.attr("fill-opacity", (d: D3Node) => {
return this.params.graphConfig.nodeIconKey() ? 1 : 0;
return this.igraphConfig.nodeIconKey ? 1 : 0;
})
.attr("class", "icon-background");
@@ -767,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")
@@ -808,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")
@@ -877,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) => {
@@ -894,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")
@@ -971,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;
@@ -995,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);
}
/**
@@ -1032,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;
}
}
@@ -1103,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:
@@ -1152,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) || "";
@@ -1194,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;
@@ -1210,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;
@@ -1245,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;
}
@@ -1261,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

@@ -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
@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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