mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
+1
-1
@@ -187,7 +187,7 @@
|
||||
"vue-template-compiler": "^2.7.14",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-dev-server": "^4.12.0",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"yaml-jest": "^1.2.0",
|
||||
"yaml-loader": "^0.8.0"
|
||||
|
||||
@@ -4,10 +4,16 @@ import { BAlert, BCard, BCardTitle } from "bootstrap-vue";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faExclamationCircle,
|
||||
faExclamationTriangle,
|
||||
faCheckCircle,
|
||||
faClock,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExportRecordModel } from "./models/exportRecordModel";
|
||||
|
||||
library.add(faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock);
|
||||
library.add(faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock, faLink);
|
||||
|
||||
const props = defineProps({
|
||||
record: {
|
||||
@@ -28,7 +34,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onReimport", "onDownload", "onActionMessageDismissed"]);
|
||||
const emit = defineEmits(["onReimport", "onDownload", "onCopyDownloadLink", "onActionMessageDismissed"]);
|
||||
|
||||
const title = computed(() => (props.record.isReady ? `Exported` : `Export started`));
|
||||
const preparingMessage = computed(
|
||||
@@ -43,6 +49,10 @@ function downloadObject() {
|
||||
emit("onDownload", props.record);
|
||||
}
|
||||
|
||||
function copyDownloadLink() {
|
||||
emit("onCopyDownloadLink", props.record);
|
||||
}
|
||||
|
||||
function onMessageDismissed() {
|
||||
emit("onActionMessageDismissed");
|
||||
}
|
||||
@@ -113,6 +123,14 @@ function onMessageDismissed() {
|
||||
@click="downloadObject">
|
||||
Download
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="props.record.canDownload"
|
||||
title="Copy Download Link"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@click.stop="copyDownloadLink">
|
||||
<font-awesome-icon icon="link" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="props.record.canReimport"
|
||||
class="record-reimport-btn"
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
faDownload,
|
||||
faFileImport,
|
||||
faSpinner,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faExclamationCircle, faCheckCircle, faDownload, faFileImport, faSpinner);
|
||||
library.add(faExclamationCircle, faCheckCircle, faDownload, faFileImport, faSpinner, faLink);
|
||||
|
||||
const props = defineProps({
|
||||
records: {
|
||||
@@ -32,7 +33,7 @@ const fields = [
|
||||
];
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const title = computed(() => (isExpanded.value ? `Hide export records` : `Show export records`));
|
||||
const title = computed(() => (isExpanded.value ? `Hide old export records` : `Show old export records`));
|
||||
|
||||
async function reimportObject(record) {
|
||||
emit("onReimport", record);
|
||||
@@ -41,6 +42,10 @@ async function reimportObject(record) {
|
||||
function downloadObject(record) {
|
||||
emit("onDownload", record);
|
||||
}
|
||||
|
||||
function copyDownloadLink(record) {
|
||||
emit("onCopyDownloadLink", record);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -113,6 +118,12 @@ function downloadObject(record) {
|
||||
@click="downloadObject(row.item)">
|
||||
<font-awesome-icon icon="download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="row.item.canDownload"
|
||||
title="Copy Download Link"
|
||||
@click.stop="copyDownloadLink(row.item)">
|
||||
<font-awesome-icon icon="link" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-b-tooltip.hover.bottom
|
||||
:disabled="!row.item.canReimport"
|
||||
|
||||
@@ -11,6 +11,8 @@ import { useTaskMonitor } from "composables/taskMonitor";
|
||||
import { useFileSources } from "composables/fileSources";
|
||||
import { useShortTermStorage, DEFAULT_EXPORT_PARAMS } from "composables/shortTermStorage";
|
||||
import { useConfirmDialog } from "composables/confirmDialog";
|
||||
import { copy as sendToClipboard } from "utils/clipboard";
|
||||
import { absPath } from "@/utils/redirect";
|
||||
|
||||
const {
|
||||
isRunning: isExportTaskRunning,
|
||||
@@ -18,8 +20,16 @@ const {
|
||||
requestHasFailed: taskMonitorRequestFailed,
|
||||
hasFailed: taskHasFailed,
|
||||
} = useTaskMonitor();
|
||||
|
||||
const { hasWritable: hasWritableFileSources } = useFileSources();
|
||||
const { isPreparing: isPreparingDownload, downloadHistory, downloadObjectByRequestId } = useShortTermStorage();
|
||||
|
||||
const {
|
||||
isPreparing: isPreparingDownload,
|
||||
downloadHistory,
|
||||
downloadObjectByRequestId,
|
||||
getDownloadObjectUrl,
|
||||
} = useShortTermStorage();
|
||||
|
||||
const { confirm } = useConfirmDialog();
|
||||
|
||||
const props = defineProps({
|
||||
@@ -104,6 +114,13 @@ function downloadFromRecord(record) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyDownloadLinkFromRecord(record) {
|
||||
if (record.canDownload) {
|
||||
const relativeLink = getDownloadObjectUrl(record.stsDownloadId);
|
||||
sendToClipboard(absPath(relativeLink), "Download link copied to your clipboard");
|
||||
}
|
||||
}
|
||||
|
||||
function findValidUpToDateDownloadRecord() {
|
||||
return exportRecords.value
|
||||
? exportRecords.value.find(
|
||||
@@ -203,6 +220,7 @@ function updateExportParams(newParams) {
|
||||
:action-message="actionMessage"
|
||||
:action-message-variant="actionMessageVariant"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onCopyDownloadLink="copyDownloadLinkFromRecord"
|
||||
@onReimport="reimportFromRecord"
|
||||
@onActionMessageDismissed="onActionMessageDismissedFromRecord" />
|
||||
<b-alert v-else id="no-export-records-alert" variant="info" class="mt-3" show>
|
||||
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
this.$emit("onResults", this.favoritesResults);
|
||||
} else {
|
||||
// keys with sorting order
|
||||
const keys = { exact: 3, name: 2, description: 1, combined: 0 };
|
||||
const keys = { exact: 4, name: 3, hyphenated: 2, description: 1, combined: 0 };
|
||||
this.$emit("onResults", searchToolsByKeys(this.toolsList, keys, q));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -92,6 +92,8 @@ export function searchToolsByKeys(tools, keys, query) {
|
||||
let actualValue = "";
|
||||
if (key === "combined") {
|
||||
actualValue = tool.name.toLowerCase() + " " + tool.description.toLowerCase();
|
||||
} else if (key === "hyphenated") {
|
||||
actualValue = tool.name.toLowerCase().replaceAll("-", " ");
|
||||
} else {
|
||||
actualValue = tool[key] ? tool[key].toLowerCase() : "";
|
||||
}
|
||||
|
||||
@@ -44,6 +44,28 @@ describe("test helpers in tool searching utilities", () => {
|
||||
keys = { description: 1, name: 2, combined: 0 };
|
||||
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
|
||||
expect(results).toEqual(expectedResults);
|
||||
|
||||
const tempToolsList = [
|
||||
{
|
||||
elems: [
|
||||
{
|
||||
panel_section_name: "FASTA/FASTQ",
|
||||
description: "Extract UMI from fastq files",
|
||||
id: "toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2",
|
||||
name: "UMI-tools extract",
|
||||
},
|
||||
],
|
||||
model_class: "ToolSection",
|
||||
id: "fasta/fastq",
|
||||
name: "FASTA/FASTQ",
|
||||
},
|
||||
];
|
||||
// hyphenated tool-name is searchable
|
||||
q = "uMi tools extract ";
|
||||
expectedResults = ["toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2"];
|
||||
keys = { description: 1, name: 2, hyphenated: 0 };
|
||||
results = searchToolsByKeys(normalizeTools(tempToolsList), keys, q);
|
||||
expect(results).toEqual(expectedResults);
|
||||
});
|
||||
|
||||
it("test tool filtering helpers on toolsList given list of ids", async () => {
|
||||
|
||||
@@ -656,6 +656,7 @@ export default {
|
||||
getModule(stepData, id, this.stateStore.setLoadingState).then((response) => {
|
||||
this.stepStore.updateStep({
|
||||
...stepData,
|
||||
id: id,
|
||||
tool_state: response.tool_state,
|
||||
inputs: response.inputs,
|
||||
outputs: response.outputs,
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
:root-offset="rootOffset"
|
||||
:scroll="scroll"
|
||||
:scale="scale"
|
||||
v-on="$listeners"
|
||||
@onChange="onChange" />
|
||||
<div v-if="showRule" class="rule" />
|
||||
<node-output
|
||||
@@ -97,7 +96,7 @@
|
||||
:scroll="scroll"
|
||||
:scale="scale"
|
||||
:datatypes-mapper="datatypesMapper"
|
||||
v-on="$listeners"
|
||||
@onDragConnector="onDragConnector"
|
||||
@stopDragging="onStopDragging"
|
||||
@onChange="onChange" />
|
||||
</div>
|
||||
@@ -117,7 +116,7 @@ import NodeOutput from "@/components/Workflow/Editor/NodeOutput.vue";
|
||||
import DraggableWrapper from "@/components/Workflow/Editor/DraggablePan.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useNodePosition } from "@/components/Workflow/Editor/composables/useNodePosition";
|
||||
import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import { useWorkflowStateStore, type TerminalPosition, type XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import type { UseElementBoundingReturn, UseScrollReturn } from "@vueuse/core";
|
||||
@@ -126,6 +125,7 @@ import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import type { OutputTerminals } from "./modules/terminals";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
@@ -158,6 +158,7 @@ const emit = defineEmits([
|
||||
"onClone",
|
||||
"onUpdateStepPosition",
|
||||
"pan-by",
|
||||
"onDragConnector",
|
||||
"stopDragging",
|
||||
]);
|
||||
|
||||
@@ -240,6 +241,10 @@ const outputs = computed(() => {
|
||||
return [...stepOutputs, ...invalidOutputs.value];
|
||||
});
|
||||
|
||||
function onDragConnector(dragPosition: TerminalPosition, terminal: OutputTerminals) {
|
||||
emit("onDragConnector", dragPosition, terminal);
|
||||
}
|
||||
|
||||
function onMoveTo(position: XYPosition) {
|
||||
emit("onUpdateStepPosition", props.id, {
|
||||
top: position.y + props.scroll.y.value / props.scale,
|
||||
|
||||
@@ -196,9 +196,6 @@ export default {
|
||||
dragLeave(event) {
|
||||
this.$root.$emit("bv::hide::tooltip", this.iconId);
|
||||
},
|
||||
onChange() {
|
||||
this.$emit("onChange");
|
||||
},
|
||||
onRemove() {
|
||||
const connections = this.connectionStore.getConnectionsForTerminal(this.id);
|
||||
connections.forEach((connection) => this.terminal.disconnect(connection));
|
||||
|
||||
@@ -1,9 +1,220 @@
|
||||
<script setup lang="ts">
|
||||
import DraggableWrapper from "./DraggablePan.vue";
|
||||
import { useCoordinatePosition, type ElementBounding } from "./composables/useCoordinatePosition";
|
||||
import { useTerminal } from "./composables/useTerminal";
|
||||
import { ref, computed, watch, nextTick, toRefs, onBeforeUnmount, type Ref } from "vue";
|
||||
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import ConnectionMenu from "@/components/Workflow/Editor/ConnectionMenu.vue";
|
||||
import {
|
||||
useWorkflowStepStore,
|
||||
type OutputTerminalSource,
|
||||
type Step,
|
||||
type PostJobActions,
|
||||
type PostJobAction,
|
||||
} from "@/stores/workflowStepStore";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
import type { UseScrollReturn } from "@vueuse/core";
|
||||
|
||||
const props = defineProps<{
|
||||
output: OutputTerminalSource;
|
||||
workflowOutputs: NonNullable<Step["workflow_outputs"]>;
|
||||
stepType: Step["type"];
|
||||
stepId: number;
|
||||
postJobActions: PostJobActions;
|
||||
stepPosition: NonNullable<Step["position"]>;
|
||||
rootOffset: Ref<ElementBounding>;
|
||||
scroll: UseScrollReturn;
|
||||
scale: number;
|
||||
datatypesMapper: DatatypesMapperModel;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["pan-by", "stopDragging", "onDragConnector"]);
|
||||
const stateStore = useWorkflowStateStore();
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const el = ref(null);
|
||||
const { rootOffset, stepPosition, output, stepId, datatypesMapper } = toRefs(props);
|
||||
const position = useCoordinatePosition(el, rootOffset, stepPosition);
|
||||
const extensions = computed(() => {
|
||||
let changeDatatype: PostJobAction | undefined;
|
||||
if ("label" in props.output && props.postJobActions[`ChangeDatatypeAction${props.output.label}`]) {
|
||||
changeDatatype = props.postJobActions[`ChangeDatatypeAction${props.output.label}`];
|
||||
} else {
|
||||
changeDatatype = props.postJobActions[`ChangeDatatypeAction${props.output.name}`];
|
||||
}
|
||||
let extensions =
|
||||
changeDatatype?.action_arguments.newtype ||
|
||||
("extensions" in props.output && props.output.extensions) ||
|
||||
("type" in props.output && props.output.type) ||
|
||||
"unspecified";
|
||||
if (!Array.isArray(extensions)) {
|
||||
extensions = [extensions];
|
||||
}
|
||||
return extensions;
|
||||
});
|
||||
const effectiveOutput = ref({ ...output.value, extensions: extensions.value });
|
||||
watch(extensions, () => {
|
||||
effectiveOutput.value = { ...output.value, extensions: extensions.value };
|
||||
});
|
||||
const { terminal, isMappedOver: isMultiple } = useTerminal(stepId, effectiveOutput, datatypesMapper);
|
||||
|
||||
const workflowOutput = computed(() =>
|
||||
props.workflowOutputs.find((workflowOutput) => workflowOutput.output_name == props.output.name)
|
||||
);
|
||||
const activeClass = computed(() => workflowOutput.value && "mark-terminal-active");
|
||||
const isVisible = computed(() => {
|
||||
const isHidden = `HideDatasetAction${props.output.name}` in props.postJobActions;
|
||||
return !isHidden;
|
||||
});
|
||||
const visibleClass = computed(() => (isVisible.value ? "mark-terminal-visible" : "mark-terminal-hidden"));
|
||||
const visibleHint = computed(() => {
|
||||
if (isVisible.value) {
|
||||
return `Output will be visible in history. Click to hide output.`;
|
||||
} else {
|
||||
return `Output will be hidden in history. Click to make output visible.`;
|
||||
}
|
||||
});
|
||||
const label = computed(() => {
|
||||
const activeLabel = workflowOutput.value?.label || props.output.name;
|
||||
return `${activeLabel} (${extensions.value.join(", ")})`;
|
||||
});
|
||||
const rowClass = computed(() => {
|
||||
const classes = ["form-row", "dataRow", "output-data-row"];
|
||||
if ("valid" in props.output && props.output?.valid === false) {
|
||||
classes.push("form-row-error");
|
||||
}
|
||||
return classes;
|
||||
});
|
||||
|
||||
const menu: Ref<InstanceType<typeof ConnectionMenu> | undefined> = ref();
|
||||
const icon: Ref<HTMLElement | undefined> = ref();
|
||||
const showChildComponent = ref(false);
|
||||
|
||||
function closeMenu() {
|
||||
showChildComponent.value = false;
|
||||
}
|
||||
|
||||
async function toggleChildComponent() {
|
||||
showChildComponent.value = !showChildComponent.value;
|
||||
if (showChildComponent.value) {
|
||||
await nextTick();
|
||||
if (menu.value?.$el instanceof HTMLElement) {
|
||||
menu.value!.$el.focus();
|
||||
}
|
||||
} else {
|
||||
icon.value!.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function onToggleActive() {
|
||||
const step = stepStore.getStep(stepId.value);
|
||||
assertDefined(step);
|
||||
let stepWorkflowOutputs = [...(step.workflow_outputs || [])];
|
||||
if (workflowOutput.value) {
|
||||
stepWorkflowOutputs = stepWorkflowOutputs.filter(
|
||||
(workflowOutput) => workflowOutput.output_name !== output.value.name
|
||||
);
|
||||
} else {
|
||||
stepWorkflowOutputs.push({ output_name: output.value.name });
|
||||
}
|
||||
stepStore.updateStep({ ...step, workflow_outputs: stepWorkflowOutputs });
|
||||
}
|
||||
|
||||
function onToggleVisible() {
|
||||
const actionKey = `HideDatasetAction${props.output.name}`;
|
||||
const step = stepStore.getStep(stepId.value);
|
||||
assertDefined(step);
|
||||
if (isVisible.value) {
|
||||
step.post_job_actions = {
|
||||
...step.post_job_actions,
|
||||
[actionKey]: {
|
||||
action_type: "HideDatasetAction",
|
||||
output_name: props.output.name,
|
||||
action_arguments: {},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
if (step.post_job_actions) {
|
||||
const { [actionKey]: ignoreUnused, ...newPostJobActions } = step.post_job_actions;
|
||||
step.post_job_actions = newPostJobActions;
|
||||
} else {
|
||||
step.post_job_actions = {};
|
||||
}
|
||||
}
|
||||
stepStore.updateStep(step);
|
||||
}
|
||||
|
||||
function onPanBy(panBy: XYPosition) {
|
||||
emit("pan-by", panBy);
|
||||
}
|
||||
|
||||
function onStopDragging() {
|
||||
isDragging.value = false;
|
||||
dragX.value = 0;
|
||||
dragY.value = 0;
|
||||
emit("stopDragging");
|
||||
}
|
||||
|
||||
const dragX = ref(0);
|
||||
const dragY = ref(0);
|
||||
const isDragging = ref(false);
|
||||
|
||||
const startX = computed(() => position.left + props.scroll.x.value / props.scale + position.width / 2);
|
||||
const startY = computed(() => position.top + props.scroll.y.value / props.scale + position.height / 2);
|
||||
const endX = computed(() => {
|
||||
return (dragX.value || startX.value) + props.scroll.x.value / props.scale;
|
||||
});
|
||||
const endY = computed(() => {
|
||||
return (dragY.value || startY.value) + props.scroll.y.value / props.scale;
|
||||
});
|
||||
const dragPosition = computed(() => {
|
||||
return {
|
||||
startX: startX.value,
|
||||
endX: endX.value,
|
||||
startY: startY.value,
|
||||
endY: endY.value,
|
||||
};
|
||||
});
|
||||
const terminalPosition = computed(() => {
|
||||
return Object.freeze({ startX: startX.value, startY: startY.value });
|
||||
});
|
||||
|
||||
watch([dragPosition, isDragging], () => {
|
||||
if (isDragging.value) {
|
||||
emit("onDragConnector", dragPosition.value, terminal.value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(terminalPosition, () =>
|
||||
stateStore.setOutputTerminalPosition(props.stepId, props.output.name, terminalPosition.value)
|
||||
);
|
||||
|
||||
function onMove(dragPosition: XYPosition) {
|
||||
dragX.value = dragPosition.x + position.width / 2;
|
||||
dragY.value = dragPosition.y + position.height / 2;
|
||||
}
|
||||
|
||||
const id = computed(() => `node-${props.stepId}-output-${props.output.name}`);
|
||||
const showCalloutActiveOutput = computed(() => props.stepType === "tool" || props.stepType === "subworkflow");
|
||||
const showCalloutVisible = computed(() => props.stepType === "tool");
|
||||
const terminalClass = computed(() => {
|
||||
const cls = "terminal output-terminal";
|
||||
if (isMultiple.value) {
|
||||
return `${cls} multiple`;
|
||||
}
|
||||
return cls;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stateStore.deleteOutputTerminalPosition(props.stepId, props.output.name);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div :class="rowClass">
|
||||
<div :class="rowClass" :data-output-name="output.name">
|
||||
<div
|
||||
v-if="showCalloutActiveOutput"
|
||||
v-b-tooltip
|
||||
:class="['callout-terminal', output.name]"
|
||||
class="callout-terminal"
|
||||
title="Checked outputs will become primary workflow outputs and are available as subworkflow outputs."
|
||||
@keyup="onToggleActive"
|
||||
@click="onToggleActive">
|
||||
@@ -12,7 +223,7 @@
|
||||
<div
|
||||
v-if="showCalloutVisible"
|
||||
v-b-tooltip
|
||||
:class="['callout-terminal', output.name]"
|
||||
class="callout-terminal"
|
||||
:title="visibleHint"
|
||||
@keyup="onToggleVisible"
|
||||
@click="onToggleVisible">
|
||||
@@ -50,267 +261,3 @@
|
||||
</draggable-wrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DraggableWrapper from "./DraggablePan";
|
||||
import { useCoordinatePosition } from "./composables/useCoordinatePosition";
|
||||
import { useTerminal } from "./composables/useTerminal";
|
||||
import { ref, computed, watch, nextTick, toRefs } from "vue";
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
import ConnectionMenu from "@/components/Workflow/Editor/ConnectionMenu";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConnectionMenu,
|
||||
DraggableWrapper,
|
||||
},
|
||||
props: {
|
||||
output: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
workflowOutputs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
stepType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stepId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
postJobActions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
stepPosition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
rootOffset: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
scroll: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
datatypesMapper: {
|
||||
type: DatatypesMapperModel,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const stateStore = useWorkflowStateStore();
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const el = ref(null);
|
||||
const { rootOffset, stepPosition, output, stepId, datatypesMapper } = toRefs(props);
|
||||
const position = useCoordinatePosition(el, rootOffset, stepPosition);
|
||||
const extensions = computed(() => {
|
||||
const changeDatatype =
|
||||
props.postJobActions[`ChangeDatatypeAction${props.output.label}`] ||
|
||||
props.postJobActions[`ChangeDatatypeAction${props.output.name}`];
|
||||
let extensions =
|
||||
changeDatatype?.action_arguments.newtype ||
|
||||
props.output.extensions ||
|
||||
props.output.type ||
|
||||
"unspecified";
|
||||
if (!Array.isArray(extensions)) {
|
||||
extensions = [extensions];
|
||||
}
|
||||
return extensions;
|
||||
});
|
||||
const effectiveOutput = ref({ ...output.value, extensions: extensions.value });
|
||||
watch(extensions, () => {
|
||||
effectiveOutput.value = { ...output.value, extensions: extensions.value };
|
||||
});
|
||||
const { terminal, isMappedOver: isMultiple } = useTerminal(stepId, effectiveOutput, datatypesMapper);
|
||||
|
||||
const workflowOutput = computed(() =>
|
||||
props.workflowOutputs.find((workflowOutput) => workflowOutput.output_name == props.output.name)
|
||||
);
|
||||
const activeClass = computed(() => workflowOutput.value && "mark-terminal-active");
|
||||
const isVisible = computed(() => {
|
||||
const isHidden = `HideDatasetAction${props.output.name}` in props.postJobActions;
|
||||
return !isHidden;
|
||||
});
|
||||
const visibleClass = computed(() => (isVisible.value ? "mark-terminal-visible" : "mark-terminal-hidden"));
|
||||
const visibleHint = computed(() => {
|
||||
if (isVisible.value) {
|
||||
return `Output will be visible in history. Click to hide output.`;
|
||||
} else {
|
||||
return `Output will be hidden in history. Click to make output visible.`;
|
||||
}
|
||||
});
|
||||
const label = computed(() => {
|
||||
const activeLabel = workflowOutput.value?.label || props.output.name;
|
||||
return `${activeLabel} (${extensions.value.join(", ")})`;
|
||||
});
|
||||
const rowClass = computed(() => {
|
||||
const classes = ["form-row", "dataRow", "output-data-row"];
|
||||
if (props.output?.valid === false) {
|
||||
classes.push("form-row-error");
|
||||
}
|
||||
return classes;
|
||||
});
|
||||
|
||||
const menu = ref(null);
|
||||
const icon = ref(null);
|
||||
const showChildComponent = ref(false);
|
||||
|
||||
function closeMenu() {
|
||||
showChildComponent.value = false;
|
||||
}
|
||||
|
||||
async function toggleChildComponent() {
|
||||
showChildComponent.value = !showChildComponent.value;
|
||||
if (showChildComponent.value) {
|
||||
await nextTick();
|
||||
menu.value.$el.focus();
|
||||
} else {
|
||||
icon.value.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function onToggleActive() {
|
||||
const step = stepStore.getStep(stepId.value);
|
||||
if (workflowOutput.value) {
|
||||
step.workflow_outputs = step.workflow_outputs.filter(
|
||||
(workflowOutput) => workflowOutput.output_name !== output.value.name
|
||||
);
|
||||
} else {
|
||||
step.workflow_outputs.push({ output_name: output.value.name });
|
||||
}
|
||||
stepStore.updateStep(step);
|
||||
}
|
||||
|
||||
function onToggleVisible() {
|
||||
const actionKey = `HideDatasetAction${props.output.name}`;
|
||||
const step = stepStore.getStep(stepId.value);
|
||||
if (isVisible.value) {
|
||||
step.post_job_actions = {
|
||||
...step.post_job_actions,
|
||||
[actionKey]: {
|
||||
action_type: "HideDatasetAction",
|
||||
output_name: props.output.name,
|
||||
action_arguments: {},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const { [actionKey]: ignoreUnused, ...newPostJobActions } = step.post_job_actions;
|
||||
step.post_job_actions = newPostJobActions;
|
||||
}
|
||||
stepStore.updateStep(step);
|
||||
}
|
||||
|
||||
return {
|
||||
el,
|
||||
icon,
|
||||
position,
|
||||
activeClass,
|
||||
visibleClass,
|
||||
visibleHint,
|
||||
rowClass,
|
||||
isVisible,
|
||||
terminal,
|
||||
isMultiple,
|
||||
label,
|
||||
stateStore,
|
||||
menu,
|
||||
showChildComponent,
|
||||
toggleChildComponent,
|
||||
closeMenu,
|
||||
effectiveOutput,
|
||||
onToggleActive,
|
||||
onToggleVisible,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDragging: false,
|
||||
dragX: 0,
|
||||
dragY: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
terminalPosition() {
|
||||
return Object.freeze({ startX: this.startX, startY: this.startY });
|
||||
},
|
||||
startX() {
|
||||
return this.position.left + this.scroll.x.value / this.scale + this.position.width / 2;
|
||||
},
|
||||
startY() {
|
||||
return this.position.top + this.scroll.y.value / this.scale + this.position.height / 2;
|
||||
},
|
||||
endX() {
|
||||
return (this.dragX || this.startX) + this.scroll.x.value / this.scale;
|
||||
},
|
||||
endY() {
|
||||
return (this.dragY || this.startY) + this.scroll.y.value / this.scale;
|
||||
},
|
||||
dragPosition() {
|
||||
return {
|
||||
startX: this.startX,
|
||||
endX: this.endX,
|
||||
startY: this.startY,
|
||||
endY: this.endY,
|
||||
};
|
||||
},
|
||||
id() {
|
||||
return `node-${this.stepId}-output-${this.output.name}`;
|
||||
},
|
||||
showCalloutActiveOutput() {
|
||||
return this.stepType === "tool" || this.stepType === "subworkflow";
|
||||
},
|
||||
showCalloutVisible() {
|
||||
return this.stepType === "tool";
|
||||
},
|
||||
terminalClass() {
|
||||
const cls = "terminal output-terminal";
|
||||
if (this.isMultiple) {
|
||||
return `${cls} multiple`;
|
||||
}
|
||||
return cls;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
terminalPosition(position) {
|
||||
this.stateStore.setOutputTerminalPosition(this.stepId, this.output.name, position);
|
||||
},
|
||||
dragPosition() {
|
||||
if (this.isDragging) {
|
||||
this.$emit("onDragConnector", this.dragPosition, this.terminal);
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stateStore.deleteOutputTerminalPosition({
|
||||
stepId: this.stepId,
|
||||
outputName: this.output.name,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onPanBy(panBy) {
|
||||
this.$emit("pan-by", panBy);
|
||||
},
|
||||
onMove(position, event) {
|
||||
this.dragX = position.x + this.position.width / 2;
|
||||
this.dragY = position.y + this.position.height / 2;
|
||||
},
|
||||
onStopDragging(e) {
|
||||
this.isDragging = false;
|
||||
this.dragX = 0;
|
||||
this.dragY = 0;
|
||||
this.$emit("stopDragging");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -30,8 +30,13 @@ export function useShortTermStorage() {
|
||||
return prepareObjectDownload(invocationId, "invocations", options);
|
||||
}
|
||||
|
||||
function downloadObjectByRequestId(storageRequestId) {
|
||||
function getDownloadObjectUrl(storageRequestId) {
|
||||
const url = withPrefix(`/api/short_term_storage/${storageRequestId}`);
|
||||
return url;
|
||||
}
|
||||
|
||||
function downloadObjectByRequestId(storageRequestId) {
|
||||
const url = getDownloadObjectUrl(storageRequestId);
|
||||
window.location.assign(url);
|
||||
}
|
||||
|
||||
@@ -119,5 +124,10 @@ export function useShortTermStorage() {
|
||||
* Whether the download is still being prepared.
|
||||
*/
|
||||
isPreparing: readonly(isPreparing),
|
||||
/**
|
||||
* Given a storageRequestId it returns the download URL for that object.
|
||||
* @param {String} storageRequestId The storage request ID associated to the object to be downloaded
|
||||
*/
|
||||
getDownloadObjectUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { pushOrSet } from "@/utils/pushOrSet";
|
||||
import Vue from "vue";
|
||||
|
||||
interface InvalidConnections {
|
||||
@@ -9,6 +10,9 @@ interface InvalidConnections {
|
||||
export interface State {
|
||||
connections: Connection[];
|
||||
invalidConnections: InvalidConnections;
|
||||
inputTerminalToOutputTerminals: TerminalToOutputTerminals;
|
||||
terminalToConnection: { [index: string]: Connection[] };
|
||||
stepToConnections: { [index: number]: Connection[] };
|
||||
}
|
||||
|
||||
export class Connection {
|
||||
@@ -44,80 +48,38 @@ interface TerminalToOutputTerminals {
|
||||
[index: string]: OutputTerminal[];
|
||||
}
|
||||
|
||||
interface TerminalToInputTerminals {
|
||||
[index: string]: InputTerminal[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
|
||||
* @param object Object which contains array
|
||||
* @param key Key which array is in
|
||||
* @param value Value to push
|
||||
*/
|
||||
function pushOrSet<T>(object: { [key: string | number]: Array<T> }, key: string | number, value: T) {
|
||||
if (key in object) {
|
||||
object[key]!.push(value);
|
||||
} else {
|
||||
object[key] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
state: (): State => ({
|
||||
connections: [] as Connection[],
|
||||
invalidConnections: {} as InvalidConnections,
|
||||
inputTerminalToOutputTerminals: {} as TerminalToOutputTerminals,
|
||||
terminalToConnection: {} as { [index: string]: Connection[] },
|
||||
stepToConnections: {} as { [index: number]: Connection[] },
|
||||
}),
|
||||
getters: {
|
||||
getOutputTerminalsForInputTerminal(state: State) {
|
||||
const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
|
||||
state.connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const inputTerminalId = getTerminalId(terminals.input);
|
||||
pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
|
||||
});
|
||||
return (terminalId: string): OutputTerminal[] => {
|
||||
return inputTerminalToOutputTerminals[terminalId] || [];
|
||||
};
|
||||
},
|
||||
getInputTerminalsForOutputTerminal(state: State) {
|
||||
const outputTerminalToInputTerminals: TerminalToInputTerminals = {};
|
||||
state.connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const outputTerminalId = getTerminalId(terminals.output);
|
||||
pushOrSet(outputTerminalToInputTerminals, outputTerminalId, terminals.input);
|
||||
});
|
||||
return (terminalId: string): BaseTerminal[] => {
|
||||
return outputTerminalToInputTerminals[terminalId] || [];
|
||||
return state.inputTerminalToOutputTerminals[terminalId] || [];
|
||||
};
|
||||
},
|
||||
getConnectionsForTerminal(state: State) {
|
||||
const terminalToConnection: { [index: string]: Connection[] } = {};
|
||||
state.connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const outputTerminalId = getTerminalId(terminals.output);
|
||||
pushOrSet(terminalToConnection, outputTerminalId, connection);
|
||||
|
||||
const inputTerminalId = getTerminalId(terminals.input);
|
||||
pushOrSet(terminalToConnection, inputTerminalId, connection);
|
||||
});
|
||||
return (terminalId: string): Connection[] => {
|
||||
return terminalToConnection[terminalId] || [];
|
||||
return state.terminalToConnection[terminalId] || [];
|
||||
};
|
||||
},
|
||||
getConnectionsForStep(state: State) {
|
||||
const stepToConnections: { [index: number]: Connection[] } = {};
|
||||
state.connections.map((connection) => {
|
||||
pushOrSet(stepToConnections, connection.input.stepId, connection);
|
||||
pushOrSet(stepToConnections, connection.output.stepId, connection);
|
||||
});
|
||||
return (stepId: number): Connection[] => stepToConnections[stepId] || [];
|
||||
return (stepId: number): Connection[] => state.stepToConnections[stepId] || [];
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
addConnection(this: State, connection: Connection) {
|
||||
addConnection(this, _connection: Connection) {
|
||||
const connection = Object.freeze(_connection);
|
||||
this.connections.push(connection);
|
||||
const stepStore = useWorkflowStepStore();
|
||||
stepStore.addConnection(connection);
|
||||
this.terminalToConnection = updateTerminalToConnection(this.connections);
|
||||
this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
|
||||
this.stepToConnections = updateStepToConnections(this.connections);
|
||||
},
|
||||
markInvalidConnection(this: State, connectionId: string, reason: string) {
|
||||
Vue.set(this.invalidConnections, connectionId, reason);
|
||||
@@ -125,7 +87,7 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
dropFromInvalidConnections(this: State, connectionId: string) {
|
||||
Vue.delete(this.invalidConnections, connectionId);
|
||||
},
|
||||
removeConnection(this: State, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
|
||||
removeConnection(this, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
|
||||
const stepStore = useWorkflowStepStore();
|
||||
this.connections = this.connections.filter((connection) => {
|
||||
if (typeof terminal === "string") {
|
||||
@@ -154,10 +116,44 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
}
|
||||
}
|
||||
});
|
||||
this.terminalToConnection = updateTerminalToConnection(this.connections);
|
||||
this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
|
||||
this.stepToConnections = updateStepToConnections(this.connections);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function updateTerminalToTerminal(connections: Connection[]) {
|
||||
const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
|
||||
connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const inputTerminalId = getTerminalId(terminals.input);
|
||||
pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
|
||||
});
|
||||
return inputTerminalToOutputTerminals;
|
||||
}
|
||||
|
||||
function updateTerminalToConnection(connections: Connection[]) {
|
||||
const terminalToConnection: { [index: string]: Connection[] } = {};
|
||||
connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const outputTerminalId = getTerminalId(terminals.output);
|
||||
pushOrSet(terminalToConnection, outputTerminalId, connection);
|
||||
const inputTerminalId = getTerminalId(terminals.input);
|
||||
pushOrSet(terminalToConnection, inputTerminalId, connection);
|
||||
});
|
||||
return terminalToConnection;
|
||||
}
|
||||
|
||||
function updateStepToConnections(connections: Connection[]) {
|
||||
const stepToConnections: { [index: number]: Connection[] } = {};
|
||||
connections.map((connection) => {
|
||||
pushOrSet(stepToConnections, connection.input.stepId, connection);
|
||||
pushOrSet(stepToConnections, connection.output.stepId, connection);
|
||||
});
|
||||
return stepToConnections;
|
||||
}
|
||||
|
||||
export function getTerminalId(item: BaseTerminal): string {
|
||||
return `node-${item.stepId}-${item.connectorType}-${item.name}`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,16 @@ import { defineStore } from "pinia";
|
||||
import type { OutputTerminals } from "@/components/Workflow/Editor/modules/terminals";
|
||||
import type { UseElementBoundingReturn } from "@vueuse/core";
|
||||
|
||||
export interface InputTerminalPosition {
|
||||
endX: number;
|
||||
endY: number;
|
||||
}
|
||||
|
||||
export interface OutputTerminalPosition {
|
||||
startX: number;
|
||||
startY: number;
|
||||
}
|
||||
|
||||
export interface TerminalPosition {
|
||||
startX: number;
|
||||
endX: number;
|
||||
@@ -17,8 +27,8 @@ export interface XYPosition {
|
||||
}
|
||||
|
||||
interface State {
|
||||
inputTerminals: { [index: number]: { [index: string]: TerminalPosition } };
|
||||
outputTerminals: { [index: number]: { [index: string]: TerminalPosition } };
|
||||
inputTerminals: { [index: number]: { [index: string]: InputTerminalPosition } };
|
||||
outputTerminals: { [index: number]: { [index: string]: OutputTerminalPosition } };
|
||||
draggingPosition: TerminalPosition | null;
|
||||
draggingTerminal: OutputTerminals | null;
|
||||
activeNodeId: number | null;
|
||||
@@ -50,14 +60,14 @@ export const useWorkflowStateStore = defineStore("workflowStateStore", {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setInputTerminalPosition(stepId: number, inputName: string, position: TerminalPosition) {
|
||||
setInputTerminalPosition(stepId: number, inputName: string, position: InputTerminalPosition) {
|
||||
if (this.inputTerminals[stepId]) {
|
||||
Vue.set(this.inputTerminals[stepId]!, inputName, position);
|
||||
} else {
|
||||
Vue.set(this.inputTerminals, stepId, { [inputName]: position });
|
||||
}
|
||||
},
|
||||
setOutputTerminalPosition(stepId: number, outputName: string, position: TerminalPosition) {
|
||||
setOutputTerminalPosition(stepId: number, outputName: string, position: OutputTerminalPosition) {
|
||||
if (this.outputTerminals[stepId]) {
|
||||
Vue.set(this.outputTerminals[stepId]!, outputName, position);
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ const stepInputConnection: StepInputConnection = {
|
||||
};
|
||||
|
||||
const workflowStepZero: NewStep = {
|
||||
id: 0,
|
||||
input_connections: {},
|
||||
inputs: [],
|
||||
name: "a step",
|
||||
@@ -33,7 +34,7 @@ describe("Connection Store", () => {
|
||||
const stepStore = useWorkflowStepStore();
|
||||
expect(stepStore.steps).toStrictEqual({});
|
||||
stepStore.addStep(workflowStepZero);
|
||||
expect(stepStore.getStep(0)).toBe(workflowStepZero);
|
||||
expect(stepStore.getStep(0)).toStrictEqual(workflowStepZero);
|
||||
expect(workflowStepZero.id).toBe(0);
|
||||
});
|
||||
it("removes step", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ interface State {
|
||||
stepIndex: number;
|
||||
stepMapOver: { [index: number]: CollectionTypeDescriptor };
|
||||
stepInputMapOver: StepInputMapOver;
|
||||
stepExtraInputs: { [index: number]: InputTerminalSource[] };
|
||||
}
|
||||
|
||||
interface StepPosition {
|
||||
@@ -130,7 +131,7 @@ export interface ConnectionOutputLink {
|
||||
input_subworkflow_step_id?: number;
|
||||
}
|
||||
|
||||
interface WorkflowOutputs {
|
||||
export interface WorkflowOutputs {
|
||||
[index: string]: {
|
||||
stepId: number;
|
||||
outputName: string;
|
||||
@@ -147,6 +148,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
stepMapOver: {} as { [index: number]: CollectionTypeDescriptor },
|
||||
stepInputMapOver: {} as StepInputMapOver,
|
||||
stepIndex: -1,
|
||||
stepExtraInputs: {} as { [index: number]: InputTerminalSource[] },
|
||||
}),
|
||||
getters: {
|
||||
getStep(state: State) {
|
||||
@@ -155,30 +157,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
};
|
||||
},
|
||||
getStepExtraInputs(state: State) {
|
||||
const extraInputs: { [index: number]: InputTerminalSource[] } = {};
|
||||
Object.values(state.steps).forEach((step) => {
|
||||
if (step?.when !== undefined) {
|
||||
Object.keys(step.input_connections).forEach((inputName) => {
|
||||
if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) {
|
||||
const terminalSource = {
|
||||
name: inputName,
|
||||
optional: false,
|
||||
input_type: "parameter" as const,
|
||||
type: "boolean" as const,
|
||||
multiple: false,
|
||||
label: inputName,
|
||||
extensions: [],
|
||||
};
|
||||
if (extraInputs[step.id]) {
|
||||
extraInputs[step.id]!.push(terminalSource);
|
||||
} else {
|
||||
extraInputs[step.id] = [terminalSource];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return (stepId: number) => extraInputs[stepId] || [];
|
||||
return (stepId: number) => this.stepExtraInputs[stepId] || [];
|
||||
},
|
||||
getStepIndex(state: State) {
|
||||
return Math.max(...Object.values(state.steps).map((step) => step.id), state.stepIndex);
|
||||
@@ -206,18 +185,19 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
actions: {
|
||||
addStep(newStep: NewStep): Step {
|
||||
const stepId = newStep.id ? newStep.id : this.getStepIndex + 1;
|
||||
newStep.id = stepId;
|
||||
const step = newStep as Step;
|
||||
const step = Object.freeze({ ...newStep, id: stepId } as Step);
|
||||
Vue.set(this.steps, stepId.toString(), step);
|
||||
const connectionStore = useConnectionStore();
|
||||
stepToConnections(step).map((connection) => connectionStore.addConnection(connection));
|
||||
this.stepExtraInputs[step.id] = getStepExtraInputs(step);
|
||||
return step;
|
||||
},
|
||||
updateStep(this: State, step: Step) {
|
||||
step.workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
|
||||
const workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
|
||||
step.outputs.find((output) => workflowOutput.output_name == output.name)
|
||||
);
|
||||
this.steps[step.id.toString()] = step;
|
||||
this.steps[step.id.toString()] = Object.freeze({ ...step, workflow_outputs });
|
||||
this.stepExtraInputs[step.id] = getStepExtraInputs(step);
|
||||
},
|
||||
changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) {
|
||||
Vue.set(this.stepMapOver, stepId, mapOver);
|
||||
@@ -298,6 +278,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
.getConnectionsForStep(stepId)
|
||||
.forEach((connection) => connectionStore.removeConnection(connection.id));
|
||||
Vue.delete(this.steps, stepId.toString());
|
||||
Vue.delete(this.stepExtraInputs, stepId);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -335,3 +316,24 @@ export function stepToConnections(step: Step): Connection[] {
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
function getStepExtraInputs(step: Step) {
|
||||
const extraInputs: InputTerminalSource[] = [];
|
||||
if (step.when !== undefined) {
|
||||
Object.keys(step.input_connections).forEach((inputName) => {
|
||||
if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) {
|
||||
const terminalSource = {
|
||||
name: inputName,
|
||||
optional: false,
|
||||
input_type: "parameter" as const,
|
||||
type: "boolean" as const,
|
||||
multiple: false,
|
||||
label: inputName,
|
||||
extensions: [],
|
||||
};
|
||||
extraInputs.push(terminalSource);
|
||||
}
|
||||
});
|
||||
}
|
||||
return extraInputs;
|
||||
}
|
||||
|
||||
@@ -612,8 +612,8 @@ workflow_editor:
|
||||
output_terminal: "${_} [output-name='${name}']"
|
||||
input_terminal: "${_} [input-name='${name}']"
|
||||
input_mapping_icon: "${_} [input-name='${name}'].multiple"
|
||||
workflow_output_toggle: "${_} .callout-terminal.${name}"
|
||||
workflow_output_toggle_active: "${_} .callout-terminal.${name} .mark-terminal-active"
|
||||
workflow_output_toggle: "${_} [data-output-name='${name}'] .callout-terminal "
|
||||
workflow_output_toggle_active: "${_} [data-output-name='${name}'] .mark-terminal-active"
|
||||
selectors:
|
||||
canvas_body: '#workflow-canvas'
|
||||
edit_annotation: '#workflow-annotation'
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
|
||||
* @param object Object which contains array
|
||||
* @param key Key which array is in
|
||||
* @param value Value to push
|
||||
*/
|
||||
export function pushOrSet<T, K extends string | number | symbol>(object: { [key in K]: Array<T> }, key: K, value: T) {
|
||||
if (key in object) {
|
||||
object[key]!.push(value);
|
||||
} else {
|
||||
object[key] = [value];
|
||||
}
|
||||
}
|
||||
+23
-9
@@ -7582,6 +7582,14 @@ last-run@^1.1.0:
|
||||
default-resolution "^2.0.0"
|
||||
es6-weak-map "^2.0.1"
|
||||
|
||||
launch-editor@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7"
|
||||
integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==
|
||||
dependencies:
|
||||
picocolors "^1.0.0"
|
||||
shell-quote "^1.7.3"
|
||||
|
||||
lazystream@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz"
|
||||
@@ -9936,6 +9944,11 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-quote@^1.7.3:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
|
||||
integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
|
||||
|
||||
side-channel@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz"
|
||||
@@ -11258,10 +11271,10 @@ webpack-dev-middleware@^5.3.1:
|
||||
range-parser "^1.2.1"
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
webpack-dev-server@^4.11.1:
|
||||
version "4.11.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5"
|
||||
integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==
|
||||
webpack-dev-server@^4.12.0:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.12.0.tgz#e2dcad4d43e486c3bac48ddbf346e77ef03c7428"
|
||||
integrity sha512-XRN9YRnvOj3TQQ5w/0pR1y1xDcVnbWtNkTri46kuEbaWUPTHsWUvOyAAI7PZHLY+hsFki2kRltJjKMw7e+IiqA==
|
||||
dependencies:
|
||||
"@types/bonjour" "^3.5.9"
|
||||
"@types/connect-history-api-fallback" "^1.3.5"
|
||||
@@ -11282,6 +11295,7 @@ webpack-dev-server@^4.11.1:
|
||||
html-entities "^2.3.2"
|
||||
http-proxy-middleware "^2.0.3"
|
||||
ipaddr.js "^2.0.1"
|
||||
launch-editor "^2.6.0"
|
||||
open "^8.0.9"
|
||||
p-retry "^4.5.0"
|
||||
rimraf "^3.0.2"
|
||||
@@ -11291,7 +11305,7 @@ webpack-dev-server@^4.11.1:
|
||||
sockjs "^0.3.24"
|
||||
spdy "^4.0.2"
|
||||
webpack-dev-middleware "^5.3.1"
|
||||
ws "^8.4.2"
|
||||
ws "^8.13.0"
|
||||
|
||||
webpack-merge@^5.7.3, webpack-merge@^5.8.0:
|
||||
version "5.8.0"
|
||||
@@ -11534,10 +11548,10 @@ ws@^8.11.0:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
|
||||
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
|
||||
|
||||
ws@^8.4.2:
|
||||
version "8.8.1"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz"
|
||||
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
|
||||
ws@^8.13.0:
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
|
||||
xml-beautifier@^0.5.0:
|
||||
version "0.5.0"
|
||||
|
||||
@@ -4881,6 +4881,9 @@
|
||||
The `broker_url` option, if unset, defaults to the value of
|
||||
`amqp_internal_connection`. The `result_backend` option must be
|
||||
set if the `enable_celery_tasks` option is set.
|
||||
The galaxy.fetch_data task can be disabled by setting its route to
|
||||
"disabled": `galaxy.fetch_data: disabled`. (Other tasks cannot be
|
||||
disabled on a per-task basis at this time.)
|
||||
For details, see Celery documentation at
|
||||
https://docs.celeryq.dev/en/stable/userguide/configuration.html.
|
||||
:Default: ``{'task_routes': {'galaxy.fetch_data': 'galaxy.external', 'galaxy.set_job_metadata': 'galaxy.external'}}``
|
||||
|
||||
@@ -137,6 +137,7 @@ LOGGING_CONFIG_DEFAULT: Dict[str, Any] = {
|
||||
|
||||
VERSION_JSON_FILE = "version.json"
|
||||
DEFAULT_EMAIL_FROM_LOCAL_PART = "galaxy-no-reply"
|
||||
DISABLED_FLAG = "disabled" # Used to mark a config option as disabled
|
||||
|
||||
|
||||
def configure_logging(config, facts=None):
|
||||
@@ -1289,6 +1290,17 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin):
|
||||
f"Config option '{key}' is deprecated and will be removed in a future release. Please consult the latest version of the sample configuration file."
|
||||
)
|
||||
|
||||
def is_fetch_with_celery_enabled(self):
|
||||
"""
|
||||
True iff celery is enabled and celery_conf["task_routes"]["galaxy.fetch_data"] != DISABLED_FLAG.
|
||||
"""
|
||||
celery_enabled = self.enable_celery_tasks
|
||||
try:
|
||||
fetch_disabled = self.celery_conf["task_routes"]["galaxy.fetch_data"] == DISABLED_FLAG
|
||||
except (TypeError, KeyError): # celery_conf is None or sub-dictionary is none or either key is not present
|
||||
fetch_disabled = False
|
||||
return celery_enabled and not fetch_disabled
|
||||
|
||||
@staticmethod
|
||||
def _parse_allowed_origin_hostnames(allowed_origin_hostnames):
|
||||
"""
|
||||
|
||||
@@ -2613,6 +2613,9 @@ galaxy:
|
||||
# The `broker_url` option, if unset, defaults to the value of
|
||||
# `amqp_internal_connection`. The `result_backend` option must be set
|
||||
# if the `enable_celery_tasks` option is set.
|
||||
# The galaxy.fetch_data task can be disabled by setting its route to
|
||||
# "disabled": `galaxy.fetch_data: disabled`. (Other tasks cannot be
|
||||
# disabled on a per-task basis at this time.)
|
||||
# For details, see Celery documentation at
|
||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html.
|
||||
#celery_conf:
|
||||
|
||||
@@ -3564,6 +3564,9 @@ mapping:
|
||||
The `broker_url` option, if unset, defaults to the value of `amqp_internal_connection`.
|
||||
The `result_backend` option must be set if the `enable_celery_tasks` option is set.
|
||||
|
||||
The galaxy.fetch_data task can be disabled by setting its route to "disabled": `galaxy.fetch_data: disabled`.
|
||||
(Other tasks cannot be disabled on a per-task basis at this time.)
|
||||
|
||||
For details, see Celery documentation at https://docs.celeryq.dev/en/stable/userguide/configuration.html.
|
||||
|
||||
enable_celery_tasks:
|
||||
|
||||
@@ -187,7 +187,7 @@ tinydb==4.7.1 ; python_version >= "3.7" and python_version < "3.12"
|
||||
tornado==6.2 ; python_version >= "3.7" and python_version < "3.12"
|
||||
tqdm==4.64.1 ; python_version >= "3.7" and python_version < "3.12"
|
||||
tuspy==1.0.0 ; python_version >= "3.7" and python_version < "3.12"
|
||||
tuswsgi==0.5.4 ; python_version >= "3.7" and python_version < "3.12"
|
||||
tuswsgi==0.5.5 ; python_version >= "3.7" and python_version < "3.12"
|
||||
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.12"
|
||||
tzlocal==2.1 ; python_version >= "3.7" and python_version < "3.12"
|
||||
ubiquerg==0.6.2 ; python_version >= "3.7" and python_version < "3.12"
|
||||
|
||||
@@ -182,7 +182,7 @@ def execute(
|
||||
tool_id = tool.id
|
||||
for job2 in execution_tracker.successful_jobs:
|
||||
# Put the job in the queue if tracking in memory
|
||||
if tool_id == "__DATA_FETCH__" and tool.app.config.enable_celery_tasks:
|
||||
if tool_id == "__DATA_FETCH__" and tool.app.config.is_fetch_with_celery_enabled():
|
||||
job_id = job2.id
|
||||
from galaxy.celery.tasks import (
|
||||
fetch_data,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Unpack a tar or tar.gz archive into a directory.
|
||||
Unpack a tar, tar.gz or zip archive into a directory.
|
||||
|
||||
usage: %prog archive_source dest_dir
|
||||
--[url|file] source type, either a URL or a file.
|
||||
@@ -11,6 +11,7 @@ import math
|
||||
import optparse
|
||||
import os
|
||||
import tarfile
|
||||
import zipfile
|
||||
from base64 import b64decode
|
||||
|
||||
from galaxy.files import ConfiguredFileSources
|
||||
@@ -35,11 +36,19 @@ def check_archive(archive_file, dest_dir):
|
||||
Ensure that a tar archive has no absolute paths or relative paths outside
|
||||
the archive.
|
||||
"""
|
||||
with tarfile.open(archive_file, mode="r") as archive_fp:
|
||||
for arc_path in archive_fp.getnames():
|
||||
assert os.path.normpath(os.path.join(dest_dir, arc_path)).startswith(
|
||||
dest_dir.rstrip(os.sep) + os.sep
|
||||
), f"Archive member would extract outside target directory: {arc_path}"
|
||||
if zipfile.is_zipfile(archive_file):
|
||||
with zipfile.ZipFile(archive_file, "r") as archive_fp:
|
||||
for arc_path in archive_fp.namelist():
|
||||
assert not os.path.isabs(arc_path), f"Archive member has absolute path: {arc_path}"
|
||||
assert not os.path.relpath(arc_path).startswith(
|
||||
".."
|
||||
), f"Archive member would extract outside target directory: {arc_path}"
|
||||
else:
|
||||
with tarfile.open(archive_file, mode="r") as archive_fp:
|
||||
for arc_path in archive_fp.getnames():
|
||||
assert os.path.normpath(os.path.join(dest_dir, arc_path)).startswith(
|
||||
dest_dir.rstrip(os.sep) + os.sep
|
||||
), f"Archive member would extract outside target directory: {arc_path}"
|
||||
return True
|
||||
|
||||
|
||||
@@ -47,9 +56,13 @@ def unpack_archive(archive_file, dest_dir):
|
||||
"""
|
||||
Unpack a tar and/or gzipped archive into a destination directory.
|
||||
"""
|
||||
archive_fp = tarfile.open(archive_file, mode="r")
|
||||
archive_fp.extractall(path=dest_dir)
|
||||
archive_fp.close()
|
||||
if zipfile.is_zipfile(archive_file):
|
||||
with zipfile.ZipFile(archive_file, "r") as zip_archive:
|
||||
zip_archive.extractall(path=dest_dir)
|
||||
else:
|
||||
archive_fp = tarfile.open(archive_file, mode="r")
|
||||
archive_fp.extractall(path=dest_dir)
|
||||
archive_fp.close()
|
||||
|
||||
|
||||
def main(options, args):
|
||||
|
||||
@@ -7,7 +7,7 @@ from galaxy.config import DEFAULT_EMAIL_FROM_LOCAL_PART
|
||||
from galaxy.util.properties import running_from_source
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@pytest.fixture()
|
||||
def appconfig():
|
||||
return config.GalaxyAppConfiguration(override_tempdir=False)
|
||||
|
||||
@@ -50,3 +50,44 @@ def test_assign_email_from(monkeypatch):
|
||||
override_tempdir=False, galaxy_infrastructure_url="http://myhost:8080/galaxy/"
|
||||
)
|
||||
assert appconfig.email_from == f"{DEFAULT_EMAIL_FROM_LOCAL_PART}@myhost"
|
||||
|
||||
|
||||
class TestIsFetchWithCeleryEnabled:
|
||||
def test_disabled_if_celery_disabled(self, appconfig):
|
||||
appconfig.enable_celery_tasks = False
|
||||
assert not appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_enabled_if_no_celeryconf(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf = None
|
||||
assert appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_enabled_if_no_task_routes_key(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf = {"some-other-key": 1}
|
||||
assert appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_enabled_if_task_routes_empty(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf["task_routes"] = None
|
||||
assert appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_enabled_if_no_route_key(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf["task_routes"] = {"some-other-route": 1}
|
||||
assert appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_enabled_if_no_route(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = None
|
||||
assert appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_enabled_if_has_route(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = "my_route"
|
||||
assert appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
def test_disabled_if_disabled_flag(self, appconfig):
|
||||
appconfig.enable_celery_tasks = True
|
||||
appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = config.DISABLED_FLAG
|
||||
assert not appconfig.is_fetch_with_celery_enabled()
|
||||
|
||||
Reference in New Issue
Block a user