diff --git a/snakemake/report/__init__.py b/snakemake/report/__init__.py index bfb061959..8c93fbda7 100644 --- a/snakemake/report/__init__.py +++ b/snakemake/report/__init__.py @@ -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) diff --git a/snakemake/report/data/__init__.py b/snakemake/report/data/__init__.py index 45fb7ccfe..6534e41b8 100644 --- a/snakemake/report/data/__init__.py +++ b/snakemake/report/data/__init__.py @@ -4,3 +4,4 @@ from .rules import render_rules from .runtimes import render_runtimes from .timeline import render_timeline +from .packages import get_packages diff --git a/snakemake/report/data/common.py b/snakemake/report/data/common.py new file mode 100644 index 000000000..ed7b5593c --- /dev/null +++ b/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) + ) diff --git a/snakemake/report/data/packages.py b/snakemake/report/data/packages.py new file mode 100644 index 000000000..58f9b0426 --- /dev/null +++ b/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} diff --git a/snakemake/report/template/components/abstract_results.js b/snakemake/report/template/components/abstract_results.js index fe51d294c..429082013 100644 --- a/snakemake/report/template/components/abstract_results.js +++ b/snakemake/report/template/components/abstract_results.js @@ -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() { diff --git a/snakemake/report/template/components/app.js b/snakemake/report/template/components/app.js index 2732e76dc..7b16be1f9 100644 --- a/snakemake/report/template/components/app.js +++ b/snakemake/report/template/components/app.js @@ -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')); \ No newline at end of file diff --git a/snakemake/report/template/components/content.js b/snakemake/report/template/components/content.js index aa3b089b8..081cc6f27 100644 --- a/snakemake/report/template/components/content.js +++ b/snakemake/report/template/components/content.js @@ -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 + ) + ); } } } \ No newline at end of file diff --git a/snakemake/report/template/components/icon.js b/snakemake/report/template/components/icon.js index f8fd391bf..49a4f5691 100644 --- a/snakemake/report/template/components/icon.js +++ b/snakemake/report/template/components/icon.js @@ -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" } ], diff --git a/snakemake/report/template/components/menu.js b/snakemake/report/template/components/menu.js index 7b1f3a967..aead13efa 100644 --- a/snakemake/report/template/components/menu.js +++ b/snakemake/report/template/components/menu.js @@ -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) { diff --git a/snakemake/report/template/components/navbar.js b/snakemake/report/template/components/navbar.js index f1276df00..f1edef648 100644 --- a/snakemake/report/template/components/navbar.js +++ b/snakemake/report/template/components/navbar.js @@ -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 } } diff --git a/snakemake/report/template/components/report_info.js b/snakemake/report/template/components/report_info.js new file mode 100644 index 000000000..5d4bf6506 --- /dev/null +++ b/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)) + } + ); + } +} \ No newline at end of file diff --git a/snakemake/report/template/index.html.jinja2 b/snakemake/report/template/index.html.jinja2 index 1decc2efa..38578ca84 100644 --- a/snakemake/report/template/index.html.jinja2 +++ b/snakemake/report/template/index.html.jinja2 @@ -9,7 +9,7 @@ Snakemake Report - + @@ -31,11 +31,11 @@
- - - - - + + + + + + + @@ -79,6 +83,7 @@ +