From fe3429d4907c5523cd84637c71137fd2b0bc0b98 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 23 Jan 2025 11:20:40 +0000 Subject: [PATCH 01/14] systems tree Test using react flow --- package.json | 5 + src/App.tsx | 12 + src/api/systems.tsx | 28 ++ src/common/baseLayoutHeader.component.tsx | 9 +- src/systems/systemsLayout.component.tsx | 56 ++- src/systems/systemsTree.component.tsx | 228 +++++++++++ yarn.lock | 452 +++++++++++++++++++++- 7 files changed, 784 insertions(+), 6 deletions(-) create mode 100644 src/systems/systemsTree.component.tsx diff --git a/package.json b/package.json index 2bad6cc3c..a7216c836 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "@uppy/react": "^4.0.2", "@uppy/xhr-upload": "^4.2.0", "@vitejs/plugin-react": "^4.3.0", + "@xyflow/react": "^12.4.2", "axios": "^1.7.2", "blob-polyfill": "^9.0.20240710", "browserslist": "^4.23.0", "browserslist-to-esbuild": "^2.1.1", + "dagre": "^0.8.5", "date-fns": "4.1.0", "history": "^5.3.0", "loglevel": "^1.9.1", @@ -86,6 +88,9 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", + "@types/d3": "^7", + "@types/d3-hierarchy": "^3", + "@types/dagre": "^0", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/eslint__js": "8.42.3", "@types/react-router-dom": "5.3.3", diff --git a/src/App.tsx b/src/App.tsx index ec3eafe08..3941595e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -64,6 +64,7 @@ import SystemsLayout, { SystemsLayoutErrorComponent, systemsLayoutLoader, } from './systems/systemsLayout.component'; +import SystemsTree from './systems/systemsTree.component'; import ViewTabs from './view/viewTabs.component'; export const paths = { @@ -81,6 +82,8 @@ export const paths = { item: '/catalogue/:catalogue_category_id/items/:catalogue_item_id/items/:item_id', systems: '/systems', system: '/systems/:system_id', + systemTree: '/systems/:system_id/tree', + systemRootTree: '/systems/tree', manufacturers: '/manufacturers', manufacturer: '/manufacturers/:manufacturer_id', }; @@ -216,6 +219,15 @@ const routeObject: RouteObject[] = [ Component: Systems, loader: systemsLayoutLoader(queryClient), }, + { + path: 'tree', + Component: SystemsTree, + }, + { + path: paths.systemTree, + Component: SystemsTree, + loader: systemsLayoutLoader(queryClient), + }, { path: '*', Component: SystemsErrorComponent, diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 63a9e48b0..d74281d86 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -87,6 +87,34 @@ export const useGetSystem = ( return useQuery(getSystemQuery(id)); }; +export interface SystemTree extends System { + subsystems?: SystemTree[]; +} + +const getSystemTree = async (parent_id?: string): Promise => { + // Fetch the systems at the current level + const systems = await getSystems(parent_id || 'null'); + + // Fetch subsystems for each system recursively + const systemsWithTree = await Promise.all( + systems.map(async (system) => { + const subsystems = await getSystemTree(system.id); // Fetch subsystems + return { ...system, subsystems }; // Attach subsystems + }) + ); + + return systemsWithTree; +}; + +export const useGetSystemsTree = ( + parent_id?: string | null +): UseQueryResult => { + return useQuery({ + queryKey: ['SystemsTree', parent_id], + queryFn: () => getSystemTree(parent_id ?? ''), + }); +}; + const getSystemsBreadcrumbs = async (id: string): Promise => { return imsApi.get(`/v1/systems/${id}/breadcrumbs`, {}).then((response) => { return response.data; diff --git a/src/common/baseLayoutHeader.component.tsx b/src/common/baseLayoutHeader.component.tsx index ccf12ff2e..7568aa53f 100644 --- a/src/common/baseLayoutHeader.component.tsx +++ b/src/common/baseLayoutHeader.component.tsx @@ -14,11 +14,16 @@ export interface BaseLayoutHeaderProps { function BaseLayoutHeader(props: BaseLayoutHeaderProps) { const { breadcrumbsInfo, children, homeLocation } = props; const navigate = useNavigate(); + + // Check if we are in Tree View or Normal View by looking for '/tree' in the URL + const isTreeView = location.pathname.includes('tree'); const onChangeNode = React.useCallback( (id: string | null) => { - navigate(`/${RoutesHomeLocation[homeLocation]}${id ? `/${id}` : ''}`); + navigate( + `/${RoutesHomeLocation[homeLocation]}${id ? `/${id}` : ''}${isTreeView && !id && homeLocation == 'Systems' ? '/tree' : ''}` + ); }, - [homeLocation, navigate] + [homeLocation, isTreeView, navigate] ); return ( diff --git a/src/systems/systemsLayout.component.tsx b/src/systems/systemsLayout.component.tsx index aef74f348..7df15f139 100644 --- a/src/systems/systemsLayout.component.tsx +++ b/src/systems/systemsLayout.component.tsx @@ -1,5 +1,14 @@ +import AccountTreeIcon from '@mui/icons-material/AccountTree'; +import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import { ToggleButton, ToggleButtonGroup } from '@mui/material'; import type { QueryClient } from '@tanstack/react-query'; -import { Outlet, useParams, type LoaderFunctionArgs } from 'react-router-dom'; +import { + Outlet, + useLocation, + useNavigate, + useParams, + type LoaderFunctionArgs, +} from 'react-router-dom'; import { getSystemQuery, useGetSystemsBreadcrumbs } from '../api/systems'; import BaseLayoutHeader from '../common/baseLayoutHeader.component'; import PageNotFoundComponent from '../common/pageNotFound/pageNotFound.component'; @@ -29,15 +38,58 @@ export const systemsLayoutLoader = }; function SystemsLayout() { + const location = useLocation(); const { system_id: systemId } = useParams(); const { data: systemsBreadcrumbs } = useGetSystemsBreadcrumbs(systemId); + const navigate = useNavigate(); + // Check if we are in Tree View or Normal View by looking for '/tree' in the URL + const isTreeView = location.pathname.includes('tree'); + + // Handle the view change using the toggle button + const handleViewChange = ( + _event: React.MouseEvent, + newView: string + ) => { + if (newView === 'tree') { + // Navigate to Tree View + navigate(`/systems${systemId ? `/${systemId}` : ''}/tree`); + } else { + // Navigate to Normal View + navigate(`/systems${systemId ? `/${systemId}` : ''}`); + } + }; return ( [ + isTreeView ? id + '/tree' : id, + name, + ]) ?? [], + }} > + + + + Normal View + + + + Tree View + + ); diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx new file mode 100644 index 000000000..9429d0828 --- /dev/null +++ b/src/systems/systemsTree.component.tsx @@ -0,0 +1,228 @@ +import { + Box, + LinearProgress, + ToggleButton, + ToggleButtonGroup, + Typography, +} from '@mui/material'; +import { + Background, + ConnectionLineType, + Controls, + Panel, + Position, + ReactFlow, + useEdgesState, + useNodesState, + type Edge, + type Node, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import dagre from 'dagre'; +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { + useGetSystem, + useGetSystemsTree, + type SystemTree, +} from '../api/systems'; +import { getPageHeightCalc } from '../utils'; + +const nodeWidth = 172; +const nodeHeight = 36; + +// Function to apply the Dagre layout +const getLayoutedElements = ( + nodes: Node[], + edges: Edge[], + direction = 'TB' +) => { + const isHorizontal = direction === 'LR'; + const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + + dagreGraph.setGraph({ rankdir: direction }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { + ...node, + targetPosition: isHorizontal ? Position.Left : Position.Top, + sourcePosition: isHorizontal ? Position.Right : Position.Bottom, + position: { + x: nodeWithPosition.x - nodeWidth / 2, + y: nodeWithPosition.y - nodeHeight / 2, + }, + }; + }); + + return { nodes: layoutedNodes, edges }; +}; + +interface SystemFlowProps { + parentId?: string | null; +} + +const SystemsTree: React.FC = () => { + const { system_id: systemId } = useParams(); + const { data: systemsTree, isLoading } = useGetSystemsTree(systemId); + const { data: rootSystem } = useGetSystem(systemId); + + const [layoutDirection, setLayoutDirection] = React.useState<'TB' | 'LR'>( + 'TB' + ); + + const xSpacing = 300; + const ySpacing = 200; + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + useEffect(() => { + if (systemsTree) { + const transformToFlowData = ( + systems: SystemTree[], + parentId?: string + ): { nodes: Node[]; edges: Edge[] } => { + let nodes: Node[] = []; + let edges: Edge[] = []; + + systems.forEach((system, index) => { + nodes.push({ + id: system.id, + data: { label: system.name }, + position: { x: index * xSpacing, y: index * ySpacing }, + }); + + if (parentId) { + edges.push({ + id: `e-${parentId}-${system.id}`, + source: parentId, + target: system.id, + type: 'smoothstep', + }); + } + + if (system.subsystems && system.subsystems.length > 0) { + const { nodes: childNodes, edges: childEdges } = + transformToFlowData(system.subsystems, system.id); + nodes = [...nodes, ...childNodes]; + edges = [...edges, ...childEdges]; + } + }); + + return { nodes, edges }; + }; + + const generateTreeWithRoot = ( + systems: SystemTree[] + ): { nodes: Node[]; edges: Edge[] } => { + const rootNode = { + id: systemId || 'root', + data: { label: rootSystem?.name || 'Root' }, + position: { x: (xSpacing * systems.length) / 2, y: 0 }, + }; + + const { nodes, edges } = transformToFlowData( + systems, + systemId || 'root' + ); + return { + nodes: [rootNode, ...nodes], + edges, + }; + }; + + const { nodes: rawNodes, edges: rawEdges } = + generateTreeWithRoot(systemsTree); + + const { nodes: layoutedNodes, edges: layoutedEdges } = + getLayoutedElements(rawNodes, rawEdges, layoutDirection); + + setNodes(layoutedNodes); + setEdges(layoutedEdges); + } + }, [ + systemsTree, + layoutDirection, + systemId, + setNodes, + setEdges, + rootSystem?.name, + ]); + + console.log(nodes); + + const handleToggleLayout = ( + _event: React.MouseEvent, + newDirection: 'TB' | 'LR' + ) => { + if (newDirection !== null) { + setLayoutDirection(newDirection); + } + }; + + if (isLoading) { + return ( + + + + + Taking time to gather data... This may take a couple of minutes. + + + Note: If this system is high up the tree with many subsystems and + items, this process might take significantly longer. + + + + ); + } + + return ( + + + + + Vertical + Horizontal + + + + + + + ); +}; + +export default SystemsTree; diff --git a/yarn.lock b/yarn.lock index f70f7a307..b0b914b3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1912,6 +1912,285 @@ __metadata: languageName: node linkType: hard +"@types/d3-array@npm:*": + version: 3.2.1 + resolution: "@types/d3-array@npm:3.2.1" + checksum: 10c0/38bf2c778451f4b79ec81a2288cb4312fe3d6449ecdf562970cc339b60f280f31c93a024c7ff512607795e79d3beb0cbda123bb07010167bce32927f71364bca + languageName: node + linkType: hard + +"@types/d3-axis@npm:*": + version: 3.0.6 + resolution: "@types/d3-axis@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a + languageName: node + linkType: hard + +"@types/d3-brush@npm:*": + version: 3.0.6 + resolution: "@types/d3-brush@npm:3.0.6" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01 + languageName: node + linkType: hard + +"@types/d3-chord@npm:*": + version: 3.0.6 + resolution: "@types/d3-chord@npm:3.0.6" + checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17 + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae + languageName: node + linkType: hard + +"@types/d3-contour@npm:*": + version: 3.0.6 + resolution: "@types/d3-contour@npm:3.0.6" + dependencies: + "@types/d3-array": "npm:*" + "@types/geojson": "npm:*" + checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c + languageName: node + linkType: hard + +"@types/d3-delaunay@npm:*": + version: 6.0.4 + resolution: "@types/d3-delaunay@npm:6.0.4" + checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 + languageName: node + linkType: hard + +"@types/d3-dispatch@npm:*": + version: 3.0.6 + resolution: "@types/d3-dispatch@npm:3.0.6" + checksum: 10c0/405eb7d0ec139fbf72fa6a43b0f3ca8a1f913bb2cb38f607827e63fca8d4393f021f32f3b96b33c93ddbd37789453a0b3624f14f504add5308fd9aec8a46dda0 + languageName: node + linkType: hard + +"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.7": + version: 3.0.7 + resolution: "@types/d3-drag@npm:3.0.7" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/65e29fa32a87c72d26c44b5e2df3bf15af21cd128386bcc05bcacca255927c0397d0cd7e6062aed5f0abd623490544a9d061c195f5ed9f018fe0b698d99c079d + languageName: node + linkType: hard + +"@types/d3-dsv@npm:*": + version: 3.0.7 + resolution: "@types/d3-dsv@npm:3.0.7" + checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871 + languageName: node + linkType: hard + +"@types/d3-ease@npm:*": + version: 3.0.2 + resolution: "@types/d3-ease@npm:3.0.2" + checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c + languageName: node + linkType: hard + +"@types/d3-fetch@npm:*": + version: 3.0.7 + resolution: "@types/d3-fetch@npm:3.0.7" + dependencies: + "@types/d3-dsv": "npm:*" + checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0 + languageName: node + linkType: hard + +"@types/d3-force@npm:*": + version: 3.0.10 + resolution: "@types/d3-force@npm:3.0.10" + checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92 + languageName: node + linkType: hard + +"@types/d3-format@npm:*": + version: 3.0.4 + resolution: "@types/d3-format@npm:3.0.4" + checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee + languageName: node + linkType: hard + +"@types/d3-geo@npm:*": + version: 3.1.0 + resolution: "@types/d3-geo@npm:3.1.0" + dependencies: + "@types/geojson": "npm:*" + checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88 + languageName: node + linkType: hard + +"@types/d3-hierarchy@npm:*, @types/d3-hierarchy@npm:^3": + version: 3.1.7 + resolution: "@types/d3-hierarchy@npm:3.1.7" + checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7 + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "npm:*" + checksum: 10c0/066ebb8da570b518dd332df6b12ae3b1eaa0a7f4f0c702e3c57f812cf529cc3500ec2aac8dc094f31897790346c6b1ebd8cd7a077176727f4860c2b181a65ca4 + languageName: node + linkType: hard + +"@types/d3-path@npm:*": + version: 3.1.0 + resolution: "@types/d3-path@npm:3.1.0" + checksum: 10c0/85e8b3aa968a60a5b33198ade06ae7ffedcf9a22d86f24859ff58e014b053ccb7141ec163b78d547bc8215bb12bb54171c666057ab6156912814005b686afb31 + languageName: node + linkType: hard + +"@types/d3-polygon@npm:*": + version: 3.0.2 + resolution: "@types/d3-polygon@npm:3.0.2" + checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439 + languageName: node + linkType: hard + +"@types/d3-quadtree@npm:*": + version: 3.0.6 + resolution: "@types/d3-quadtree@npm:3.0.6" + checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616 + languageName: node + linkType: hard + +"@types/d3-random@npm:*": + version: 3.0.3 + resolution: "@types/d3-random@npm:3.0.3" + checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882 + languageName: node + linkType: hard + +"@types/d3-scale-chromatic@npm:*": + version: 3.1.0 + resolution: "@types/d3-scale-chromatic@npm:3.1.0" + checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2 + languageName: node + linkType: hard + +"@types/d3-scale@npm:*": + version: 4.0.8 + resolution: "@types/d3-scale@npm:4.0.8" + dependencies: + "@types/d3-time": "npm:*" + checksum: 10c0/57de90e4016f640b83cb960b7e3a0ab3ed02e720898840ddc5105264ffcfea73336161442fdc91895377c2d2f91904d637282f16852b8535b77e15a761c8e99e + languageName: node + linkType: hard + +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10": + version: 3.0.11 + resolution: "@types/d3-selection@npm:3.0.11" + checksum: 10c0/0c512956c7503ff5def4bb32e0c568cc757b9a2cc400a104fc0f4cfe5e56d83ebde2a97821b6f2cb26a7148079d3b86a2f28e11d68324ed311cf35c2ed980d1d + languageName: node + linkType: hard + +"@types/d3-shape@npm:*": + version: 3.1.7 + resolution: "@types/d3-shape@npm:3.1.7" + dependencies: + "@types/d3-path": "npm:*" + checksum: 10c0/38e59771c1c4c83b67aa1f941ce350410522a149d2175832fdc06396b2bb3b2c1a2dd549e0f8230f9f24296ee5641a515eaf10f55ee1ef6c4f83749e2dd7dcfd + languageName: node + linkType: hard + +"@types/d3-time-format@npm:*": + version: 4.0.3 + resolution: "@types/d3-time-format@npm:4.0.3" + checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90 + languageName: node + linkType: hard + +"@types/d3-time@npm:*": + version: 3.0.4 + resolution: "@types/d3-time@npm:3.0.4" + checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174 + languageName: node + linkType: hard + +"@types/d3-timer@npm:*": + version: 3.0.2 + resolution: "@types/d3-timer@npm:3.0.2" + checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1 + languageName: node + linkType: hard + +"@types/d3-transition@npm:*, @types/d3-transition@npm:^3.0.8": + version: 3.0.9 + resolution: "@types/d3-transition@npm:3.0.9" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/4f68b9df7ac745b3491216c54203cbbfa0f117ae4c60e2609cdef2db963582152035407fdff995b10ee383bae2f05b7743493f48e1b8e46df54faa836a8fb7b5 + languageName: node + linkType: hard + +"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.8": + version: 3.0.8 + resolution: "@types/d3-zoom@npm:3.0.8" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 10c0/1dbdbcafddcae12efb5beb6948546963f29599e18bc7f2a91fb69cc617c2299a65354f2d47e282dfb86fec0968406cd4fb7f76ba2d2fb67baa8e8d146eb4a547 + languageName: node + linkType: hard + +"@types/d3@npm:^7": + version: 7.4.3 + resolution: "@types/d3@npm:7.4.3" + dependencies: + "@types/d3-array": "npm:*" + "@types/d3-axis": "npm:*" + "@types/d3-brush": "npm:*" + "@types/d3-chord": "npm:*" + "@types/d3-color": "npm:*" + "@types/d3-contour": "npm:*" + "@types/d3-delaunay": "npm:*" + "@types/d3-dispatch": "npm:*" + "@types/d3-drag": "npm:*" + "@types/d3-dsv": "npm:*" + "@types/d3-ease": "npm:*" + "@types/d3-fetch": "npm:*" + "@types/d3-force": "npm:*" + "@types/d3-format": "npm:*" + "@types/d3-geo": "npm:*" + "@types/d3-hierarchy": "npm:*" + "@types/d3-interpolate": "npm:*" + "@types/d3-path": "npm:*" + "@types/d3-polygon": "npm:*" + "@types/d3-quadtree": "npm:*" + "@types/d3-random": "npm:*" + "@types/d3-scale": "npm:*" + "@types/d3-scale-chromatic": "npm:*" + "@types/d3-selection": "npm:*" + "@types/d3-shape": "npm:*" + "@types/d3-time": "npm:*" + "@types/d3-time-format": "npm:*" + "@types/d3-timer": "npm:*" + "@types/d3-transition": "npm:*" + "@types/d3-zoom": "npm:*" + checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879 + languageName: node + linkType: hard + +"@types/dagre@npm:^0": + version: 0.7.52 + resolution: "@types/dagre@npm:0.7.52" + checksum: 10c0/0e196a8c17a92765d6e28b10d78d5c1cb1ee540598428cbb61ce3b90e0fedaac2b11f6dbeebf0d2f69d5332d492b12091be5f1e575f538194e20d8887979d006 + languageName: node + linkType: hard + "@types/eslint-plugin-jsx-a11y@npm:6.10.0": version: 6.10.0 resolution: "@types/eslint-plugin-jsx-a11y@npm:6.10.0" @@ -1947,6 +2226,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:*": + version: 7946.0.15 + resolution: "@types/geojson@npm:7946.0.15" + checksum: 10c0/535d21ceaa01717cfdacc8f3dcbb7bc60a04361f401d80e60be22ce8dea23d669e4d0026c2c3da1168e807ee5ad4c9b2b4913ecd78eb0aabbcf76e92dc69808d + languageName: node + linkType: hard + "@types/history@npm:^4.7.11": version: 4.7.11 resolution: "@types/history@npm:4.7.11" @@ -2610,6 +2896,35 @@ __metadata: languageName: node linkType: hard +"@xyflow/react@npm:^12.4.2": + version: 12.4.2 + resolution: "@xyflow/react@npm:12.4.2" + dependencies: + "@xyflow/system": "npm:0.0.50" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.0" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/7f58fd5fa7d9a04645228ad867273c660cc4ca4b77f8dc045c4d2dd52dec2ce31d5a7d92290ec54ea46aaf2e32e4fbf90f81c07cdd4da5ee8a64f06bea6ab373 + languageName: node + linkType: hard + +"@xyflow/system@npm:0.0.50": + version: 0.0.50 + resolution: "@xyflow/system@npm:0.0.50" + dependencies: + "@types/d3-drag": "npm:^3.0.7" + "@types/d3-selection": "npm:^3.0.10" + "@types/d3-transition": "npm:^3.0.8" + "@types/d3-zoom": "npm:^3.0.8" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + checksum: 10c0/7a7e45340efb7e59f898eed726a1f3323857bdeb5b700eb3f2d9338f0bbddccb75c74ddecae15b244fbefe3d5a45d58546e7768730797d39f8219181c8a65753 + languageName: node + linkType: hard + "@zeit/schemas@npm:2.36.0": version: 2.36.0 resolution: "@zeit/schemas@npm:2.36.0" @@ -3364,6 +3679,13 @@ __metadata: languageName: node linkType: hard +"classcat@npm:^5.0.3": + version: 5.0.5 + resolution: "classcat@npm:5.0.5" + checksum: 10c0/ff8d273055ef9b518529cfe80fd0486f7057a9917373807ff802d75ceb46e8f8e148f41fa094ee7625c8f34642cfaa98395ff182d9519898da7cbf383d4a210d + languageName: node + linkType: hard + "classnames@npm:^2.2.6": version: 2.5.1 resolution: "classnames@npm:2.5.1" @@ -3715,6 +4037,98 @@ __metadata: languageName: node linkType: hard +"d3-color@npm:1 - 3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: 10c0/d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 10c0/19f4b4daa8d733906671afff7767c19488f51a43d251f8b7f484d5d3cfc36c663f0a66c38fe91eee30f40327443d799be17169f55a293a3ba949e84e57a33e6a + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 10c0/4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad + languageName: node + linkType: hard + +"d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: 10c0/ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3 + languageName: node + linkType: hard + +"dagre@npm:^0.8.5": + version: 0.8.5 + resolution: "dagre@npm:0.8.5" + dependencies: + graphlib: "npm:^2.1.8" + lodash: "npm:^4.17.15" + checksum: 10c0/1c021b66961aa9a700bb6ec51747bcc214720a661ad6cb1878eab7316ecb550a759664a6754081a315b37d0355e3c19ff162813b36f20cbeb2e37f7440364d62 + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -5219,6 +5633,15 @@ __metadata: languageName: node linkType: hard +"graphlib@npm:^2.1.8": + version: 2.1.8 + resolution: "graphlib@npm:2.1.8" + dependencies: + lodash: "npm:^4.17.15" + checksum: 10c0/41c525e4d91a6d8b4e8da1883bf4e85689a547e908557ccc53f64db9141bdfb351b9162a79f13cae81c5b3a410027f59e4fc1edc1ea442234ec08e629859b188 + languageName: node + linkType: hard + "graphql@npm:^16.8.1": version: 16.9.0 resolution: "graphql@npm:16.9.0" @@ -5515,6 +5938,9 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" + "@types/d3": "npm:^7" + "@types/d3-hierarchy": "npm:^3" + "@types/dagre": "npm:^0" "@types/eslint-plugin-jsx-a11y": "npm:6.10.0" "@types/eslint__js": "npm:8.42.3" "@types/node": "npm:^22.0.0" @@ -5532,12 +5958,14 @@ __metadata: "@uppy/xhr-upload": "npm:^4.2.0" "@vitejs/plugin-react": "npm:^4.3.0" "@vitest/coverage-v8": "npm:2.1.8" + "@xyflow/react": "npm:^12.4.2" axios: "npm:^1.7.2" blob-polyfill: "npm:^9.0.20240710" browserslist: "npm:^4.23.0" browserslist-to-esbuild: "npm:^2.1.1" cross-env: "npm:7.0.3" cypress: "npm:13.17.0" + dagre: "npm:^0.8.5" date-fns: "npm:4.1.0" eslint: "npm:9.18.0" eslint-config-prettier: "npm:9.1.0" @@ -6291,7 +6719,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.21": +"lodash@npm:^4.17.15, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c @@ -8987,7 +9415,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.4.0": version: 1.4.0 resolution: "use-sync-external-store@npm:1.4.0" peerDependencies: @@ -9477,3 +9905,23 @@ __metadata: checksum: 10c0/0223d21dbaa15d8928fe0da3b54696391d8e3e1e2d0283a1a070b5980a1dbba945ce631c2d1eccc088fdbad0f2dfa40155590bf83732d3ac4fcca2cc9237591b languageName: node linkType: hard + +"zustand@npm:^4.4.0": + version: 4.5.6 + resolution: "zustand@npm:4.5.6" + dependencies: + use-sync-external-store: "npm:^1.2.2" + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 10c0/5b39aff2ef57e5a8ada647261ec1115697d397be311c51461d9ea81b5b63c6d2c498b960477ad2db72dc21db6aa229a92bdf644f6a8ecf7b1d71df5b4a5e95d3 + languageName: node + linkType: hard From c612ffdafc62d757613303772f566f1fd17436f8 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Fri, 24 Jan 2025 17:00:41 +0000 Subject: [PATCH 02/14] tree --- src/App.tsx | 4 - src/api/catalogueItems.tsx | 2 +- src/api/items.tsx | 2 +- src/api/systems.tsx | 60 +++++++- src/common/baseLayoutHeader.component.tsx | 8 +- src/systems/systemItemsTable.component.tsx | 1 + src/systems/systemsLayout.component.tsx | 36 ++--- src/systems/systemsNodeHeader.component.tsx | 101 +++++++++++++ src/systems/systemsTree.component.tsx | 157 +++++++++++++++----- 9 files changed, 298 insertions(+), 73 deletions(-) create mode 100644 src/systems/systemsNodeHeader.component.tsx diff --git a/src/App.tsx b/src/App.tsx index 3941595e5..74f1101e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -219,10 +219,6 @@ const routeObject: RouteObject[] = [ Component: Systems, loader: systemsLayoutLoader(queryClient), }, - { - path: 'tree', - Component: SystemsTree, - }, { path: paths.systemTree, Component: SystemsTree, diff --git a/src/api/catalogueItems.tsx b/src/api/catalogueItems.tsx index 4912acbe3..8598ea975 100644 --- a/src/api/catalogueItems.tsx +++ b/src/api/catalogueItems.tsx @@ -68,7 +68,7 @@ export const useGetCatalogueItems = ( }); }; -const getCatalogueItem = async ( +export const getCatalogueItem = async ( catalogueCategoryId: string | undefined ): Promise => { const queryParams = new URLSearchParams(); diff --git a/src/api/items.tsx b/src/api/items.tsx index 53d23e57a..a774a533f 100644 --- a/src/api/items.tsx +++ b/src/api/items.tsx @@ -89,7 +89,7 @@ export const usePostItems = (): UseMutationResult< }); }; -const getItems = async ( +export const getItems = async ( system_id?: string, catalogue_item_id?: string ): Promise => { diff --git a/src/api/systems.tsx b/src/api/systems.tsx index d74281d86..998a8de28 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -18,7 +18,10 @@ import { SystemImportanceType, SystemPatch, SystemPost, + type CatalogueItem, } from './api.types'; +import { getCatalogueItem } from './catalogueItems'; +import { getItems } from './items'; /** Utility for turning an importance into an MUI palette colour to display */ export const getSystemImportanceColour = ( @@ -87,23 +90,66 @@ export const useGetSystem = ( return useQuery(getSystemQuery(id)); }; -export interface SystemTree extends System { +export interface SystemTree extends Partial { + catalogueItems: (CatalogueItem & { itemsQuantity: number })[]; subsystems?: SystemTree[]; } -const getSystemTree = async (parent_id?: string): Promise => { +const getSystemTree = async (parent_id: string): Promise => { // Fetch the systems at the current level + + const rootSystem = await getSystem(parent_id); + const systems = await getSystems(parent_id || 'null'); - // Fetch subsystems for each system recursively - const systemsWithTree = await Promise.all( + // Fetch subsystems and catalogue items for each system recursively + const systemsWithTree: SystemTree[] = await Promise.all( systems.map(async (system) => { - const subsystems = await getSystemTree(system.id); // Fetch subsystems - return { ...system, subsystems }; // Attach subsystems + // Fetch subsystems recursively + const subsystems = await getSystemTree(system.id); + + // Fetch all items for the current system + const items = await getItems(system.id); + + // Group items into catalogue categories and fetch catalogue item details + const catalogueItemIdSet = new Set( + items.map((item) => item.catalogue_item_id) + ); + + const catalogueItems: SystemTree['catalogueItems'] = await Promise.all( + Array.from(catalogueItemIdSet).map(async (id) => { + const catalogueItem = await getCatalogueItem(id); + const categoryItems = items.filter( + (item) => item.catalogue_item_id === id + ); + return { ...catalogueItem, itemsQuantity: categoryItems.length }; + }) + ); + + return { ...system, subsystems, catalogueItems }; + }) + ); + + // Handle the case when there are no systems (leaf nodes or empty levels) + + const items = await getItems(parent_id); + + // Group items into catalogue categories and fetch catalogue item details + const catalogueItemIdSet = new Set( + items.map((item) => item.catalogue_item_id) + ); + + const catalogueItems: SystemTree['catalogueItems'] = await Promise.all( + Array.from(catalogueItemIdSet).map(async (id) => { + const catalogueItem = await getCatalogueItem(id); + const categoryItems = items.filter( + (item) => item.catalogue_item_id === id + ); + return { ...catalogueItem, itemsQuantity: categoryItems.length }; }) ); - return systemsWithTree; + return [{ ...rootSystem, catalogueItems, subsystems: systemsWithTree }]; }; export const useGetSystemsTree = ( diff --git a/src/common/baseLayoutHeader.component.tsx b/src/common/baseLayoutHeader.component.tsx index 7568aa53f..4617ec91e 100644 --- a/src/common/baseLayoutHeader.component.tsx +++ b/src/common/baseLayoutHeader.component.tsx @@ -15,15 +15,11 @@ function BaseLayoutHeader(props: BaseLayoutHeaderProps) { const { breadcrumbsInfo, children, homeLocation } = props; const navigate = useNavigate(); - // Check if we are in Tree View or Normal View by looking for '/tree' in the URL - const isTreeView = location.pathname.includes('tree'); const onChangeNode = React.useCallback( (id: string | null) => { - navigate( - `/${RoutesHomeLocation[homeLocation]}${id ? `/${id}` : ''}${isTreeView && !id && homeLocation == 'Systems' ? '/tree' : ''}` - ); + navigate(`/${RoutesHomeLocation[homeLocation]}${id ? `/${id}` : ''}`); }, - [homeLocation, isTreeView, navigate] + [homeLocation, navigate] ); return ( diff --git a/src/systems/systemItemsTable.component.tsx b/src/systems/systemItemsTable.component.tsx index 52fcba4f6..c05052965 100644 --- a/src/systems/systemItemsTable.component.tsx +++ b/src/systems/systemItemsTable.component.tsx @@ -211,6 +211,7 @@ export function SystemItemsTable(props: SystemItemsTableProps) { header: 'Catalogue Item', Header: TableHeaderOverflowTip, accessorFn: (row) => row.catalogueItem?.name, + getGroupingValue: (row) => row.catalogueItem?.id ?? '', id: 'catalogueItem.name', Cell: type === 'normal' diff --git a/src/systems/systemsLayout.component.tsx b/src/systems/systemsLayout.component.tsx index 7df15f139..2e82344f6 100644 --- a/src/systems/systemsLayout.component.tsx +++ b/src/systems/systemsLayout.component.tsx @@ -73,23 +73,25 @@ function SystemsLayout() { ]) ?? [], }} > - - - - Normal View - - - - Tree View - - + {location.pathname !== '/systems' && ( + + + + Normal View + + + + Tree View + + + )} ); diff --git a/src/systems/systemsNodeHeader.component.tsx b/src/systems/systemsNodeHeader.component.tsx new file mode 100644 index 000000000..e194479f0 --- /dev/null +++ b/src/systems/systemsNodeHeader.component.tsx @@ -0,0 +1,101 @@ +import { MoreHoriz } from '@mui/icons-material'; +import { Grid, IconButton } from '@mui/material'; +import { Handle, Position } from '@xyflow/react'; +import React from 'react'; +import { OverflowTip } from '../utils'; + +interface SystemsNodeHeaderProps { + data: { + title: string | React.ReactNode; + label: string | React.ReactNode; + direction?: 'TB' | 'LR'; + setNodeDimensions: (nodeId: string, width: number, height: number) => void; + nodeId: string; + }; +} + +const SystemsNodeHeader = (props: SystemsNodeHeaderProps) => { + const { data } = props; + + const containerRef = React.useRef(null); + + React.useEffect(() => { + if (containerRef.current) { + const { offsetWidth, offsetHeight } = containerRef.current; + data.setNodeDimensions(data.nodeId, offsetWidth, offsetHeight); + } + }, [data, containerRef]); + + const isHorizontal = data.direction === 'LR'; + return ( + + {/* Header Section */} + + + + {data.title} + + + + {/* Actions Menu */} + + + + + + + {/* Label Section */} + + {typeof data.label === 'string' ? ( + + {data.label} + + ) : ( + data.label + )} + + + + + + ); +}; + +export default SystemsNodeHeader; diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx index 9429d0828..71eb93473 100644 --- a/src/systems/systemsTree.component.tsx +++ b/src/systems/systemsTree.component.tsx @@ -1,6 +1,7 @@ import { Box, LinearProgress, + Link as MuiLink, ToggleButton, ToggleButtonGroup, Typography, @@ -9,6 +10,7 @@ import { Background, ConnectionLineType, Controls, + MiniMap, Panel, Position, ReactFlow, @@ -20,16 +22,13 @@ import { import '@xyflow/react/dist/style.css'; import dagre from 'dagre'; import React, { useEffect } from 'react'; -import { useParams } from 'react-router-dom'; -import { - useGetSystem, - useGetSystemsTree, - type SystemTree, -} from '../api/systems'; +import { Link, useParams } from 'react-router-dom'; +import { useGetSystemsTree, type SystemTree } from '../api/systems'; import { getPageHeightCalc } from '../utils'; +import SystemsNodeHeader from './systemsNodeHeader.component'; -const nodeWidth = 172; -const nodeHeight = 36; +const nodeWidth = 250; +const nodeHeight = 250; // Function to apply the Dagre layout const getLayoutedElements = ( @@ -40,10 +39,12 @@ const getLayoutedElements = ( const isHorizontal = direction === 'LR'; const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); - dagreGraph.setGraph({ rankdir: direction }); + dagreGraph.setGraph({ + rankdir: direction, + }); nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + dagreGraph.setNode(node.id, { width: node.width, height: node.height }); }); edges.forEach((edge) => { @@ -54,13 +55,30 @@ const getLayoutedElements = ( const layoutedNodes = nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); + // This is done to prevent overlapping nodes when a system only has one subsystem + const sourceIds = edges.map((val) => val.source); + const idCount = sourceIds.reduce( + (acc, id) => { + acc[id] = (acc[id] || 0) + 1; + return acc; + }, + {} as Record + ); + const hasMoreThanOneChild = Object.values(idCount).some( + (count) => count > 1 + ); + return { ...node, targetPosition: isHorizontal ? Position.Left : Position.Top, sourcePosition: isHorizontal ? Position.Right : Position.Bottom, position: { - x: nodeWithPosition.x - nodeWidth / 2, - y: nodeWithPosition.y - nodeHeight / 2, + x: nodeWithPosition.x - nodeWithPosition.width / 2, + y: isHorizontal + ? nodeWithPosition.y + : hasMoreThanOneChild + ? nodeWithPosition.y + : nodeWithPosition.y - nodeWithPosition.height / 2, }, }; }); @@ -75,20 +93,36 @@ interface SystemFlowProps { const SystemsTree: React.FC = () => { const { system_id: systemId } = useParams(); const { data: systemsTree, isLoading } = useGetSystemsTree(systemId); - const { data: rootSystem } = useGetSystem(systemId); const [layoutDirection, setLayoutDirection] = React.useState<'TB' | 'LR'>( 'TB' ); + const [nodeDimensionsReady, setNodeDimensionsReady] = React.useState(false); + + const nodeDimensions = React.useRef< + Record + >({}); + const totalNodes = React.useRef(0); // To keep track of the total nodes. + + const setNodeDimensions = React.useCallback( + (nodeId: string, width: number, height: number) => { + nodeDimensions.current[nodeId] = { width, height }; + if (Object.keys(nodeDimensions.current).length === totalNodes.current) { + setNodeDimensionsReady(true); // Mark dimensions as ready when all are set. + } + }, + [] + ); - const xSpacing = 300; - const ySpacing = 200; + const xSpacing = 800; + const ySpacing = 500; - const [nodes, setNodes, onNodesChange] = useNodesState([]); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [nodes, setNodes, _onNodesChange] = useNodesState([]); + const [edges, setEdges, _onEdgesChange] = useEdgesState([]); useEffect(() => { if (systemsTree) { + let systemIndex = 0; const transformToFlowData = ( systems: SystemTree[], parentId?: string @@ -98,46 +132,84 @@ const SystemsTree: React.FC = () => { systems.forEach((system, index) => { nodes.push({ - id: system.id, - data: { label: system.name }, + id: system.id ?? '', + type: 'systems', + data: { + title: + systemIndex === 0 ? ( + system.name + ) : ( + + {system.name} + + ), + label: ( + + {/* Items Heading */} + + Items: + + {/* List of Catalogue Items */} + {system.catalogueItems.length > 0 ? ( + system.catalogueItems.map((catalogueItem) => ( + + {catalogueItem.name}: {catalogueItem.itemsQuantity} + + )) + ) : ( + + No Items + + )} + + ), + direction: layoutDirection, + setNodeDimensions, + nodeId: system.id ?? '', + }, position: { x: index * xSpacing, y: index * ySpacing }, }); - - if (parentId) { + // only have the edges for nodes that connect to other nodes + if (parentId && system.id !== parentId) { edges.push({ id: `e-${parentId}-${system.id}`, source: parentId, - target: system.id, + target: system.id ?? '', type: 'smoothstep', }); } + // Handle subsystems recursively if (system.subsystems && system.subsystems.length > 0) { + systemIndex++; const { nodes: childNodes, edges: childEdges } = transformToFlowData(system.subsystems, system.id); + nodes = [...nodes, ...childNodes]; edges = [...edges, ...childEdges]; } }); - - return { nodes, edges }; + // Ensure unique nodes + const uniqueNodes = Array.from( + new Map(nodes.map((node) => [node.id, node])).values() + ); + totalNodes.current = uniqueNodes.length; + return { nodes: uniqueNodes, edges: edges }; }; const generateTreeWithRoot = ( systems: SystemTree[] ): { nodes: Node[]; edges: Edge[] } => { - const rootNode = { - id: systemId || 'root', - data: { label: rootSystem?.name || 'Root' }, - position: { x: (xSpacing * systems.length) / 2, y: 0 }, - }; - const { nodes, edges } = transformToFlowData( systems, systemId || 'root' ); return { - nodes: [rootNode, ...nodes], + nodes: [...nodes], edges, }; }; @@ -146,10 +218,21 @@ const SystemsTree: React.FC = () => { generateTreeWithRoot(systemsTree); const { nodes: layoutedNodes, edges: layoutedEdges } = - getLayoutedElements(rawNodes, rawEdges, layoutDirection); + getLayoutedElements( + rawNodes.map((node) => ({ + ...node, + width: nodeDimensions.current[node.id]?.width || nodeWidth, + height: nodeDimensions.current[node.id]?.height || nodeHeight, + })), + rawEdges, + layoutDirection + ); setNodes(layoutedNodes); setEdges(layoutedEdges); + } else { + setNodes([]); + setEdges([]); } }, [ systemsTree, @@ -157,11 +240,11 @@ const SystemsTree: React.FC = () => { systemId, setNodes, setEdges, - rootSystem?.name, + nodeDimensions, + nodeDimensionsReady, + setNodeDimensions, ]); - console.log(nodes); - const handleToggleLayout = ( _event: React.MouseEvent, newDirection: 'TB' | 'LR' @@ -202,9 +285,8 @@ const SystemsTree: React.FC = () => { @@ -218,6 +300,7 @@ const SystemsTree: React.FC = () => { Horizontal + From 5212be43a7ec8d130e94c4de5d4cab32b0d0f09b Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Sat, 25 Jan 2025 18:45:54 +0000 Subject: [PATCH 03/14] Reafctor implementation --- package.json | 2 +- src/systems/systemsNodeHeader.component.tsx | 111 +++--- src/systems/systemsTree.component.tsx | 381 +++++++++++--------- yarn.lock | 39 +- 4 files changed, 272 insertions(+), 261 deletions(-) diff --git a/package.json b/package.json index a7216c836..b5d436a11 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "dependencies": { "@codecov/vite-plugin": "1.7.0", + "@dagrejs/dagre": "^1.1.4", "@date-io/date-fns": "3.2.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", @@ -33,7 +34,6 @@ "blob-polyfill": "^9.0.20240710", "browserslist": "^4.23.0", "browserslist-to-esbuild": "^2.1.1", - "dagre": "^0.8.5", "date-fns": "4.1.0", "history": "^5.3.0", "loglevel": "^1.9.1", diff --git a/src/systems/systemsNodeHeader.component.tsx b/src/systems/systemsNodeHeader.component.tsx index e194479f0..a255e869b 100644 --- a/src/systems/systemsNodeHeader.component.tsx +++ b/src/systems/systemsNodeHeader.component.tsx @@ -1,5 +1,5 @@ import { MoreHoriz } from '@mui/icons-material'; -import { Grid, IconButton } from '@mui/material'; +import { Card, Divider, Grid, IconButton } from '@mui/material'; import { Handle, Position } from '@xyflow/react'; import React from 'react'; import { OverflowTip } from '../utils'; @@ -9,92 +9,79 @@ interface SystemsNodeHeaderProps { title: string | React.ReactNode; label: string | React.ReactNode; direction?: 'TB' | 'LR'; - setNodeDimensions: (nodeId: string, width: number, height: number) => void; - nodeId: string; + id: string; }; } const SystemsNodeHeader = (props: SystemsNodeHeaderProps) => { const { data } = props; - const containerRef = React.useRef(null); - - React.useEffect(() => { - if (containerRef.current) { - const { offsetWidth, offsetHeight } = containerRef.current; - data.setNodeDimensions(data.nodeId, offsetWidth, offsetHeight); - } - }, [data, containerRef]); - const isHorizontal = data.direction === 'LR'; return ( - - {/* Header Section */} - + + {/* Header Section */} - - {data.title} - + + + {data.title} + + + + {/* Actions Menu */} + + + + - - {/* Actions Menu */} - - - + + {/* Label Section */} + + {typeof data.label === 'string' ? ( + + {data.label} + + ) : ( + data.label + )} - - - {/* Label Section */} - - {typeof data.label === 'string' ? ( - - {data.label} - - ) : ( - data.label - )} - - + - + ); }; diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx index 71eb93473..b52051ed0 100644 --- a/src/systems/systemsTree.component.tsx +++ b/src/systems/systemsTree.component.tsx @@ -1,3 +1,4 @@ +import dagre from '@dagrejs/dagre'; import { Box, LinearProgress, @@ -5,6 +6,7 @@ import { ToggleButton, ToggleButtonGroup, Typography, + useTheme, } from '@mui/material'; import { Background, @@ -14,37 +16,64 @@ import { Panel, Position, ReactFlow, + ReactFlowProvider, useEdgesState, useNodesState, + useReactFlow, + useStore, type Edge, + type InternalNode, type Node, + type ReactFlowState, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import dagre from 'dagre'; -import React, { useEffect } from 'react'; +import type { NodeLookup } from '@xyflow/system'; +import React from 'react'; import { Link, useParams } from 'react-router-dom'; import { useGetSystemsTree, type SystemTree } from '../api/systems'; import { getPageHeightCalc } from '../utils'; import SystemsNodeHeader from './systemsNodeHeader.component'; -const nodeWidth = 250; +interface SystemFlowProps { + rawEdges: Edge[]; + rawNodes: Node[]; + layoutDirection: 'TB' | 'LR'; + handleToggleLayout: ( + _event: React.MouseEvent, + newDirection: 'TB' | 'LR' + ) => void; +} + +const nodeWidth = 300; const nodeHeight = 250; -// Function to apply the Dagre layout +const calculateRanksep = (nodes: Node[]): number => { + const maxHeight = Math.max(...nodes.map((node) => node.height || nodeHeight)); + return maxHeight; // Add extra space to avoid overlap +}; + const getLayoutedElements = ( nodes: Node[], edges: Edge[], - direction = 'TB' + direction = 'TB', + nodeInternals: NodeLookup> ) => { const isHorizontal = direction === 'LR'; + const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); dagreGraph.setGraph({ rankdir: direction, + nodesep: 50, + ranksep: isHorizontal ? 150 : calculateRanksep(nodes), }); nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: node.width, height: node.height }); + const nodeInternal = nodeInternals.get(node.id); + dagreGraph.setNode(node.id, { + width: nodeWidth, + height: nodeInternal?.measured.height || nodeHeight, + }); }); edges.forEach((edge) => { @@ -55,7 +84,7 @@ const getLayoutedElements = ( const layoutedNodes = nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); - // This is done to prevent overlapping nodes when a system only has one subsystem + // This prevents overlapping nodes when a system has only one subsystem const sourceIds = edges.map((val) => val.source); const idCount = sourceIds.reduce( (acc, id) => { @@ -75,7 +104,9 @@ const getLayoutedElements = ( position: { x: nodeWithPosition.x - nodeWithPosition.width / 2, y: isHorizontal - ? nodeWithPosition.y + ? hasMoreThanOneChild + ? nodeWithPosition.y + : nodeWithPosition.y - nodeWithPosition.height / 2 : hasMoreThanOneChild ? nodeWithPosition.y : nodeWithPosition.y - nodeWithPosition.height / 2, @@ -86,164 +117,172 @@ const getLayoutedElements = ( return { nodes: layoutedNodes, edges }; }; -interface SystemFlowProps { - parentId?: string | null; -} +const SystemsFlow = (props: SystemFlowProps) => { + const { rawEdges, rawNodes, layoutDirection, handleToggleLayout } = props; -const SystemsTree: React.FC = () => { - const { system_id: systemId } = useParams(); - const { data: systemsTree, isLoading } = useGetSystemsTree(systemId); + const [nodes, setNodes, _onNodesChange] = useNodesState(rawNodes); + const [edges, setEdges, _onEdgesChange] = useEdgesState(rawEdges); + const { fitView } = useReactFlow(); + const theme = useTheme(); - const [layoutDirection, setLayoutDirection] = React.useState<'TB' | 'LR'>( - 'TB' - ); - const [nodeDimensionsReady, setNodeDimensionsReady] = React.useState(false); + const nodeInternals = useStore((state: ReactFlowState) => state.nodeLookup); - const nodeDimensions = React.useRef< - Record - >({}); - const totalNodes = React.useRef(0); // To keep track of the total nodes. + const flattenedNodes = Array.from(nodeInternals.values()); - const setNodeDimensions = React.useCallback( - (nodeId: string, width: number, height: number) => { - nodeDimensions.current[nodeId] = { width, height }; - if (Object.keys(nodeDimensions.current).length === totalNodes.current) { - setNodeDimensionsReady(true); // Mark dimensions as ready when all are set. - } - }, - [] + const [firstNodeHeight, setFirstNodeHeight] = React.useState< + number | undefined + >(flattenedNodes[0]?.measured?.height); + + // Triggers a refresh for the Layout when the nodes have been measured + React.useEffect(() => { + if (flattenedNodes[0]?.measured?.height) { + setFirstNodeHeight(flattenedNodes[0]?.measured?.height); + } + }, [flattenedNodes]); + + // Sets the new node edges positions using dagre layouting + React.useEffect(() => { + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( + rawNodes, + rawEdges, + layoutDirection, + nodeInternals + ); + setNodes(layoutedNodes); + setEdges(layoutedEdges); + }, [ + layoutDirection, + nodeInternals, + rawEdges, + rawNodes, + setEdges, + setNodes, + firstNodeHeight, + ]); + + React.useEffect(() => { + window.requestAnimationFrame(() => fitView()); + }, [fitView, layoutDirection, nodes]); + return ( + + + + + Vertical + Horizontal + + + + + + + ); +}; - const xSpacing = 800; - const ySpacing = 500; +const SystemsTree = () => { + const { system_id: systemId } = useParams(); + const { data: systemsTree, isLoading } = useGetSystemsTree(systemId); - const [nodes, setNodes, _onNodesChange] = useNodesState([]); - const [edges, setEdges, _onEdgesChange] = useEdgesState([]); + const [layoutDirection, setLayoutDirection] = React.useState<'TB' | 'LR'>( + 'TB' + ); - useEffect(() => { - if (systemsTree) { - let systemIndex = 0; - const transformToFlowData = ( - systems: SystemTree[], - parentId?: string - ): { nodes: Node[]; edges: Edge[] } => { - let nodes: Node[] = []; - let edges: Edge[] = []; + let systemIndex = 0; + const transformToFlowData = React.useCallback( + ( + systems: SystemTree[], + parentId?: string + ): { nodes: Node[]; edges: Edge[] } => { + let nodes: Node[] = []; + let edges: Edge[] = []; - systems.forEach((system, index) => { - nodes.push({ - id: system.id ?? '', - type: 'systems', - data: { - title: - systemIndex === 0 ? ( - system.name + systems.forEach((system) => { + nodes.push({ + id: system.id ?? '', + type: 'systems', + style: { width: '300px' }, + data: { + title: + systemIndex === 0 ? ( + system.name + ) : ( + + {system.name} + + ), + label: ( + + {/* Items Heading */} + + Items: + + {/* List of Catalogue Items */} + {system.catalogueItems.length > 0 ? ( + system.catalogueItems.map((catalogueItem) => ( + + {catalogueItem.name}: {catalogueItem.itemsQuantity} + + )) ) : ( - - {system.name} - - ), - label: ( - - {/* Items Heading */} - - Items: + + No Items - {/* List of Catalogue Items */} - {system.catalogueItems.length > 0 ? ( - system.catalogueItems.map((catalogueItem) => ( - - {catalogueItem.name}: {catalogueItem.itemsQuantity} - - )) - ) : ( - - No Items - - )} - - ), - direction: layoutDirection, - setNodeDimensions, - nodeId: system.id ?? '', - }, - position: { x: index * xSpacing, y: index * ySpacing }, - }); - // only have the edges for nodes that connect to other nodes - if (parentId && system.id !== parentId) { - edges.push({ - id: `e-${parentId}-${system.id}`, - source: parentId, - target: system.id ?? '', - type: 'smoothstep', - }); - } - - // Handle subsystems recursively - if (system.subsystems && system.subsystems.length > 0) { - systemIndex++; - const { nodes: childNodes, edges: childEdges } = - transformToFlowData(system.subsystems, system.id); - - nodes = [...nodes, ...childNodes]; - edges = [...edges, ...childEdges]; - } + )} + + ), + direction: layoutDirection, + id: system.id ?? '', + }, + // position will be set by dagre + position: { x: 0, y: 0 }, }); - // Ensure unique nodes - const uniqueNodes = Array.from( - new Map(nodes.map((node) => [node.id, node])).values() - ); - totalNodes.current = uniqueNodes.length; - return { nodes: uniqueNodes, edges: edges }; - }; - - const generateTreeWithRoot = ( - systems: SystemTree[] - ): { nodes: Node[]; edges: Edge[] } => { - const { nodes, edges } = transformToFlowData( - systems, - systemId || 'root' - ); - return { - nodes: [...nodes], - edges, - }; - }; - - const { nodes: rawNodes, edges: rawEdges } = - generateTreeWithRoot(systemsTree); + // only have the edges for nodes that connect to other nodes + if (parentId && system.id !== parentId) { + edges.push({ + id: `e-${parentId}-${system.id}`, + source: parentId, + target: system.id ?? '', + type: 'smoothstep', + }); + } - const { nodes: layoutedNodes, edges: layoutedEdges } = - getLayoutedElements( - rawNodes.map((node) => ({ - ...node, - width: nodeDimensions.current[node.id]?.width || nodeWidth, - height: nodeDimensions.current[node.id]?.height || nodeHeight, - })), - rawEdges, - layoutDirection - ); + // Handle subsystems recursively + if (system.subsystems && system.subsystems.length > 0) { + systemIndex++; + const { nodes: childNodes, edges: childEdges } = transformToFlowData( + system.subsystems, + system.id + ); - setNodes(layoutedNodes); - setEdges(layoutedEdges); - } else { - setNodes([]); - setEdges([]); - } - }, [ - systemsTree, - layoutDirection, - systemId, - setNodes, - setEdges, - nodeDimensions, - nodeDimensionsReady, - setNodeDimensions, - ]); + nodes = [...nodes, ...childNodes]; + edges = [...edges, ...childEdges]; + } + }); + // Ensure unique nodes + const uniqueNodes = Array.from( + new Map(nodes.map((node) => [node.id, node])).values() + ); + return { nodes: uniqueNodes, edges: edges }; + }, + [layoutDirection, systemIndex] + ); const handleToggleLayout = ( _event: React.MouseEvent, @@ -254,7 +293,7 @@ const SystemsTree: React.FC = () => { } }; - if (isLoading) { + if (isLoading || !systemsTree) { return ( @@ -280,31 +319,19 @@ const SystemsTree: React.FC = () => { ); } + const { nodes: rawNodes, edges: rawEdges } = transformToFlowData( + systemsTree, + systemId + ); return ( - - - - - Vertical - Horizontal - - - - - - - + + + ); }; diff --git a/yarn.lock b/yarn.lock index b0b914b3b..551344fe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -453,6 +453,22 @@ __metadata: languageName: node linkType: hard +"@dagrejs/dagre@npm:^1.1.4": + version: 1.1.4 + resolution: "@dagrejs/dagre@npm:1.1.4" + dependencies: + "@dagrejs/graphlib": "npm:2.2.4" + checksum: 10c0/1717ecb1667a70199881871a69990fc2521b9e002e9cc5088cad55ae6fffe349b0bfd8fcb8a7c30e6d19c29dc6a080138bff2e675336bb5f85d5439e12581bf5 + languageName: node + linkType: hard + +"@dagrejs/graphlib@npm:2.2.4": + version: 2.2.4 + resolution: "@dagrejs/graphlib@npm:2.2.4" + checksum: 10c0/14597ea9294c46b2571aee78bcaad3a24e3e5e0ebcdf198b6eae5b3805f99af727ac54a477dd9152e8b0a576efea0528fb7d4919c74801e9f669c90e5e6f5bd9 + languageName: node + linkType: hard + "@date-io/core@npm:^3.2.0": version: 3.2.0 resolution: "@date-io/core@npm:3.2.0" @@ -4119,16 +4135,6 @@ __metadata: languageName: node linkType: hard -"dagre@npm:^0.8.5": - version: 0.8.5 - resolution: "dagre@npm:0.8.5" - dependencies: - graphlib: "npm:^2.1.8" - lodash: "npm:^4.17.15" - checksum: 10c0/1c021b66961aa9a700bb6ec51747bcc214720a661ad6cb1878eab7316ecb550a759664a6754081a315b37d0355e3c19ff162813b36f20cbeb2e37f7440364d62 - languageName: node - linkType: hard - "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -5633,15 +5639,6 @@ __metadata: languageName: node linkType: hard -"graphlib@npm:^2.1.8": - version: 2.1.8 - resolution: "graphlib@npm:2.1.8" - dependencies: - lodash: "npm:^4.17.15" - checksum: 10c0/41c525e4d91a6d8b4e8da1883bf4e85689a547e908557ccc53f64db9141bdfb351b9162a79f13cae81c5b3a410027f59e4fc1edc1ea442234ec08e629859b188 - languageName: node - linkType: hard - "graphql@npm:^16.8.1": version: 16.9.0 resolution: "graphql@npm:16.9.0" @@ -5920,6 +5917,7 @@ __metadata: dependencies: "@babel/eslint-parser": "npm:7.25.9" "@codecov/vite-plugin": "npm:1.7.0" + "@dagrejs/dagre": "npm:^1.1.4" "@date-io/date-fns": "npm:3.2.0" "@emotion/react": "npm:^11.11.4" "@emotion/styled": "npm:^11.11.5" @@ -5965,7 +5963,6 @@ __metadata: browserslist-to-esbuild: "npm:^2.1.1" cross-env: "npm:7.0.3" cypress: "npm:13.17.0" - dagre: "npm:^0.8.5" date-fns: "npm:4.1.0" eslint: "npm:9.18.0" eslint-config-prettier: "npm:9.1.0" @@ -6719,7 +6716,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15, lodash@npm:^4.17.21": +"lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c From 7160fd98b0588168f6f7cf39a9f56ca267782e50 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Tue, 28 Jan 2025 14:01:44 +0000 Subject: [PATCH 04/14] optimisations #1272 --- src/api/systems.tsx | 116 ++++++++++++++-- src/systems/systemsTree.component.tsx | 185 +++++++++++++++++++------- 2 files changed, 239 insertions(+), 62 deletions(-) diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 998a8de28..5538e800e 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -95,18 +95,74 @@ export interface SystemTree extends Partial { subsystems?: SystemTree[]; } -const getSystemTree = async (parent_id: string): Promise => { - // Fetch the systems at the current level +const getSystemTree = async ( + parent_id: string, + maxSubsystems: number, // Total max subsystems allowed + maxDepth?: number, + currentDepth: number = 0, // Default value + subsystemsCutOffPoint?: number, // Total cutoff point + totalSubsystems: { count: number } = { count: 0 }, // Shared counter object + catalogueItemCache: Map = new Map() // Shared cache for catalogue items +): Promise => { + // Stop recursion if currentDepth exceeds maxDepth + if (maxDepth !== undefined && currentDepth >= maxDepth) { + return []; + } + // Fetch the root system const rootSystem = await getSystem(parent_id); + // Fetch systems at the current level const systems = await getSystems(parent_id || 'null'); + // Increment the total count of subsystems + totalSubsystems.count += systems.length; + + // Throw an AxiosError if the total count exceeds maxSubsystems + if (maxSubsystems !== undefined && totalSubsystems.count > maxSubsystems) { + throw new AxiosError( + `Total subsystems exceeded the maximum allowed limit of ${maxSubsystems}. Current count: ${totalSubsystems.count}`, + 'SubsystemLimitExceeded', + undefined, + null, + { + status: 400, + statusText: 'Bad Request', + headers: {}, + // @ts-expect-error: not needed + config: {}, + data: { + message: `Subsystem limit exceeded. Max: ${maxSubsystems}, Current: ${totalSubsystems.count}`, + }, + } + ); + } + + // Stop recursion if totalSubsystems count exceeds the cutoff point + if ( + subsystemsCutOffPoint !== undefined && + totalSubsystems.count > subsystemsCutOffPoint + ) { + return systems.map((system) => ({ + ...system, + subsystems: [], + catalogueItems: [], + })); + } + // Fetch subsystems and catalogue items for each system recursively const systemsWithTree: SystemTree[] = await Promise.all( systems.map(async (system) => { - // Fetch subsystems recursively - const subsystems = await getSystemTree(system.id); + // Fetch subsystems recursively, increasing the depth + const subsystems = await getSystemTree( + system.id, + maxSubsystems, + maxDepth, + currentDepth + 1, + subsystemsCutOffPoint, + totalSubsystems, + catalogueItemCache + ); // Fetch all items for the current system const items = await getItems(system.id); @@ -118,7 +174,15 @@ const getSystemTree = async (parent_id: string): Promise => { const catalogueItems: SystemTree['catalogueItems'] = await Promise.all( Array.from(catalogueItemIdSet).map(async (id) => { - const catalogueItem = await getCatalogueItem(id); + // Check if the item exists in the cache + if (!catalogueItemCache.has(id)) { + // If not, fetch and store it in the cache + const fetchedCatalogueItem = await getCatalogueItem(id); + catalogueItemCache.set(id, fetchedCatalogueItem); + } + + // Retrieve the item from the cache + const catalogueItem = catalogueItemCache.get(id)!; const categoryItems = items.filter( (item) => item.catalogue_item_id === id ); @@ -131,7 +195,6 @@ const getSystemTree = async (parent_id: string): Promise => { ); // Handle the case when there are no systems (leaf nodes or empty levels) - const items = await getItems(parent_id); // Group items into catalogue categories and fetch catalogue item details @@ -141,7 +204,15 @@ const getSystemTree = async (parent_id: string): Promise => { const catalogueItems: SystemTree['catalogueItems'] = await Promise.all( Array.from(catalogueItemIdSet).map(async (id) => { - const catalogueItem = await getCatalogueItem(id); + // Check if the item exists in the cache + if (!catalogueItemCache.has(id)) { + // If not, fetch and store it in the cache + const fetchedCatalogueItem = await getCatalogueItem(id); + catalogueItemCache.set(id, fetchedCatalogueItem); + } + + // Retrieve the item from the cache + const catalogueItem = catalogueItemCache.get(id)!; const categoryItems = items.filter( (item) => item.catalogue_item_id === id ); @@ -149,15 +220,38 @@ const getSystemTree = async (parent_id: string): Promise => { }) ); - return [{ ...rootSystem, catalogueItems, subsystems: systemsWithTree }]; + return [ + { + ...rootSystem, + catalogueItems, + subsystems: systemsWithTree, + }, + ]; }; export const useGetSystemsTree = ( - parent_id?: string | null + parent_id?: string | null, + maxDepth?: number, + subsystemsCutOffPoint?: number, // Add cutoff point as a parameter + maxSubsystems?: number ): UseQueryResult => { return useQuery({ - queryKey: ['SystemsTree', parent_id], - queryFn: () => getSystemTree(parent_id ?? ''), + queryKey: [ + 'SystemsTree', + parent_id, + maxSubsystems, + maxDepth, + subsystemsCutOffPoint, + ], + queryFn: () => + getSystemTree( + parent_id ?? '', + maxSubsystems ?? 150, + maxDepth, + 0, + subsystemsCutOffPoint + ), + staleTime: 1000 * 60 * 5, }); }; diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx index b52051ed0..57b055759 100644 --- a/src/systems/systemsTree.component.tsx +++ b/src/systems/systemsTree.component.tsx @@ -1,10 +1,13 @@ import dagre from '@dagrejs/dagre'; +import { Warning } from '@mui/icons-material'; import { Box, + IconButton, LinearProgress, Link as MuiLink, ToggleButton, ToggleButtonGroup, + Tooltip, Typography, useTheme, } from '@mui/material'; @@ -13,7 +16,6 @@ import { ConnectionLineType, Controls, MiniMap, - Panel, Position, ReactFlow, ReactFlowProvider, @@ -38,10 +40,6 @@ interface SystemFlowProps { rawEdges: Edge[]; rawNodes: Node[]; layoutDirection: 'TB' | 'LR'; - handleToggleLayout: ( - _event: React.MouseEvent, - newDirection: 'TB' | 'LR' - ) => void; } const nodeWidth = 300; @@ -118,7 +116,7 @@ const getLayoutedElements = ( }; const SystemsFlow = (props: SystemFlowProps) => { - const { rawEdges, rawNodes, layoutDirection, handleToggleLayout } = props; + const { rawEdges, rawNodes, layoutDirection } = props; const [nodes, setNodes, _onNodesChange] = useNodesState(rawNodes); const [edges, setEdges, _onEdgesChange] = useEdgesState(rawEdges); @@ -173,17 +171,6 @@ const SystemsFlow = (props: SystemFlowProps) => { nodeTypes={{ systems: SystemsNodeHeader }} fitView > - - - Vertical - Horizontal - - @@ -194,12 +181,26 @@ const SystemsFlow = (props: SystemFlowProps) => { const SystemsTree = () => { const { system_id: systemId } = useParams(); - const { data: systemsTree, isLoading } = useGetSystemsTree(systemId); const [layoutDirection, setLayoutDirection] = React.useState<'TB' | 'LR'>( 'TB' ); + const [maxDepth, setMaxDepth] = React.useState<-1 | 1 | 2 | 3>(1); + + const maxSubsystems = 150; + const subsystemsCutOff = 100; + + const { + data: systemsTree, + isLoading, + error, + } = useGetSystemsTree( + systemId, + maxDepth === -1 ? undefined : maxDepth, + subsystemsCutOff, + maxSubsystems + ); let systemIndex = 0; const transformToFlowData = React.useCallback( ( @@ -233,7 +234,7 @@ const SystemsTree = () => { {system.catalogueItems.length > 0 ? ( system.catalogueItems.map((catalogueItem) => ( @@ -293,44 +294,126 @@ const SystemsTree = () => { } }; - if (isLoading || !systemsTree) { - return ( - - - - - Taking time to gather data... This may take a couple of minutes. - - - Note: If this system is high up the tree with many subsystems and - items, this process might take significantly longer. - - - - ); - } + const handleToggleMaxDepth = ( + _event: React.MouseEvent, + newDepth: -1 | 1 | 2 | 3 + ) => { + if (newDepth !== null) { + setMaxDepth(newDepth); + } + }; + const isLimitedReached = + (error?.response?.data as { message?: string })?.message?.includes( + 'Subsystem limit exceeded' + ) ?? false; const { nodes: rawNodes, edges: rawEdges } = transformToFlowData( - systemsTree, + systemsTree ?? [], systemId ); return ( - + + + + Depth: + + + + 1 + 2 + 3 + unlimited + + + + The larger the depth, the longer the query may take. If the + number of subsystems exceeds {maxSubsystems}, the tree will not + load. + + } + placement="right" + enterTouchDelay={0} + > + + + + + + + + Vertical + Horizontal + + + + {isLoading || !systemsTree ? ( + + {!isLimitedReached && } + + {!isLimitedReached ? ( + <> + + Taking time to gather data... This may take a couple of + minutes. + + + Note: If this system is high up the tree with many subsystems + and items, this process might take significantly longer. + + + ) : ( + <> + + The maximum number of subsystems has been reached. + + + To view more data, consider decreasing the depth of the tree, + navigating down to a subtree with fewer subsystems, or try + looking at the normal view for more limited results. + + + )} + + + ) : ( + + )} ); }; From 5203e9cf8382414be82e3321f87ef06a4204ffee Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 13 Feb 2025 20:00:38 +0000 Subject: [PATCH 05/14] use react query instead of axios --- src/api/catalogueCategories.tsx | 5 +- src/api/catalogueItems.tsx | 12 ++- src/api/items.tsx | 32 +++++-- src/api/manufacturers.tsx | 8 +- src/api/systems.tsx | 89 +++++++++++------- src/app.types.tsx | 11 +++ src/catalogue/catalogueLayout.component.tsx | 6 +- .../manufacturerLayout.component.tsx | 2 +- src/systems/systemsLayout.component.tsx | 4 +- src/systems/systemsTree.component.tsx | 92 ++++++++++++------- 10 files changed, 174 insertions(+), 87 deletions(-) diff --git a/src/api/catalogueCategories.tsx b/src/api/catalogueCategories.tsx index be099495e..e18618f4a 100644 --- a/src/api/catalogueCategories.tsx +++ b/src/api/catalogueCategories.tsx @@ -11,6 +11,7 @@ import { CopyToCatalogueCategory, MoveToCatalogueCategory, TransferState, + type GetQueryOptionsType, } from '../app.types'; import handleTransferState from '../handleTransferState'; @@ -475,7 +476,7 @@ const getCatalogueCategory = async ( export const getCatalogueCategoryQuery = ( id?: string | null, - loader?: boolean + extraOptions?: GetQueryOptionsType ) => queryOptions({ queryKey: ['CatalogueCategory', id], @@ -483,7 +484,7 @@ export const getCatalogueCategoryQuery = ( return getCatalogueCategory(id ?? ''); }, enabled: !!id, - retry: loader ? false : undefined, + ...extraOptions, }); export const useGetCatalogueCategory = ( diff --git a/src/api/catalogueItems.tsx b/src/api/catalogueItems.tsx index 8598ea975..e6a4d12a3 100644 --- a/src/api/catalogueItems.tsx +++ b/src/api/catalogueItems.tsx @@ -8,7 +8,11 @@ import { useQueryClient, } from '@tanstack/react-query'; import { AxiosError } from 'axios'; -import { TransferState, TransferToCatalogueItem } from '../app.types'; +import { + TransferState, + TransferToCatalogueItem, + type GetQueryOptionsType, +} from '../app.types'; import { imsApi } from './api'; import { APIError, @@ -68,7 +72,7 @@ export const useGetCatalogueItems = ( }); }; -export const getCatalogueItem = async ( +const getCatalogueItem = async ( catalogueCategoryId: string | undefined ): Promise => { const queryParams = new URLSearchParams(); @@ -84,7 +88,7 @@ export const getCatalogueItem = async ( export const getCatalogueItemQuery = ( catalogueCategoryId: string | undefined, - loader?: boolean + extraOptions?: GetQueryOptionsType ) => queryOptions({ queryKey: ['CatalogueItem', catalogueCategoryId], @@ -92,7 +96,7 @@ export const getCatalogueItemQuery = ( return getCatalogueItem(catalogueCategoryId); }, enabled: catalogueCategoryId !== undefined, - retry: loader ? false : undefined, + ...extraOptions, }); export const useGetCatalogueItem = ( diff --git a/src/api/items.tsx b/src/api/items.tsx index a774a533f..2dc904877 100644 --- a/src/api/items.tsx +++ b/src/api/items.tsx @@ -7,7 +7,12 @@ import { UseQueryResult, } from '@tanstack/react-query'; import { AxiosError } from 'axios'; -import { MoveItemsToSystem, PostItems, TransferState } from '../app.types'; +import { + MoveItemsToSystem, + PostItems, + TransferState, + type GetQueryOptionsType, +} from '../app.types'; import { imsApi } from './api'; import { APIError, Item, ItemPatch, ItemPost } from './api.types'; @@ -89,7 +94,7 @@ export const usePostItems = (): UseMutationResult< }); }; -export const getItems = async ( +const getItems = async ( system_id?: string, catalogue_item_id?: string ): Promise => { @@ -107,17 +112,25 @@ export const getItems = async ( }); }; -export const useGetItems = ( +export const getItemsQuery = ( system_id?: string, - catalogue_item_id?: string -): UseQueryResult => { - return useQuery({ + catalogue_item_id?: string, + extraOptions?: GetQueryOptionsType +) => + queryOptions({ queryKey: ['Items', system_id, catalogue_item_id], queryFn: () => { return getItems(system_id, catalogue_item_id); }, enabled: system_id !== undefined || catalogue_item_id !== undefined, + ...extraOptions, }); + +export const useGetItems = ( + system_id?: string, + catalogue_item_id?: string +): UseQueryResult => { + return useQuery(getItemsQuery(system_id, catalogue_item_id)); }; const getItem = async (id: string): Promise => { @@ -132,14 +145,17 @@ const getItem = async (id: string): Promise => { }); }; -export const getItemQuery = (id?: string | null, loader?: boolean) => +export const getItemQuery = ( + id?: string | null, + extraOptions?: GetQueryOptionsType +) => queryOptions({ queryKey: ['Item', id], queryFn: () => { return getItem(id ?? ''); }, enabled: !!id, - retry: loader ? false : undefined, + ...extraOptions, }); export const useGetItem = ( diff --git a/src/api/manufacturers.tsx b/src/api/manufacturers.tsx index b862826bc..8765146b2 100644 --- a/src/api/manufacturers.tsx +++ b/src/api/manufacturers.tsx @@ -9,6 +9,7 @@ import { } from '@tanstack/react-query'; import { AxiosError } from 'axios'; +import type { GetQueryOptionsType } from '../app.types'; import { imsApi } from './api'; import { Manufacturer, ManufacturerPatch, ManufacturerPost } from './api.types'; @@ -76,12 +77,15 @@ const getManufacturer = async (id: string): Promise => { .get(`/v1/manufacturers/${id}`) .then((response) => response.data); }; -export const getManufacturerQuery = (id?: string | null, retry?: boolean) => +export const getManufacturerQuery = ( + id?: string | null, + extraOptions?: GetQueryOptionsType +) => queryOptions({ queryKey: ['Manufacturer', id], queryFn: () => getManufacturer(id ?? ''), enabled: !!id, - retry: retry ? false : undefined, + ...extraOptions, }); export const useGetManufacturer = ( diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 5538e800e..26a6ca8a9 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -6,9 +6,15 @@ import { useQueries, useQuery, useQueryClient, + type QueryClient, } from '@tanstack/react-query'; import { AxiosError } from 'axios'; -import { CopyToSystem, MoveToSystem, TransferState } from '../app.types'; +import { + CopyToSystem, + MoveToSystem, + TransferState, + type GetQueryOptionsType, +} from '../app.types'; import { generateUniqueNameUsingCode } from '../utils'; import { imsApi } from './api'; import { @@ -20,8 +26,8 @@ import { SystemPost, type CatalogueItem, } from './api.types'; -import { getCatalogueItem } from './catalogueItems'; -import { getItems } from './items'; +import { getCatalogueItemQuery } from './catalogueItems'; +import { getItemsQuery } from './items'; /** Utility for turning an importance into an MUI palette colour to display */ export const getSystemImportanceColour = ( @@ -56,15 +62,22 @@ export const useGetSystemIds = (ids: string[]): UseQueryResult[] => { }); }; -export const useGetSystems = ( - parent_id?: string -): UseQueryResult => { - return useQuery({ - queryKey: ['Systems', parent_id], +export const getSystemsQuery = ( + id?: string, + extraOptions?: GetQueryOptionsType +) => + queryOptions({ + queryKey: ['Systems', id], queryFn: () => { - return getSystems(parent_id); + return getSystems(id); }, + ...extraOptions, }); + +export const useGetSystems = ( + parent_id?: string +): UseQueryResult => { + return useQuery(getSystemsQuery(parent_id)); }; const getSystem = async (id: string): Promise => { @@ -73,14 +86,17 @@ const getSystem = async (id: string): Promise => { }); }; -export const getSystemQuery = (id?: string | null, loader?: boolean) => +export const getSystemQuery = ( + id?: string | null, + extraOptions?: GetQueryOptionsType +) => queryOptions({ queryKey: ['System', id], queryFn: () => { return getSystem(id ?? ''); }, enabled: !!id, - retry: loader ? false : undefined, + ...extraOptions, }); // Allows a value of undefined or null to disable @@ -95,7 +111,12 @@ export interface SystemTree extends Partial { subsystems?: SystemTree[]; } +const GET_SYSTEM_TREE_QUERY_OPTIONS = { + staleTime: 1000 * 60 * 5, +}; + const getSystemTree = async ( + queryClient: QueryClient, parent_id: string, maxSubsystems: number, // Total max subsystems allowed maxDepth?: number, @@ -110,31 +131,22 @@ const getSystemTree = async ( } // Fetch the root system - const rootSystem = await getSystem(parent_id); + const rootSystem = await queryClient.fetchQuery( + getSystemQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); // Fetch systems at the current level - const systems = await getSystems(parent_id || 'null'); + const systems = await queryClient.fetchQuery( + getSystemsQuery(parent_id ?? 'null', GET_SYSTEM_TREE_QUERY_OPTIONS) + ); // Increment the total count of subsystems totalSubsystems.count += systems.length; // Throw an AxiosError if the total count exceeds maxSubsystems if (maxSubsystems !== undefined && totalSubsystems.count > maxSubsystems) { - throw new AxiosError( - `Total subsystems exceeded the maximum allowed limit of ${maxSubsystems}. Current count: ${totalSubsystems.count}`, - 'SubsystemLimitExceeded', - undefined, - null, - { - status: 400, - statusText: 'Bad Request', - headers: {}, - // @ts-expect-error: not needed - config: {}, - data: { - message: `Subsystem limit exceeded. Max: ${maxSubsystems}, Current: ${totalSubsystems.count}`, - }, - } + throw new Error( + `Total subsystems exceeded the maximum allowed limit of ${maxSubsystems}. Current count: ${totalSubsystems.count}` ); } @@ -155,6 +167,7 @@ const getSystemTree = async ( systems.map(async (system) => { // Fetch subsystems recursively, increasing the depth const subsystems = await getSystemTree( + queryClient, system.id, maxSubsystems, maxDepth, @@ -165,7 +178,9 @@ const getSystemTree = async ( ); // Fetch all items for the current system - const items = await getItems(system.id); + const items = await queryClient.fetchQuery( + getItemsQuery(system.id, undefined, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); // Group items into catalogue categories and fetch catalogue item details const catalogueItemIdSet = new Set( @@ -177,7 +192,9 @@ const getSystemTree = async ( // Check if the item exists in the cache if (!catalogueItemCache.has(id)) { // If not, fetch and store it in the cache - const fetchedCatalogueItem = await getCatalogueItem(id); + const fetchedCatalogueItem = await queryClient.fetchQuery( + getCatalogueItemQuery(id, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); catalogueItemCache.set(id, fetchedCatalogueItem); } @@ -195,7 +212,9 @@ const getSystemTree = async ( ); // Handle the case when there are no systems (leaf nodes or empty levels) - const items = await getItems(parent_id); + const items = await queryClient.fetchQuery( + getItemsQuery(parent_id, undefined, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); // Group items into catalogue categories and fetch catalogue item details const catalogueItemIdSet = new Set( @@ -207,7 +226,9 @@ const getSystemTree = async ( // Check if the item exists in the cache if (!catalogueItemCache.has(id)) { // If not, fetch and store it in the cache - const fetchedCatalogueItem = await getCatalogueItem(id); + const fetchedCatalogueItem = await queryClient.fetchQuery( + getCatalogueItemQuery(id, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); catalogueItemCache.set(id, fetchedCatalogueItem); } @@ -235,6 +256,7 @@ export const useGetSystemsTree = ( subsystemsCutOffPoint?: number, // Add cutoff point as a parameter maxSubsystems?: number ): UseQueryResult => { + const queryClient = useQueryClient(); return useQuery({ queryKey: [ 'SystemsTree', @@ -245,13 +267,14 @@ export const useGetSystemsTree = ( ], queryFn: () => getSystemTree( + queryClient, parent_id ?? '', maxSubsystems ?? 150, maxDepth, 0, subsystemsCutOffPoint ), - staleTime: 1000 * 60 * 5, + ...GET_SYSTEM_TREE_QUERY_OPTIONS, }); }; diff --git a/src/app.types.tsx b/src/app.types.tsx index 64087fc00..5de859754 100644 --- a/src/app.types.tsx +++ b/src/app.types.tsx @@ -1,3 +1,5 @@ +import type { UndefinedInitialDataOptions } from '@tanstack/react-query'; +import type { AxiosError } from 'axios'; import { CatalogueCategory, CatalogueItem, @@ -42,6 +44,15 @@ export interface AllowedValuesList { }; } +export type GetQueryOptionsType = Omit< + UndefinedInitialDataOptions< + T, + AxiosError, + T, + readonly unknown[] + >, + 'queryKey' +>; // ------------------------------------ CATALOGUE CATEGORIES ------------------------------------ export type AllowedValues = AllowedValuesList; diff --git a/src/catalogue/catalogueLayout.component.tsx b/src/catalogue/catalogueLayout.component.tsx index 49fe45a9b..850308129 100644 --- a/src/catalogue/catalogueLayout.component.tsx +++ b/src/catalogue/catalogueLayout.component.tsx @@ -43,12 +43,12 @@ export const catalogueLayoutLoader = if (catalogueCategoryId) { await queryClient.ensureQueryData( - getCatalogueCategoryQuery(catalogueCategoryId, true) + getCatalogueCategoryQuery(catalogueCategoryId, { retry: false }) ); } if (catalogueItemId && catalogueCategoryId) { const catalogueItem = await queryClient.ensureQueryData( - getCatalogueItemQuery(catalogueItemId, true) + getCatalogueItemQuery(catalogueItemId, { retry: false }) ); if (catalogueItem.catalogue_category_id !== catalogueCategoryId) { @@ -59,7 +59,7 @@ export const catalogueLayoutLoader = } if (catalogueItemId && catalogueCategoryId && itemId) { const item = await queryClient.ensureQueryData( - getItemQuery(itemId, true) + getItemQuery(itemId, { retry: false }) ); if (item.catalogue_item_id !== catalogueItemId) { throw new Error( diff --git a/src/manufacturer/manufacturerLayout.component.tsx b/src/manufacturer/manufacturerLayout.component.tsx index 12daad5db..ac8ef0dec 100644 --- a/src/manufacturer/manufacturerLayout.component.tsx +++ b/src/manufacturer/manufacturerLayout.component.tsx @@ -26,7 +26,7 @@ export const manufacturerLayoutLoader = if (manufacturerId) { await queryClient.ensureQueryData( - getManufacturerQuery(manufacturerId, true) + getManufacturerQuery(manufacturerId, { retry: false }) ); } diff --git a/src/systems/systemsLayout.component.tsx b/src/systems/systemsLayout.component.tsx index 2e82344f6..e0c7803be 100644 --- a/src/systems/systemsLayout.component.tsx +++ b/src/systems/systemsLayout.component.tsx @@ -31,7 +31,9 @@ export const systemsLayoutLoader = const { system_id: systemId } = params; if (systemId) { - await queryClient.ensureQueryData(getSystemQuery(systemId, true)); + await queryClient.ensureQueryData( + getSystemQuery(systemId, { retry: false }) + ); } return { ...params }; diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx index 57b055759..24c6c2006 100644 --- a/src/systems/systemsTree.component.tsx +++ b/src/systems/systemsTree.component.tsx @@ -31,15 +31,22 @@ import { import '@xyflow/react/dist/style.css'; import type { NodeLookup } from '@xyflow/system'; import React from 'react'; -import { Link, useParams } from 'react-router-dom'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; import { useGetSystemsTree, type SystemTree } from '../api/systems'; import { getPageHeightCalc } from '../utils'; import SystemsNodeHeader from './systemsNodeHeader.component'; +type LayoutDirectionType = 'TB' | 'LR'; +type MaxDepthType = -1 | 1 | 2 | 3; +const DEFAULT_LAYOUT_DIRECTION: LayoutDirectionType = 'TB'; +const DEFAULT_MAX_DEPTH: MaxDepthType = 1; +const LAYOUT_DIRECTION_STATE = 'layoutDirection'; +const MAX_DEPTH_STATE = 'maxDepth'; + interface SystemFlowProps { rawEdges: Edge[]; rawNodes: Node[]; - layoutDirection: 'TB' | 'LR'; + layoutDirection: LayoutDirectionType; } const nodeWidth = 300; @@ -62,7 +69,7 @@ const getLayoutedElements = ( dagreGraph.setGraph({ rankdir: direction, - nodesep: 50, + nodesep: isHorizontal ? calculateRanksep(nodes) : 50, ranksep: isHorizontal ? 150 : calculateRanksep(nodes), }); @@ -102,9 +109,7 @@ const getLayoutedElements = ( position: { x: nodeWithPosition.x - nodeWithPosition.width / 2, y: isHorizontal - ? hasMoreThanOneChild - ? nodeWithPosition.y - : nodeWithPosition.y - nodeWithPosition.height / 2 + ? nodeWithPosition.y - nodeWithPosition.height / 2 : hasMoreThanOneChild ? nodeWithPosition.y : nodeWithPosition.y - nodeWithPosition.height / 2, @@ -160,7 +165,7 @@ const SystemsFlow = (props: SystemFlowProps) => { React.useEffect(() => { window.requestAnimationFrame(() => fitView()); - }, [fitView, layoutDirection, nodes]); + }, [fitView, nodes]); return ( { const SystemsTree = () => { const { system_id: systemId } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); - const [layoutDirection, setLayoutDirection] = React.useState<'TB' | 'LR'>( - 'TB' + const layoutDirection = React.useMemo( + () => + (searchParams.get(LAYOUT_DIRECTION_STATE) as LayoutDirectionType) || + DEFAULT_LAYOUT_DIRECTION, + [searchParams] ); - const [maxDepth, setMaxDepth] = React.useState<-1 | 1 | 2 | 3>(1); - - const maxSubsystems = 150; - const subsystemsCutOff = 100; + const maxDepth = React.useMemo( + () => + (Number(searchParams.get(MAX_DEPTH_STATE)) || + DEFAULT_MAX_DEPTH) as MaxDepthType, + [searchParams] + ); + const maxSubsystems = 100; + const subsystemsCutOff = 75; const { data: systemsTree, @@ -285,27 +298,38 @@ const SystemsTree = () => { [layoutDirection, systemIndex] ); - const handleToggleLayout = ( - _event: React.MouseEvent, - newDirection: 'TB' | 'LR' - ) => { - if (newDirection !== null) { - setLayoutDirection(newDirection); - } - }; - - const handleToggleMaxDepth = ( - _event: React.MouseEvent, - newDepth: -1 | 1 | 2 | 3 - ) => { - if (newDepth !== null) { - setMaxDepth(newDepth); - } - }; + const handleToggleLayout = React.useCallback( + ( + _event: React.MouseEvent, + newDirection: LayoutDirectionType + ) => { + if (newDirection !== null) { + if (newDirection === DEFAULT_LAYOUT_DIRECTION) { + searchParams.delete(LAYOUT_DIRECTION_STATE); + } else { + searchParams.set(LAYOUT_DIRECTION_STATE, newDirection); + } + setSearchParams(searchParams, { replace: false }); + } + }, + [searchParams, setSearchParams] + ); + + const handleToggleMaxDepth = React.useCallback( + (_event: React.MouseEvent, newDepth: MaxDepthType) => { + if (newDepth !== null) { + if (newDepth === DEFAULT_MAX_DEPTH) { + searchParams.delete(MAX_DEPTH_STATE); + } else { + searchParams.set(MAX_DEPTH_STATE, newDepth.toString()); + } + setSearchParams(searchParams, { replace: false }); + } + }, + [searchParams, setSearchParams] + ); const isLimitedReached = - (error?.response?.data as { message?: string })?.message?.includes( - 'Subsystem limit exceeded' - ) ?? false; + error?.message?.includes('exceeded the maximum allowed limit') ?? false; const { nodes: rawNodes, edges: rawEdges } = transformToFlowData( systemsTree ?? [], @@ -409,6 +433,8 @@ const SystemsTree = () => { ) : ( Date: Fri, 14 Feb 2025 15:13:25 +0000 Subject: [PATCH 06/14] add action menu --- src/systems/systemsLayout.component.tsx | 27 +++++--- src/systems/systemsNodeHeader.component.tsx | 70 ++++++++++++++++++--- src/systems/systemsTree.component.tsx | 25 ++++---- 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/src/systems/systemsLayout.component.tsx b/src/systems/systemsLayout.component.tsx index e0c7803be..53387cf40 100644 --- a/src/systems/systemsLayout.component.tsx +++ b/src/systems/systemsLayout.component.tsx @@ -1,7 +1,8 @@ import AccountTreeIcon from '@mui/icons-material/AccountTree'; import ViewModuleIcon from '@mui/icons-material/ViewModule'; -import { ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material'; import type { QueryClient } from '@tanstack/react-query'; +import React from 'react'; import { Outlet, useLocation, @@ -82,16 +83,22 @@ function SystemsLayout() { onChange={handleViewChange} aria-label="view mode toggle" size="small" - sx={{ margin: 1 }} + sx={{ ml: 1, mb: 1 }} > - - - Normal View - - - - Tree View - + + + + + + + + + + + + + + )} diff --git a/src/systems/systemsNodeHeader.component.tsx b/src/systems/systemsNodeHeader.component.tsx index a255e869b..0b767b6b5 100644 --- a/src/systems/systemsNodeHeader.component.tsx +++ b/src/systems/systemsNodeHeader.component.tsx @@ -1,7 +1,18 @@ -import { MoreHoriz } from '@mui/icons-material'; -import { Card, Divider, Grid, IconButton } from '@mui/material'; +import MoreHoriz from '@mui/icons-material/MoreHoriz'; +import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import { + Card, + Divider, + Grid, + IconButton, + Menu, + MenuItem, + Tooltip, +} from '@mui/material'; import { Handle, Position } from '@xyflow/react'; import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { SystemTree } from '../api/systems'; import { OverflowTip } from '../utils'; interface SystemsNodeHeaderProps { @@ -9,14 +20,26 @@ interface SystemsNodeHeaderProps { title: string | React.ReactNode; label: string | React.ReactNode; direction?: 'TB' | 'LR'; - id: string; + system: SystemTree; }; } const SystemsNodeHeader = (props: SystemsNodeHeaderProps) => { const { data } = props; - + const navigate = useNavigate(); const isHorizontal = data.direction === 'LR'; + + const [anchorEl, setAnchorEl] = React.useState(null); + const menuOpen = Boolean(anchorEl); + + const handleMenuClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + return ( <> { sx={{ display: 'flex', alignItems: 'center', margin: 1 }} xs={2} > - {/* Actions Menu */} - - - + + + + + + + + + + { + navigate(`/systems/${data.system.id}`); + handleMenuClose(); + }} + > + + Navigate to system page + + @@ -74,12 +124,12 @@ const SystemsNodeHeader = (props: SystemsNodeHeaderProps) => { ); diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx index 24c6c2006..eca0d30c0 100644 --- a/src/systems/systemsTree.component.tsx +++ b/src/systems/systemsTree.component.tsx @@ -42,6 +42,8 @@ const DEFAULT_LAYOUT_DIRECTION: LayoutDirectionType = 'TB'; const DEFAULT_MAX_DEPTH: MaxDepthType = 1; const LAYOUT_DIRECTION_STATE = 'layoutDirection'; const MAX_DEPTH_STATE = 'maxDepth'; +const MAX_SUBSYSTEMS = 100; +const SUBSYSTEMS_CUT_OFF = 75; interface SystemFlowProps { rawEdges: Edge[]; @@ -201,8 +203,6 @@ const SystemsTree = () => { DEFAULT_MAX_DEPTH) as MaxDepthType, [searchParams] ); - const maxSubsystems = 100; - const subsystemsCutOff = 75; const { data: systemsTree, @@ -211,8 +211,8 @@ const SystemsTree = () => { } = useGetSystemsTree( systemId, maxDepth === -1 ? undefined : maxDepth, - subsystemsCutOff, - maxSubsystems + SUBSYSTEMS_CUT_OFF, + MAX_SUBSYSTEMS ); let systemIndex = 0; const transformToFlowData = React.useCallback( @@ -229,14 +229,11 @@ const SystemsTree = () => { type: 'systems', style: { width: '300px' }, data: { - title: - systemIndex === 0 ? ( - system.name - ) : ( - - {system.name} - - ), + title: ( + + {system.name} + + ), label: ( {/* Items Heading */} @@ -261,8 +258,8 @@ const SystemsTree = () => { )} ), + system: system, direction: layoutDirection, - id: system.id ?? '', }, // position will be set by dagre position: { x: 0, y: 0 }, @@ -369,7 +366,7 @@ const SystemsTree = () => { title={ The larger the depth, the longer the query may take. If the - number of subsystems exceeds {maxSubsystems}, the tree will not + number of subsystems exceeds {MAX_SUBSYSTEMS}, the tree will not load. } From d10765631781a2f69811c50268925c053f72d35d Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 3 Mar 2025 22:20:14 +0000 Subject: [PATCH 07/14] Allow treeview at root --- src/App.tsx | 4 ++ src/api/systems.tsx | 26 +++++++-- src/common/baseLayoutHeader.component.tsx | 13 +++-- src/systems/systemsLayout.component.tsx | 59 +++++++++------------ src/systems/systemsNodeHeader.component.tsx | 6 ++- src/systems/systemsTree.component.tsx | 56 ++++++++++--------- 6 files changed, 97 insertions(+), 67 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 74f1101e4..829bb3ecb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -219,6 +219,10 @@ const routeObject: RouteObject[] = [ Component: Systems, loader: systemsLayoutLoader(queryClient), }, + { + path: paths.systemRootTree, + Component: SystemsTree, + }, { path: paths.systemTree, Component: SystemsTree, diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 26a6ca8a9..7051945d6 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -131,13 +131,28 @@ const getSystemTree = async ( } // Fetch the root system - const rootSystem = await queryClient.fetchQuery( - getSystemQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) - ); + + let rootSystem: System = { + name: 'Root', + code: 'root', + id: 'root', + created_time: '', + modified_time: '', + description: null, + location: null, + owner: null, + importance: SystemImportanceType.LOW, + parent_id: null, + }; + + if (parent_id !== 'null') + rootSystem = await queryClient.fetchQuery( + getSystemQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); // Fetch systems at the current level const systems = await queryClient.fetchQuery( - getSystemsQuery(parent_id ?? 'null', GET_SYSTEM_TREE_QUERY_OPTIONS) + getSystemsQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) ); // Increment the total count of subsystems @@ -257,6 +272,7 @@ export const useGetSystemsTree = ( maxSubsystems?: number ): UseQueryResult => { const queryClient = useQueryClient(); + return useQuery({ queryKey: [ 'SystemsTree', @@ -268,7 +284,7 @@ export const useGetSystemsTree = ( queryFn: () => getSystemTree( queryClient, - parent_id ?? '', + parent_id ?? 'null', maxSubsystems ?? 150, maxDepth, 0, diff --git a/src/common/baseLayoutHeader.component.tsx b/src/common/baseLayoutHeader.component.tsx index 4617ec91e..2574200c5 100644 --- a/src/common/baseLayoutHeader.component.tsx +++ b/src/common/baseLayoutHeader.component.tsx @@ -1,6 +1,6 @@ import { Box, Grid } from '@mui/material'; import React from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import type { BreadcrumbsInfo } from '../api/api.types'; import { RoutesHomeLocation, type RoutesHomeLocationType } from '../app.types'; import Breadcrumbs from '../view/breadcrumbs.component'; @@ -14,12 +14,19 @@ export interface BaseLayoutHeaderProps { function BaseLayoutHeader(props: BaseLayoutHeaderProps) { const { breadcrumbsInfo, children, homeLocation } = props; const navigate = useNavigate(); + const location = useLocation(); + + // Check if we are in Tree View or Normal View by looking for '/tree' in the URL + const isTreeView = + homeLocation === 'Systems' && location.pathname.includes('tree'); const onChangeNode = React.useCallback( (id: string | null) => { - navigate(`/${RoutesHomeLocation[homeLocation]}${id ? `/${id}` : ''}`); + navigate( + `/${RoutesHomeLocation[homeLocation]}${id ? `/${id}` : ''}${isTreeView ? '/tree' : ''}` + ); }, - [homeLocation, navigate] + [homeLocation, isTreeView, navigate] ); return ( diff --git a/src/systems/systemsLayout.component.tsx b/src/systems/systemsLayout.component.tsx index 53387cf40..bcc05c311 100644 --- a/src/systems/systemsLayout.component.tsx +++ b/src/systems/systemsLayout.component.tsx @@ -66,41 +66,32 @@ function SystemsLayout() { return ( [ - isTreeView ? id + '/tree' : id, - name, - ]) ?? [], - }} + breadcrumbsInfo={systemsBreadcrumbs} > - {location.pathname !== '/systems' && ( - - - - - - - - - - - - - - - - - )} + + + + + + + + + + + + + + + + + ); diff --git a/src/systems/systemsNodeHeader.component.tsx b/src/systems/systemsNodeHeader.component.tsx index 0b767b6b5..a82ffab52 100644 --- a/src/systems/systemsNodeHeader.component.tsx +++ b/src/systems/systemsNodeHeader.component.tsx @@ -99,7 +99,11 @@ const SystemsNodeHeader = (props: SystemsNodeHeaderProps) => { > { - navigate(`/systems/${data.system.id}`); + navigate( + data.system.id !== 'root' + ? `/systems/${data.system.id}` + : '/systems' + ); handleMenuClose(); }} > diff --git a/src/systems/systemsTree.component.tsx b/src/systems/systemsTree.component.tsx index eca0d30c0..146becc7a 100644 --- a/src/systems/systemsTree.component.tsx +++ b/src/systems/systemsTree.component.tsx @@ -230,34 +230,42 @@ const SystemsTree = () => { style: { width: '300px' }, data: { title: ( - + {system.name} ), - label: ( - - {/* Items Heading */} - - Items: - - {/* List of Catalogue Items */} - {system.catalogueItems.length > 0 ? ( - system.catalogueItems.map((catalogueItem) => ( - - {catalogueItem.name}: {catalogueItem.itemsQuantity} - - )) - ) : ( - - No Items + label: + system.id === 'root' ? undefined : ( + + {/* Items Heading */} + + Items: - )} - - ), + {/* List of Catalogue Items */} + {system.catalogueItems.length > 0 ? ( + system.catalogueItems.map((catalogueItem) => ( + + {catalogueItem.name}: {catalogueItem.itemsQuantity} + + )) + ) : ( + + No Items + + )} + + ), system: system, direction: layoutDirection, }, From 5d396b7b97d637fc9f9117097ceb74f9164f7bf1 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Tue, 4 Mar 2025 16:24:55 +0000 Subject: [PATCH 08/14] Improve the systemTree fetch --- src/api/systems.tsx | 189 +++++++++++++++++++++----------------------- 1 file changed, 89 insertions(+), 100 deletions(-) diff --git a/src/api/systems.tsx b/src/api/systems.tsx index 7051945d6..8d846227f 100644 --- a/src/api/systems.tsx +++ b/src/api/systems.tsx @@ -118,54 +118,62 @@ const GET_SYSTEM_TREE_QUERY_OPTIONS = { const getSystemTree = async ( queryClient: QueryClient, parent_id: string, - maxSubsystems: number, // Total max subsystems allowed + maxSubsystems: number, maxDepth?: number, - currentDepth: number = 0, // Default value - subsystemsCutOffPoint?: number, // Total cutoff point - totalSubsystems: { count: number } = { count: 0 }, // Shared counter object - catalogueItemCache: Map = new Map() // Shared cache for catalogue items + currentDepth: number = 0, + subsystemsCutOffPoint?: number, + totalSubsystems: { count: number } = { count: 0 }, + catalogueItemCache: Map = new Map(), + systemsCache: Map = new Map() // Cache for systems ): Promise => { - // Stop recursion if currentDepth exceeds maxDepth - if (maxDepth !== undefined && currentDepth >= maxDepth) { - return []; + if (maxDepth !== undefined && currentDepth >= maxDepth) return []; + + // Determine the root system from cache + let rootSystem: System; + + if (parent_id === 'null') { + rootSystem = { + name: 'Root', + code: 'root', + id: 'root', + created_time: '', + modified_time: '', + description: null, + location: null, + owner: null, + importance: SystemImportanceType.LOW, + parent_id: null, + }; + } else { + // Check cache for the parent system + if (!systemsCache.has(parent_id)) { + const fetchedSystem = await queryClient.fetchQuery( + getSystemQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) + ); + systemsCache.set(parent_id, fetchedSystem); // Cache only the parent system + } + rootSystem = systemsCache.get(parent_id)!; } - // Fetch the root system - - let rootSystem: System = { - name: 'Root', - code: 'root', - id: 'root', - created_time: '', - modified_time: '', - description: null, - location: null, - owner: null, - importance: SystemImportanceType.LOW, - parent_id: null, - }; - - if (parent_id !== 'null') - rootSystem = await queryClient.fetchQuery( - getSystemQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) - ); - // Fetch systems at the current level const systems = await queryClient.fetchQuery( getSystemsQuery(parent_id, GET_SYSTEM_TREE_QUERY_OPTIONS) ); - // Increment the total count of subsystems - totalSubsystems.count += systems.length; + // Store all fetched systems in cache + systems.forEach((system) => { + if (!systemsCache.has(system.id)) { + systemsCache.set(system.id, system); + } + }); - // Throw an AxiosError if the total count exceeds maxSubsystems + totalSubsystems.count += systems.length; if (maxSubsystems !== undefined && totalSubsystems.count > maxSubsystems) { throw new Error( `Total subsystems exceeded the maximum allowed limit of ${maxSubsystems}. Current count: ${totalSubsystems.count}` ); } - // Stop recursion if totalSubsystems count exceeds the cutoff point if ( subsystemsCutOffPoint !== undefined && totalSubsystems.count > subsystemsCutOffPoint @@ -177,11 +185,11 @@ const getSystemTree = async ( })); } - // Fetch subsystems and catalogue items for each system recursively - const systemsWithTree: SystemTree[] = await Promise.all( - systems.map(async (system) => { - // Fetch subsystems recursively, increasing the depth - const subsystems = await getSystemTree( + // Fetch subsystems recursively + const systemsWithTree = await Promise.all( + systems.map(async (system) => ({ + ...system, + subsystems: await getSystemTree( queryClient, system.id, maxSubsystems, @@ -189,80 +197,61 @@ const getSystemTree = async ( currentDepth + 1, subsystemsCutOffPoint, totalSubsystems, + catalogueItemCache, + systemsCache + ), + catalogueItems: await fetchCatalogueItems( + queryClient, + system.id, catalogueItemCache - ); - - // Fetch all items for the current system - const items = await queryClient.fetchQuery( - getItemsQuery(system.id, undefined, GET_SYSTEM_TREE_QUERY_OPTIONS) - ); - - // Group items into catalogue categories and fetch catalogue item details - const catalogueItemIdSet = new Set( - items.map((item) => item.catalogue_item_id) - ); - - const catalogueItems: SystemTree['catalogueItems'] = await Promise.all( - Array.from(catalogueItemIdSet).map(async (id) => { - // Check if the item exists in the cache - if (!catalogueItemCache.has(id)) { - // If not, fetch and store it in the cache - const fetchedCatalogueItem = await queryClient.fetchQuery( - getCatalogueItemQuery(id, GET_SYSTEM_TREE_QUERY_OPTIONS) - ); - catalogueItemCache.set(id, fetchedCatalogueItem); - } - - // Retrieve the item from the cache - const catalogueItem = catalogueItemCache.get(id)!; - const categoryItems = items.filter( - (item) => item.catalogue_item_id === id - ); - return { ...catalogueItem, itemsQuantity: categoryItems.length }; - }) - ); - - return { ...system, subsystems, catalogueItems }; - }) + ), + })) ); - // Handle the case when there are no systems (leaf nodes or empty levels) + return [ + { + ...rootSystem, + catalogueItems: await fetchCatalogueItems( + queryClient, + parent_id, + catalogueItemCache + ), + subsystems: systemsWithTree, + }, + ]; +}; + +// Helper function for fetching and caching catalogue items +const fetchCatalogueItems = async ( + queryClient: QueryClient, + systemId: string, + catalogueItemCache: Map +): Promise => { const items = await queryClient.fetchQuery( - getItemsQuery(parent_id, undefined, GET_SYSTEM_TREE_QUERY_OPTIONS) + getItemsQuery(systemId, undefined, GET_SYSTEM_TREE_QUERY_OPTIONS) ); - // Group items into catalogue categories and fetch catalogue item details - const catalogueItemIdSet = new Set( + const catalogueItems: SystemTree['catalogueItems'] = []; + const catalogueItemIdSet = new Set( items.map((item) => item.catalogue_item_id) ); - const catalogueItems: SystemTree['catalogueItems'] = await Promise.all( - Array.from(catalogueItemIdSet).map(async (id) => { - // Check if the item exists in the cache - if (!catalogueItemCache.has(id)) { - // If not, fetch and store it in the cache - const fetchedCatalogueItem = await queryClient.fetchQuery( + for (const id of Array.from(catalogueItemIdSet)) { + catalogueItemCache.set( + id, + catalogueItemCache.get(id) || + (await queryClient.fetchQuery( getCatalogueItemQuery(id, GET_SYSTEM_TREE_QUERY_OPTIONS) - ); - catalogueItemCache.set(id, fetchedCatalogueItem); - } - - // Retrieve the item from the cache - const catalogueItem = catalogueItemCache.get(id)!; - const categoryItems = items.filter( - (item) => item.catalogue_item_id === id - ); - return { ...catalogueItem, itemsQuantity: categoryItems.length }; - }) - ); + )) + ); + catalogueItems.push({ + ...catalogueItemCache.get(id)!, + itemsQuantity: items.filter((item) => item.catalogue_item_id === id) + .length, + }); + } - return [ - { - ...rootSystem, - catalogueItems, - subsystems: systemsWithTree, - }, - ]; + return catalogueItems; }; export const useGetSystemsTree = ( From 93676dd69d9663c6c1ccaa2baa026e9f807a5e50 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 6 Mar 2025 15:22:21 +0000 Subject: [PATCH 09/14] update snapshots --- .../systemsLayout.component.test.tsx.snap | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/systems/__snapshots__/systemsLayout.component.test.tsx.snap b/src/systems/__snapshots__/systemsLayout.component.test.tsx.snap index 677e593f8..eca92bd05 100644 --- a/src/systems/__snapshots__/systemsLayout.component.test.tsx.snap +++ b/src/systems/__snapshots__/systemsLayout.component.test.tsx.snap @@ -48,6 +48,70 @@ exports[`Systems Layout > renders root systems correctly 1`] = ` +
+ + + + + + +
`; @@ -136,6 +200,70 @@ exports[`Systems Layout > renders units breadcrumbs correctly 1`] = ` +
+ + + + + + +
`; From 58e10054f00f915546aefc60fc965ec8ad4fe030 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Thu, 6 Mar 2025 15:24:46 +0000 Subject: [PATCH 10/14] remove unnessary packages --- package.json | 3 - yarn.lock | 244 +-------------------------------------------------- 2 files changed, 3 insertions(+), 244 deletions(-) diff --git a/package.json b/package.json index 9735b69bc..2f2b8ce30 100644 --- a/package.json +++ b/package.json @@ -88,9 +88,6 @@ "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", - "@types/d3": "^7", - "@types/d3-hierarchy": "^3", - "@types/dagre": "^0", "@types/eslint-plugin-jsx-a11y": "6.10.0", "@types/eslint__js": "8.42.3", "@types/react-router-dom": "5.3.3", diff --git a/yarn.lock b/yarn.lock index cbd0fff4d..21fb303ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1928,38 +1928,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-array@npm:*": - version: 3.2.1 - resolution: "@types/d3-array@npm:3.2.1" - checksum: 10c0/38bf2c778451f4b79ec81a2288cb4312fe3d6449ecdf562970cc339b60f280f31c93a024c7ff512607795e79d3beb0cbda123bb07010167bce32927f71364bca - languageName: node - linkType: hard - -"@types/d3-axis@npm:*": - version: 3.0.6 - resolution: "@types/d3-axis@npm:3.0.6" - dependencies: - "@types/d3-selection": "npm:*" - checksum: 10c0/d756d42360261f44d8eefd0950c5bb0a4f67a46dd92069da3f723ac36a1e8cb2b9ce6347d836ef19d5b8aef725dbcf8fdbbd6cfbff676ca4b0642df2f78b599a - languageName: node - linkType: hard - -"@types/d3-brush@npm:*": - version: 3.0.6 - resolution: "@types/d3-brush@npm:3.0.6" - dependencies: - "@types/d3-selection": "npm:*" - checksum: 10c0/fd6e2ac7657a354f269f6b9c58451ffae9d01b89ccb1eb6367fd36d635d2f1990967215ab498e0c0679ff269429c57fad6a2958b68f4d45bc9f81d81672edc01 - languageName: node - linkType: hard - -"@types/d3-chord@npm:*": - version: 3.0.6 - resolution: "@types/d3-chord@npm:3.0.6" - checksum: 10c0/c5a25eb5389db01e63faec0c5c2ec7cc41c494e9b3201630b494c4e862a60f1aa83fabbc33a829e7e1403941e3c30d206c741559b14406ac2a4239cfdf4b4c17 - languageName: node - linkType: hard - "@types/d3-color@npm:*": version: 3.1.3 resolution: "@types/d3-color@npm:3.1.3" @@ -1967,31 +1935,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-contour@npm:*": - version: 3.0.6 - resolution: "@types/d3-contour@npm:3.0.6" - dependencies: - "@types/d3-array": "npm:*" - "@types/geojson": "npm:*" - checksum: 10c0/e7d83e94719af4576ceb5ac7f277c5806f83ba6c3631744ae391cffc3641f09dfa279470b83053cd0b2acd6784e8749c71141d05bdffa63ca58ffb5b31a0f27c - languageName: node - linkType: hard - -"@types/d3-delaunay@npm:*": - version: 6.0.4 - resolution: "@types/d3-delaunay@npm:6.0.4" - checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 - languageName: node - linkType: hard - -"@types/d3-dispatch@npm:*": - version: 3.0.6 - resolution: "@types/d3-dispatch@npm:3.0.6" - checksum: 10c0/405eb7d0ec139fbf72fa6a43b0f3ca8a1f913bb2cb38f607827e63fca8d4393f021f32f3b96b33c93ddbd37789453a0b3624f14f504add5308fd9aec8a46dda0 - languageName: node - linkType: hard - -"@types/d3-drag@npm:*, @types/d3-drag@npm:^3.0.7": +"@types/d3-drag@npm:^3.0.7": version: 3.0.7 resolution: "@types/d3-drag@npm:3.0.7" dependencies: @@ -2000,59 +1944,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-dsv@npm:*": - version: 3.0.7 - resolution: "@types/d3-dsv@npm:3.0.7" - checksum: 10c0/c0f01da862465594c8a28278b51c850af3b4239cc22b14fd1a19d7a98f93d94efa477bf59d8071beb285dca45bf614630811451e18e7c52add3a0abfee0a1871 - languageName: node - linkType: hard - -"@types/d3-ease@npm:*": - version: 3.0.2 - resolution: "@types/d3-ease@npm:3.0.2" - checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c - languageName: node - linkType: hard - -"@types/d3-fetch@npm:*": - version: 3.0.7 - resolution: "@types/d3-fetch@npm:3.0.7" - dependencies: - "@types/d3-dsv": "npm:*" - checksum: 10c0/3d147efa52a26da1a5d40d4d73e6cebaaa964463c378068062999b93ea3731b27cc429104c21ecbba98c6090e58ef13429db6399238c5e3500162fb3015697a0 - languageName: node - linkType: hard - -"@types/d3-force@npm:*": - version: 3.0.10 - resolution: "@types/d3-force@npm:3.0.10" - checksum: 10c0/c82b459079a106b50e346c9b79b141f599f2fc4f598985a5211e72c7a2e20d35bd5dc6e91f306b323c8bfa325c02c629b1645f5243f1c6a55bd51bc85cccfa92 - languageName: node - linkType: hard - -"@types/d3-format@npm:*": - version: 3.0.4 - resolution: "@types/d3-format@npm:3.0.4" - checksum: 10c0/3ac1600bf9061a59a228998f7cd3f29e85cbf522997671ba18d4d84d10a2a1aff4f95aceb143fa9960501c3ec351e113fc75884e6a504ace44dc1744083035ee - languageName: node - linkType: hard - -"@types/d3-geo@npm:*": - version: 3.1.0 - resolution: "@types/d3-geo@npm:3.1.0" - dependencies: - "@types/geojson": "npm:*" - checksum: 10c0/3745a93439038bb5b0b38facf435f7079812921d46406f5d38deaee59e90084ff742443c7ea0a8446df81a0d81eaf622fe7068cf4117a544bd4aa3b2dc182f88 - languageName: node - linkType: hard - -"@types/d3-hierarchy@npm:*, @types/d3-hierarchy@npm:^3": - version: 3.1.7 - resolution: "@types/d3-hierarchy@npm:3.1.7" - checksum: 10c0/873711737d6b8e7b6f1dda0bcd21294a48f75024909ae510c5d2c21fad2e72032e0958def4d9f68319d3aaac298ad09c49807f8bfc87a145a82693b5208613c7 - languageName: node - linkType: hard - "@types/d3-interpolate@npm:*": version: 3.0.4 resolution: "@types/d3-interpolate@npm:3.0.4" @@ -2062,50 +1953,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-path@npm:*": - version: 3.1.0 - resolution: "@types/d3-path@npm:3.1.0" - checksum: 10c0/85e8b3aa968a60a5b33198ade06ae7ffedcf9a22d86f24859ff58e014b053ccb7141ec163b78d547bc8215bb12bb54171c666057ab6156912814005b686afb31 - languageName: node - linkType: hard - -"@types/d3-polygon@npm:*": - version: 3.0.2 - resolution: "@types/d3-polygon@npm:3.0.2" - checksum: 10c0/f46307bb32b6c2aef8c7624500e0f9b518de8f227ccc10170b869dc43e4c542560f6c8d62e9f087fac45e198d6e4b623e579c0422e34c85baf56717456d3f439 - languageName: node - linkType: hard - -"@types/d3-quadtree@npm:*": - version: 3.0.6 - resolution: "@types/d3-quadtree@npm:3.0.6" - checksum: 10c0/7eaa0a4d404adc856971c9285e1c4ab17e9135ea669d847d6db7e0066126a28ac751864e7ce99c65d526e130f56754a2e437a1617877098b3bdcc3ef23a23616 - languageName: node - linkType: hard - -"@types/d3-random@npm:*": - version: 3.0.3 - resolution: "@types/d3-random@npm:3.0.3" - checksum: 10c0/5f4fea40080cd6d4adfee05183d00374e73a10c530276a6455348983dda341003a251def28565a27c25d9cf5296a33e870e397c9d91ff83fb7495a21c96b6882 - languageName: node - linkType: hard - -"@types/d3-scale-chromatic@npm:*": - version: 3.1.0 - resolution: "@types/d3-scale-chromatic@npm:3.1.0" - checksum: 10c0/93c564e02d2e97a048e18fe8054e4a935335da6ab75a56c3df197beaa87e69122eef0dfbeb7794d4a444a00e52e3123514ee27cec084bd21f6425b7037828cc2 - languageName: node - linkType: hard - -"@types/d3-scale@npm:*": - version: 4.0.8 - resolution: "@types/d3-scale@npm:4.0.8" - dependencies: - "@types/d3-time": "npm:*" - checksum: 10c0/57de90e4016f640b83cb960b7e3a0ab3ed02e720898840ddc5105264ffcfea73336161442fdc91895377c2d2f91904d637282f16852b8535b77e15a761c8e99e - languageName: node - linkType: hard - "@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10": version: 3.0.11 resolution: "@types/d3-selection@npm:3.0.11" @@ -2113,37 +1960,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-shape@npm:*": - version: 3.1.7 - resolution: "@types/d3-shape@npm:3.1.7" - dependencies: - "@types/d3-path": "npm:*" - checksum: 10c0/38e59771c1c4c83b67aa1f941ce350410522a149d2175832fdc06396b2bb3b2c1a2dd549e0f8230f9f24296ee5641a515eaf10f55ee1ef6c4f83749e2dd7dcfd - languageName: node - linkType: hard - -"@types/d3-time-format@npm:*": - version: 4.0.3 - resolution: "@types/d3-time-format@npm:4.0.3" - checksum: 10c0/9ef5e8e2b96b94799b821eed5d61a3d432c7903247966d8ad951b8ce5797fe46554b425cb7888fa5bf604b4663c369d7628c0328ffe80892156671c58d1a7f90 - languageName: node - linkType: hard - -"@types/d3-time@npm:*": - version: 3.0.4 - resolution: "@types/d3-time@npm:3.0.4" - checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174 - languageName: node - linkType: hard - -"@types/d3-timer@npm:*": - version: 3.0.2 - resolution: "@types/d3-timer@npm:3.0.2" - checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1 - languageName: node - linkType: hard - -"@types/d3-transition@npm:*, @types/d3-transition@npm:^3.0.8": +"@types/d3-transition@npm:^3.0.8": version: 3.0.9 resolution: "@types/d3-transition@npm:3.0.9" dependencies: @@ -2152,7 +1969,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-zoom@npm:*, @types/d3-zoom@npm:^3.0.8": +"@types/d3-zoom@npm:^3.0.8": version: 3.0.8 resolution: "@types/d3-zoom@npm:3.0.8" dependencies: @@ -2162,51 +1979,6 @@ __metadata: languageName: node linkType: hard -"@types/d3@npm:^7": - version: 7.4.3 - resolution: "@types/d3@npm:7.4.3" - dependencies: - "@types/d3-array": "npm:*" - "@types/d3-axis": "npm:*" - "@types/d3-brush": "npm:*" - "@types/d3-chord": "npm:*" - "@types/d3-color": "npm:*" - "@types/d3-contour": "npm:*" - "@types/d3-delaunay": "npm:*" - "@types/d3-dispatch": "npm:*" - "@types/d3-drag": "npm:*" - "@types/d3-dsv": "npm:*" - "@types/d3-ease": "npm:*" - "@types/d3-fetch": "npm:*" - "@types/d3-force": "npm:*" - "@types/d3-format": "npm:*" - "@types/d3-geo": "npm:*" - "@types/d3-hierarchy": "npm:*" - "@types/d3-interpolate": "npm:*" - "@types/d3-path": "npm:*" - "@types/d3-polygon": "npm:*" - "@types/d3-quadtree": "npm:*" - "@types/d3-random": "npm:*" - "@types/d3-scale": "npm:*" - "@types/d3-scale-chromatic": "npm:*" - "@types/d3-selection": "npm:*" - "@types/d3-shape": "npm:*" - "@types/d3-time": "npm:*" - "@types/d3-time-format": "npm:*" - "@types/d3-timer": "npm:*" - "@types/d3-transition": "npm:*" - "@types/d3-zoom": "npm:*" - checksum: 10c0/a9c6d65b13ef3b42c87f2a89ea63a6d5640221869f97d0657b0cb2f1dac96a0f164bf5605643c0794e0de3aa2bf05df198519aaf15d24ca135eb0e8bd8a9d879 - languageName: node - linkType: hard - -"@types/dagre@npm:^0": - version: 0.7.52 - resolution: "@types/dagre@npm:0.7.52" - checksum: 10c0/0e196a8c17a92765d6e28b10d78d5c1cb1ee540598428cbb61ce3b90e0fedaac2b11f6dbeebf0d2f69d5332d492b12091be5f1e575f538194e20d8887979d006 - languageName: node - linkType: hard - "@types/eslint-plugin-jsx-a11y@npm:6.10.0": version: 6.10.0 resolution: "@types/eslint-plugin-jsx-a11y@npm:6.10.0" @@ -2242,13 +2014,6 @@ __metadata: languageName: node linkType: hard -"@types/geojson@npm:*": - version: 7946.0.15 - resolution: "@types/geojson@npm:7946.0.15" - checksum: 10c0/535d21ceaa01717cfdacc8f3dcbb7bc60a04361f401d80e60be22ce8dea23d669e4d0026c2c3da1168e807ee5ad4c9b2b4913ecd78eb0aabbcf76e92dc69808d - languageName: node - linkType: hard - "@types/history@npm:^4.7.11": version: 4.7.11 resolution: "@types/history@npm:4.7.11" @@ -5948,9 +5713,6 @@ __metadata: "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.1.0" "@testing-library/user-event": "npm:14.5.2" - "@types/d3": "npm:^7" - "@types/d3-hierarchy": "npm:^3" - "@types/dagre": "npm:^0" "@types/eslint-plugin-jsx-a11y": "npm:6.10.0" "@types/eslint__js": "npm:8.42.3" "@types/node": "npm:^22.0.0" From b95d7890b5e5f6699eeb6e1599e1b128189e53d9 Mon Sep 17 00:00:00 2001 From: Joshua Kitenge Date: Mon, 17 Mar 2025 14:17:16 +0000 Subject: [PATCH 11/14] add snapshot tests --- .../systemsTree.component.test.tsx.snap | 3469 +++++++++++++++++ src/systems/systemsTree.component.test.tsx | 58 + src/systems/systemsTree.component.tsx | 4 +- 3 files changed, 3530 insertions(+), 1 deletion(-) create mode 100644 src/systems/__snapshots__/systemsTree.component.test.tsx.snap create mode 100644 src/systems/systemsTree.component.test.tsx diff --git a/src/systems/__snapshots__/systemsTree.component.test.tsx.snap b/src/systems/__snapshots__/systemsTree.component.test.tsx.snap new file mode 100644 index 000000000..4741675c4 --- /dev/null +++ b/src/systems/__snapshots__/systemsTree.component.test.tsx.snap @@ -0,0 +1,3469 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Systems > renders admin card view correctly 1`] = ` + +
+
+
+ Depth: +
+
+ + + + +
+ +
+
+ + +
+
+
+
+
+
+
+
+
+
+