@truedat/lm 8.4.6 → 8.4.8

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/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@truedat/lm",
3
- "version": "8.4.6",
3
+ "version": "8.4.8",
4
4
  "description": "Truedat Link Manager",
5
- "sideEffects": false,
5
+ "sideEffects": [
6
+ "**/*.css",
7
+ "**/*.less"
8
+ ],
6
9
  "module": "src/index.js",
7
10
  "files": [
8
11
  "src",
@@ -51,7 +54,7 @@
51
54
  "@testing-library/jest-dom": "^6.6.3",
52
55
  "@testing-library/react": "^16.3.0",
53
56
  "@testing-library/user-event": "^14.6.1",
54
- "@truedat/test": "8.4.6",
57
+ "@truedat/test": "8.4.8",
55
58
  "identity-obj-proxy": "^3.0.0",
56
59
  "jest": "^29.7.0",
57
60
  "redux-saga-test-plan": "^4.0.6"
@@ -71,11 +74,9 @@
71
74
  "react-csv": "^2.2.2",
72
75
  "react-dom": "^19.1.0",
73
76
  "react-dropzone": "^14.3.8",
74
- "react-graph-vis": "1.0.7",
75
77
  "react-hook-form": "^7.56.4",
76
78
  "react-intl": "^7.1.11",
77
79
  "react-moment": "^1.1.3",
78
- "react-rangeslider": "^2.2.0",
79
80
  "react-redux": "^9.2.0",
80
81
  "react-router": "^7.6.0",
81
82
  "redux": "^5.0.1",
@@ -85,5 +86,5 @@
85
86
  "semantic-ui-react": "^3.0.0-beta.2",
86
87
  "swr": "^2.3.3"
87
88
  },
88
- "gitHead": "958ab0a57f628c19a7aee905a8f4ae624a3cdf59"
89
+ "gitHead": "7829e377f78c3cfc66e449f5dc31a8b03c0f5f00"
89
90
  }
@@ -1,53 +1,97 @@
1
1
  import _ from "lodash/fp";
2
- import { useState } from "react";
2
+ import { useMemo } from "react";
3
3
  import { useIntl } from "react-intl";
4
4
  import PropTypes from "prop-types";
5
5
  import { Graph } from "@truedat/core/components";
6
+ import { EMPTY_EDGE_TYPE } from "../services/edgeColorPalette";
6
7
 
