@cyvest/cyvest-vis 3.2.0 → 4.1.0
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/README.md +122 -10
- package/dist/index.d.mts +156 -6
- package/dist/index.d.ts +156 -6
- package/dist/index.js +1485 -700
- package/dist/index.mjs +1494 -712
- package/package.json +9 -9
- package/src/components/CyvestGraph.tsx +115 -46
- package/src/components/FloatingEdge.tsx +41 -11
- package/src/components/Icons.tsx +730 -0
- package/src/components/InvestigationGraph.tsx +120 -42
- package/src/components/InvestigationNode.tsx +129 -112
- package/src/components/ObservableNode.tsx +116 -135
- package/src/components/ObservablesGraph.tsx +258 -123
- package/src/hooks/useForceLayout.ts +136 -62
- package/src/index.ts +25 -2
- package/src/types.ts +9 -11
- package/src/utils/observables.ts +28 -115
- package/tests/observables.test.ts +13 -21
- package/vitest.config.ts +14 -0
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyvest/cyvest-vis",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"main": "dist/index.cjs",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"peerDependencies": {
|
|
9
|
-
"react": "^
|
|
10
|
-
"react-dom": "^
|
|
9
|
+
"react": "^19.0.0",
|
|
10
|
+
"react-dom": "^19.0.0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@dagrejs/dagre": "^1.1.
|
|
14
|
-
"@xyflow/react": "^12.
|
|
13
|
+
"@dagrejs/dagre": "^1.1.8",
|
|
14
|
+
"@xyflow/react": "^12.10.0",
|
|
15
15
|
"d3-force": "^3.0.0",
|
|
16
|
-
"@cyvest/cyvest-js": "
|
|
16
|
+
"@cyvest/cyvest-js": "4.1.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/d3-force": "^3.0.10",
|
|
20
20
|
"@types/react": "^19.2.7",
|
|
21
21
|
"@types/react-dom": "^19.2.3",
|
|
22
|
-
"tsup": "^8.
|
|
23
|
-
"typescript": "^5.
|
|
24
|
-
"vitest": "^
|
|
22
|
+
"tsup": "^8.5.1",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^4.0.16"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=20"
|
|
@@ -2,63 +2,128 @@
|
|
|
2
2
|
* CyvestGraph component - combined view with toggle between observables and investigation views.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React, { useState, useCallback } from "react";
|
|
5
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
6
6
|
import type { CyvestGraphProps, InvestigationNodeType } from "../types";
|
|
7
7
|
import { ObservablesGraph } from "./ObservablesGraph";
|
|
8
8
|
import { InvestigationGraph } from "./InvestigationGraph";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* View toggle button component.
|
|
11
|
+
* View toggle button component with modern design.
|
|
12
12
|
*/
|
|
13
13
|
const ViewToggle: React.FC<{
|
|
14
14
|
currentView: "observables" | "investigation";
|
|
15
15
|
onChange: (view: "observables" | "investigation") => void;
|
|
16
16
|
}> = ({ currentView, onChange }) => {
|
|
17
|
+
const containerStyle: React.CSSProperties = useMemo(
|
|
18
|
+
() => ({
|
|
19
|
+
position: "absolute",
|
|
20
|
+
top: 12,
|
|
21
|
+
left: 12,
|
|
22
|
+
display: "flex",
|
|
23
|
+
gap: 2,
|
|
24
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
25
|
+
backdropFilter: "blur(8px)",
|
|
26
|
+
padding: 4,
|
|
27
|
+
borderRadius: 10,
|
|
28
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.1)",
|
|
29
|
+
zIndex: 10,
|
|
30
|
+
fontFamily:
|
|
31
|
+
"'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
32
|
+
border: "1px solid rgba(0,0,0,0.06)",
|
|
33
|
+
}),
|
|
34
|
+
[]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const getButtonStyle = useCallback(
|
|
38
|
+
(isActive: boolean): React.CSSProperties => ({
|
|
39
|
+
padding: "8px 14px",
|
|
40
|
+
border: "none",
|
|
41
|
+
borderRadius: 7,
|
|
42
|
+
cursor: "pointer",
|
|
43
|
+
fontSize: 12,
|
|
44
|
+
fontWeight: isActive ? 600 : 500,
|
|
45
|
+
background: isActive
|
|
46
|
+
? "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)"
|
|
47
|
+
: "transparent",
|
|
48
|
+
color: isActive ? "white" : "#4b5563",
|
|
49
|
+
transition: "all 0.15s ease",
|
|
50
|
+
letterSpacing: "-0.01em",
|
|
51
|
+
}),
|
|
52
|
+
[]
|
|
53
|
+
);
|
|
54
|
+
|
|
17
55
|
return (
|
|
18
|
-
<div
|
|
19
|
-
style={{
|
|
20
|
-
position: "absolute",
|
|
21
|
-
top: 10,
|
|
22
|
-
left: 10,
|
|
23
|
-
display: "flex",
|
|
24
|
-
gap: 4,
|
|
25
|
-
background: "white",
|
|
26
|
-
padding: 4,
|
|
27
|
-
borderRadius: 8,
|
|
28
|
-
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
|
29
|
-
zIndex: 10,
|
|
30
|
-
fontFamily: "system-ui, sans-serif",
|
|
31
|
-
}}
|
|
32
|
-
>
|
|
56
|
+
<div style={containerStyle}>
|
|
33
57
|
<button
|
|
34
58
|
onClick={() => onChange("observables")}
|
|
35
|
-
style={
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
style={getButtonStyle(currentView === "observables")}
|
|
60
|
+
onMouseEnter={(e) => {
|
|
61
|
+
if (currentView !== "observables") {
|
|
62
|
+
e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
|
|
63
|
+
e.currentTarget.style.color = "#3b82f6";
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
onMouseLeave={(e) => {
|
|
67
|
+
if (currentView !== "observables") {
|
|
68
|
+
e.currentTarget.style.background = "transparent";
|
|
69
|
+
e.currentTarget.style.color = "#4b5563";
|
|
70
|
+
}
|
|
44
71
|
}}
|
|
45
72
|
>
|
|
46
|
-
|
|
73
|
+
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
74
|
+
<svg
|
|
75
|
+
width="14"
|
|
76
|
+
height="14"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
strokeWidth="2"
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
>
|
|
84
|
+
<circle cx="12" cy="12" r="3" />
|
|
85
|
+
<circle cx="12" cy="12" r="10" />
|
|
86
|
+
<line x1="12" y1="2" x2="12" y2="4" />
|
|
87
|
+
<line x1="12" y1="20" x2="12" y2="22" />
|
|
88
|
+
<line x1="2" y1="12" x2="4" y2="12" />
|
|
89
|
+
<line x1="20" y1="12" x2="22" y2="12" />
|
|
90
|
+
</svg>
|
|
91
|
+
Observables
|
|
92
|
+
</span>
|
|
47
93
|
</button>
|
|
48
94
|
<button
|
|
49
95
|
onClick={() => onChange("investigation")}
|
|
50
|
-
style={
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
style={getButtonStyle(currentView === "investigation")}
|
|
97
|
+
onMouseEnter={(e) => {
|
|
98
|
+
if (currentView !== "investigation") {
|
|
99
|
+
e.currentTarget.style.background = "rgba(59, 130, 246, 0.1)";
|
|
100
|
+
e.currentTarget.style.color = "#3b82f6";
|
|
101
|
+
}
|
|
102
|
+
}}
|
|
103
|
+
onMouseLeave={(e) => {
|
|
104
|
+
if (currentView !== "investigation") {
|
|
105
|
+
e.currentTarget.style.background = "transparent";
|
|
106
|
+
e.currentTarget.style.color = "#4b5563";
|
|
107
|
+
}
|
|
59
108
|
}}
|
|
60
109
|
>
|
|
61
|
-
|
|
110
|
+
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
111
|
+
<svg
|
|
112
|
+
width="14"
|
|
113
|
+
height="14"
|
|
114
|
+
viewBox="0 0 24 24"
|
|
115
|
+
fill="none"
|
|
116
|
+
stroke="currentColor"
|
|
117
|
+
strokeWidth="2"
|
|
118
|
+
strokeLinecap="round"
|
|
119
|
+
strokeLinejoin="round"
|
|
120
|
+
>
|
|
121
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
122
|
+
<path d="M9 3v18" />
|
|
123
|
+
<path d="M3 9h18" />
|
|
124
|
+
</svg>
|
|
125
|
+
Investigation
|
|
126
|
+
</span>
|
|
62
127
|
</button>
|
|
63
128
|
</div>
|
|
64
129
|
);
|
|
@@ -76,7 +141,9 @@ export const CyvestGraph: React.FC<CyvestGraphProps> = ({
|
|
|
76
141
|
className,
|
|
77
142
|
showViewToggle = true,
|
|
78
143
|
}) => {
|
|
79
|
-
const [view, setView] = useState<"observables" | "investigation">(
|
|
144
|
+
const [view, setView] = useState<"observables" | "investigation">(
|
|
145
|
+
initialView
|
|
146
|
+
);
|
|
80
147
|
|
|
81
148
|
const handleNodeClick = useCallback(
|
|
82
149
|
(nodeId: string, _nodeType?: InvestigationNodeType) => {
|
|
@@ -85,15 +152,17 @@ export const CyvestGraph: React.FC<CyvestGraphProps> = ({
|
|
|
85
152
|
[onNodeClick]
|
|
86
153
|
);
|
|
87
154
|
|
|
155
|
+
const containerStyle: React.CSSProperties = useMemo(
|
|
156
|
+
() => ({
|
|
157
|
+
width,
|
|
158
|
+
height,
|
|
159
|
+
position: "relative",
|
|
160
|
+
}),
|
|
161
|
+
[width, height]
|
|
162
|
+
);
|
|
163
|
+
|
|
88
164
|
return (
|
|
89
|
-
<div
|
|
90
|
-
className={className}
|
|
91
|
-
style={{
|
|
92
|
-
width,
|
|
93
|
-
height,
|
|
94
|
-
position: "relative",
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
165
|
+
<div className={className} style={containerStyle}>
|
|
97
166
|
{showViewToggle && <ViewToggle currentView={view} onChange={setView} />}
|
|
98
167
|
|
|
99
168
|
{view === "observables" ? (
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Floating Edge component for use with force-directed layout.
|
|
3
|
-
* Uses
|
|
3
|
+
* Uses smooth bezier curves that connect to node centers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { memo } from "react";
|
|
7
|
-
import { BaseEdge,
|
|
6
|
+
import React, { memo, useMemo } from "react";
|
|
7
|
+
import { BaseEdge, getBezierPath, type EdgeProps } from "@xyflow/react";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Calculate control point offset based on distance.
|
|
11
|
+
* Longer edges get more curve for better visibility.
|
|
12
|
+
*/
|
|
13
|
+
function getControlOffset(
|
|
14
|
+
sourceX: number,
|
|
15
|
+
sourceY: number,
|
|
16
|
+
targetX: number,
|
|
17
|
+
targetY: number
|
|
18
|
+
): number {
|
|
19
|
+
const dx = targetX - sourceX;
|
|
20
|
+
const dy = targetY - sourceY;
|
|
21
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
22
|
+
// Scale curve intensity with distance, capped
|
|
23
|
+
return Math.min(Math.max(distance * 0.15, 20), 60);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Floating edge component with smooth bezier curves.
|
|
28
|
+
* The curve adapts to the edge length for optimal visibility.
|
|
12
29
|
*/
|
|
13
30
|
function FloatingEdgeComponent({
|
|
14
31
|
id,
|
|
@@ -18,23 +35,36 @@ function FloatingEdgeComponent({
|
|
|
18
35
|
targetY,
|
|
19
36
|
style,
|
|
20
37
|
markerEnd,
|
|
38
|
+
selected,
|
|
21
39
|
}: EdgeProps) {
|
|
22
|
-
const
|
|
40
|
+
const offset = useMemo(
|
|
41
|
+
() => getControlOffset(sourceX, sourceY, targetX, targetY),
|
|
42
|
+
[sourceX, sourceY, targetX, targetY]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const [edgePath] = getBezierPath({
|
|
23
46
|
sourceX,
|
|
24
47
|
sourceY,
|
|
25
48
|
targetX,
|
|
26
49
|
targetY,
|
|
50
|
+
curvature: 0.15,
|
|
27
51
|
});
|
|
28
52
|
|
|
53
|
+
const edgeStyle = useMemo(
|
|
54
|
+
() => ({
|
|
55
|
+
strokeWidth: selected ? 2.5 : 1.5,
|
|
56
|
+
stroke: selected ? "#3b82f6" : "#94a3b8",
|
|
57
|
+
transition: "stroke 0.15s ease, stroke-width 0.15s ease",
|
|
58
|
+
...style,
|
|
59
|
+
}),
|
|
60
|
+
[selected, style]
|
|
61
|
+
);
|
|
62
|
+
|
|
29
63
|
return (
|
|
30
64
|
<BaseEdge
|
|
31
65
|
id={id}
|
|
32
66
|
path={edgePath}
|
|
33
|
-
style={
|
|
34
|
-
strokeWidth: 1.5,
|
|
35
|
-
stroke: "#94a3b8",
|
|
36
|
-
...style,
|
|
37
|
-
}}
|
|
67
|
+
style={edgeStyle}
|
|
38
68
|
markerEnd={markerEnd}
|
|
39
69
|
/>
|
|
40
70
|
);
|