Merge pull request #11863 from guerler/import_history

Allow importing histories through pages / reports
This commit is contained in:
Dannon
2021-04-20 20:46:31 -04:00
committed by GitHub
7 changed files with 220 additions and 49 deletions
@@ -0,0 +1,67 @@
<template>
<div>
<b-link v-if="showLink" @click="onClick">Click to Import History: {{ name }}.</b-link>
<div v-if="imported" class="text-success">
<font-awesome-icon icon="check" class="mr-1" />
<span>Successfully Imported History: {{ name }}!</span>
</div>
<div v-if="!!error" class="text-danger">
<font-awesome-icon icon="exclamation-triangle" class="mr-1" />
<span>Failed to Import History: {{ name }}!</span>
<span>{{ error }}</span>
</div>
</div>
</template>
<script>
import axios from "axios";
import { getAppRoot } from "onload/loadConfig";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { errorMessageAsString } from "utils/simple-error";
Vue.use(BootstrapVue);
export default {
components: {
FontAwesomeIcon,
},
props: {
args: {
type: Object,
default: null,
},
histories: {
type: Object,
required: true,
},
},
data() {
return {
imported: false,
error: false,
};
},
computed: {
name() {
return this.histories[this.args.history_id].name;
},
showLink() {
return !this.imported && !this.error;
},
},
methods: {
onClick() {
axios
.post(`${getAppRoot()}api/histories`, { history_id: this.args.history_id })
.then(() => {
this.imported = true;
})
.catch((e) => {
this.error = errorMessageAsString(e);
});
},
},
};
</script>
@@ -44,6 +44,7 @@
<div v-else-if="obj.name == 'generate_time'" class="galaxy-time">
<pre><code>{{ getTime }}</code></pre>
</div>
<HistoryLink v-else-if="obj.name == 'history_link'" :args="obj.args" :histories="histories" />
<HistoryDatasetAsImage v-else-if="obj.name == 'history_dataset_as_image'" :args="obj.args" />
<HistoryDatasetLink v-else-if="obj.name == 'history_dataset_link'" :args="obj.args" />
<HistoryDatasetIndex v-else-if="obj.name == 'history_dataset_index'" :args="obj.args" />
@@ -105,6 +106,7 @@ import HistoryDatasetLink from "./Elements/HistoryDatasetLink";
import HistoryDatasetIndex from "./Elements/HistoryDatasetIndex";
import HistoryDatasetCollectionDisplay from "./Elements/HistoryDatasetCollection/CollectionDisplay";
import HistoryDatasetDetails from "./Elements/HistoryDatasetDetails";
import HistoryLink from "./Elements/HistoryLink";
import InvocationTime from "./Elements/InvocationTime";
import JobMetrics from "./Elements/JobMetrics";
import JobParameters from "./Elements/JobParameters";
@@ -137,6 +139,7 @@ export default {
HistoryDatasetDisplay,
HistoryDatasetIndex,
HistoryDatasetLink,
HistoryLink,
JobMetrics,
JobParameters,
LoadingSpan,
@@ -165,6 +168,7 @@ export default {
markdownObjects: [],
markdownErrors: [],
historyDatasets: {},
histories: {},
historyDatasetCollections: {},
workflows: {},
jobs: {},
@@ -201,6 +205,7 @@ export default {
this.markdownErrors = config.errors || [];
this.markdownObjects = this.splitMarkdown(markdown);
this.historyDatasets = config.history_datasets || {};
this.histories = config.histories || {};
this.historyDatasetCollections = config.history_dataset_collections || {};
this.workflows = config.workflows || {};
this.jobs = config.jobs || {};
@@ -54,6 +54,14 @@
@onOk="onWorkflow"
@onCancel="onCancel"
/>
<BasicSelectionDialog
v-else-if="historyShow"
:get-data="getHistories"
title="History"
label-key="name"
@onOk="onHistory"
@onCancel="onCancel"
/>
</span>
</template>
@@ -120,9 +128,11 @@ export default {
jobsUrl: `${getAppRoot()}api/jobs`,
workflowsUrl: `${getAppRoot()}api/workflows`,
invocationsUrl: `${getAppRoot()}api/invocations`,
historiesUrl: `${getAppRoot()}api/histories?view=detailed&q=published&qv=True`,
selectedShow: false,
visualizationShow: false,
workflowShow: false,
historyShow: false,
jobShow: false,
invocationShow: false,
dataShow: false,
@@ -148,6 +158,9 @@ export default {
getWorkflows() {
return axios.get(this.workflowsUrl);
},
getHistories() {
return axios.get(this.historiesUrl);
},
onData(response) {
this.dataShow = false;
this.$emit("onInsert", `${this.argumentName}(history_dataset_id=${response})`);
@@ -164,6 +177,10 @@ export default {
this.invocationShow = false;
this.$emit("onInsert", `${this.argumentName}(invocation_id=${response.id})`);
},
onHistory(response) {
this.historyShow = false;
this.$emit("onInsert", `history_link(history_id=${response.id})`);
},
onWorkflow(response) {
this.workflowShow = false;
this.$emit("onInsert", `workflow_display(workflow_id=${response.id})`);
@@ -175,6 +192,8 @@ export default {
onCreate() {
if (this.argumentType == "workflow_id") {
this.workflowShow = true;
} else if (this.argumentType == "history_id") {
this.historyShow = true;
} else if (this.argumentType == "history_dataset_id") {
if (this.useLabels) {
this.selectedShow = true;
@@ -10,7 +10,13 @@
<b-alert v-if="error" variant="danger" class="my-2 mx-3 px-2 py-1" show>
{{ error }}
</b-alert>
<tool-section :category="historySection" @onClick="onClick" :expanded="true" />
<tool-section
v-if="isWorkflow"
:category="historyInEditorSection"
@onClick="onClick"
:expanded="true"
/>
<tool-section v-else :category="historySection" @onClick="onClick" :expanded="true" />
<tool-section :category="jobSection" @onClick="onClick" :expanded="true" />
<tool-section
v-if="isWorkflow"
@@ -52,6 +58,59 @@ import { getAppRoot } from "onload/loadConfig";
Vue.use(BootstrapVue);
const historySharedElements = [
{
id: "history_dataset_display",
name: "Dataset",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_collection_display",
name: "Collection",
emitter: "onHistoryCollectionId",
},
{
id: "history_dataset_as_image",
name: "Image",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_index",
name: "Dataset Index",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_embedded",
name: "Embedded Dataset",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_type",
name: "Dataset Type",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_link",
name: "Link to Dataset",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_name",
name: "Name of Dataset",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_peek",
name: "Peek into Dataset",
emitter: "onHistoryDatasetId",
},
{
id: "history_dataset_info",
name: "Dataset Details",
emitter: "onHistoryDatasetId",
},
];
export default {
components: {
MarkdownDialog,
@@ -69,61 +128,29 @@ export default {
selectedType: null,
selectedLabels: null,
selectedShow: false,
selectedPayload: null,
visualizationIndex: {},
error: null,
historySection: {
title: "History",
name: "history",
elems: [
...historySharedElements,
{
id: "history_dataset_display",
name: "Dataset",
id: "history_link",
name: "Link to Import",
emitter: "onHistoryId",
},
],
},
historyInEditorSection: {
title: "History",
name: "history",
elems: [
...historySharedElements,
{
id: "history_dataset_collection_display",
name: "Collection",
emitter: "onHistoryCollectionId",
},
{
id: "history_dataset_as_image",
name: "Image",
emitter: "onHistoryId",
},
{
id: "history_dataset_index",
name: "Dataset Index",
emitter: "onHistoryId",
},
{
id: "history_dataset_embedded",
name: "Embedded Dataset",
emitter: "onHistoryId",
},
{
id: "history_dataset_type",
name: "Dataset Type",
emitter: "onHistoryId",
},
{
id: "history_dataset_link",
name: "Link to Dataset",
emitter: "onHistoryId",
},
{
id: "history_dataset_name",
name: "Name of Dataset",
emitter: "onHistoryId",
},
{
id: "history_dataset_peek",
name: "Peek into Dataset",
emitter: "onHistoryId",
},
{
id: "history_dataset_info",
name: "Dataset Details",
emitter: "onHistoryId",
id: "history_link",
name: "Link to Import",
},
],
},
@@ -264,6 +291,9 @@ export default {
},
onClick(item) {
switch (item.emitter) {
case "onHistoryDatasetId":
this.onHistoryDatasetId(item.id);
break;
case "onHistoryId":
this.onHistoryId(item.id);
break;
@@ -304,6 +334,11 @@ export default {
this.selectedShow = true;
},
onHistoryId(argumentName) {
this.selectedArgumentName = argumentName;
this.selectedType = "history_id";
this.selectedShow = true;
},
onHistoryDatasetId(argumentName) {
this.selectedArgumentName = argumentName;
this.selectedType = "history_dataset_id";
this.selectedLabels = this.getOutputs();
+1
View File
@@ -15,6 +15,7 @@ GALAXY_FLAVORED_MARKDOWN_CONTAINER_LINE_PATTERN = re.compile(
VALID_CONTAINER_END_PATTERN = re.compile(r"^```\s*$")
DYNAMIC_ARGUMENTS = object()
VALID_ARGUMENTS = {
"history_link": ["history_id"],
"history_dataset_display": ["input", "output", "history_dataset_id"],
"history_dataset_embedded": ["input", "output", "history_dataset_id"],
"history_dataset_as_image": ["input", "output", "history_dataset_id", "path"],
+24 -3
View File
@@ -49,8 +49,8 @@ INPUT_LABEL_PATTERN = re.compile(r'input=\s*%s\s*' % ARG_VAL_CAPTURED_REGEX)
STEP_LABEL_PATTERN = re.compile(r'step=\s*%s\s*' % ARG_VAL_CAPTURED_REGEX)
PATH_LABEL_PATTERN = re.compile(r'path=\s*%s\s*' % ARG_VAL_CAPTURED_REGEX)
# STEP_OUTPUT_LABEL_PATTERN = re.compile(r'step_output=([\w_\-]+)/([\w_\-]+)')
UNENCODED_ID_PATTERN = re.compile(r'(workflow_id|history_dataset_id|history_dataset_collection_id|job_id|invocation_id)=([\d]+)')
ENCODED_ID_PATTERN = re.compile(r'(workflow_id|history_dataset_id|history_dataset_collection_id|job_id|invocation_id)=([a-z0-9]+)')
UNENCODED_ID_PATTERN = re.compile(r'(history_id|workflow_id|history_dataset_id|history_dataset_collection_id|job_id|invocation_id)=([\d]+)')
ENCODED_ID_PATTERN = re.compile(r'(history_id|workflow_id|history_dataset_id|history_dataset_collection_id|job_id|invocation_id)=([a-z0-9]+)')
INVOCATION_SECTION_MARKDOWN_CONTAINER_LINE_PATTERN = re.compile(
r"```\s*galaxy\s*"
)
@@ -80,6 +80,7 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta):
def walk(self, trans, internal_galaxy_markdown):
hda_manager = trans.app.hda_manager
history_manager = trans.app.history_manager
workflow_manager = trans.app.workflow_manager
job_manager = JobManager(trans.app)
collection_manager = trans.app.dataset_collections_service
@@ -96,7 +97,11 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta):
object_id = int(id_match.group(2))
encoded_id = trans.security.encode_id(object_id)
line = line.replace(id_match.group(), "{}={}".format(id_match.group(1), encoded_id))
if container == "history_dataset_display":
if container == "history_link":
_check_object(object_id, line)
history = history_manager.get_accessible(object_id, trans.user)
rval = self.handle_history_link(line, history)
elif container == "history_dataset_display":
_check_object(object_id, line)
hda = hda_manager.get_accessible(object_id, trans.user)
rval = self.handle_dataset_display(line, hda)
@@ -176,6 +181,10 @@ class GalaxyInternalMarkdownDirectiveHandler(metaclass=abc.ABCMeta):
export_markdown = _remap_galaxy_markdown_calls(_remap_container, internal_galaxy_markdown)
return export_markdown
@abc.abstractmethod
def handle_history_link(self, line, history):
pass
@abc.abstractmethod
def handle_dataset_display(self, line, hda):
pass
@@ -288,6 +297,9 @@ class ReadyForExportMarkdownDirectiveHandler(GalaxyInternalMarkdownDirectiveHand
def handle_tool_stderr(self, line, job):
self.ensure_rendering_data_for("jobs", job)["tool_stderr"] = job.tool_stderr or "*No Standard Error Available*"
def handle_history_link(self, line, history):
self.ensure_rendering_data_for("histories", history)["name"] = history.name
# Following three cases - the client side widgets have everything they need
# from the encoded ID. Don't implement a default on the base class though because
# it is good to force both Client and PDF/HTML export to deal with each new directive
@@ -396,6 +408,13 @@ class ToBasicMarkdownDirectiveHandler(GalaxyInternalMarkdownDirectiveHandler):
rval = (f"![{name}](data:image/png;base64,{base64_image_data})", True)
return rval
def handle_history_link(self, line, history):
if history:
content = self.markdown_formatting_helpers.literal_via_fence(history.name)
else:
content = "*No History available*"
return (content, True)
def handle_dataset_peek(self, line, hda):
if hda.peek:
content = self.markdown_formatting_helpers.literal_via_fence(hda.peek)
@@ -664,6 +683,8 @@ history_dataset_collection_display(input={})
# TODO: this really should be workflow id not stored workflow id but the API
# it consumes wants the stored id.
return ("workflow_display(workflow_id=%s)\n" % invocation.workflow.stored_workflow.id, False)
if container == "history_link":
return ("history_link(history_id=%s)\n" % invocation.history.id, False)
if container == "invocation_date":
return ("invocation_date(invocation_id=%s)\n" % invocation.id, False)
ref_object_type = None
@@ -22,6 +22,12 @@ class BaseExportTestCase(BaseTestCase):
self.app.history_manager = mock.MagicMock()
self.app.dataset_collections_service = mock.MagicMock()
def _new_history(self):
history = model.History()
history.id = 1
history.name = "New History"
return history
def _new_hda(self, contents=None):
hda = model.HistoryDatasetAssociation()
hda.id = 1
@@ -39,6 +45,12 @@ class BaseExportTestCase(BaseTestCase):
invocation.create_time = now()
return invocation
@contextmanager
def _expect_get_history(self, history):
self.app.history_manager.get_accessible.return_value = history
yield
self.app.history_manager.get_accessible.assert_called_once_with(history.id, self.trans.user)
@contextmanager
def _expect_get_hda(self, hda, hda_id=1):
self.app.hda_manager.get_accessible.return_value = hda
@@ -128,6 +140,17 @@ history_dataset_peek(history_dataset_id=1)
result = self._to_basic(example)
assert '\n*No Dataset Peek Available*\n' in result
def test_history_link(self):
history = self._new_history()
example = """# Example
```galaxy
history_link(history_id=1)
```
"""
with self._expect_get_history(history):
result = self._to_basic(example)
assert '\n New History\n\n' in result
def test_history_display_binary(self):
hda = self._new_hda()
hda.extension = 'ab1'