Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add html output for graph build-order and graph build-order-merge #16611

Merged
merged 13 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions conan/cli/commands/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from conan.cli.command import conan_command, conan_subcommand
from conan.cli.commands.list import prepare_pkglist_compact, print_serial
from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot
from conan.cli.formatters.graph.build_order_html import format_build_order_html
from conan.cli.formatters.graph.graph_info_text import format_graph_info
from conan.cli.printers.graph import print_graph_packages, print_graph_basic
from conan.errors import ConanException
Expand Down Expand Up @@ -62,7 +63,8 @@ def json_build_order(build_order):
cli_out_write(json.dumps(build_order, indent=4))


@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order})
@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order,
"html": format_build_order_html})
def graph_build_order(conan_api, parser, subparser, *args):
"""
Compute the build order of a dependency graph.
Expand Down Expand Up @@ -129,7 +131,8 @@ def graph_build_order(conan_api, parser, subparser, *args):
return install_order_serialized


@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order})
@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order,
"html": format_build_order_html})
def graph_build_order_merge(conan_api, parser, subparser, *args):
"""
Merge more than 1 build-order file.
Expand Down
266 changes: 266 additions & 0 deletions conan/cli/formatters/graph/build_order_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
from jinja2 import select_autoescape, Template

from conan.api.output import cli_out_write

