Merge remote-tracking branch 'upstream/release_23.0' into dev

This commit is contained in:
Dannon Baker
2023-01-30 14:25:55 -05:00
28 changed files with 387 additions and 140 deletions
@@ -20,12 +20,16 @@
@click="submit('attribute', 'attributes')">
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
</b-button>
<b-button @click="submit('attribute', 'autodetect')">
<b-button v-if="!result['metadata_disable']" @click="submit('attribute', 'autodetect')">
<font-awesome-icon icon="redo" class="mr-1" />{{ "Auto-detect" | l }}
</b-button>
</div>
</b-tab>
<b-tab v-if="!result['conversion_disable'] || !result['datatype_disable']">
<b-tab
v-if="
(!result['conversion_disable'] || !result['datatype_disable']) &&
!result['metadata_disable']
">
<template v-slot:title>
<span v-if="!result['conversion_disable']">
<font-awesome-icon icon="cog" class="mr-1" />{{ "Convert" | l }}
@@ -56,6 +56,11 @@ describe("History Navigation", () => {
provide: { store },
});
const createButton = wrapper.find("*[data-description='create new history']");
expect(createButton.attributes().disabled).toBeFalsy();
const switchButton = wrapper.find("*[data-description='switch to another history']");
expect(switchButton.attributes().disabled).toBeFalsy();
const dropDown = wrapper.find("*[data-description='history options']");
const optionElements = dropDown.findAll("b-dropdown-item-stub");
const optionTexts = optionElements.wrappers.map((el) => el.text());
@@ -76,8 +81,12 @@ describe("History Navigation", () => {
provide: { store },
});
const dropDown = wrapper.find("*[data-description='history options']");
const createButton = wrapper.find("*[data-description='create new history']");
expect(createButton.attributes().disabled).toBeTruthy();
const switchButton = wrapper.find("*[data-description='switch to another history']");
expect(switchButton.attributes().disabled).toBeTruthy();
const dropDown = wrapper.find("*[data-description='history options']");
const enabledOptionElements = dropDown.findAll("b-dropdown-item-stub:not([disabled])");
const enabledOptionTexts = enabledOptionElements.wrappers.map((el) => el.text());
expect(enabledOptionTexts).toStrictEqual(anonymousOptions);
@@ -12,7 +12,8 @@
data-description="create new history"
size="sm"
variant="link"
title="Create new history"
:disabled="currentUser.isAnonymous"
:title="userTitle('Create new history')"
@click="$emit('createNewHistory')">
<Icon fixed-width icon="plus" />
</b-button>
@@ -23,7 +24,8 @@
data-description="switch to another history"
size="sm"
variant="link"
title="Switch to history">
:disabled="currentUser.isAnonymous"
:title="userTitle('Switch to history')">
<Icon fixed-width icon="exchange-alt" />
</b-button>
@@ -5,7 +5,7 @@ import SelectorModal from "./SelectorModal";
const localVue = getLocalVue();
const SELECTED_HISTORY_ID = "COOL_ID";
const CURRENT_HISTORY_ID = "COOL_ID";
const getFakeHistorySummaries = (num, selectedIndex = 0) => {
const result = Array.from({ length: num }, (_, index) => ({
id: `ID-${index}`,
@@ -13,11 +13,11 @@ const getFakeHistorySummaries = (num, selectedIndex = 0) => {
tags: [],
update_time: new Date().toISOString(),
}));
result[selectedIndex].id = SELECTED_HISTORY_ID;
result[selectedIndex].id = CURRENT_HISTORY_ID;
return result;
};
const PROPS_WITH_10_HISTORIES = {
currentHistoryId: SELECTED_HISTORY_ID,
currentHistoryId: CURRENT_HISTORY_ID,
histories: getFakeHistorySummaries(10),
perPage: 3,
static: true, // Force the modal visible for testing
@@ -27,6 +27,8 @@ const PROPS_WITH_10_HISTORY_MULTIPLE_SELECT = {
multiple: true,
};
const CURRENT_HISTORY_INDICATION_TEXT = "(Current)";
describe("History SelectorModal.vue", () => {
let wrapper;
@@ -38,12 +40,11 @@ describe("History SelectorModal.vue", () => {
await flushPromises();
}
it("should highlight the currently selected history", async () => {
it("should indicate the currently selected history", async () => {
await mountWith(PROPS_WITH_10_HISTORIES);
const selectedRows = wrapper.findAll(".table-info");
expect(selectedRows.length).toBe(1);
expect(selectedRows.at(0).attributes("data-pk")).toBe(SELECTED_HISTORY_ID);
const currentHistoryRow = wrapper.find(`[data-pk="${CURRENT_HISTORY_ID}"]`);
expect(currentHistoryRow.html()).toContain(CURRENT_HISTORY_INDICATION_TEXT);
});
it("paginates the histories", async () => {
@@ -67,25 +68,29 @@ describe("History SelectorModal.vue", () => {
expect(wrapper.emitted()["selectHistory"][0][0].id).toBe(targetHistoryId);
});
it("select multiple histories", async () => {
await mountWith(PROPS_WITH_10_HISTORY_MULTIPLE_SELECT);
describe("Multi-selection Mode", () => {
it("should select multiple histories", async () => {
await mountWith(PROPS_WITH_10_HISTORY_MULTIPLE_SELECT);
expect(wrapper.emitted()["selectHistories"]).toBeUndefined();
expect(wrapper.emitted()["selectHistories"]).toBeUndefined();
const targetHistoryId1 = "ID-1";
const targetRow1 = wrapper.find(`[data-pk="${targetHistoryId1}"]`);
await targetRow1.trigger("click");
const targetHistoryId1 = "ID-1";
const targetRow1 = wrapper.find(`[data-pk="${targetHistoryId1}"]`);
await targetRow1.trigger("click");
const targetHistoryId2 = "ID-2";
const targetRow2 = wrapper.find(`[data-pk="${targetHistoryId2}"]`);
await targetRow2.trigger("click");
const targetHistoryId2 = "ID-2";
const targetRow2 = wrapper.find(`[data-pk="${targetHistoryId2}"]`);
await targetRow2.trigger("click");
expect(wrapper.vm.selectedHistories.length).toBe(2);
expect(wrapper.vm.selectedHistories.length).toBe(2);
const button = wrapper.find(".btn-primary");
const button = wrapper.find(".btn-primary");
await button.trigger("click");
await button.trigger("click");
expect(wrapper.emitted()["selectHistories"][0][0][0].id).toBe(targetHistoryId1);
expect(wrapper.emitted()["selectHistories"][0][0][0].id).toBe(targetHistoryId1);
console.debug(wrapper.html());
});
});
});
@@ -11,7 +11,7 @@
primary-key="id"
:fields="fields"
:filter="filter"
:items="formattedItems"
:items="histories"
:per-page="perPage"
:current-page="currentPage"
:selectable="true"
@@ -22,6 +22,9 @@
selected-variant="success"
@row-selected="rowSelected"
@filtered="onFiltered">
<template v-slot:cell(name)="row">
{{ row.item.name }} <i v-if="row.item.id === currentHistoryId"><b>(Current)</b></i>
</template>
<template v-slot:cell(tags)="row">
<stateless-tags :value="row.item.tags" :disabled="true" />
</template>
@@ -31,7 +34,9 @@
</b-table>
<template v-slot:modal-footer>
<b-pagination v-model="currentPage" :total-rows="totalRows" :per-page="perPage" />
<b-button v-if="multiple" variant="primary" @click="addSelected">Add Selected</b-button>
<b-button v-if="multiple" :disabled="isEmptySelection" variant="primary" @click="addSelected">
Add Selected
</b-button>
</template>
</b-modal>
</template>
@@ -69,13 +74,8 @@ export default {
};
},
computed: {
formattedItems() {
return this.histories.map((item) => {
if (item.id === this.currentHistoryId) {
item._rowVariant = "info";
}
return item;
});
isEmptySelection() {
return this.selectedHistories.length === 0;
},
},
watch: {
+7 -2
View File
@@ -1,7 +1,7 @@
<template>
<span>
<span :class="spinnerClasses"></span>
<span class="loading-message">{{ message }}.<span class="blinking">..</span></span>
<span :class="spinnerClasses" title="loading"></span>
<span v-if="!spinnerOnly" class="loading-message">{{ message }}.<span class="blinking">..</span></span>
</span>
</template>
<script>
@@ -16,6 +16,11 @@ export default {
type: String,
default: "Loading",
},
spinnerOnly: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
spinnerClasses() {
+2 -2
View File
@@ -59,9 +59,9 @@ export default {
computed: {
targetClass() {
if (this.toolKey) {
return `tool-menu-item-${this.tool[this.toolKey]} title-link`;
return `tool-menu-item-${this.tool[this.toolKey]} title-link cursor-pointer`;
} else {
return `title-link`;
return `title-link cursor-pointer`;
}
},
},
@@ -109,7 +109,7 @@ export default {
this.$emit("onResults", this.favoritesResults);
} else {
// keys with sorting order
const keys = { exact: 2, name: 1, description: 0 };
const keys = { exact: 3, name: 2, description: 1, combined: 0 };
this.$emit("onResults", searchToolsByKeys(this.toolsList, keys, q));
}
} else {
@@ -18,7 +18,7 @@
<tool-search
:current-panel-view="currentPanelView"
placeholder="search tools"
:toolbox="toolbox"
:toolbox="workflowTools"
:query="query"
@onQuery="onQuery"
@onResults="onResults" />
@@ -74,7 +74,7 @@
import _l from "utils/localization";
import ToolSection from "./Common/ToolSection";
import ToolSearch from "./Common/ToolSearch";
import { filterToolSections } from "./utilities";
import { filterToolSections, removeDisabledTools } from "./utilities";
import PanelViewButton from "./Buttons/PanelViewButton";
export default {
@@ -140,12 +140,13 @@ export default {
};
},
sections() {
return filterToolSections(this.toolsLayout, this.results);
return filterToolSections(this.workflowTools, this.results);
},
toolsLayout() {
return this.toolbox.map((section) => {
return {
...section,
disabled: !section.elems && !section.is_workflow_compatible,
elems:
section.elems &&
section.elems.map((el) => {
@@ -155,6 +156,9 @@ export default {
};
});
},
workflowTools() {
return removeDisabledTools(this.toolsLayout);
},
},
methods: {
onQuery(query) {
+22 -1
View File
@@ -89,7 +89,12 @@ export function searchToolsByKeys(tools, keys, query) {
const returnedTools = [];
for (const tool of tools) {
for (const key of Object.keys(keys)) {
const actualValue = tool[key] ? tool[key].toLowerCase() : "";
let actualValue = "";
if (key === "combined") {
actualValue = tool.name.toLowerCase() + " " + tool.description.toLowerCase();
} else {
actualValue = tool[key] ? tool[key].toLowerCase() : "";
}
const queryLowerCase = query.toLowerCase();
if (actualValue.match(queryLowerCase)) {
// do we care for exact matches && is it an exact match ?
@@ -109,6 +114,22 @@ export function normalizeTools(tools) {
return tools;
}
export function removeDisabledTools(tools) {
return tools.filter((section) => {
if (section.model_class === "ToolSectionLabel") {
return true;
} else if (!section.elems && section.disabled) {
return false;
} else if (section.elems) {
section.elems = section.elems.filter((el) => !el.disabled);
if (!section.elems.length) {
return false;
}
}
return true;
});
}
function flattenToolsSection(section) {
const flattenTools = [];
if (section.elems) {
@@ -17,7 +17,7 @@ describe("test helpers in tool searching utilities", () => {
});
it("test tool search helper that searches for tools given keys", async () => {
const q = "collection";
let q = "collection";
let expectedResults = [
"__FILTER_FAILED_DATASETS__",
"__FILTER_EMPTY_DATASETS__",
@@ -37,6 +37,12 @@ describe("test helpers in tool searching utilities", () => {
keys = { description: 0, name: 1 };
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
expect(results).toEqual(expectedResults);
q = "filter empty datasets";
expectedResults = ["__FILTER_EMPTY_DATASETS__"];
keys = { description: 1, name: 2, combined: 0 };
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
expect(results).toEqual(expectedResults);
});
it("test tool filtering helpers on toolsList given list of ids", async () => {
@@ -11,9 +11,7 @@ import { createTestingPinia } from "@pinia/testing";
const localVue = getLocalVue();
describe("FormTool", () => {
let wrapper;
beforeEach(() => {
function mountTarget() {
const store = new Vuex.Store({
modules: {
user: mockModule(userStore),
@@ -21,7 +19,7 @@ describe("FormTool", () => {
},
});
wrapper = mount(FormTool, {
return mount(FormTool, {
propsData: {
id: "input",
datatypes: [],
@@ -32,7 +30,7 @@ describe("FormTool", () => {
name: "tool_name",
version: "1.0",
description: "description",
inputs: [],
inputs: [{ name: "input", label: "input", type: "text", value: "value" }],
help: "help_text",
versions: ["1.0", "2.0", "3.0"],
citations: false,
@@ -46,15 +44,16 @@ describe("FormTool", () => {
stubs: {
CurrentUser: MockCurrentUser({ id: "fakeuser" }),
ConfigProvider: MockConfigProvider({ id: "fakeconfig" }),
FormElement: { template: "<div>form-element</div>" },
ToolFooter: { template: "<div>tool-footer</div>" },
},
pinia: createTestingPinia(),
provide: { store },
});
});
}
it("changes between different versions", async () => {
const wrapper = mountTarget();
const dropdowns = wrapper.findAll(".tool-versions .dropdown-item");
let version = dropdowns.at(1);
expect(version.text()).toBe("Switch to 2.0");
@@ -163,9 +163,16 @@ export default {
onLabel(newLabel) {
this.$emit("onLabel", this.stepId, newLabel);
},
/**
* Change event is triggered on component creation and input changes.
* @param { Object } values contains flat key-value pairs `prefixed-name=value`
*/
onChange(values) {
const initialRequest = Object.keys(this.mainValues).length === 0;
this.mainValues = values;
this.postChanges();
if (!initialRequest) {
this.postChanges();
}
},
onChangePostJobActions(postJobActions) {
this.$emit("onChangePostJobActions", this.stepId, postJobActions);
@@ -108,6 +108,7 @@
<div>
<FormTool
v-if="hasActiveNodeTool"
:key="activeStep.id"
:step="activeStep"
:datatypes="datatypes"
@onChangePostJobActions="onChangePostJobActions"
@@ -253,6 +254,10 @@ export default {
const hasChanges = ref(false);
const hasInvalidConnections = computed(() => Object.keys(connectionsStore.invalidConnections).length > 0);
stepStore.$subscribe((mutation, state) => {
hasChanges.value = true;
});
function resetStores() {
connectionsStore.$reset();
stepStore.$reset();
@@ -341,9 +346,6 @@ export default {
this.hasChanges = true;
}
},
steps(newSteps, oldSteps) {
this.hasChanges = true;
},
hasChanges() {
this.$emit("update:confirmation", this.hasChanges);
},
@@ -356,7 +358,6 @@ export default {
methods: {
onUpdateStep(step) {
this.stepStore.updateStep(step);
this.hasChanges = true;
},
onUpdateStepPosition(stepId, position) {
const step = { ...this.steps[stepId], position };
@@ -408,6 +409,7 @@ export default {
hide_modal(); // hide other modals created in utilities also...
},
async onRefactor(response) {
this.resetStores();
await fromSimple(response.workflow);
this._loadEditorData(response.workflow);
},
@@ -11,8 +11,8 @@
@move="onMoveTo"
@pan-by="onPanBy">
<div class="node-header unselectable clearfix" @click="makeActive" @keyup.enter="makeActive">
<loading-span v-if="isLoading" message="Loading details" />
<b-button-group class="float-right">
<loading-span v-if="isLoading" spinner-only />
<b-button
v-if="canClone"
v-b-tooltip.hover
@@ -3,7 +3,7 @@ import { ref } from "vue";
export function useUniqueLabelError(
workflowStateStore: ReturnType<typeof useWorkflowStepStore>,
label: string | undefined
label: string | null | undefined
) {
const error = ref("");
if (label && workflowStateStore.workflowOutputs[label]) {
@@ -1,6 +1,6 @@
import { setActivePinia, createPinia } from "pinia";
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
import { useWorkflowStepStore, type TerminalSource } from "@/stores/workflowStepStore";
import {
terminalFactory,
InputCollectionTerminal,
@@ -15,7 +15,11 @@ import {
import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures";
import { useConnectionStore } from "@/stores/workflowConnectionStore";
import type { DataOutput, Steps } from "@/stores/workflowStepStore";
import { NULL_COLLECTION_TYPE_DESCRIPTION } from "./collectionTypeDescription";
import {
ANY_COLLECTION_TYPE_DESCRIPTION,
CollectionTypeDescription,
NULL_COLLECTION_TYPE_DESCRIPTION,
} from "./collectionTypeDescription";
import { simpleSteps, advancedSteps } from "../test_fixtures";
function setupAdvanced() {
@@ -35,6 +39,17 @@ function setupAdvanced() {
return terminals;
}
function rebuildTerminal<T extends ReturnType<typeof terminalFactory>>(terminal: T): T {
let terminalSource: TerminalSource;
const step = terminal.stepStore.getStep(terminal.stepId);
if (terminal.terminalType === "input") {
terminalSource = step.inputs.find((input) => input.name == terminal.name)!;
} else {
terminalSource = step.outputs.find((output) => output.name == terminal.name)!;
}
return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper) as T;
}
describe("terminalFactory", () => {
let terminals: { [index: string]: { [index: string]: ReturnType<typeof terminalFactory> } } = {};
beforeEach(() => {
@@ -64,6 +79,8 @@ describe("terminalFactory", () => {
expect(terminals["any collection"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
expect(terminals["multi data"]["advanced|advanced_threshold"]).toBeInstanceOf(InputParameterTerminal);
expect(terminals["list collection input"]["input1"]).toBeInstanceOf(InputCollectionTerminal);
expect(terminals["filter_failed"]["input"]).toBeInstanceOf(InputCollectionTerminal);
expect(terminals["filter_failed"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
});
it("throws error on invalid terminalSource", () => {
const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper);
@@ -257,7 +274,7 @@ describe("canAccept", () => {
"Can't map over this input with output collection type - this step has outputs defined constraining the mapping of this tool. Disconnect outputs and retry."
);
});
it("rejects attaching non-collection outputs to mapper over inputs", () => {
it("rejects attaching non-collection outputs to mapped-over inputs", () => {
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
const simpleDataOut = terminals["data input"]["output"] as OutputTerminal;
const dataIn = terminals["simple data"]["input"] as InputTerminal;
@@ -281,11 +298,7 @@ describe("canAccept", () => {
dataInTwo.disconnect(dataOut);
// terminal isn't a reactive class in any way (worth a thought doing, but difficult!), the following happens when a new terminal is built
// in useTerminal.ts
const rebuiltDataIn = terminalFactory(
dataIn.stepId,
advancedSteps[dataIn.stepId].inputs[0],
testDatatypesMapper
);
const rebuiltDataIn = rebuildTerminal(dataIn);
expect(rebuiltDataIn.localMapOver.isCollection).toBe(false);
expect(rebuiltDataIn.canAccept(simpleDataOut).canAccept).toBe(true);
});
@@ -389,7 +402,9 @@ describe("canAccept", () => {
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
const collectionIn = terminals["list collection input"]["input1"] as InputCollectionTerminal;
expect(collectionIn.canAccept(collectionOut).canAccept).toBe(true);
expect(collectionIn.isMappedOver()).toBe(false);
collectionIn.connect(collectionOut);
expect(collectionIn.isMappedOver()).toBe(true);
expect(collectionIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
const intermediateOut = terminals["list collection input"]["out_file1"] as OutputCollectionTerminal;
const otherListIn = terminals["list collection input 2"]["input1"] as InputCollectionTerminal;
@@ -459,6 +474,20 @@ describe("canAccept", () => {
"Effective output data type(s) [tabular] do not appear to match input type(s) [ab1]."
);
});
it("resolves collection type source", () => {
const filterFailedInput = terminals["filter_failed"]["input"] as InputCollectionTerminal;
const filterFailedOutput = terminals["filter_failed"]["output"] as OutputCollectionTerminal;
expect(filterFailedOutput.collectionType).toBe(ANY_COLLECTION_TYPE_DESCRIPTION);
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
filterFailedInput.connect(collectionOut);
expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(new CollectionTypeDescription("list"));
filterFailedInput.disconnect(collectionOut);
const listPairedOutput = terminals["list:paired input"]["output"] as OutputCollectionTerminal;
filterFailedInput.connect(listPairedOutput);
expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(
new CollectionTypeDescription("list:paired")
);
});
});
describe("Input terminal", () => {
@@ -97,6 +97,7 @@ class Terminal extends EventEmitter {
this.resetMappingIfNeeded(connection);
}
setMapOver(val: CollectionTypeDescriptor) {
// we use this method to determine the map over status based on all terminals connected to this step
let outputVal = val;
if (this.multiple) {
// emulate list input
@@ -112,7 +113,11 @@ class Terminal extends EventEmitter {
this.stepStore.changeStepInputMapOver(this.stepId, this.name, effectiveMapOver);
this.localMapOver = effectiveMapOver;
}
if (!this.mapOver.equal(effectiveMapOver)) {
if (
!this.mapOver.equal(effectiveMapOver) &&
(effectiveMapOver.isCollection ||
!Object.values(this.stepStore.stepInputMapOver[this.stepId]).find((mapOver) => mapOver.isCollection))
) {
this.stepStore.changeStepMapOver(this.stepId, effectiveMapOver);
}
}
@@ -242,6 +247,11 @@ class BaseInputTerminal extends Terminal {
}
return null;
}
isMappedOver(): boolean {
return Boolean(this.localMapOver.isCollection);
}
connected() {
return Boolean(this._getFirstOutputTerminal());
}
@@ -400,10 +410,6 @@ export class InputTerminal extends BaseInputTerminal {
this.getStepMapOver();
}
isMappedOver(): boolean {
return Boolean(this.localMapOver.isCollection);
}
attachable(other: BaseOutputTerminal) {
const otherCollectionType = this._otherCollectionType(other);
const mapOver = this.mapOver;
@@ -490,10 +496,10 @@ export class InputParameterTerminal extends BaseInputTerminal {
const effectiveThisType = this.effectiveType(this.type);
const otherType = ("type" in other && other.type) || "data";
const effectiveOtherType = this.effectiveType(otherType);
const canAccept = effectiveThisType === effectiveOtherType
const canAccept = effectiveThisType === effectiveOtherType;
return new ConnectionAcceptable(
canAccept,
canAccept ? null: `Cannot attach a ${effectiveOtherType} parameter to a ${effectiveThisType} input`
canAccept ? null : `Cannot attach a ${effectiveOtherType} parameter to a ${effectiveThisType} input`
);
}
}
@@ -672,19 +678,56 @@ interface OutputCollectionTerminalArgs extends BaseOutputTerminalArgs {
}
export class OutputCollectionTerminal extends BaseOutputTerminal {
collectionTypeSource: string | null;
constructor(attr: OutputCollectionTerminalArgs) {
super(attr);
this.collectionTypeSource = null;
if (attr.collection_type) {
this.collectionType = new CollectionTypeDescription(attr.collection_type);
} else {
const collectionTypeSource = attr.collection_type_source;
if (!collectionTypeSource) {
this.collectionTypeSource = attr.collection_type_source;
if (!this.collectionTypeSource) {
console.log("Warning: No collection type or collection type source defined.");
}
this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
this.collectionType = this.getCollectionTypeFromInput() || ANY_COLLECTION_TYPE_DESCRIPTION;
}
this.isCollection = true;
}
getCollectionTypeFromInput() {
const connection = this.connectionStore.connections.find(
(connection) =>
connection.input.name === this.collectionTypeSource && connection.input.stepId === this.stepId
);
if (connection) {
const outputStep = this.stepStore.getStep(connection.output.stepId);
const inputStep = this.stepStore.getStep(this.stepId);
if (outputStep) {
const stepOutput = outputStep.outputs.find((output) => output.name == connection.output.name);
const stepInput = inputStep.inputs.find((input) => input.name === this.collectionTypeSource);
if (stepInput && stepOutput) {
const outputTerminal = terminalFactory(connection.output.stepId, stepOutput, this.datatypesMapper);
const inputTerminal = terminalFactory(connection.output.stepId, stepInput, this.datatypesMapper);
// otherCollectionType is the mapped over output collection as it would appear at the input terminal
const otherCollectionType = inputTerminal._otherCollectionType(outputTerminal);
// we need to find which of the possible input collection types is connected
if ("collectionTypes" in inputTerminal) {
// collection_type_source must point at input collection terminal
const connectedCollectionType = inputTerminal.collectionTypes.find(
(collectionType) =>
otherCollectionType.canMatch(collectionType) ||
otherCollectionType.canMapOver(collectionType)
);
if (connectedCollectionType) {
return connectedCollectionType;
}
}
}
}
}
return ANY_COLLECTION_TYPE_DESCRIPTION;
}
}
interface OutputParameterTerminalArgs extends Omit<BaseOutputTerminalArgs, "datatypes"> {
@@ -812,5 +812,99 @@
"left": 70.45245361328125,
"top": 316.3525695800781
}
},
"17": {
"id": 17,
"type": "tool",
"label": "filter_failed",
"content_id": "__FILTER_FAILED_DATASETS__",
"name": "Filter failed datasets",
"tool_state": {
"input": "{\"__class__\": \"RuntimeValue\"}",
"__page__": null,
"__rerun_remap_job_id__": null
},
"errors": null,
"inputs": [
{
"name": "input",
"label": "Input Collection",
"multiple": false,
"input_type": "dataset_collection",
"collection_types": [
"list",
"list:paired"
],
"optional": false,
"extensions": [
"data"
]
}
],
"outputs": [
{
"name": "output",
"extensions": [
"input"
],
"type": "collection",
"optional": false,
"collection": true,
"collection_type": null,
"collection_type_source": "input",
"label": "input dataset(s) (filtered failed datasets)"
}
],
"annotation": "",
"post_job_actions": {},
"uuid": "f5de18b9-b08e-4160-b71b-54768a4e5208",
"when": null,
"workflow_outputs": [
{
"output_name": "output",
"uuid": "3774e554-b7d1-41ba-9acd-ee82ba9fbf54",
"label": null
}
],
"input_connections": {},
"position": {
"left": 0,
"top": 0
}
},
"18": {
"id": 18,
"type": "data_collection_input",
"label": "list:paired input",
"content_id": null,
"name": "Input dataset collection",
"tool_state": {
"collection_type": "\"list:paired\"",
"optional": "\"false\"",
"tag": "\"\"",
"__page__": null,
"__rerun_remap_job_id__": null
},
"errors": null,
"inputs": [],
"outputs": [
{
"name": "output",
"extensions": [
"input"
],
"collection": true,
"collection_type": "list:paired",
"optional": false
}
],
"annotation": "",
"post_job_actions": {},
"workflow_outputs": [],
"input_connections": {},
"position": {
"left": 12.8729248046875,
"top": 501.7839660644531
}
}
}
@@ -34,6 +34,7 @@ export interface BaseTerminal {
export interface InputTerminal extends BaseTerminal {
connectorType: "input";
input_subworkflow_step_id?: number;
}
export interface OutputTerminal extends BaseTerminal {
+21 -7
View File
@@ -63,6 +63,7 @@ interface BaseStepInput {
extensions: string[];
optional: boolean;
input_type: string;
input_subworkflow_step_id?: number;
}
export interface DataStepInput extends BaseStepInput {
@@ -85,8 +86,8 @@ export type TerminalSource = InputTerminalSource | OutputTerminalSource;
interface WorkflowOutput {
output_name: string;
label?: string;
uuid?: string;
label?: string | null;
uuid?: string | null;
}
export interface NewStep {
@@ -97,7 +98,7 @@ export interface NewStep {
errors?: string[] | null;
input_connections: StepInputConnection;
inputs: Array<InputTerminalSource>;
label?: string;
label?: string | null;
name: string;
outputs: Array<OutputTerminalSource>;
position?: StepPosition;
@@ -106,7 +107,7 @@ export interface NewStep {
tooltip?: string;
type: "tool" | "data_input" | "data_collection_input" | "subworkflow" | "parameter_input" | "pause";
uuid?: string;
when?: string;
when?: string | null;
workflow_outputs?: WorkflowOutput[];
}
@@ -125,6 +126,7 @@ export interface StepInputConnection {
export interface ConnectionOutputLink {
output_name: string;
id: number;
input_subworkflow_step_id?: number;
}
interface WorkflowOutputs {
@@ -231,11 +233,19 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
},
addConnection(connection: Connection) {
const inputStep = this.getStep(connection.input.stepId);
const input = inputStep.inputs.find((input) => input.name === connection.input.name);
const connectionLink: ConnectionOutputLink = {
output_name: connection.output.name,
id: connection.output.stepId,
};
if (input && "input_subworkflow_step_id" in input && input.input_subworkflow_step_id !== undefined) {
connectionLink["input_subworkflow_step_id"] = input.input_subworkflow_step_id;
}
const updatedStep = {
...inputStep,
input_connections: {
...inputStep.input_connections,
[connection.input.name]: { output_name: connection.output.name, id: connection.output.stepId },
[connection.input.name]: connectionLink,
},
};
this.updateStep(updatedStep);
@@ -258,7 +268,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
export function stepToConnections(step: Step): Connection[] {
const connections: Connection[] = [];
if (step.input_connections) {
Object.entries(step?.input_connections).forEach(([input_name, outputArray]) => {
Object.entries(step?.input_connections).forEach(([inputName, outputArray]) => {
if (outputArray === undefined) {
return;
}
@@ -269,7 +279,7 @@ export function stepToConnections(step: Step): Connection[] {
const connection = new Connection(
{
stepId: step.id,
name: input_name,
name: inputName,
connectorType: "input",
},
{
@@ -278,6 +288,10 @@ export function stepToConnections(step: Step): Connection[] {
connectorType: "output",
}
);
const connectionInput = step.inputs.find((input) => input.name == inputName);
if (connectionInput && "input_subworkflow_step_id" in connectionInput) {
connection.input.input_subworkflow_step_id = connectionInput.input_subworkflow_step_id;
}
connections.push(connection);
});
});
+5 -6
View File
@@ -6,15 +6,14 @@ tags:
- "UI"
- "visualization"
requirements:
- new_history
steps:
- content: "This short tour will walk you through <b>Galaxy's Window Manager</b> feature"
- element: "#current-history-panel .create-hist-btn"
intro: "Before using the Window Manager, we will create a new history for this tour."
postclick: true
- element: "#tool-panel-upload-button"
intro: "Now, we will upload some tabular data into our new history."
intro: "First, we will upload some tabular data into our new history."
postclick: true
- element: "#btn-new"
@@ -78,7 +77,7 @@ steps:
- element: "#right"
intro: "This is your history. It contains our two tables being uploaded."
- component: history_panel.item(hid=1,state=ok).display_button
intro: "Clicking the eye-icon usually displays a dataset in the center panel."
postclick: true
+14 -2
View File
@@ -7163,8 +7163,20 @@ class WorkflowStep(Base, RepresentById):
conn.output_name = output_name
add_object_to_object_session(conn, output_step)
conn.output_step = output_step
if input_subworkflow_step_index is not None:
input_subworkflow_step = self.subworkflow.step_by_index(input_subworkflow_step_index)
if self.subworkflow:
if input_subworkflow_step_index is not None:
input_subworkflow_step = self.subworkflow.step_by_index(input_subworkflow_step_index)
else:
input_subworkflow_steps = [step for step in self.subworkflow.input_steps if step.label == input_name]
if not input_subworkflow_steps:
inferred_order_index = input_name.split(":", 1)[0]
if inferred_order_index.isdigit():
input_subworkflow_steps = [self.subworkflow.step_by_index(int(inferred_order_index))]
if len(input_subworkflow_steps) != 1:
raise galaxy.exceptions.MessageException(
f"Invalid subworkflow connection at step index {self.order_index + 1}"
)
input_subworkflow_step = input_subworkflow_steps[0]
conn.input_subworkflow_step = input_subworkflow_step
return conn
+20 -47
View File
@@ -4,12 +4,10 @@ adapt cwltool to Galaxy features and abstract the library away from the rest
of the framework.
"""
import base64
import copy
import json
import logging
import os
import pickle
from abc import (
ABCMeta,
abstractmethod,
@@ -31,7 +29,6 @@ from galaxy.exceptions import MessageException
from galaxy.util import (
listify,
safe_makedirs,
unicodify,
)
from galaxy.util.bunch import Bunch
from .cwltool_deps import (
@@ -86,8 +83,6 @@ SUPPORTED_TOOL_REQUIREMENTS = [
SUPPORTED_WORKFLOW_REQUIREMENTS = SUPPORTED_TOOL_REQUIREMENTS + []
PERSISTED_REPRESENTATION = "cwl_tool_object"
ToolStateType = Dict[str, Union[None, str, bool, Dict[str, str]]]
@@ -173,29 +168,29 @@ class ToolProxy(metaclass=ABCMeta):
def to_persistent_representation(self):
"""Return a JSON representation of this tool. Not for serialization
over the wire, but serialization in a database."""
# TODO: Replace this with some more readable serialization,
# I really don't like using pickle here.
if PERSISTED_REPRESENTATION == "cwl_tool_object":
persisted_obj = remove_pickle_problems(self._tool)
else:
persisted_obj = self._raw_process_reference
persisted_obj = self._tool.tool
persisted_obj["requirements"] = self.requirements
if not persisted_obj.get("cwlVersion"):
# This happens for any inline process, but getting it from metadata is correct for inline processes at least
persisted_obj["cwlVersion"] = self._tool.metadata["cwlVersion"]
return {
"class": self._class,
"pickle": unicodify(base64.b64encode(pickle.dumps(persisted_obj, pickle.HIGHEST_PROTOCOL))),
# Should maybe be yaml instead
"raw_process_reference": persisted_obj,
"uuid": self.uuid,
}
@staticmethod
def from_persistent_representation(as_object) -> "ToolProxy":
def from_persistent_representation(as_object, strict_cwl_validation=True, tool_directory=None) -> "ToolProxy":
"""Recover an object serialized with to_persistent_representation."""
if "class" not in as_object:
raise Exception("Failed to deserialize tool proxy from JSON object - no class found.")
if "pickle" not in as_object:
raise Exception("Failed to deserialize tool proxy from JSON object - no pickle representation found.")
if "uuid" not in as_object:
raise Exception("Failed to deserialize tool proxy from JSON object - no uuid found.")
to_unpickle = base64.b64decode(as_object["pickle"])
loaded_object = pickle.loads(to_unpickle)
loaded_object = tool_proxy(
tool_object=as_object["raw_process_reference"],
strict_cwl_validation=strict_cwl_validation,
tool_directory=tool_directory,
uuid=as_object.get("uuid"),
)
return loaded_object
@property
@@ -743,14 +738,8 @@ def tool_proxy_from_persistent_representation(
) -> ToolProxy:
"""Load a ToolProxy from a previously persisted representation."""
ensure_cwltool_available()
if PERSISTED_REPRESENTATION == "cwl_tool_object":
kwds = {"cwl_tool_object": ToolProxy.from_persistent_representation(persisted_tool)}
else:
raw_process_reference = persisted_tool # ???
kwds = {"raw_process_reference": ToolProxy.from_persistent_representation(raw_process_reference)}
uuid = persisted_tool["uuid"]
return _to_cwl_tool_object(
uuid=uuid, strict_cwl_validation=strict_cwl_validation, tool_directory=tool_directory, **kwds
return ToolProxy.from_persistent_representation(
persisted_tool, strict_cwl_validation=strict_cwl_validation, tool_directory=tool_directory
)
@@ -765,15 +754,10 @@ def load_job_proxy(job_directory, strict_cwl_validation=True) -> JobProxy:
job_objects = json.load(open(job_objects_path))
job_inputs = job_objects["job_inputs"]
output_dict = job_objects["output_dict"]
# Any reason to retain older tool_path variant of this? Probably not?
if "tool_path" in job_objects:
tool_path = job_objects["tool_path"]
cwl_tool = tool_proxy(tool_path, strict_cwl_validation=strict_cwl_validation)
else:
persisted_tool = job_objects["tool_representation"]
cwl_tool = tool_proxy_from_persistent_representation(
persisted_tool=persisted_tool, strict_cwl_validation=strict_cwl_validation
)
persisted_tool = job_objects["tool_representation"]
cwl_tool = tool_proxy_from_persistent_representation(
persisted_tool=persisted_tool, strict_cwl_validation=strict_cwl_validation
)
return cwl_tool.job_proxy(job_inputs, output_dict, job_directory=job_directory)
@@ -1107,17 +1091,6 @@ class SubworkflowStepProxy(BaseStepProxy):
return self._subworkflow_proxy
def remove_pickle_problems(obj):
"""doc_loader does not pickle correctly"""
if hasattr(obj, "doc_loader"):
obj.doc_loader = None
if hasattr(obj, "embedded_tool"):
obj.embedded_tool = remove_pickle_problems(obj.embedded_tool)
if hasattr(obj, "steps"):
obj.steps = [remove_pickle_problems(s) for s in obj.steps]
return obj
def _outer_field_to_input_instance(field):
field_type = field_to_field_type(field) # Must be a list if in here?
if not isinstance(field_type, list):
@@ -291,6 +291,10 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE
if data.missing_meta():
message = 'Required metadata values are missing. Some of these values may not be editable by the user. Selecting "Auto-detect" will attempt to fix these values.'
status = "warning"
metadata_disable = data.state not in [
trans.model.Dataset.states.OK,
trans.model.Dataset.states.FAILED_METADATA,
]
# datatype conversion
conversion_options = [
(f"{convert_id} (using '{convert_name}')", convert_id)
@@ -369,6 +373,7 @@ class DatasetInterface(BaseUIController, UsesAnnotations, UsesItemRatings, UsesE
"message": message,
"status": status,
"dataset_id": dataset_id,
"metadata_disable": metadata_disable,
"attribute_inputs": attribute_inputs,
"conversion_inputs": conversion_inputs,
"conversion_disable": conversion_disable,
+3
View File
@@ -2051,6 +2051,8 @@ steps:
inputs:
some_file:
type: data
should_run:
type: boolean
steps:
a_tool_step:
tool_id: cat1
@@ -2154,6 +2156,7 @@ steps:
class: GalaxyWorkflow
inputs:
boolean_input_file: data
should_run: boolean
steps:
consume_expression_parameter:
tool_id: cat1
@@ -1012,6 +1012,7 @@ steps:
class: GalaxyWorkflow
inputs:
boolean_input_file: data
should_run: boolean
steps:
create_more_inputs:
tool_id: collection_creates_dynamic_nested
@@ -697,7 +697,10 @@ steps:
workflow_populator.upload_yaml_workflow(WORKFLOW_OPTIONAL_TRUE_INPUT_COLLECTION, name=child_workflow_name)
parent_workflow_id = workflow_populator.upload_yaml_workflow(
"""class: GalaxyWorkflow
inputs: []
inputs:
input_collection:
type: collection
collection_type: "list"
steps:
- tool_id: multiple_versions
tool_version: 0.1
@@ -716,10 +719,16 @@ steps:
self.sleep_for(self.wait_types.UX_RENDER)
self.assert_workflow_has_changes_and_save()
workflow = self.workflow_populator.download_workflow(parent_workflow_id)
subworkflow_step = workflow["steps"]["1"]
subworkflow_step = workflow["steps"]["2"]
assert subworkflow_step["name"] == child_workflow_name
assert subworkflow_step["type"] == "subworkflow"
assert subworkflow_step["subworkflow"]["a_galaxy_workflow"] == "true"
self.workflow_editor_connect("input_collection#output", f"{child_workflow_name}#input1")
self.assert_connected("input_collection#output", f"{child_workflow_name}#input1")
self.assert_workflow_has_changes_and_save()
workflow = self.workflow_populator.download_workflow(parent_workflow_id)
subworkflow_step = workflow["steps"]["2"]
assert subworkflow_step["input_connections"]["input1"]["input_subworkflow_step_id"] == 0
@selenium_test
def test_editor_insert_steps(self):