@cyber-harbour/ui 1.0.33 → 1.0.35

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-harbour/ui",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "@types/d3-force": "^3.0.10",
31
31
  "d3-force": "^3.0.0",
32
+ "react-content-loader": "^7.0.2",
32
33
  "react-force-graph-2d": "^1.27.1",
33
34
  "react-tiny-popover": "^8.1.6",
34
35
  "styled-components": "^6.1.18"
@@ -50,6 +50,8 @@ export const RowActionsMenu = ({
50
50
  color={color}
51
51
  fullWidth
52
52
  size={size}
53
+ py={10}
54
+ px={5}
53
55
  onClick={() => {
54
56
  onClick();
55
57
  closeMenu();
@@ -1,26 +1,57 @@
1
1
  import styled from 'styled-components';
2
+ import { pxToRem } from '../Theme';
2
3
 
3
4
  interface FullscreenCardProps {
4
5
  children: any;
6
+ className?: string;
5
7
  position: 'absolute' | 'fixed';
6
8
  isActive: boolean;
9
+ top?: number;
10
+ left?: number;
11
+ right?: number;
12
+ bottom?: number;
7
13
  }
8
14
 
9
- export const FullscreenCard = ({ isActive, position, ...props }: FullscreenCardProps) => {
10
- return <StyledContainer $isActive={isActive} $position={position} {...props} />;
15
+ export const FullscreenCard = ({
16
+ isActive,
17
+ position,
18
+ top = 0,
19
+ left = 0,
20
+ right,
21
+ bottom,
22
+ ...props
23
+ }: FullscreenCardProps) => {
24
+ return (
25
+ <StyledContainer
26
+ $isActive={isActive}
27
+ $position={position}
28
+ $top={top}
29
+ $left={left}
30
+ $right={right}
31
+ $bottom={bottom}
32
+ {...props}
33
+ />
34
+ );
11
35
  };
12
36
 
13
- const StyledContainer = styled.div<{ $isActive: boolean; $position: 'absolute' | 'fixed' }>(
14
- ({ $isActive, $position }) => `
37
+ const StyledContainer = styled.div<{
38
+ $isActive: boolean;
39
+ $top?: number;
40
+ $left?: number;
41
+ $right?: number;
42
+ $bottom?: number;
43
+ $position: 'absolute' | 'fixed';
44
+ }>(
45
+ ({ $isActive, $top, $left, $right, $bottom, $position, theme }) => `
15
46
  ${
16
47
  $isActive
17
48
  ? `
18
49
  position: ${$position};
19
- top: 0;
20
- left: 0;
21
- height: 100%;
22
- width: 100%;
23
50
  z-index: 1000;
51
+ ${$top ? `top: ${pxToRem($top, theme.baseSize)};` : ''}
52
+ ${$left ? `left: ${pxToRem($left, theme.baseSize)};` : ''}
53
+ ${$right ? `right: ${pxToRem($right, theme.baseSize)};` : ''}
54
+ ${$bottom ? `bottom: ${pxToRem($bottom, theme.baseSize)};` : ''}
24
55
  `
25
56
  : ''
26
57
  }
@@ -5,10 +5,9 @@ import { forceCollide } from 'd3-force';
5
5
  import { styled } from 'styled-components';
6
6
  import eyeLightIcon from './eye_light.png';
7
7
  import eyeLightHoverIcon from './eye_light_hover.png';
8
- import crossLightIcon from './cross_light.png';
9
- import crossLightHoverIcon from './cross_light_hover.png';
10
-
11
- const ALPHA_MIN = 0.5;
8
+ import groupLightIcon from './group_light.png';
9
+ import groupLightHoverIcon from './group_light_hover.png';
10
+ import GraphLoader from './GraphLoader';
12
11
 
13
12
  // Створюємо та налаштовуємо об'єкти зображень
14
13
  const imgEyeLightIcon = new Image();
@@ -17,11 +16,11 @@ imgEyeLightIcon.src = eyeLightIcon;
17
16
  const imgEyeLightHoverIcon = new Image();
18
17
  imgEyeLightHoverIcon.src = eyeLightHoverIcon;
19
18
 
20
- const imgCrossLightIcon = new Image();
21
- imgCrossLightIcon.src = crossLightIcon;
19
+ const imgGroupLightIcon = new Image();
20
+ imgGroupLightIcon.src = groupLightIcon;
22
21
 
23
- const imgCrossLightHoverIcon = new Image();
24
- imgCrossLightHoverIcon.src = crossLightHoverIcon;
22
+ const imgGroupLightHoverIcon = new Image();
23
+ imgGroupLightHoverIcon.src = groupLightHoverIcon;
25
24
 
26
25
  export const Graph2D = ({
27
26
  graphData,
@@ -29,6 +28,7 @@ export const Graph2D = ({
29
28
  height,
30
29
  linkTarget,
31
30
  linkSource,
31
+ loading = false,
32
32
  config = {
33
33
  fontSize: 3, // Максимальний розмір шрифту при максимальному зумі
34
34
  nodeSizeBase: 30, // Базовий розмір вузла
@@ -83,7 +83,7 @@ export const Graph2D = ({
83
83
  };
84
84
 
85
85
  // Обробка подій наведення на вузол
86
- const handleNodeHover = useCallback((node: NodeObject | null, _: NodeObject | null) => {
86
+ const handleNodeHover = (node: NodeObject | null, _: NodeObject | null) => {
87
87
  const newHighlightNodes = new Set();
88
88
  const newHighlightLinks = new Set();
89
89
 
@@ -106,10 +106,10 @@ export const Graph2D = ({
106
106
  setHoverNode(node || null);
107
107
  setHighlightNodes(newHighlightNodes);
108
108
  setHighlightLinks(newHighlightLinks);
109
- }, []);
109
+ };
110
110
 
111
111
  // Обробка подій наведення на зв'язок
112
- const handleLinkHover = useCallback((link: any) => {
112
+ const handleLinkHover = (link: any) => {
113
113
  const newHighlightNodes = new Set();
114
114
  const newHighlightLinks = new Set();
115
115
 
@@ -122,7 +122,7 @@ export const Graph2D = ({
122
122
 
123
123
  setHighlightNodes(newHighlightNodes);
124
124
  setHighlightLinks(newHighlightLinks);
125
- }, []);
125
+ };
126
126
 
127
127
  const handleEngineTick = useCallback(() => {
128
128
  if (isRendering)
@@ -131,7 +131,7 @@ export const Graph2D = ({
131
131
  fgRef.current &&
132
132
  fgRef.current.tick &&
133
133
  graphData.nodes.length > 0 &&
134
- graphData.nodes.length * ALPHA_MIN <= fgRef.current.tick
134
+ graphData.nodes.length <= fgRef.current.tick
135
135
  ) {
136
136
  if (tickTimerRef.current) {
137
137
  clearTimeout(tickTimerRef.current);
@@ -152,7 +152,7 @@ export const Graph2D = ({
152
152
  }, [graphData]);
153
153
 
154
154
  // Створення взаємозв'язків між вузлами
155
- useLayoutEffect(() => {
155
+ useEffect(() => {
156
156
  if (!graphData) return;
157
157
 
158
158
  // Прив'язка вузлів до їхніх сусідів та зв'язків
@@ -176,33 +176,14 @@ export const Graph2D = ({
176
176
  target.links.push(link);
177
177
  });
178
178
 
179
- // Налаштування відстані між вузлами
180
- fgRef.current?.d3Force('link')?.distance((link: any) => {
181
- // Отримуємо вузли на кінцях зв'язку
182
- const source = link.source;
183
- const target = link.target;
184
-
185
- // Базова відстань
186
- const baseDistance = config.nodeSizeBase * 2;
187
-
188
- // Динамічна відстань на основі розміру вузлів
189
- // Більші вузли повинні бути далі один від одного
190
- const sourceSizeBase = source.size || config.nodeSizeBase;
191
- const targetSizeBase = target.size || config.nodeSizeBase;
192
-
193
- // Відстань залежить від суми розмірів вузлів
194
- // Додаємо базову відстань 100
195
- return baseDistance + (sourceSizeBase + targetSizeBase);
196
- });
197
-
198
179
  // Додаємо різні сили для уникнення перекриття вузлів
199
180
  if (fgRef.current) {
200
181
  // 1. Додаємо силу відштовхування між всіма вузлами (charge force)
201
182
  const chargeForce = fgRef.current.d3Force('charge');
202
183
  if (chargeForce) {
203
184
  chargeForce
204
- .strength(-100) // Збільшуємо силу відштовхування (negative for repulsion)
205
- .distanceMax(100); // Максимальна дистанція, на якій діє ця сила
185
+ .strength(config.nodeSizeBase) // Збільшуємо силу відштовхування (negative for repulsion)
186
+ .distanceMax(50); // Максимальна дистанція, на якій діє ця сила
206
187
  }
207
188
 
208
189
  // 2. Додаємо силу центрування для кращої організації графа
@@ -222,13 +203,21 @@ export const Graph2D = ({
222
203
  .iterations(3) // Більше ітерацій для точнішого розрахунку
223
204
  .strength(1); // Максимальна сила (1 - тверде обмеження)
224
205
 
225
- fgRef.current.pauseAnimation().d3Force('collide', collideForce).resumeAnimation();
206
+ fgRef.current.d3Force('collide', collideForce);
226
207
  } catch (err) {
227
208
  console.error('Error setting up collision force:', err);
228
209
  }
229
210
  }
230
211
  }, [graphData]);
231
212
 
213
+ useEffect(() => {
214
+ if (!isRendering && fgRef.current) {
215
+ setIsRendering(true);
216
+ fgRef.current.tick = 0;
217
+ fgRef.current.d3ReheatSimulation();
218
+ }
219
+ }, [graphData]);
220
+
232
221
  // Функція для малювання кільця навколо підсвічених вузлів
233
222
  const paintRing = useCallback(
234
223
  (node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
@@ -283,51 +272,52 @@ export const Graph2D = ({
283
272
  const iconSize = buttonRadius * 0.3; // Розмір іконки відносно радіуса кнопки (зменшено вдвічі)
284
273
 
285
274
  // Вибір іконки в залежності від стану наведення для верхньої кнопки (сховати)
286
- const crossIcon = hoverTopButton ? imgCrossLightHoverIcon : imgCrossLightIcon;
287
- const renderCrossIcon = () => {
275
+ const groupIcon = hoverTopButton ? imgGroupLightHoverIcon : imgGroupLightIcon;
276
+ // Додаємо іконку ока для кнопки "згорнути дочірні вузли"
277
+ const eyeIcon = hoverBottomButton ? imgEyeLightHoverIcon : imgEyeLightIcon;
278
+
279
+ const renderEyeIcon = () => {
288
280
  try {
289
- ctx.drawImage(crossIcon, x - iconSize / 2, y - (buttonRadius * 2) / 4 - iconSize - 1, iconSize, iconSize);
281
+ ctx.drawImage(eyeIcon, x - iconSize / 2, y - (buttonRadius * 2) / 4 - iconSize - 1, iconSize, iconSize);
290
282
  } catch (error) {
291
- console.log('Error rendering cross icon:', error);
283
+ console.warn('Error rendering group icon:', error);
292
284
  }
293
285
  };
294
286
  // Використовуємо безпосередньо зображення, якщо воно вже завантажене
295
- if (crossIcon.complete) {
287
+ if (eyeIcon.complete) {
296
288
  // Розміщуємо іконку в центрі верхньої половини кнопки
297
- renderCrossIcon();
289
+ renderEyeIcon();
298
290
  } else {
299
291
  // Якщо зображення ще не завантажене, додаємо обробник завершення завантаження
300
- crossIcon.onload = () => {
301
- renderCrossIcon();
292
+ eyeIcon.onload = () => {
293
+ renderEyeIcon();
302
294
  };
303
295
 
304
- crossIcon.onerror = () => {
305
- console.log('Error loading cross icon image');
296
+ eyeIcon.onerror = () => {
297
+ console.warn('Error loading group icon image');
306
298
  };
307
299
  }
308
300
 
309
- // Додаємо іконку ока для кнопки "згорнути дочірні вузли"
310
- const eyeIcon = hoverBottomButton ? imgEyeLightHoverIcon : imgEyeLightIcon;
311
- const renderEyeIcon = () => {
301
+ const renderGroupIcon = () => {
312
302
  try {
313
- ctx.drawImage(eyeIcon, x - iconSize / 2, y + (buttonRadius * 2) / 4 + 1, iconSize, iconSize);
303
+ ctx.drawImage(groupIcon, x - iconSize / 2, y + (buttonRadius * 2) / 4 + 1, iconSize, iconSize);
314
304
  } catch (error) {
315
- console.log('Error rendering eye icon:', error);
305
+ console.warn('Error rendering eye icon:', error);
316
306
  }
317
307
  };
318
308
  // Використовуємо безпосередньо зображення, якщо воно вже завантажене
319
309
  if (eyeIcon.complete) {
320
310
  // Розміщуємо іконку в центрі нижньої половини кнопки
321
311
 
322
- renderEyeIcon();
312
+ renderGroupIcon();
323
313
  } else {
324
314
  // Якщо зображення ще не завантажене, додаємо обробник завершення завантаження
325
315
  eyeIcon.onload = () => {
326
- renderEyeIcon();
316
+ renderGroupIcon();
327
317
  };
328
318
 
329
319
  eyeIcon.onerror = () => {
330
- console.log('Error loading eye icon image');
320
+ console.warn('Error loading eye icon image');
331
321
  };
332
322
  }
333
323
 
@@ -921,10 +911,12 @@ export const Graph2D = ({
921
911
 
922
912
  const handleBackgroundClick = (event: MouseEvent) => {
923
913
  setSelectedNode(null);
914
+ onBackgroundClick?.();
924
915
  };
925
916
 
926
917
  return (
927
918
  <Wrapper ref={wrapperRef}>
919
+ {(loading || isRendering) && <GraphLoader width={width} height={height} />}
928
920
  <ForceGraph2D
929
921
  ref={fgRef}
930
922
  width={width}
@@ -948,7 +940,7 @@ export const Graph2D = ({
948
940
  onNodeHover={handleNodeHover}
949
941
  onLinkHover={handleLinkHover}
950
942
  onEngineTick={handleEngineTick}
951
- d3AlphaMin={ALPHA_MIN}
943
+ d3AlphaMin={0.001}
952
944
  d3VelocityDecay={0.4}
953
945
  d3AlphaDecay={0.038}
954
946
  // Виділення зв'язків при наведенні
@@ -984,5 +976,8 @@ export const Graph2D = ({
984
976
  };
985
977
 
986
978
  const Wrapper = styled.div`
987
- display: inline-block;
979
+ display: block;
980
+ width: 100%;
981
+ min-width: 0;
982
+ position: relative;
988
983
  `;
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import ContentLoader from 'react-content-loader';
3
+ import styled from 'styled-components';
4
+
5
+ const LoaderWrapper = styled.div`
6
+ position: absolute;
7
+ top: 0;
8
+ left: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ z-index: 10;
15
+ `;
16
+
17
+ interface GraphLoaderProps {
18
+ width?: number;
19
+ height?: number;
20
+ }
21
+
22
+ const GraphLoader: React.FC<GraphLoaderProps> = ({ width = 280, height = 280 }) => {
23
+ // Helper function to create a rect from line coordinates
24
+ const lineToRect = (x1: number, y1: number, x2: number, y2: number, thickness: number = 1) => {
25
+ // Calculate length and angle of the line
26
+ const length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
27
+ const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
28
+
29
+ // Calculate center point of the line
30
+ const centerX = (x1 + x2) / 2;
31
+ const centerY = (y1 + y2) / 2;
32
+
33
+ return (
34
+ <rect
35
+ x={centerX - length / 2}
36
+ y={centerY - thickness / 2}
37
+ width={length}
38
+ height={thickness}
39
+ transform={`rotate(${angle}, ${centerX}, ${centerY})`}
40
+ />
41
+ );
42
+ };
43
+
44
+ return (
45
+ <LoaderWrapper>
46
+ <ContentLoader width={width} height={height} viewBox="0 0 280 280">
47
+ <path d="m55 38-0.97266 0.22852 7.0801 30.092-18.355-20.979-0.75195 0.6582 19.596 22.395 0.43164 1.834 0.97266-0.22852 0.75195-0.6582-0.37695-0.42969 9.625-27.912-0.94531-0.32617-9.4375 27.371-0.10547-0.12109zm8 34-0.78516 0.61914 0.0957 0.12305-12.311 13.258 0.73242 0.67969 12.205-13.145 14.277 18.084 0.78516-0.61914-14.373-18.207 0.10547-0.11328zm15 19-0.48438 0.875 46.992 25.996 8e-3 4e-3 20.506 11.592-28.182 4.5449 0.15998 0.98819 29.418-4.7441 0.25 0.14062-12.555 24.143 0.88672 0.46094 13-25 15 25 26 30v18l-11 18 0.85352 0.52148 9.8008-16.039-4.6543 33.518 0.99023 0.13867 4.7793-34.408 7.2305 16.27 0.91406-0.40625-7.9141-17.807v-17.104l18 12.316 0.56445-0.82617-18.896-12.928-25.855-29.836-14.633-24.387 0.01562-0.02344h23.805v-1h-23.152l13.848-21.234 55.201-28.791-0.45898-0.88476 0.77734 0.62305 11.402-14.25 16.668-11.842-0.58008-0.81641-16.785 11.928-11.486 14.355-55.434 28.912-14.277 21.893-7.7617-27.166-0.96094 0.27344 7.7227 27.031-1.1191 0.17969-21.604-12.211zm140.43-12.912-6.957-17.338-0.92773 0.37305 6.957 17.338zm-6.957-17.338 0.72266 0.69336 16.232-16.896-0.7207-0.69336zm-3.4766 137.25 5 15 0.94922-0.31641-5-15zm-91-63-0.48047-0.87695-31 17 0.48047 0.87695zm-31 17 5 18 0.96289-0.26758-5-18zm0 0-0.70703-0.70703-12.898 12.898-17.881 9.9336 0.48633 0.875 18-10zm5 18-0.64062-0.76758-18 15 0.64062 0.76758z" />
48
+ <circle cx="229.92" cy="63.7318" r="5" transform="rotate(173.661 229.92 63.7318)" />
49
+ <circle cx="227.711" cy="43.8541" r="5" transform="rotate(173.661 227.711 43.8541)" />
50
+ <circle cx="211.478" cy="60.7499" r="5" transform="rotate(173.661 211.478 60.7499)" />
51
+ <circle cx="218.434" cy="78.0877" r="5" transform="rotate(173.661 218.434 78.0877)" />
52
+ <circle cx="246.705" cy="51.8054" r="5" transform="rotate(173.661 246.705 51.8054)" />
53
+ <circle cx="42" cy="48" r="5" />
54
+ <circle cx="55" cy="38" r="5" />
55
+ <circle cx="73" cy="43" r="5" />
56
+ <circle cx="63" cy="72" r="5" />
57
+ <circle cx="50" cy="86" r="5" />
58
+ <circle cx="78" cy="91" r="5" />
59
+ <circle cx="73" cy="165" r="5" />
60
+ <circle cx="73" cy="185" r="5" />
61
+ <circle cx="91" cy="170" r="5" />
62
+ <circle cx="86" cy="152" r="5" />
63
+ <circle cx="148" cy="130" r="5" />
64
+ <circle cx="189" cy="185" r="5" />
65
+ <circle cx="163" cy="107" r="5" />
66
+ <circle cx="140" cy="102" r="5" />
67
+ <circle cx="117" cy="135" r="5" />
68
+ <circle cx="125" cy="117" r="5" />
69
+ <circle cx="208" cy="198" r="5" />
70
+ <circle cx="189" cy="203" r="5" />
71
+ <circle cx="198" cy="221" r="5" />
72
+ <circle cx="178" cy="221" r="5" />
73
+ <circle cx="184" cy="239" r="5" />
74
+ <circle cx="213" cy="213" r="5" />
75
+ <circle cx="173" cy="130" r="5" />
76
+ <circle cx="163" cy="155" r="5" />
77
+ <circle cx="135" cy="155" r="5" />
78
+ <circle cx="55" cy="175" r="5" />
79
+ </ContentLoader>
80
+ </LoaderWrapper>
81
+ );
82
+ };
83
+
84
+ export default GraphLoader;
Binary file
Binary file
Binary file
@@ -4,6 +4,7 @@ export interface Graph2DProps {
4
4
  graphData?: GraphData;
5
5
  linkSource?: string;
6
6
  linkTarget?: string;
7
+ loading?: boolean;
7
8
 
8
9
  // Container layout
9
10
  width?: number;
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file