Skip to content

Commit

Permalink
Sync eng/common directory with azure-sdk-tools repository (#7202)
Browse files Browse the repository at this point in the history
  • Loading branch information
azure-sdk authored Feb 7, 2020
1 parent 8d7ac6e commit 2da0427
Show file tree
Hide file tree
Showing 2 changed files with 491 additions and 0 deletions.
356 changes: 356 additions & 0 deletions eng/common/InterdependencyGraph.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Interdependency Graph</title>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.4/dagre.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/cytoscape-dagre.min.js"></script>
<script type="application/javascript">
const renderGraph = (data) => {
const config = {
container: document.getElementById('cy'),
elements: [],
autounselectify: true,

layout: {
name: 'dagre',
ranker: 'tight-tree',
nodeSep: 10,
rankSep: 400,
padding: 10
},

style: [
{
selector: '.hidden',
style: {
'display': 'none'
}
},
{
selector: 'node',
style: {
'background-color': '#fff',
'border-color': '#333',
'border-width': '1px',
'height': 'label',
'label': 'data(label)',
'padding': '8px',
'shape': 'round-rectangle',
'text-halign': 'center',
'text-valign': 'center',
'text-wrap': 'wrap',
'width': 'label'
}
},
{
selector: 'node.internal',
style: {
'background-color': '#7f7'
}
},
{
selector: 'node.internalbinary',
style: {
'background-color': '#fb7'
}
},
{
selector: 'node.collapsed',
style: {
'background-color': '#b7f'
}
},
{
selector: 'node.search',
style: {
'background-color': '#ff7',
'border-width': '6px',
'display': 'element'
}
},
{
selector: 'node.highlight',
style: {
'background-color': '#fff',
'border-width': '6px',
'display': 'element'
}
},
{
selector: 'node.highlight.in',
style: {
'border-color': '#7bf'
}
},
{
selector: 'node.highlight.out',
style: {
'border-color': '#f77'
}
},
{
selector: 'node.highlight.source',
style: {
'border-color': '#f77'
}
},
{
selector: 'node.highlight.internal',
style: {
'background-color': '#7f7'
}
},
{
selector: 'node.highlight.internalbinary',
style: {
'background-color': '#fb7'
}
},
{
selector: 'node.highlight.collapsed',
style: {
'background-color': '#b7f'
}
},
{
selector: 'node.highlight.search',
style: {
'background-color': '#ff7'
}
},
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'label': 'data(label)',
'line-color': '#333',
'target-arrow-color': '#333',
'target-arrow-shape': 'triangle',
'width': '1.5px'
}
},
{
selector: 'edge.highlight',
style: {
'display': 'element',
'width': '6px'
}
},
{
selector: 'edge.highlight.in',
style: {
'line-color': '#7bf',
'target-arrow-color': '#7bf'
}
},
{
selector: 'edge.highlight.out',
style: {
'line-color': '#f77',
'target-arrow-color': '#f77'
}
}
]
}

// Add the nodes
for (const pkg of Object.keys(data)) {
config.elements.push({
data: {
id: pkg,
label: `${data[pkg].name}\n${data[pkg].version}`
},
classes: data[pkg].type
})
}

// Add the edges
for (const pkg of Object.keys(data)) {
for (const dep of data[pkg].deps) {
const dest = `${dep.name}:${dep.version}`
const edge = {
data: {
id: `${pkg}:${dest}`,
source: pkg,
target: dest,
label: dep.label || ''
}
}
config.elements.push(edge)
}
}

const cy = cytoscape(config)

cy.on('mouseover', 'node', event => {
const element = event.target
if (element.hasClass('pinned')) { return }

element.addClass('highlight source')
element.outgoers().addClass('highlight out')
element.incomers().addClass('highlight in')
})

cy.on('mouseout', 'node', event => {
const element = event.target
if (element.hasClass('pinned')) { return }

element.removeClass('source')
if (!element.hasClass('in') && !element.hasClass('out')) {
element.removeClass('highlight')
}

element.outgoers().forEach(e => {
e.removeClass('out')
if (!e.hasClass('in') && !e.hasClass('source')) {
e.removeClass('highlight')
}
})

element.incomers().forEach(e => {
e.removeClass('in')
if (!e.hasClass('out') && !e.hasClass('source')) {
e.removeClass('highlight')
}
})
})

cy.on('cxttap', 'node', event => {
const element = event.target
if (!element.hasClass('pinned')) {
element.addClass('pinned')

} else {
element.removeClass('pinned')
}
})

document.addEventListener('keydown', event => {
if (document.activeElement.id === 'search') { return }

if (event.key === '-') {
cy.nodes('.internal').forEach(node => {
if (!node.hasClass('hidden')) {
triggerCollapse(cy, node, true)
}
})
} else if (event.key === '=') {
cy.nodes('.internal').forEach(node => {
triggerCollapse(cy, node, false)
})
}
})

let searchTerm = ''
document.getElementById('search').addEventListener('input', event => {
const newValue = event.target.value
if (searchTerm !== newValue) {
searchTerm = newValue
cy.nodes().removeClass('search')
if (searchTerm.length > 0) {
const matches = cy.nodes(`[label *= '${searchTerm}']`)
matches.addClass('search')
document.getElementById('matches').innerText = `Matches: ${matches.length}`
} else {
document.getElementById('matches').innerText = ''
}
}
})

cy.on('tap', 'node', event => {
const element = event.target
const collapse = !element.hasClass('collapsed')
triggerCollapse(cy, element, collapse)
element.emit('mouseout')
element.emit('mouseover')
})
}

const triggerCollapse = (cy, element, collapse) => {
if (element.outgoers().length === 0) { return }

if (collapse) {
element.addClass('collapsed')
} else {
element.removeClass('collapsed')
}

if (collapse) {
element.outgoers('edge').addClass('hidden')
const orphans = cy.filter(e => {
return e.isNode() &&
!e.hasClass('internal') &&
!e.incomers('edge').some(g => !g.hasClass('hidden'))
})
orphans.forEach(o => {
o.addClass('hidden')
o.successors().addClass('hidden') // no-op when only one tier of external nodes are present
})
} else {
element.outgoers().removeClass('hidden')
}
}
</script>
<style>
body {
margin: 10 auto;
color: #333;
font-weight: 300;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
pointer-events: none;
}

h1 {
font-size: 3em;
font-weight: 300;
z-index: -2;
}

#cy {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: -1;
pointer-events: all;
}

.panel {
display: inline-block;
}

.panel div {
margin: 4px auto;
}

.panel input {
pointer-events: all;
}
</style>
</head>
<body>
<div class="panel">
<h1>Dependency Graph</h1>
<label for="search">Search:</label>
<input id="search" type="search" autocomplete="off" size="64" />
<div id="matches"></div>
</div>
<div id="cy"></div>
<script type="application/javascript">
const params = new URLSearchParams(window.location.search);
const src = params.get("data") || "data.js";
const script = document.createElement("script");
script.src = src;
script.async = false;
script.addEventListener("load", () => renderGraph(data));
script.addEventListener("error", e => {
const dest = document.getElementsByClassName("panel")[0];
dest.innerText = `Failed to load ${src}`;
});
document.head.appendChild(script);
</script>
</body>
</html>
Loading

0 comments on commit 2da0427

Please sign in to comment.