@reltio/graph 1.4.1585 → 1.4.1586-mui5

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.
Files changed (164) hide show
  1. package/index.ts +1 -0
  2. package/package.json +26 -9
  3. package/public/bundle.js +205 -0
  4. package/public/bundle.js.LICENSE.txt +88 -0
  5. package/public/package.json +29 -0
  6. package/scripts/build/index.js +20 -0
  7. package/src/__tests__/GraphView.test.tsx +25 -0
  8. package/src/components/CircleLayout/CircleLayout.tsx +76 -0
  9. package/src/components/CircleLayout/__tests__/CircleLayout.test.tsx +194 -0
  10. package/src/components/GotoButton/GotoButton.tsx +23 -0
  11. package/src/components/GotoButton/__tests__/GotoButton.test.tsx +33 -0
  12. package/src/components/GotoButton/styles.ts +15 -0
  13. package/src/components/GraphArea/GraphArea.tsx +113 -0
  14. package/src/components/GraphArea/__tests__/GraphArea.test.tsx +202 -0
  15. package/src/components/GraphArea/styles.ts +30 -0
  16. package/src/components/GraphLayoutSelector/GraphLayoutSelector.tsx +43 -0
  17. package/src/components/GraphLayoutSelector/__tests__/GraphLayoutSelector.spec.tsx +44 -0
  18. package/src/components/GraphLayoutSelector/styles.ts +15 -0
  19. package/src/components/GraphPerspectiveView/GraphPerspectiveView.tsx +58 -0
  20. package/src/components/GraphPerspectiveView/__tests__/GraphPerspectiveView.test.tsx +115 -0
  21. package/src/components/GraphPerspectiveView/__tests__/useGraphLoader.test.tsx +1519 -0
  22. package/src/components/GraphPerspectiveView/__tests__/useRelationshipTable.test.tsx +1003 -0
  23. package/src/components/GraphPerspectiveView/__tests__/useSelectedEntity.test.tsx +373 -0
  24. package/src/components/GraphPerspectiveView/styles.ts +34 -0
  25. package/src/components/GraphRightSidePanel/GraphRightSidePanel.tsx +27 -0
  26. package/src/components/GraphRightSidePanel/__tests__/GraphRightSidePanel.test.tsx +18 -0
  27. package/src/components/GraphRightSidePanel/icons/relationship-icon.svg +3 -0
  28. package/src/components/GraphRightSidePanel/styles.ts +8 -0
  29. package/src/components/GraphTypeSelector/GraphTypeSelector.tsx +56 -0
  30. package/src/components/GraphTypeSelector/__tests__/GraphTypeSelector.test.tsx +109 -0
  31. package/src/components/GraphTypeSelector/styles.ts +17 -0
  32. package/src/components/HierarchyLayout/HierarchyLayout.ts +102 -0
  33. package/src/components/HierarchyLayout/__tests__/HierarchyLayout.spec.tsx +241 -0
  34. package/src/components/HierarchyLayout/__tests__/buildHierarchy.spec.ts +130 -0
  35. package/src/components/HierarchyLayout/__tests__/calculations.spec.ts +230 -0
  36. package/src/components/HierarchyLayout/buildHierarchy.ts +109 -0
  37. package/src/components/HierarchyLayout/calculations.ts +86 -0
  38. package/src/components/RightPanelEntityDetails/RightPanelEntityDetails.tsx +33 -0
  39. package/src/components/RightPanelEntityDetails/__tests__/RightPanelEntityDetails.test.tsx +39 -0
  40. package/src/components/RightPanelEntityDetails/styles.ts +12 -0
  41. package/src/components/RightPanelProfileBand/RightPanelProfileBand.tsx +33 -0
  42. package/src/components/RightPanelProfileBand/__tests__/RightPanelProfileBand.test.tsx +67 -0
  43. package/src/components/RightPanelProfileBand/styles.ts +8 -0
  44. package/src/components/RightPanelRelationship/RightPanelRelationship.tsx +25 -0
  45. package/src/components/RightPanelRelationship/__tests__/RightPanelRelationship.test.tsx +18 -0
  46. package/src/components/RightPanelRelationship/components/CollapsibleTable/CollapsibleTable.tsx +100 -0
  47. package/src/components/RightPanelRelationship/components/CollapsibleTable/TableHead.tsx +38 -0
  48. package/src/components/RightPanelRelationship/components/CollapsibleTable/__tests__/CollapsibleTable.test.tsx +169 -0
  49. package/src/components/RightPanelRelationship/components/CollapsibleTable/__tests__/TableHead.test.tsx +107 -0
  50. package/src/components/RightPanelRelationship/components/CollapsibleTable/styles.ts +38 -0
  51. package/src/components/RightPanelRelationship/components/CollapsibleTable/types.ts +20 -0
  52. package/src/components/RightPanelRelationship/components/DeleteRelationDialog/DeleteRelationDialog.tsx +34 -0
  53. package/src/components/RightPanelRelationship/components/DeleteRelationDialog/__tests__/DeleteRelationDialog.test.tsx +46 -0
  54. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/GraphAddRelationButton.tsx +43 -0
  55. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/GraphAddRelationDialog.tsx +121 -0
  56. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/__tests__/GraphAddRelationButton.test.tsx +80 -0
  57. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/__tests__/GraphAddRelationDialog.test.tsx +262 -0
  58. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/__tests__/mockdata.ts +75 -0
  59. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/styles.ts +25 -0
  60. package/src/components/RightPanelRelationship/components/GraphAddRelationDialog/useCreatableInOutRelationTypes.ts +13 -0
  61. package/src/components/RightPanelRelationship/components/RelationshipTable/RelationshipTable.tsx +126 -0
  62. package/src/components/RightPanelRelationship/components/RelationshipTable/__tests__/RelationshipTable.test.tsx +176 -0
  63. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/ControlsCellRenderer.tsx +59 -0
  64. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/DefaultCellRenderer.tsx +29 -0
  65. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/ExpandedRowRenderer.tsx +94 -0
  66. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/HeadCellRenderer.tsx +34 -0
  67. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/ProfileCellRenderer.tsx +41 -0
  68. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/__tests__/ControlsCellRenderer.test.tsx +80 -0
  69. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/__tests__/DefaultCellRenderer.test.tsx +47 -0
  70. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/__tests__/ExpandedRowRenderer.test.tsx +166 -0
  71. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/__tests__/HeadCellRenderer.test.tsx +23 -0
  72. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/__tests__/ProfileCellRenderer.test.tsx +47 -0
  73. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/components/DeleteRelationButton.tsx +32 -0
  74. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/components/__tests__/DeleteRelationButton.test.tsx +28 -0
  75. package/src/components/RightPanelRelationship/components/RelationshipTable/cell-renderers/styles.ts +113 -0
  76. package/src/components/RightPanelRelationship/components/RelationshipTable/styles.ts +36 -0
  77. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsFilters.tsx +41 -0
  78. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsFiltersDialog/RelationshipsFiltersDialog.tsx +91 -0
  79. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsFiltersDialog/__tests__/RelationshipsFilterDialog.test.tsx +192 -0
  80. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsFiltersDialog/styles.ts +34 -0
  81. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsSearchTextField/RelationshipsSearchTextField.tsx +31 -0
  82. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsSearchTextField/__tests__/RelationshipsSearchTextField.test.tsx +43 -0
  83. package/src/components/RightPanelRelationship/components/RelationshipsFilters/RelationshipsSearchTextField/styles.ts +17 -0
  84. package/src/components/RightPanelRelationship/components/RelationshipsFilters/__tests__/RelationshipsFilters.test.tsx +76 -0
  85. package/src/components/RightPanelRelationship/components/RelationshipsFilters/styles.ts +19 -0
  86. package/src/components/RightPanelRelationship/styles.ts +20 -0
  87. package/src/components/SigmaCustomRenderers/SigmaCustomRenderersContainer.tsx +16 -0
  88. package/src/components/SigmaGraphContainer/SigmaGraphContainer.tsx +55 -0
  89. package/src/components/SigmaGraphContainer/__tests__/SigmaGraphContainer.test.tsx +25 -0
  90. package/src/components/SigmaGraphResizer/SigmaGraphResizer.tsx +13 -0
  91. package/src/components/SigmaGraphResizer/__tests__/SigmaGraphResizer.test.tsx +28 -0
  92. package/src/components/TreeLayout/TreeLayout.tsx +33 -0
  93. package/src/components/TreeLayout/__tests__/TreeLayout.test.tsx +42 -0
  94. package/src/components/TreeLayout/styles.ts +10 -0
  95. package/src/components/ZoomSlider/ZoomSlider.tsx +90 -0
  96. package/src/components/ZoomSlider/__tests__/ZoomSlider.test.tsx +84 -0
  97. package/src/components/ZoomSlider/styles.ts +57 -0
  98. package/src/constants/index.ts +1 -0
  99. package/src/contexts/GraphStateContext.tsx +19 -0
  100. package/src/contexts/SigmaCustomRenderersContext.ts +20 -0
  101. package/src/helpers/relationsHelpers.ts +9 -0
  102. package/src/hooks/__tests__/useGraphLayout.test.tsx +825 -0
  103. package/src/hooks/__tests__/useGraphLayoutSelector.test.tsx +19 -0
  104. package/src/hooks/__tests__/useSelfRelationLoopsRenderer.test.ts +198 -0
  105. package/src/hooks/__tests__/useSigmaCustomRenderers.test.ts +273 -0
  106. package/src/hooks/__tests__/useTooltipRenderer.test.ts +289 -0
  107. package/src/hooks/useGraphLayout.ts +308 -0
  108. package/src/hooks/useGraphLayoutSelector.ts +19 -0
  109. package/src/hooks/useGraphLoader.ts +279 -0
  110. package/src/hooks/useGraphRightSidePanelElements.tsx +36 -0
  111. package/src/hooks/useGraphState.ts +40 -0
  112. package/src/hooks/useGraphType.ts +32 -0
  113. package/src/hooks/useRelationshipTable.ts +212 -0
  114. package/src/hooks/useRelationshipTableFilters.ts +43 -0
  115. package/src/hooks/useSelectedEntity.ts +89 -0
  116. package/src/hooks/useSelfRelationLoopsRenderer.ts +52 -0
  117. package/src/hooks/useSigmaCustomRenderers.ts +112 -0
  118. package/src/hooks/useTooltipRenderer.ts +80 -0
  119. package/src/index.tsx +37 -0
  120. package/src/rendering/canvas/__tests__/hover.spec.ts +31 -0
  121. package/src/rendering/canvas/__tests__/label.spec.ts +64 -0
  122. package/src/rendering/canvas/__tests__/nodeBackground.spec.ts +99 -0
  123. package/src/rendering/canvas/__tests__/selfRelationLoop.spec.ts +88 -0
  124. package/src/rendering/canvas/__tests__/tooltip.spec.ts +33 -0
  125. package/src/rendering/canvas/hover.ts +13 -0
  126. package/src/rendering/canvas/label.ts +33 -0
  127. package/src/rendering/canvas/nodeBackground.ts +61 -0
  128. package/src/rendering/canvas/selfRelationLoop.ts +108 -0
  129. package/src/rendering/canvas/tooltip.ts +38 -0
  130. package/src/rendering/icons/collapseIcon.inline.svg.ts +6 -0
  131. package/src/rendering/icons/expandIcon.inline.svg.ts +5 -0
  132. package/src/rendering/icons/no_photo.inline.svg.ts +13 -0
  133. package/src/rendering/webgl/edge.arrowHead.ts +87 -0
  134. package/src/rendering/webgl/edge.clamped.ts +246 -0
  135. package/src/rendering/webgl/edge.reversedArrowHead.ts +14 -0
  136. package/src/rendering/webgl/helpers/__tests__/imageHelper.spec.ts +36 -0
  137. package/src/rendering/webgl/helpers/imageHelper.ts +13 -0
  138. package/src/rendering/webgl/image.ts +115 -0
  139. package/src/rendering/webgl/node.border.ts +78 -0
  140. package/src/rendering/webgl/node.buttons.ts +241 -0
  141. package/src/rendering/webgl/node.image.ts +169 -0
  142. package/src/rendering/webgl/shaders/edge.clamped.vert.glsl.ts +55 -0
  143. package/src/rendering/webgl/shaders/node.border.frag.glsl.ts +30 -0
  144. package/src/rendering/webgl/shaders/node.border.vert.glsl.ts +37 -0
  145. package/src/rendering/webgl/shaders/node.buttons.frag.glsl.ts +32 -0
  146. package/src/rendering/webgl/shaders/node.buttons.vert.glsl.ts +49 -0
  147. package/src/rendering/webgl/shaders/node.image.frag.glsl.ts +46 -0
  148. package/src/rendering/webgl/shaders/node.image.vert.glsl.ts +42 -0
  149. package/src/types/graphDataTypes.ts +143 -0
  150. package/src/types/sigmaCustomRenderersTypes.ts +13 -0
  151. package/src/utils/__tests__/graph.spec.ts +152 -0
  152. package/src/utils/__tests__/graphLayout.spec.ts +125 -0
  153. package/src/utils/__tests__/hopsData.spec.ts +456 -0
  154. package/src/utils/__tests__/processResponceToGraph.spec.ts +165 -0
  155. package/src/utils/graph.ts +83 -0
  156. package/src/utils/graphLayout.ts +70 -0
  157. package/src/utils/hopsData.ts +198 -0
  158. package/src/utils/processResponseToGraph.ts +32 -0
  159. package/tsconfig.json +4 -0
  160. package/webpack.config.js +10 -0
  161. package/bundle.js +0 -2
  162. package/bundle.js.LICENSE.txt +0 -54
  163. /package/{5271a9e7b6651c852e93.png → public/5271a9e7b6651c852e93.png} +0 -0
  164. /package/{main.css → public/main.css} +0 -0
