diff --git a/package.json b/package.json index 106fc87a..154fea73 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "baseui": "^9.14.1", "brace": "^0.11.1", "chart.js": "^2.9.3", + "dagre": "^0.8.5", "graphviz-react": "1.1.1", "leaflet": "^1.5.1", "moment": "^2.24.0", @@ -24,6 +25,7 @@ "react-chartjs-2": "^2.8.0", "react-cookies": "^0.1.1", "react-dom": "^16.11.0", + "react-flow-renderer": "^8.3.6", "react-json-pretty": "^2.1.0", "react-leaflet": "^2.5.0", "react-router-dom": "^5.0.1", @@ -75,7 +77,8 @@ "eslint-plugin-react-hooks": "^1.6.1", "eslint-plugin-standard": "^4.0.0", "husky": "^3.0.2", - "prettier": "^1.18.2" + "prettier": "^1.18.2", + "typescript": "^2.8.0" }, "husky": { "hooks": { diff --git a/src/App.js b/src/App.js index d880d598..e1cdf29b 100644 --- a/src/App.js +++ b/src/App.js @@ -134,18 +134,9 @@ class App extends Component { ) : ( - <> - -
- -
-
- -
- -
-
- +
+ +
); }; diff --git a/src/Utilities.js b/src/Utilities.js index b175e680..9ae89967 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -107,3 +107,4 @@ export function useWindowDimensions() { return windowDimensions; } + diff --git a/src/components/Global/OriginNode.js b/src/components/Global/OriginNode.js new file mode 100644 index 00000000..1f11fa56 --- /dev/null +++ b/src/components/Global/OriginNode.js @@ -0,0 +1,21 @@ +import React, { memo } from "react"; +import { Handle } from "react-flow-renderer"; + +const OriginNode = memo(({ data }) => { + return ( + <> + console.log('handle onConnect', params)} + /> +

+ {data.name} +

+ + ); +}); + +export { OriginNode }; diff --git a/src/components/Global/TemprNode.js b/src/components/Global/TemprNode.js new file mode 100644 index 00000000..41c11772 --- /dev/null +++ b/src/components/Global/TemprNode.js @@ -0,0 +1,76 @@ +import React, { memo } from "react"; +import { Handle } from "react-flow-renderer"; + +const TemprNode = memo(({ data }) => { + const qRes = data.tempr.queueResponse ? 'green' : 'red'; + const qReq = data.tempr.queueRequest ? 'green' : 'red'; + const fs = data.primary ? 'large' : 'small'; + return ( + <> + {data.tempr.temprId ? ( + console.log('handle onConnect', params)} + /> + ) : ( + console.log('handle onConnect', params)} + /> + )} +
+

+ {data.tempr.name} - + + {data.tempr.id} + +

+
+

Queue Response

+

Queue Request

+

{`${data.tempr.endpointType}`}

+
+
+ + console.log('handle onConnect', params)} + /> + console.log('handle onConnect', params)} + /> + + ); +}); + +export { TemprNode }; diff --git a/src/components/Global/TemprSidebar.js b/src/components/Global/TemprSidebar.js new file mode 100644 index 00000000..ef2c36cf --- /dev/null +++ b/src/components/Global/TemprSidebar.js @@ -0,0 +1,31 @@ +import React from "react"; + +const TemprSidebar = props => { + const onDragStart = (event, id) => { + event.dataTransfer.setData("application/reactflow", id); + event.dataTransfer.effectAllowed = "move"; + }; + + const temprNodes = props.temprs.map(tempr => { + return ( +
onDragStart(event, tempr.id)} + draggable + > + {tempr.name} +
+ ); + }); + + return ( + <> + + + ); +}; + +export { TemprSidebar }; diff --git a/src/components/Global/index.js b/src/components/Global/index.js index 35c0ab7c..bda380af 100644 --- a/src/components/Global/index.js +++ b/src/components/Global/index.js @@ -2,13 +2,16 @@ export * from "./DatetimeFilter"; export * from "./HttpTemprTemplate"; export * from "./NavigationItem"; export * from "./NavigationGroup"; +export * from "./OriginNode"; export * from "./PairInput"; export * from "./SiteSelector"; export * from "./TableFilter"; export * from "./Toast"; export * from "./TrueFalseCheckboxes"; export * from "./TemprSelector"; +export * from "./TemprSidebar"; export * from "./TemprForm"; export * from "./TemprPreview"; export * from "./TemprOutputTest"; export * from "./TemprModal"; +export * from "./TemprNode"; diff --git a/src/components/Universal/Page.js b/src/components/Universal/Page.js index 89f29c22..19434108 100644 --- a/src/components/Universal/Page.js +++ b/src/components/Universal/Page.js @@ -4,11 +4,12 @@ import { Link, Prompt } from "react-router-dom"; import { useStyletron } from "baseui"; import { Button, KIND } from "baseui/button"; import { Heading, HeadingLevel } from "baseui/heading"; -import { Block } from 'baseui/block'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; +import { useWindowDimensions } from "../../Utilities"; + const Actions = props => { const [css] = useStyletron(); @@ -57,24 +58,29 @@ const Page = props => { padding: theme.sizing.scale800, }); + // eslint-disable-next-line no-unused-vars + const { height, width } = useWindowDimensions(); + const mobileView = width < 651; + return (
{props.alert && } - - - - {props.heading} - - - - - - - {props.heading} - - - + { + mobileView ? ( + + + {props.heading} + + + ) : ( + + + {props.heading} + + + ) + } {props.children}
diff --git a/src/components/Universal/PaginatedTable.js b/src/components/Universal/PaginatedTable.js index 1e5d0757..762b68db 100644 --- a/src/components/Universal/PaginatedTable.js +++ b/src/components/Universal/PaginatedTable.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { withRouter } from "react-router-dom"; import { useQueryParam, NumberParam, ObjectParam } from "use-query-params"; import { Pagination, PaginationMobile, Table } from "."; -import { Block } from 'baseui/block'; +import { useWindowDimensions } from "../../Utilities"; const PaginatedTable = withRouter(props => { const [data, setData] = useState(null); @@ -30,101 +30,110 @@ const PaginatedTable = withRouter(props => { }); }, [page, pageSize, filters, getData]); + // eslint-disable-next-line no-unused-vars + const { height, width } = useWindowDimensions(); + return ( <> - - { - if (filters) { - if (value === "") { - delete filters[colId]; - } else { - filters[colId] = value; - } - setFilters(filters); - } else { - setFilters({ [colId]: value }); - } - }} - /> - { - setPageSize(pageSize); - }} - currentPageSize={pageSize} - updatePageNumber={pageNumber => setPage(pageNumber)} - totalRecords={data ? data.totalRecords : "-"} - numberOfPages={data ? data.numberOfPages : "-"} - currentPage={page || 1} - /> - - -
{ - if (filters) { - if (value === "") { - delete filters[colId]; - } else { - filters[colId] = value; - } - setFilters(filters); - } else { - setFilters({ [colId]: value }); - } - }} - /> - { - setPageSize(pageSize); - }} - currentPageSize={pageSize} - updatePageNumber={pageNumber => setPage(pageNumber)} - totalRecords={data ? data.totalRecords : "-"} - numberOfPages={data ? data.numberOfPages : "-"} - currentPage={page || 1} - /> - - -
{ - if (filters) { - if (value === "") { - delete filters[colId]; - } else { - filters[colId] = value; - } - setFilters(filters); - } else { - setFilters({ [colId]: value }); - } - }} - /> - { - setPageSize(pageSize); - }} - currentPageSize={pageSize} - updatePageNumber={pageNumber => setPage(pageNumber)} - totalRecords={data ? data.totalRecords : "-"} - numberOfPages={data ? data.numberOfPages : "-"} - currentPage={page || 1} - /> - + { + width >= 1100 ? ( + <> +
{ + if (filters) { + if (value === "") { + delete filters[colId]; + } else { + filters[colId] = value; + } + setFilters(filters); + } else { + setFilters({ [colId]: value }); + } + }} + /> + { + setPageSize(pageSize); + }} + currentPageSize={pageSize} + updatePageNumber={pageNumber => setPage(pageNumber)} + totalRecords={data ? data.totalRecords : "-"} + numberOfPages={data ? data.numberOfPages : "-"} + currentPage={page || 1} + /> + + ) : width >= 650 ? ( + <> +
{ + if (filters) { + if (value === "") { + delete filters[colId]; + } else { + filters[colId] = value; + } + setFilters(filters); + } else { + setFilters({ [colId]: value }); + } + }} + /> + { + setPageSize(pageSize); + }} + currentPageSize={pageSize} + updatePageNumber={pageNumber => setPage(pageNumber)} + totalRecords={data ? data.totalRecords : "-"} + numberOfPages={data ? data.numberOfPages : "-"} + currentPage={page || 1} + /> + + ) : ( + <> +
{ + if (filters) { + if (value === "") { + delete filters[colId]; + } else { + filters[colId] = value; + } + setFilters(filters); + } else { + setFilters({ [colId]: value }); + } + }} + /> + { + setPageSize(pageSize); + }} + currentPageSize={pageSize} + updatePageNumber={pageNumber => setPage(pageNumber)} + totalRecords={data ? data.totalRecords : "-"} + numberOfPages={data ? data.numberOfPages : "-"} + currentPage={page || 1} + /> + + ) + } ); }); diff --git a/src/components/View/Tempr.js b/src/components/View/Tempr.js index cfe17edf..ebdaf60f 100644 --- a/src/components/View/Tempr.js +++ b/src/components/View/Tempr.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, memo } from "react"; import { Link } from "react-router-dom"; -import { Button } from "baseui/button"; +import { Button, KIND } from "baseui/button"; import { FormControl } from "baseui/form-control"; import { Textarea } from "baseui/textarea"; @@ -199,11 +199,11 @@ const Tempr = props => { async function getChildren(temprId) { var none = true; if (!blankTempr) { - const ts = await OopCore.getTemprs({temprId: temprId}); + const ts = await OopCore.getTemprs({filter: {temprId: temprId}}); if (ts) { // eslint-disable-next-line no-unused-vars for (const tempr of ts.data) { - if (tempr.temprId === temprId) { + if (tempr.temprId === parseInt(temprId)) { none = false; break; } @@ -410,6 +410,7 @@ const Tempr = props => { $as={Link} to={`${props.location.pathname}/audit-logs`} aria-label={"History"} + kind={(noParents && noChildren) ? KIND.primary : KIND.tertiary} > History diff --git a/src/components/View/TemprMap.js b/src/components/View/TemprMap.js index 3703005a..a2099d27 100644 --- a/src/components/View/TemprMap.js +++ b/src/components/View/TemprMap.js @@ -1,153 +1,414 @@ import React, { useState } from "react"; +import { Redirect } from "react-router"; -import { Redirect } from 'react-router'; +import ReactFlow, { + ReactFlowProvider, + removeElements, + isNode, + Controls, + MiniMap, + getBezierPath, + getMarkerEnd, + useStoreState, +} from "react-flow-renderer"; -import { - Page, - DataProvider, -} from "../Universal"; - -import { Graphviz } from 'graphviz-react'; +import { TemprSidebar, TemprNode, OriginNode } from "../Global"; +import { DataProvider, Page, InPlaceGifSpinner } from "../Universal"; import OopCore from "../../OopCore"; +import { useWindowDimensions } from "../../Utilities"; + +import dagre from "dagre"; + +const dagreGraph = new dagre.graphlib.Graph(); +dagreGraph.setDefaultEdgeLabel(() => ({})); +const getLayoutedElements = (elements, positions) => { + dagreGraph.setGraph({ rankdir: 'TB' }); + elements.forEach((el) => { + if (isNode(el)) { + dagreGraph.setNode(el.id, { width: 250, height: 80 }); + } else { + dagreGraph.setEdge(el.source, el.target); + } + }); + dagre.layout(dagreGraph); + return elements.map((el) => { + if (isNode(el)) { + var newPosition = null; + if (positions) { + newPosition = getNewPos(el, positions); + } + el.targetPosition = 'top'; + el.sourcePosition = 'bottom'; + if (newPosition) { + el.position = newPosition; + } else { + const nodeWithPosition = dagreGraph.node(el.id); + el.position = { + x: nodeWithPosition.x + Math.random() / 1000, + y: nodeWithPosition.y, + }; + } + } + return el; + }); +}; + +const getNewPos = (element, positions) => { + const elId = element.id; + var newPos = null; + positions.forEach(p => { + if (p.id === elId) { + newPos = p.__rf.position; + }; + }); + return newPos; +}; + +const ConnectionLine = ({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + connectionLineType, + connectionLineStyle, +}) => { + return ( + + + + + ); +}; + +const CustomEdge = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + data, + arrowHeadType, + markerEndId, +}) => { + const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); + const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); + return ( + <> + + data.onClick(id, data.els)} + cx={(sourceX+targetX) / 2} + cy={(sourceY+targetY) / 2} + r="6" + fill="none" + /> + data.onClick(id, data.els)} + x1={(sourceX + targetX) / 2 - 6} + x2={(sourceX + targetX) / 2 + 6} + y1={(sourceY + targetY) / 2 - 6} + y2={(sourceY + targetY) / 2 + 6} + /> + data.onClick(id, data.els)} + x1={(sourceX + targetX) / 2 + 6} + x2={(sourceX + targetX) / 2 - 6} + y1={(sourceY + targetY) / 2 - 6} + y2={(sourceY + targetY) / 2 + 6} + /> + + ); +}; const TemprMap = props => { const [noMap, setNoMap] = useState(false); - const [nodes, setNodes] = useState({}); - const [paths, setPaths] = useState([]); const [title, setTitle] = useState(""); + const [loading, setLoading] = useState(false); + const temprOriginPath = "/temprs/" + props.match.params.temprId; + const [reactFlowInstance, setReactFlowInstance] = useState(null); + const [elements, setElements] = useState([]); + const [unusedTemprs, setUnusedTemprs] = useState([]); + var pos = []; - const temprOriginPath = '/temprs/' + props.match.params.temprId; + const { height, width } = useWindowDimensions(); + const noEdit = width < 1100 || height < 500; + const nodeTypes = { + temprNode: TemprNode, + originNode: OriginNode, + }; - async function getChildren(temprId) { - const ts = await OopCore.getTemprs({temprId: temprId}); - var none = true; - if (ts) { - // eslint-disable-next-line no-unused-vars - for (const tempr of ts.data) { - if (tempr.temprId === temprId) { - none = false; - break; - } + const edgeTypes = { + custom: CustomEdge, + }; + + async function positionChange(p) { + pos = await p; + }; + + const NodeState = props => { + const nodes = useStoreState((store) => store.nodes); + props.onChange(nodes); + return null; + }; + + async function onConnect(params) { + if ( + params.source !== params.target && + params.targetHandle[0] === "Y" && + params.sourceHandle.split("-")[0] === "bottom" + ) { + const newT = await OopCore.updateTempr(params.target, { + temprId: params.source, + }); + if (newT.temprId === parseInt(params.source)) { + refresh(); } } - return none; + } + + const onElementsRemove = elementsToRemove => { + setElements(els => removeElements(elementsToRemove, els)); }; - async function getParents(temprId) { - const t = await OopCore.getTempr(temprId); - return (!t.temprId); + const onLoad = _reactFlowInstance => { + setReactFlowInstance(_reactFlowInstance); }; - const formatNode = (temprObj) => { - return `${temprObj.id}[shape=plain, fontname=Helvetica, - label=< -
- - - - - - - - -
- ${temprObj.name} - - - - - - - - - - - - -
- -
-
 
- - - - - - - -
 ${temprObj.endpointType}
-
- >]`; + const onDragOver = event => { + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }; + + async function onDrop(event) { + event.preventDefault(); + + const id = event.dataTransfer.getData("application/reactflow"); + const position = reactFlowInstance.project({ + x: event.clientX - 175, + y: event.clientY - 225, + }); + + setLoading(true); + + const tempr = await OopCore.getTempr(id); + + if (tempr.id) { + const newNode = formatNode(tempr, position); + var newElements = elements.concat(newNode); + + setElements(newElements); + + var remainingTemprs = unusedTemprs.filter(t => t.id !== tempr.id); + + setUnusedTemprs(remainingTemprs); + } + + setLoading(false); + } + + async function deletePath(edgeId, els) { + var target = parseInt(edgeId.split("-")[1]); + + const newT = await OopCore.updateTempr(target, { temprId: null }); + if (newT.temprId === null) { + refresh(); + } + } + + async function getLinks(temprId) { + var dts = await OopCore.getDeviceTemprs({ + filter: { temprId: temprId }, + "page[size]": -1, + }); + var sts = await OopCore.getScheduleTemprs({ + filter: { temprId: temprId }, + "page[size]": -1, + }); + + var allLinks = dts.data.push(...sts.data); + console.log(allLinks); + return allLinks; + } + + async function refresh() { + setLoading(true); + const response = await getData(props.match.params.temprId); + if (response) { + const layoutedEls = await getLayoutedElements(response.nodes, pos); + setElements(els => { + return layoutedEls; + }); + setUnusedTemprs(response.remainingTemprs); + setLoading(false); + } else { + setNoMap(true); + } + } + + const formatNode = (temprObj, pos) => { + const primary = temprObj.id === parseInt(props.match.params.temprId); + const position = pos || { x: 200, y: 50 }; + const border = primary ? "2px solid #177692" : "1px solid #777"; + return ({ + id: `${temprObj.id}`, + type: "temprNode", + data: { tempr: temprObj, primary: primary }, + style: { + border: border, + borderRadius: "2px", + padding: 10, + backgroundColor: "white", + }, + position: position, + }); + }; + + const formatOriginNode = (originObj, pos, device) => { + const position = pos || { x: 200, y: 50 }; + const style = device ? { + border: "1px solid #777", + borderRadius: "20px", + padding: 10, + backgroundColor: "white", + } : { + border: "1px solid #777", + borderRadius: "20px", + padding: 10, + backgroundColor: "white", + }; + return ({ + id: `${device ? 'D' : 'S'}${originObj.id}`, + type: "originNode", + data: originObj, + style: style, + position: position, + }); + }; + + const formatPath = (sourceId, targetId, sourceHandle, targetHandle, dtId, stId) => { + const type = noEdit ? "bezier" : "custom"; + return ( + { + id: `${sourceId}-${targetId}`, + source: `${sourceId}`, + target: `${targetId}`, + style: { stroke: '#777', strokeWidth: 1.5 }, + type: type, + data: { onClick: deletePath, deviceTemprId: dtId, scheduleTemprId: stId }, + sourceHandle: sourceHandle, + targetHandle: targetHandle, + } + ); }; async function getData(temprId) { - var ps = await getParents(temprId); - var cs = await getChildren(temprId); var nodeData = []; var pathData = new Set(); - if (ps && cs) { - return null; - } else { - var tempr = await OopCore.getTempr(temprId); - var children = await OopCore.getTemprs({temprId: temprId}); - var childrenData = []; - for (var i = children.data.length - 1; i >= 0; i--) { - if (children.data[i].temprId === temprId) { - childrenData.push(children.data[i]); - } + var tempr = await OopCore.getTempr(temprId); + const originalTempr = tempr; + var allTemprs = await OopCore.getTemprs({ + filter: { deviceGroupId: tempr.deviceGroupId }, + }); + var children = await OopCore.getTemprs({ + filter: { temprId: temprId }, + }); + var childrenData = children.data; + const titleNode = tempr.name; + while (tempr) { + if (!nodeData[tempr.id]) { + nodeData[tempr.id] = formatNode(tempr); } - const titleNode = tempr.name; - while (tempr) { - if (!nodeData[tempr.id]) { - nodeData[tempr.id] = formatNode(tempr); - } - if (tempr.temprId) { - pathData.add(`${tempr.temprId}->${tempr.id}`); - if (nodeData[tempr.temprId]){ - tempr = null; - } else { - tempr = await OopCore.getTempr(tempr.temprId); - childrenData.push(tempr); - } - } else { + if (tempr.temprId) { + pathData.add(formatPath(tempr.temprId, tempr.id)); + if (nodeData[tempr.temprId]) { tempr = null; + } else { + tempr = await OopCore.getTempr(tempr.temprId); + childrenData.push(tempr); } + } else { + tempr = null; } - while (childrenData.length > 0) { - const c = childrenData.shift(); - if (c.temprId) { - if (!nodeData[c.id]) { - nodeData[c.id] = formatNode(c); - } - pathData.add(`${c.temprId}->${c.id}`); - var new_children = await OopCore.getTemprs({temprId: c.id}); - new_children = new_children.data; - for (var q = new_children.length - 1; q >= 0; q--) { - if (!nodes[new_children[q].id] && new_children[q].temprId === c.id) { - childrenData.push(new_children[q]); - } - } - } + } + while (childrenData.length > 0) { + const c = childrenData.shift(); + if (!nodeData[c.id]) { + nodeData[c.id] = formatNode(c); + pathData.add(formatPath(c.temprId, c.id)); } - const pathArray = [...pathData]; - let response = {nodes:nodeData,paths:pathArray,title:titleNode}; - return (response); + var new_children = await OopCore.getTemprs({ + filter: { temprId: c.id }, + }); + var filtered_children = new_children.data.filter( + c => !nodeData[c.id], + ); + childrenData.push(...filtered_children); } - }; + const pathArray = [...pathData]; + var filteredNodes = nodeData.filter(Boolean); + var remainingTemprs = allTemprs.data.filter(t => !nodeData[t.id]); + filteredNodes.push(...pathArray); + const devices = await OopCore.getDevices({ + filter: { deviceGroupId: originalTempr.deviceGroupId }, + }); + const deviceNodes = devices.data.map(d => formatOriginNode(d, false, true)); + const schedules = await OopCore.getSchedules(); + const scheduleNodes = schedules.data.map(s => formatOriginNode(s, false, false)); + const newPaths = await Promise.all(nodeData.filter(Boolean).map(t => { + const links = getLinks(t.id); + + })); + console.log(newPaths); + const newP = formatPath(originalTempr.id, 'D22', `DT-${originalTempr.id}`, 'D-22'); + filteredNodes.push(...deviceNodes); + filteredNodes.push(...scheduleNodes); + filteredNodes.push(newP); + return { + title: titleNode, + nodes: filteredNodes, + remainingTemprs: remainingTemprs, + }; + } return ( { return getData(props.match.params.temprId).then(response => { if (response) { - setNodes(response.nodes); - setPaths(response.paths); + const layoutedEls = getLayoutedElements(response.nodes); + setElements(layoutedEls); setTitle(response.title); + setUnusedTemprs(response.remainingTemprs); return response; } else { setNoMap(true); @@ -155,31 +416,57 @@ const TemprMap = props => { } }); }} - renderData={() => (!(noMap) ? - - - - : - )} + renderData={() => + !noMap ? ( + +
+ +
+ {loading ? ( + + ) : ( + + + + {!noEdit && ( + { + if (n.data.primary) + return "#177692"; + return "black"; + }} + /> + )} + + )} +
+ {!noEdit && ( + + )} +
+
+
+ ) : ( + + ) + } /> ); }; diff --git a/src/components/View/Temprs.js b/src/components/View/Temprs.js index 4a8ef66e..f83eaf36 100644 --- a/src/components/View/Temprs.js +++ b/src/components/View/Temprs.js @@ -4,7 +4,7 @@ import { Link } from "react-router-dom"; import { Button, KIND } from "baseui/button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faEdit, faHistory } from "@fortawesome/free-solid-svg-icons"; +import { faPlus, faEdit, faHistory, faProjectDiagram } from "@fortawesome/free-solid-svg-icons"; import { useQueryParam, StringParam } from "use-query-params"; import { PaginatedTable, Page } from "../Universal"; @@ -64,6 +64,16 @@ const Temprs = props => { if (columnName === "action") { return ( <> +