diff options
author | Mitch Wagner <dev@mitchwag.com> | 2022-06-24 20:21:44 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-06-24 20:51:13 +0000 |
commit | b05965036e8659ea4ef5a312dca8d12761c17a33 (patch) | |
tree | 5a5898c4e84b12fadb84d8e7a828c371f3cbca03 | |
parent | 5ab8714a12cbe55b6a3ca417e0a53b228625a057 (diff) | |
download | mongo-b05965036e8659ea4ef5a312dca8d12761c17a33.tar.gz |
SERVER-53526 Move Libdeps Visualizer Backend State to Frontend
14 files changed, 431 insertions, 376 deletions
diff --git a/buildscripts/libdeps/graph_visualizer.py b/buildscripts/libdeps/graph_visualizer.py index 8d601355a56..e156ea380bb 100644 --- a/buildscripts/libdeps/graph_visualizer.py +++ b/buildscripts/libdeps/graph_visualizer.py @@ -24,9 +24,9 @@ """ Libdeps Graph Visualization Tool. -Starts a web service which creates a UI for interacting and examing the libdeps graph. -The web service front end consist of React+Redux for the framework, SocketIO for backend -communication, and Material UI for the GUI. The web service back end use flask and socketio. +Starts a web service which creates a UI for interacting and examining the libdeps graph. +The web service front end consist of React+Redux for the framework, flask API for backend +communication, and Material UI for the GUI. The web service back end uses flask. This script will automatically install the npm modules, and build and run the production web service if not debug. @@ -116,9 +116,8 @@ def check_node(node_check, cwd): def start_backend(web_service_info, debug): """Start the backend in debug mode.""" - web_service_info['socketio'].run(app=web_service_info['app'], - host=web_service_info['backend_host'], - port=web_service_info['backend_port'], debug=debug) + web_service_info['app'].run(host=web_service_info['backend_host'], + port=web_service_info['backend_port'], debug=debug) def start_frontend_thread(web_service_info, npm_command, debug): @@ -160,12 +159,11 @@ def main(): server = BackendServer(graphml_dir=args.graphml_dir, frontend_url=f"http://{args.frontend_host}:{args.frontend_port}") - app, socketio = server.get_app() + app = server.get_app() cwd = Path(__file__).parent / 'graph_visualizer_web_stack' web_service_info = { 'app': app, - 'socketio': socketio, 'cwd': cwd, 'frontend_host': args.frontend_host, 'frontend_port': args.frontend_port, diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py b/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py index ce321a1bd00..f9f1b2b25aa 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py +++ b/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py @@ -33,10 +33,10 @@ from collections import namedtuple, OrderedDict import flask import networkx -from flask_socketio import SocketIO, emit from flask_cors import CORS from flask_session import Session from lxml import etree +from flask import request import libdeps.graph import libdeps.analyzer @@ -49,51 +49,50 @@ class BackendServer: def __init__(self, graphml_dir, frontend_url): """Create and setup the state variables.""" self.app = flask.Flask(__name__) - self.socketio = SocketIO(self.app, cors_allowed_origins=frontend_url) self.app.config['CORS_HEADERS'] = 'Content-Type' CORS(self.app, resources={r"/*": {"origins": frontend_url}}) - self.app.add_url_rule("/graph_files", "return_graph_files", self.return_graph_files) - self.socketio.on_event('git_hash_selected', self.git_hash_selected) - self.socketio.on_event('row_selected', self.row_selected) - self.socketio.on_event('receive_graph_paths', self.receive_graph_paths) + self.app.add_url_rule("/api/graphs", "return_graph_files", self.return_graph_files) + self.app.add_url_rule("/api/graphs/<git_hash>/nodes", "return_node_list", + self.return_node_list) + self.app.add_url_rule("/api/graphs/<git_hash>/analysis", "return_analyze_counts", + self.return_analyze_counts) + self.app.add_url_rule("/api/graphs/<git_hash>/d3", "return_d3", self.return_d3, + methods=['POST']) + self.app.add_url_rule("/api/graphs/<git_hash>/nodes/details", "return_node_infos", + self.return_node_infos, methods=['POST']) + self.app.add_url_rule("/api/graphs/<git_hash>/paths", "return_paths_between", + self.return_paths_between, methods=['POST']) self.loaded_graphs = {} - self.current_selected_rows = {} self.graphml_dir = Path(graphml_dir) self.frontend_url = frontend_url self.graph_file_tuple = namedtuple('GraphFile', ['version', 'git_hash', 'graph_file']) self.graph_files = self.get_graphml_files() - self._dependents_graph = None - self._dependency_graph = None + @staticmethod + def get_dependency_graph(graph): + """Returns the dependency graph of a given graph.""" - try: - default_selected_graph = list(self.graph_files.items())[0][1].graph_file - self.load_graph_from_file(default_selected_graph) - except (IndexError, AttributeError) as ex: - print(ex) - print( - f"Failed to load read a graph file from {list(self.graph_files.items())} for graphml_dir '{self.graphml_dir}'" - ) - exit(1) + if graph.graph['graph_schema_version'] == 1: + return networkx.reverse_view(graph) + else: + return graph - def load_graph_from_file(self, file_path): - """Load a graph file from disk and handle version.""" + @staticmethod + def get_dependents_graph(graph): + """Returns the dependents graph of a given graph.""" - graph = libdeps.graph.LibdepsGraph(networkx.read_graphml(file_path)) if graph.graph['graph_schema_version'] == 1: - self._dependents_graph = graph - self._dependency_graph = networkx.reverse_view(self._dependents_graph) + return graph else: - self._dependency_graph = graph - self._dependents_graph = networkx.reverse_view(self._dependency_graph) + return networkx.reverse_view(graph) def get_app(self): - """Return the app and socketio instances.""" + """Return the app instance.""" - return self.app, self.socketio + return self.app def get_graph_build_data(self, graph_file): """Fast method for extracting basic build data from the graph file.""" @@ -132,187 +131,182 @@ class BackendServer: }) return data - def send_node_infos(self): - """Search through the selected rows and find information about the selected rows.""" + def return_node_infos(self, git_hash): + """Returns details about a set of selected nodes.""" - with self.app.test_request_context(): + req_body = request.get_json() + if "selected_nodes" in req_body.keys(): + selected_nodes = req_body["selected_nodes"] + + if graph := self.load_graph(git_hash): + dependents_graph = self.get_dependents_graph(graph) + dependency_graph = self.get_dependency_graph(graph) + + nodeinfo_data = {'nodeInfos': []} - nodeinfo_data = {'nodeInfos': []} - - for node, _ in self.current_selected_rows.items(): - - nodeinfo_data['nodeInfos'].append({ - 'id': - len(nodeinfo_data['nodeInfos']), - 'node': - str(node), - 'name': - node.name, - 'attribs': [{ - 'name': key, 'value': value - } for key, value in self._dependents_graph.nodes(data=True)[str(node)].items()], - 'dependers': [{ + for node in selected_nodes: + + nodeinfo_data['nodeInfos'].append({ + 'id': + len(nodeinfo_data['nodeInfos']), 'node': - depender, 'symbols': - self._dependents_graph[str(node)][depender].get('symbols', + str(node), + 'name': + Path(node).name, + 'attribs': [{ + 'name': key, 'value': value + } for key, value in dependents_graph.nodes(data=True)[str(node)].items()], + 'dependers': [{ + 'node': + depender, 'symbols': + dependents_graph[str(node)][depender].get('symbols', + '').split(' ') + } for depender in dependents_graph[str(node)]], + 'dependencies': [{ + 'node': + dependency, 'symbols': + dependents_graph[dependency][str(node)].get('symbols', '').split(' ') - } for depender in self._dependents_graph[str(node)]], - 'dependencies': [{ - 'node': - dependency, 'symbols': - self._dependents_graph[dependency][str(node)].get('symbols', - '').split(' ') - } for dependency in self._dependency_graph[str(node)]], - }) + } for dependency in dependency_graph[str(node)]], + }) - self.socketio.emit("node_infos", nodeinfo_data) + return nodeinfo_data, 200 + return { + 'error': 'Git commit hash (' + git_hash + ') does not have a matching graph file.' + }, 400 + return {'error': 'Request body does not contain "selected_nodes" attribute.'}, 400 - def send_graph_data(self, extra_nodes=None): + def return_d3(self, git_hash): """Convert the current selected rows into a format for D3.""" - with self.app.test_request_context(): + req_body = request.get_json() + if "selected_nodes" in req_body.keys(): + selected_nodes = req_body["selected_nodes"] - nodes = {} - links = {} - - def add_node_to_graph_data(node): - nodes[str(node)] = { - 'id': str(node), 'name': Path(node).name, - 'type': self._dependents_graph.nodes()[str(node)]['bin_type'] - } + if graph := self.load_graph(git_hash): + dependents_graph = self.get_dependents_graph(graph) + dependency_graph = self.get_dependency_graph(graph) - def add_link_to_graph_data(source, target): - links[str(source) + str(target)] = {'source': str(source), 'target': str(target)} + nodes = {} + links = {} - for node, _ in self.current_selected_rows.items(): - add_node_to_graph_data(node) + def add_node_to_graph_data(node): + nodes[str(node)] = { + 'id': str(node), 'name': Path(node).name, 'type': dependents_graph.nodes() + [str(node)]['bin_type'] + } - for libdep in self._dependency_graph[str(node)]: - if self._dependents_graph[libdep][str(node)].get('direct'): - add_node_to_graph_data(libdep) - add_link_to_graph_data(node, libdep) + def add_link_to_graph_data(source, target): + links[str(source) + str(target)] = { + 'source': str(source), 'target': str(target) + } - if extra_nodes is not None: - for node in extra_nodes: + for node in selected_nodes: add_node_to_graph_data(node) - for libdep in self._dependency_graph.get_direct_nonprivate_graph()[str(node)]: - add_node_to_graph_data(libdep) - add_link_to_graph_data(node, libdep) - - node_data = { - 'graphData': { - 'nodes': [data for node, data in nodes.items()], - 'links': [data for link, data in links.items()], - }, 'selectedNodes': [str(node) for node in list(self.current_selected_rows.keys())] - } - self.socketio.emit("graph_data", node_data) - - def row_selected(self, message): - """Construct the new graphData nodeInfo when a cell is selected.""" - - if message['isSelected'] == 'flip': - if message['data']['node'] in self.current_selected_rows: - self.current_selected_rows.pop(Path(message['data']['node'])) - else: - self.current_selected_rows[Path(message['data']['node'])] = message['data'] - else: - if message['isSelected'] and message: - self.current_selected_rows[Path(message['data']['node'])] = message['data'] - else: - self.current_selected_rows.pop(Path(message['data']['node'])) - - self.socketio.start_background_task(self.send_graph_data) - self.socketio.start_background_task(self.send_node_infos) + for libdep in dependency_graph[str(node)]: + if dependents_graph[libdep][str(node)].get('direct'): + add_node_to_graph_data(libdep) + add_link_to_graph_data(node, libdep) + + if "extra_nodes" in req_body.keys(): + extra_nodes = req_body["extra_nodes"] + for node in extra_nodes: + add_node_to_graph_data(node) + + for libdep in dependency_graph.get_direct_nonprivate_graph()[str(node)]: + add_node_to_graph_data(libdep) + add_link_to_graph_data(node, libdep) + + node_data = { + 'graphData': { + 'nodes': [data for node, data in nodes.items()], + 'links': [data for link, data in links.items()], + } + } + return node_data, 200 + return { + 'error': 'Git commit hash (' + git_hash + ') does not have a matching graph file.' + }, 400 + return {'error': 'Request body does not contain "selected_nodes" attribute.'}, 400 - def analyze_counts(self): + def return_analyze_counts(self, git_hash): """Perform count analysis and send the results back to frontend.""" with self.app.test_request_context(): - - analysis = libdeps.analyzer.counter_factory( - self._dependency_graph, - [name[0] for name in libdeps.analyzer.CountTypes.__members__.items()]) - ga = libdeps.analyzer.LibdepsGraphAnalysis(analysis) - results = ga.get_results() - - graph_data = [] - for i, data in enumerate(results): - graph_data.append({'id': i, 'type': data, 'value': results[data]}) - self.socketio.emit("graph_results", graph_data) - - def receive_graph_paths(self, message): - """Receive the reqest message and kick it off to another thread.""" - - self.socketio.start_background_task(self.send_paths, message) - - def send_paths(self, message): - """Gather all the nodes in the graph for the node list.""" - - with self.app.test_request_context(): - - analysis = [ - libdeps.analyzer.GraphPaths(self._dependency_graph, message['fromNode'], - message['toNode']) - ] - ga = libdeps.analyzer.LibdepsGraphAnalysis(analysis=analysis) - results = ga.get_results() - - paths = results[libdeps.analyzer.DependsReportTypes.GRAPH_PATHS.name][( - message['fromNode'], message['toNode'])] - paths.sort(key=len) - nodes = set() - for path in paths: - for node in path: - nodes.add(node) - - self.send_graph_data(extra_nodes=list(nodes)) - - self.socketio.emit( - "graph_path_results", - {'fromNode': message['fromNode'], 'toNode': message['toNode'], 'paths': paths}) - - def send_node_list(self): + if graph := self.load_graph(git_hash): + dependency_graph = self.get_dependency_graph(graph) + + analysis = libdeps.analyzer.counter_factory( + dependency_graph, + [name[0] for name in libdeps.analyzer.CountTypes.__members__.items()]) + ga = libdeps.analyzer.LibdepsGraphAnalysis(analysis) + results = ga.get_results() + + graph_data = [] + for i, data in enumerate(results): + graph_data.append({'id': i, 'type': data, 'value': results[data]}) + return {'results': graph_data}, 200 + return { + 'error': 'Git commit hash (' + git_hash + ') does not have a matching graph file.' + }, 400 + + def return_paths_between(self, git_hash): + """Gather all the paths in the graph between a fromNode and toNode.""" + + message = request.get_json() + if "fromNode" in message.keys() and "toNode" in message.keys(): + if graph := self.load_graph(git_hash): + dependency_graph = self.get_dependency_graph(graph) + analysis = [ + libdeps.analyzer.GraphPaths(dependency_graph, message['fromNode'], + message['toNode']) + ] + ga = libdeps.analyzer.LibdepsGraphAnalysis(analysis=analysis) + results = ga.get_results() + + paths = results[libdeps.analyzer.DependsReportTypes.GRAPH_PATHS.name][( + message['fromNode'], message['toNode'])] + paths.sort(key=len) + nodes = set() + for path in paths: + for node in path: + nodes.add(node) + + # Need to handle self.send_graph_data(extra_nodes=list(nodes)) + return { + 'fromNode': message['fromNode'], 'toNode': message['toNode'], 'paths': paths, + 'extraNodes': list(nodes) + }, 200 + return { + 'error': 'Git commit hash (' + git_hash + ') does not have a matching graph file.' + }, 400 + return {'error': 'Body must contain toNode and fromNode'}, 400 + + def return_node_list(self, git_hash): """Gather all the nodes in the graph for the node list.""" with self.app.test_request_context(): - node_data = { - 'graphData': {'nodes': [], 'links': []}, - "selectedNodes": [str(node) for node in list(self.current_selected_rows.keys())] - } - - for node in sorted(self._dependents_graph.nodes()): - node_path = Path(node) - node_data['graphData']['nodes'].append( - {'id': str(node_path), 'name': node_path.name}) - self.socketio.emit("graph_nodes", node_data) - - def load_graph(self, message): - """Load the graph into application memory and kick off threads for analysis on new graph.""" + node_data = {'nodes': [], 'links': []} + if graph := self.load_graph(git_hash): + for node in sorted(graph.nodes()): + node_path = Path(node) + node_data['nodes'].append(str(node_path)) + return node_data, 200 + return { + 'error': 'Git commit hash (' + git_hash + ') does not have a matching graph file.' + }, 400 + + def load_graph(self, git_hash): + """Load the graph into application memory.""" with self.app.test_request_context(): - - current_hash = self._dependents_graph.graph.get('git_hash', 'NO_HASH')[:7] - if current_hash != message['hash']: - self.current_selected_rows = {} - if message['hash'] in self.loaded_graphs: - self._dependents_graph = self.loaded_graphs[message['hash']] - self._dependency_graph = networkx.reverse_view(self._dependents_graph) - else: - print( - f'loading new graph {current_hash} because different than {message["hash"]}' - ) - - self.load_graph_from_file(self.graph_files[message['hash']].graph_file) - self.loaded_graphs[message['hash']] = self._dependents_graph - - self.socketio.start_background_task(self.analyze_counts) - self.socketio.start_background_task(self.send_node_list) - self.socketio.emit("graph_data", {'graphData': {'nodes': [], 'links': []}}) - - def git_hash_selected(self, message): - """Load the new graph and perform queries on it.""" - - emit("other_hash_selected", message, broadcast=True) - - self.socketio.start_background_task(self.load_graph, message) + if git_hash in self.loaded_graphs: + return self.loaded_graphs[git_hash] + else: + if git_hash in self.graph_files: + file_path = self.graph_files[git_hash].graph_file + graph = libdeps.graph.LibdepsGraph(networkx.read_graphml(file_path)) + self.loaded_graphs[git_hash] = graph + return graph + return None diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/package.json b/buildscripts/libdeps/graph_visualizer_web_stack/package.json index 214f87ca4c8..f1da6698942 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/package.json +++ b/buildscripts/libdeps/graph_visualizer_web_stack/package.json @@ -43,7 +43,6 @@ "react-virtualized": "^9.22.2", "react-window": "^1.8.6", "redux": "^4.0.5", - "socket.io-client": "latest", "typescript": "^3.9.7" }, "browserslist": { diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js index dd16b5a33d8..a1d43ca7de2 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js @@ -10,7 +10,8 @@ import Typography from "@material-ui/core/Typography"; import { getRows } from "./redux/store"; import { updateSelected } from "./redux/nodes"; -import { socket } from "./connect"; +import { setGraphData } from "./redux/graphData"; +import { setNodeInfos } from "./redux/nodeInfo"; function componentToHex(c) { var hex = c.toString(16); @@ -88,12 +89,46 @@ const DataGrid = ({ onNodeClicked, updateSelected, classes, + setGraphData, + selectedGraph, + setNodeInfos, + selectedNodes, + searchedNodes, }) => { const [checkBoxes, setCheckBoxes] = React.useState([]); React.useEffect(() => { - setCheckBoxes(nodes); - }, [nodes]); + setCheckBoxes(searchedNodes); + }, [searchedNodes]); + + function newGraphData() { + let gitHash = selectedGraph; + let postData = { + "selected_nodes": nodes.filter(node => node.selected == true).map(node => node.node) + }; + fetch('/api/graphs/' + gitHash + '/d3', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + setGraphData(data.graphData); + }); + fetch('/api/graphs/' + gitHash + '/nodes/details', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + setNodeInfos(data.nodeInfos); + }); + } const getRowClassName = ({ index }) => { return clsx( @@ -125,10 +160,7 @@ const DataGrid = ({ if (checkBoxes[rowIndex].selected != event.target.checked) { updateSelected({ index: rowIndex, value: event.target.checked }); } - socket.emit("row_selected", { - data: { node: nodes[rowIndex].node, name: nodes[rowIndex].name }, - isSelected: event.target.checked, - }); + newGraphData(); }} /> ); @@ -217,6 +249,6 @@ const DataGrid = ({ ); }; -export default connect(getRows, { updateSelected })( +export default connect(getRows, { updateSelected, setGraphData, setNodeInfos })( withStyles(styles)(DataGrid) ); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js index 375942f4852..2e1b429928a 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js @@ -8,10 +8,11 @@ import Button from "@material-ui/core/Button"; import TextField from "@material-ui/core/TextField"; import theme from "./theme"; -import { socket } from "./connect"; import { getGraphData } from "./redux/store"; import { updateCheckbox } from "./redux/nodes"; import { setFindNode } from "./redux/findNode"; +import { setGraphData } from "./redux/graphData"; +import { setNodeInfos } from "./redux/nodeInfo"; import LoadingBar from "./LoadingBar"; const handleFindNode = (node_value, graphData, activeComponent, forceRef) => { @@ -59,9 +60,11 @@ const DrawGraph = ({ updateCheckbox, findNode, setFindNode, + setGraphData, + setNodeInfos, + selectedGraph }) => { const [activeComponent, setActiveComponent] = React.useState("2D"); - const [selectedNodes, setSelectedNodes] = React.useState([]); const [pathNodes, setPathNodes] = React.useState({}); const [pathEdges, setPathEdges] = React.useState([]); const forceRef = useRef(null); @@ -71,15 +74,7 @@ const DrawGraph = ({ setFindNode(""); }, [findNode, graphData, activeComponent, forceRef]); - React.useEffect(() => { - setSelectedNodes( - nodes.map((node) => { - if (node.selected) { - return node.node; - } - }) - ); - }, [nodes]); + const selectedNodes = nodes.filter(node => node.selected == true).map(node => node.node); React.useEffect(() => { setPathNodes({ fromNode: graphPaths.fromNode, toNode: graphPaths.toNode }); @@ -112,6 +107,35 @@ const DrawGraph = ({ } }, [forceRef.current, activeComponent]); + function newGraphData() { + let gitHash = selectedGraph; + let postData = { + "selected_nodes": nodes.filter(node => node.selected == true).map(node => node.node) + }; + fetch('/api/graphs/' + gitHash + '/d3', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + setGraphData(data.graphData); + }); + fetch('/api/graphs/' + gitHash + '/nodes/details', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + setNodeInfos(data.nodeInfos); + }); + } + const paintRing = React.useCallback( (node, ctx) => { // add ring just for highlighted nodes @@ -244,10 +268,7 @@ const DrawGraph = ({ nodeCanvasObject={paintRing} onNodeClick={(node, event) => { updateCheckbox({ node: node.id, value: "flip" }); - socket.emit("row_selected", { - data: { node: node.id, name: node.name }, - isSelected: !selectedNodes.includes(node.id), - }); + newGraphData(); }} /> <ForceGraph3D @@ -271,10 +292,7 @@ const DrawGraph = ({ }} onNodeClick={(node, event) => { updateCheckbox({ node: node.id, value: "flip" }); - socket.emit("row_selected", { - data: { node: node.id, name: node.name }, - isSelected: !selectedNodes.includes(node.id), - }); + newGraphData(); }} linkColor={(d) => { if (graphPaths.selectedPath >= 0) { @@ -344,6 +362,6 @@ const DrawGraph = ({ ); }; -export default connect(getGraphData, { setFindNode, updateCheckbox })( +export default connect(getGraphData, { setFindNode, updateCheckbox, setGraphData, setNodeInfos })( DrawGraph ); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js index 63e8a99427c..02afb784523 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js @@ -4,10 +4,11 @@ import LoadingButton from "@material-ui/lab/LoadingButton"; import GitIcon from "@material-ui/icons/GitHub"; import { green, grey } from "@material-ui/core/colors"; -import { socket } from "./connect"; import { getGraphFiles } from "./redux/store"; import { setLoading } from "./redux/loading"; import theme from "./theme"; +import { selectGraphFile } from "./redux/graphFiles"; +import { nodeInfo, setNodeInfos } from "./redux/nodeInfo"; const selectedStyle = { color: theme.palette.getContrastText(green[500]), @@ -31,15 +32,28 @@ const unselectedStyle = { }, }; -const GitHashButton = ({ loading, graphFiles, setLoading, text }) => { +const GitHashButton = ({ loading, graphFiles, setLoading, selectGraphFile, setNodeInfos, text }) => { const [selected, setSelected] = React.useState(false); const [selfLoading, setSelfLoading] = React.useState(false); const [firstLoad, setFirstLoad] = React.useState(true); function handleClick() { + const selectedGraphFiles = graphFiles.filter( + (graphFile) => graphFile.selected == true + ); + + if (selectedGraphFiles.length > 0) { + if (selectedGraphFiles[0]["git"] == text) { + return; + } + } + setSelfLoading(true); setLoading(true); - socket.emit("git_hash_selected", { hash: text, selected: true }); + selectGraphFile({ + hash: text, + selected: true, + }); } React.useEffect(() => { @@ -76,4 +90,4 @@ const GitHashButton = ({ loading, graphFiles, setLoading, text }) => { ); }; -export default connect(getGraphFiles, { setLoading })(GitHashButton); +export default connect(getGraphFiles, { setLoading, selectGraphFile, setNodeInfos })(GitHashButton); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js index adce3295596..86d01e88b87 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js @@ -10,11 +10,13 @@ import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import TextField from "@material-ui/core/TextField"; -import SocketConnection from "./connect"; import { getGraphFiles } from "./redux/store"; +import { setGraphFiles } from "./redux/graphFiles"; import GitHashButton from "./GitHashButton"; +const { REACT_APP_API_URL } = process.env; + const flexContainer = { display: "flex", flexDirection: "row", @@ -29,7 +31,19 @@ const textFields = [ "Commit Range End", ]; -const GraphCommitDisplay = ({ graphFiles }) => { +const GraphCommitDisplay = ({ graphFiles, setGraphFiles }) => { + React.useEffect(() => { + fetch(REACT_APP_API_URL + "/api/graphs") + .then((res) => res.json()) + .then((data) => { + setGraphFiles(data.graph_files); + }) + .catch((err) => { + /* eslint-disable no-console */ + console.log("Error Reading data " + err); + }); + }, []); + return ( <Paper style={{ height: "100%", width: "100%" }}> <List style={flexContainer}> @@ -56,10 +70,9 @@ const GraphCommitDisplay = ({ graphFiles }) => { </TableRow> </TableBody> </Table> - <SocketConnection /> </ScrollContainer> </Paper> ); }; -export default connect(getGraphFiles)(GraphCommitDisplay); +export default connect(getGraphFiles, { setGraphFiles })(GraphCommitDisplay); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js index 626306a1b4f..b5f3cec93eb 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js @@ -9,6 +9,7 @@ import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import { connect } from "react-redux"; import { getCounts } from "./redux/store"; +import { setCounts } from "./redux/counts"; const columns = [ { id: "ID", field: "type", headerName: "Count Type", width: 50 }, @@ -21,7 +22,16 @@ const useStyles = makeStyles({ }, }); -const GraphInfo = ({ counts, datawidth }) => { +const GraphInfo = ({ selectedGraph, counts, datawidth, setCounts }) => { + React.useEffect(() => { + let gitHash = selectedGraph; + fetch('/api/graphs/' + gitHash + '/analysis') + .then(response => response.json()) + .then(data => { + setCounts(data.results); + }); + }, [selectedGraph]); + const classes = useStyles(); return ( @@ -49,4 +59,4 @@ const GraphInfo = ({ counts, datawidth }) => { ); }; -export default connect(getCounts)(GraphInfo); +export default connect(getCounts, { setCounts })(GraphInfo); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js index 0b86129bd98..39d35aeb50d 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js @@ -12,11 +12,11 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import MuiAccordion from "@material-ui/core/Accordion"; import MuiAccordionSummary from "@material-ui/core/AccordionSummary"; import MuiAccordionDetails from "@material-ui/core/AccordionDetails"; -import { socket } from "./connect"; import useResizeAware from "react-resize-aware"; import { getSelected } from "./redux/store"; import { selectedGraphPaths, setSelectedPath } from "./redux/graphPaths"; +import { setGraphData } from "./redux/graphData"; import OverflowTooltip from "./OverflowTooltip"; @@ -63,7 +63,7 @@ const AccordionDetails = withStyles((theme) => ({ }, }))(MuiAccordionDetails); -const GraphPaths = ({ selectedNodes, graphPaths, setSelectedPath, width }) => { +const GraphPaths = ({ nodes, selectedGraph, selectedNodes, graphPaths, setSelectedPath, width, selectedGraphPaths, setGraphData }) => { const [fromNode, setFromNode] = React.useState(""); const [toNode, setToNode] = React.useState(""); const [fromNodeId, setFromNodeId] = React.useState(0); @@ -90,6 +90,40 @@ const GraphPaths = ({ selectedNodes, graphPaths, setSelectedPath, width }) => { })); const classes = useStyles(); + function getGraphPaths(fromNode, toNode) { + let gitHash = selectedGraph; + let postData = { + "fromNode": fromNode, + "toNode": toNode + }; + fetch('/api/graphs/' + gitHash + '/paths', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + selectedGraphPaths(data); + let postData = { + "selected_nodes": nodes.filter(node => node.selected == true).map(node => node.node), + "extra_nodes": data.extraNodes + }; + fetch('/api/graphs/' + gitHash + '/d3', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + setGraphData(data.graphData); + }); + }); + } + function toNodeRow({ index, style, data }) { return ( <ListItem @@ -102,11 +136,7 @@ const GraphPaths = ({ selectedNodes, graphPaths, setSelectedPath, width }) => { setToNodeExpanded(false); setPaneSize("50%"); if (fromNode != "" && data[fromNodeId]) { - const nodes = { - fromNode: data[fromNodeId].node, - toNode: data[index].node, - }; - socket.emit("receive_graph_paths", nodes); + getGraphPaths(data[fromNodeId].node, data[index].node); } }} > @@ -128,11 +158,7 @@ const GraphPaths = ({ selectedNodes, graphPaths, setSelectedPath, width }) => { setPaneSize("50%"); if (toNode != "" && data[toNodeId]) { - const nodes = { - fromNode: data[index].node, - toNode: data[toNodeId].node, - }; - socket.emit("receive_graph_paths", nodes); + getGraphPaths(data[fromNodeId].node, data[index].node); } }} > @@ -289,6 +315,6 @@ const GraphPaths = ({ selectedNodes, graphPaths, setSelectedPath, width }) => { ); }; -export default connect(getSelected, { selectedGraphPaths, setSelectedPath })( +export default connect(getSelected, { selectedGraphPaths, setSelectedPath, setGraphData })( GraphPaths ); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js index eed58ee8875..4043ef57c5b 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js @@ -3,26 +3,42 @@ import React from "react"; import { connect } from "react-redux"; import { getNodes } from "./redux/store"; import { setFindNode } from "./redux/findNode"; -import { setListSearchTerm } from "./redux/listSearchTerm"; -import { socket } from "./connect"; import DataGrid from "./DataGrid"; import LoadingBar from "./LoadingBar"; import TextField from "@material-ui/core/TextField"; +import { setNodes } from "./redux/nodes"; +import { addLinks } from "./redux/links"; +import { setLoading } from "./redux/loading"; +import { setListSearchTerm } from "./redux/listSearchTerm"; + const columns = [ { dataKey: "check", label: "Selected", width: 70 }, { dataKey: "name", label: "Name", width: 200 }, { id: "ID", dataKey: "node", label: "Node", width: 200 }, ]; -const NodeList = ({ nodes, loading, setFindNode, setListSearchTerm}) => { - function handleCheckBoxes(rowIndex, event) { - socket.emit("row_selected", { - data: { node: nodes[rowIndex].node, name: nodes[rowIndex].name }, - isSelected: event.target.checked, - }); - } +const NodeList = ({ selectedGraph, nodes, loading, setFindNode, setNodes, addLinks, setLoading, setListSearchTerm }) => { + + React.useEffect(() => { + let gitHash = selectedGraph; + fetch('/api/graphs/' + gitHash + '/nodes') + .then(response => response.json()) + .then(data => { + setNodes(data.nodes.map((node, index) => { + return { + id: index, + node: node, + name: node.substring(node.lastIndexOf('/') + 1), + check: "checkbox", + selected: false, + }; + })); + addLinks(data.links); + setLoading(false); + }); + }, [selectedGraph]); function handleRowClick(event) { setFindNode(event.target.textContent); @@ -46,10 +62,9 @@ const NodeList = ({ nodes, loading, setFindNode, setListSearchTerm}) => { rowHeight={30} headerHeight={35} onNodeClicked={handleRowClick} - onRowSelect={handleCheckBoxes} /> </LoadingBar> ); }; -export default connect(getNodes, { setFindNode, setListSearchTerm })(NodeList); +export default connect(getNodes, { setFindNode, setNodes, addLinks, setLoading, setListSearchTerm })(NodeList);
\ No newline at end of file diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js index 4546073c838..d41891364a1 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js @@ -7,8 +7,10 @@ import IconButton from "@material-ui/core/IconButton"; import AddCircleOutline from "@material-ui/icons/AddCircleOutline"; import Typography from "@material-ui/core/Typography"; -import { socket } from "./connect"; import { updateCheckbox } from "./redux/nodes"; +import { setGraphData } from "./redux/graphData"; +import { setNodeInfos } from "./redux/nodeInfo"; +import { getGraphData } from "./redux/store"; const OverflowTip = (props) => { const textElementRef = useRef(null); @@ -22,6 +24,35 @@ const OverflowTip = (props) => { } }; + function newGraphData() { + let gitHash = props.selectedGraph; + let postData = { + "selected_nodes": props.nodes.filter(node => node.selected == true).map(node => node.node) + }; + fetch('/api/graphs/' + gitHash + '/d3', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + props.setGraphData(data.graphData); + }); + fetch('/api/graphs/' + gitHash + '/nodes/details', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) + }) + .then(response => response.json()) + .then(data => { + props.setNodeInfos(data.nodeInfos); + }); + } + useEffect(() => { compareSize(textElementRef); window.addEventListener("resize", compareSize); @@ -33,7 +64,7 @@ const OverflowTip = (props) => { return ( <Tooltip title={props.value} - interactive + interactive="true" disableHoverListener={!hoverStatus} style={{ fontSize: "1em" }} enterDelay={500} @@ -54,10 +85,7 @@ const OverflowTip = (props) => { color="secondary" onClick={(event) => { props.updateCheckbox({ node: props.text, value: "flip" }); - socket.emit("row_selected", { - data: { node: props.text, name: props.name }, - isSelected: "flip", - }); + newGraphData(); }} > <AddCircleOutline style={{ height: "15px", width: "15px" }} /> @@ -70,4 +98,4 @@ const OverflowTip = (props) => { ); }; -export default connect(null, { updateCheckbox })(OverflowTip); +export default connect(getGraphData, { updateCheckbox, setGraphData, setNodeInfos })(OverflowTip); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js deleted file mode 100644 index efa3b8b9ec3..00000000000 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from "react"; -import io from "socket.io-client"; -import { connect as reduxConnect } from "react-redux"; - -import { setNodes, updateCheckboxes } from "./redux/nodes"; -import { setCounts } from "./redux/counts"; -import { setNodeInfos } from "./redux/nodeInfo"; -import { setGraphFiles, selectGraphFile } from "./redux/graphFiles"; -import { setLoading } from "./redux/loading"; -import { addLinks } from "./redux/links"; -import { setGraphData } from "./redux/graphData"; -import { selectedGraphPaths } from "./redux/graphPaths"; - -const { REACT_APP_API_URL } = process.env; - -export const socket = io.connect(REACT_APP_API_URL, { - reconnection: true, - transports: ["websocket"], -}); - -const SocketConnection = ({ - setNodes, - updateCheckboxes, - setCounts, - setGraphFiles, - setLoading, - selectGraphFile, - addLinks, - setGraphData, - setNodeInfos, - selectedGraphPaths, -}) => { - React.useEffect(() => { - fetch(REACT_APP_API_URL + "/graph_files") - .then((res) => res.json()) - .then((data) => { - setGraphFiles(data.graph_files); - }) - .catch((err) => { - /* eslint-disable no-console */ - console.log("Error Reading data " + err); - }); - - socket.on("other_hash_selected", (incomingData) => { - selectGraphFile({ - hash: incomingData.hash, - selected: incomingData.selected, - }); - }); - - socket.on("graph_nodes", (incomingData) => { - setLoading(false); - setNodes( - incomingData.graphData.nodes.map((node, index) => { - return { - id: index, - node: node.id, - name: node.name, - check: "checkbox", - selected: false, - }; - }) - ); - addLinks(incomingData.graphData.links); - }); - - socket.on("graph_data", (incomingData) => { - if (incomingData.graphData) { - setGraphData(incomingData.graphData); - } - if (incomingData.selectedNodes) { - updateCheckboxes( - incomingData.selectedNodes.map((node, index) => { - return { node: node, value: true }; - }) - ); - } - }); - - socket.on("graph_results", (incomingData) => { - setCounts(incomingData); - }); - - socket.on("node_infos", (incomingData) => { - setNodeInfos(incomingData.nodeInfos); - }); - - socket.on("graph_path_results", (incomingData) => { - selectedGraphPaths(incomingData); - }); - }, []); - - return null; -}; - -export default reduxConnect(null, { - setNodes, - updateCheckboxes, - setCounts, - setNodeInfos, - setGraphFiles, - setLoading, - selectGraphFile, - addLinks, - setGraphData, - selectedGraphPaths, -})(SocketConnection); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js index e539ef21292..4ef6b2e74d5 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js @@ -65,6 +65,15 @@ export const initialState = { listSearchTerm: "", }; +export const getCurrentGraphHash = (state) => { + let selectedGraphFiles = state.graphFiles.filter(x => x.selected == true); + let selectedGraph = '0000000'; + if (selectedGraphFiles.length > 0) { + selectedGraph = selectedGraphFiles[0].git; + } + return selectedGraph; +}; + export const getLoading = (state) => { return { loading: state }; }; @@ -85,6 +94,7 @@ export const getNodeInfos = (state) => { export const getCounts = (state) => { const counts = state.counts; return { + selectedGraph: getCurrentGraphHash(state), counts: state.counts, }; }; @@ -92,16 +102,20 @@ export const getCounts = (state) => { export const getRows = (state) => { let searchedNodes = state.nodes.filter(node => node.node.indexOf(state.listSearchTerm) > -1); return { + selectedGraph: getCurrentGraphHash(state), rowCount: searchedNodes.length, rowGetter: ({ index }) => searchedNodes[index], checkBox: ({ index }) => searchedNodes[index].selected, - nodes: searchedNodes, + nodes: state.nodes, + searchedNodes: searchedNodes }; }; export const getSelected = (state) => { return { + selectedGraph: getCurrentGraphHash(state), selectedNodes: state.nodes.filter((node) => node.selected), + nodes: state.nodes, selectedEdges: [], loading: state.loading, graphPaths: state.graphPaths, @@ -110,6 +124,7 @@ export const getSelected = (state) => { export const getNodes = (state) => { return { + selectedGraph: getCurrentGraphHash(state), nodes: state.nodes, loading: state.loading, listSearchTerm: state.listSearchTerm, @@ -119,6 +134,7 @@ export const getNodes = (state) => { export const getGraphData = (state) => { return { + selectedGraph: getCurrentGraphHash(state), nodes: state.nodes, graphData: state.graphData, loading: state.loading, diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/setupProxy.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/setupProxy.js index d3b4e0dc9ed..f9c1a14588f 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/setupProxy.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/setupProxy.js @@ -7,11 +7,10 @@ const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function(app) { app.use( - createProxyMiddleware('/socket.io', { + createProxyMiddleware('/api', { target: 'http://localhost:5000', - ws: true, changeOrigin: true, - secure: false + secure: false, }) ); };
\ No newline at end of file |