7
- export const RelationGraph = ({ navigate, currentId, relationsGraph }) => {
8
+ export const RelationGraph = ({ navigate, currentId, relationsGraph, colorMap }) => {
8
9
  const { formatMessage } = useIntl();
9
10
 
10
- const nodes = _.flow(
11
- _.get("nodes"),
12
- _.map(({ id, name: label, resource_id }) => ({
13
- id,
14
- data: { label },
15
- resource_id,
16
- style:
17
- currentId === id
18
- ? {
19
- background: "#ed5c17",
20
- color: "white",
21
- fontWeight: "bold",
22
- }
23
- : { cursor: "pointer" },
24
- }))
25
- )(relationsGraph);
26
-
27
- const edges = _.flow(
28
- _.get("edges"),
29
- _.map(({ id: id, source_id: source, target_id: target, tags }) => ({
30
- id,
31
- source,
32
- target,
33
- label: _.flow(
34
- _.map((tag) =>
35
- formatMessage({
36
- id: `source.${_.prop("value.type")(tag)}`,
37
- defaultMessage: _.prop("value.type")(tag),
38
- })
39
- ),
40
- _.join("\n")
41
- )(tags),
42
- }))
43
- )(relationsGraph);
11
+ const nodes = useMemo(
12
+ () =>
13
+ _.flow(
14
+ _.get("nodes"),
15
+ _.map(({ id, name: label, resource_id }) => ({
16
+ id,
17
+ type: "concept",
18
+ data: { label, isActive: currentId === id },
19
+ resource_id,
20
+ }))
21
+ )(relationsGraph),
22
+ [relationsGraph, currentId]
23
+ );
24
+
25
+ const edges = useMemo(
26
+ () =>
27
+ _.flow(
28
+ _.get("edges"),
29
+ _.map(({ id: id, source_id: source, target_id: target, tags }) => {
30
+ const primaryType = _.path([0, "value", "type"])(tags) || EMPTY_EDGE_TYPE;
31
+ const label = _.isEmpty(tags)
32
+ ? formatMessage({
33
+ id: `source.${EMPTY_EDGE_TYPE}`,
34
+ defaultMessage: "Empty",
35
+ })
36
+ : _.flow(
37
+ _.map((tag) =>
38
+ formatMessage({
39
+ id: `source.${_.prop("value.type")(tag)}`,
40
+ defaultMessage: _.prop("value.type")(tag),
41
+ })
42
+ ),
43
+ _.join("\n")
44
+ )(tags);
45
+ const color = primaryType && colorMap ? colorMap.get(primaryType) : undefined;
46
+
47
+ return {
48
+ id,
49
+ source,
50
+ target,
51
+ type: "colored",
52
+ data: { label, primaryType },
53
+ ...(color && {
54
+ style: { stroke: color, strokeWidth: 1.5 },
55
+ markerEnd: {
56
+ type: "arrowclosed",
57
+ width: 12,
58
+ height: 12,
59
+ color,
60
+ },
61
+ }),
62
+ };
63
+ })
64
+ )(relationsGraph),
65
+ [relationsGraph, formatMessage, colorMap]
66
+ );
67
+
68
+ const safeEdges = useMemo(() => {
69
+ const safeNodeIds = new Set(_.map("id", nodes));
70
+ return _.filter(
71
+ ({ source, target }) => safeNodeIds.has(source) && safeNodeIds.has(target),
72
+ edges
73
+ );
74
+ }, [nodes, edges]);
75
+
76
+ const graphData = useMemo(
77
+ () => ({
78
+ nodes,
79
+ edges: safeEdges,
80
+ }),
81
+ [nodes, safeEdges]
82
+ );
44
83
 
45
84
  const onClick = (_, { resource_id }) => {
46
85
  if (navigate) navigate({ resource_id });
47
86
  };
48
87
 
49
88
  return !_.isEmpty(relationsGraph) ? (
50
- <Graph nodes={nodes} edges={edges} onNodeClick={onClick} />
89
+ <Graph
90
+ nodes={graphData.nodes}
91
+ edges={graphData.edges}
92
+ onNodeClick={onClick}
93
+ rootNodeId={currentId}
94
+ />
51
95
  ) : null;
52
96
  };
53
97
 
@@ -55,6 +99,7 @@ RelationGraph.propTypes = {
55
99
  navigate: PropTypes.func,
56
100
  currentId: PropTypes.string,
57
101
  relationsGraph: PropTypes.object,
102
+ colorMap: PropTypes.instanceOf(Map),
58
103
  };
59
104
 
60
105
  export default RelationGraph;
@@ -1,25 +1,71 @@
1
+ import { useState, useRef, useEffect, useCallback } from "react";
1
2
  import _ from "lodash/fp";
2
3
  import PropTypes from "prop-types";
3
- import { Segment } from "semantic-ui-react";
4
- import Slider from "react-rangeslider";
5
- import "react-rangeslider/lib/index.css";
6
- import { FormattedMessage } from "react-intl";
4
+ import "../styles/relationGraph.less";
7
5
 
8
6
  export const RelationGraphDepth = ({ onChange, depth, maxDepth }) => {
9
- return _.isUndefined(maxDepth) ? null : (
10
- <Segment className={`graph-depth ${maxDepth == 0 ? "disabled" : ""}`}>
11
- <FormattedMessage id="relationGraph.depth" />
12
- <div>{depth}</div>
13
- <Slider
14
- value={depth}
15
- orientation="vertical"
16
- reverse
17
- onChange={onChange}
18
- min={0}
19
- max={maxDepth}
20
- tooltip={false}
21
- />
22
- </Segment>
7
+ const [localDepth, setLocalDepth] = useState(depth);
8
+ const [isDragging, setIsDragging] = useState(false);
9
+ const sliderRef = useRef(null);
10
+ const latestDepthRef = useRef(depth);
11
+
12
+ useEffect(() => {
13
+ setLocalDepth(depth);
14
+ latestDepthRef.current = depth;
15
+ }, [depth]);
16
+
17
+ const updateDepthFromY = useCallback(
18
+ (clientY) => {
19
+ if (!sliderRef.current || !maxDepth) return;
20
+ const rect = sliderRef.current.getBoundingClientRect();
21
+ const relativeY = Math.max(0, Math.min(1, (rect.bottom - clientY) / rect.height));
22
+ const newDepth = Math.round(relativeY * maxDepth);
23
+ setLocalDepth(newDepth);
24
+ latestDepthRef.current = newDepth;
25
+ },
26
+ [maxDepth]
27
+ );
28
+
29
+ useEffect(() => {
30
+ const handleMouseMove = (e) => { if (isDragging) updateDepthFromY(e.clientY); };
31
+ const handleTouchMove = (e) => { if (isDragging) updateDepthFromY(e.touches[0].clientY); };
32
+ const handleEnd = () => {
33
+ setIsDragging(false);
34
+ if (onChange) onChange(latestDepthRef.current);
35
+ };
36
+ if (isDragging) {
37
+ window.addEventListener("mousemove", handleMouseMove);
38
+ window.addEventListener("mouseup", handleEnd);
39
+ window.addEventListener("touchmove", handleTouchMove);
40
+ window.addEventListener("touchend", handleEnd);
41
+ }
42
+ return () => {
43
+ window.removeEventListener("mousemove", handleMouseMove);
44
+ window.removeEventListener("mouseup", handleEnd);
45
+ window.removeEventListener("touchmove", handleTouchMove);
46
+ window.removeEventListener("touchend", handleEnd);
47
+ };
48
+ }, [isDragging, updateDepthFromY, onChange]);
49
+
50
+ if (_.isUndefined(maxDepth)) return null;
51
+
52
+ const percentage = maxDepth > 0 ? (localDepth / maxDepth) * 100 : 0;
53
+ const label = `${localDepth}`;
54
+
55
+ return (
56
+ <div className={`graph-depth-v${maxDepth === 0 ? " disabled" : ""}`}>
57
+ <span className="graph-depth-v-label">{label}</span>
58
+ <div
59
+ ref={sliderRef}
60
+ onMouseDown={(e) => { setIsDragging(true); updateDepthFromY(e.clientY); }}
61
+ onTouchStart={(e) => { setIsDragging(true); updateDepthFromY(e.touches[0].clientY); }}
62
+ className="graph-depth-v-track"
63
+ style={{ "--td-depth-pct": `${percentage}%` }}
64
+ >
65
+ <div className="graph-depth-v-fill" />
66
+ <div className={`graph-depth-v-thumb${isDragging ? " dragging" : ""}`} />
67
+ </div>
68
+ </div>
23
69
  );
24
70
  };
25
71
 
@@ -0,0 +1,67 @@
1
+ import {
2
+ buildColorMap,
3
+ EDGE_COLOR_PALETTE,
4
+ EMPTY_EDGE_COLOR,
5
+ EMPTY_EDGE_TYPE,
6
+ } from "../edgeColorPalette";
7
+
8
+ describe("buildColorMap", () => {
9
+ it("returns the empty edge color for empty edges", () => {
10
+ expect(buildColorMap([])).toEqual(
11
+ new Map([[EMPTY_EDGE_TYPE, EMPTY_EDGE_COLOR]])
12
+ );
13
+ });
14
+
15
+ it("assigns the first palette color to the first distinct type", () => {
16
+ const edges = [{ tags: [{ value: { type: "Contains" } }] }];
17
+ const map = buildColorMap(edges);
18
+ expect(map.get("Contains")).toBe(EDGE_COLOR_PALETTE[0]);
19
+ });
20
+
21
+ it("assigns different colors to different types", () => {
22
+ const edges = [
23
+ { tags: [{ value: { type: "Contains" } }, { value: { type: "Uses" } }] },
24
+ ];
25
+ const map = buildColorMap(edges);
26
+ expect(map.get("Contains")).not.toBe(map.get("Uses"));
27
+ });
28
+
29
+ it("assigns the same color to the same type across multiple edges", () => {
30
+ const edges = [
31
+ { tags: [{ value: { type: "Contains" } }] },
32
+ { tags: [{ value: { type: "Contains" } }] },
33
+ ];
34
+ const map = buildColorMap(edges);
35
+ expect(map.size).toBe(2);
36
+ expect(map.get(EMPTY_EDGE_TYPE)).toBe(EMPTY_EDGE_COLOR);
37
+ expect(map.get("Contains")).toBe(EDGE_COLOR_PALETTE[0]);
38
+ });
39
+
40
+ it("wraps palette when there are more types than palette entries", () => {
41
+ const edges = EDGE_COLOR_PALETTE.map((_, i) => ({
42
+ tags: [{ value: { type: `type${i}` } }],
43
+ }));
44
+ // Add one more type beyond palette length
45
+ edges.push({ tags: [{ value: { type: "overflow" } }] });
46
+ const map = buildColorMap(edges);
47
+ expect(map.get("overflow")).toBe(EDGE_COLOR_PALETTE[0]);
48
+ });
49
+
50
+ it("skips tags with missing value.type", () => {
51
+ const edges = [
52
+ { tags: [{ value: {} }, { value: { type: "Contains" } }] },
53
+ ];
54
+ const map = buildColorMap(edges);
55
+ expect(map.size).toBe(2);
56
+ expect(map.get(EMPTY_EDGE_TYPE)).toBe(EMPTY_EDGE_COLOR);
57
+ expect(map.get("Contains")).toBe(EDGE_COLOR_PALETTE[0]);
58
+ });
59
+
60
+ it("handles edges with no tags array", () => {
61
+ const edges = [{ tags: undefined }, { tags: [] }];
62
+ expect(() => buildColorMap(edges)).not.toThrow();
63
+ expect(buildColorMap(edges)).toEqual(
64
+ new Map([[EMPTY_EDGE_TYPE, EMPTY_EDGE_COLOR]])
65
+ );
66
+ });
67
+ });
@@ -0,0 +1,27 @@
1
+ export const EDGE_COLOR_PALETTE = [
2
+ "#4a90d9",
3
+ "#27ae60",
4
+ "#8e44ad",
5
+ "#c0392b",
6
+ "#16a085",
7
+ "#2c3e50",
8
+ ];
9
+
10
+ export const EMPTY_EDGE_COLOR = "#5f6672";
11
+ export const EMPTY_EDGE_TYPE = "_empty";
12
+
13
+ export const buildColorMap = (edges = []) => {
14
+ const map = new Map([[EMPTY_EDGE_TYPE, EMPTY_EDGE_COLOR]]);
15
+ let paletteIndex = 0;
16
+
17
+ edges.forEach(({ tags = [] }) => {
18
+ (tags || []).forEach(({ value }) => {
19
+ const type = value?.type;
20
+ if (type && !map.has(type)) {
21
+ map.set(type, EDGE_COLOR_PALETTE[paletteIndex % EDGE_COLOR_PALETTE.length]);
22
+ paletteIndex += 1;
23
+ }
24
+ });
25
+ });
26
+ return map;
27
+ };
@@ -1,31 +1,177 @@
1
1
  .graph-depth {
2
- position: absolute !important;
3
- z-index: 1;
4
- margin-right: 0px;
5
- right: 14px;
6
-
7
- div {
8
- text-align: center;
9
- font-weight: bold;
2
+ position: absolute;
3
+ left: 12px;
4
+ bottom: 12px;
5
+ z-index: 2;
6
+ width: 170px;
7
+ margin: 0;
8
+ min-width: 170px;
9
+ padding: 0;
10
+ border: 0;
11
+ box-shadow: none;
12
+ background: transparent;
13
+
14
+ .graph-depth-header {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: flex-start;
18
+ gap: 8px;
19
+ margin-bottom: 4px;
20
+ line-height: 1;
21
+
22
+ .graph-depth-title {
23
+ font-weight: 600;
24
+ }
25
+
26
+ .graph-depth-value {
27
+ min-width: 12px;
28
+ font-weight: bold;
29
+ color: var(--td-graph-accent);
30
+ }
10
31
  }
11
32
 
12
33
  &.disabled {
13
34
  pointer-events: none;
14
- opacity: 0.7;
35
+ opacity: 0.6;
36
+
37
+ .graph-depth-step,
38
+ .graph-depth-range {
39
+ accent-color: var(--td-graph-depth-disabled);
40
+ }
41
+
42
+ .graph-depth-step {
43
+ background-color: var(--td-graph-depth-disabled);
44
+ border-color: var(--td-graph-depth-disabled);
45
+ }
46
+ }
47
+
48
+ .graph-depth-slider {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: 6px;
52
+
53
+ .graph-depth-step {
54
+ width: 16px;
55
+ height: 16px;
56
+ padding: 0;
57
+ border-radius: 50%;
58
+ border: 1px solid var(--td-graph-accent);
59
+ background: #fff;
60
+ color: var(--td-graph-accent);
61
+ font-size: 11px;
62
+ line-height: 14px;
63
+ text-align: center;
64
+ cursor: pointer;
65
+ }
66
+
67
+ .graph-depth-step:disabled {
68
+ border-color: var(--td-graph-depth-disabled);
69
+ color: var(--td-graph-depth-disabled);
70
+ cursor: default;
71
+ }
15
72
 
16
- .rangeslider-vertical .rangeslider__fill {
17
- background-color: gray;
73
+ .graph-depth-range {
74
+ flex: 1;
75
+ height: 18px;
76
+ margin: 0;
77
+ accent-color: var(--td-graph-accent);
78
+ cursor: pointer;
18
79
  }
19
80
  }
20
81
 
21
- .rangeslider-vertical {
22
- .rangeslider__fill {
23
- background-color: #ed5c17;
24
- border: 1px solid gray;
82
+ .graph-depth-labels {
83
+ display: flex;
84
+ justify-content: space-between;
85
+ font-size: 11px;
86
+ font-weight: 600;
87
+ margin-top: 2px;
88
+ color: #666;
89
+
90
+ span {
91
+ min-width: 12px;
92
+ text-align: center;
93
+ user-select: none;
25
94
  }
26
-
27
- .rangeslider__handle {
28
- border-radius: 4px;
95
+ }
96
+ }
97
+
98
+ .relation-graph-wrapper {
99
+ position: relative;
100
+ width: 100%;
101
+ margin: 0;
102
+ overflow: hidden;
103
+ }
104
+
105
+ .graph-depth-v-container {
106
+ position: absolute;
107
+ top: 12px;
108
+ right: 20px;
109
+ z-index: 2;
110
+ }
111
+
112
+ .graph-depth-v {
113
+ width: 32px;
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: center;
117
+ gap: 4px;
118
+ padding: 4px 8px 2px;
119
+ background: rgba(255, 255, 255, 0.65);
120
+ backdrop-filter: blur(4px);
121
+ border-radius: 12px;
122
+ user-select: none;
123
+
124
+ &.disabled {
125
+ pointer-events: none;
126
+ opacity: 0.5;
127
+ }
128
+
129
+ .graph-depth-v-label {
130
+ font-size: 16px;
131
+ font-weight: 700;
132
+ color: var(--td-graph-accent);
133
+ line-height: 1;
134
+ margin-top: -4px;
135
+ margin-bottom: 8px;
136
+ }
137
+
138
+ .graph-depth-v-track {
139
+ position: relative;
140
+ width: 4px;
141
+ height: 88px;
142
+ background: var(--td-graph-depth-inactive);
143
+ border-radius: 2px;
144
+ cursor: pointer;
145
+
146
+ .graph-depth-v-fill {
147
+ position: absolute;
148
+ bottom: 0;
149
+ left: 0;
150
+ width: 100%;
151
+ height: var(--td-depth-pct, 0%);
152
+ background: var(--td-graph-accent);
153
+ border-radius: 2px;
154
+ transition: height 0.2s ease-out;
155
+ }
156
+
157
+ .graph-depth-v-thumb {
158
+ position: absolute;
159
+ left: 50%;
160
+ bottom: calc(var(--td-depth-pct, 0%) - 7px);
161
+ transform: translateX(-50%);
162
+ width: 14px;
163
+ height: 14px;
164
+ background: #ffffff;
165
+ border: 2px solid var(--td-graph-accent);
166
+ border-radius: 50%;
167
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
168
+ cursor: grab;
169
+ transition: bottom 0.1s ease-out, transform 0.1s ease-out;
170
+
171
+ &.dragging {
172
+ transform: translateX(-50%) scale(1.2);
173
+ cursor: grabbing;
174
+ }
29
175
  }
30
176
  }
31
- }
177
+ }