@@ -0,0 +1,89 @@
1
+ import {useEffect, useState, useRef, useCallback} from 'react';
2
+ import {useDispatch} from 'react-redux';
3
+ import {propEq} from 'ramda';
4
+ import i18n from 'ui-i18n';
5
+ import {Entity, getEntity, getRequestErrorMessage, HopsResponse} from '@reltio/mdm-sdk';
6
+ import {ui} from '@reltio/mdm-module';
7
+ import {useSafePromise} from '@reltio/components';
8
+
9
+ import {GraphState} from '../types/graphDataTypes';
10
+
11
+ const MAX_CACHE_SIZE = 50;
12
+
13
+ const addToEntitiesMap = (entitiesMap, entity) => {
14
+ const newEntitiesMap = {
15
+ ...entitiesMap,
16
+ [entity?.uri]: entity
17
+ };
18
+ const keys = Object.keys(newEntitiesMap);
19
+ if (keys.length > MAX_CACHE_SIZE) {
20
+ delete newEntitiesMap[keys[0]];
21
+ }
22
+ return newEntitiesMap;
23
+ };
24
+
25
+ export const useSelectedEntity = (
26
+ entity: Entity,
27
+ data: HopsResponse
28
+ ): Pick<GraphState, 'selectedEntity' | 'onEntitySelect' | 'selectedEntityLoading'> => {
29
+ const safePromise = useSafePromise();
30
+ const dispatch = useDispatch();
31
+ const [selectedEntity, setSelectedEntity] = useState<Entity>(null);
32
+ const entitiesMap = useRef<Record<string, Entity>>({});
33
+ const [loading, setLoading] = useState(false);
34
+
35
+ const getCachedEntity = useCallback((uri) => {
36
+ const cachedEntity = entitiesMap.current[uri];
37
+ return cachedEntity
38
+ ? Promise.resolve(cachedEntity)
39
+ : getEntity(uri).then((entity) => {
40
+ entitiesMap.current = addToEntitiesMap(entitiesMap.current, entity);
41
+ return entity;
42
+ });
43
+ }, []);
44
+
45
+ const onEntitySelect = useCallback(
46
+ (selectedEntityUri) => {
47
+ const briefEntityInfo = data?.entities?.find(propEq('uri', selectedEntityUri));
48
+ if (briefEntityInfo) {
49
+ setSelectedEntity(briefEntityInfo);
50
+ }
51
+ setLoading(true);
52
+ safePromise(getCachedEntity(selectedEntityUri))
53
+ .then(setSelectedEntity)
54
+ .catch((error) => {
55
+ dispatch(ui.actions.errorSet(getRequestErrorMessage(error, i18n.text('Something went wrong'))));
56
+ })
57
+ .finally(() => {
58
+ setLoading(false);
59
+ });
60
+ },
61
+ [dispatch, data, safePromise, getCachedEntity]
62
+ );
63
+
64
+ useEffect(() => {
65
+ if (entity?.uri) {
66
+ entitiesMap.current = addToEntitiesMap({}, entity);
67
+ onEntitySelect(entity.uri);
68
+ }
69
+ }, [entity?.uri]); //eslint-disable-line react-hooks/exhaustive-deps
70
+
71
+ useEffect(() => {
72
+ const isActualSelectedEntity = data?.entities.find((entity) => selectedEntity?.uri === entity.uri);
73
+ if (!isActualSelectedEntity) {
74
+ if (entity?.uri) {
75
+ entitiesMap.current = addToEntitiesMap({}, entity);
76
+ onEntitySelect(entity.uri);
77
+ } else {
78
+ entitiesMap.current = {};
79
+ setSelectedEntity(null);
80
+ }
81
+ }
82
+ }, [data]); //eslint-disable-line react-hooks/exhaustive-deps
83
+
84
+ return {
85
+ selectedEntityLoading: loading,
86
+ selectedEntity,
87
+ onEntitySelect
88
+ };
89
+ };
@@ -0,0 +1,52 @@
1
+ import {useEffect, useRef} from 'react';
2
+ import Graph from 'graphology';
3
+ import {useSigma} from '@react-sigma/core';
4
+ import {useSigmaCustomRenderers} from '../contexts/SigmaCustomRenderersContext';
5
+ import {drawSelfRelationLoop} from '../rendering/canvas/selfRelationLoop';
6
+ import {EdgeAttributes, NodeAttributes} from '../types/graphDataTypes';
7
+
8
+ export const useSelfRelationLoopsRenderer = (graph: Graph) => {
9
+ const sigma = useSigma();
10
+ const {addRenderers: addCustomRenderers, canvases} = useSigmaCustomRenderers();
11
+ const selfRelationsPathsMap = useRef<Record<string, Path2D>>({});
12
+ const {selfRelationLoops: selfRelationLoopsCanvas} = canvases;
13
+
14
+ useEffect(() => {
15
+ addCustomRenderers({
16
+ selfRelationLoops: {
17
+ renderer: (context) => {
18
+ graph.forEachEdge((edgeId, edgeData, sourceId) => {
19
+ if (edgeData.loop) {
20
+ delete selfRelationsPathsMap.current[edgeId];
21
+
22
+ const edgeDisplayData = sigma.getEdgeDisplayData(edgeId) as EdgeAttributes;
23
+ const sourceDisplayData = sigma.getNodeDisplayData(sourceId) as NodeAttributes;
24
+
25
+ const isHidden = edgeDisplayData.hidden || sourceDisplayData.hidden;
26
+
27
+ if (isHidden) return;
28
+
29
+ const path = drawSelfRelationLoop(
30
+ context,
31
+ {
32
+ ...edgeDisplayData,
33
+ size: sigma.scaleSize(edgeDisplayData.size)
34
+ },
35
+ {
36
+ ...sourceDisplayData,
37
+ ...sigma.framedGraphToViewport(sourceDisplayData),
38
+ size: sigma.scaleSize(sourceDisplayData.size)
39
+ }
40
+ );
41
+
42
+ selfRelationsPathsMap.current[edgeId] = path;
43
+ }
44
+ });
45
+ },
46
+ insertBefore: 'edges'
47
+ }
48
+ });
49
+ }, [addCustomRenderers, graph, sigma]);
50
+
51
+ return {selfRelationsPathsMap: selfRelationsPathsMap.current, selfRelationLoopsCanvas};
52
+ };
@@ -0,0 +1,112 @@
1
+ import {useEffect, useRef, useCallback, useState} from 'react';
2
+ import {mergeLeft} from 'ramda';
3
+ import {useSigmaContext} from '@react-sigma/core';
4
+ import {getPixelRatio} from 'sigma/utils';
5
+ import {isEmptyValue} from '@reltio/mdm-sdk';
6
+ import {Canvases, Renderers} from '../types/sigmaCustomRenderersTypes';
7
+
8
+ const createCanvasContext = (canvas: HTMLCanvasElement) => {
9
+ const contextOptions = {preserveDrawingBuffer: false, antialias: false};
10
+ return canvas.getContext('2d', contextOptions) as CanvasRenderingContext2D;
11
+ };
12
+
13
+ const createCanvas = (id: string) => {
14
+ const canvas = document.createElement('canvas');
15
+ canvas.style.position = 'absolute';
16
+ canvas.setAttribute('class', `sigma-${id}`);
17
+ return canvas;
18
+ };
19
+
20
+ export const useSigmaCustomRenderers = () => {
21
+ const {container, sigma} = useSigmaContext();
22
+
23
+ const [renderers, setRenderers] = useState<Renderers>({});
24
+
25
+ const canvasesRef = useRef<Canvases>({});
26
+ const widthRef = useRef<number>(0);
27
+ const heightRef = useRef<number>(0);
28
+
29
+ const addRenderers = useCallback((renderers: Renderers) => {
30
+ setRenderers(mergeLeft(renderers));
31
+ }, []);
32
+
33
+ const resize = useCallback(() => {
34
+ widthRef.current = container.offsetWidth;
35
+ heightRef.current = container.offsetHeight;
36
+ const pixelRatio = getPixelRatio();
37
+
38
+ for (const id in canvasesRef.current) {
39
+ const {canvasElement: canvas, context} = canvasesRef.current[id];
40
+
41
+ canvas.style.width = widthRef.current + 'px';
42
+ canvas.style.height = heightRef.current + 'px';
43
+
44
+ canvas.setAttribute('width', widthRef.current * pixelRatio + 'px');
45
+ canvas.setAttribute('height', heightRef.current * pixelRatio + 'px');
46
+
47
+ if (pixelRatio !== 1) {
48
+ context.scale(pixelRatio, pixelRatio);
49
+ }
50
+ }
51
+ }, [container.offsetHeight, container.offsetWidth]);
52
+
53
+ const afterRender = useCallback(() => {
54
+ for (const id in renderers) {
55
+ const {context} = canvasesRef.current[id];
56
+ context.clearRect(0, 0, widthRef.current, heightRef.current);
57
+ const {renderer} = renderers[id];
58
+ renderer(context);
59
+ }
60
+ }, [renderers]);
61
+
62
+ const beforeRender = useCallback(() => {
63
+ for (const id in renderers) {
64
+ const shouldCreateCanvas = isEmptyValue(canvasesRef.current[id]);
65
+
66
+ if (!shouldCreateCanvas) continue;
67
+
68
+ const {insertBefore} = renderers[id];
69
+ const sigmaContainer = container.querySelector('.sigma-container');
70
+ const canvas = createCanvas(id);
71
+ const context = createCanvasContext(canvas);
72
+ sigmaContainer.insertBefore(canvas, sigmaContainer.querySelector(`.sigma-${insertBefore}`));
73
+ canvasesRef.current[id] = {canvasElement: canvas, context: context};
74
+ resize();
75
+ }
76
+ }, [renderers, container, resize]);
77
+
78
+ useEffect(() => {
79
+ sigma.addListener('beforeRender', beforeRender);
80
+ return () => {
81
+ sigma.off('beforeRender', beforeRender);
82
+ };
83
+ }, [sigma, beforeRender]);
84
+
85
+ useEffect(() => {
86
+ sigma.addListener('afterRender', afterRender);
87
+ return () => {
88
+ sigma.off('afterRender', afterRender);
89
+ };
90
+ }, [sigma, afterRender]);
91
+
92
+ useEffect(() => {
93
+ sigma.addListener('resize', resize);
94
+ return () => {
95
+ sigma.off('resize', resize);
96
+ };
97
+ }, [sigma, resize]);
98
+
99
+ useEffect(() => {
100
+ const canvases = canvasesRef.current;
101
+
102
+ return () => {
103
+ for (const id in canvases) {
104
+ const canvas = canvases[id];
105
+ canvas.canvasElement.remove();
106
+ delete canvases[id];
107
+ }
108
+ };
109
+ }, []);
110
+
111
+ return {addRenderers, canvases: canvasesRef.current};
112
+ };
@@ -0,0 +1,80 @@
1
+ import {useEffect, useState, useRef} from 'react';
2
+ import Graph from 'graphology';
3
+ import {useRegisterEvents, useSigma} from '@react-sigma/core';
4
+ import {useSigmaCustomRenderers} from '../contexts/SigmaCustomRenderersContext';
5
+ import {getMousePos} from '../utils/graphLayout';
6
+ import {EdgeAttributes, NodeAttributes} from '../types/graphDataTypes';
7
+ import {drawTooltip} from '../rendering/canvas/tooltip';
8
+ import {Canvas} from '../types/sigmaCustomRenderersTypes';
9
+
10
+ type PointerCoordinates = {
11
+ x: number;
12
+ y: number;
13
+ };
14
+
15
+ export const useTooltipRenderer = (
16
+ graph: Graph,
17
+ selfRelationLoopsCanvas: Canvas,
18
+ selfRelationsPathsMap: Record<string, Path2D>
19
+ ) => {
20
+ const sigma = useSigma();
21
+ const registerEvents = useRegisterEvents();
22
+ const {addRenderers: addCustomRenderers} = useSigmaCustomRenderers();
23
+
24
+ const pointerCoordinates = useRef<PointerCoordinates>({x: 0, y: 0});
25
+
26
+ const [hoveredNormalEdge, setHoveredNormalEdge] = useState<string>(null);
27
+ const [hoveredSelfRelationEdge, setHoveredSelfRelationEdge] = useState<string>(null);
28
+
29
+ const hoveredEdge = hoveredNormalEdge || hoveredSelfRelationEdge;
30
+
31
+ useEffect(() => {
32
+ registerEvents({
33
+ enterEdge: ({edge}) => {
34
+ setHoveredNormalEdge(edge);
35
+ },
36
+ leaveEdge: () => {
37
+ setHoveredNormalEdge(null);
38
+ },
39
+ mousemove: (event) => {
40
+ pointerCoordinates.current.x = event.x;
41
+ pointerCoordinates.current.y = event.y;
42
+
43
+ if (!selfRelationLoopsCanvas) return;
44
+
45
+ const {context, canvasElement} = selfRelationLoopsCanvas;
46
+ const {x, y} = getMousePos(canvasElement, event.original);
47
+
48
+ for (const id in selfRelationsPathsMap) {
49
+ const path = selfRelationsPathsMap[id];
50
+ if (context.isPointInStroke(path, x, y)) {
51
+ setHoveredSelfRelationEdge(id);
52
+ break;
53
+ } else {
54
+ setHoveredSelfRelationEdge(null);
55
+ }
56
+ }
57
+ }
58
+ });
59
+ }, [sigma, registerEvents, selfRelationLoopsCanvas, selfRelationsPathsMap]);
60
+
61
+ useEffect(() => {
62
+ addCustomRenderers({
63
+ tooltips: {
64
+ renderer: (context) => {
65
+ graph.forEachEdge((edgeId, _edgeData, sourceId) => {
66
+ const edgeDisplayData = sigma.getEdgeDisplayData(edgeId) as EdgeAttributes;
67
+ const sourceDisplayData = sigma.getNodeDisplayData(sourceId) as NodeAttributes;
68
+ if (!edgeDisplayData.hovered || sourceDisplayData.highlighted) return;
69
+ const {x, y} = pointerCoordinates.current;
70
+ const yOffset = 15;
71
+ drawTooltip(context, x, y - yOffset, edgeDisplayData.relationLabel);
72
+ });
73
+ },
74
+ insertBefore: 'mouse'
75
+ }
76
+ });
77
+ }, [addCustomRenderers, graph, sigma]);
78
+
79
+ return hoveredEdge;
80
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import {Provider} from 'react-redux';
3
+ import {Store} from 'redux';
4
+ import createGenerateClassName from '@mui/styles/createGenerateClassName';
5
+ import StylesProvider from '@mui/styles/StylesProvider';
6
+ import {ErrorPopup} from '@reltio/components';
7
+ import {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider';
8
+ import {AdapterMoment} from '@mui/x-date-pickers/AdapterMoment';
9
+ import {GraphLayout} from '@reltio/mdm-sdk';
10
+ import GraphPerspectiveView from './components/GraphPerspectiveView/GraphPerspectiveView';
11
+
12
+ const generateClassName = createGenerateClassName({
13
+ productionPrefix: 'graph',
14
+ disableGlobal: true,
15
+ seed: 'grp'
16
+ });
17
+
18
+ type Props = {
19
+ store: Store<unknown>;
20
+ graphTypeUri?: string;
21
+ graphLayout?: GraphLayout;
22
+ };
23
+
24
+ const GraphPerspective = ({store, graphTypeUri, graphLayout}: Props) => {
25
+ return (
26
+ <Provider store={store}>
27
+ <StylesProvider generateClassName={generateClassName}>
28
+ <LocalizationProvider dateAdapter={AdapterMoment}>
29
+ <GraphPerspectiveView graphTypeUri={graphTypeUri} graphLayout={graphLayout} />
30
+ <ErrorPopup showErrorFromStore={true} />
31
+ </LocalizationProvider>
32
+ </StylesProvider>
33
+ </Provider>
34
+ );
35
+ };
36
+
37
+ export default GraphPerspective;
@@ -0,0 +1,31 @@
1
+ import {Settings} from 'sigma/settings';
2
+ import {drawHover} from '../hover';
3
+ import {drawLabel} from '../label';
4
+ import {drawNodeBackground} from '../nodeBackground';
5
+
6
+ jest.mock('../label', () => ({
7
+ drawLabel: jest.fn()
8
+ }));
9
+
10
+ jest.mock('../nodeBackground', () => ({
11
+ drawNodeBackground: jest.fn()
12
+ }));
13
+
14
+ describe('drawHover tests', () => {
15
+ const context = ({} as unknown) as CanvasRenderingContext2D;
16
+
17
+ const settings = {} as Settings;
18
+
19
+ const data = {
20
+ x: 1,
21
+ y: 1,
22
+ size: 12,
23
+ label: 'Test label'
24
+ };
25
+
26
+ it('should call drawNodeBackground and drawLabel', () => {
27
+ drawHover(context, data, settings);
28
+ expect(drawNodeBackground).toHaveBeenCalledWith(context, data, settings, true);
29
+ expect(drawLabel).toHaveBeenCalledWith(context, data, settings, false);
30
+ });
31
+ });
@@ -0,0 +1,64 @@
1
+ import {Settings} from 'sigma/settings';
2
+ import {drawLabel} from '../label';
3
+ import {drawNodeBackground} from '../nodeBackground';
4
+
5
+ jest.mock('../nodeBackground', () => ({
6
+ drawNodeBackground: jest.fn()
7
+ }));
8
+
9
+ describe('drawLabel tests', () => {
10
+ const context = ({
11
+ fillText: jest.fn()
12
+ } as unknown) as CanvasRenderingContext2D;
13
+
14
+ const settings = {
15
+ labelFont: 'Roboto',
16
+ labelSize: 12,
17
+ labelColor: {
18
+ color: 'rgba(0, 0, 0, 0.87)'
19
+ },
20
+ labelWeight: 'normal'
21
+ } as Settings;
22
+
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ it('should not call context.fillText if label is empty', () => {
28
+ const data = {
29
+ x: 1,
30
+ y: 1,
31
+ size: 12,
32
+ label: ''
33
+ };
34
+ drawLabel(context, data, settings);
35
+ expect(context.fillText).not.toHaveBeenCalled();
36
+ });
37
+
38
+ it('should call context.fillText if label is not empty', () => {
39
+ const data = {
40
+ x: 1,
41
+ y: 1,
42
+ size: 12,
43
+ label: 'Test label'
44
+ };
45
+
46
+ drawLabel(context, data, settings, false);
47
+ expect(context.font).toBe('normal 12px Roboto');
48
+ expect(context.fillStyle).toBe(settings.labelColor.color);
49
+ expect(context.fillText).toHaveBeenCalledWith(data.label, 24, 5);
50
+ expect(drawNodeBackground).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('should call drawNodeBackground to show background', () => {
54
+ const data = {
55
+ x: 1,
56
+ y: 1,
57
+ size: 12,
58
+ label: 'Test label'
59
+ };
60
+ drawLabel(context, data, settings, true);
61
+ expect(drawNodeBackground).toHaveBeenCalledWith(context, data, settings, false);
62
+ expect(context.fillText).toHaveBeenCalledWith(data.label, 24, 5);
63
+ });
64
+ });
@@ -0,0 +1,99 @@
1
+ import {Settings} from 'sigma/settings';
2
+ import {drawNodeBackground} from '../nodeBackground';
3
+
4
+ describe('drawNodeBackground tests', () => {
5
+ const context = ({
6
+ beginPath: jest.fn(),
7
+ moveTo: jest.fn(),
8
+ lineTo: jest.fn(),
9
+ arc: jest.fn(),
10
+ closePath: jest.fn(),
11
+ fill: jest.fn(),
12
+ measureText: jest.fn().mockReturnValue({
13
+ width: 100
14
+ }),
15
+ fillText: jest.fn(),
16
+ quadraticCurveTo: jest.fn()
17
+ } as unknown) as CanvasRenderingContext2D;
18
+
19
+ const settings = {
20
+ labelFont: 'Roboto',
21
+ labelSize: 12,
22
+ labelColor: {
23
+ color: 'rgba(0, 0, 0, 0.87)'
24
+ },
25
+ labelWeight: 'normal'
26
+ } as Settings;
27
+
28
+ afterEach(() => {
29
+ jest.clearAllMocks();
30
+ });
31
+
32
+ it('should call context methods if label is string (hover behavior)', () => {
33
+ const data = {
34
+ x: 1,
35
+ y: 1,
36
+ size: 12,
37
+ label: 'Test label'
38
+ };
39
+ drawNodeBackground(context, data, settings, true);
40
+ expect(context.beginPath).toHaveBeenCalled();
41
+ expect(context.moveTo).toHaveBeenCalled();
42
+ expect(context.lineTo).toHaveBeenCalledTimes(3);
43
+ expect(context.quadraticCurveTo).toHaveBeenCalledTimes(4);
44
+ expect(context.arc).toHaveBeenCalled();
45
+ expect(context.closePath).toHaveBeenCalled();
46
+ expect(context.fill).toHaveBeenCalled();
47
+ });
48
+
49
+ it('should call context methods if label is not string (hover behavior)', () => {
50
+ const data = {
51
+ x: 1,
52
+ y: 1,
53
+ size: 12,
54
+ label: null
55
+ };
56
+ drawNodeBackground(context, data, settings, true);
57
+ expect(context.beginPath).toHaveBeenCalled();
58
+ expect(context.moveTo).not.toHaveBeenCalled();
59
+ expect(context.lineTo).not.toHaveBeenCalled();
60
+ expect(context.quadraticCurveTo).not.toHaveBeenCalled();
61
+ expect(context.arc).toHaveBeenCalled();
62
+ expect(context.closePath).toHaveBeenCalled();
63
+ expect(context.fill).toHaveBeenCalled();
64
+ });
65
+
66
+ it('should call context methods if label is string and drawNodeArc is false', () => {
67
+ const data = {
68
+ x: 1,
69
+ y: 1,
70
+ size: 12,
71
+ label: 'Test label'
72
+ };
73
+ drawNodeBackground(context, data, settings, false);
74
+ expect(context.beginPath).toHaveBeenCalled();
75
+ expect(context.moveTo).toHaveBeenCalled();
76
+ expect(context.lineTo).toHaveBeenCalledTimes(3);
77
+ expect(context.quadraticCurveTo).toHaveBeenCalledTimes(4);
78
+ expect(context.arc).not.toHaveBeenCalled();
79
+ expect(context.closePath).toHaveBeenCalled();
80
+ expect(context.fill).toHaveBeenCalled();
81
+ });
82
+
83
+ it('should not call context methods if label is not string and drawNodeArc is false', () => {
84
+ const data = {
85
+ x: 1,
86
+ y: 1,
87
+ size: 12,
88
+ label: null
89
+ };
90
+ drawNodeBackground(context, data, settings, false);
91
+ expect(context.beginPath).not.toHaveBeenCalled();
92
+ expect(context.moveTo).not.toHaveBeenCalled();
93
+ expect(context.lineTo).not.toHaveBeenCalled();
94
+ expect(context.quadraticCurveTo).not.toHaveBeenCalled();
95
+ expect(context.arc).not.toHaveBeenCalled();
96
+ expect(context.closePath).not.toHaveBeenCalled();
97
+ expect(context.fill).not.toHaveBeenCalled();
98
+ });
99
+ });
@@ -0,0 +1,88 @@
1
+ import {drawSelfRelationLoop} from '../selfRelationLoop';
2
+
3
+ describe('Graph selfRelationLoop behavior', () => {
4
+ const context = {
5
+ beginPath: jest.fn(),
6
+ moveTo: jest.fn(),
7
+ lineTo: jest.fn(),
8
+ bezierCurveTo: jest.fn(),
9
+ stroke: jest.fn(),
10
+ closePath: jest.fn(),
11
+ fill: jest.fn()
12
+ } as unknown as CanvasRenderingContext2D;
13
+
14
+ const pathInstance = {
15
+ moveTo: jest.fn(),
16
+ bezierCurveTo: jest.fn()
17
+ };
18
+
19
+ const edgeData = {
20
+ key: 'id_47',
21
+ size: 1,
22
+ color: 'rgba(0, 0, 0, 0.2)',
23
+ forceLabel: true,
24
+ loop: true,
25
+ hidden: false,
26
+ label: '',
27
+ type: 'line',
28
+ zIndex: 1
29
+ };
30
+
31
+ const sourceData = {
32
+ x: 20,
33
+ y: 30,
34
+ size: 1
35
+ };
36
+
37
+ beforeEach(() => {
38
+ jest.spyOn(window, 'Path2D').mockImplementation(() => {
39
+ return pathInstance as any;
40
+ });
41
+ });
42
+
43
+ afterEach(() => {
44
+ jest.clearAllMocks();
45
+ });
46
+
47
+ it('should call context methods for not arrow edge', () => {
48
+ drawSelfRelationLoop(context, edgeData, sourceData);
49
+ expect(Path2D).toHaveBeenCalled();
50
+ expect(pathInstance.moveTo).toHaveBeenCalledWith(sourceData.x, sourceData.y);
51
+ expect(pathInstance.bezierCurveTo).toHaveBeenCalledWith(15.8, 30, 20, 34.2, sourceData.x, sourceData.y);
52
+ expect(context.stroke).toHaveBeenCalledWith(pathInstance);
53
+ });
54
+
55
+ it('should call context methods for arrow edge correctly', () => {
56
+ drawSelfRelationLoop(context, {...edgeData, type: 'arrow'}, sourceData);
57
+ expect(Path2D).toHaveBeenCalled();
58
+ expect(pathInstance.moveTo).toHaveBeenCalledWith(sourceData.x, sourceData.y);
59
+ expect(pathInstance.bezierCurveTo).toHaveBeenCalledWith(sourceData.x, 34.2, 15.8, 30, 16.8, sourceData.y);
60
+ expect(context.stroke).toHaveBeenCalledWith(pathInstance);
61
+ expect(context.beginPath).toHaveBeenCalled();
62
+ expect(context.moveTo).toHaveBeenCalledWith(19, 30);
63
+ expect(context.lineTo).toHaveBeenNthCalledWith(1, 16.8, 29.12);
64
+ expect(context.lineTo).toHaveBeenNthCalledWith(2, 16.8, 30.88);
65
+ expect(context.lineTo).toHaveBeenNthCalledWith(3, 19, 30);
66
+ expect(context.closePath).toHaveBeenCalled();
67
+ expect(context.fill).toHaveBeenCalled();
68
+ });
69
+
70
+ it('should call context methods for doubleArrow edge correctly', () => {
71
+ drawSelfRelationLoop(context, {...edgeData, type: 'doubleArrow'}, sourceData);
72
+ expect(Path2D).toHaveBeenCalled();
73
+ expect(context.beginPath).toHaveBeenCalledTimes(2);
74
+ expect(context.fill).toHaveBeenCalledTimes(2);
75
+ expect(context.closePath).toHaveBeenCalledTimes(2);
76
+ expect(context.moveTo).toHaveBeenNthCalledWith(1, sourceData.x, 31);
77
+ expect(context.lineTo).toHaveBeenNthCalledWith(1, 19.119999999999997, 33.2);
78
+ expect(context.lineTo).toHaveBeenNthCalledWith(2, 20.880000000000003, 33.2);
79
+ expect(context.lineTo).toHaveBeenNthCalledWith(3, sourceData.x, 31);
80
+ expect(pathInstance.moveTo).toHaveBeenCalledWith(sourceData.x, 33.2);
81
+ expect(pathInstance.bezierCurveTo).toHaveBeenCalledWith(sourceData.x, 34.2, 15.8, 30, 16.8, sourceData.y);
82
+ expect(context.stroke).toHaveBeenCalledWith(pathInstance);
83
+ expect(context.moveTo).toHaveBeenNthCalledWith(2, 19, sourceData.y);
84
+ expect(context.lineTo).toHaveBeenNthCalledWith(4, 16.8, 29.12);
85
+ expect(context.lineTo).toHaveBeenNthCalledWith(5, 16.8, 30.88);
86
+ expect(context.lineTo).toHaveBeenNthCalledWith(6, 19, sourceData.y);
87
+ });
88
+ });
@@ -0,0 +1,33 @@
1
+ import {drawTooltip} from '../tooltip';
2
+
3
+ describe('tests', () => {
4
+ const context = {
5
+ beginPath: jest.fn(),
6
+ moveTo: jest.fn(),
7
+ fill: jest.fn(),
8
+ measureText: jest.fn((text) => ({width: text.length * 5})),
9
+ arcTo: jest.fn(),
10
+ fillText: jest.fn()
11
+ } as unknown as CanvasRenderingContext2D;
12
+
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ it('drawTooltip should work correctly', () => {
18
+ const label = 'label';
19
+ drawTooltip(context, 600, 600, label);
20
+ expect(context.beginPath).toHaveBeenCalledTimes(1);
21
+ expect(context.measureText).toHaveBeenCalledWith(label);
22
+ expect(context.moveTo).toHaveBeenCalledWith(579.5, 592);
23
+ expect(context.arcTo).toHaveBeenNthCalledWith(1, 579.5, 612, 583.5, 612, 4);
24
+ expect(context.arcTo).toHaveBeenNthCalledWith(2, 620.5, 612, 620.5, 608, 4);
25
+ expect(context.arcTo).toHaveBeenNthCalledWith(3, 620.5, 588, 616.5, 588, 4);
26
+ expect(context.arcTo).toHaveBeenNthCalledWith(4, 579.5, 588, 579.5, 592, 4);
27
+ expect(context.fill).toHaveBeenCalled();
28
+ expect(context.fillText).toHaveBeenCalledWith(label, 600, 600);
29
+ expect(context.font).toBe('normal 10px Roboto');
30
+ expect(context.textAlign).toBe('center');
31
+ expect(context.textBaseline).toBe('middle');
32
+ });
33
+ });