summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Moody <daniel.moody@mongodb.com>2021-02-25 21:25:11 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2021-02-26 07:48:36 +0000
commit27aecefac7cb33c82c831126f646907a2b567253 (patch)
treedc7cb748a8c805bab5104d1fb8bc12b1a31f3c5f
parentdac6c3ae77b6dfa8150a3827ea39f15ec0883373 (diff)
downloadmongo-27aecefac7cb33c82c831126f646907a2b567253.tar.gz
SERVER-52576 Added libdeps graph visualizer web service with basic features.
-rw-r--r--.gitignore5
-rw-r--r--buildscripts/libdeps/README.md74
-rwxr-xr-xbuildscripts/libdeps/gacli.py63
-rw-r--r--buildscripts/libdeps/graph_visualizer.py197
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py263
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/package.json56
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/public/favicon.icobin0 -> 7851 bytes
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/public/index.html40
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/public/manifest.json15
-rwxr-xr-xbuildscripts/libdeps/graph_visualizer_web_stack/setup_node_env.sh49
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/App.js57
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js186
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js194
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js79
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js65
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js52
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js59
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js107
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/LoadingBar.js25
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/NodeInfo.js187
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js43
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js73
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/SwitchComponent.js4
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js100
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/index.js21
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/counts.js16
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/findNode.js16
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphData.js16
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphFiles.js30
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/links.js16
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/loading.js16
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodeInfo.js18
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodes.js66
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js130
-rw-r--r--buildscripts/libdeps/graph_visualizer_web_stack/src/theme.js22
-rw-r--r--buildscripts/libdeps/libdeps/analyzer.py (renamed from buildscripts/libdeps/graph_analyzer.py)26
-rw-r--r--buildscripts/libdeps/libdeps/graph.py (renamed from buildscripts/libdeps/libdeps_graph_enums.py)15
-rw-r--r--etc/pip/components/compile.req2
-rw-r--r--etc/pip/components/external_auth.req2
-rw-r--r--etc/pip/components/libdeps.req8
-rw-r--r--etc/pip/libdeps-requirements.txt2
-rw-r--r--site_scons/libdeps_next.py6
42 files changed, 2366 insertions, 55 deletions
diff --git a/.gitignore b/.gitignore
index 7cb6d71c0d0..aacba7ef7d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@
*.*sdf
*.psess
*.tmp
+*.eslintcache
*#
.#*
@@ -55,6 +56,10 @@
/src/third_party/*/*.cache
/src/third_party/*/*.tlog
/src/third_party/*/*.lastbuildstate
+/buildscripts/libdeps/graph_visualizer_web_stack/build
+/buildscripts/libdeps/graph_visualizer_web_stack/node_modules
+package-lock.json
+libdeps.graphml
config.log
settings.py
log_config.py
diff --git a/buildscripts/libdeps/README.md b/buildscripts/libdeps/README.md
new file mode 100644
index 00000000000..a0fd283496e
--- /dev/null
+++ b/buildscripts/libdeps/README.md
@@ -0,0 +1,74 @@
+# Libdeps Graph Analysis Tools
+
+The Libdeps Graph analysis tools perform analysis and queries on graph representing the libdeps dependencies in the mongodb server builds.
+
+## Generating the graph file
+
+The scons build can create the graph files for analysis. To build the graphml file run the build with this minimal set of args required:
+
+ python3 buildscripts/scons.py --link-model=dynamic --build-tools=next generate-libdeps-graph
+
+The target `generate-libdeps-graph` has special meaning and will turn on extra build items to generate the graph. This target will build everything so that the graph is fully representative of the build. The graph file by default will be found at `build/opt/libdeps/libdeps.graphml` (where `build/opt` is the `$BUILD_DIR`).
+
+## Command Line Tool
+
+The Command Line tool will process a single graph file based off a list of input args. To see the full list of args run the command:
+
+ python3 buildscripts/libdeps/gacli.py --help
+
+By default it will performs some basic operations and print the output in human readable format:
+
+ python3.8 buildscripts/libdeps/gacli.py --graph-file build/opt/libdeps/libdeps.graphml
+
+Which will give an output similar to this:
+
+ Loading graph data...Loaded!
+
+ Graph built from git hash:
+ 19da729e2696bbf15d3a35c340281e4385069b88
+
+ Graph Schema version:
+ 1
+
+ Build invocation:
+ "/usr/bin/python3.8" "buildscripts/scons.py" "--variables-files=etc/scons/mongodbtoolchain_v3_gcc.vars" "--dbg=on" "--opt=on" "--enable-free-mon=on" "--enable-http-client=on" "--cache=all" "--cache-dir=/home/ubuntu/scons-cache" "--install-action=hardlink" "--link-model=dynamic" "--build-tools=next" "--ssl" "--modules=enterprise" "CCACHE=ccache" "ICECC=icecc" "-j50" "generate-libdeps-graph"
+
+ Nodes in Graph: 859
+ Edges in Graph: 90843
+ Direct Edges in Graph: 5808
+ Transitive Edges in Graph: 85035
+ Direct Public Edges in Graph: 3511
+ Public Edges in Graph: 88546
+ Private Edges in Graph: 2272
+ Interface Edges in Graph: 25
+ Shim Nodes in Graph: 20
+ Program Nodes in Graph: 134
+ Library Nodes in Graph: 725
+
+ LibdepsLinter: PUBLIC libdeps that could be PRIVATE: 0
+
+## Graph Visualizer Tool
+
+The graph visualizer tools starts up a web service to provide a frontend GUI to navigating and examining the graph files. The Visualizer used a Python Flask backend and React Javascript frontend. You will need to install the libdeps requirements file to python to run the backend:
+
+ python3 -m pip install -r etc/pip/libdeps-requirements.txt
+
+For installing the dependencies for the frontend, you will need node >= 12.0.0 and npm installed and in the PATH. To install the dependencies navigate to directory where package.json lives, and run:
+
+ cd buildscripts/libdeps/graph_visualizer_web_stack && npm install
+
+Alternatively if you are on linux, you can use the setup_node_env.sh script to automatically download node 12 and npm, setup the local environment and install the dependencies. Run the command:
+
+ buildscripts/libdeps/graph_visualizer_web_stack/setup_node_end.sh install
+
+Assuming you are on a remote workstation and using defaults, you will need to make ssh tunnels to the web service to access the service in your local browser. The frontend and backend both use a port (this case 3000 is the frontend and 5000 is the backend), and the default host is localhost, so you will need to open two tunnels so the frontend running in your local web browser can communicate with the backend. If you are using the default host and port the tunnel command will look like this:
+
+ ssh -L 3000:localhost:3000 -L 5000:localhost:5000 ubuntu@workstation.hostname
+
+Next we need to start the web service. It will require you to pass a directory where it will search for `.graphml` files which contain the graph data for various commits:
+
+ python3 buildscripts/libdeps/graph_visualizer.py --graphml-dir build/opt/libdeps
+
+The script will download nodejs, use npm to install all required packages, launch the backend and then build the optimized production frontend. You can supply the `--debug` argument to work in development load which allows real time updates as files are modified.
+
+After the server has started up, it should notify you via the terminal that you can access it at http://localhost:3000 locally in your browser.
diff --git a/buildscripts/libdeps/gacli.py b/buildscripts/libdeps/gacli.py
index 3c4660604ba..23168518853 100755
--- a/buildscripts/libdeps/gacli.py
+++ b/buildscripts/libdeps/gacli.py
@@ -33,8 +33,8 @@ import sys
from pathlib import Path
import networkx
-import graph_analyzer
-from libdeps_graph_enums import CountTypes, LinterTypes
+import libdeps.analyzer
+from libdeps.graph import CountTypes, LinterTypes
class LinterSplitArgs(argparse.Action):
@@ -50,6 +50,7 @@ class LinterSplitArgs(argparse.Action):
if invalid_choices:
raise Exception(
f"Invalid choices: {invalid_choices}\nMust use choices from {self.valid_choices}")
+
if 'all' in selected_choices or selected_choices == []:
selected_choices = self.valid_choices
selected_choices.remove('all')
@@ -74,38 +75,38 @@ class CustomFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHe
@staticmethod
def _get_help_length(enum_type):
max_length = max([len(name[0]) for name in enum_type.__members__.items()])
- count_help = {}
+ help_text = {}
for name in enum_type.__members__.items():
- count_help[name[0]] = name[0] + ('-' * (max_length - len(name[0]))) + ": "
- return count_help
+ help_text[name[0]] = name[0] + ('-' * (max_length - len(name[0]))) + ": "
+ return help_text
def _get_help_string(self, action):
if isinstance(action, CountSplitArgs):
- count_help = self._get_help_length(CountTypes)
+ help_text = self._get_help_length(CountTypes)
return textwrap.dedent(f"""\
{action.help}
default: all, choices:
- {count_help[CountTypes.all.name]}perform all counts
- {count_help[CountTypes.node.name]}count nodes
- {count_help[CountTypes.edge.name]}count edges
- {count_help[CountTypes.dir_edge.name]}count edges declared directly on a node
- {count_help[CountTypes.trans_edge.name]}count edges induced by direct public edges
- {count_help[CountTypes.dir_pub_edge.name]}count edges that are directly public
- {count_help[CountTypes.pub_edge.name]}count edges that are public
- {count_help[CountTypes.priv_edge.name]}count edges that are private
- {count_help[CountTypes.if_edge.name]}count edges that are interface
- {count_help[CountTypes.shim.name]}count shim nodes
- {count_help[CountTypes.lib.name]}count library nodes
- {count_help[CountTypes.prog.name]}count program nodes
+ {help_text[CountTypes.all.name]}perform all counts
+ {help_text[CountTypes.node.name]}count nodes
+ {help_text[CountTypes.edge.name]}count edges
+ {help_text[CountTypes.dir_edge.name]}count edges declared directly on a node
+ {help_text[CountTypes.trans_edge.name]}count edges induced by direct public edges
+ {help_text[CountTypes.dir_pub_edge.name]}count edges that are directly public
+ {help_text[CountTypes.pub_edge.name]}count edges that are public
+ {help_text[CountTypes.priv_edge.name]}count edges that are private
+ {help_text[CountTypes.if_edge.name]}count edges that are interface
+ {help_text[CountTypes.shim.name]}count shim nodes
+ {help_text[CountTypes.lib.name]}count library nodes
+ {help_text[CountTypes.prog.name]}count program nodes
""")
elif isinstance(action, LintSplitArgs):
- count_help = self._get_help_length(LinterTypes)
+ help_text = self._get_help_length(LinterTypes)
return textwrap.dedent(f"""\
{action.help}
default: all, choices:
- {count_help[LinterTypes.all.name]}perform all linters
- {count_help[LinterTypes.node.name]}find unnecessary public libdeps
+ {help_text[LinterTypes.all.name]}perform all linters
+ {help_text[LinterTypes.public_unused.name]}find unnecessary public libdeps
""")
return super()._get_help_string(action)
@@ -165,30 +166,30 @@ def main():
args = setup_args_parser()
graph = load_graph_data(args.graph_file, args.format)
- libdeps = graph_analyzer.LibdepsGraph(graph)
+ libdeps_graph = libdeps.graph.LibdepsGraph(graph)
- analysis = graph_analyzer.counter_factory(libdeps, args.counts)
+ analysis = libdeps.analyzer.counter_factory(libdeps_graph, args.counts)
for depends in args.direct_depends:
- analysis.append(graph_analyzer.DirectDependencies(libdeps, depends))
+ analysis.append(libdeps.analyzer.DirectDependencies(libdeps_graph, depends))
for depends in args.common_depends:
- analysis.append(graph_analyzer.CommonDependencies(libdeps, depends))
+ analysis.append(libdeps.analyzer.CommonDependencies(libdeps_graph, depends))
for depends in args.exclude_depends:
- analysis.append(graph_analyzer.ExcludeDependencies(libdeps, depends))
+ analysis.append(libdeps.analyzer.ExcludeDependencies(libdeps_graph, depends))
- analysis += graph_analyzer.linter_factory(libdeps, args.lint)
+ analysis += libdeps.analyzer.linter_factory(libdeps_graph, args.lint)
if args.build_data:
- analysis.append(graph_analyzer.BuildDataReport(libdeps))
+ analysis.append(libdeps.analyzer.BuildDataReport(libdeps_graph))
- ga = graph_analyzer.LibdepsGraphAnalysis(libdeps_graph=libdeps, analysis=analysis)
+ ga = libdeps.analyzer.LibdepsGraphAnalysis(libdeps_graph=libdeps_graph, analysis=analysis)
if args.format == 'pretty':
- ga_printer = graph_analyzer.GaPrettyPrinter(ga)
+ ga_printer = libdeps.analyzer.GaPrettyPrinter(ga)
elif args.format == 'json':
- ga_printer = graph_analyzer.GaJsonPrinter(ga)
+ ga_printer = libdeps.analyzer.GaJsonPrinter(ga)
else:
return
diff --git a/buildscripts/libdeps/graph_visualizer.py b/buildscripts/libdeps/graph_visualizer.py
new file mode 100644
index 00000000000..510edf8d97c
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 MongoDB Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+"""
+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.
+
+This script will automatically install the npm modules, and build and run the production
+web service if not debug.
+"""
+
+import os
+from pathlib import Path
+import argparse
+import shutil
+import subprocess
+import platform
+import threading
+import copy
+import textwrap
+
+import flask
+from graph_visualizer_web_stack.flask.flask_backend import BackendServer
+
+
+def get_args():
+ """Create the argparse and return passed args."""
+
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument(
+ '--debug', action='store_true', help=
+ 'Whether or not to run debug server. Note for non-debug, you must build the production frontend with "npm run build".'
+ )
+ parser.add_argument(
+ '--graphml-dir', type=str, action='store', help=
+ "Directory where libdeps graphml files live. The UI will allow selecting different graphs from this location",
+ default="build/opt")
+
+ parser.add_argument('--frontend-host', type=str, action='store',
+ help="Hostname where the front end will run.", default="localhost")
+
+ parser.add_argument('--backend-host', type=str, action='store',
+ help="Hostname where the back end will run.", default="localhost")
+
+ parser.add_argument('--frontend-port', type=str, action='store',
+ help="Port where the front end will run.", default="3000")
+
+ parser.add_argument('--backend-port', type=str, action='store',
+ help="Port where the back end will run.", default="5000")
+
+ parser.add_argument('--launch', choices=['frontend', 'backend', 'both'], default='both',
+ help="Specifies which part of the web service to launch.")
+
+ return parser.parse_args()
+
+
+def execute_and_read_stdout(cmd, cwd, env):
+ """Execute passed command and get realtime output."""
+
+ popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=str(cwd), env=env,
+ universal_newlines=True)
+ for stdout_line in iter(popen.stdout.readline, ""):
+ yield stdout_line
+ popen.stdout.close()
+ return_code = popen.wait()
+ if return_code:
+ raise subprocess.CalledProcessError(return_code, cmd)
+
+
+def check_node(node_check, cwd):
+ """Check node version and install npm packages."""
+
+ status, output = subprocess.getstatusoutput(node_check)
+ if status != 0 or not output.split('\n')[-1].startswith('v12'):
+ print(
+ textwrap.dedent(f"""\
+ Failed to get node version 12 from 'node -v':
+ output: '{output}'
+ Perhaps run 'source {cwd}/setup_node_env.sh install'"""))
+ exit(1)
+
+ node_modules = cwd / 'node_modules'
+
+ if not node_modules.exists():
+ print(f"{node_modules} not found, you need to run 'npm install' in {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)
+
+
+def start_frontend_thread(web_service_info, npm_command, debug):
+ """Start the backend in debug mode."""
+ env = os.environ.copy()
+ backend_url = f"http://{web_service_info['backend_host']}:{web_service_info['backend_port']}"
+ env['REACT_APP_API_URL'] = backend_url
+
+ if debug:
+ env['HOST'] = web_service_info['frontend_host']
+ env['PORT'] = web_service_info['frontend_port']
+
+ for output in execute_and_read_stdout(npm_command, cwd=web_service_info['cwd'], env=env):
+ print(output, end="")
+ else:
+ for output in execute_and_read_stdout(npm_command, cwd=web_service_info['cwd'], env=env):
+ print(output, end="")
+
+ env['PATH'] = 'node_modules/.bin:' + env['PATH']
+ react_frontend = subprocess.Popen([
+ 'http-server',
+ 'build',
+ '-a',
+ web_service_info['frontend_host'],
+ '-p',
+ web_service_info['frontend_port'],
+ f"--cors={backend_url}",
+ ], env=env, cwd=str(web_service_info['cwd']))
+ stdout, stderr = react_frontend.communicate()
+ print(f"frontend stdout: '{stdout}'\n\nfrontend stderr: '{stderr}'")
+
+
+def main():
+ """Start up the server."""
+
+ args = get_args()
+
+ # TODO: add https command line option and support
+ server = BackendServer(graphml_dir=args.graphml_dir,
+ frontend_url=f"http://{args.frontend_host}:{args.frontend_port}")
+
+ app, socketio = 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,
+ 'backend_host': args.backend_host,
+ 'backend_port': args.backend_port,
+ }
+
+ node_check = 'node -v'
+ npm_start = ['npm', 'start']
+ npm_build = ['npm', 'run', 'build']
+
+ check_node(node_check, cwd)
+
+ frontend_thread = None
+ if args.launch in ['frontend', 'both']:
+ if args.debug:
+ npm_command = npm_start
+ else:
+ npm_command = npm_build
+
+ frontend_thread = threading.Thread(target=start_frontend_thread,
+ args=(web_service_info, npm_command, args.debug))
+ frontend_thread.start()
+
+ if args.launch in ['backend', 'both']:
+ start_backend(web_service_info, args.debug)
+
+ if frontend_thread:
+ frontend_thread.join()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py b/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py
new file mode 100644
index 00000000000..333e714d8dc
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 MongoDB Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+"""
+Flask backend web server.
+
+The backend interacts with the graph_analyzer to perform queries on various libdeps graphs.
+"""
+
+from pathlib import Path
+from collections import namedtuple
+
+import flask
+import networkx
+
+from flask_socketio import SocketIO, emit
+from flask_cors import CORS
+from flask_session import Session
+from lxml import etree
+
+import libdeps.graph
+import libdeps.analyzer
+
+
+class BackendServer:
+ """Create small class for storing variables and state of the backend."""
+
+ # pylint: disable=too-many-instance-attributes
+ 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.current_graph = networkx.DiGraph()
+ 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()
+
+ def get_app(self):
+ """Return the app and socketio instances."""
+
+ return self.app, self.socketio
+
+ def get_graph_build_data(self, graph_file):
+ """Fast method for extracting basic build data from the graph file."""
+
+ version = ''
+ git_hash = ''
+ # pylint: disable=c-extension-no-member
+ for _, element in etree.iterparse(
+ str(graph_file), tag="{http://graphml.graphdrawing.org/xmlns}data"):
+ if element.get('key') == 'graph_schema_version':
+ version = element.text
+ if element.get('key') == 'git_hash':
+ git_hash = element.text
+ element.clear()
+ if version and git_hash:
+ break
+ return self.graph_file_tuple(version, git_hash, graph_file)
+
+ def get_graphml_files(self):
+ """Find all graphml files in the target graphml dir."""
+
+ graph_files = {}
+ for graph_file in self.graphml_dir.glob("**/*.graphml"):
+ graph_file_tuple = self.get_graph_build_data(graph_file)
+ graph_files[graph_file_tuple.git_hash[:7]] = graph_file_tuple
+ return graph_files
+
+ def return_graph_files(self):
+ """Prepare the list of graph files for the frontend."""
+
+ data = {'graph_files': []}
+ for i, (_, graph_file_data) in enumerate(self.graph_files.items(), start=1):
+ data['graph_files'].append({
+ 'id': i, 'version': graph_file_data.version, 'git': graph_file_data.git_hash[:7],
+ 'selected': False
+ })
+ return data
+
+ def send_node_infos(self):
+ """Search through the selected rows and find information about the selected rows."""
+
+ with self.app.test_request_context():
+
+ 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.current_graph.nodes(data=True)[str(node)].items()],
+ 'dependers': [{
+ 'node':
+ depender, 'symbols':
+ self.current_graph[str(node)][depender].get('symbols',
+ '').split(' ')
+ } for depender in self.current_graph[str(node)]],
+ 'dependencies': [{
+ 'node':
+ dependency, 'symbols':
+ self.current_graph[dependency][str(node)].get('symbols',
+ '').split(' ')
+ } for dependency in self.current_graph.rgraph[str(node)]],
+ })
+
+ self.socketio.emit("node_infos", nodeinfo_data)
+
+ def send_graph_data(self):
+ """Convert the current selected rows into a format for D3."""
+
+ with self.app.test_request_context():
+
+ nodes = set()
+ links = set()
+
+ for node, _ in self.current_selected_rows.items():
+ nodes.add(
+ tuple({
+ 'id': str(node), 'name': node.name, 'type': self.current_graph.nodes()
+ [str(node)]['bin_type']
+ }.items()))
+
+ for depender in self.current_graph.rgraph[str(node)]:
+
+ depender_path = Path(depender)
+ if self.current_graph[depender][str(node)].get('direct'):
+ nodes.add(
+ tuple({
+ 'id': str(depender_path), 'name': depender_path.name,
+ 'type': self.current_graph.nodes()[str(depender_path)]['bin_type']
+ }.items()))
+ links.add(
+ tuple({'source': str(node), 'target': str(depender_path)}.items()))
+
+ node_data = {
+ 'graphData': {
+ 'nodes': [dict(node) for node in nodes],
+ 'links': [dict(link) for link in links],
+ }, '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."""
+
+ print(f"Got row {message}!")
+
+ if message['isSelected'] == 'flip':
+ if message['data']['node'] in self.current_selected_rows:
+ self.current_selected_rows.pop(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(message['data']['node'])
+
+ self.socketio.start_background_task(self.send_graph_data)
+ self.socketio.start_background_task(self.send_node_infos)
+
+ def analyze_counts(self):
+ """Perform count analysis and send the results back to frontend."""
+
+ with self.app.test_request_context():
+
+ analysis = libdeps.analyzer.counter_factory(
+ self.current_graph,
+ [name[0] for name in libdeps.analyzer.CountTypes.__members__.items()])
+ ga = libdeps.analyzer.LibdepsGraphAnalysis(libdeps_graph=self.current_graph,
+ analysis=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 send_node_list(self):
+ """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 self.current_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."""
+
+ with self.app.test_request_context():
+
+ current_hash = self.current_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.current_graph = self.loaded_graphs[message['hash']]
+ else:
+ print(
+ f'loading new graph {current_hash} because different than {message["hash"]}'
+ )
+
+ graph = networkx.read_graphml(self.graph_files[message['hash']].graph_file)
+
+ self.current_graph = libdeps.graph.LibdepsGraph(graph)
+ self.loaded_graphs[message['hash']] = self.current_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."""
+
+ print(f"Got requests2 {message}!")
+
+ emit("other_hash_selected", message, broadcast=True)
+
+ self.socketio.start_background_task(self.load_graph, message)
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/package.json b/buildscripts/libdeps/graph_visualizer_web_stack/package.json
new file mode 100644
index 00000000000..3a69d02752e
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "graph_visualizer",
+ "version": "4.0.0",
+ "private": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "engineStrict": true,
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "start-flask": "cd flask && flask run --no-debugger",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.1.4",
+ "@emotion/styled": "^11.0.0",
+ "@material-ui/core": "^5.0.0-alpha.1",
+ "@material-ui/icons": "^5.0.0-alpha.1",
+ "@material-ui/lab": "^5.0.0-alpha.1",
+ "canvas": "^2.5.0",
+ "date-fns": "^2.16.1",
+ "dayjs": "^1.9.7",
+ "http-server": "^0.12.3",
+ "luxon": "^1.25.0",
+ "moment": "^2.29.1",
+ "p-limit": "^3.0.2",
+ "react": "^16.8",
+ "react-dom": "^16.0.0",
+ "react-force-graph-2d": "^1.18.1",
+ "react-force-graph-3d": "^1.18.8",
+ "react-indiana-drag-scroll": "^1.8.0",
+ "react-redux": "^7.2.2",
+ "react-scripts": "latest",
+ "react-split-pane": "^0.1.92",
+ "react-virtualized": "^9.22.2",
+ "react-window": "^1.8.6",
+ "redux": "^4.0.5",
+ "socket.io-client": "latest",
+ "typescript": "^3.9.7"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "proxy": "http://localhost:5000"
+}
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/public/favicon.ico b/buildscripts/libdeps/graph_visualizer_web_stack/public/favicon.ico
new file mode 100644
index 00000000000..9484edb2f18
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/public/favicon.ico
Binary files differ
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/public/index.html b/buildscripts/libdeps/graph_visualizer_web_stack/public/index.html
new file mode 100644
index 00000000000..593946a5ba0
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/public/index.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
+ <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
+ <meta name="theme-color" content="#000000" />
+ <!--
+ manifest.json provides metadata used when your web app is installed on a
+ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+ -->
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
+ <!--
+ Notice the use of %PUBLIC_URL% in the tags above.
+ It will be replaced with the URL of the `public` folder during the build.
+ Only files inside the `public` folder can be referenced from the HTML.
+
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+ work correctly both with client-side routing and a non-root public URL.
+ Learn how to configure a non-root public URL by running `npm run build`.
+ -->
+ <title>Libdeps Graph</title>
+ <!-- Fonts to support Material Design -->
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
+ </head>
+ <body>
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root"></div>
+ <!--
+ This HTML file is a template.
+ If you open it directly in the browser, you will see an empty page.
+
+ You can add webfonts, meta tags, or analytics to this file.
+ The build step will place the bundled scripts into the <body> tag.
+
+ To begin the development, run `npm start` or `yarn start`.
+ To create a production bundle, use `npm run build` or `yarn build`.
+ -->
+ </body>
+</html>
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/public/manifest.json b/buildscripts/libdeps/graph_visualizer_web_stack/public/manifest.json
new file mode 100644
index 00000000000..d4885e3d415
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Libdeps Graph",
+ "name": "Libdeps Graph Visualizer Service",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/setup_node_env.sh b/buildscripts/libdeps/graph_visualizer_web_stack/setup_node_env.sh
new file mode 100755
index 00000000000..22c2d2295cf
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/setup_node_env.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+SCRIPTPATH="$( cd "$(dirname "$BASH_SOURCE")" >/dev/null 2>&1 ; pwd -P )"
+pushd $SCRIPTPATH > /dev/null
+
+function quit {
+ popd > /dev/null
+}
+trap quit EXIT
+trap quit SIGINT
+trap quit SIGTERM
+
+export NVM_DIR="$HOME/.nvm"
+if [ -s "$NVM_DIR/nvm.sh" ]
+then
+ \. "$NVM_DIR/nvm.sh"
+else
+ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | sh
+ \. "$NVM_DIR/nvm.sh"
+fi
+
+nvm install 12
+
+if [ "$1" = "install" ]
+then
+ npm install
+fi
+
+if [ "$1" = "start" ]
+then
+ npm start
+fi
+
+if [ "$1" = "build" ]
+then
+ npm run build
+fi
+
+if [ "$1" = "update" ]
+then
+ set -u
+ git -C "$NVM_DIR" fetch --tags
+ TAG=$(git -C "$NVM_DIR" describe --tags `git -C "$NVM_DIR" rev-list --tags --max-count=1`)
+ echo "Checking out tag $TAG..."
+ git -C "$NVM_DIR" checkout "$TAG"
+
+ . "$NVM_DIR/nvm.sh"
+fi
+popd > /dev/null \ No newline at end of file
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/App.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/App.js
new file mode 100644
index 00000000000..26f2e6fa074
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/App.js
@@ -0,0 +1,57 @@
+import React from "react";
+import SplitPane from "react-split-pane";
+
+import theme from "./theme";
+
+import GraphCommitDisplay from "./GraphCommitDisplay";
+import GraphInfoTabs from "./GraphInfoTabs";
+import DrawGraph from "./DrawGraph";
+
+const resizerStyle = {
+ background: theme.palette.text.secondary,
+ width: "1px",
+ cursor: "col-resize",
+ margin: "1px",
+ padding: "1px",
+ height: "100%",
+};
+
+const topPaneStyle = {
+ height: "100vh",
+ overflow: "visible",
+};
+
+export default function App() {
+ const [infosize, setInfosize] = React.useState(450);
+ const [drawsize, setDrawsize] = React.useState(
+ window.screen.width - infosize
+ );
+
+ React.useEffect(() => {
+ setInfosize(window.screen.width - drawsize);
+ }, [drawsize]);
+
+ return (
+ <SplitPane
+ pane1Style={{ height: "12%" }}
+ pane2Style={{ height: "88%" }}
+ split="horizontal"
+ style={topPaneStyle}
+ >
+ <GraphCommitDisplay />
+ <SplitPane
+ split="vertical"
+ minSize={100}
+ style={{ position: "relative" }}
+ defaultSize={infosize}
+ pane1Style={{ height: "100%" }}
+ pane2Style={{ height: "100%", width: "100%" }}
+ resizerStyle={resizerStyle}
+ onChange={(size) => setDrawsize(window.screen.width - size)}
+ >
+ <GraphInfoTabs width={infosize} />
+ <DrawGraph size={drawsize} />
+ </SplitPane>
+ </SplitPane>
+ );
+}
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js
new file mode 100644
index 00000000000..d1da8c4cc0d
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js
@@ -0,0 +1,186 @@
+import React from "react";
+import { connect } from "react-redux";
+import clsx from "clsx";
+import { AutoSizer, Column, Table } from "react-virtualized";
+import "react-virtualized/styles.css"; // only needs to be imported once
+import { withStyles } from "@material-ui/core/styles";
+import TableCell from "@material-ui/core/TableCell";
+import { Checkbox } from "@material-ui/core";
+import Typography from "@material-ui/core/Typography";
+
+import { getRows } from "./redux/store";
+import { updateSelected } from "./redux/nodes";
+import { socket } from "./connect";
+
+const styles = (theme) => ({
+ flexContainer: {
+ display: "flex",
+ alignItems: "center",
+ },
+ table: {
+ // temporary right-to-left patch, waiting for
+ // https://github.com/bvaughn/react-virtualized/issues/454
+ "& .ReactVirtualized__Table__headerRow": {
+ flip: false,
+ paddingRight: theme.direction === "rtl" ? "0 !important" : undefined,
+ },
+ },
+ tableRowOdd: {
+ backgroundColor: "#4d4d4d",
+ },
+ tableRowEven: {},
+ tableRowHover: {
+ "&:hover": {
+ backgroundColor: theme.palette.grey[700],
+ },
+ },
+ tableCell: {
+ flex: 1,
+ },
+ noClick: {
+ cursor: "initial",
+ },
+});
+
+const DataGrid = ({
+ rowGetter,
+ rowCount,
+ nodes,
+ rowHeight,
+ headerHeight,
+ columns,
+ onNodeClicked,
+ updateSelected,
+ classes,
+}) => {
+ const [checkBoxes, setCheckBoxes] = React.useState([]);
+
+ React.useEffect(() => {
+ setCheckBoxes(nodes);
+ }, [nodes]);
+
+ const getRowClassName = ({ index }) => {
+ return clsx(
+ index % 2 == 0 ? classes.tableRowEven : classes.tableRowOdd,
+ classes.flexContainer,
+ {
+ [classes.tableRowHover]: index !== -1,
+ }
+ );
+ };
+
+ const cellRenderer = ({ cellData, columnIndex, rowIndex }) => {
+ var finalCellData;
+ var style = { height: rowHeight, padding: "0px" };
+ if (cellData == "checkbox") {
+ style["justifyContent"] = "space-evenly";
+ finalCellData = (
+ <Checkbox
+ checked={checkBoxes[rowIndex].selected}
+ onChange={(event) => {
+ setCheckBoxes(
+ checkBoxes.map((checkbox, index) => {
+ if (index == rowIndex) {
+ checkbox.selected = event.target.checked;
+ }
+ return checkbox;
+ })
+ );
+ 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,
+ });
+ }}
+ />
+ );
+ } else {
+ finalCellData = cellData;
+ }
+
+ return (
+ <TableCell
+ component="div"
+ className={clsx(
+ classes.tableCell,
+ classes.flexContainer,
+ classes.noClick
+ )}
+ variant="body"
+ onClick={onNodeClicked}
+ style={style}
+ >
+ {finalCellData}
+ </TableCell>
+ );
+ };
+
+ const headerRenderer = ({ label, columnIndex }) => {
+ return (
+ <TableCell
+ component="div"
+ className={clsx(
+ classes.tableCell,
+ classes.flexContainer,
+ classes.noClick
+ )}
+ variant="head"
+ style={{ height: headerHeight, padding: "0px" }}
+ >
+ <Typography
+ style={{ width: "100%" }}
+ align="left"
+ variant="caption"
+ component="h2"
+ >
+ {label}
+ </Typography>
+ </TableCell>
+ );
+ };
+
+ return (
+ <AutoSizer>
+ {({ height, width }) => (
+ <Table
+ height={height}
+ width={width}
+ rowCount={rowCount}
+ rowHeight={rowHeight}
+ gridStyle={{
+ direction: "inherit",
+ }}
+ size={"small"}
+ rowGetter={rowGetter}
+ className={clsx(classes.table, classes.noClick)}
+ rowClassName={getRowClassName}
+ headerHeight={headerHeight}
+ >
+ {columns.map(({ dataKey, ...other }, index) => {
+ return (
+ <Column
+ key={dataKey}
+ headerRenderer={(headerProps) =>
+ headerRenderer({
+ ...headerProps,
+ columnIndex: index,
+ })
+ }
+ className={classes.flexContainer}
+ cellRenderer={cellRenderer}
+ dataKey={dataKey}
+ {...other}
+ />
+ );
+ })}
+ </Table>
+ )}
+ </AutoSizer>
+ );
+};
+
+export default connect(getRows, { updateSelected })(
+ 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
new file mode 100644
index 00000000000..672bdaa41c3
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js
@@ -0,0 +1,194 @@
+import React, { useRef, useEffect } from "react";
+import * as THREE from "three";
+import { connect } from "react-redux";
+import ForceGraph2D from "react-force-graph-2d";
+import ForceGraph3D from "react-force-graph-3d";
+import SwitchComponents from "./SwitchComponent";
+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 LoadingBar from "./LoadingBar";
+
+const handleFindNode = (node_value, graphData, activeComponent, forceRef) => {
+ var targetNode = null;
+ if (graphData) {
+ for (var i = 0; i < graphData.nodes.length; i++) {
+ var node = graphData.nodes[i];
+ if (node.name == node_value || node.id == node_value) {
+ targetNode = node;
+ break;
+ }
+ }
+ if (targetNode != null) {
+ if (activeComponent == "3D") {
+ if (forceRef.current != null) {
+ forceRef.current.centerAt(targetNode.x, targetNode.y, 2000);
+ forceRef.current.zoom(6, 1000);
+ }
+ } else {
+ const distance = 100;
+ const distRatio =
+ 1 + distance / Math.hypot(targetNode.x, targetNode.y, targetNode.z);
+ if (forceRef.current != null) {
+ forceRef.current.cameraPosition(
+ {
+ x: targetNode.x * distRatio,
+ y: targetNode.y * distRatio,
+ z: targetNode.z * distRatio,
+ }, // new position
+ targetNode, // lookAt ({ x, y, z })
+ 3000 // ms transition duration
+ );
+ }
+ }
+ }
+ }
+};
+
+const DrawGraph = ({
+ size,
+ graphData,
+ nodes,
+ loading,
+ findNode,
+ setFindNode,
+}) => {
+ const [activeComponent, setActiveComponent] = React.useState("2D");
+ const [selectedNodes, setSelectedNodes] = React.useState([]);
+ const forceRef = useRef(null);
+
+ React.useEffect(() => {
+ handleFindNode(findNode, graphData, activeComponent, forceRef);
+ setFindNode("");
+ }, [findNode, graphData, activeComponent, forceRef]);
+
+ React.useEffect(() => {
+ setSelectedNodes(
+ nodes.map((node) => {
+ if (node.selected) {
+ return node.node;
+ }
+ })
+ );
+ }, [nodes]);
+
+ React.useEffect(() => {
+ if (forceRef.current != null) {
+ forceRef.current.d3Force("charge").strength(-1400);
+ }
+ }, [forceRef.current]);
+
+ const paintRing = React.useCallback((node, ctx) => {
+ // add ring just for highlighted nodes
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, 4 * 1.4, 0, 2 * Math.PI, false);
+ ctx.fillStyle = "green";
+ ctx.fill();
+ });
+
+ function colorNodes(node) {
+ switch (node.type) {
+ case "SharedLibrary":
+ return "#e6ed11"; // yellow
+ case "Program":
+ return "#1120ed"; // blue
+ case "shim":
+ return "#800303"; // dark red
+ default:
+ return "#5a706f"; // grey
+ }
+ }
+
+ return (
+ <LoadingBar loading={loading} height={"100%"}>
+ <Button
+ onClick={() => {
+ if (activeComponent == "2D") {
+ setActiveComponent("3D");
+ } else {
+ setActiveComponent("2D");
+ }
+ }}
+ >
+ {activeComponent}
+ </Button>
+ <TextField
+ size="small"
+ label="Find Node"
+ onChange={(event) => {
+ handleFindNode(
+ event.target.value,
+ graphData,
+ activeComponent,
+ forceRef
+ );
+ }}
+ />
+ <SwitchComponents active={activeComponent}>
+ <ForceGraph2D
+ name="3D"
+ width={size}
+ dagMode="radialout"
+ graphData={graphData}
+ ref={forceRef}
+ nodeColor={colorNodes}
+ nodeOpacity={1}
+ backgroundColor={theme.palette.secondary.dark}
+ linkDirectionalArrowLength={6}
+ linkDirectionalArrowRelPos={1}
+ nodeCanvasObjectMode={(node) => {
+ if (selectedNodes.includes(node.id)) {
+ return "before";
+ }
+ }}
+ nodeCanvasObject={paintRing}
+ onNodeClick={(node, event) => {
+ updateCheckbox(node.id);
+ socket.emit("row_selected", {
+ data: { node: node.id, name: node.name },
+ isSelected: !selectedNodes.includes(node.id),
+ });
+ }}
+ />
+ <ForceGraph3D
+ name="2D"
+ width={size}
+ dagMode="radialout"
+ graphData={graphData}
+ nodeColor={colorNodes}
+ nodeOpacity={1}
+ nodeThreeObject={(node) => {
+ if (!selectedNodes.includes(node.id)) {
+ return new THREE.Mesh(
+ new THREE.SphereGeometry(5, 5, 5),
+ new THREE.MeshLambertMaterial({
+ color: colorNodes(node),
+ transparent: true,
+ opacity: 0.2,
+ })
+ );
+ }
+ }}
+ onNodeClick={(node, event) => {
+ updateCheckbox(node.id);
+ socket.emit("row_selected", {
+ data: { node: node.id, name: node.name },
+ isSelected: !selectedNodes.includes(node.id),
+ });
+ }}
+ backgroundColor={theme.palette.secondary.dark}
+ linkDirectionalArrowLength={3.5}
+ linkDirectionalArrowRelPos={1}
+ ref={forceRef}
+ />
+ </SwitchComponents>
+ </LoadingBar>
+ );
+};
+
+export default connect(getGraphData, { setFindNode })(DrawGraph);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js
new file mode 100644
index 00000000000..63e8a99427c
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GitHashButton.js
@@ -0,0 +1,79 @@
+import React from "react";
+import { connect } from "react-redux";
+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";
+
+const selectedStyle = {
+ color: theme.palette.getContrastText(green[500]),
+ backgroundColor: green[500],
+ "&:hover": {
+ backgroundColor: green[400],
+ },
+ "&:active": {
+ backgroundColor: green[700],
+ },
+};
+
+const unselectedStyle = {
+ color: theme.palette.getContrastText(grey[100]),
+ backgroundColor: grey[100],
+ "&:hover": {
+ backgroundColor: grey[200],
+ },
+ "&:active": {
+ backgroundColor: grey[400],
+ },
+};
+
+const GitHashButton = ({ loading, graphFiles, setLoading, text }) => {
+ const [selected, setSelected] = React.useState(false);
+ const [selfLoading, setSelfLoading] = React.useState(false);
+ const [firstLoad, setFirstLoad] = React.useState(true);
+
+ function handleClick() {
+ setSelfLoading(true);
+ setLoading(true);
+ socket.emit("git_hash_selected", { hash: text, selected: true });
+ }
+
+ React.useEffect(() => {
+ const selectedGraphFile = graphFiles.filter(
+ (graphFile) => graphFile.git == text
+ );
+ setSelected(selectedGraphFile[0].selected);
+
+ if (firstLoad && graphFiles.length > 0) {
+ if (graphFiles[0]["git"] == text) {
+ handleClick();
+ }
+ setFirstLoad(false);
+ }
+ }, [graphFiles]);
+
+ React.useEffect(() => {
+ if (!loading) {
+ setSelfLoading(false);
+ }
+ }, [loading]);
+
+ return (
+ <LoadingButton
+ pending={selfLoading}
+ pendingPosition="start"
+ startIcon={<GitIcon />}
+ variant="contained"
+ style={selected ? selectedStyle : unselectedStyle}
+ onClick={handleClick}
+ >
+ {text}
+ </LoadingButton>
+ );
+};
+
+export default connect(getGraphFiles, { setLoading })(GitHashButton);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js
new file mode 100644
index 00000000000..adce3295596
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphCommitDisplay.js
@@ -0,0 +1,65 @@
+import React from "react";
+import ScrollContainer from "react-indiana-drag-scroll";
+import { connect } from "react-redux";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import Paper from "@material-ui/core/Paper";
+import TableRow from "@material-ui/core/TableRow";
+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 GitHashButton from "./GitHashButton";
+
+const flexContainer = {
+ display: "flex",
+ flexDirection: "row",
+ padding: 0,
+ width: "50%",
+ height: "50%",
+};
+
+const textFields = [
+ "Scroll to commit",
+ "Commit Range Begin",
+ "Commit Range End",
+];
+
+const GraphCommitDisplay = ({ graphFiles }) => {
+ return (
+ <Paper style={{ height: "100%", width: "100%" }}>
+ <List style={flexContainer}>
+ {textFields.map((text) => (
+ <ListItem key={text}>
+ <TextField size="small" label={text} />
+ </ListItem>
+ ))}
+ </List>
+ <ScrollContainer
+ vertical={false}
+ style={{ height: "50%" }}
+ className="scroll-container"
+ hideScrollbars={true}
+ >
+ <Table style={{ height: "100%" }}>
+ <TableBody>
+ <TableRow>
+ {graphFiles.map((file) => (
+ <TableCell key={file.id}>
+ <GitHashButton text={file.git} />
+ </TableCell>
+ ))}
+ </TableRow>
+ </TableBody>
+ </Table>
+ <SocketConnection />
+ </ScrollContainer>
+ </Paper>
+ );
+};
+
+export default connect(getGraphFiles)(GraphCommitDisplay);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js
new file mode 100644
index 00000000000..626306a1b4f
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfo.js
@@ -0,0 +1,52 @@
+import React from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import Table from "@material-ui/core/Table";
+import TableBody from "@material-ui/core/TableBody";
+import TableCell from "@material-ui/core/TableCell";
+import TableContainer from "@material-ui/core/TableContainer";
+import TableHead from "@material-ui/core/TableHead";
+import TableRow from "@material-ui/core/TableRow";
+import Paper from "@material-ui/core/Paper";
+import { connect } from "react-redux";
+import { getCounts } from "./redux/store";
+
+const columns = [
+ { id: "ID", field: "type", headerName: "Count Type", width: 50 },
+ { field: "value", headerName: "Value", width: 50 },
+];
+
+const useStyles = makeStyles({
+ table: {
+ minWidth: 50,
+ },
+});
+
+const GraphInfo = ({ counts, datawidth }) => {
+ const classes = useStyles();
+
+ return (
+ <TableContainer component={Paper}>
+ <Table className={classes.table} size="small" aria-label="simple table">
+ <TableHead>
+ <TableRow>
+ {columns.map((column, index) => {
+ return <TableCell key={index}>{column.headerName}</TableCell>;
+ })}
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {counts.map((row) => (
+ <TableRow key={row.id}>
+ <TableCell component="th" scope="row">
+ {row.type}
+ </TableCell>
+ <TableCell>{row.value}</TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </TableContainer>
+ );
+};
+
+export default connect(getCounts)(GraphInfo);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js
new file mode 100644
index 00000000000..0cdec3d12b4
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js
@@ -0,0 +1,59 @@
+import React from "react";
+import { makeStyles } from "@material-ui/core/styles";
+import AppBar from "@material-ui/core/AppBar";
+import Tabs from "@material-ui/core/Tabs";
+import Tab from "@material-ui/core/Tab";
+
+import NodeList from "./NodeList";
+import InfoExpander from "./InfoExpander";
+
+function a11yProps(index) {
+ return {
+ id: `scrollable-auto-tab-${index}`,
+ "aria-controls": `scrollable-auto-tabpanel-${index}`,
+ };
+}
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ flexGrow: 1,
+ width: "100%",
+ height: "100%",
+ backgroundColor: theme.palette.background.paper,
+ },
+}));
+
+export default function GraphInfoTabs({ nodes, width }) {
+ const classes = useStyles();
+ const [value, setValue] = React.useState(0);
+
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+
+ return (
+ <div className={classes.root}>
+ <AppBar position="static" color="default">
+ <Tabs
+ value={value}
+ onChange={handleChange}
+ indicatorColor="primary"
+ textColor="primary"
+ variant="scrollable"
+ scrollButtons="auto"
+ aria-label="scrollable auto tabs example"
+ >
+ <Tab label="Selected Info" {...a11yProps(0)} />
+ <Tab label="Node List" {...a11yProps(1)} />
+ <Tab label="Edge List" {...a11yProps(2)} />
+ </Tabs>
+ </AppBar>
+ <div style={{ height: "100%" }} hidden={value != 0}>
+ <InfoExpander width={width}></InfoExpander>
+ </div>
+ <div style={{ height: "100%" }} hidden={value != 1}>
+ <NodeList nodes={nodes}></NodeList>
+ </div>
+ </div>
+ );
+}
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js
new file mode 100644
index 00000000000..8912b1c9d5b
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js
@@ -0,0 +1,107 @@
+import React from "react";
+import { connect } from "react-redux";
+import { makeStyles, withStyles } from "@material-ui/core/styles";
+import Typography from "@material-ui/core/Typography";
+import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
+import Paper from "@material-ui/core/Paper";
+import MuiAccordion from "@material-ui/core/Accordion";
+import MuiAccordionSummary from "@material-ui/core/AccordionSummary";
+import MuiAccordionDetails from "@material-ui/core/AccordionDetails";
+
+import { getSelected } from "./redux/store";
+
+import GraphInfo from "./GraphInfo";
+import NodeInfo from "./NodeInfo";
+import LoadingBar from "./LoadingBar";
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ width: "100%",
+ },
+ heading: {
+ fontSize: theme.typography.pxToRem(15),
+ fontWeight: theme.typography.fontWeightRegular,
+ },
+}));
+
+const Accordion = withStyles({
+ root: {
+ border: "1px solid rgba(0, 0, 0, .125)",
+ boxShadow: "none",
+ "&:not(:last-child)": {
+ borderBottom: 0,
+ },
+ "&:before": {
+ display: "none",
+ },
+ "&$expanded": {
+ margin: "auto",
+ },
+ },
+ expanded: {},
+})(MuiAccordion);
+
+const AccordionSummary = withStyles({
+ root: {
+ backgroundColor: "rgba(0, 0, 0, .03)",
+ borderBottom: "1px solid rgba(0, 0, 0, .125)",
+ marginBottom: -1,
+ minHeight: 56,
+ "&$expanded": {
+ minHeight: 56,
+ },
+ },
+ content: {
+ "&$expanded": {
+ margin: "12px 0",
+ },
+ },
+ expanded: {},
+})(MuiAccordionSummary);
+
+const AccordionDetails = withStyles((theme) => ({
+ root: {
+ padding: theme.spacing(2),
+ },
+}))(MuiAccordionDetails);
+
+const InfoExpander = ({ selectedNodes, selectedEdges, loading, width }) => {
+ const classes = useStyles();
+
+ return (
+ <div className={classes.root}>
+ <LoadingBar loading={loading} height={"100%"}>
+ <Paper style={{ maxHeight: "82vh", overflow: "auto" }}>
+ <Accordion>
+ <AccordionSummary
+ expandIcon={<ExpandMoreIcon />}
+ aria-controls="panel1a-content"
+ id="panel1a-header"
+ >
+ <Typography className={classes.heading}>Counts</Typography>
+ </AccordionSummary>
+ <AccordionDetails>
+ <GraphInfo datawidth={width} />
+ </AccordionDetails>
+ </Accordion>
+ {selectedNodes.map((node) => (
+ <Accordion key={node.node}>
+ <AccordionSummary
+ expandIcon={<ExpandMoreIcon />}
+ aria-controls="panel1a-content"
+ id="panel1a-header"
+ >
+ <Typography className={classes.heading}>{node.name}</Typography>
+ </AccordionSummary>
+ <AccordionDetails>
+ <NodeInfo node={node} width={width} />
+ </AccordionDetails>
+ </Accordion>
+ ))}
+ </Paper>
+ </LoadingBar>
+ </div>
+ );
+};
+
+export default connect(getSelected)(InfoExpander);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/LoadingBar.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/LoadingBar.js
new file mode 100644
index 00000000000..20b4ca1129e
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/LoadingBar.js
@@ -0,0 +1,25 @@
+import React from "react";
+import LinearProgress from "@material-ui/core/LinearProgress";
+import Fade from "@material-ui/core/Fade";
+
+export default function LoadingBar({ loading, height, children }) {
+ const dimOnTrue = (flag) => {
+ return {
+ opacity: flag ? 0.15 : 1,
+ height: "100%",
+ };
+ };
+
+ return (
+ <div style={{ height: height }}>
+ <Fade
+ in={loading}
+ style={{ transitionDelay: loading ? "300ms" : "0ms" }}
+ unmountOnExit
+ >
+ <LinearProgress />
+ </Fade>
+ <div style={dimOnTrue(loading)}>{children}</div>
+ </div>
+ );
+}
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeInfo.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeInfo.js
new file mode 100644
index 00000000000..159613c7aab
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeInfo.js
@@ -0,0 +1,187 @@
+import React from "react";
+import { connect } from "react-redux";
+import { FixedSizeList } from "react-window";
+import { AutoSizer } from "react-virtualized";
+import { makeStyles } from "@material-ui/core/styles";
+import List from "@material-ui/core/List";
+import ListItem from "@material-ui/core/ListItem";
+import ListItemText from "@material-ui/core/ListItemText";
+import Collapse from "@material-ui/core/Collapse";
+import ExpandLess from "@material-ui/icons/ExpandLess";
+import ExpandMore from "@material-ui/icons/ExpandMore";
+import Paper from "@material-ui/core/Paper";
+import Box from "@material-ui/core/Box";
+
+import { getNodeInfos } from "./redux/store";
+
+import theme from "./theme";
+
+import OverflowTooltip from "./OverflowTooltip";
+
+const NodeInfo = ({ nodeInfos, node, width }) => {
+ const useStyles = makeStyles((theme) => ({
+ root: {
+ width: "100%",
+ maxWidth: width,
+ backgroundColor: theme.palette.background.paper,
+ },
+ nested: {
+ paddingLeft: theme.spacing(4),
+ },
+ listItem: {
+ width: width,
+ },
+ }));
+
+ const rowHeight = 25;
+ const classes = useStyles();
+ const [openDependers, setOpenDependers] = React.useState(false);
+ const [openDependencies, setOpenDependencies] = React.useState(false);
+ const [openNodeAttribs, setOpenNodeAttribs] = React.useState(false);
+
+ const [nodeInfo, setNodeInfo] = React.useState({
+ id: 0,
+ node: "test/test.so",
+ name: "test",
+ attribs: [{ name: "test", value: "test" }],
+ dependers: [{ node: "test/test3.so", symbols: [] }],
+ dependencies: [{ node: "test/test2.so", symbols: [] }],
+ });
+
+ React.useEffect(() => {
+ setNodeInfo(nodeInfos.filter((nodeInfo) => nodeInfo.node == node.node)[0]);
+ }, [nodeInfos]);
+
+ function renderAttribRow({ index, style, data }) {
+ return (
+ <ListItem style={style} key={index}>
+ <Box style={{ margin: "5px" }}>
+ <OverflowTooltip
+ value={data[index].name}
+ text={String(data[index].name) + ":"}
+ />
+ </Box>
+ <OverflowTooltip
+ value={String(data[index].value)}
+ text={String(data[index].value)}
+ />
+ </ListItem>
+ );
+ }
+
+ function renderNodeRow({ index, style, data }) {
+ return (
+ <ListItem style={style} key={index}>
+ <OverflowTooltip
+ button
+ name={data[index].name}
+ value={data[index].node}
+ text={data[index].node}
+ />
+ </ListItem>
+ );
+ }
+
+ function listHeight(numItems) {
+ const size = numItems * rowHeight;
+ if (size > 350) {
+ return 350;
+ }
+ return size;
+ }
+
+ if (nodeInfo == undefined) {
+ return "";
+ }
+ return (
+ <List
+ component="nav"
+ aria-labelledby="nested-list-subheader"
+ className={classes.root}
+ dense={true}
+ >
+ <Paper elevation={3} style={{ backgroundColor: "rgba(0, 0, 0, .03)" }}>
+ <ListItem button>
+ <ListItemText primary={nodeInfo.node} />
+ </ListItem>
+ <ListItem button>
+ <ListItemText primary={nodeInfo.name} />
+ </ListItem>
+
+ <ListItem button onClick={() => setOpenNodeAttribs(!openNodeAttribs)}>
+ <ListItemText primary="Attributes" />
+ {openNodeAttribs ? <ExpandLess /> : <ExpandMore />}
+ </ListItem>
+ <Collapse in={openNodeAttribs} timeout="auto" unmountOnExit>
+ <Paper
+ elevation={2}
+ style={{
+ width: "100%",
+ backgroundColor: theme.palette.background.paper,
+ }}
+ >
+ <AutoSizer disableHeight={true}>
+ {({ height, width }) => (
+ <FixedSizeList
+ height={listHeight(nodeInfo.attribs.length)}
+ width={width}
+ itemSize={rowHeight}
+ itemCount={nodeInfo.attribs.length}
+ itemData={nodeInfo.attribs}
+ >
+ {renderAttribRow}
+ </FixedSizeList>
+ )}
+ </AutoSizer>
+ </Paper>
+ </Collapse>
+
+ <ListItem button onClick={() => setOpenDependers(!openDependers)}>
+ <ListItemText primary="Dependers" />
+ {openDependers ? <ExpandLess /> : <ExpandMore />}
+ </ListItem>
+ <Collapse in={openDependers} timeout="auto" unmountOnExit>
+ <Paper elevation={4}>
+ <AutoSizer disableHeight={true}>
+ {({ height, width }) => (
+ <FixedSizeList
+ height={listHeight(nodeInfo.dependers.length)}
+ width={width}
+ itemSize={rowHeight}
+ itemCount={nodeInfo.dependers.length}
+ itemData={nodeInfo.dependers}
+ >
+ {renderNodeRow}
+ </FixedSizeList>
+ )}
+ </AutoSizer>
+ </Paper>
+ </Collapse>
+
+ <ListItem button onClick={() => setOpenDependencies(!openDependencies)}>
+ <ListItemText primary="Dependencies" />
+ {openDependencies ? <ExpandLess /> : <ExpandMore />}
+ </ListItem>
+ <Collapse in={openDependencies} timeout="auto" unmountOnExit>
+ <Paper elevation={4}>
+ <AutoSizer disableHeight={true}>
+ {({ height, width }) => (
+ <FixedSizeList
+ height={listHeight(nodeInfo.dependencies.length)}
+ width={width}
+ itemSize={rowHeight}
+ itemCount={nodeInfo.dependencies.length}
+ itemData={nodeInfo.dependencies}
+ >
+ {renderNodeRow}
+ </FixedSizeList>
+ )}
+ </AutoSizer>
+ </Paper>
+ </Collapse>
+ </Paper>
+ </List>
+ );
+};
+
+export default connect(getNodeInfos)(NodeInfo);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js
new file mode 100644
index 00000000000..96f19a5d071
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/NodeList.js
@@ -0,0 +1,43 @@
+import React from "react";
+
+import { connect } from "react-redux";
+import { getNodes } from "./redux/store";
+import { setFindNode } from "./redux/findNode";
+import { socket } from "./connect";
+
+import DataGrid from "./DataGrid";
+import LoadingBar from "./LoadingBar";
+
+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 }) => {
+ function handleCheckBoxes(rowIndex, event) {
+ socket.emit("row_selected", {
+ data: { node: nodes[rowIndex].node, name: nodes[rowIndex].name },
+ isSelected: event.target.checked,
+ });
+ }
+
+ function handleRowClick(event) {
+ setFindNode(event.target.textContent);
+ }
+
+ return (
+ <LoadingBar loading={loading} height={"95%"}>
+ <DataGrid
+ rows={nodes}
+ columns={columns}
+ rowHeight={30}
+ headerHeight={35}
+ onNodeClicked={handleRowClick}
+ onRowSelect={handleCheckBoxes}
+ />
+ </LoadingBar>
+ );
+};
+
+export default connect(getNodes, { setFindNode })(NodeList);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js
new file mode 100644
index 00000000000..4546073c838
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/OverflowTooltip.js
@@ -0,0 +1,73 @@
+import React, { useRef, useEffect, useState } from "react";
+import { connect } from "react-redux";
+import Tooltip from "@material-ui/core/Tooltip";
+import Fade from "@material-ui/core/Fade";
+import Box from "@material-ui/core/Box";
+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";
+
+const OverflowTip = (props) => {
+ const textElementRef = useRef(null);
+ const [hoverStatus, setHover] = useState(false);
+
+ const compareSize = (textElementRef) => {
+ if (textElementRef.current != null) {
+ const compare =
+ textElementRef.current.scrollWidth > textElementRef.current.offsetWidth;
+ setHover(compare);
+ }
+ };
+
+ useEffect(() => {
+ compareSize(textElementRef);
+ window.addEventListener("resize", compareSize);
+ return function () {
+ window.removeEventListener("resize", compareSize);
+ };
+ }, [props, textElementRef.current]);
+
+ return (
+ <Tooltip
+ title={props.value}
+ interactive
+ disableHoverListener={!hoverStatus}
+ style={{ fontSize: "1em" }}
+ enterDelay={500}
+ TransitionComponent={Fade}
+ >
+ <Box
+ style={{
+ fontSize: "1em",
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ }}
+ >
+ <Typography noWrap variant={"body2"} gutterBottom>
+ {props.button && (
+ <IconButton
+ size="small"
+ color="secondary"
+ onClick={(event) => {
+ props.updateCheckbox({ node: props.text, value: "flip" });
+ socket.emit("row_selected", {
+ data: { node: props.text, name: props.name },
+ isSelected: "flip",
+ });
+ }}
+ >
+ <AddCircleOutline style={{ height: "15px", width: "15px" }} />
+ </IconButton>
+ )}
+ <span ref={textElementRef}>{props.text}</span>
+ </Typography>
+ </Box>
+ </Tooltip>
+ );
+};
+
+export default connect(null, { updateCheckbox })(OverflowTip);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/SwitchComponent.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/SwitchComponent.js
new file mode 100644
index 00000000000..5d5da96c960
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/SwitchComponent.js
@@ -0,0 +1,4 @@
+export default function SwitchComponents({ active, children }) {
+ // Switch all children and return the "active" one
+ return children.filter((child) => child.props.name == active);
+}
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js
new file mode 100644
index 00000000000..b055f1910a5
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js
@@ -0,0 +1,100 @@
+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";
+
+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,
+}) => {
+ 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);
+ });
+ }, []);
+
+ return null;
+};
+
+export default reduxConnect(null, {
+ setNodes,
+ updateCheckboxes,
+ setCounts,
+ setNodeInfos,
+ setGraphFiles,
+ setLoading,
+ selectGraphFile,
+ addLinks,
+ setGraphData,
+})(SocketConnection);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/index.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/index.js
new file mode 100644
index 00000000000..2cf4e2644c2
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/index.js
@@ -0,0 +1,21 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import { Provider } from "react-redux";
+import CssBaseline from "@material-ui/core/CssBaseline";
+import { ThemeProvider } from "@material-ui/core/styles";
+
+import theme from "./theme";
+import store from "./redux/store";
+
+import App from "./App";
+
+ReactDOM.render(
+ <Provider store={store}>
+ <ThemeProvider theme={theme}>
+ <CssBaseline />
+ <App />
+ </ThemeProvider>
+ </Provider>,
+
+ document.querySelector("#root")
+);
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/counts.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/counts.js
new file mode 100644
index 00000000000..eda9fe8327c
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/counts.js
@@ -0,0 +1,16 @@
+import { initialState } from "./store";
+
+export const counts = (state = initialState, action) => {
+ switch (action.type) {
+ case "setCounts":
+ return action.payload;
+
+ default:
+ return state;
+ }
+};
+
+export const setCounts = (counts) => ({
+ type: "setCounts",
+ payload: counts,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/findNode.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/findNode.js
new file mode 100644
index 00000000000..742232b950d
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/findNode.js
@@ -0,0 +1,16 @@
+import { initialState } from "./store";
+
+export const findNode = (state = initialState, action) => {
+ switch (action.type) {
+ case "setFindNode":
+ return action.payload;
+
+ default:
+ return state;
+ }
+};
+
+export const setFindNode = (node) => ({
+ type: "setFindNode",
+ payload: node,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphData.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphData.js
new file mode 100644
index 00000000000..b30ff698de6
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphData.js
@@ -0,0 +1,16 @@
+import { initialState } from "./store";
+
+export const graphData = (state = initialState, action) => {
+ switch (action.type) {
+ case "setGraphData":
+ return action.payload;
+
+ default:
+ return state;
+ }
+};
+
+export const setGraphData = (graphData) => ({
+ type: "setGraphData",
+ payload: graphData,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphFiles.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphFiles.js
new file mode 100644
index 00000000000..d0d4713c9be
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphFiles.js
@@ -0,0 +1,30 @@
+import { initialState } from "./store";
+
+export const graphFiles = (state = initialState, action) => {
+ switch (action.type) {
+ case "setGraphFiles":
+ return action.payload;
+ case "selectGraphFile":
+ const newState = state.map((graphFile, index) => {
+ if (action.payload.hash == graphFile.git) {
+ graphFile.selected = action.payload.selected;
+ } else {
+ graphFile.selected = false;
+ }
+ return graphFile;
+ });
+ return newState;
+ default:
+ return state;
+ }
+};
+
+export const setGraphFiles = (graphFiles) => ({
+ type: "setGraphFiles",
+ payload: graphFiles,
+});
+
+export const selectGraphFile = (graphFiles) => ({
+ type: "selectGraphFile",
+ payload: graphFiles,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/links.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/links.js
new file mode 100644
index 00000000000..04d0d3d2de8
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/links.js
@@ -0,0 +1,16 @@
+import { initialState } from "./store";
+
+export const links = (state = initialState, action) => {
+ switch (action.type) {
+ case "addLinks":
+ return action.payload;
+
+ default:
+ return state;
+ }
+};
+
+export const addLinks = (links) => ({
+ type: "addLinks",
+ payload: links,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/loading.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/loading.js
new file mode 100644
index 00000000000..8b7a9c09c70
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/loading.js
@@ -0,0 +1,16 @@
+import { initialState } from "./store";
+
+export const loading = (state = initialState, action) => {
+ switch (action.type) {
+ case "setLoading":
+ return action.payload;
+
+ default:
+ return state;
+ }
+};
+
+export const setLoading = (loading) => ({
+ type: "setLoading",
+ payload: loading,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodeInfo.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodeInfo.js
new file mode 100644
index 00000000000..381c66f4bc7
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodeInfo.js
@@ -0,0 +1,18 @@
+import { initialState } from "./store";
+
+export const nodeInfo = (state = initialState, action) => {
+ switch (action.type) {
+ case "setNodeInfos":
+ return action.payload;
+ case "addNodeInfo":
+ return [...state, action.payload];
+
+ default:
+ return state;
+ }
+};
+
+export const setNodeInfos = (nodeInfos) => ({
+ type: "setNodeInfos",
+ payload: nodeInfos,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodes.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodes.js
new file mode 100644
index 00000000000..755fd17d253
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/nodes.js
@@ -0,0 +1,66 @@
+import { initialState } from "./store";
+
+export const nodes = (state = initialState, action) => {
+ switch (action.type) {
+ case "addNode":
+ var arr = Object.assign(state);
+ return [...arr, action.payload];
+ case "setNodes":
+ return action.payload;
+ case "updateSelected":
+ var newState = Object.assign(state);
+ newState[action.payload.index].selected = action.payload.value;
+ return newState;
+ case "updateCheckbox":
+ var newState = Object.assign(state);
+ newState = state.map((stateNode) => {
+ if (stateNode.node == action.payload.node) {
+ if (action.payload.value == "flip") {
+ stateNode.selected = !stateNode.selected;
+ } else {
+ stateNode.selected = action.payload.value;
+ }
+ }
+ return stateNode;
+ });
+ return newState;
+ case "updateCheckboxes":
+ var newState = state.map((stateNode, index) => {
+ const nodeToUpdate = action.payload.filter(
+ (node) => stateNode.node == node.node
+ );
+ if (nodeToUpdate.length > 0) {
+ stateNode.selected = nodeToUpdate[0].value;
+ }
+ return stateNode;
+ });
+ return newState;
+ default:
+ return state;
+ }
+};
+
+export const addNode = (node) => ({
+ type: "addNode",
+ payload: node,
+});
+
+export const setNodes = (nodes) => ({
+ type: "setNodes",
+ payload: nodes,
+});
+
+export const updateSelected = (newValue) => ({
+ type: "updateSelected",
+ payload: newValue,
+});
+
+export const updateCheckbox = (newValue) => ({
+ type: "updateCheckbox",
+ payload: newValue,
+});
+
+export const updateCheckboxes = (newValue) => ({
+ type: "updateCheckboxes",
+ payload: newValue,
+});
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js
new file mode 100644
index 00000000000..c4058f510eb
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js
@@ -0,0 +1,130 @@
+import { createStore, combineReducers } from "redux";
+import { nodes } from "./nodes";
+import { graphFiles } from "./graphFiles";
+import { counts } from "./counts";
+import { nodeInfo } from "./nodeInfo";
+import { loading } from "./loading";
+import { links } from "./links";
+import { graphData } from "./graphData";
+import { findNode } from "./findNode";
+
+export const initialState = {
+ loading: false,
+ graphFiles: [
+ // {id: 0, value: 'graphfile.graphml', version: 1, git: '1234567', selected: false}
+ ],
+ nodes: [
+ {
+ id: 0,
+ node: "test/test1.so",
+ name: "test1",
+ check: "checkbox",
+ selected: false,
+ },
+ {
+ id: 1,
+ node: "test/test2.so",
+ name: "test2",
+ check: "checkbox",
+ selected: false,
+ },
+ ],
+ links: [{ source: "test/test1.so", target: "test/test2.so" }],
+ graphData: {
+ nodes: [
+ // {id: 'test/test1.so', name: 'test1.so'},
+ // {id: 'test/test2.so', name: 'test2.so'}
+ ],
+ links: [
+ // {source: 'test/test1.so', target: 'test/test2.so'}
+ ],
+ },
+ counts: [{ id: 0, type: "node2", value: 0 }],
+ findNode: "",
+ nodeInfo: [
+ {
+ id: 0,
+ node: "test/test.so",
+ name: "test",
+ attribs: [{ name: "test", value: "test" }],
+ dependers: [{ node: "test/test3.so", symbols: [] }],
+ dependencies: [{ node: "test/test2.so", symbols: [] }],
+ },
+ ],
+};
+
+export const getLoading = (state) => {
+ return { loading: state };
+};
+
+export const getGraphFiles = (state) => {
+ return {
+ loading: state.loading,
+ graphFiles: state.graphFiles,
+ };
+};
+
+export const getNodeInfos = (state) => {
+ return {
+ nodeInfos: state.nodeInfo,
+ };
+};
+
+export const getCounts = (state) => {
+ const counts = state.counts;
+ return {
+ counts: state.counts,
+ };
+};
+
+export const getRows = (state) => {
+ return {
+ rowCount: state.nodes.length,
+ rowGetter: ({ index }) => state.nodes[index],
+ checkBox: ({ index }) => state.nodes[index].selected,
+ nodes: state.nodes,
+ };
+};
+
+export const getSelected = (state) => {
+ return {
+ selectedNodes: state.nodes.filter((node) => node.selected),
+ selectedEdges: [],
+ loading: state.loading,
+ };
+};
+
+export const getNodes = (state) => {
+ return {
+ nodes: state.nodes,
+ loading: state.loading,
+ };
+};
+
+export const getGraphData = (state) => {
+ return {
+ nodes: state.nodes,
+ graphData: state.graphData,
+ loading: state.loading,
+ findNode: state.findNode,
+ };
+};
+
+export const getFullState = (state) => {
+ return { state };
+};
+
+const store = createStore(
+ combineReducers({
+ nodes,
+ counts,
+ nodeInfo,
+ graphFiles,
+ loading,
+ links,
+ graphData,
+ findNode,
+ }),
+ initialState
+);
+export default store;
diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/theme.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/theme.js
new file mode 100644
index 00000000000..16323d2cc1b
--- /dev/null
+++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/theme.js
@@ -0,0 +1,22 @@
+import { lightGreen, blueGrey, grey } from "@material-ui/core/colors";
+import { createMuiTheme } from "@material-ui/core/styles";
+
+// A custom theme for this app
+const theme = createMuiTheme({
+ palette: {
+ primary: {
+ light: lightGreen[300],
+ main: lightGreen[500],
+ dark: lightGreen[700],
+ },
+ secondary: {
+ light: grey[300],
+ main: grey[500],
+ dark: grey[800],
+ darkAccent: "#4d4d4d",
+ },
+ mode: "dark",
+ },
+});
+
+export default theme;
diff --git a/buildscripts/libdeps/graph_analyzer.py b/buildscripts/libdeps/libdeps/analyzer.py
index c9138b2dc63..3844eed85ab 100644
--- a/buildscripts/libdeps/graph_analyzer.py
+++ b/buildscripts/libdeps/libdeps/analyzer.py
@@ -33,11 +33,9 @@ import sys
import textwrap
from pathlib import Path
-import networkx
+from libdeps.graph import CountTypes, DependsReportTypes, LinterTypes, EdgeProps, NodeProps
-from libdeps_graph_enums import CountTypes, DependsReportTypes, LinterTypes, EdgeProps, NodeProps
-
-sys.path.append(str(Path(__file__).parent.parent))
+sys.path.append(str(Path(__file__).parent.parent.parent))
import scons # pylint: disable=wrong-import-position
sys.path.append(str(Path(scons.MONGODB_ROOT).joinpath('site_scons')))
@@ -542,19 +540,6 @@ class BuildDataReport(Analyzer):
report['graph_schema_version'] = self.graph.graph.get('graph_schema_version')
-class LibdepsGraph(networkx.DiGraph):
- """Class for analyzing the graph."""
-
- def __init__(self, graph=networkx.DiGraph()):
- """Load the graph data."""
-
- super().__init__(incoming_graph_data=graph)
-
- # Load in the graph and store a reversed version as well for quick look ups
- # the in directions.
- self.rgraph = graph.reverse()
-
-
class LibdepsGraphAnalysis:
"""Runs the given analysis on the input graph."""
@@ -572,6 +557,13 @@ class LibdepsGraphAnalysis:
return self.results
+ def run_linters(self, linters):
+ """Run the various dependency reports."""
+
+ if LinterTypes.public_unused.name in linters:
+ self.results[LinterTypes.public_unused.name] = \
+ self.libdeps_graph.unused_public_linter()
+
class GaPrinter:
"""Base class for printers of the graph analysis."""
diff --git a/buildscripts/libdeps/libdeps_graph_enums.py b/buildscripts/libdeps/libdeps/graph.py
index 0fde3a84733..03b2a29f4f9 100644
--- a/buildscripts/libdeps/libdeps_graph_enums.py
+++ b/buildscripts/libdeps/libdeps/graph.py
@@ -29,6 +29,8 @@ These are used for attributing data across the build scripts and analyzer script
from enum import Enum, auto
+import networkx
+
class CountTypes(Enum):
"""Enums for the different types of counts to perform on a graph."""
@@ -76,3 +78,16 @@ class NodeProps(Enum):
shim = auto()
bin_type = auto()
+
+
+class LibdepsGraph(networkx.DiGraph):
+ """Class for analyzing the graph."""
+
+ def __init__(self, graph=networkx.DiGraph()):
+ """Load the graph data."""
+
+ super().__init__(incoming_graph_data=graph)
+
+ # Load in the graph and store a reversed version as well for quick look ups
+ # the in directions.
+ self.rgraph = graph.reverse()
diff --git a/etc/pip/components/compile.req b/etc/pip/components/compile.req
index 7c1e8f4f45d..1e29997f42d 100644
--- a/etc/pip/components/compile.req
+++ b/etc/pip/components/compile.req
@@ -3,4 +3,4 @@ Cheetah3 # src/mongo/base/generate_error_codes.py
psutil
regex
requirements_parser
-setuptools \ No newline at end of file
+setuptools
diff --git a/etc/pip/components/external_auth.req b/etc/pip/components/external_auth.req
index e5bf1b10300..d9b71263314 100644
--- a/etc/pip/components/external_auth.req
+++ b/etc/pip/components/external_auth.req
@@ -4,6 +4,6 @@ pyOpenSSL == 19.0.0
pyparsing == 2.4.0
service_identity == 18.1.0
twisted == 20.3.0
-zope.interface == 4.7.2
+zope.interface == 5.0.0
ldaptor == 19.0.0
diff --git a/etc/pip/components/libdeps.req b/etc/pip/components/libdeps.req
index 6ae7ffa249b..d6d1ed52ec6 100644
--- a/etc/pip/components/libdeps.req
+++ b/etc/pip/components/libdeps.req
@@ -1 +1,7 @@
-networkx \ No newline at end of file
+networkx
+flask
+flask_socketio
+flask_cors
+lxml
+eventlet
+gevent
diff --git a/etc/pip/libdeps-requirements.txt b/etc/pip/libdeps-requirements.txt
index 629dcb5d0d8..4b74c044044 100644
--- a/etc/pip/libdeps-requirements.txt
+++ b/etc/pip/libdeps-requirements.txt
@@ -1 +1 @@
--r components/libdeps.req \ No newline at end of file
+-r components/libdeps.req
diff --git a/site_scons/libdeps_next.py b/site_scons/libdeps_next.py
index a5522221ac9..9b361cc82a0 100644
--- a/site_scons/libdeps_next.py
+++ b/site_scons/libdeps_next.py
@@ -883,7 +883,7 @@ def _get_node_with_ixes(env, node, node_builder_type):
_get_node_with_ixes.node_type_ixes = dict()
def add_node_from(env, node):
- from buildscripts.libdeps.libdeps_graph_enums import NodeProps
+ from buildscripts.libdeps.libdeps.graph import NodeProps
env.GetLibdepsGraph().add_nodes_from([(
str(node.abspath),
@@ -893,7 +893,7 @@ def add_node_from(env, node):
})])
def add_edge_from(env, depender_node, dependent_node, visibility, direct):
- from buildscripts.libdeps.libdeps_graph_enums import EdgeProps
+ from buildscripts.libdeps.libdeps.graph import EdgeProps
env.GetLibdepsGraph().add_edges_from([(
dependent_node,
@@ -1247,7 +1247,7 @@ def generate_graph(env, target, source):
import fileinput
import networkx
import json
- from buildscripts.libdeps.libdeps_graph_enums import EdgeProps, NodeProps
+ from buildscripts.libdeps.libdeps.graph import EdgeProps, NodeProps
for symbol_deps_file in source:
with open(str(symbol_deps_file)) as f: