diff options
14 files changed, 776 insertions, 139 deletions
diff --git a/buildscripts/libdeps/gacli.py b/buildscripts/libdeps/gacli.py index 9b71e1544fe..836dc633660 100755 --- a/buildscripts/libdeps/gacli.py +++ b/buildscripts/libdeps/gacli.py @@ -178,6 +178,18 @@ def setup_args_parser(): return parser.parse_args() +def strip_build_dir(build_dir, node): + """Small util function for making args match the graph paths.""" + + return str(Path(node).relative_to(build_dir)) + + +def strip_build_dirs(build_dir, nodes): + """Small util function for making a list of nodes match graph paths.""" + + return [strip_build_dir(build_dir, node) for node in nodes] + + def load_graph_data(graph_file, output_format): """Load a graphml file.""" @@ -196,25 +208,38 @@ def main(): args = setup_args_parser() graph = load_graph_data(args.graph_file, args.format) libdeps_graph = LibdepsGraph(graph=graph) + build_dir = libdeps_graph.graph['build_dir'] + + if libdeps_graph.graph['graph_schema_version'] == 1: + libdeps_graph = networkx.reverse_view(libdeps_graph) analysis = libdeps_analyzer.counter_factory(libdeps_graph, args.counts) for analyzer_args in args.direct_depends: - analysis.append(libdeps_analyzer.DirectDependents(libdeps_graph, analyzer_args)) + analysis.append( + libdeps_analyzer.DirectDependents(libdeps_graph, + strip_build_dir(build_dir, analyzer_args))) for analyzer_args in args.common_depends: - analysis.append(libdeps_analyzer.CommonDependents(libdeps_graph, analyzer_args)) + analysis.append( + libdeps_analyzer.CommonDependents(libdeps_graph, + strip_build_dirs(build_dir, analyzer_args))) for analyzer_args in args.exclude_depends: - analysis.append(libdeps_analyzer.ExcludeDependents(libdeps_graph, analyzer_args)) + analysis.append( + libdeps_analyzer.ExcludeDependents(libdeps_graph, + strip_build_dirs(build_dir, analyzer_args))) for analyzer_args in args.graph_paths: analysis.append( - libdeps_analyzer.GraphPaths(libdeps_graph, analyzer_args[0], analyzer_args[1])) + libdeps_analyzer.GraphPaths(libdeps_graph, strip_build_dir(build_dir, analyzer_args[0]), + strip_build_dir(build_dir, analyzer_args[1]))) for analyzer_args in args.critical_edges: analysis.append( - libdeps_analyzer.CriticalEdges(libdeps_graph, analyzer_args[0], analyzer_args[1])) + libdeps_analyzer.CriticalEdges(libdeps_graph, + strip_build_dir(build_dir, analyzer_args[0]), + strip_build_dir(build_dir, analyzer_args[1]))) if args.indegree_one: analysis.append(libdeps_analyzer.InDegreeOne(libdeps_graph)) 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 af34ffa65a9..ce321a1bd00 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py +++ b/buildscripts/libdeps/graph_visualizer_web_stack/flask/flask_backend.py @@ -56,6 +56,7 @@ class BackendServer: 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.loaded_graphs = {} self.current_selected_rows = {} @@ -65,10 +66,12 @@ class BackendServer: 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 + try: default_selected_graph = list(self.graph_files.items())[0][1].graph_file self.load_graph_from_file(default_selected_graph) - self._dependents_graph = networkx.reverse_view(self._dependency_graph) except (IndexError, AttributeError) as ex: print(ex) print( @@ -164,40 +167,43 @@ class BackendServer: self.socketio.emit("node_infos", nodeinfo_data) - def send_graph_data(self): + def send_graph_data(self, extra_nodes=None): """Convert the current selected rows into a format for D3.""" with self.app.test_request_context(): - nodes = set() - links = set() + 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'] + } + + def add_link_to_graph_data(source, target): + links[str(source) + str(target)] = {'source': str(source), 'target': str(target)} for node, _ in self.current_selected_rows.items(): - nodes.add( - tuple({ - 'id': str(node), 'name': node.name, 'type': self._dependents_graph.nodes() - [str(node)]['bin_type'] - }.items())) - - for depender in self._dependency_graph[str(node)]: - - depender_path = Path(depender) - if self._dependents_graph[depender][str(node)].get('direct'): - nodes.add( - tuple({ - 'id': - str(depender_path), 'name': - depender_path.name, 'type': - self._dependents_graph.nodes()[str(depender_path)] - ['bin_type'] - }.items())) - links.add( - tuple({'source': str(node), 'target': str(depender_path)}.items())) + add_node_to_graph_data(node) + + 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) + + if extra_nodes is not None: + for node in extra_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': [dict(node) for node in nodes], - 'links': [dict(link) for link in links], + '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) @@ -205,18 +211,16 @@ class BackendServer: 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']) + 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(message['data']['node']) + 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) @@ -227,7 +231,7 @@ class BackendServer: with self.app.test_request_context(): analysis = libdeps.analyzer.counter_factory( - self._dependents_graph, + self._dependency_graph, [name[0] for name in libdeps.analyzer.CountTypes.__members__.items()]) ga = libdeps.analyzer.LibdepsGraphAnalysis(analysis) results = ga.get_results() @@ -237,6 +241,37 @@ class BackendServer: 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): """Gather all the nodes in the graph for the node list.""" @@ -246,7 +281,7 @@ class BackendServer: "selectedNodes": [str(node) for node in list(self.current_selected_rows.keys())] } - for node in self._dependents_graph.nodes(): + 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}) @@ -262,7 +297,7 @@ class BackendServer: self.current_selected_rows = {} if message['hash'] in self.loaded_graphs: self._dependents_graph = self.loaded_graphs[message['hash']] - self._dependents_graph = networkx.reverse_view(self._dependency_graph) + self._dependency_graph = networkx.reverse_view(self._dependents_graph) else: print( f'loading new graph {current_hash} because different than {message["hash"]}' @@ -278,8 +313,6 @@ class BackendServer: 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 index 3a69d02752e..4d8196478bb 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/package.json +++ b/buildscripts/libdeps/graph_visualizer_web_stack/package.json @@ -13,12 +13,13 @@ "test": "react-scripts test", "eject": "react-scripts eject" }, + "//": "TODO: adding bezier and force-graph and locking versions until https://github.com/vasturiano/force-graph/issues/182 is resolved", "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", + "@material-ui/core": "5.0.0-alpha.22", + "@material-ui/icons": "5.0.0-alpha.22", + "@material-ui/lab": "5.0.0-alpha.22", "canvas": "^2.5.0", "date-fns": "^2.16.1", "dayjs": "^1.9.7", @@ -28,10 +29,14 @@ "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", + "bezier-js": "4.0.3", + "force-graph": "1.40.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-resize-aware": "^3.1.0", + "react-resize-detector": "^6.6.5", "react-scripts": "latest", "react-split-pane": "^0.1.92", "react-virtualized": "^9.22.2", diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/AlgorithmExpander.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/AlgorithmExpander.js new file mode 100644 index 00000000000..c75a6513d7d --- /dev/null +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/AlgorithmExpander.js @@ -0,0 +1,105 @@ +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 GraphPaths from "./GraphPaths"; +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 AlgorithmExpander = ({ 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> + <Accordion> + <AccordionSummary + expandIcon={<ExpandMoreIcon />} + aria-controls="panel1a-content" + id="panel1a-header" + > + <Typography className={classes.heading}>Graph Paths</Typography> + </AccordionSummary> + <AccordionDetails> + <GraphPaths datawidth={width} /> + </AccordionDetails> + </Accordion> + </Paper> + </LoadingBar> + </div> + ); +}; + +export default connect(getSelected)(AlgorithmExpander); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js index 3975ffe9f3a..8364b55ff67 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/DataGrid.js @@ -24,25 +24,26 @@ function rgbToHex(r, g, b) { function hexToRgb(hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function(m, r, g, b) { + hex = hex.replace(shorthandRegex, function (m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null; + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null; } -function incrementPallete(palleteColor, increment){ +function incrementPallete(palleteColor, increment) { var rgb = hexToRgb(palleteColor); rgb.r += increment; rgb.g += increment; rgb.b += increment; return rgbToHex(rgb.r, rgb.g, rgb.b); - } const styles = (theme) => ({ diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js index 672bdaa41c3..375942f4852 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/DrawGraph.js @@ -55,11 +55,15 @@ const DrawGraph = ({ graphData, nodes, loading, + graphPaths, + updateCheckbox, findNode, setFindNode, }) => { 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); React.useEffect(() => { @@ -78,18 +82,52 @@ const DrawGraph = ({ }, [nodes]); React.useEffect(() => { + setPathNodes({ fromNode: graphPaths.fromNode, toNode: graphPaths.toNode }); + var paths = Array(); + for (var path = 0; path < graphPaths.paths.length; path++) { + var pathArr = Array(); + for (var i = 0; i < graphPaths.paths[path].length; i++) { + if (i == 0) { + continue; + } + pathArr.push({ + source: graphPaths.paths[path][i - 1], + target: graphPaths.paths[path][i], + }); + } + paths.push(pathArr); + } + setPathEdges(paths); + }, [graphPaths]); + + React.useEffect(() => { if (forceRef.current != null) { - forceRef.current.d3Force("charge").strength(-1400); + if (activeComponent == '3D'){ + forceRef.current.d3Force("charge").strength(-2000); + } + else { + forceRef.current.d3Force("charge").strength(-10000); + } + } - }, [forceRef.current]); + }, [forceRef.current, activeComponent]); - 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(); - }); + const paintRing = React.useCallback( + (node, ctx) => { + // add ring just for highlighted nodes + ctx.beginPath(); + ctx.arc(node.x, node.y, 7 * 1.4, 0, 2 * Math.PI, false); + if (node.id == pathNodes.fromNode) { + ctx.fillStyle = "blue"; + } else if (node.id == pathNodes.toNode) { + ctx.fillStyle = "red"; + } else { + ctx.fillStyle = "green"; + } + ctx.fill(); + }, + [pathNodes] + ); function colorNodes(node) { switch (node.type) { @@ -134,6 +172,7 @@ const DrawGraph = ({ name="3D" width={size} dagMode="radialout" + dagLevelDistance={50} graphData={graphData} ref={forceRef} nodeColor={colorNodes} @@ -141,14 +180,70 @@ const DrawGraph = ({ backgroundColor={theme.palette.secondary.dark} linkDirectionalArrowLength={6} linkDirectionalArrowRelPos={1} + linkDirectionalParticles={(d) => { + if (graphPaths.selectedPath >= 0) { + for ( + var i = 0; + i < pathEdges[graphPaths.selectedPath].length; + i++ + ) { + if ( + pathEdges[graphPaths.selectedPath][i].source == d.source.id && + pathEdges[graphPaths.selectedPath][i].target == d.target.id + ) { + return 5; + } + } + } + return 0; + }} + linkDirectionalParticleSpeed={(d) => { + return 0.01; + }} nodeCanvasObjectMode={(node) => { if (selectedNodes.includes(node.id)) { return "before"; } }} + linkColor={(d) => { + if (graphPaths.selectedPath >= 0) { + for ( + var i = 0; + i < pathEdges[graphPaths.selectedPath].length; + i++ + ) { + if ( + pathEdges[graphPaths.selectedPath][i].source == d.source.id && + pathEdges[graphPaths.selectedPath][i].target == d.target.id + ) { + return "#12FF19"; + } + } + } + return "#FAFAFA"; + }} + linkDirectionalParticleWidth={6} + linkWidth={(d) => { + if (graphPaths.selectedPath >= 0) { + for ( + var i = 0; + i < pathEdges[graphPaths.selectedPath].length; + i++ + ) { + if ( + pathEdges[graphPaths.selectedPath][i].source == d.source.id && + pathEdges[graphPaths.selectedPath][i].target == d.target.id + ) { + return 2; + } + } + } + return 1; + }} + nodeRelSize={7} nodeCanvasObject={paintRing} onNodeClick={(node, event) => { - updateCheckbox(node.id); + updateCheckbox({ node: node.id, value: "flip" }); socket.emit("row_selected", { data: { node: node.id, name: node.name }, isSelected: !selectedNodes.includes(node.id), @@ -175,12 +270,70 @@ const DrawGraph = ({ } }} onNodeClick={(node, event) => { - updateCheckbox(node.id); + updateCheckbox({ node: node.id, value: "flip" }); socket.emit("row_selected", { data: { node: node.id, name: node.name }, isSelected: !selectedNodes.includes(node.id), }); }} + linkColor={(d) => { + if (graphPaths.selectedPath >= 0) { + for ( + var i = 0; + i < pathEdges[graphPaths.selectedPath].length; + i++ + ) { + if ( + pathEdges[graphPaths.selectedPath][i].source == d.source.id && + pathEdges[graphPaths.selectedPath][i].target == d.target.id + ) { + return "#12FF19"; + } + } + } + return "#FAFAFA"; + }} + linkDirectionalParticleWidth={7} + linkWidth={(d) => { + if (graphPaths.selectedPath >= 0) { + for ( + var i = 0; + i < pathEdges[graphPaths.selectedPath].length; + i++ + ) { + if ( + pathEdges[graphPaths.selectedPath][i].source == d.source.id && + pathEdges[graphPaths.selectedPath][i].target == d.target.id + ) { + return 5; + } + } + } + return 1; + }} + linkDirectionalParticles={(d) => { + if (graphPaths.selectedPath >= 0) { + for ( + var i = 0; + i < pathEdges[graphPaths.selectedPath].length; + i++ + ) { + if ( + pathEdges[graphPaths.selectedPath][i].source == d.source.id && + pathEdges[graphPaths.selectedPath][i].target == d.target.id + ) { + return 5; + } + } + } + return 0; + }} + linkDirectionalParticleSpeed={(d) => { + return 0.01; + }} + linkDirectionalParticleResolution={10} + linkOpacity={0.6} + nodeRelSize={7} backgroundColor={theme.palette.secondary.dark} linkDirectionalArrowLength={3.5} linkDirectionalArrowRelPos={1} @@ -191,4 +344,6 @@ const DrawGraph = ({ ); }; -export default connect(getGraphData, { setFindNode })(DrawGraph); +export default connect(getGraphData, { setFindNode, updateCheckbox })( + DrawGraph +); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js index 0cdec3d12b4..3636581f375 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphInfoTabs.js @@ -6,6 +6,7 @@ import Tab from "@material-ui/core/Tab"; import NodeList from "./NodeList"; import InfoExpander from "./InfoExpander"; +import AlgorithmExpander from "./AlgorithmExpander"; function a11yProps(index) { return { @@ -46,6 +47,7 @@ export default function GraphInfoTabs({ nodes, width }) { <Tab label="Selected Info" {...a11yProps(0)} /> <Tab label="Node List" {...a11yProps(1)} /> <Tab label="Edge List" {...a11yProps(2)} /> + <Tab label="Algorithms" {...a11yProps(3)} /> </Tabs> </AppBar> <div style={{ height: "100%" }} hidden={value != 0}> @@ -54,6 +56,10 @@ export default function GraphInfoTabs({ nodes, width }) { <div style={{ height: "100%" }} hidden={value != 1}> <NodeList nodes={nodes}></NodeList> </div> + <div style={{ height: "100%" }} hidden={value != 2}></div> + <div style={{ height: "100%" }} hidden={value != 3}> + <AlgorithmExpander width={width}></AlgorithmExpander> + </div> </div> ); } diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js new file mode 100644 index 00000000000..0b86129bd98 --- /dev/null +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/GraphPaths.js @@ -0,0 +1,294 @@ +import React from "react"; +import { connect } from "react-redux"; +import { FixedSizeList } from "react-window"; +import SplitPane from "react-split-pane"; +import { makeStyles, withStyles } from "@material-ui/core/styles"; +import ListItem from "@material-ui/core/ListItem"; +import ListItemText from "@material-ui/core/ListItemText"; +import Paper from "@material-ui/core/Paper"; +import Typography from "@material-ui/core/Typography"; +import Box from "@material-ui/core/Box"; +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 OverflowTooltip from "./OverflowTooltip"; + +const rowHeight = 25; + +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 GraphPaths = ({ selectedNodes, graphPaths, setSelectedPath, width }) => { + const [fromNode, setFromNode] = React.useState(""); + const [toNode, setToNode] = React.useState(""); + const [fromNodeId, setFromNodeId] = React.useState(0); + const [toNodeId, setToNodeId] = React.useState(0); + const [fromNodeExpanded, setFromNodeExpanded] = React.useState(false); + const [toNodeExpanded, setToNodeExpanded] = React.useState(false); + const [paneSize, setPaneSize] = React.useState("50%"); + + const [fromResizeListener, fromSizes] = useResizeAware(); + const [toResizeListener, toSizes] = useResizeAware(); + + const useStyles = makeStyles((theme) => ({ + root: { + width: "100%", + maxWidth: width, + backgroundColor: theme.palette.background.paper, + }, + nested: { + paddingLeft: theme.spacing(4), + }, + listItem: { + width: width, + }, + })); + const classes = useStyles(); + + function toNodeRow({ index, style, data }) { + return ( + <ListItem + button + style={style} + key={index} + onClick={() => { + setToNode(data[index].name); + setToNodeId(index); + setToNodeExpanded(false); + setPaneSize("50%"); + if (fromNode != "" && data[fromNodeId]) { + const nodes = { + fromNode: data[fromNodeId].node, + toNode: data[index].node, + }; + socket.emit("receive_graph_paths", nodes); + } + }} + > + <ListItemText primary={data[index].name} /> + </ListItem> + ); + } + + function fromNodeRow({ index, style, data }) { + return ( + <ListItem + button + style={style} + key={index} + onClick={() => { + setFromNode(data[index].name); + setFromNodeId(index); + setFromNodeExpanded(false); + setPaneSize("50%"); + + if (toNode != "" && data[toNodeId]) { + const nodes = { + fromNode: data[index].node, + toNode: data[toNodeId].node, + }; + socket.emit("receive_graph_paths", nodes); + } + }} + > + <ListItemText primary={data[index].name} /> + </ListItem> + ); + } + + function pathRow({ index, style, data }) { + return ( + <ListItem + button + style={style} + key={index} + onClick={() => { + setSelectedPath(index); + }} + > + <ListItemText + primary={ + "Path #" + + (index + 1).toString() + + " - Hops: " + + (data[index].length - 1).toString() + } + /> + </ListItem> + ); + } + + function listHeight(numItems, minHeight, maxHeight) { + const size = numItems * rowHeight; + if (size > maxHeight) { + return maxHeight; + } + if (size < minHeight) { + return minHeight; + } + return size; + } + + const handleToChange = (panel) => (event, newExpanded) => { + setPaneSize(newExpanded ? "0%" : "50%"); + setToNodeExpanded(newExpanded ? panel : false); + }; + + const handleFromChange = (panel) => (event, newExpanded) => { + setPaneSize(newExpanded ? "100%" : "50%"); + setFromNodeExpanded(newExpanded ? panel : false); + }; + + return ( + <Paper elevation={3} style={{ backgroundColor: "rgba(0, 0, 0, .03)" }}> + <SplitPane + split="vertical" + minSize={"50%"} + size={paneSize} + style={{ position: "relative" }} + defaultSize={"50%"} + pane1Style={{ height: "100%" }} + pane2Style={{ height: "100%", width: "100%" }} + > + <Accordion + expanded={fromNodeExpanded} + onChange={handleFromChange(!fromNodeExpanded)} + > + <AccordionSummary + expandIcon={<ExpandMoreIcon />} + aria-controls="panel1a-content" + id="panel1a-header" + > + <Box + style={{ + display: "flex", + flexDirection: "column", + }} + > + <Typography className={classes.heading}>From Node:</Typography> + <Typography + className={classes.heading} + style={{ width: fromSizes.width - 50 }} + noWrap={true} + display={"block"} + > + {fromResizeListener} + {fromNode} + </Typography> + </Box> + </AccordionSummary> + <AccordionDetails> + <FixedSizeList + height={listHeight(selectedNodes.length, 100, 200)} + width={width} + itemSize={rowHeight} + itemCount={selectedNodes.length} + itemData={selectedNodes} + > + {fromNodeRow} + </FixedSizeList> + </AccordionDetails> + </Accordion> + + <Accordion + expanded={toNodeExpanded} + onChange={handleToChange(!toNodeExpanded)} + > + <AccordionSummary + expandIcon={<ExpandMoreIcon />} + aria-controls="panel1a-content" + id="panel1a-header" + > + <Box style={{ display: "flex", flexDirection: "column" }}> + <Typography className={classes.heading}>To Node:</Typography> + <Typography + className={classes.heading} + style={{ width: toSizes.width - 50 }} + noWrap={true} + display={"block"} + > + {toResizeListener} + {toNode} + </Typography> + </Box> + </AccordionSummary> + <AccordionDetails> + <FixedSizeList + height={listHeight(selectedNodes.length, 100, 200)} + width={width} + itemSize={rowHeight} + itemCount={selectedNodes.length} + itemData={selectedNodes} + > + {toNodeRow} + </FixedSizeList> + </AccordionDetails> + </Accordion> + </SplitPane> + <Paper elevation={2} style={{ backgroundColor: "rgba(0, 0, 0, .03)" }}> + <Typography className={classes.heading} style={{ margin: "10px" }}> + Num Paths: {graphPaths.paths.length}{" "} + </Typography> + </Paper> + <FixedSizeList + height={listHeight(graphPaths.paths.length, 100, 200)} + width={width} + itemSize={rowHeight} + itemCount={graphPaths.paths.length} + itemData={graphPaths.paths} + style={{ margin: "10px" }} + > + {pathRow} + </FixedSizeList> + </Paper> + ); +}; + +export default connect(getSelected, { selectedGraphPaths, setSelectedPath })( + GraphPaths +); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js index 8912b1c9d5b..dbe6f579eea 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/InfoExpander.js @@ -72,18 +72,6 @@ const InfoExpander = ({ selectedNodes, selectedEdges, loading, width }) => { <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 diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js index b055f1910a5..efa3b8b9ec3 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/connect.js @@ -9,6 +9,7 @@ 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; @@ -27,6 +28,7 @@ const SocketConnection = ({ addLinks, setGraphData, setNodeInfos, + selectedGraphPaths, }) => { React.useEffect(() => { fetch(REACT_APP_API_URL + "/graph_files") @@ -82,6 +84,10 @@ const SocketConnection = ({ socket.on("node_infos", (incomingData) => { setNodeInfos(incomingData.nodeInfos); }); + + socket.on("graph_path_results", (incomingData) => { + selectedGraphPaths(incomingData); + }); }, []); return null; @@ -97,4 +103,5 @@ export default reduxConnect(null, { selectGraphFile, addLinks, setGraphData, + selectedGraphPaths, })(SocketConnection); diff --git a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphPaths.js b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphPaths.js new file mode 100644 index 00000000000..135c9c30e32 --- /dev/null +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/graphPaths.js @@ -0,0 +1,23 @@ +import { initialState } from "./store"; + +export const graphPaths = (state = initialState, action) => { + switch (action.type) { + case "selectedGraphPaths": + return action.payload; + case "setSelectedPath": + const newState = { ...state, selectedPath: action.payload }; + return newState; + default: + return state; + } +}; + +export const selectedGraphPaths = (pathData) => ({ + type: "selectedGraphPaths", + payload: pathData, +}); + +export const setSelectedPath = (path) => ({ + type: "setSelectedPath", + payload: path, +}); 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 c4058f510eb..8024c7cb327 100644 --- a/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js +++ b/buildscripts/libdeps/graph_visualizer_web_stack/src/redux/store.js @@ -7,6 +7,7 @@ import { loading } from "./loading"; import { links } from "./links"; import { graphData } from "./graphData"; import { findNode } from "./findNode"; +import { graphPaths } from "./graphPaths"; export const initialState = { loading: false, @@ -39,6 +40,15 @@ export const initialState = { // {source: 'test/test1.so', target: 'test/test2.so'} ], }, + graphPaths: { + fromNode: "test", + toNode: "test", + paths: [ + ["test1", "test2"], + ["test1", "test3", "test2"], + ], + selectedPath: -1, + }, counts: [{ id: 0, type: "node2", value: 0 }], findNode: "", nodeInfo: [ @@ -91,6 +101,7 @@ export const getSelected = (state) => { selectedNodes: state.nodes.filter((node) => node.selected), selectedEdges: [], loading: state.loading, + graphPaths: state.graphPaths, }; }; @@ -107,6 +118,7 @@ export const getGraphData = (state) => { graphData: state.graphData, loading: state.loading, findNode: state.findNode, + graphPaths: state.graphPaths, }; }; @@ -124,6 +136,7 @@ const store = createStore( links, graphData, findNode, + graphPaths, }), initialState ); diff --git a/buildscripts/libdeps/libdeps/analyzer.py b/buildscripts/libdeps/libdeps/analyzer.py index 756d410d4e8..c440fcf7d8f 100644 --- a/buildscripts/libdeps/libdeps/analyzer.py +++ b/buildscripts/libdeps/libdeps/analyzer.py @@ -115,17 +115,14 @@ class Analyzer: """Base class for different types of analyzers.""" # pylint: disable=too-many-instance-attributes - def __init__(self, graph, progress=True): + def __init__(self, dependency_graph, progress=True): """Store the graph and extract the build_dir from the graph.""" - self.graph_schema = graph.graph.get('graph_schema_version') - if self.graph_schema == 1: - self._dependents_graph = graph - else: - self._dependency_graph = graph + self.graph_schema = dependency_graph.graph.get('graph_schema_version') + self._dependency_graph = dependency_graph - self._build_dir = Path(graph.graph['build_dir']) - self.deptypes = json.loads(graph.graph.get('deptypes', "{}")) + self._build_dir = Path(dependency_graph.graph['build_dir']) + self.deptypes = json.loads(dependency_graph.graph.get('deptypes', "{}")) self.set_progress(progress) @property @@ -153,16 +150,6 @@ class Analyzer: return int(self._dependency_graph.get_deptype(deptype)) - def _strip_build_dir(self, node): - """Small util function for making args match the graph paths.""" - - return str(Path(node).relative_to(self._build_dir)) - - def _strip_build_dirs(self, nodes): - """Small util function for making a list of nodes match graph paths.""" - - return [self._strip_build_dir(node) for node in nodes] - def set_progress(self, value=None): """Get a progress bar from the loaded graph.""" @@ -198,10 +185,10 @@ class Counter(Analyzer): class NodeCounter(Counter): """Counts and reports number of nodes in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.NODE.name @schema_check(schema_version=1) @@ -214,10 +201,10 @@ class NodeCounter(Counter): class EdgeCounter(Counter): """Counts and reports number of edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.EDGE.name @schema_check(schema_version=1) @@ -230,10 +217,10 @@ class EdgeCounter(Counter): class DirectEdgeCounter(Counter): """Counts and reports number of direct edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.DIR_EDGE.name @schema_check(schema_version=1) @@ -246,10 +233,10 @@ class DirectEdgeCounter(Counter): class TransEdgeCounter(Counter): """Counts and reports number of transitive edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.TRANS_EDGE.name @schema_check(schema_version=1) @@ -262,10 +249,10 @@ class TransEdgeCounter(Counter): class DirectPubEdgeCounter(Counter): """Counts and reports number of direct public edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.DIR_PUB_EDGE.name @schema_check(schema_version=1) @@ -281,10 +268,10 @@ class DirectPubEdgeCounter(Counter): class PublicEdgeCounter(Counter): """Counts and reports number of public edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.PUB_EDGE.name @schema_check(schema_version=1) @@ -297,10 +284,10 @@ class PublicEdgeCounter(Counter): class PrivateEdgeCounter(Counter): """Counts and reports number of private edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.PRIV_EDGE.name @schema_check(schema_version=1) @@ -314,10 +301,10 @@ class PrivateEdgeCounter(Counter): class InterfaceEdgeCounter(Counter): """Counts and reports number of interface edges in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.IF_EDGE.name @schema_check(schema_version=1) @@ -331,10 +318,10 @@ class InterfaceEdgeCounter(Counter): class LibCounter(Counter): """Counts and reports number of library nodes in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.LIB.name @schema_check(schema_version=1) @@ -347,10 +334,10 @@ class LibCounter(Counter): class ProgCounter(Counter): """Counts and reports number of program nodes in the graph.""" - def __init__(self, graph): + def __init__(self, dependency_graph): """Store graph and set type.""" - super().__init__(graph) + super().__init__(dependency_graph) self._count_type = CountTypes.PROG.name @schema_check(schema_version=1) @@ -360,7 +347,7 @@ class ProgCounter(Counter): return self.node_type_count(NodeProps.bin_type.name, 'Program') -def counter_factory(graph, counters, progressbar=True): +def counter_factory(dependency_graph, counters, progressbar=True): """Construct counters from a list of strings.""" counter_map = { @@ -382,7 +369,7 @@ def counter_factory(graph, counters, progressbar=True): counter_objs = [] for counter in counters: if counter in counter_map: - counter_obj = counter_map[counter](graph) + counter_obj = counter_map[counter](dependency_graph) counter_obj.set_progress(progressbar) counter_objs.append(counter_obj) @@ -395,11 +382,11 @@ def counter_factory(graph, counters, progressbar=True): class CommonDependents(Analyzer): """Finds common dependent nodes for a set of given dependency nodes.""" - def __init__(self, graph, nodes): + def __init__(self, dependency_graph, nodes): """Store graph and strip the nodes.""" - super().__init__(graph) - self._nodes = self._strip_build_dirs(nodes) + super().__init__(dependency_graph) + self._nodes = nodes @schema_check(schema_version=1) def run(self): @@ -419,11 +406,11 @@ class CommonDependents(Analyzer): class DirectDependents(Analyzer): """Finds direct dependent nodes for a given dependency node.""" - def __init__(self, graph, node): + def __init__(self, dependency_graph, node): """Store graph and strip the node.""" - super().__init__(graph) - self._node = self._strip_build_dir(node) + super().__init__(dependency_graph) + self._node = node @schema_check(schema_version=1) def run(self): @@ -445,11 +432,11 @@ class DirectDependents(Analyzer): class ExcludeDependents(Analyzer): """Finds dependents which depend on the first input node, but exclude the other input nodes.""" - def __init__(self, graph, nodes): + def __init__(self, dependency_graph, nodes): """Store graph and strip the nodes.""" - super().__init__(graph) - self._nodes = self._strip_build_dirs(nodes) + super().__init__(dependency_graph) + self._nodes = nodes @schema_check(schema_version=1) def run(self): @@ -509,11 +496,11 @@ class InDegreeOne(Analyzer): class GraphPaths(Analyzer): """Finds all paths between two nodes in the graph.""" - def __init__(self, graph, from_node, to_node): + def __init__(self, dependency_graph, from_node, to_node): """Store graph and strip the nodes.""" - super().__init__(graph) - self._from_node, self._to_node = self._strip_build_dirs([from_node, to_node]) + super().__init__(dependency_graph) + self._from_node, self._to_node = from_node, to_node @schema_check(schema_version=1) def run(self): @@ -545,11 +532,11 @@ class GraphPaths(Analyzer): class CriticalEdges(Analyzer): """Finds all edges between two nodes, where removing those edges disconnects the two nodes.""" - def __init__(self, graph, from_node, to_node): + def __init__(self, dependency_graph, from_node, to_node): """Store graph and strip the nodes.""" - super().__init__(graph) - self._from_node, self._to_node = self._strip_build_dirs([from_node, to_node]) + super().__init__(dependency_graph) + self._from_node, self._to_node = from_node, to_node @schema_check(schema_version=1) def run(self): @@ -672,7 +659,7 @@ class UnusedPublicLinter(Analyzer): report[LinterTypes.PUBLIC_UNUSED.name] = self.run() -def linter_factory(graph, linters, progressbar=True): +def linter_factory(dependency_graph, linters, progressbar=True): """Construct linters from a list of strings.""" linter_map = { @@ -685,7 +672,7 @@ def linter_factory(graph, linters, progressbar=True): linters_objs = [] for linter in linters: if linter in linter_map: - linters_objs.append(linter_map[linter](graph, progressbar)) + linters_objs.append(linter_map[linter](dependency_graph, progressbar)) else: print(f"Skipping unknown counter: {linter}") diff --git a/buildscripts/libdeps/libdeps/graph.py b/buildscripts/libdeps/libdeps/graph.py index 2e7dbbcd234..74446ceff6f 100644 --- a/buildscripts/libdeps/libdeps/graph.py +++ b/buildscripts/libdeps/libdeps/graph.py @@ -120,11 +120,6 @@ class LibdepsGraph(networkx.DiGraph): return self._deptypes[deptype] - def _strip_build_dir(self, node): - """Small util function for making args match the graph paths.""" - - return str(Path(node).resolve().relative_to(Path(self.graph['build_dir']).resolve())) - def get_direct_nonprivate_graph(self): """Get a graph view of direct nonprivate edges.""" |