diff --git a/config/plugins/visualizations/common/templates/script_entry_point.mako b/config/plugins/visualizations/common/templates/script_entry_point.mako index dfdf4137666b..80ad34908930 100644 --- a/config/plugins/visualizations/common/templates/script_entry_point.mako +++ b/config/plugins/visualizations/common/templates/script_entry_point.mako @@ -1,10 +1,36 @@ # -*- coding: utf-8 -*- <%inherit file="visualization_base.mako"/> -## No stylesheets -<%def name="stylesheets()"> -## No javascript libraries -<%def name="late_javascripts()"> - <% tag_attrs = ' '.join([ '{0}="{1}"'.format( key, attr ) for key, attr in script_attributes.items() ]) %> - +## Add stylesheet +<%def name="stylesheets()"> + <% script_css = script_attributes.get("css") %> + %if script_css is not None: + <% script_css = script_css if h.is_url(script_css) else f"{static_url}{script_css}" %> + + %endif + + +## Create a container, attach data and import script file +<%def name="get_body()"> + ## Collect incoming data + <% + from markupsafe import escape + data_incoming = { + "root": h.url_for("/"), + "visualization_id": visualization_id, + "visualization_name": visualization_name, + "visualization_plugin": visualization_plugin, + "visualization_title": escape(title), + "visualization_config": config } + %> + + ## Create a container with default identifier `app` + <% container = script_attributes.get("container") or "app" %> +
+ + ## Add script tag + <% script_src = script_attributes.get("src") %> + <% script_src = script_src if h.is_url(script_src) else f"{static_url}{script_src}" %> + <% script_type = script_attributes.get("type") or "module" %> + diff --git a/config/plugins/visualizations/example/config/example.xml b/config/plugins/visualizations/example/config/example.xml index cc2dd898fb79..4fe2e0f38ff5 100644 --- a/config/plugins/visualizations/example/config/example.xml +++ b/config/plugins/visualizations/example/config/example.xml @@ -1,45 +1,33 @@ - - This is a developer example which demonstrates how to implement and configure a basic d3-based plugin for charts. + + Welcome to the Minimal JS-Based Example Plugin. HistoryDatasetAssociation tabular.Tabular - tabular.CSV dataset_id dataset_id - + - data_dialog - - data_dialog - false + setting_input + setting help + setting_type - x - - data_column - true - - - y - - data_column - true - - - z - - data_column - true + group_input + group help + group_type - \ No newline at end of file + + spec_value + + diff --git a/config/plugins/visualizations/example/package.json b/config/plugins/visualizations/example/package.json deleted file mode 100644 index 199cab67b083..000000000000 --- a/config/plugins/visualizations/example/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "visualization", - "version": "0.2.0", - "keywords": [ - "galaxy", - "visualization" - ], - "license": "AFL-3.0", - "dependencies": { - "@galaxyproject/charts": "^0.1.0", - "babel-preset-env": "^1.6.1", - "backbone": "^1.3.3", - "bootstrap": "^3.3.7", - "d3": "^4.12.2", - "jquery": "^3.1.1", - "underscore": "^1.8.3" - }, - "scripts": { - "build": "parcel build src/script.js --dist-dir static" - }, - "devDependencies": { - "parcel": "2.11.0" - } -} diff --git a/config/plugins/visualizations/example/src/script.js b/config/plugins/visualizations/example/src/script.js deleted file mode 100644 index 4fda38c2380d..000000000000 --- a/config/plugins/visualizations/example/src/script.js +++ /dev/null @@ -1,54 +0,0 @@ -import * as d3 from "d3"; -import * as _ from "underscore"; -import { request as requestDatasets } from "@galaxyproject/charts/lib/utilities/datasets"; - -_.extend(window.bundleEntries || {}, { - load: function(options) { - var chart = options.chart; - var root = options.root; - requestDatasets({ - root: root, - dataset_id: chart.get("dataset_id"), - dataset_groups: chart.groups, - success: function(groups) { - var colors = d3.scaleOrdinal(d3.schemeCategory20); - var error = null; - _.each(groups, function(group, group_index) { - try { - $("#" + options.target).append(""); - var svg = d3.select("#myexample"); - var height = parseInt(svg.style("height")); - var width = parseInt(svg.style("width")); - var maxValue = d3.max(group.values, function(d) { - return Math.max(d.x, d.y); - }); - svg - .selectAll("bubbles") - .data(group.values) - .enter() - .append("circle") - .attr("r", function(d) { - return Math.abs(d.z) * 20 / maxValue; - }) - .attr("cy", function(d, i) { - return height * d.y / maxValue; - }) - .attr("cx", function(d) { - return width * d.x / maxValue; - }) - .style("stroke", colors(group_index)) - .style("fill", "white"); - } catch (err) { - error = err; - } - }); - if (error) { - chart.state("failed", error); - } else { - chart.state("ok", "Workshop chart has been drawn."); - } - options.process.resolve(); - } - }); - } -}); diff --git a/config/plugins/visualizations/example/static/script.js b/config/plugins/visualizations/example/static/script.js new file mode 100644 index 000000000000..e17270b74957 --- /dev/null +++ b/config/plugins/visualizations/example/static/script.js @@ -0,0 +1,33 @@ +const { visualization_config, visualization_plugin, root } = JSON.parse(document.getElementById("app").dataset.incoming); + +const div = Object.assign(document.createElement("div"), { + style: "border: 2px solid #25537b; border-radius: 1rem; padding: 1rem" +}); + +const img = Object.assign(document.createElement("img"), { + src: root + visualization_plugin.logo, + style: "height: 3rem" +}); +div.appendChild(img); + +Object.entries(visualization_plugin).forEach(([key, value]) => { + const row = document.createElement("div"); + const spanKey = Object.assign(document.createElement("span"), { + innerText: `${key}: `, + style: "font-weight: bold" + }); + const spanValue = Object.assign(document.createElement("span"), { + innerText: JSON.stringify(value) + }); + row.appendChild(spanKey); + row.appendChild(spanValue); + div.appendChild(row); +}); + +const dataset = Object.assign(document.createElement("div"), { + innerText: `You have selected dataset: ${visualization_config.dataset_id}.`, + style: "font-weight: bold; padding-top: 1rem;" +}); +div.appendChild(dataset); + +document.body.appendChild(div); diff --git a/lib/galaxy/app_unittest_utils/galaxy_mock.py b/lib/galaxy/app_unittest_utils/galaxy_mock.py index 95715c042543..0fcec287878f 100644 --- a/lib/galaxy/app_unittest_utils/galaxy_mock.py +++ b/lib/galaxy/app_unittest_utils/galaxy_mock.py @@ -430,8 +430,17 @@ def remove(self): class MockTemplateHelpers: - def js(*js_files): + def css(*css_files): pass - def css(*css_files): + def dumps(*kwargs): + return {} + + def js(*js_files): pass + + def is_url(*kwargs): + return True + + def url_for(*kwargs): + return "/" diff --git a/lib/galaxy/visualization/plugins/config_parser.py b/lib/galaxy/visualization/plugins/config_parser.py index 85b1ccc79be9..160f52acef2c 100644 --- a/lib/galaxy/visualization/plugins/config_parser.py +++ b/lib/galaxy/visualization/plugins/config_parser.py @@ -221,7 +221,7 @@ def parse(self, xml_tree): # when no tests are given, default to isinstance( object, model_class ) returned["tests"] = self.parse_tests(xml_tree.findall("test")) - # to_params (optional, 0 or more) - tells the registry to set certain params based on the model_clas, tests + # to_params (optional, 0 or more) - tells the registry to set certain params based on the model_class, tests returned["to_params"] = {} if to_params := self.parse_to_params(xml_tree.findall("to_param")): returned["to_params"] = to_params diff --git a/lib/galaxy/visualization/plugins/plugin.py b/lib/galaxy/visualization/plugins/plugin.py index dfe625e53dcb..c53f7977834a 100644 --- a/lib/galaxy/visualization/plugins/plugin.py +++ b/lib/galaxy/visualization/plugins/plugin.py @@ -161,7 +161,7 @@ def _build_render_vars(self, config: Dict[str, Any], trans=None, **kwargs) -> Di render_vars.update( visualization_name=self.name, visualization_display_name=self.config["name"], - title=kwargs.get("title", None), + title=kwargs.get("title", "Unnamed Visualization"), saved_visualization=None, visualization_id=None, visualization_plugin=self.to_dict(), diff --git a/lib/galaxy/web/framework/helpers/__init__.py b/lib/galaxy/web/framework/helpers/__init__.py index 7fde6906cf19..f6f90bc54820 100644 --- a/lib/galaxy/web/framework/helpers/__init__.py +++ b/lib/galaxy/web/framework/helpers/__init__.py @@ -6,6 +6,7 @@ GalaxyWebTransaction in galaxy/webapps/base/webapp.py """ +import re from datetime import ( datetime, timedelta, @@ -105,6 +106,22 @@ def is_true(val): return val is True or val in ["True", "true", "T", "t"] +def is_url(val): + """ + Regular expression to match common URL protocols + + >>> assert is_url(None) == False + >>> assert is_url("is_url") == False + >>> assert is_url("http://is_url") == True + >>> assert is_url("https://is_url") == True + """ + if val is not None: + url_pattern = re.compile(r"^(https?:\/\/|ftp:\/\/)") + return bool(url_pattern.match(val)) + else: + return False + + def to_js_bool(val): """ Prints javascript boolean for passed value. diff --git a/test/unit/app/visualizations/plugins/test_VisualizationPlugin.py b/test/unit/app/visualizations/plugins/test_VisualizationPlugin.py index baf72fe065c8..772c54bb925e 100644 --- a/test/unit/app/visualizations/plugins/test_VisualizationPlugin.py +++ b/test/unit/app/visualizations/plugins/test_VisualizationPlugin.py @@ -74,7 +74,7 @@ def test_build_render_vars_default(self): render_vars = plugin._build_render_vars(config) assert render_vars["visualization_name"] == plugin.name assert render_vars["visualization_display_name"] == plugin.config["name"] - assert render_vars["title"] is None + assert render_vars["title"] == "Unnamed Visualization" assert render_vars["saved_visualization"] is None assert render_vars["visualization_id"] is None assert render_vars["query"] == {} diff --git a/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py b/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py index 1c7f9707f047..6f5ce4274e70 100644 --- a/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py +++ b/test/unit/app/visualizations/plugins/test_VisualizationsRegistry.py @@ -15,7 +15,7 @@ glx_dir = galaxy_directory() template_cache_dir = os.path.join(glx_dir, "database", "compiled_templates") -addtional_templates_dir = os.path.join(glx_dir, "config", "plugins", "visualizations", "common", "templates") +additional_templates_dir = os.path.join(glx_dir, "config", "plugins", "visualizations", "common", "templates") vis_reg_path = "config/plugins/visualizations" config1 = """\ @@ -144,7 +144,7 @@ def test_script_entry(self): HistoryDatasetAssociation - + """ ) @@ -167,11 +167,11 @@ def test_script_entry(self): assert script_entry.serves_templates trans = galaxy_mock.MockTrans() - script_entry._set_up_template_plugin(mock_app_dir.root_path, [addtional_templates_dir]) - response = script_entry._render({}, trans=trans, embedded=True) - assert 'src="bler"' in response - assert 'type="text/javascript"' in response - assert 'data-main="one"' in response + script_entry._set_up_template_plugin(mock_app_dir.root_path, [additional_templates_dir]) + response = script_entry.render(trans=trans, embedded=True) + assert '