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 @@
+
+
+
This cell type `{{ name }}` is not available.
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
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()