@truedat/lm 4.42.2 → 4.42.5
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 -5
- package/src/components/RelationGraph.js +15 -78
- package/src/components/RelationGraphDepth.js +18 -20
- package/src/components/index.js +2 -0
- package/src/selectors/__tests__/relationGraphTraversal.spec.js +188 -0
- package/src/selectors/index.js +1 -0
- package/src/selectors/relationGraphTraversal.js +113 -0
- package/src/styles/relationGraph.less +16 -29
- package/src/components/__tests__/RelationGraph.spec.js +0 -264
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/lm",
|
|
3
|
-
"version": "4.42.
|
|
3
|
+
"version": "4.42.5",
|
|
4
4
|
"description": "Truedat Link Manager",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"jsnext:main": "src/index.js",
|
|
@@ -33,13 +33,12 @@
|
|
|
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.42.
|
|
36
|
+
"@truedat/test": "4.42.5",
|
|
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",
|
|
43
42
|
"enzyme": "^3.11.0",
|
|
44
43
|
"enzyme-adapter-react-16": "^1.15.6",
|
|
45
44
|
"enzyme-to-json": "^3.6.2",
|
|
@@ -84,12 +83,13 @@
|
|
|
84
83
|
]
|
|
85
84
|
},
|
|
86
85
|
"dependencies": {
|
|
87
|
-
"@truedat/core": "4.42.
|
|
86
|
+
"@truedat/core": "4.42.5",
|
|
88
87
|
"path-to-regexp": "^1.7.0",
|
|
89
88
|
"prop-types": "^15.7.2",
|
|
90
89
|
"react-graph-vis": "1.0.5",
|
|
91
90
|
"react-hook-form": "^6.15.8",
|
|
92
91
|
"react-intl": "^5.20.10",
|
|
92
|
+
"react-rangeslider": "^2.2.0",
|
|
93
93
|
"react-redux": "^7.2.4",
|
|
94
94
|
"react-router-dom": "^5.2.0",
|
|
95
95
|
"redux": "^4.1.1",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
"react-dom": ">= 16.8.6 < 17",
|
|
102
102
|
"semantic-ui-react": ">= 0.88.2 < 2.1"
|
|
103
103
|
},
|
|
104
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "77641b0e3a7555a6f6c4c0649ff7b6164292b4b0"
|
|
105
105
|
}
|
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
|
-
import React, {
|
|
3
|
-
import { connect } from "react-redux";
|
|
2
|
+
import React, { useState } from "react";
|
|
4
3
|
import { useIntl } from "react-intl";
|
|
5
4
|
import PropTypes from "prop-types";
|
|
6
5
|
import Graph from "react-graph-vis";
|
|
7
|
-
import "../styles/relationGraph.less";
|
|
8
|
-
import { dfsMaxDepth, dfs, getEdges } from "../services/relationGraphTraversal";
|
|
9
|
-
import RelationGraphDepth from "./RelationGraphDepth";
|
|
10
6
|
|
|
11
|
-
export const RelationGraph = ({
|
|
12
|
-
navigate,
|
|
13
|
-
currentId,
|
|
14
|
-
relationsGraph,
|
|
15
|
-
initialDepth,
|
|
16
|
-
}) => {
|
|
7
|
+
export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
|
|
17
8
|
const { formatMessage } = useIntl();
|
|
18
9
|
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();
|
|
23
10
|
|
|
24
|
-
const
|
|
11
|
+
const nodes = _.flow(
|
|
25
12
|
_.get("nodes"),
|
|
26
13
|
_.map(({ id, name: label }) => ({
|
|
27
14
|
id,
|
|
@@ -44,9 +31,9 @@ export const RelationGraph = ({
|
|
|
44
31
|
}
|
|
45
32
|
: null,
|
|
46
33
|
}))
|
|
47
|
-
);
|
|
34
|
+
)(relationsGraph);
|
|
48
35
|
|
|
49
|
-
const
|
|
36
|
+
const edges = _.flow(
|
|
50
37
|
_.get("edges"),
|
|
51
38
|
_.map(({ source_id: from, target_id: to, tags }) => ({
|
|
52
39
|
from,
|
|
@@ -61,36 +48,7 @@ export const RelationGraph = ({
|
|
|
61
48
|
_.join("\n")
|
|
62
49
|
)(tags),
|
|
63
50
|
}))
|
|
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]);
|
|
51
|
+
)(relationsGraph);
|
|
94
52
|
|
|
95
53
|
const options = {
|
|
96
54
|
autoResize: true,
|
|
@@ -167,26 +125,14 @@ export const RelationGraph = ({
|
|
|
167
125
|
};
|
|
168
126
|
return (
|
|
169
127
|
<>
|
|
170
|
-
{!_.isEmpty(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
</>
|
|
128
|
+
{!_.isEmpty(relationsGraph) && (
|
|
129
|
+
<Graph
|
|
130
|
+
graph={{ nodes, edges }}
|
|
131
|
+
options={options}
|
|
132
|
+
events={events}
|
|
133
|
+
style={{ height: "640px" }}
|
|
134
|
+
getNetwork={setNetwork}
|
|
135
|
+
/>
|
|
190
136
|
)}
|
|
191
137
|
</>
|
|
192
138
|
);
|
|
@@ -196,15 +142,6 @@ RelationGraph.propTypes = {
|
|
|
196
142
|
navigate: PropTypes.func,
|
|
197
143
|
currentId: PropTypes.string,
|
|
198
144
|
relationsGraph: PropTypes.object,
|
|
199
|
-
initialDepth: PropTypes.number,
|
|
200
145
|
};
|
|
201
146
|
|
|
202
|
-
export
|
|
203
|
-
relationsGraph,
|
|
204
|
-
conceptGraphInitialDepth,
|
|
205
|
-
}) => ({
|
|
206
|
-
initialDepth: conceptGraphInitialDepth,
|
|
207
|
-
relationsGraph,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
export default connect(mapStateToProps)(RelationGraph);
|
|
147
|
+
export default RelationGraph;
|
|
@@ -1,32 +1,30 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
1
2
|
import React from "react";
|
|
2
3
|
import PropTypes from "prop-types";
|
|
3
|
-
import {
|
|
4
|
+
import { Segment } from "semantic-ui-react";
|
|
5
|
+
import Slider from "react-rangeslider";
|
|
6
|
+
import "react-rangeslider/lib/index.css";
|
|
4
7
|
import { FormattedMessage } from "react-intl";
|
|
5
8
|
|
|
6
|
-
export const RelationGraphDepth = ({
|
|
7
|
-
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type="number"
|
|
16
|
-
min="1"
|
|
17
|
-
max={maxDepth}
|
|
18
|
-
value={depth === Infinity ? 0 : depth}
|
|
9
|
+
export const RelationGraphDepth = ({ onChange, depth, maxDepth }) => {
|
|
10
|
+
return _.isUndefined(maxDepth) ? null : (
|
|
11
|
+
<Segment className="graph-depth">
|
|
12
|
+
<FormattedMessage id="relationGraph.depth" />
|
|
13
|
+
<div>{depth}</div>
|
|
14
|
+
<Slider
|
|
15
|
+
value={depth}
|
|
16
|
+
orientation="vertical"
|
|
17
|
+
reverse={true}
|
|
19
18
|
onChange={onChange}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</
|
|
19
|
+
min={1}
|
|
20
|
+
max={maxDepth}
|
|
21
|
+
tooltip={false}
|
|
22
|
+
/>
|
|
23
|
+
</Segment>
|
|
25
24
|
);
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
RelationGraphDepth.propTypes = {
|
|
29
|
-
onClick: PropTypes.func,
|
|
30
28
|
onChange: PropTypes.func,
|
|
31
29
|
depth: PropTypes.number,
|
|
32
30
|
maxDepth: PropTypes.number,
|
package/src/components/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import StructureLinks from "./StructureLinks";
|
|
|
12
12
|
import StructureRelationForm from "./StructureRelationForm";
|
|
13
13
|
import TagTypeSelector from "./TagTypeSelector";
|
|
14
14
|
import RelationGraph from "./RelationGraph";
|
|
15
|
+
import RelationGraphDepth from "./RelationGraphDepth";
|
|
15
16
|
import RelationRoutes from "./RelationRoutes";
|
|
16
17
|
import RelationTags from "./RelationTags";
|
|
17
18
|
import RelationTagCards from "./RelationTagCards";
|
|
@@ -32,6 +33,7 @@ export {
|
|
|
32
33
|
StructureRelationForm,
|
|
33
34
|
TagTypeSelector,
|
|
34
35
|
RelationGraph,
|
|
36
|
+
RelationGraphDepth,
|
|
35
37
|
RelationRoutes,
|
|
36
38
|
RelationTags,
|
|
37
39
|
RelationTagCards,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
import { selectLimitedGraph } from "..";
|
|
3
|
+
import { selectMaxDepth } from "../relationGraphTraversal";
|
|
4
|
+
|
|
5
|
+
const nodes = [
|
|
6
|
+
{
|
|
7
|
+
id: "business_concept:92",
|
|
8
|
+
name: "My Awesome Concept",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "business_concept:5",
|
|
12
|
+
name: "New Concept 4",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "business_concept:18",
|
|
16
|
+
name: "Float Concept",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const edges = [
|
|
21
|
+
{
|
|
22
|
+
id: 1,
|
|
23
|
+
source_id: "business_concept:92",
|
|
24
|
+
target_id: "business_concept:5",
|
|
25
|
+
tags: [
|
|
26
|
+
{
|
|
27
|
+
id: 3,
|
|
28
|
+
value: {
|
|
29
|
+
target_type: "business_concept",
|
|
30
|
+
type: "bc_padre",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 2,
|
|
37
|
+
source_id: "business_concept:5",
|
|
38
|
+
target_id: "business_concept:18",
|
|
39
|
+
tags: [
|
|
40
|
+
{
|
|
41
|
+
id: 3,
|
|
42
|
+
value: {
|
|
43
|
+
target_type: "business_concept",
|
|
44
|
+
type: "bc_padre",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 3,
|
|
51
|
+
source_id: "business_concept:18",
|
|
52
|
+
target_id: "business_concept:5",
|
|
53
|
+
tags: [
|
|
54
|
+
{
|
|
55
|
+
id: 3,
|
|
56
|
+
value: {
|
|
57
|
+
target_type: "business_concept",
|
|
58
|
+
type: "bc_padre",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const relationsGraph = {
|
|
66
|
+
nodes,
|
|
67
|
+
edges,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
describe("selectors: relationGraphTraversal", () => {
|
|
71
|
+
const currentId = "business_concept:92";
|
|
72
|
+
|
|
73
|
+
it("gets Graph on non-empty relationsGraph", () => {
|
|
74
|
+
const limitedGraph = selectLimitedGraph({ relationsGraph }, currentId, 3);
|
|
75
|
+
|
|
76
|
+
expect(
|
|
77
|
+
_.isUndefined(
|
|
78
|
+
_.find({ id: nodes[0].id, name: nodes[0].name }, limitedGraph.nodes)
|
|
79
|
+
) &&
|
|
80
|
+
_.isUndefined(
|
|
81
|
+
_.find({ id: nodes[1].id, name: nodes[1].name }, limitedGraph.nodes)
|
|
82
|
+
) &&
|
|
83
|
+
_.isUndefined(
|
|
84
|
+
_.find({ id: nodes[2].id, name: nodes[2].name }, limitedGraph.nodes)
|
|
85
|
+
)
|
|
86
|
+
).toBeFalsy();
|
|
87
|
+
|
|
88
|
+
expect(
|
|
89
|
+
_.isUndefined(
|
|
90
|
+
_.find(
|
|
91
|
+
{
|
|
92
|
+
source_id: edges[0].source_id,
|
|
93
|
+
target_id: edges[0].target_id,
|
|
94
|
+
},
|
|
95
|
+
limitedGraph.edges
|
|
96
|
+
)
|
|
97
|
+
) &&
|
|
98
|
+
_.isUndefined(
|
|
99
|
+
_.find(
|
|
100
|
+
{
|
|
101
|
+
source_id: edges[1].source_id,
|
|
102
|
+
target_id: edges[1].target_id,
|
|
103
|
+
},
|
|
104
|
+
limitedGraph.edges
|
|
105
|
+
)
|
|
106
|
+
) &&
|
|
107
|
+
_.isUndefined(
|
|
108
|
+
_.find(
|
|
109
|
+
{
|
|
110
|
+
source_id: edges[2].source_id,
|
|
111
|
+
target_id: edges[2].target_id,
|
|
112
|
+
},
|
|
113
|
+
limitedGraph.edges
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
).toBeFalsy();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("gets Graph on empty relationsGraph", () => {
|
|
120
|
+
const limitedGraph = selectLimitedGraph(
|
|
121
|
+
{ relationsGraph: {} },
|
|
122
|
+
currentId,
|
|
123
|
+
3
|
|
124
|
+
);
|
|
125
|
+
expect(limitedGraph).toStrictEqual({});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("gets depth limited graph", () => {
|
|
129
|
+
const limitedGraph = selectLimitedGraph({ relationsGraph }, currentId, 2);
|
|
130
|
+
|
|
131
|
+
expect(
|
|
132
|
+
_.isUndefined(
|
|
133
|
+
_.find({ id: nodes[0].id, name: nodes[0].name }, limitedGraph.nodes)
|
|
134
|
+
) &&
|
|
135
|
+
_.isUndefined(
|
|
136
|
+
_.find({ id: nodes[1].id, name: nodes[1].name }, limitedGraph.nodes)
|
|
137
|
+
)
|
|
138
|
+
).toBeFalsy();
|
|
139
|
+
|
|
140
|
+
expect(
|
|
141
|
+
_.isUndefined(
|
|
142
|
+
_.find({ id: nodes[2].id, name: nodes[2].name }, limitedGraph.nodes)
|
|
143
|
+
)
|
|
144
|
+
).toBeTruthy();
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
_.isUndefined(
|
|
148
|
+
_.find(
|
|
149
|
+
{
|
|
150
|
+
source_id: edges[0].source_id,
|
|
151
|
+
target_id: edges[0].target_id,
|
|
152
|
+
},
|
|
153
|
+
limitedGraph.edges
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
).toBeFalsy();
|
|
157
|
+
|
|
158
|
+
expect(
|
|
159
|
+
_.isUndefined(
|
|
160
|
+
_.find(
|
|
161
|
+
{
|
|
162
|
+
source_id: edges[1].source_id,
|
|
163
|
+
target_id: edges[1].target_id,
|
|
164
|
+
},
|
|
165
|
+
limitedGraph.edges
|
|
166
|
+
)
|
|
167
|
+
) &&
|
|
168
|
+
_.isUndefined(
|
|
169
|
+
_.find(
|
|
170
|
+
{
|
|
171
|
+
source_id: edges[2].source_id,
|
|
172
|
+
target_id: edges[2].target_id,
|
|
173
|
+
},
|
|
174
|
+
limitedGraph.edges
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
).toBeTruthy();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("get graph max depth", () => {
|
|
181
|
+
const start = {
|
|
182
|
+
id: "business_concept:92",
|
|
183
|
+
name: "My Awesome Concept",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
expect(selectMaxDepth({ relationsGraph }, currentId, start)).toBe(3);
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/selectors/index.js
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* eslint-disable fp/no-mutation */
|
|
2
|
+
/* eslint-disable fp/no-let */
|
|
3
|
+
import _ from "lodash/fp";
|
|
4
|
+
import { createSelector } from "reselect";
|
|
5
|
+
|
|
6
|
+
const selectStart = createSelector(
|
|
7
|
+
(state) => state.relationsGraph,
|
|
8
|
+
(state, currentId) => currentId,
|
|
9
|
+
(relationsGraph, currentId) => {
|
|
10
|
+
return (
|
|
11
|
+
_.flow(_.getOr([], "nodes"), _.find({ id: currentId }))(relationsGraph) ||
|
|
12
|
+
{}
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const selectMaxDepth = createSelector(
|
|
18
|
+
(state) => state.relationsGraph,
|
|
19
|
+
(state, currentId) => selectStart(state, currentId),
|
|
20
|
+
(relationsGraph, start) => bfsMaxDepth(relationsGraph, start)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const selectLimitedGraph = createSelector(
|
|
24
|
+
(state) => state.relationsGraph,
|
|
25
|
+
(state, currentId) => selectStart(state, currentId),
|
|
26
|
+
(state, currentId) => selectMaxDepth(state, currentId),
|
|
27
|
+
(state, currentId, depth) => depth,
|
|
28
|
+
(relationsGraph, start, maxDepth, depth) =>
|
|
29
|
+
getLimitedGraph(relationsGraph, start, maxDepth, depth)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const getLimitedGraph = (relationsGraph, start, maxDepth, depth) => {
|
|
33
|
+
if (!_.isEmpty(relationsGraph) && !_.isEmpty(start) && depth !== Infinity) {
|
|
34
|
+
const nodes = dfs(relationsGraph, start, {}, depth);
|
|
35
|
+
return depth < maxDepth
|
|
36
|
+
? {
|
|
37
|
+
nodes,
|
|
38
|
+
edges: getEdges(relationsGraph, nodes),
|
|
39
|
+
}
|
|
40
|
+
: relationsGraph;
|
|
41
|
+
} else {
|
|
42
|
+
return relationsGraph;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const getNeighbours = (relationsGraph, start) => {
|
|
47
|
+
const neighbours = _.concat(
|
|
48
|
+
// Up
|
|
49
|
+
_.flow(
|
|
50
|
+
_.filter((edge) => edge.target_id === start.id),
|
|
51
|
+
_.flatMap((edge) => _.find({ id: edge.source_id }, relationsGraph.nodes))
|
|
52
|
+
)(relationsGraph.edges),
|
|
53
|
+
// Down
|
|
54
|
+
_.flow(
|
|
55
|
+
_.filter((edge) => edge.source_id === start.id),
|
|
56
|
+
_.flatMap((edge) => _.find({ id: edge.target_id }, relationsGraph.nodes))
|
|
57
|
+
)(relationsGraph.edges)
|
|
58
|
+
);
|
|
59
|
+
return neighbours;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getEdges = (relationsGraph, nodes) => {
|
|
63
|
+
const nodeIds = Object.keys(nodes);
|
|
64
|
+
return _.filter(
|
|
65
|
+
(edge) =>
|
|
66
|
+
nodeIds.includes(edge.source_id) && nodeIds.includes(edge.target_id),
|
|
67
|
+
relationsGraph.edges
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const dfs = (relationsGraph, start, visited, maxDepth) => {
|
|
72
|
+
if (Object.keys(visited).includes(start.id) || maxDepth === 0) return {};
|
|
73
|
+
|
|
74
|
+
return _.reduce(
|
|
75
|
+
(acc, neighbour) => ({
|
|
76
|
+
...acc,
|
|
77
|
+
...dfs(
|
|
78
|
+
relationsGraph,
|
|
79
|
+
neighbour,
|
|
80
|
+
{ ...visited, [start.id]: start },
|
|
81
|
+
maxDepth - 1
|
|
82
|
+
),
|
|
83
|
+
}),
|
|
84
|
+
{ [start.id]: start }
|
|
85
|
+
)(getNeighbours(relationsGraph, start));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* Shortest Path (Unweighted Graph)
|
|
90
|
+
* If the graph is unweighed, then finding the shortest path is easy: we can
|
|
91
|
+
* use the breadth-first search algorithm. For a weighted graph, we can use Dijkstra's algorithm.
|
|
92
|
+
* https://aquarchitect.github.io/swift-algorithm-club/Shortest%20Path%20%28Unweighted%29/
|
|
93
|
+
*/
|
|
94
|
+
const bfsMaxDepth = (relationsGraph, start) => {
|
|
95
|
+
let queue = [];
|
|
96
|
+
let nodesDistance = {};
|
|
97
|
+
|
|
98
|
+
queue.push(start);
|
|
99
|
+
nodesDistance[start.id] = 1;
|
|
100
|
+
|
|
101
|
+
let current = start;
|
|
102
|
+
while (!_.isEmpty(current)) {
|
|
103
|
+
current = queue.shift() || {};
|
|
104
|
+
for (let neighbour of getNeighbours(relationsGraph, current)) {
|
|
105
|
+
if (!nodesDistance[neighbour.id]) {
|
|
106
|
+
queue.push(neighbour);
|
|
107
|
+
nodesDistance[neighbour.id] = nodesDistance[current.id] + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Math.max(...Object.values(nodesDistance));
|
|
113
|
+
};
|
|
@@ -1,34 +1,21 @@
|
|
|
1
|
+
.graph-depth {
|
|
2
|
+
position: absolute !important;
|
|
3
|
+
z-index: 1;
|
|
4
|
+
margin-right: 0px;
|
|
1
5
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
}
|
|
6
|
+
div {
|
|
7
|
+
text-align: center;
|
|
8
|
+
font-weight: bold;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
30
11
|
|
|
12
|
+
.graph-depth .rangeslider-vertical {
|
|
13
|
+
.rangeslider__fill {
|
|
14
|
+
background-color: #ed5c17;
|
|
15
|
+
border: 1px solid gray;
|
|
31
16
|
}
|
|
32
17
|
|
|
33
|
-
|
|
18
|
+
.rangeslider__handle {
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
}
|
|
34
21
|
}
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import _ from "lodash/fp";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { mount } from "enzyme";
|
|
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";
|
|
8
|
-
import { RelationGraph } from "../RelationGraph";
|
|
9
|
-
|
|
10
|
-
jest.spyOn(React, "useContext").mockImplementation(() => intl);
|
|
11
|
-
|
|
12
|
-
const renderOpts = {
|
|
13
|
-
messages: {
|
|
14
|
-
en: {
|
|
15
|
-
"relationGraph.all": "All",
|
|
16
|
-
"relationGraph.depth": "Depth",
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
|
|
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
|
-
|
|
36
|
-
const nodes = [
|
|
37
|
-
{
|
|
38
|
-
id: "business_concept:92",
|
|
39
|
-
name: "My Awesome Concept",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "business_concept:5",
|
|
43
|
-
name: "New Concept 4",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
id: "business_concept:18",
|
|
47
|
-
name: "Float Concept",
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
const edges = [
|
|
52
|
-
{
|
|
53
|
-
id: 1,
|
|
54
|
-
source_id: "business_concept:92",
|
|
55
|
-
target_id: "business_concept:5",
|
|
56
|
-
tags: [
|
|
57
|
-
{
|
|
58
|
-
id: 3,
|
|
59
|
-
value: {
|
|
60
|
-
target_type: "business_concept",
|
|
61
|
-
type: "bc_padre",
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: 2,
|
|
68
|
-
source_id: "business_concept:5",
|
|
69
|
-
target_id: "business_concept:18",
|
|
70
|
-
tags: [
|
|
71
|
-
{
|
|
72
|
-
id: 3,
|
|
73
|
-
value: {
|
|
74
|
-
target_type: "business_concept",
|
|
75
|
-
type: "bc_padre",
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 3,
|
|
82
|
-
source_id: "business_concept:18",
|
|
83
|
-
target_id: "business_concept:5",
|
|
84
|
-
tags: [
|
|
85
|
-
{
|
|
86
|
-
id: 3,
|
|
87
|
-
value: {
|
|
88
|
-
target_type: "business_concept",
|
|
89
|
-
type: "bc_padre",
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
const relationsGraph = {
|
|
97
|
-
nodes,
|
|
98
|
-
edges,
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const defaultProps = {
|
|
102
|
-
currentId: "business_concept:92",
|
|
103
|
-
navigate: jest.fn(),
|
|
104
|
-
relationsGraph: {},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
it("renders Graph on non-empty relationsGraph", () => {
|
|
108
|
-
const props = { ...defaultProps, relationsGraph };
|
|
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();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("does not render Graph on empty relationsGraph", () => {
|
|
165
|
-
const wrapper = mount(<RelationGraph {...defaultProps} />, {
|
|
166
|
-
attachTo: document.getElementById("container"),
|
|
167
|
-
});
|
|
168
|
-
expect(wrapper.find("Graph").length).toBe(0);
|
|
169
|
-
});
|
|
170
|
-
|
|
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", () => {
|
|
182
|
-
const props = { ...defaultProps, relationsGraph };
|
|
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();
|
|
263
|
-
});
|
|
264
|
-
});
|