build_order_html = r"""<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.0/cytoscape.min.js"
integrity="sha512-zHc90yHSbkgx0bvVpDK/nVgxANlE+yKN/jKy91tZ4P/vId8AL7HyjSpZqHmEujWDWNwxYXcfaLdYWjAULl35MQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
</head>

<body>
<style>
body {
font: 14px helvetica neue, helvetica, arial, sans-serif;
display: flex;
margin: 0;
padding: 0;
height: 100vh;
}

.sidebar {
background: #f9f9f9;
border-right: 1px solid #ccc;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
font-size: 14px;
width: 400px;
}

.content {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}

#cy {
flex-grow: 1;
}

#node-info {
margin-top: 20px;
background: #f9f9f9;
padding: 10px;
font-family: monospace;
white-space: pre-wrap;
font-size: 12px;
word-wrap: break-word;
}

.legend {
margin-top: 20px;
font-size: 14px;
}

.legend-item {
display: flex;
align-items: center;
margin-bottom: 5px;
}

.legend-color {
width: 20px;
height: 20px;
margin-right: 10px;
}
</style>

<div class="sidebar">
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background-color: #FFCC80;"></div>
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved
<span>All packages need to be built</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #FFEB3B;"></div>
<span>Some packages need to be built</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #A5D6A7;"></div>
<span>Cache</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #A5D6FF;"></div>
<span>Download</span>
</div>
</div>
<div id="node-info">
<p>Click on a node to see details.</p>
</div>
</div>
<div class="content">
<div id="cy"></div>
</div>

<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
var buildOrderData = {{ build_order | tojson }};

var elements = [];
var edges = [];

var yOffset = 50;
var posX = 0;
var posY = 0;
var columns = 4;

buildOrderData.forEach((step, stepIndex) => {
var stepId = 'step' + stepIndex;
elements.push({
data: { id: stepId, label: 'Step ' + (stepIndex + 1) },
position: { x: posX, y: posY }
});

step.forEach((lib, libIndex) => {
var libId = stepId + '_lib' + libIndex;
var libLabel = lib.ref.split('#')[0];

// Determine the type of the library
var libData = Array.isArray(lib.packages) ? lib.packages[0][0] : lib;
var libType = libData.binary || libData.build || libData.cache;

var isAllBuild = true;
var isSomeBuild = false;
var shape = 'rectangle';
var borderColor = '#00695C';

if (Array.isArray(lib.packages)) {
lib.packages.forEach(pkgArray => {
pkgArray.forEach(pkg => {
if (pkg.binary === "Build") {
isSomeBuild = true;
} else {
isAllBuild = false;
}
if (pkg.context === "build") {
shape = 'cut-rectangle';
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved
borderColor = '#0000FF'; // Different border color for build context
}
});
});
}

var nodeColor;
if (isAllBuild) {
nodeColor = "#FFCC80"; // Light orange for all build
} else if (isSomeBuild) {
nodeColor = "#FFEB3B"; // Yellow for some build
} else if (libType === "Cache") {
nodeColor = "#A5D6A7"; // Light green
} else if (libType === "Download") {
nodeColor = "#A5D6FF"; // Light blue
} else {
nodeColor = "#FFFFFF"; // Default color
}

if (libIndex % columns === 0) {
posX = 0;
posY += yOffset; // move to the next row
}

elements.push({
data: { id: libId, parent: stepId, label: libLabel, info: lib, color: nodeColor, shape: shape, borderColor: borderColor },
position: { x: posX + libLabel.length / 2.0 * 12, y: posY }
});
posX += libLabel.length * 12;
});

if (stepIndex > 0) {
var prevStepId = 'step' + (stepIndex - 1);
edges.push({ data: { id: prevStepId + '_to_' + stepId, source: prevStepId, target: stepId } });
}

posY += yOffset * 2;
posX = 0;
});

var cy = cytoscape({
container: document.getElementById('cy'),
boxSelectionEnabled: false,
style: [
{
selector: 'node[color][shape][borderColor]',
style: {
'shape': 'data(shape)',
'content': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'background-color': 'data(color)',
'border-color': 'data(borderColor)',
'border-width': 1,
'width': 'label',
'height': 'label',
'padding': '5px',
'font-family': 'monospace',
'font-size': '16px'
}
},
{
selector: ':parent',
style: {
'text-valign': 'top',
'text-halign': 'center',
'shape': 'round-rectangle',
'background-opacity': 0.1,
'border-color': '#004D40',
'border-width': 2,
'padding': 10,
'font-family': 'monospace',
'font-size': '16px'
}
},
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'target-arrow-shape': 'triangle',
'line-color': '#004D40',
'target-arrow-color': '#004D40',
'width': 2
}
}
],
elements: {
nodes: elements,
edges: edges
},
layout: {
name: 'preset',
padding: 5,
fit: true
}
});

// Add click event listener to nodes
cy.on('tap', 'node', function(evt){
var node = evt.target;
var info = node.data('info');
var infoHtml = '';
for (var key in info) {
if (info.hasOwnProperty(key)) {
infoHtml += '<p><strong>' + key + ':</strong> ' + JSON.stringify(info[key], null, 2) + '</p>';
}
}
document.getElementById('node-info').innerHTML = infoHtml;
});
});
</script>
</body>
</html>
"""


def _render_build_order(build_order, template):
from conans import __version__ as client_version
context = {'build_order': build_order, 'version': client_version, }
return template.render(context)


def format_build_order_html(build_order):
build_order = build_order["order"] if isinstance(build_order, dict) else build_order
template = Template(build_order_html, autoescape=select_autoescape(['html', 'xml']))
cli_out_write(_render_build_order(build_order, template))
13 changes: 13 additions & 0 deletions test/integration/command_v2/test_info_build_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ def test_info_build_order():
assert bo_json["order_by"] == "recipe"
assert bo_json["order"] == result

# test html format
c.run("graph build-order consumer --build=missing --format=html")
assert "<body>" in c.stdout
c.run("graph build-order consumer --order-by=recipe --format=html")
assert "<body>" in c.stdout
c.run("graph build-order consumer --order-by=configuration --format=html")
assert "<body>" in c.stdout


def test_info_build_order_configuration():
c = TestClient()
Expand Down Expand Up @@ -332,6 +340,11 @@ def test_info_build_order_merge_multi_product():

assert bo_json == result

# test that html format for build-order-merge generates something
c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=html")
assert "<body>" in c.stdout



def test_info_build_order_merge_multi_product_configurations():
c = TestClient()
Expand Down