Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions frontend/src/components/common/flows/getLayoutedElements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import dagre from "@dagrejs/dagre";

/* eslint-disable id-length */
export function getLayoutedElements(
nodes,
edges,
nodeWidth,
nodeHeight,
deltaX,
deltaY,
) {
// needed for graph layout
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));

dagreGraph.setGraph({ rankdir: "LR" });

nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});

edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});

dagre.layout(dagreGraph);

nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// eslint-disable-next-line no-param-reassign
node.position = {
x: nodeWithPosition.x - nodeWidth / 2 + deltaX,
y: nodeWithPosition.y - nodeHeight / 2 + deltaY,
};
return node;
});
return { nodes, edges };
}
38 changes: 5 additions & 33 deletions frontend/src/components/investigations/flow/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import dagre from "@dagrejs/dagre";
import { getLayoutedElements } from "../../common/flows/getLayoutedElements";
import { JobFinalStatuses } from "../../../constants/jobConst";

/* eslint-disable id-length */
Expand Down Expand Up @@ -53,38 +53,6 @@ function addEdge(edges, job, parentType, parentId) {
}
}

function getLayoutedElements(nodes, edges) {
// needed for graph layout
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));

const nodeWidth = 300;
const nodeHeight = 60;

dagreGraph.setGraph({ rankdir: "LR" });

nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});

edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});

dagre.layout(dagreGraph);

nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// eslint-disable-next-line no-param-reassign
node.position = {
x: nodeWithPosition.x - nodeWidth / 2 + 150,
y: nodeWithPosition.y - nodeHeight / 2 + 70,
};
return node;
});
return { nodes, edges };
}

export function getNodesAndEdges(
investigationTree,
investigationId,
Expand Down Expand Up @@ -131,6 +99,10 @@ export function getNodesAndEdges(
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
jobsNodes,
jobsEdges,
300,
60,
150,
70,
);
return [
initialNode.concat(layoutedNodes),
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/plugins/flows/CustomPivotNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from "react";
import PropTypes from "prop-types";
import { Handle, Position, NodeToolbar } from "reactflow";
import "reactflow/dist/style.css";
import { Badge } from "reactstrap";

function CustomPivotNode({ data }) {
return (
<>
<NodeToolbar position="top" align="start" isVisible offset={3}>
<Badge color="#b5ba66" style={{ backgroundColor: "#b5ba66" }}>
Pivot
</Badge>
</NodeToolbar>
{/* Info */}
<NodeToolbar
position="right"
style={{
background: "#000f12",
border: "1px solid #6c757d",
borderRadius: "10px",
}}
id={`toolbar-pivot-${data.id}`}
className="p-3 px-4 my-2 mx-2 d-flex flex-column bg-body"
>
<small className="d-flex justify-content-between">
<span className="me-4">Analyzers:</span>
<span style={{ color: "#b5ba66" }}>{data?.analyzers || "-"}</span>
</small>
<small className="d-flex justify-content-between">
<span className="me-4">Connectors:</span>
<span style={{ color: "#b5ba66" }}>{data?.connectors || "-"}</span>
</small>
<small className="d-flex justify-content-between">
<span className="me-4">Type:</span>
<span style={{ color: "#b5ba66" }}>{data?.type}</span>
</small>
<small className="d-flex justify-content-between">
<span className="me-4"> Field to analyze:</span>
<span style={{ color: "#b5ba66" }}>
{data?.fieldToCompare || "-"}
</span>
</small>
</NodeToolbar>
<div
className="react-flow__node-input"
id={`pivot-${data.id}`}
style={{
background: "#2f515e",
color: "#fff",
border: "1px solid #b5ba66",
minWidth: "250px",
}}
>
<strong>{data?.label}</strong>
</div>
<Handle
type="source"
position={Position.Right}
id={`pivotHandleSource-${data.id}`}
isConnectable
/>
<Handle
type="target"
position={Position.Left}
id={`pivotHandleTarget-${data.id}`}
isConnectable
/>
</>
);
}

CustomPivotNode.propTypes = {
data: PropTypes.object.isRequired,
};

export default React.memo(CustomPivotNode);
66 changes: 66 additions & 0 deletions frontend/src/components/plugins/flows/CustomPlaybookNode.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import PropTypes from "prop-types";
import { Handle, Position, NodeToolbar } from "reactflow";
import "reactflow/dist/style.css";
import { Badge } from "reactstrap";

function CustomPlaybookNode({ data }) {
return (
<>
{/* Badge */}
<NodeToolbar position="top" align="start" isVisible offset={3}>
<Badge color="#5593ab" style={{ backgroundColor: "#5593ab" }}>
Playbook
</Badge>
</NodeToolbar>
{/* Info */}
<NodeToolbar
position="right"
style={{
background: "#000f12",
border: "1px solid #6c757d",
borderRadius: "10px",
}}
id={`toolbar-pivot-${data.id}`}
className="p-3 px-4 my-2 mx-2 d-flex flex-column bg-body"
>
<small
className="d-flex justify-content-between"
style={{ maxWidth: "25vh" }}
>
<span>{data?.description}</span>
</small>
</NodeToolbar>
<div
className="react-flow__node-input"
id={`playbook-${data.id}`}
style={{
background: "#2f515e",
color: "#fff",
border: "1px solid #5593ab",
minWidth: "250px",
}}
>
<strong>{data?.label}</strong>
</div>
<Handle
type="source"
position={Position.Right}
id={`playbookHandleSource-${data.id}`}
isConnectable
/>
<Handle
type="target"
position={Position.Left}
id={`playbookHandleTarget-${data.id}`}
isConnectable
/>
</>
);
}

CustomPlaybookNode.propTypes = {
data: PropTypes.object.isRequired,
};

export default React.memo(CustomPlaybookNode);
75 changes: 75 additions & 0 deletions frontend/src/components/plugins/flows/PlaybookFlows.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-disable id-length */
import React from "react";
import PropTypes from "prop-types";
import ReactFlow, { Controls, useNodesState, useEdgesState } from "reactflow";
import "reactflow/dist/style.css";

import CustomPlaybookNode from "./CustomPlaybookNode";
import CustomPivotNode from "./CustomPivotNode";
import { getNodesAndEdges } from "./utils";
import { usePluginConfigurationStore } from "../../../stores/usePluginConfigurationStore";

// Important! This must be defined outside of the component
const nodeTypes = {
playbookNode: CustomPlaybookNode,
pivotNode: CustomPivotNode,
};

const defaultEdgeOptions = {
style: { strokeWidth: 3 },
type: "step",
};

export function PlaybookFlows({ playbook }) {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);

// API/ store
const [pivotsLoading, pivotStored, playbooksLoading, playbooksStored] =
usePluginConfigurationStore((state) => [
state.pivotsLoading,
state.pivots,
state.playbooksLoading,
state.playbooks,
]);

React.useEffect(() => {
if (!pivotsLoading && !playbooksLoading) {
const [initialNodes, initialEdges] = getNodesAndEdges(
playbook,
pivotStored,
playbooksStored,
);
setNodes(initialNodes);
setEdges(initialEdges);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playbook, pivotsLoading, playbooksLoading]);

return (
<div
id="PlaybookFlows"
className="pt-4"
style={{ width: "100%", height: "500px", overflowX: "scroll" }}
>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
defaultEdgeOptions={defaultEdgeOptions}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
nodeTypes={nodeTypes}
deleteKeyCode={null}
preventScrolling
zoomOnDoubleClick={false}
>
<Controls />
</ReactFlow>
</div>
);
}

PlaybookFlows.propTypes = {
playbook: PropTypes.object.isRequired,
};
Loading
Loading