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

fix: add about page to report, including embedded packages and licenses #1511

Merged
merged 2 commits into from Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 3 additions & 13 deletions snakemake/report/__init__.py
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions snakemake/report/data/__init__.py
Expand Up @@ -4,3 +4,4 @@
from .rules import render_rules
from .runtimes import render_runtimes
from .timeline import render_timeline
from .packages import get_packages
17 changes: 17 additions & 0 deletions snakemake/report/data/common.py
@@ -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)
)
82 changes: 82 additions & 0 deletions snakemake/report/data/packages.py
@@ -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}
19 changes: 18 additions & 1 deletion snakemake/report/template/components/abstract_results.js
Expand Up @@ -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() {
Expand Down
13 changes: 13 additions & 0 deletions snakemake/report/template/components/app.js
Expand Up @@ -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;
}
Expand All @@ -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,
})
}

Expand All @@ -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'));
12 changes: 11 additions & 1 deletion snakemake/report/template/components/content.js
Expand Up @@ -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
)
);
}
}
}
6 changes: 6 additions & 0 deletions snakemake/report/template/components/icon.js
Expand Up @@ -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" }
],
Expand Down
6 changes: 3 additions & 3 deletions snakemake/report/template/components/menu.js
Expand Up @@ -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()
)
}
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions snakemake/report/template/components/navbar.js
Expand Up @@ -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 });
}
}

Expand Down Expand Up @@ -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 }
)
}
}

Expand All @@ -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 }
}
Expand Down
23 changes: 23 additions & 0 deletions snakemake/report/template/components/report_info.js
@@ -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))
}
);
}
}
17 changes: 11 additions & 6 deletions snakemake/report/template/index.html.jinja2
Expand Up @@ -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>

Expand All @@ -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}};
Expand Down Expand Up @@ -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>
Expand All @@ -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>
Expand Down