From ff12a36baf68e67a26c2abbd7f472aa8837efe07 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Wed, 22 Mar 2023 15:59:27 -0400 Subject: [PATCH] chore(matchExpr): clean up and save graph positions Signed-off-by: Thuan Vo --- src/app/Rules/CreateRule.tsx | 5 +- .../Credentials/CreateCredentialModal.tsx | 4 +- .../MatchExpressionVisualizer.tsx | 73 +++++++++++++++++-- src/app/app.css | 4 + src/app/utils/LocalStorage.ts | 2 + src/test/Rules/CreateRule.test.tsx | 2 +- 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/app/Rules/CreateRule.tsx b/src/app/Rules/CreateRule.tsx index cb18e7925..a68819ef1 100644 --- a/src/app/Rules/CreateRule.tsx +++ b/src/app/Rules/CreateRule.tsx @@ -296,7 +296,6 @@ const CreateRuleForm: React.FC = ({ ...props }) => { } setMatchExpressionValid(validation); matchedTargets.next(matches); - console.log(matchExpression); }, [matchExpression, targets, matchedTargets, setMatchExpressionValid, setTemplateName]); const createButtonLoadingProps = React.useMemo( @@ -634,14 +633,14 @@ const Comp: React.FC = () => { - + - + diff --git a/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx b/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx index 1bb1994f8..30a1ca8df 100644 --- a/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx +++ b/src/app/SecurityPanel/Credentials/CreateCredentialModal.tsx @@ -92,7 +92,7 @@ export const CreateCredentialModal: React.FunctionComponent - + - + diff --git a/src/app/Shared/MatchExpression/MatchExpressionVisualizer.tsx b/src/app/Shared/MatchExpression/MatchExpressionVisualizer.tsx index 9aa32be7a..f82efb374 100644 --- a/src/app/Shared/MatchExpression/MatchExpressionVisualizer.tsx +++ b/src/app/Shared/MatchExpression/MatchExpressionVisualizer.tsx @@ -36,16 +36,17 @@ * SOFTWARE. */ import { TopologyControlBar } from '@app/Topology/GraphView/TopologyControlBar'; +import { SavedGraphPosition, SavedNodePosition } from '@app/Topology/GraphView/TopologyGraphView'; import { getNodeById } from '@app/Topology/GraphView/UtilsFactory'; import EntityDetails, { AlertOptions } from '@app/Topology/Shared/Entity/EntityDetails'; import { useSearchExpression } from '@app/Topology/Shared/utils'; import { TopologySideBar } from '@app/Topology/SideBar/TopologySideBar'; import { NodeType } from '@app/Topology/typings'; +import { getFromLocalStorage, saveToLocalStorage } from '@app/utils/LocalStorage'; import { useSubscriptions } from '@app/utils/useSubscriptions'; import { evaluateTargetWithExpr, hashCode } from '@app/utils/utils'; import { Bullseye, - Button, DataList, DataListCell, DataListContent, @@ -69,19 +70,18 @@ import { ContainerNodeIcon, SearchIcon } from '@patternfly/react-icons'; import { css } from '@patternfly/react-styles'; import { action, - BOTTOM_LAYER, - DEFAULT_LAYER, GraphElement, - GROUPS_LAYER, + GRAPH_POSITION_CHANGE_EVENT, Model, + NODE_POSITIONED_EVENT, SelectionEventListener, SELECTION_EVENT, TopologyView, - TOP_LAYER, Visualization, VisualizationProvider, VisualizationSurface, } from '@patternfly/react-topology'; +import _ from 'lodash'; import * as React from 'react'; import { ServiceContext } from '../Services/Services'; import { Target } from '../Services/Target.service'; @@ -170,6 +170,40 @@ const GraphView: React.FC<{ alertOptions?: AlertOptions }> = ({ alertOptions, .. setSelectedEntity(ids[0] ? _newVisualization.getElementById(ids[0]) : undefined); }); + _newVisualization.addEventListener( + GRAPH_POSITION_CHANGE_EVENT, + _.debounce(() => { + const { graph } = _newVisualization.toModel(); + if (graph) { + const saved: SavedGraphPosition = { + id: graph.id, + type: graph.type, + x: graph.x, + y: graph.y, + scale: graph.scale, + scaleExtent: graph.scaleExtent, + }; + saveToLocalStorage('MATCH_EXPRES_VIS_GRAPH_POSITIONS', saved); + } + }, 200) + ); + + _newVisualization.addEventListener( + NODE_POSITIONED_EVENT, + _.debounce(() => { + const { nodes } = _newVisualization.toModel(); + if (nodes) { + const savedPos: SavedNodePosition[] = nodes.map((n) => ({ + id: n.id, + x: n.x, + y: n.y, + collapsed: n.collapsed, + })); + saveToLocalStorage('MATCH_EXPRES_VIS_NODE_POSITIONS', savedPos); + } + }, 200) + ); + return _newVisualization; }, [setSelectedIds, setSelectedEntity]); @@ -184,8 +218,22 @@ const GraphView: React.FC<{ alertOptions?: AlertOptions }> = ({ alertOptions, .. }, [addSubscription, context.targets, setTargets]); React.useEffect(() => { + const graphData: SavedGraphPosition = getFromLocalStorage('MATCH_EXPRES_VIS_GRAPH_POSITIONS', {}); + const nodePositions: SavedNodePosition[] = getFromLocalStorage('MATCH_EXPRES_VIS_NODE_POSITIONS', []); + const model: Model = { - nodes: _transformedData.nodes, + nodes: _transformedData.nodes.map((n) => { + const savedData = nodePositions.find((ps) => ps.id === n.id); + if (savedData) { + n = { + ...n, + x: savedData.x, + y: savedData.y, + collapsed: savedData.collapsed, + }; + } + return n; + }), edges: _transformedData.edges, graph: { id: 'cryostat-match-expression-visualizer', @@ -198,12 +246,23 @@ const GraphView: React.FC<{ alertOptions?: AlertOptions }> = ({ alertOptions, .. labels: {}, children: targetNodes, }, + x: graphData.x, + y: graphData.y, + scale: graphData.scale, + scaleExtent: graphData.scaleExtent, }, }; // Initialize the controller with model to create nodes visualization.fromModel(model, false); - const _id = setTimeout(action(() => visualization.getGraph().fit())); + + const _id = setTimeout( + action(() => { + if (!graphData.id || !graphData.x || !graphData.y) { + visualization.getGraph().fit(); + } + }) + ); return () => clearTimeout(_id); }, [_transformedData, targetNodes, visualization]); diff --git a/src/app/app.css b/src/app/app.css index 09a803aa4..49eda68e4 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -519,3 +519,7 @@ input[type=number].datetime-picker__number-input { width: 80% !important; height: 80%; } + +.overflow-auto { + overflow: auto; +} diff --git a/src/app/utils/LocalStorage.ts b/src/app/utils/LocalStorage.ts index 3c0ccb683..6a274afff 100644 --- a/src/app/utils/LocalStorage.ts +++ b/src/app/utils/LocalStorage.ts @@ -60,6 +60,8 @@ export enum LocalStorageKey { NOTIFICATIONS_ENABLED, WEBSOCKET_DEBOUNCE_MS, DATETIME_FORMAT, + MATCH_EXPRES_VIS_GRAPH_POSITIONS, + MATCH_EXPRES_VIS_NODE_POSITIONS, } export type LocalStorageKeyStrings = keyof typeof LocalStorageKey; diff --git a/src/test/Rules/CreateRule.test.tsx b/src/test/Rules/CreateRule.test.tsx index 4f0543d31..9e8d69b55 100644 --- a/src/test/Rules/CreateRule.test.tsx +++ b/src/test/Rules/CreateRule.test.tsx @@ -41,7 +41,7 @@ import { Rule } from '@app/Rules/Rules'; import { defaultServices } from '@app/Shared/Services/Services'; import { Target } from '@app/Shared/Services/Target.service'; import '@testing-library/jest-dom'; -import { act, cleanup, screen, waitFor } from '@testing-library/react'; +import { cleanup, screen, waitFor } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import * as React from 'react'; import { of } from 'rxjs';