SERVER-52576 Added libdeps graph visualizer web service with basic features.

This commit is contained in:
Daniel Moody
2021-02-25 21:25:11 +00:00
committed by Evergreen Agent
parent dac6c3ae77
commit 27aecefac7
42 changed files with 2366 additions and 55 deletions

View File

@@ -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>
);
}

View File

@@ -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)
);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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")
);

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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;

View File

@@ -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;