diff --git a/client/src/components/Markdown/Editor/CellAction.vue b/client/src/components/Markdown/Editor/CellAction.vue index 40f9d9b6992c..4519801719c9 100644 --- a/client/src/components/Markdown/Editor/CellAction.vue +++ b/client/src/components/Markdown/Editor/CellAction.vue @@ -13,7 +13,7 @@ {{ title }} import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { BAlert } from "bootstrap-vue"; -import { computed, ref } from "vue"; +import { computed, onMounted, type Ref, ref } from "vue"; +import { getVisualizations } from "./services"; import cellTemplates from "./templates.yml"; import type { CellType, TemplateEntry } from "./types"; @@ -46,9 +47,11 @@ defineEmits<{ const buttonRef = ref(); const query = ref(""); +const visualizations: Ref> = ref([]); const allTemplates = computed(() => { const result = { ...(cellTemplates as Record>) }; + result["Visualization"] = visualizations.value; return result; }); @@ -67,6 +70,10 @@ const filteredTemplates = computed(() => { }); return filteredCategories; }); + +onMounted(async () => { + visualizations.value = await getVisualizations(); +}); diff --git a/client/src/components/Markdown/Sections/SectionWrapper.vue b/client/src/components/Markdown/Sections/SectionWrapper.vue index 9f864996a225..11afb04beb0f 100644 --- a/client/src/components/Markdown/Sections/SectionWrapper.vue +++ b/client/src/components/Markdown/Sections/SectionWrapper.vue @@ -1,14 +1,23 @@ + + + + diff --git a/config/plugins/visualizations/fits_graph_viewer/config/fits_graph_viewer.xml b/config/plugins/visualizations/fits_graph_viewer/config/fits_graph_viewer.xml index a8c455130dfe..66bf782ddee5 100644 --- a/config/plugins/visualizations/fits_graph_viewer/config/fits_graph_viewer.xml +++ b/config/plugins/visualizations/fits_graph_viewer/config/fits_graph_viewer.xml @@ -1,6 +1,6 @@ - + Basic plugin for fits file table visualization diff --git a/config/plugins/visualizations/fits_graph_viewer/src/index.js b/config/plugins/visualizations/fits_graph_viewer/src/index.js index 87bf33349263..0d74ab128b4e 100644 --- a/config/plugins/visualizations/fits_graph_viewer/src/index.js +++ b/config/plugins/visualizations/fits_graph_viewer/src/index.js @@ -1,13 +1,8 @@ -import {init} from 'astrovisjs/dist/astrovis/astrovis'; +import { init } from "astrovisjs/dist/astrovis/astrovis"; -document.addEventListener('DOMContentLoaded', () => { +const incoming = document.getElementById("app").getAttribute("data-incoming") || "{}"; +const { root, visualization_config } = JSON.parse(incoming); +const dataset_id = visualization_config.dataset_id; +const file_url = root + "datasets/" + dataset_id + "/display" - const {root, visualization_config} = JSON.parse( - document.getElementById("app") - .getAttribute("data-incoming") || "{}"); - - const dataset_id = visualization_config.dataset_id; - const file_url = root + "datasets/" + dataset_id + "/display" - - init('app', file_url); -}); \ No newline at end of file +init('app', file_url); diff --git a/config/plugins/visualizations/h5web/config/h5web.xml b/config/plugins/visualizations/h5web/config/h5web.xml index a06faaabe49c..92ff19ea0318 100644 --- a/config/plugins/visualizations/h5web/config/h5web.xml +++ b/config/plugins/visualizations/h5web/config/h5web.xml @@ -12,7 +12,7 @@ dataset_id - + diff --git a/config/plugins/visualizations/heatmap/config/heatmap.xml b/config/plugins/visualizations/heatmap/config/heatmap.xml index c1a95a5c4781..4d602a50fe4f 100644 --- a/config/plugins/visualizations/heatmap/config/heatmap.xml +++ b/config/plugins/visualizations/heatmap/config/heatmap.xml @@ -3,7 +3,7 @@ Renders a heatmap from matrix data provided in 3-column format (x, y, observation). - + diff --git a/config/plugins/visualizations/tiffviewer/src/script.js b/config/plugins/visualizations/tiffviewer/src/script.js index f97bf19d255a..01dfd7ff4a10 100644 --- a/config/plugins/visualizations/tiffviewer/src/script.js +++ b/config/plugins/visualizations/tiffviewer/src/script.js @@ -12,7 +12,7 @@ const { root, visualization_config } = JSON.parse(document.getElementById("app") const datasetId = visualization_config.dataset_id; -const url = window.location.origin + root + "api/datasets/" + datasetId + "/display"; +const url = root + "api/datasets/" + datasetId + "/display"; const rootElement = createRoot(document.getElementById("app")); rootElement.render( diff --git a/config/plugins/visualizations/vitessce/config/vitessce.xml b/config/plugins/visualizations/vitessce/config/vitessce.xml index c0244bcd8d49..d184993b7cf3 100644 --- a/config/plugins/visualizations/vitessce/config/vitessce.xml +++ b/config/plugins/visualizations/vitessce/config/vitessce.xml @@ -13,7 +13,7 @@ dataset_id - + diff --git a/config/plugins/visualizations/vizarr/config/vizarr.xml b/config/plugins/visualizations/vizarr/config/vizarr.xml index 5c4196460108..b213605eeaf8 100644 --- a/config/plugins/visualizations/vizarr/config/vizarr.xml +++ b/config/plugins/visualizations/vizarr/config/vizarr.xml @@ -3,7 +3,7 @@ Basic visualization for Zarr-based images like OME-Zarr - + diff --git a/lib/galaxy/managers/markdown_util.py b/lib/galaxy/managers/markdown_util.py index f24daa4c0d39..179deae8fec4 100644 --- a/lib/galaxy/managers/markdown_util.py +++ b/lib/galaxy/managers/markdown_util.py @@ -66,6 +66,7 @@ log = logging.getLogger(__name__) +# Matches galaxy block attributes ARG_VAL_CAPTURED_REGEX = r"""(?:([\w_\-\|]+)|\"([^\"]+)\"|\'([^\']+)\')""" OUTPUT_LABEL_PATTERN = re.compile(rf"output=\s*{ARG_VAL_CAPTURED_REGEX}\s*") INPUT_LABEL_PATTERN = re.compile(rf"input=\s*{ARG_VAL_CAPTURED_REGEX}\s*") @@ -73,13 +74,37 @@ STEP_LABEL_PATTERN = re.compile(rf"step=\s*{ARG_VAL_CAPTURED_REGEX}\s*") PATH_LABEL_PATTERN = re.compile(rf"path=\s*{ARG_VAL_CAPTURED_REGEX}\s*") +# Matches encoded and unencoded ids in galaxy blocks UNENCODED_ID_PATTERN = re.compile( r"(history_id|workflow_id|history_dataset_id|history_dataset_collection_id|job_id|implicit_collection_jobs_id|invocation_id)=([\d]+)" ) ENCODED_ID_PATTERN = re.compile( r"(history_id|workflow_id|history_dataset_id|history_dataset_collection_id|job_id|implicit_collection_jobs_id|invocation_id)=([a-z0-9]+)" ) -GALAXY_FENCED_BLOCK = re.compile(r"^```\s*galaxy\s*(.*?)^```", re.MULTILINE ^ re.DOTALL) + +# Matches blocks of various types +GALAXY_FENCED_BLOCK = re.compile(r"^```\s*galaxy\s*(.*?)^```", re.MULTILINE | re.DOTALL) +VISUALIZATION_FENCED_BLOCK = re.compile(r"^```\s*visualization+\n\s*(.*?)^```", re.MULTILINE | re.DOTALL) + +# Match invocation ids in json blocks +INVOCATION_ID_JSON_PATTERN = re.compile(r'("invocation_id"\s*:\s*)"([^"]*)"') + + +def process_invocation_ids(f, workflow_markdown: str) -> str: + """Finds all invocation ids in JSONs inside visualization blocks and applies f to them.""" + + def replace_invocation_id(match): + """Replaces only the invocation_id value while preserving the JSON structure.""" + original_id = match.group(2) + new_id = f(original_id) + return f'{match.group(1)}"{new_id}"' + + def process_block(block_match): + """Processes a matched block and replaces invocation_id inside it.""" + block_content = block_match.group(0) + return re.sub(INVOCATION_ID_JSON_PATTERN, replace_invocation_id, block_content) + + return re.sub(VISUALIZATION_FENCED_BLOCK, process_block, workflow_markdown) def ready_galaxy_markdown_for_import(trans, external_galaxy_markdown): @@ -96,6 +121,7 @@ def _remap(container, line): return (line, False) internal_markdown = _remap_galaxy_markdown_calls(_remap, external_galaxy_markdown) + internal_markdown = process_invocation_ids(trans.security.decode_id, internal_markdown) return internal_markdown @@ -534,10 +560,12 @@ def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown): "generate_time": now().isoformat(), "generate_version": trans.app.config.version_major, } + # Walk Galaxy directives inside the Galaxy Markdown and collect dict-ified data # needed to render this efficiently. directive_handler = ReadyForExportMarkdownDirectiveHandler(trans, extra_rendering_data) export_markdown = directive_handler.walk(trans, internal_galaxy_markdown) + export_markdown = process_invocation_ids(lambda value: trans.security.encode_id(int(value)), export_markdown) return export_markdown, extra_rendering_data @@ -925,6 +953,7 @@ def _remap(container, line): return (line, False) galaxy_markdown = _remap_galaxy_markdown_calls(_remap, workflow_markdown) + galaxy_markdown = process_invocation_ids(lambda _: invocation.id, galaxy_markdown) return galaxy_markdown diff --git a/test/unit/workflows/test_workflow_markdown.py b/test/unit/workflows/test_workflow_markdown.py index 108ee6435925..584955f33af7 100644 --- a/test/unit/workflows/test_workflow_markdown.py +++ b/test/unit/workflows/test_workflow_markdown.py @@ -101,6 +101,59 @@ def test_output_reference_mapping(): assert "```galaxy\nhistory_dataset_as_image(history_dataset_id=563)\n```" in galaxy_markdown +def test_populating_invocation_json(): + workflow_markdown_0 = """ +```visualization +{ + "invocation_id": "", + "other_key": "other_value" +} +``` +""" + + workflow_markdown_1 = """ +```visualization +{ + "nested_structure": { + "invocation_id": "" + } +} +``` +""" + + workflow_markdown_2 = """ +```visualization +{ + "nested_structure": { + "invocation_id": "REPLACE" + } +} +``` +""" + + workflow_markdown_3 = """ +{ + "invocation_id": "" +} +""" + galaxy_markdown = populate_markdown(workflow_markdown_0) + assert ( + '\n```visualization\n{\n "invocation_id": "44",\n "other_key": "other_value"\n}\n```\n' in galaxy_markdown + ) + galaxy_markdown = populate_markdown(workflow_markdown_1) + assert ( + '\n```visualization\n{\n "nested_structure": {\n "invocation_id": "44"\n }\n}\n```\n' + in galaxy_markdown + ) + galaxy_markdown = populate_markdown(workflow_markdown_2) + assert ( + '\n```visualization\n{\n "nested_structure": {\n "invocation_id": "44"\n }\n}\n```\n' + in galaxy_markdown + ) + galaxy_markdown = populate_markdown(workflow_markdown_3) + assert '\n{\n "invocation_id": ""\n}\n' in galaxy_markdown + + def populate_markdown(workflow_markdown): # Add invocation ids to internal Galaxy markdown trans = MockTrans()