@truedat/lm 4.41.5 → 4.42.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +5 -4
- package/src/components/RelationGraph.js +97 -37
- package/src/components/RelationGraphDepth.js +35 -0
- package/src/components/__tests__/RelationGraph.spec.js +203 -32
- package/src/messages/en.js +2 -0
- package/src/messages/es.js +2 -1
- package/src/services/relationGraphTraversal.js +60 -0
- package/src/styles/relationGraph.less +34 -0
- package/src/components/__tests__/__snapshots__/RelationGraph.spec.js.snap +0 -107
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/lm",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.42.3",
|
|
4
4
|
"description": "Truedat Link Manager",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"jsnext:main": "src/index.js",
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
"@testing-library/jest-dom": "^5.14.1",
|
|
34
34
|
"@testing-library/react": "^12.0.0",
|
|
35
35
|
"@testing-library/user-event": "^13.2.1",
|
|
36
|
-
"@truedat/test": "4.
|
|
36
|
+
"@truedat/test": "4.42.0",
|
|
37
37
|
"babel-jest": "^27.0.6",
|
|
38
38
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
|
39
39
|
"babel-plugin-lodash": "^3.3.4",
|
|
40
40
|
"babel-plugin-react-intl": "^5.1.18",
|
|
41
41
|
"babel-plugin-transform-semantic-ui-react-imports": "^1.4.1",
|
|
42
|
+
"canvas": "^2.9.1",
|
|
42
43
|
"enzyme": "^3.11.0",
|
|
43
44
|
"enzyme-adapter-react-16": "^1.15.6",
|
|
44
45
|
"enzyme-to-json": "^3.6.2",
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
]
|
|
84
85
|
},
|
|
85
86
|
"dependencies": {
|
|
86
|
-
"@truedat/core": "4.
|
|
87
|
+
"@truedat/core": "4.42.3",
|
|
87
88
|
"path-to-regexp": "^1.7.0",
|
|
88
89
|
"prop-types": "^15.7.2",
|
|
89
90
|
"react-graph-vis": "1.0.5",
|
|
@@ -100,5 +101,5 @@
|
|
|
100
101
|
"react-dom": ">= 16.8.6 < 17",
|
|
101
102
|
"semantic-ui-react": ">= 0.88.2 < 2.1"
|
|
102
103
|
},
|
|
103
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "5adf3b4e2996ade1df6c42dcacec8cf74807db3e"
|
|
104
105
|
}
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import React, { useState } from "react";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
3
|
import { connect } from "react-redux";
|
|
4
4
|
import { useIntl } from "react-intl";
|
|
5
5
|
import PropTypes from "prop-types";
|
|
6
6
|
import Graph from "react-graph-vis";
|
|
7
|
+
import "../styles/relationGraph.less";
|
|
8
|
+
import { dfsMaxDepth, dfs, getEdges } from "../services/relationGraphTraversal";
|
|
9
|
+
import RelationGraphDepth from "./RelationGraphDepth";
|
|
7
10
|
|
|
8
|
-
export const RelationGraph = ({
|
|
11
|
+
export const RelationGraph = ({
|
|
12
|
+
navigate,
|
|
13
|
+
currentId,
|
|
14
|
+
relationsGraph,
|
|
15
|
+
initialDepth,
|
|
16
|
+
}) => {
|
|
9
17
|
const { formatMessage } = useIntl();
|
|
10
18
|
const [network, setNetwork] = useState({});
|
|
19
|
+
const [depth, setDepth] = useState(initialDepth || Infinity);
|
|
20
|
+
const [maxDepth, setMaxDepth] = useState();
|
|
21
|
+
const [limitedRelationsGraph, setLimitedRelationsGraph] = useState();
|
|
22
|
+
const [start, setStart] = useState();
|
|
11
23
|
|
|
12
|
-
const
|
|
24
|
+
const getVisNodes = _.flow(
|
|
13
25
|
_.get("nodes"),
|
|
14
26
|
_.map(({ id, name: label }) => ({
|
|
15
27
|
id,
|
|
@@ -19,7 +31,7 @@ export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
|
19
31
|
currentId === id
|
|
20
32
|
? {
|
|
21
33
|
background: "#ed5c17",
|
|
22
|
-
border: "black"
|
|
34
|
+
border: "black",
|
|
23
35
|
}
|
|
24
36
|
: null,
|
|
25
37
|
|
|
@@ -28,28 +40,57 @@ export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
|
28
40
|
? {
|
|
29
41
|
color: "white",
|
|
30
42
|
bold: true,
|
|
31
|
-
face: "Lato, Helvetica Neue, Arial, Helvetica, sans-serif"
|
|
43
|
+
face: "Lato, Helvetica Neue, Arial, Helvetica, sans-serif",
|
|
32
44
|
}
|
|
33
|
-
: null
|
|
45
|
+
: null,
|
|
34
46
|
}))
|
|
35
|
-
)
|
|
47
|
+
);
|
|
36
48
|
|
|
37
|
-
const
|
|
49
|
+
const getVisEdges = _.flow(
|
|
38
50
|
_.get("edges"),
|
|
39
51
|
_.map(({ source_id: from, target_id: to, tags }) => ({
|
|
40
52
|
from,
|
|
41
53
|
to,
|
|
42
54
|
label: _.flow(
|
|
43
|
-
_.map(tag =>
|
|
55
|
+
_.map((tag) =>
|
|
44
56
|
formatMessage({
|
|
45
57
|
id: `source.${_.prop("value.type")(tag)}`,
|
|
46
|
-
defaultMessage: _.prop("value.type")(tag)
|
|
58
|
+
defaultMessage: _.prop("value.type")(tag),
|
|
47
59
|
})
|
|
48
60
|
),
|
|
49
61
|
_.join("\n")
|
|
50
|
-
)(tags)
|
|
62
|
+
)(tags),
|
|
51
63
|
}))
|
|
52
|
-
)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!_.isEmpty(relationsGraph)) {
|
|
68
|
+
const start = _.flow(
|
|
69
|
+
_.getOr([], "nodes"),
|
|
70
|
+
_.find({ id: currentId })
|
|
71
|
+
)(relationsGraph);
|
|
72
|
+
|
|
73
|
+
const useEffectMaxDepth = dfsMaxDepth(relationsGraph, start, {}, 0);
|
|
74
|
+
|
|
75
|
+
setStart(start);
|
|
76
|
+
setMaxDepth(useEffectMaxDepth);
|
|
77
|
+
if (depth === Infinity) {
|
|
78
|
+
setDepth(useEffectMaxDepth);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}, [relationsGraph]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!_.isEmpty(relationsGraph) && !_.isEmpty(start) && depth !== Infinity) {
|
|
85
|
+
const nodes = dfs(relationsGraph, start, {}, depth);
|
|
86
|
+
depth < maxDepth
|
|
87
|
+
? setLimitedRelationsGraph({
|
|
88
|
+
nodes,
|
|
89
|
+
edges: getEdges(relationsGraph, nodes),
|
|
90
|
+
})
|
|
91
|
+
: setLimitedRelationsGraph(relationsGraph);
|
|
92
|
+
}
|
|
93
|
+
}, [depth, start]);
|
|
53
94
|
|
|
54
95
|
const options = {
|
|
55
96
|
autoResize: true,
|
|
@@ -60,43 +101,43 @@ export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
|
60
101
|
direction: "UD",
|
|
61
102
|
sortMethod: "directed",
|
|
62
103
|
levelSeparation: 170,
|
|
63
|
-
edgeMinimization: false
|
|
64
|
-
}
|
|
104
|
+
edgeMinimization: false,
|
|
105
|
+
},
|
|
65
106
|
},
|
|
66
107
|
edges: {
|
|
67
108
|
color: "#000000",
|
|
68
109
|
smooth: true,
|
|
69
110
|
font: {
|
|
70
|
-
face: "Lato, Helvetica Neue, Arial, Helvetica, sans-serif"
|
|
71
|
-
}
|
|
111
|
+
face: "Lato, Helvetica Neue, Arial, Helvetica, sans-serif",
|
|
112
|
+
},
|
|
72
113
|
},
|
|
73
114
|
nodes: {
|
|
74
115
|
shape: "box",
|
|
75
116
|
margin: 10,
|
|
76
117
|
color: {
|
|
77
118
|
background: "white",
|
|
78
|
-
border: "grey"
|
|
119
|
+
border: "grey",
|
|
79
120
|
},
|
|
80
121
|
widthConstraint: {
|
|
81
122
|
maximum: 200,
|
|
82
|
-
minimum: 100
|
|
123
|
+
minimum: 100,
|
|
83
124
|
},
|
|
84
125
|
font: {
|
|
85
|
-
face: "Lato, Helvetica Neue, Arial, Helvetica, sans-serif"
|
|
86
|
-
}
|
|
126
|
+
face: "Lato, Helvetica Neue, Arial, Helvetica, sans-serif",
|
|
127
|
+
},
|
|
87
128
|
},
|
|
88
129
|
physics: {
|
|
89
130
|
hierarchicalRepulsion: {
|
|
90
|
-
nodeDistance: 250
|
|
91
|
-
}
|
|
131
|
+
nodeDistance: 250,
|
|
132
|
+
},
|
|
92
133
|
},
|
|
93
134
|
interaction: {
|
|
94
|
-
hover: true
|
|
95
|
-
}
|
|
135
|
+
hover: true,
|
|
136
|
+
},
|
|
96
137
|
};
|
|
97
138
|
|
|
98
139
|
const events = {
|
|
99
|
-
select: event => {
|
|
140
|
+
select: (event) => {
|
|
100
141
|
const { nodes, edges } = event;
|
|
101
142
|
if (!_.isEmpty(nodes)) {
|
|
102
143
|
const nodeId = _.head(nodes);
|
|
@@ -110,7 +151,7 @@ export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
|
110
151
|
);
|
|
111
152
|
}
|
|
112
153
|
},
|
|
113
|
-
hoverNode: event => {
|
|
154
|
+
hoverNode: (event) => {
|
|
114
155
|
const { node } = event;
|
|
115
156
|
if (node !== currentId) {
|
|
116
157
|
const style = _.path("canvas.frame.style")(network);
|
|
@@ -122,18 +163,30 @@ export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
|
122
163
|
const style = _.path("canvas.frame.style")(network);
|
|
123
164
|
style.setProperty("cursor", "default");
|
|
124
165
|
setNetwork(network);
|
|
125
|
-
}
|
|
166
|
+
},
|
|
126
167
|
};
|
|
127
168
|
return (
|
|
128
169
|
<>
|
|
129
|
-
{!_.isEmpty(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
{!_.isEmpty(limitedRelationsGraph?.nodes) && (
|
|
171
|
+
<>
|
|
172
|
+
<RelationGraphDepth
|
|
173
|
+
onClick={(_e) => setDepth(maxDepth)}
|
|
174
|
+
onChange={(event) => setDepth(parseInt(event.target.value))}
|
|
175
|
+
depth={depth}
|
|
176
|
+
maxDepth={maxDepth}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
<Graph
|
|
180
|
+
graph={{
|
|
181
|
+
nodes: getVisNodes(limitedRelationsGraph),
|
|
182
|
+
edges: getVisEdges(limitedRelationsGraph),
|
|
183
|
+
}}
|
|
184
|
+
options={options}
|
|
185
|
+
events={events}
|
|
186
|
+
style={{ height: "640px" }}
|
|
187
|
+
getNetwork={setNetwork}
|
|
188
|
+
/>
|
|
189
|
+
</>
|
|
137
190
|
)}
|
|
138
191
|
</>
|
|
139
192
|
);
|
|
@@ -142,9 +195,16 @@ export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
|
142
195
|
RelationGraph.propTypes = {
|
|
143
196
|
navigate: PropTypes.func,
|
|
144
197
|
currentId: PropTypes.string,
|
|
145
|
-
relationsGraph: PropTypes.object
|
|
198
|
+
relationsGraph: PropTypes.object,
|
|
199
|
+
initialDepth: PropTypes.number,
|
|
146
200
|
};
|
|
147
201
|
|
|
148
|
-
export const mapStateToProps = ({
|
|
202
|
+
export const mapStateToProps = ({
|
|
203
|
+
relationsGraph,
|
|
204
|
+
conceptGraphInitialDepth,
|
|
205
|
+
}) => ({
|
|
206
|
+
initialDepth: conceptGraphInitialDepth,
|
|
207
|
+
relationsGraph,
|
|
208
|
+
});
|
|
149
209
|
|
|
150
210
|
export default connect(mapStateToProps)(RelationGraph);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import PropTypes from "prop-types";
|
|
3
|
+
import { Button } from "semantic-ui-react";
|
|
4
|
+
import { FormattedMessage } from "react-intl";
|
|
5
|
+
|
|
6
|
+
export const RelationGraphDepth = ({ onClick, onChange, depth, maxDepth }) => {
|
|
7
|
+
return (
|
|
8
|
+
<div className="graph">
|
|
9
|
+
<label htmlFor="depth">
|
|
10
|
+
<FormattedMessage id="relationGraph.depth" /> [1-{maxDepth}]
|
|
11
|
+
</label>
|
|
12
|
+
<input
|
|
13
|
+
id="depth"
|
|
14
|
+
aria-label="depth"
|
|
15
|
+
type="number"
|
|
16
|
+
min="1"
|
|
17
|
+
max={maxDepth}
|
|
18
|
+
value={depth === Infinity ? 0 : depth}
|
|
19
|
+
onChange={onChange}
|
|
20
|
+
></input>
|
|
21
|
+
<Button disabled={depth === maxDepth} onClick={onClick}>
|
|
22
|
+
<FormattedMessage id="relationGraph.all" />
|
|
23
|
+
</Button>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
RelationGraphDepth.propTypes = {
|
|
29
|
+
onClick: PropTypes.func,
|
|
30
|
+
onChange: PropTypes.func,
|
|
31
|
+
depth: PropTypes.number,
|
|
32
|
+
maxDepth: PropTypes.number,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default RelationGraphDepth;
|
|
@@ -1,93 +1,264 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
1
2
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
+
import { mount } from "enzyme";
|
|
3
4
|
import { intl } from "@truedat/test/intl-stub";
|
|
5
|
+
import userEvent from "@testing-library/user-event";
|
|
6
|
+
import { render } from "@truedat/test/render";
|
|
7
|
+
import { waitFor } from "@testing-library/react";
|
|
4
8
|
import { RelationGraph } from "../RelationGraph";
|
|
5
9
|
|
|
6
10
|
jest.spyOn(React, "useContext").mockImplementation(() => intl);
|
|
7
11
|
|
|
12
|
+
const renderOpts = {
|
|
13
|
+
messages: {
|
|
14
|
+
en: {
|
|
15
|
+
"relationGraph.all": "All",
|
|
16
|
+
"relationGraph.depth": "Depth",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
8
21
|
describe("<RelationGraph />", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Avoid `attachTo: document.body` Warning
|
|
24
|
+
const div = document.createElement("div");
|
|
25
|
+
div.setAttribute("id", "container");
|
|
26
|
+
document.body.appendChild(div);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
const div = document.getElementById("container");
|
|
31
|
+
if (div) {
|
|
32
|
+
document.body.removeChild(div);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
9
36
|
const nodes = [
|
|
10
37
|
{
|
|
11
38
|
id: "business_concept:92",
|
|
12
|
-
|
|
39
|
+
name: "My Awesome Concept",
|
|
13
40
|
},
|
|
14
41
|
{
|
|
15
42
|
id: "business_concept:5",
|
|
16
|
-
|
|
43
|
+
name: "New Concept 4",
|
|
17
44
|
},
|
|
18
45
|
{
|
|
19
46
|
id: "business_concept:18",
|
|
20
|
-
|
|
21
|
-
}
|
|
47
|
+
name: "Float Concept",
|
|
48
|
+
},
|
|
22
49
|
];
|
|
23
50
|
|
|
24
51
|
const edges = [
|
|
25
52
|
{
|
|
26
|
-
|
|
53
|
+
id: 1,
|
|
54
|
+
source_id: "business_concept:92",
|
|
55
|
+
target_id: "business_concept:5",
|
|
27
56
|
tags: [
|
|
28
57
|
{
|
|
29
58
|
id: 3,
|
|
30
59
|
value: {
|
|
31
60
|
target_type: "business_concept",
|
|
32
|
-
type: "bc_padre"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
61
|
+
type: "bc_padre",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
35
64
|
],
|
|
36
|
-
to: "business_concept:5"
|
|
37
65
|
},
|
|
38
66
|
{
|
|
39
|
-
|
|
67
|
+
id: 2,
|
|
68
|
+
source_id: "business_concept:5",
|
|
69
|
+
target_id: "business_concept:18",
|
|
40
70
|
tags: [
|
|
41
71
|
{
|
|
42
72
|
id: 3,
|
|
43
73
|
value: {
|
|
44
74
|
target_type: "business_concept",
|
|
45
|
-
type: "bc_padre"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
75
|
+
type: "bc_padre",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
48
78
|
],
|
|
49
|
-
to: "business_concept:18"
|
|
50
79
|
},
|
|
51
80
|
{
|
|
52
|
-
|
|
81
|
+
id: 3,
|
|
82
|
+
source_id: "business_concept:18",
|
|
83
|
+
target_id: "business_concept:5",
|
|
53
84
|
tags: [
|
|
54
85
|
{
|
|
55
86
|
id: 3,
|
|
56
87
|
value: {
|
|
57
88
|
target_type: "business_concept",
|
|
58
|
-
type: "bc_padre"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
89
|
+
type: "bc_padre",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
61
92
|
],
|
|
62
|
-
|
|
63
|
-
}
|
|
93
|
+
},
|
|
64
94
|
];
|
|
65
95
|
|
|
66
96
|
const relationsGraph = {
|
|
67
97
|
nodes,
|
|
68
|
-
edges
|
|
98
|
+
edges,
|
|
69
99
|
};
|
|
70
100
|
|
|
71
101
|
const defaultProps = {
|
|
72
|
-
currentId: "
|
|
102
|
+
currentId: "business_concept:92",
|
|
73
103
|
navigate: jest.fn(),
|
|
74
|
-
relationsGraph: {}
|
|
104
|
+
relationsGraph: {},
|
|
75
105
|
};
|
|
76
106
|
|
|
77
|
-
it("
|
|
107
|
+
it("renders Graph on non-empty relationsGraph", () => {
|
|
78
108
|
const props = { ...defaultProps, relationsGraph };
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
|
|
110
|
+
const wrapper = mount(<RelationGraph {...props} />, {
|
|
111
|
+
attachTo: document.getElementById("container"),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const graphComponent = wrapper.find("Graph");
|
|
115
|
+
expect(graphComponent.length).toBe(1);
|
|
116
|
+
const limitedGraph = graphComponent.prop("graph");
|
|
117
|
+
|
|
118
|
+
expect(
|
|
119
|
+
_.isUndefined(
|
|
120
|
+
_.find({ id: nodes[0].id, label: nodes[0].name }, limitedGraph.nodes)
|
|
121
|
+
) &&
|
|
122
|
+
_.isUndefined(
|
|
123
|
+
_.find({ id: nodes[1].id, label: nodes[1].name }, limitedGraph.nodes)
|
|
124
|
+
) &&
|
|
125
|
+
_.isUndefined(
|
|
126
|
+
_.find({ id: nodes[2].id, label: nodes[2].name }, limitedGraph.nodes)
|
|
127
|
+
)
|
|
128
|
+
).toBeFalsy();
|
|
129
|
+
|
|
130
|
+
expect(
|
|
131
|
+
_.isUndefined(
|
|
132
|
+
_.find(
|
|
133
|
+
{
|
|
134
|
+
from: edges[0].source_id,
|
|
135
|
+
to: edges[0].target_id,
|
|
136
|
+
label: `source.${edges[0].tags[0].value.type}`,
|
|
137
|
+
},
|
|
138
|
+
limitedGraph.edges
|
|
139
|
+
)
|
|
140
|
+
) &&
|
|
141
|
+
_.isUndefined(
|
|
142
|
+
_.find(
|
|
143
|
+
{
|
|
144
|
+
from: edges[1].source_id,
|
|
145
|
+
to: edges[1].target_id,
|
|
146
|
+
label: `source.${edges[1].tags[1].value.type}`,
|
|
147
|
+
},
|
|
148
|
+
limitedGraph.edges
|
|
149
|
+
)
|
|
150
|
+
) &&
|
|
151
|
+
_.isUndefined(
|
|
152
|
+
_.find(
|
|
153
|
+
{
|
|
154
|
+
from: edges[2].source_id,
|
|
155
|
+
to: edges[2].target_id,
|
|
156
|
+
label: `source.${edges[2].tags[2].value.type}`,
|
|
157
|
+
},
|
|
158
|
+
limitedGraph.edges
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
).toBeFalsy();
|
|
81
162
|
});
|
|
82
163
|
|
|
83
|
-
it("
|
|
84
|
-
const wrapper =
|
|
164
|
+
it("does not render Graph on empty relationsGraph", () => {
|
|
165
|
+
const wrapper = mount(<RelationGraph {...defaultProps} />, {
|
|
166
|
+
attachTo: document.getElementById("container"),
|
|
167
|
+
});
|
|
85
168
|
expect(wrapper.find("Graph").length).toBe(0);
|
|
86
169
|
});
|
|
87
170
|
|
|
88
|
-
it("
|
|
171
|
+
it("sets initial depth", () => {
|
|
172
|
+
const initialDepth = 1;
|
|
173
|
+
const props = { ...defaultProps, relationsGraph, initialDepth };
|
|
174
|
+
const wrapper = mount(<RelationGraph {...props} />, {
|
|
175
|
+
attachTo: document.getElementById("container"),
|
|
176
|
+
});
|
|
177
|
+
const input = wrapper.find("input");
|
|
178
|
+
expect(input.prop("value")).toBe(initialDepth);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("no initial depth defaults to max graph depth", () => {
|
|
89
182
|
const props = { ...defaultProps, relationsGraph };
|
|
90
|
-
const wrapper =
|
|
91
|
-
|
|
183
|
+
const wrapper = mount(<RelationGraph {...props} />, {
|
|
184
|
+
attachTo: document.getElementById("container"),
|
|
185
|
+
});
|
|
186
|
+
const input = wrapper.find("input");
|
|
187
|
+
expect(input.prop("value")).toBe(3);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("button sets max depth", async () => {
|
|
191
|
+
const initialDepth = 1;
|
|
192
|
+
const props = { ...defaultProps, relationsGraph, initialDepth };
|
|
193
|
+
const { getByRole, getByLabelText } = render(
|
|
194
|
+
<RelationGraph {...props} />,
|
|
195
|
+
renderOpts
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
userEvent.click(await getByRole("button"));
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(parseInt(getByLabelText("depth").value)).toBe(3);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("limits graph", async () => {
|
|
205
|
+
const props = { ...defaultProps, relationsGraph };
|
|
206
|
+
const wrapper = await mount(<RelationGraph {...props} />, {
|
|
207
|
+
attachTo: document.getElementById("container"),
|
|
208
|
+
});
|
|
209
|
+
const input = wrapper.find("input");
|
|
210
|
+
input.simulate("change", { target: { value: 2 } });
|
|
211
|
+
const limitedGraph = wrapper.find("Graph").prop("graph");
|
|
212
|
+
|
|
213
|
+
expect(
|
|
214
|
+
_.isUndefined(
|
|
215
|
+
_.find({ id: nodes[0].id, label: nodes[0].name }, limitedGraph.nodes)
|
|
216
|
+
) &&
|
|
217
|
+
_.isUndefined(
|
|
218
|
+
_.find({ id: nodes[1].id, label: nodes[1].name }, limitedGraph.nodes)
|
|
219
|
+
)
|
|
220
|
+
).toBeFalsy();
|
|
221
|
+
|
|
222
|
+
expect(
|
|
223
|
+
_.isUndefined(
|
|
224
|
+
_.find({ id: nodes[2].id, label: nodes[2].name }, limitedGraph.nodes)
|
|
225
|
+
)
|
|
226
|
+
).toBeTruthy();
|
|
227
|
+
|
|
228
|
+
expect(
|
|
229
|
+
_.isUndefined(
|
|
230
|
+
_.find(
|
|
231
|
+
{
|
|
232
|
+
from: edges[0].source_id,
|
|
233
|
+
to: edges[0].target_id,
|
|
234
|
+
label: `source.${edges[0].tags[0].value.type}`,
|
|
235
|
+
},
|
|
236
|
+
limitedGraph.edges
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
).toBeFalsy();
|
|
240
|
+
|
|
241
|
+
expect(
|
|
242
|
+
_.isUndefined(
|
|
243
|
+
_.find(
|
|
244
|
+
{
|
|
245
|
+
from: edges[1].source_id,
|
|
246
|
+
to: edges[1].target_id,
|
|
247
|
+
label: `source.${edges[1].tags[0].value.type}`,
|
|
248
|
+
},
|
|
249
|
+
limitedGraph.edges
|
|
250
|
+
)
|
|
251
|
+
) &&
|
|
252
|
+
_.isUndefined(
|
|
253
|
+
_.find(
|
|
254
|
+
{
|
|
255
|
+
from: edges[2].source_id,
|
|
256
|
+
to: edges[2].target_id,
|
|
257
|
+
label: `source.${edges[2].tags[0].value.type}`,
|
|
258
|
+
},
|
|
259
|
+
limitedGraph.edges
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
).toBeTruthy();
|
|
92
263
|
});
|
|
93
264
|
});
|
package/src/messages/en.js
CHANGED
|
@@ -8,6 +8,8 @@ export default {
|
|
|
8
8
|
"relation.actions.delete.confirmation.content":
|
|
9
9
|
"The relation will be deleted. Are you sure?",
|
|
10
10
|
"ingest.relatedTo.dataStructure": "Related to data structure",
|
|
11
|
+
"relationGraph.all": "All",
|
|
12
|
+
"relationGraph.depth": "Depth",
|
|
11
13
|
"relationTags.header": "Relation Types",
|
|
12
14
|
"relationTags.subheader": "In this section you can manage Relation Types",
|
|
13
15
|
"relationTag.actions.delete.confirmation.header": "Delete Relation Type",
|
package/src/messages/es.js
CHANGED
|
@@ -4,12 +4,13 @@ export default {
|
|
|
4
4
|
"links.actions.create": "Añadir vinculación",
|
|
5
5
|
"relations.actions.create": "Añadir relación",
|
|
6
6
|
"relations.empty": "No se han encontrado relaciones",
|
|
7
|
-
|
|
8
7
|
"relations.relationType": "Tipos de relaciones",
|
|
9
8
|
"relation.actions.delete.confirmation.header": "Borrar Relación",
|
|
10
9
|
"relation.actions.delete.confirmation.content":
|
|
11
10
|
"La relación será eliminada. ¿Estás seguro?",
|
|
12
11
|
"ingest.relatedTo.dataStructure": "Relacionado con estructura",
|
|
12
|
+
"relationGraph.all": "Todos",
|
|
13
|
+
"relationGraph.depth": "Profundidad",
|
|
13
14
|
"relationTags.header": "Tipos de Relaciones",
|
|
14
15
|
"relationTags.subheader": "Aquí podrás gestionar los tipos de relaciones",
|
|
15
16
|
"relationTag.actions.delete.confirmation.header": "Borrar Tipo de Relación",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
|
|
3
|
+
const getNeighbours = (relationsGraph, start) => {
|
|
4
|
+
return _.concat(
|
|
5
|
+
// Up
|
|
6
|
+
_.flow(
|
|
7
|
+
_.filter((edge) => edge.target_id === start.id),
|
|
8
|
+
_.flatMap((edge) => _.find({ id: edge.source_id }, relationsGraph.nodes))
|
|
9
|
+
)(relationsGraph.edges),
|
|
10
|
+
// Down
|
|
11
|
+
_.flow(
|
|
12
|
+
_.filter((edge) => edge.source_id === start.id),
|
|
13
|
+
_.flatMap((edge) => _.find({ id: edge.target_id }, relationsGraph.nodes))
|
|
14
|
+
)(relationsGraph.edges)
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getEdges = (relationsGraph, nodes) => {
|
|
19
|
+
const nodeIds = Object.keys(nodes);
|
|
20
|
+
return _.filter(
|
|
21
|
+
(edge) =>
|
|
22
|
+
nodeIds.includes(edge.source_id) && nodeIds.includes(edge.target_id),
|
|
23
|
+
relationsGraph.edges
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const dfs = (relationsGraph, start, visited, maxDepth) => {
|
|
28
|
+
if (Object.keys(visited).includes(start.id) || maxDepth === 0) return {};
|
|
29
|
+
|
|
30
|
+
return _.reduce(
|
|
31
|
+
(acc, neighbour) => ({
|
|
32
|
+
...acc,
|
|
33
|
+
...dfs(
|
|
34
|
+
relationsGraph,
|
|
35
|
+
neighbour,
|
|
36
|
+
{ ...visited, [start.id]: start },
|
|
37
|
+
maxDepth - 1
|
|
38
|
+
),
|
|
39
|
+
}),
|
|
40
|
+
{ [start.id]: start }
|
|
41
|
+
)(getNeighbours(relationsGraph, start));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const dfsMaxDepth = (relationsGraph, start, visited, currDepth) => {
|
|
45
|
+
if (Object.keys(visited).includes(start.id)) return currDepth;
|
|
46
|
+
|
|
47
|
+
return _.reduce(
|
|
48
|
+
(currMax, neighbour) =>
|
|
49
|
+
Math.max(
|
|
50
|
+
currMax,
|
|
51
|
+
dfsMaxDepth(
|
|
52
|
+
relationsGraph,
|
|
53
|
+
neighbour,
|
|
54
|
+
{ ...visited, [start.id]: start },
|
|
55
|
+
currDepth + 1
|
|
56
|
+
)
|
|
57
|
+
),
|
|
58
|
+
0
|
|
59
|
+
)(getNeighbours(relationsGraph, start));
|
|
60
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
.graph{
|
|
3
|
+
|
|
4
|
+
div&{
|
|
5
|
+
position: absolute;
|
|
6
|
+
right: 0;
|
|
7
|
+
z-index: 1;
|
|
8
|
+
padding-right: 1rem;
|
|
9
|
+
|
|
10
|
+
input[type=number] {
|
|
11
|
+
vertical-align: top;
|
|
12
|
+
line-height: 34px;
|
|
13
|
+
width: 105px;
|
|
14
|
+
font-size: 20px;
|
|
15
|
+
text-align: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
label {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: -20px;
|
|
21
|
+
}
|
|
22
|
+
.ui.button {
|
|
23
|
+
vertical-align: top;
|
|
24
|
+
width: 80px;
|
|
25
|
+
border-radius: 0 4px 4px 0;
|
|
26
|
+
border: 1px solid grey;
|
|
27
|
+
border-left: 0;
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
-
|
|
3
|
-
exports[`<RelationGraph /> matches the latest snapshot 1`] = `
|
|
4
|
-
<Fragment>
|
|
5
|
-
<Graph
|
|
6
|
-
events={
|
|
7
|
-
Object {
|
|
8
|
-
"blurNode": [Function],
|
|
9
|
-
"hoverNode": [Function],
|
|
10
|
-
"select": [Function],
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
getNetwork={[Function]}
|
|
14
|
-
graph={
|
|
15
|
-
Object {
|
|
16
|
-
"edges": Array [
|
|
17
|
-
Object {
|
|
18
|
-
"from": undefined,
|
|
19
|
-
"label": "source.bc_padre",
|
|
20
|
-
"to": undefined,
|
|
21
|
-
},
|
|
22
|
-
Object {
|
|
23
|
-
"from": undefined,
|
|
24
|
-
"label": "source.bc_padre",
|
|
25
|
-
"to": undefined,
|
|
26
|
-
},
|
|
27
|
-
Object {
|
|
28
|
-
"from": undefined,
|
|
29
|
-
"label": "source.bc_padre",
|
|
30
|
-
"to": undefined,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
"nodes": Array [
|
|
34
|
-
Object {
|
|
35
|
-
"color": null,
|
|
36
|
-
"font": null,
|
|
37
|
-
"id": "business_concept:92",
|
|
38
|
-
"label": undefined,
|
|
39
|
-
},
|
|
40
|
-
Object {
|
|
41
|
-
"color": null,
|
|
42
|
-
"font": null,
|
|
43
|
-
"id": "business_concept:5",
|
|
44
|
-
"label": undefined,
|
|
45
|
-
},
|
|
46
|
-
Object {
|
|
47
|
-
"color": null,
|
|
48
|
-
"font": null,
|
|
49
|
-
"id": "business_concept:18",
|
|
50
|
-
"label": undefined,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
options={
|
|
56
|
-
Object {
|
|
57
|
-
"autoResize": true,
|
|
58
|
-
"edges": Object {
|
|
59
|
-
"color": "#000000",
|
|
60
|
-
"font": Object {
|
|
61
|
-
"face": "Lato, Helvetica Neue, Arial, Helvetica, sans-serif",
|
|
62
|
-
},
|
|
63
|
-
"smooth": true,
|
|
64
|
-
},
|
|
65
|
-
"height": "99%",
|
|
66
|
-
"interaction": Object {
|
|
67
|
-
"hover": true,
|
|
68
|
-
},
|
|
69
|
-
"layout": Object {
|
|
70
|
-
"hierarchical": Object {
|
|
71
|
-
"direction": "UD",
|
|
72
|
-
"edgeMinimization": false,
|
|
73
|
-
"levelSeparation": 170,
|
|
74
|
-
"sortMethod": "directed",
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
"nodes": Object {
|
|
78
|
-
"color": Object {
|
|
79
|
-
"background": "white",
|
|
80
|
-
"border": "grey",
|
|
81
|
-
},
|
|
82
|
-
"font": Object {
|
|
83
|
-
"face": "Lato, Helvetica Neue, Arial, Helvetica, sans-serif",
|
|
84
|
-
},
|
|
85
|
-
"margin": 10,
|
|
86
|
-
"shape": "box",
|
|
87
|
-
"widthConstraint": Object {
|
|
88
|
-
"maximum": 200,
|
|
89
|
-
"minimum": 100,
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
"physics": Object {
|
|
93
|
-
"hierarchicalRepulsion": Object {
|
|
94
|
-
"nodeDistance": 250,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
"width": "99%",
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
style={
|
|
101
|
-
Object {
|
|
102
|
-
"height": "640px",
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/>
|
|
106
|
-
</Fragment>
|
|
107
|
-
`;
|