Skip to content

Instantly share code, notes, and snippets.

@Slavenin
Last active January 13, 2020 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Slavenin/fec07f8eb07f6721e666f561e86a847d to your computer and use it in GitHub Desktop.
Save Slavenin/fec07f8eb07f6721e666f561e86a847d to your computer and use it in GitHub Desktop.
Vizualize graph of your react app dependencies like a boss
#add deps
yarn add --dev react-spring 3d-force-graph blueimp-md5 dependency-cruiser 
yarn add @material-ui/core @material-ui/icons @material-ui/lab

#create deps file
node_modules/.bin/depcruise --exclude "^node_modules" --output-type json assets/js > deps.json
import React, { useEffect } from 'react';
import ForceGraph3D from '3d-force-graph';
import md5 from 'blueimp-md5';
import { makeStyles } from '@material-ui/core/styles';
import Close from '@material-ui/icons/Close';
import Input from '@material-ui/core/Input';
import InputAdornment from '@material-ui/core/InputAdornment';
import IconButton from '@material-ui/core/IconButton';
import SelectedTree from './Tree';
//!!!change this path to data file from prev step
import data from '../../../../deps.json'
const useStyles = makeStyles(() => ({
root: {
position: 'absolute',
zIndex: 1,
backgroundColor: 'white'
},
searchList: {
position: 'absolute',
zIndex: 1,
backgroundColor: 'white',
marginTop: '35px',
maxWidth: '257px',
maxHeight: '500px',
overflow: 'auto'
}
}));
//да это не стиль реакта, но всунуть ForceGraph3D в стейт нельзя
//заводить под это отдельный контекст выглядит как оверхед
//если всё усложнится, то можно вынести эти переменные в
// отдельный контекст и там всё делать
const myGraph = ForceGraph3D();
let highlightNodes = [];
let highlightLink = [];
let globalQ = '';
let files = {
nodes: [],
links: []
};
function hashCode(str) { // java String#hashCode
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return hash;
}
function intToRGB(i) {
var c = (i & 0x00FFFFFF)
.toString(16)
.toUpperCase();
return "00000".substring(0, 6 - c.length) + c;
}
const DepsViz = () => {
let divRef = React.createRef();
const classes = useStyles();
const [search, setSearch] = React.useState('');
const handleChange = prop => event => {
setSearch(event.target.value);
};
const handleClickShowSearch = () => {
setSearch('');
};
const handleMouseDownSearch = event => {
event.preventDefault();
};
const handleNodeClick = node => {
highlightNodes = [node];
const links = files.links.filter((el) => el.source.id === node.id);
highlightNodes = [...highlightNodes, ...links.map(({ target }) => target)];
highlightLink = links;
updateHighlight();
};
//хз что тм либа делает с колбэками, но на изменение стейта им всё-равно
const updateNodeColor = node => (
(highlightNodes.indexOf(node) === -1)
? (
(globalQ === '' && !highlightNodes.length)
? node.color
: 'rgba(0,255,255,0.6)'
)
: 'rgb(255,0,0,1)'
);
const directionalParticles = link => (highlightLink.indexOf(link) !== -1) ? 4 : 0;
const linkWidth = link => (highlightLink.indexOf(link) !== -1) ? 4 : 2;
useEffect(() => {
data.modules.forEach((el) => {
let hash = md5(el.source);
let deps = [];
let sName = el.source.replace('assets/js', '');
el.dependencies.forEach((d) => {
let id = md5(d.resolved);
let name = d.resolved.replace('assets/js', '');
deps.push({id, name});
files.links.push({
source: hash,
target: id,
name: `${sName} >>> ${name}`
})
});
files.nodes.push({
id: hash,
deps,
color: "#" + intToRGB(hashCode(hash)),
name: sName,
val: 10 * el.dependencies.length / 2,
group: hash
});
});
myGraph(divRef.current)
.enableNodeDrag(false)
.enableNavigationControls(true)
.linkOpacity(0.5)
.graphData(files)
.nodeColor(updateNodeColor)
.linkWidth(linkWidth)
.linkDirectionalParticles(directionalParticles)
.linkDirectionalParticleWidth(4)
.linkDirectionalArrowLength(3.5)
.linkDirectionalArrowRelPos(1)
.onBackgroundClick(() => {
highlightNodes = [];
highlightLink = [];
setSearch('');
updateHighlight();
})
.onNodeClick(handleNodeClick)
}, []);
useEffect(() => {
globalQ = search;
highlightLink = [];
if (search) {
const reg = new RegExp(search, 'ui');
highlightNodes = files.nodes.filter((el) => el.name.search(reg) !== -1);
} else {
highlightNodes = [];
}
updateHighlight();
}, [search]);
const updateHighlight = () => {
myGraph
.nodeColor(myGraph.nodeColor())
.linkWidth(myGraph.linkWidth())
.linkDirectionalParticles(myGraph.linkDirectionalParticles());
};
return (
<>
<Input
className={classes.root}
id="standard-adornment-search"
value={search}
onChange={handleChange('search')}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle search visibility"
onClick={handleClickShowSearch}
onMouseDown={handleMouseDownSearch}
>
<Close/>
</IconButton>
</InputAdornment>
}
/>
{search !== ''
&& (
<div className={classes.searchList}>
<SelectedTree list={highlightNodes} />
</div>
)}
<div ref={divRef}></div>
</>
);
};
export default DepsViz;
import React from 'react';
import { fade, makeStyles, withStyles } from '@material-ui/core/styles';
import SvgIcon from '@material-ui/core/SvgIcon';
import TreeView from '@material-ui/lab/TreeView';
import TreeItem from '@material-ui/lab/TreeItem';
import Collapse from '@material-ui/core/Collapse';
import { useSpring, animated } from 'react-spring/web.cjs'; // web.cjs is required for IE 11 support
function MinusSquare(props) {
return (
<SvgIcon fontSize="inherit" {...props}>
{/* tslint:disable-next-line: max-line-length */}
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 11.023h-11.826q-.375 0-.669.281t-.294.682v0q0 .401.294 .682t.669.281h11.826q.375 0 .669-.281t.294-.682v0q0-.401-.294-.682t-.669-.281z"/>
</SvgIcon>
);
}
function PlusSquare(props) {
return (
<SvgIcon fontSize="inherit" {...props}>
{/* tslint:disable-next-line: max-line-length */}
<path
d="M22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0zM17.873 12.977h-4.923v4.896q0 .401-.281.682t-.682.281v0q-.375 0-.669-.281t-.294-.682v-4.896h-4.923q-.401 0-.682-.294t-.281-.669v0q0-.401.281-.682t.682-.281h4.923v-4.896q0-.401.294-.682t.669-.281v0q.401 0 .682.281t.281.682v4.896h4.923q.401 0 .682.281t.281.682v0q0 .375-.281.669t-.682.294z"/>
</SvgIcon>
);
}
function CloseSquare(props) {
return (
<SvgIcon className="close" fontSize="inherit" {...props}>
{/* tslint:disable-next-line: max-line-length */}
<path
d="M17.485 17.512q-.281.281-.682.281t-.696-.268l-4.12-4.147-4.12 4.147q-.294.268-.696.268t-.682-.281-.281-.682.294-.669l4.12-4.147-4.12-4.147q-.294-.268-.294-.669t.281-.682.682-.281.696 .268l4.12 4.147 4.12-4.147q.294-.268.696-.268t.682.281 .281.669-.294.682l-4.12 4.147 4.12 4.147q.294.268 .294.669t-.281.682zM22.047 22.074v0 0-20.147 0h-20.12v0 20.147 0h20.12zM22.047 24h-20.12q-.803 0-1.365-.562t-.562-1.365v-20.147q0-.776.562-1.351t1.365-.575h20.147q.776 0 1.351.575t.575 1.351v20.147q0 .803-.575 1.365t-1.378.562v0z"/>
</SvgIcon>
);
}
function TransitionComponent(props) {
const style = useSpring({
from: { opacity: 0, transform: 'translate3d(20px,0,0)' },
to: { opacity: props.in ? 1 : 0, transform: `translate3d(${props.in ? 0 : 20}px,0,0)` },
});
return (
<animated.div style={style}>
<Collapse {...props} />
</animated.div>
);
}
const StyledTreeItem = withStyles(theme => ({
iconContainer: {
'& .close': {
opacity: 0.3,
},
},
group: {
marginLeft: 12,
paddingLeft: 12,
borderLeft: `1px dashed ${fade(theme.palette.text.primary, 0.4)}`,
},
}))(props => <TreeItem {...props} TransitionComponent={TransitionComponent}/>);
const useStyles = makeStyles({
root: {
flexGrow: 1
},
});
const SelectedTree = ({ list }) => {
const classes = useStyles();
return (
<TreeView
className={classes.root}
defaultExpanded={['1']}
defaultCollapseIcon={<MinusSquare/>}
defaultExpandIcon={<PlusSquare/>}
defaultEndIcon={<CloseSquare/>}
>
{list.map((el) => (
<StyledTreeItem key={el.id} nodeId={el.id} label={el.name}>
{el.deps.map((d) => <StyledTreeItem key={d.id} nodeId={d.id} label={d.name}/>)}
</StyledTreeItem>
))}
</TreeView>
);
};
export default SelectedTree;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment