Skip to content
Permalink
Browse files
fix: add about page to report, including embedded packages and licens…
…es (#1511)

* fix: add about page to reports, listing embedded software and licenses

* add heroicons
  • Loading branch information
johanneskoester committed Mar 23, 2022
1 parent dc65e29 commit 142a45256f1b192246dd8e9843abedb24badecc6
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 24 deletions.
@@ -31,6 +31,7 @@
from docutils.core import publish_file, publish_parts

from snakemake import script, wrapper, notebook
from snakemake.report.data.common import get_resource_as_string
from snakemake.utils import format
from snakemake.logging import logger
from snakemake.io import (
@@ -542,18 +543,6 @@ def filename(self):
return os.path.basename(self.path)


def get_resource_as_string(path_or_uri):
if is_local_file(path_or_uri):
return open(Path(__file__).parent / "template" / path_or_uri).read()
else:
r = requests.get(path_or_uri)
if r.status_code == requests.codes.ok:
return r.text
raise WorkflowError(
"Failed to download resource needed for " "report: {}".format(path_or_uri)
)


def expand_labels(labels, wildcards, job):
if labels is None:
return None
@@ -864,6 +853,7 @@ class Snakemake:
rules = data.render_rules(rules)
runtimes = data.render_runtimes(runtimes)
timeline = data.render_timeline(timeline)
packages = data.get_packages()

template = env.get_template("index.html.jinja2")

@@ -877,11 +867,11 @@ class Snakemake:
workflow_desc=json.dumps(text),
runtimes=runtimes,
timeline=timeline,
packages=packages,
pygments_css=HtmlFormatter(style="stata-dark").get_style_defs(".source"),
custom_stylesheet=custom_stylesheet,
logo=data_uri_from_file(Path(__file__).parent / "template" / "logo.svg"),
now=now,
version=__version__.split("+")[0],
)

# TODO look into supporting .WARC format, also see (https://webrecorder.io)
@@ -4,3 +4,4 @@
from .rules import render_rules
from .runtimes import render_runtimes
from .timeline import render_timeline
from .packages import get_packages
@@ -0,0 +1,17 @@
from pathlib import Path
from snakemake.common import is_local_file
from snakemake.exceptions import WorkflowError


def get_resource_as_string(path_or_uri):
import requests

if is_local_file(path_or_uri):
return open(Path(__file__).parent.parent / "template" / path_or_uri).read()
else:
r = requests.get(path_or_uri)
if r.status_code == requests.codes.ok:
return r.text
raise WorkflowError(
"Failed to download resource needed for " "report: {}".format(path_or_uri)
)
@@ -0,0 +1,82 @@
import json
from snakemake.exceptions import WorkflowError
from snakemake.report.data.common import get_resource_as_string
import snakemake


def get_packages():
try:
import pygments
except ImportError:
raise WorkflowError(
"Python package pygments must be installed to create reports."
)

return Packages(
{
"snakemake": Package(
version=snakemake.__version__.split("+")[0],
license_url="https://raw.githubusercontent.com/snakemake/snakemake/main/LICENSE.md",
),
"pygments": Package(
version=pygments.__version__,
license_url="https://raw.githubusercontent.com/pygments/pygments/master/LICENSE",
),
"tailwindcss": Package(
version="3.0",
license_url="https://raw.githubusercontent.com/tailwindlabs/tailwindcss/master/LICENSE",
url="https://cdn.tailwindcss.com/3.0.23?plugins=forms@0.4.0,typography@0.5.2",
),
"react": Package(
version="17",
license_url="https://raw.githubusercontent.com/facebook/react/main/LICENSE",
main="https://unpkg.com/react@17/umd/react.development.js",
dom="https://unpkg.com/react-dom@17/umd/react-dom.development.js",
),
"vega": Package(
version="5.21",
url="https://cdnjs.cloudflare.com/ajax/libs/vega/5.21.0/vega.js",
license_url="https://raw.githubusercontent.com/vega/vega/main/LICENSE",
),
"vega-lite": Package(
version="5.2",
url="https://cdnjs.cloudflare.com/ajax/libs/vega-lite/5.2.0/vega-lite.js",
license_url="https://raw.githubusercontent.com/vega/vega-lite/next/LICENSE",
),
"vega-embed": Package(
version="6.20",
url="https://cdnjs.cloudflare.com/ajax/libs/vega-embed/6.20.8/vega-embed.js",
license_url="https://raw.githubusercontent.com/vega/vega-embed/next/LICENSE",
),
"heroicons": Package(
version="1.0.6",
license_url="https://raw.githubusercontent.com/tailwindlabs/heroicons/master/LICENSE",
),
}
)


class Packages:
def __init__(self, packages):
self.packages = packages

def __getitem__(self, package):
return self.packages[package]

def get_json(self):
return json.dumps(
{name: package.get_record() for name, package in self.packages.items()}
)


class Package:
def __init__(self, version=None, license_url=None, url=None, **urls):
self.version = version
self.license = get_resource_as_string(license_url)
if url is not None:
self.url = url
else:
self.urls = urls

def get_record(self):
return {"version": self.version, "license": self.license}
@@ -39,7 +39,24 @@ class AbstractResults extends React.Component {
}

getLabels() {
return Array.from(new Set(this.getResults().map(function ([path, result]) { return Object.keys(result.labels) }).flat())).sort();
let first_index = {};
console.log(this.getResults());
this.getResults().map(function ([path, result]) {
console.log(path, result);
let i = 0;
for (let key in result.labels) {
if (!(key in first_index)) {
first_index[key] = i;
}
i += 1;
}
})
let labels = Object.keys(first_index);

return labels.sort(function (a, b) {
return first_index[a] - first_index[b];
});
//return Array.from(new Set(this.getResults().map(function ([path, result]) { return Object.keys(result.labels) }).flat())).sort();
}

isLabelled() {
@@ -11,6 +11,7 @@ class App extends React.Component {
this.setView = this.setView.bind(this);
this.showCategory = this.showCategory.bind(this);
this.showResultInfo = this.showResultInfo.bind(this);
this.showReportInfo = this.showReportInfo.bind(this);
// store in global variable
app = this;
}
@@ -33,6 +34,7 @@ class App extends React.Component {
searchTerm: view.searchTerm || this.state.searchTerm,
resultPath: view.resultPath || this.state.resultPath,
contentPath: view.contentPath || this.state.contentPath,
contentText: view.contentText || this.state.contentText,
})
}

@@ -46,9 +48,20 @@ class App extends React.Component {
this.setView({ navbarMode: mode, category: category, subcategory: subcategory })
}

showReportInfo() {
this.setView({ navbarMode: "reportinfo" });
}

showResultInfo(resultPath) {
this.setView({ navbarMode: "resultinfo", resultPath: resultPath });
}

showLicense(package_name) {
this.setView({
content: "text",
contentText: packages[package_name].license
});
}
}

ReactDOM.render(e(App), document.querySelector('#app'));
@@ -59,7 +59,17 @@ class ContentDisplay extends React.Component {
return e(
"iframe",
{ src: this.props.app.state.contentPath, className: "w-screen h-screen" }
)
);
case "text":
return e(
"div",
{ className: "p-3 w-3/4" },
e(
"pre",
{ className: "whitespace-pre-line text-sm" },
this.props.app.state.contentText
)
);
}
}
}
@@ -3,6 +3,12 @@
class Icon extends React.Component {
// paths are imported from https://heroicons.com
paths = {
"dots-vertical": [
{ path: "M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" }
],
"cube": [
{ path: "M11 17a1 1 0 001.447.894l4-2A1 1 0 0017 15V9.236a1 1 0 00-1.447-.894l-4 2a1 1 0 00-.553.894V17zM15.211 6.276a1 1 0 000-1.788l-4.764-2.382a1 1 0 00-.894 0L4.789 4.488a1 1 0 000 1.788l4.764 2.382a1 1 0 00.894 0l4.764-2.382zM4.447 8.342A1 1 0 003 9.236V15a1 1 0 00.553.894l4 2A1 1 0 009 17v-5.764a1 1 0 00-.553-.894l-4-2z" }
],
"chevron-right": [
{ rule: "evenodd", path: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" }
],
@@ -14,6 +14,7 @@ class Menu extends AbstractMenu {
this.getHeading(),
this.getMenuItem("Workflow", "share", this.showWorkflow),
this.getMenuItem("Statistics", "chart", this.showStatistics),
this.getMenuItem("About", "information-circle", this.props.app.showReportInfo),
this.getCategoryMenumitems()
)
}
@@ -47,9 +48,8 @@ class Menu extends AbstractMenu {
return [];
} else {
let items = [e(
"li",
{ key: "Results", className: "uppercase font-bold p-1 mt-2" },
"Results"
ListHeading,
{ key: "Results", text: "Result" }
)];

items.push(...Object.keys(categories).map(function (category) {
@@ -104,6 +104,8 @@ class Navbar extends React.Component {
return e(ResultInfo, { resultPath: this.props.app.state.resultPath, app: this.props.app });
case "ruleinfo":
return e(RuleInfo, { rule: this.props.app.state.ruleinfo });
case "reportinfo":
return e(ReportInfo, { app: this.props.app });
}
}

@@ -147,6 +149,11 @@ class Navbar extends React.Component {
{ entries: [this.getMenuBreadcrumb(), this.getRuleBreadcrumb(), this.getRuleinfoBreadcrumb()], setView: setView }
)
}
case "reportinfo":
return e(
Breadcrumbs,
{ entries: [this.getMenuBreadcrumb(), this.getReportInfoBreadcrumb()], setView: setView }
)
}
}

@@ -155,6 +162,11 @@ class Navbar extends React.Component {
return { name: "menu", icon: "home", func: function () { setView({ navbarMode: "menu", category: undefined, subcategory: undefined }) } };
}

getReportInfoBreadcrumb() {
let setView = this.props.app.setView;
return { name: "reportinfo", icon: "information-circle", func: function () { setView({ navbarMode: "reportinfo", category: undefined, subcategory: undefined }) } };
}

getRuleBreadcrumb() {
return { name: "Rule", func: undefined }
}
@@ -0,0 +1,23 @@
'use strict';

class ReportInfo extends AbstractMenu {
render() {
return e(
"ul",
{},
e(
ListHeading,
{ text: "Embedded packages" },
),
this.getPackages()
);
}

getPackages() {
let _this = this;
return Object.entries(packages).map(function ([name, record]) {
return _this.getMenuItem(`${name} ${record.version}`, "cube", () => _this.props.app.showLicense(name))
}
);
}
}
@@ -9,7 +9,7 @@
<meta name="author" content="">
<title>Snakemake Report</title>

<script>{{ "https://cdn.tailwindcss.com?plugins=forms,typography"|get_resource_as_string }}</script>
<script>{{ packages["tailwindcss"].url|get_resource_as_string }}</script>
<style>{{ pygments_css|safe }}</style>
<style>{{ "style.css"|get_resource_as_string }}</style>

@@ -31,11 +31,11 @@
<div id="app">
</div>

<script>{{"https://unpkg.com/react@17/umd/react.development.js"|get_resource_as_string}}</script>
<script>{{"https://unpkg.com/react-dom@17/umd/react-dom.development.js"|get_resource_as_string}}</script>
<script>{{"https://cdnjs.cloudflare.com/ajax/libs/vega/5.21.0/vega.js"|get_resource_as_string}}</script>
<script>{{"https://cdnjs.cloudflare.com/ajax/libs/vega-lite/5.2.0/vega-lite.js"|get_resource_as_string}}</script>
<script>{{"https://cdnjs.cloudflare.com/ajax/libs/vega-embed/6.20.8/vega-embed.js"|get_resource_as_string}}</script>
<script>{{packages["react"].urls["main"]|get_resource_as_string}}</script>
<script>{{packages["react"].urls["dom"]|get_resource_as_string}}</script>
<script>{{packages["vega"].url|get_resource_as_string}}</script>
<script>{{packages["vega-lite"].url|get_resource_as_string}}</script>
<script>{{packages["vega-embed"].url|get_resource_as_string}}</script>

<script id="workflow-desc">
var workflow_desc = {{workflow_desc}};
@@ -65,6 +65,10 @@
var rules = {{rules}};
</script>

<script id="packages">
var packages = {{packages.get_json()}};
</script>

<script id="logo">
var logo_uri = "{{logo}}";
</script>
@@ -79,6 +83,7 @@
<script>{{"components/abstract_results.js"|get_resource_as_string}}</script>
<script>{{"components/breadcrumbs.js"|get_resource_as_string}}</script>
<script>{{"components/menu.js"|get_resource_as_string}}</script>
<script>{{"components/report_info.js"|get_resource_as_string}}</script>
<script>{{"components/category.js"|get_resource_as_string}}</script>
<script>{{"components/subcategory.js"|get_resource_as_string}}</script>
<script>{{"components/search_results.js"|get_resource_as_string}}</script>

0 comments on commit 142a452

Please sign in to comment.