Skip to content

Commit

Permalink
Features/Bug Fixes (Azure-Samples#243)
Browse files Browse the repository at this point in the history
* Bulk upload - Twin ID's that are all numbers fail, Complex query constructs do not show relationships, Model uploaded with an array of contexts fails in digital explorer tool - but works fine via API.

* Graph navigator mini-screen is broken when no twins are present

* Stop the re-layout of graph when adding nodes

* "Create Relationship" UI updates and enabled "swap"

* After Import Graph, the user is left with a blank screen, that requires them to click the twins graph again

* Show more detatiled import errors

* Node color persistence

* Ensure twins are created in the correct color

* Quickly deleting relationships after each other will show an error that is not relevant

* KlayConfig - stop node name overlapping

* Update styling of "no relationship" warning

* Adding Multi-line query

* Fixing problem for node names containing quotes

* Fix issue with deleting relationships with underscores

* Stopping Layout from reload on relationship changes

* Fixing Recolor of relationship when adding or deleting

Co-authored-by: Selim Díaz Araya <[email protected]>
Co-authored-by: JoseZarmada <[email protected]>
  • Loading branch information
3 people authored Feb 15, 2022
1 parent 4bbd8fa commit 3d5f8c0
Show file tree
Hide file tree
Showing 19 changed files with 291 additions and 93 deletions.
4 changes: 3 additions & 1 deletion client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ class App extends Component {
eventService.subscribeCloseComponent(component => {
switch (component) {
case "importComponent":
this.setState(prevState => ({ layout: { ...prevState.layout, showImport: false, importFile: null } }));
this.setState(prevState => ({ layout: { ...prevState.layout, showImport: false, importFile: null } }), () => {
this.handleMainContentPivotChange("graph-viewer");
});
break;
default:
break;
Expand Down
1 change: 1 addition & 0 deletions client/src/assets/SwapRelationship.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions client/src/assets/WarningRelationship.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ class GraphViewerComponent extends React.Component {
eventService.subscribeDeleteRelationship(data => data && this.onRelationshipDelete(data));
eventService.subscribeCreateTwin(data => {
this.cyRef.current.addTwins([ data ]);
this.cyRef.current.doLayout();
this.cyRef.current.updateNodeColors();
this.cyRef.current.zoomToFit();
});
eventService.subscribeConfigure(evt => {
if (evt.type === "end" && evt.config) {
Expand Down Expand Up @@ -427,11 +428,10 @@ class GraphViewerComponent extends React.Component {
}
}

onTwinDelete = async ids => {
onTwinDelete = ids => {
if (ids) {
this.cyRef.current.removeTwins(ids);
this.cyRef.current.clearSelection();
await this.cyRef.current.doLayout();
} else {
this.cyRef.current.clearTwins();
}
Expand Down Expand Up @@ -467,10 +467,9 @@ class GraphViewerComponent extends React.Component {
this.setState({ canShowAllRelationships: true });
}

onRelationshipCreate = async relationship => {
onRelationshipCreate = relationship => {
if (relationship) {
this.cyRef.current.addRelationships([ relationship ]);
await this.cyRef.current.doLayout();
this.setState({ selectedNode: null, selectedNodes: null });
this.cyRef.current.unselectSelectedNodes();
this.cyRef.current.clearSelection();
Expand All @@ -481,10 +480,10 @@ class GraphViewerComponent extends React.Component {
}
}

onRelationshipDelete = async relationship => {
onRelationshipDelete = relationship => {
if (relationship) {
this.cyRef.current.removeRelationships([ getUniqueRelationshipId(relationship) ]);
await this.cyRef.current.doLayout();
this.setState({ selectedEdges: null });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,29 @@
margin-right: 0;
}
}
.btn-icon {
float: right;
}
.warning-icon-wrapper {
background-color: #FFD4DD;
height: 45px;
margin-bottom: 10px;
}
.warning-icon {
float: left;
margin-top: 7px
}
.warning-text {
float: right;
background-color: #FFD4DD;
margin: 1px;
width: 190px;
height: 95%
}
.warning-label {
font-weight: 400;
padding-left: 5px;
margin-top: -4px;
color: black;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ export class GraphViewerCytoscapeComponent extends React.Component {
checked.push(rel);
}
}

this.graphControl.add(checked);
this.updateNodeColors();
}

getRelationships() {
Expand Down Expand Up @@ -474,15 +474,15 @@ export class GraphViewerCytoscapeComponent extends React.Component {
});
}

doLayout() {
updateNodeColors() {
const cy = this.graphControl;
const modelColors = settingsService.getModelColors();
cy.batch(() => {
const types = {};
const mtypes = {};
const rtypes = {};
const el = cy.nodes("*");
const rels = cy.edges("*");

// Color by type attribute
for (let i = 0; i < el.length; i++) {
types[el[i].data("type")] = `#${this.getColor(i)}`;
Expand All @@ -493,9 +493,10 @@ export class GraphViewerCytoscapeComponent extends React.Component {

// Color by model type
for (let i = 0; i < el.length; i++) {
mtypes[el[i].data("modelId")] = {
backgroundColor: `#${this.getColor(i)}`,
backgroundImage: this.getBackgroundImage(el[i].data("modelId"))
const modelId = el[i].data("modelId");
mtypes[modelId] = {
backgroundColor: modelColors[modelId],
backgroundImage: this.getBackgroundImage(modelId)
};
}
for (const t of Object.keys(mtypes)) {
Expand All @@ -513,16 +514,21 @@ export class GraphViewerCytoscapeComponent extends React.Component {
});
}
}

// Color relationships by label
for (let i = 0; i < rels.length; i++) {
rtypes[rels[i].data("label")] = `#${this.getColor(i)}`;
if (!rtypes[rels[i].data("label")]) {
rtypes[rels[i].data("label")] = `#${this.getColor(i)}`;
}
}
for (const r of Object.keys(rtypes)) {
cy.elements(`edge[label="${r}"]`).style("line-color", rtypes[r]);
}
});
}

doLayout() {
this.updateNodeColors();
const cy = this.graphControl;
return new Promise(resolve => {
const layout = cy.layout(GraphViewerCytoscapeLayouts[this.layout]);
layout.on("layoutstop", () => resolve());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export const coseOptions = {

export const klayOptions = {
name: "klay",
nodeDimensionsIncludeLabels: false, // Boolean which changes whether label dimensions are included when calculating node dimensions
nodeDimensionsIncludeLabels: true, // Boolean which changes whether label dimensions are included when calculating node dimensions
fit: true, // Whether to fit
padding: 20, // Padding on fit
animate: true, // Whether to transition the node positions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT license.

import React, { Component } from "react";
import { TextField, DefaultButton, Dropdown } from "office-ui-fabric-react";
import { TextField, DefaultButton, Dropdown, IconButton, Label } from "office-ui-fabric-react";
import { v4 as uuidv4 } from "uuid";

import ModalComponent from "../../ModalComponent/ModalComponent";
Expand All @@ -12,6 +12,8 @@ import "../GraphViewerComponentShared.scss";
import { eventService } from "../../../services/EventService";
import { ModelService } from "../../../services/ModelService";

const swapIconName = "SwapRelationship";
const warningIconName = "WarningRelationship";
export class GraphViewerRelationshipCreateComponent extends Component {

constructor(props) {
Expand All @@ -20,7 +22,11 @@ export class GraphViewerRelationshipCreateComponent extends Component {
isLoading: false,
relationshipItems: [],
relationshipId: null,
hasRequiredRelError: false
hasRequiredRelError: false,
hasRelationships: true,
hasSwapExecuted: true,
sourceId: "",
targetId: ""
};
}

Expand Down Expand Up @@ -57,11 +63,10 @@ export class GraphViewerRelationshipCreateComponent extends Component {

save = async () => {
const { onCreate } = this.props;
const { relationshipId, relationshipItems } = this.state;
const { relationshipId, relationshipItems, sourceId, targetId } = this.state;
if (relationshipId === null) {
this.setState({ hasRequiredRelError: true });
} else {
const { sourceId, targetId } = this.getNodes();
this.setState({ isLoading: true });
try {
const id = uuidv4();
Expand All @@ -88,18 +93,16 @@ export class GraphViewerRelationshipCreateComponent extends Component {
}

open = async () => {
const { sourceModelId } = this.state;
this.setState({ showModal: true, isLoading: true });
setTimeout(() => {
document.getElementById("create-relationship-heading").focus();
}, 200);

const { selectedNode, selectedNodes } = this.props;
const sourceModelId = selectedNodes.find(x => x.id !== selectedNode.id).modelId;
const targetModelId = selectedNode.modelId;

try {
const relationshipItems = await new ModelService().getRelationships(sourceModelId, targetModelId);
this.setState({ relationshipItems });
const { selectedNode, selectedNodes } = this.props;
const source = selectedNodes.find(x => x.id !== selectedNode.id);
const target = selectedNode;
this.setState({ sourceId: source.id, targetId: target.id });
const relationshipItems = await new ModelService().getRelationships(source.modelId, target.modelId);
this.setState({ hasRelationships: relationshipItems.length > 0, relationshipItems });
} catch (exc) {
this.setState({ relationshipItems: [] });
exc.customMessage = `Error in retrieving model. Requested ${sourceModelId}`;
Expand All @@ -109,42 +112,70 @@ export class GraphViewerRelationshipCreateComponent extends Component {
this.setState({ isLoading: false });
}

getNodes() {
const { selectedNode, selectedNodes } = this.props;
const source = selectedNodes && selectedNodes.find(x => x.id !== selectedNode.id);
const sourceId = source ? source.id : "";
const targetId = selectedNode ? selectedNode.id : "";

return { sourceId, targetId };
swap = async () => {
this.setState(({ hasSwapExecuted }) => ({ hasSwapExecuted: !hasSwapExecuted }));
const { sourceModelId, hasSwapExecuted } = this.state;
this.setState({ isLoading: true });
try {
const { selectedNode, selectedNodes } = this.props;
let source = selectedNodes.find(x => x.id !== selectedNode.id);
let target = selectedNode;
if (hasSwapExecuted) {
source = selectedNode;
target = selectedNodes.find(x => x.id !== selectedNode.id);
}
this.setState({ sourceId: source.id, targetId: target.id });
const relationshipItems = await new ModelService().getRelationships(source.modelId, target.modelId);
this.setState({ hasRelationships: relationshipItems.length > 0, relationshipItems });
} catch (exc) {
this.setState({ relationshipItems: [] });
exc.customMessage = `Error in retrieving model. Requested ${sourceModelId}`;
eventService.publishError(exc);
}
this.setState({ isLoading: false });
}

render() {
const { relationshipItems, relationshipId, isLoading, showModal, hasRequiredRelError } = this.state;
const { sourceId, targetId } = this.getNodes();
const { relationshipItems, relationshipId, isLoading, showModal, hasRequiredRelError, sourceId, targetId, hasRelationships } = this.state;

return (
<ModalComponent isVisible={showModal} isLoading={isLoading} className="gc-dialog">
<h2 className="heading-2" tabIndex="0" id="create-relationship-heading">Create Relationship</h2>
<h4>Source ID</h4>
<TextField disabled readOnly id="sourceIdField" ariaLabel="Source ID" className="modal-input" styles={this.getStyles} value={sourceId} />
<div className="btn-icon">
<IconButton iconOnly="true" iconProps={{ iconName: swapIconName }} title="Swap Relationship" ariaLabel="Swap Relationship" onClick={this.swap} />
</div>
<h4>Target ID</h4>
<TextField disabled readOnly id="targetIdField" ariaLabel="Target ID" className="modal-input" styles={this.getStyles} value={targetId} />
<h4>Relationship</h4>
<Dropdown
tabIndex="0"
ariaLabel="Select an option"
required
placeholder="Select an option"
className="modal-input"
selectedKey={relationshipId}
options={relationshipItems.map((q, i) => ({ key: i, text: q }))}
styles={{
dropdown: { width: 208 }
}}
errorMessage={hasRequiredRelError ? "Please select a relationship" : null}
onChange={this.onSelectedRelChange} />
<h4>Relationship</h4> {
hasRelationships && <div>
<Dropdown
tabIndex="0"
ariaLabel="Select an option"
required
placeholder="Select an option"
className="modal-input"
selectedKey={relationshipId}
options={relationshipItems.map((q, i) => ({ key: i, text: q }))}
styles={{
dropdown: { width: 208 }
}}
errorMessage={hasRequiredRelError ? "Please select a relationship" : null}
onChange={this.onSelectedRelChange} />
</div>
} {
!hasRelationships && <div className="warning-icon-wrapper">
<div className="warning-icon">
<IconButton iconOnly="true" iconProps={{ iconName: warningIconName }} title="Warning Relationship" ariaLabel="Warning Relationship" />
</div>
<div className="warning-text">
<Label className="warning-label">No relationship available, try swapping source and target</Label>
</div>
</div>
}
<div className="btn-group">
<DefaultButton className="modal-button save-button" onClick={this.save}>Save</DefaultButton>
<DefaultButton className="modal-button save-button" onClick={this.save} disabled={!hasRelationships}>Save</DefaultButton>
<DefaultButton className="modal-button cancel-button" onClick={this.cancel}>Cancel</DefaultButton>
</div>
</ModalComponent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ class GraphViewerRelationshipDeleteComponent extends Component {

async deleteRelationship(edge) {
try {
const { source } = edge;
let { id } = edge;
id = id.split("_").pop();
print(`*** Deleting relationship ${id}`, "info");
await apiService.deleteRelationship(source, id);
eventService.publishDeleteRelationship({ $sourceId: source, $relationshipId: id });
const { source, relationshipId } = edge;
print(`*** Deleting relationship ${relationshipId}`, "info");
await apiService.deleteRelationship(source, relationshipId);
eventService.publishDeleteRelationship({ $sourceId: source, $relationshipId: relationshipId });
} catch (exc) {
exc.customMessage = "Error deleting relationship";
eventService.publishError(exc);
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/ImportComponent/ImportComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export class ImportComponent extends Component {
}

focus = async () => {
await this.cyRef.current.doLayout();
if (this.cyRef.current) {
await this.cyRef.current.doLayout();
}
}

onSaveClicked = async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class ModelViewerComponent extends Component {
sortArray(items, "displayName", "key");

this.originalItems = items.slice(0, items.length);
settingsService.setModelColors(items.map(item => item.key));
this.setState({ items, isLoading: false });
}

Expand Down
Loading

0 comments on commit 3d5f8c0

Please sign in to comment.