-
Notifications
You must be signed in to change notification settings - Fork 849
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Sync eng/common directory with azure-sdk-tools repository (#7202)
- Loading branch information
Showing
2 changed files
with
491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.