diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java index 3b86e14f05..7f000cc7c0 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java @@ -35,6 +35,9 @@ public class InjectOutput { @Min(value = 0L, message = "The value must be positive") private Long dependsDuration; + @JsonProperty("inject_depends_on") + private String dependsOn; + @JsonProperty("inject_injector_contract") private InjectorContract injectorContract; @@ -68,6 +71,7 @@ public InjectOutput( String exerciseId, String scenarioId, Long dependsDuration, + String dependsOn, InjectorContract injectorContract, String[] tags, String[] teams, @@ -80,6 +84,7 @@ public InjectOutput( this.exercise = exerciseId; this.scenario = scenarioId; this.dependsDuration = dependsDuration; + this.dependsOn = dependsOn; this.injectorContract = injectorContract; this.tags = tags != null ? new HashSet<>(Arrays.asList(tags)) : new HashSet<>(); diff --git a/openbas-api/src/main/java/io/openbas/service/InjectService.java b/openbas-api/src/main/java/io/openbas/service/InjectService.java index 3fdaf81b05..b9b10f1750 100644 --- a/openbas-api/src/main/java/io/openbas/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/service/InjectService.java @@ -111,6 +111,7 @@ private void selectForInject(CriteriaBuilder cb, CriteriaQuery cq, Root injectScenarioJoin = createLeftJoin(injectRoot, "scenario"); Join injectorContractJoin = createLeftJoin(injectRoot, "injectorContract"); Join injectorJoin = injectorContractJoin.join("injector", JoinType.LEFT); + Join injectDependsJoin = createLeftJoin(injectRoot, "dependsOn"); // Array aggregations Expression tagIdsExpression = createJoinArrayAggOnId(cb, injectRoot, "tags"); Expression teamIdsExpression = createJoinArrayAggOnId(cb, injectRoot, "teams"); @@ -127,6 +128,7 @@ private void selectForInject(CriteriaBuilder cb, CriteriaQuery cq, Root cq, Root execInject(TypedQuery query) { tuple.get("inject_exercise", String.class), tuple.get("inject_scenario", String.class), tuple.get("inject_depends_duration", Long.class), + tuple.get("inject_depends_from_another", String.class), tuple.get("inject_injector_contract", InjectorContract.class), tuple.get("inject_tags", String[].class), tuple.get("inject_teams", String[].class), diff --git a/openbas-front/package.json b/openbas-front/package.json index d943702ada..980ae7852c 100644 --- a/openbas-front/package.json +++ b/openbas-front/package.json @@ -60,7 +60,7 @@ "react-markdown": "9.0.1", "react-redux": "8.1.3", "react-router-dom": "6.23.1", - "reactflow": "11.11.3", + "reactflow": "11.11.4", "redux": "4.2.1", "redux-first-history": "5.2.0", "redux-thunk": "2.4.2", diff --git a/openbas-front/src/admin/components/common/injects/Injects.js b/openbas-front/src/admin/components/common/injects/Injects.js index 0a39850be8..426f7e27ca 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.js +++ b/openbas-front/src/admin/components/common/injects/Injects.js @@ -1,7 +1,7 @@ import React, { useContext, useState } from 'react'; import { makeStyles } from '@mui/styles'; import { Checkbox, Chip, List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, Menu, MenuItem, ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material'; -import { BarChartOutlined, MoreVert, ReorderOutlined } from '@mui/icons-material'; +import { BarChartOutlined, MoreVert, ReorderOutlined, ViewTimelineOutlined, Link } from '@mui/icons-material'; import { CSVLink } from 'react-csv'; import { splitDuration } from '../../../../utils/Time'; import ItemTags from '../../../../components/ItemTags'; @@ -21,6 +21,7 @@ import CreateInject from './CreateInject'; import UpdateInject from './UpdateInject'; import PlatformIcon from '../../../../components/PlatformIcon'; import Timeline from '../../../../components/Timeline'; +import ChainedTimeline from '../../../../components/ChainedTimeline'; import { isNotEmptyField } from '../../../../utils/utils'; const useStyles = makeStyles(() => ({ @@ -171,6 +172,7 @@ const Injects = (props) => { selectAll, onToggleShiftEntity, handleToggleSelectAll, + onConnectInjects, } = props; // Standard hooks const classes = useStyles(); @@ -184,6 +186,7 @@ const Injects = (props) => { ); const { permissions } = useContext(PermissionsContext); const injectContext = useContext(InjectContext); + const [chainMode, setChainMode] = useState('chaining'); // Filter and sort hook const searchColumns = ['title', 'description', 'content']; @@ -311,17 +314,54 @@ const Injects = (props) => { ) : null} + {setChainMode ? ( + + + setChainMode('timeline')} + aria-label="Timeline view mode" + > + + + + + setChainMode('chaining')} + aria-label="Chaining view mode" + > + + + + + ) : null}
{showTimeline && (
- setSelectedInjectId(id)} - teams={teams} - /> -
+
+ {chainMode !== "chaining" ? ( + setSelectedInjectId(id)} + teams={teams} + /> + ) : ( + + )} +
+
)} diff --git a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx index 9c9530ae7f..5c35bca0ca 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx @@ -21,6 +21,7 @@ import ToolBar from '../../../common/ToolBar'; import { isNotEmptyField } from '../../../../../utils/utils'; import injectContextForScenario from '../ScenarioContext'; import { fetchScenarioInjectsSimple } from '../../../../../actions/injects/inject-action'; +import {Connection} from "reactflow"; interface Props { @@ -105,6 +106,17 @@ const ScenarioInjects: FunctionComponent = () => { return onToggleEntity(currentEntity, event); }; + const handleConnectInjects = async (connection: Connection) => { + const updateFields = [ + 'inject_title', + 'inject_depends_from_another', + 'inject_depends_duration', + ] + let sourceInject = injects.find((inject: Inject) => inject.inject_id === connection.source) + sourceInject.inject_depends_from_another = connection.target; + await injectContext.onUpdateInject(sourceInject.inject_id, R.pick(updateFields, sourceInject)); + } + const massUpdateInjects = async (actions: { field: string, type: string, values: { value: string }[] }[]) => { const updateFields = [ 'inject_title', @@ -181,6 +193,7 @@ const ScenarioInjects: FunctionComponent = () => { selectedElements={selectedElements} deSelectedElements={deSelectedElements} selectAll={selectAll} + onConnectInjects={handleConnectInjects} /> ({ + container: { + marginTop: 60, + paddingRight: 40, + }, + names: { + float: 'left', + width: '10%', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + lineName: { + width: '100%', + height: 50, + lineHeight: '50px', + }, + name: { + fontSize: 14, + fontWeight: 400, + display: 'flex', + alignItems: 'center', + }, + timeline: { + float: 'left', + width: '90%', + position: 'relative', + }, + line: { + position: 'relative', + width: '100%', + height: 50, + lineHeight: '50px', + padding: '0 20px 0 20px', + borderBottom: '1px solid rgba(255, 255, 255, 0.15)', + verticalAlign: 'middle', + }, + scale: { + position: 'absolute', + width: '100%', + height: '100%', + top: 0, + left: 0, + }, + tick: { + position: 'absolute', + width: 1, + }, + tickLabelTop: { + position: 'absolute', + left: -28, + top: -20, + width: 100, + fontSize: 10, + }, + tickLabelBottom: { + position: 'absolute', + left: -28, + bottom: -20, + width: 100, + fontSize: 10, + }, + injectGroup: { + position: 'absolute', + padding: '6px 5px 0 5px', + zIndex: 1000, + display: 'grid', + gridAutoFlow: 'column', + gridTemplateRows: 'repeat(2, 20px)', + }, +})); + +interface Props { + injects: InjectStore[], + onConnectInjects(connection: Connection): void +} + +const ChainedTimelineFlow: FunctionComponent = ({ injects, onConnectInjects}) => { + // Standard hooks + const classes = useStyles(); + const theme = useTheme(); + const { t } = useFormatter(); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [injectsToShow, setInjectsToShow] = useState([]); + + // Flow + const layoutOptions: LayoutOptions = { + algorithm: 'd3-hierarchy', + direction: 'LR', + spacing: [50, 50], + }; + useAutoLayoutInject(layoutOptions, injectsToShow); + const { fitView } = useReactFlow(); + useEffect(() => { + fitView(); + }, [nodes, fitView]); + + useEffect(() => { + if (injects.length > 0) { + setInjectsToShow(injects); + setNodes(injects.map((inject: InjectStore, index: number) => ({ + id: `${inject.inject_id}`, + type: 'inject', + data: { + key: inject.inject_id, + label: inject.inject_title, + color: 'green', + background: 'black', + onConnectInjects, + isTargeted: onConnectInjects !== undefined || injects.some(value => value.inject_depends_on === inject.inject_id), + isTargeting: onConnectInjects !== undefined || inject.inject_depends_on !== null + }, + position: {x: 0, y: 0}, + }))); + setEdges(injects.filter(inject => inject.inject_depends_on != null).map((inject, i) => { + return ({ + id: `${inject.inject_id}->${inject.inject_depends_on}`, + source: `${inject.inject_id}`, + sourceHandle: `source-${inject.inject_id}`, + target: `${inject.inject_depends_on}`, + targetHandle: `target-${inject.inject_depends_on}`, + label: '', + labelShowBg: false, + labelStyle: {fill: theme.palette.text?.primary, fontSize: 9}, + }) + + })); + } + }, [injects]); + const proOptions = {account: 'paid-pro', hideAttribution: true}; + const defaultEdgeOptions = { + type: 'straight', + markerEnd: {type: MarkerType.ArrowClosed}, + }; + + const reconnectDone = useRef(true); + + const onReconnect = + (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); + const edgeUpdateStart = () => { + reconnectDone.current = false; + }; + + const edgeUpdate = (oldEdge: Edge, newConnection: Connection) => { + reconnectDone.current = true; + setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); + }; + + const edgeUpdateEnd = (_: any, edge: Edge) => { + if (!reconnectDone.current) { + setEdges((eds) => eds.filter((e) => e.id !== edge.id)); + onConnectInjects({ + target: null, + source: edge.source, + sourceHandle: null, + targetHandle: null, + }) + } + }; + + return ( + <> + {injectsToShow.length > 0 ? ( +
+ +
+ ) : null + } + + ); +}; + +const ChainedTimeline: FunctionComponent = (props) => { + return ( + <> + + + + + ); +}; + +export default ChainedTimeline; diff --git a/openbas-front/src/components/nodes/NodeInject.tsx b/openbas-front/src/components/nodes/NodeInject.tsx new file mode 100644 index 0000000000..4b5cc3c143 --- /dev/null +++ b/openbas-front/src/components/nodes/NodeInject.tsx @@ -0,0 +1,79 @@ +import React, { memo } from 'react'; +import { Handle, NodeProps, Position } from 'reactflow'; +import { makeStyles } from '@mui/styles'; +import { Tooltip } from '@mui/material'; +import { FlagOutlined, HelpOutlined, ModeStandbyOutlined, ScoreOutlined } from '@mui/icons-material'; +import {Theme} from "../Theme"; + +// Deprecated - https://mui.com/system/styles/basics/ +// Do not use it for new code. +const useStyles = makeStyles((theme) => ({ + node: { + position: 'relative', + border: + theme.palette.mode === 'dark' + ? '1px solid rgba(255, 255, 255, 0.12)' + : '1px solid rgba(0, 0, 0, 0.12)', + borderRadius: 4, + width: 200, + height: 100, + padding: '8px 5px 5px 5px', + }, + icon: { + textAlign: 'center', + margin: '10px 0 10px 0', + }, + label: { + margin: '0 auto', + textAlign: 'center', + fontSize: 15, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + description: { + maxWidth: 100, + color: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, 0.5)' + : 'rgba(0, 0, 0, 0.5)', + fontSize: 8, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, +})); + +const renderIcon = (icon: string) => { + switch (icon) { + case 'attack-started': + return ; + case 'attack-ended': + return ; + case 'result': + return ; + default: + return ; + } +}; + +const NodeInject = ({ data }: NodeProps) => { + const classes = useStyles(); + return ( +
+
+ {renderIcon(data.key)} +
+ +
{data.label}
+
+ +
{data.description}
+
+ {(data.isTargeted ? () : null)} + {(data.isTargeting ? () : null)} +
+ ); +}; + +export default memo(NodeInject); diff --git a/openbas-front/src/components/nodes/index.ts b/openbas-front/src/components/nodes/index.ts new file mode 100644 index 0000000000..d90acb3b08 --- /dev/null +++ b/openbas-front/src/components/nodes/index.ts @@ -0,0 +1,9 @@ +import { NodeTypes } from 'reactflow'; + +import NodeInject from './NodeInject'; + +const nodeTypes: NodeTypes = { + inject: NodeInject, +}; + +export default nodeTypes; diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index ec61e185fe..7d4e54c12d 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -998,7 +998,7 @@ export interface Inject { * @min 0 */ inject_depends_duration: number; - inject_depends_on?: Inject; + inject_depends_on?: string; inject_description?: string; inject_documents?: InjectDocument[]; inject_enabled?: boolean; diff --git a/openbas-front/src/utils/flows/useAutoLayout.ts b/openbas-front/src/utils/flows/useAutoLayout.ts index 028e55b955..4ab82a9f08 100644 --- a/openbas-front/src/utils/flows/useAutoLayout.ts +++ b/openbas-front/src/utils/flows/useAutoLayout.ts @@ -4,6 +4,7 @@ import { type Node, type Edge, useReactFlow, useNodesInitialized, useStore } fro import { getSourceHandlePosition, getTargetHandlePosition } from './utils'; import layoutAlgorithms, { type LayoutAlgorithmOptions } from './algorithms'; import type { InjectExpectationsStore } from '../../admin/components/common/injects/expectations/Expectation'; +import {InjectStore} from "../../actions/injects/Inject"; export type LayoutOptions = { algorithm: keyof typeof layoutAlgorithms; @@ -60,6 +61,57 @@ function useAutoLayout(options: LayoutOptions, targetResults: InjectExpectations }, [nodesInitialized, elements, setNodes, setEdges, targetResults]); } +export function useAutoLayoutInject(options: LayoutOptions, injects: InjectStore[]) { + const { getNodes, getEdges, setNodes, setEdges } = useReactFlow(); + const elements = useStore( + (state) => ({ + nodeMap: state.nodeInternals, + edgeMap: state.edges.reduce( + (acc, edge) => acc.set(edge.id, edge), + new Map(), + ), + }), + // The compare elements function will only update `elements` if something has + // changed that should trigger a layout. This includes changes to a node's + // dimensions, the number of nodes, or changes to edge sources/targets. + // eslint-disable-next-line @typescript-eslint/no-use-before-define + compareElements, + ); + const nodesInitialized = useNodesInitialized(); + useEffect(() => { + // Only run the layout if there are nodes and they have been initialized with + // their dimensions + if (!nodesInitialized || elements.nodeMap.size === 0) { + return; + } + // The callback passed to `useEffect` cannot be `async` itself, so instead we + // create an async function here and call it immediately afterwards. + const runLayout = async () => { + const layoutAlgorithm = layoutAlgorithms[options.algorithm]; + const nodes = getNodes(); + const edges = getEdges(); + const { nodes: nextNodes, edges: nextEdges } = await layoutAlgorithm( + nodes, + edges, + options, + ); + // Mutating the nodes and edges directly here is fine because we expect our + // layouting algorithms to return a new array of nodes/edges. + for (const node of nextNodes) { + node.style = { ...node.style, opacity: 1 }; + node.sourcePosition = getSourceHandlePosition(options.direction); + node.targetPosition = getTargetHandlePosition(options.direction); + } + for (const edge of edges) { + edge.style = { ...edge.style, opacity: 1 }; + } + setNodes(nextNodes); + setEdges(nextEdges); + }; + runLayout(); + }, [nodesInitialized, elements, setNodes, setEdges, injects]); +} + export default useAutoLayout; type Elements = { diff --git a/openbas-front/yarn.lock b/openbas-front/yarn.lock index 43ca1f0dd3..238bc56464 100644 --- a/openbas-front/yarn.lock +++ b/openbas-front/yarn.lock @@ -3698,37 +3698,37 @@ __metadata: languageName: node linkType: hard -"@reactflow/background@npm:11.3.13": - version: 11.3.13 - resolution: "@reactflow/background@npm:11.3.13" +"@reactflow/background@npm:11.3.14": + version: 11.3.14 + resolution: "@reactflow/background@npm:11.3.14" dependencies: - "@reactflow/core": "npm:11.11.3" + "@reactflow/core": "npm:11.11.4" classcat: "npm:^5.0.3" zustand: "npm:^4.4.1" peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/54929a506c1b73b6406d511a0a55a89cf88eb2073bc4e72defde63b3da46794ea87638e017180492798d717e9220e833f66c7419270137ace9acaa040f790e6e + checksum: 10c0/a4ef8ca38f9cfd72d578dd2b2672d5bf841410f5a399e0c1d7bf4ea10e5d76003dec2fee15f927d4d5f121fc4414c811f2c4b9e702d28e31a4bd9399fc8cbe29 languageName: node linkType: hard -"@reactflow/controls@npm:11.2.13": - version: 11.2.13 - resolution: "@reactflow/controls@npm:11.2.13" +"@reactflow/controls@npm:11.2.14": + version: 11.2.14 + resolution: "@reactflow/controls@npm:11.2.14" dependencies: - "@reactflow/core": "npm:11.11.3" + "@reactflow/core": "npm:11.11.4" classcat: "npm:^5.0.3" zustand: "npm:^4.4.1" peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/219285855f5a76ad77bf858e1eb3ed867d95f96b8c2f6480e6f9f89dce9e68617cd97565960455aa6fa9c190292ebf81f28da3a254abd413c358a0288773203c + checksum: 10c0/6cd79e66590e301e5b3034dccfa1dcfb01fe27bbd1b8aadf8afaa8a0f68e3abe1c3d4a90a6e1ea54eecf5acb019073ead7379fbbf1252af06a63e413f82a74dc languageName: node linkType: hard -"@reactflow/core@npm:11.11.3": - version: 11.11.3 - resolution: "@reactflow/core@npm:11.11.3" +"@reactflow/core@npm:11.11.4": + version: 11.11.4 + resolution: "@reactflow/core@npm:11.11.4" dependencies: "@types/d3": "npm:^7.4.0" "@types/d3-drag": "npm:^3.0.1" @@ -3742,15 +3742,15 @@ __metadata: peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/08c8353316c38ebc398e645f2e1e2d7246ea4e331485d604f8c6a8d54a83cc85e87921427b3fd7d161314258072b44508c9bba79faefa67e9d401e6b3f262b19 + checksum: 10c0/895a0c1491ba81a6ed06ee9f2619870083753c6faf26ad6cb37a5c27f2f2fa80ab88d4be5c77fb7cad17ba6b20b212ea3171a976f3817056196cf85f6d90e86c languageName: node linkType: hard -"@reactflow/minimap@npm:11.7.13": - version: 11.7.13 - resolution: "@reactflow/minimap@npm:11.7.13" +"@reactflow/minimap@npm:11.7.14": + version: 11.7.14 + resolution: "@reactflow/minimap@npm:11.7.14" dependencies: - "@reactflow/core": "npm:11.11.3" + "@reactflow/core": "npm:11.11.4" "@types/d3-selection": "npm:^3.0.3" "@types/d3-zoom": "npm:^3.0.1" classcat: "npm:^5.0.3" @@ -3760,15 +3760,15 @@ __metadata: peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/71f89a7ae36ce6b50237401640043be20264fedfcd055f6a3e16cf84adcac343ad2f3fd74eb48875ab12538d3566aaddc21cce6160d602120e71797c79ee374e + checksum: 10c0/5bc99ab12e4daafacfb3d60e36c7d2253cfd7cd7ba5765a9ff7c35a3a6835c49cae9aa1acde181baf5b2de3e2c48226758f5a61125ecf7014a9d58fe6ef62bb6 languageName: node linkType: hard -"@reactflow/node-resizer@npm:2.2.13": - version: 2.2.13 - resolution: "@reactflow/node-resizer@npm:2.2.13" +"@reactflow/node-resizer@npm:2.2.14": + version: 2.2.14 + resolution: "@reactflow/node-resizer@npm:2.2.14" dependencies: - "@reactflow/core": "npm:11.11.3" + "@reactflow/core": "npm:11.11.4" classcat: "npm:^5.0.4" d3-drag: "npm:^3.0.0" d3-selection: "npm:^3.0.0" @@ -3776,21 +3776,21 @@ __metadata: peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/8fa01dc3c2805af56bfb93a05d7c2aecbf1c40fdd25d768ae0a0d252cec9ac04911493103abfbe3f8fcd840a8ea4a8c8342646eed7dcfcb3147d1de2a0661dff + checksum: 10c0/094905bc9eb4ec9e20ebe732983a44895822f7fae56c8d348b478bbee776342b286f08578c57eeeb5408a931febced32a5fef68163755b37fc19ca523b78e0dd languageName: node linkType: hard -"@reactflow/node-toolbar@npm:1.3.13": - version: 1.3.13 - resolution: "@reactflow/node-toolbar@npm:1.3.13" +"@reactflow/node-toolbar@npm:1.3.14": + version: 1.3.14 + resolution: "@reactflow/node-toolbar@npm:1.3.14" dependencies: - "@reactflow/core": "npm:11.11.3" + "@reactflow/core": "npm:11.11.4" classcat: "npm:^5.0.3" zustand: "npm:^4.4.1" peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/bfba042b97bcb11c11f71db3c3eab5f153fb2b666f6135d0658efb0bf2b6f210ecb52fa1da3af59eb995dd91d04fe223ef88af7c6e3bc349650fb93e17ed876c + checksum: 10c0/cd29257969a11feb273849d3aa540d534513b292115ebf697db1357fae704426be899dcf882ea36e13a33d49073b6c2b44fc67c4ddbc779bf2dcd49ba87053a3 languageName: node linkType: hard @@ -11958,7 +11958,7 @@ __metadata: react-markdown: "npm:9.0.1" react-redux: "npm:8.1.3" react-router-dom: "npm:6.23.1" - reactflow: "npm:11.11.3" + reactflow: "npm:11.11.4" redux: "npm:4.2.1" redux-first-history: "npm:5.2.0" redux-thunk: "npm:2.4.2" @@ -13250,20 +13250,20 @@ __metadata: languageName: node linkType: hard -"reactflow@npm:11.11.3": - version: 11.11.3 - resolution: "reactflow@npm:11.11.3" +"reactflow@npm:11.11.4": + version: 11.11.4 + resolution: "reactflow@npm:11.11.4" dependencies: - "@reactflow/background": "npm:11.3.13" - "@reactflow/controls": "npm:11.2.13" - "@reactflow/core": "npm:11.11.3" - "@reactflow/minimap": "npm:11.7.13" - "@reactflow/node-resizer": "npm:2.2.13" - "@reactflow/node-toolbar": "npm:1.3.13" + "@reactflow/background": "npm:11.3.14" + "@reactflow/controls": "npm:11.2.14" + "@reactflow/core": "npm:11.11.4" + "@reactflow/minimap": "npm:11.7.14" + "@reactflow/node-resizer": "npm:2.2.14" + "@reactflow/node-toolbar": "npm:1.3.14" peerDependencies: react: ">=17" react-dom: ">=17" - checksum: 10c0/1de038357a3b1ec440a06f041540ec4738419366bff13829996407b963f69bec7acfd1dca475c067727146bfd8f6ce1b230ce499abdbd6a8a2a37493cf547a94 + checksum: 10c0/5faf9895db29d39c190b78a6113a4f97430d576086efeb3570deec81035559297823dd9ca96b9dd7daeb17159a12ac9117e399e71696485e6ccf43e9f98c1f35 languageName: node linkType: hard