Skip to content

Commit

Permalink
Merge branch 'release_23.1' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Nov 20, 2023
2 parents 1666892 + 4659546 commit f655c83
Show file tree
Hide file tree
Showing 19 changed files with 249 additions and 31 deletions.
5 changes: 5 additions & 0 deletions client/src/components/Common/ButtonSpinner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
variant="primary"
class="d-flex flex-nowrap align-items-center text-nowrap"
:title="tooltip"
:disabled="disabled"
@click="$emit('onClick')">
<FontAwesomeIcon icon="play" class="mr-2" />{{ title }}
</b-button>
Expand Down Expand Up @@ -43,6 +44,10 @@ export default {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
6 changes: 5 additions & 1 deletion client/src/components/Form/FormDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export default {
type: Boolean,
default: false,
},
allowEmptyValueOnRequiredInput: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand All @@ -96,7 +100,7 @@ export default {
},
computed: {
validation() {
return validateInputs(this.formIndex, this.formData);
return validateInputs(this.formIndex, this.formData, this.allowEmptyValueOnRequiredInput);
},
},
watch: {
Expand Down
8 changes: 5 additions & 3 deletions client/src/components/Form/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function matchInputs(index, response) {
* @param{dict} index - Index of input elements
* @param{dict} values - Dictionary of parameter values
*/
export function validateInputs(index, values) {
export function validateInputs(index, values, allowEmptyValueOnRequiredInput = false) {
let batchN = -1;
let batchSrc = null;
for (const inputId in values) {
Expand All @@ -113,8 +113,10 @@ export function validateInputs(index, values) {
if (!inputDef || inputDef.step_linked) {
continue;
}
if (inputValue == null && !inputDef.optional && inputDef.type != "hidden") {
return [inputId, "Please provide a value for this option."];
if (!inputDef.optional && inputDef.type != "hidden") {
if (inputValue == null || (allowEmptyValueOnRequiredInput && inputValue === "")) {
return [inputId, "Please provide a value for this option."];
}
}
if (inputDef.wp_linked && inputDef.text_value == inputValue) {
return [inputId, "Please provide a value for this workflow parameter."];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import BroadcastsOverlay from "./BroadcastsOverlay.vue";
const localVue = getLocalVue(true);

const now = new Date();
const inTwoMonths = new Date(now.setMonth(now.getMonth() + 2));
const inTwoMonths = new Date(new Date(now).setMonth(now.getMonth() + 2));

/** API date-time does not have timezone indicator and it's always UTC. */
function toApiDate(date: Date): string {
return date.toISOString().replace("Z", "");
}

let idCounter = 0;

Expand All @@ -20,10 +25,10 @@ function generateBroadcastNotification(overwrites: Partial<BroadcastNotification

return {
id: id,
create_time: now.toISOString(),
update_time: now.toISOString(),
publication_time: now.toISOString(),
expiration_time: inTwoMonths.toISOString(),
create_time: toApiDate(now),
update_time: toApiDate(now),
publication_time: toApiDate(now),
expiration_time: toApiDate(inTwoMonths),
source: "testing",
variant: "info",
content: {
Expand Down Expand Up @@ -137,4 +142,23 @@ describe("BroadcastsOverlay.vue", () => {
await leftButton.trigger("click");
expect(wrapper.find(messageCssSelector).text()).toContain("Test message 2");
});

it("should not render the broadcast when it has expired", async () => {
const expiredBroadcast = generateBroadcastNotification({ id: "expired", expiration_time: toApiDate(now) });
const wrapper = await mountBroadcastsOverlayWith([expiredBroadcast]);

expect(wrapper.exists()).toBe(true);
expect(wrapper.html()).toBe("");
});

it("should not render the broadcast when it has not been published yet", async () => {
const unpublishedBroadcast = generateBroadcastNotification({
id: "unpublished",
publication_time: toApiDate(inTwoMonths),
});
const wrapper = await mountBroadcastsOverlayWith([unpublishedBroadcast]);

expect(wrapper.exists()).toBe(true);
expect(wrapper.html()).toBe("");
});
});
43 changes: 34 additions & 9 deletions client/src/components/Workflow/Run/WorkflowRunFormSimple.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<template>
<div>
<div class="h4 clearfix mb-3">
<div v-if="isConfigLoaded" class="h4 clearfix mb-3">
<b>Workflow: {{ model.name }}</b>
<ButtonSpinner id="run-workflow" class="float-right" title="Run Workflow" @onClick="onExecute" />
<ButtonSpinner
id="run-workflow"
:wait="waitingForRequest"
:disabled="hasValidationErrors"
class="float-right"
title="Run Workflow"
@onClick="onExecute" />
<b-dropdown
v-if="showRuntimeSettings(currentUser)"
id="dropdown-form"
Expand Down Expand Up @@ -39,7 +45,11 @@
</b-dropdown-form>
</b-dropdown>
</div>
<FormDisplay :inputs="formInputs" @onChange="onChange" />
<FormDisplay
:inputs="formInputs"
:allow-empty-value-on-required-input="true"
@onChange="onChange"
@onValidation="onValidation" />
<!-- Options to default one way or the other, disable if admins want, etc.. -->
<a href="#" class="workflow-expand-form-link" @click="$emit('showAdvanced')">Expand to full workflow form.</a>
</div>
Expand All @@ -50,8 +60,9 @@ import ButtonSpinner from "components/Common/ButtonSpinner";
import FormDisplay from "components/Form/FormDisplay";
import { allowCachedJobs } from "components/Tool/utilities";
import { isWorkflowInput } from "components/Workflow/constants";
import { mapState } from "pinia";
import { storeToRefs } from "pinia";
import { errorMessageAsString } from "utils/simple-error";
import Vue from "vue";
import { useConfig } from "@/composables/config";
import { useUserStore } from "@/stores/userStore";
Expand Down Expand Up @@ -81,22 +92,24 @@ export default {
},
setup() {
const { config, isConfigLoaded } = useConfig(true);
return { config, isConfigLoaded };
const { currentUser } = storeToRefs(useUserStore());
return { config, isConfigLoaded, currentUser };
},
data() {
const newHistory = this.targetHistory == "new" || this.targetHistory == "prefer_new";
return {
formData: {},
inputTypes: {},
stepValidations: {},
sendToNewHistory: newHistory,
useCachedJobs: this.useJobCache, // TODO:
splitObjectStore: false,
preferredObjectStoreId: null,
preferredIntermediateObjectStoreId: null,
waitingForRequest: false,
};
},
computed: {
...mapState(useUserStore, ["currentUser"]),
formInputs() {
const inputs = [];
// Add workflow parameters.
Expand Down Expand Up @@ -127,13 +140,23 @@ export default {
});
return inputs;
},
hasValidationErrors() {
return Boolean(Object.values(this.stepValidations).find((value) => value !== null && value !== undefined));
},
},
methods: {
onValidation(validation) {
if (validation) {
Vue.set(this.stepValidations, validation[0], validation[1]);
} else {
this.stepValidations = {};
}
},
reuseAllowed(user) {
return allowCachedJobs(user.preferences);
return user && allowCachedJobs(user.preferences);
},
showRuntimeSettings(user) {
return this.targetHistory.indexOf("prefer") >= 0 || this.reuseAllowed(user);
return this.targetHistory.indexOf("prefer") >= 0 || (user && this.reuseAllowed(user));
},
onChange(data) {
this.formData = data;
Expand Down Expand Up @@ -182,13 +205,15 @@ export default {
data.preferred_object_store_id = this.preferredObjectStoreId;
}
}
this.waitingForRequest = true;
invokeWorkflow(this.model.workflowId, data)
.then((invocations) => {
this.$emit("submissionSuccess", invocations);
})
.catch((error) => {
this.$emit("submissionError", errorMessageAsString(error));
});
})
.finally(() => (this.waitingForRequest = false));
},
},
};
Expand Down
16 changes: 15 additions & 1 deletion client/src/stores/broadcastsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const useBroadcastsStore = defineStore("broadcastsStore", () => {
const dismissedBroadcasts = useUserLocalStorage<{ [key: string]: Expirable }>("dismissed-broadcasts", {});

const activeBroadcasts = computed(() => {
return broadcasts.value.filter((b) => !dismissedBroadcasts.value[b.id]);
return broadcasts.value.filter(isActive);
});

async function loadBroadcasts() {
Expand Down Expand Up @@ -49,6 +49,20 @@ export const useBroadcastsStore = defineStore("broadcastsStore", () => {
return now > expirationTime;
}

function isActive(broadcast: BroadcastNotification) {
return (
!dismissedBroadcasts.value[broadcast.id] &&
!hasExpired(broadcast.expiration_time) &&
hasBeenPublished(broadcast)
);
}

function hasBeenPublished(broadcast: BroadcastNotification) {
const publicationTime = new Date(`${broadcast.publication_time}Z`);
const now = new Date();
return now >= publicationTime;
}

function clearExpiredDismissedBroadcasts() {
for (const key in dismissedBroadcasts.value) {
if (hasExpired(dismissedBroadcasts.value[key]?.expiration_time)) {
Expand Down
4 changes: 4 additions & 0 deletions client/src/utils/navigation/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -653,12 +653,16 @@ workflow_run:
# TODO: put step labels in the DOM ideally
subworkflow_step_icon: ".portlet-title-icon.fa-sitemap"
run_workflow: "#run-workflow"
run_workflow_disabled: "#run-workflow.disabled"
validation_error: ".validation-error"
expand_form_link: '.workflow-expand-form-link'
expanded_form: '.workflow-expanded-form'
new_history_target_link: '.workflow-new-history-target-link'
runtime_setting_button: '.workflow-run-settings'
runtime_setting_target: '.workflow-run-settings-target'
simplified_input:
type: xpath
selector: //div[@data-label="${label}"]//input
input_select_field:
type: xpath
selector: '//div[@data-label="${label}"]//div[contains(@class, "multiselect")]'
Expand Down
8 changes: 7 additions & 1 deletion lib/galaxy/tool_util/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ def add_entries(
self.add_entry(
entry, allow_duplicates=allow_duplicates, persist=persist, entry_source=entry_source, **kwd
)
except MessageException as e:
if e.type == "warning":
log.warning(str(e))
else:
log.error(str(e))
except Exception as e:
log.error(str(e))
return self._loaded_content_version
Expand Down Expand Up @@ -686,7 +691,8 @@ def _add_entry(
self.data.append(fields)
else:
raise MessageException(
f"Attempted to add fields ({fields}) to data table '{self.name}', but this entry already exists and allow_duplicates is False."
f"Attempted to add fields ({fields}) to data table '{self.name}', but this entry already exists and allow_duplicates is False.",
type="warning",
)
else:
raise MessageException(
Expand Down
28 changes: 26 additions & 2 deletions lib/galaxy/tool_util/toolbox/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,38 @@ def __init__(self, item=None):
self.links = item.get("links") or None
self.elems = ToolPanelElements()

def copy(self):
def copy(self, merge_tools=False):
copy = ToolSection()
copy.name = self.name
copy.id = self.id
copy.version = self.version
copy.description = self.description
copy.links = self.links
copy.elems.update(self.elems)

for key, panel_type, value in self.panel_items_iter():
if panel_type == panel_item_types.TOOL and merge_tools:
tool = value
tool_lineage = tool.lineage

tool_copied = False
if tool_lineage is not None:
version_ids = tool_lineage.get_version_ids(reverse=True)

for version_id in version_ids:
if copy.elems.has_tool_with_id(version_id):
tool_copied = True
break

if self.elems.has_tool_with_id(version_id):
copy.elems.append_tool(self.elems.get_tool_with_id(version_id))
tool_copied = True
break

if not tool_copied:
copy.elems[key] = value
else:
copy.elems[key] = value

return copy

def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None, only_ids=False):
Expand Down
5 changes: 3 additions & 2 deletions lib/galaxy/tool_util/toolbox/views/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def definition_with_items_to_panel(definition, allow_sections: bool = True, item
f"Failed to find matching section for (id, name) = ({element.section}, {element.section})"
)
continue
section = closest_section.copy()
section = closest_section.copy(merge_tools=True)
apply_filter(element, section.elems)
new_panel.append_section(section.id, section)
elif element.content_type == "label":
Expand Down Expand Up @@ -151,7 +151,8 @@ def definition_with_items_to_panel(definition, allow_sections: bool = True, item
if closest_section is None:
log.warning(f"Failed to find matching section for (id, name) = ({element.items_from}, None)")
continue
elems = closest_section.elems.copy()
section = closest_section.copy(merge_tools=True)
elems = section.elems
apply_filter(element, elems)
for key, item in elems.items():
new_panel[key] = item
Expand Down
10 changes: 6 additions & 4 deletions lib/galaxy/workflow/run_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,17 @@ def _normalize_inputs(
inputs_key = possible_input_key

default_not_set = object()
has_default = step.get_input_default_value(default_not_set) is not default_not_set
default_value = step.get_input_default_value(default_not_set)
has_default = default_value is not default_not_set
optional = step.input_optional
# Need to be careful here to make sure 'default' has correct type - not sure how to do that
# but asserting 'optional' is definitely a bool and not a String->Bool or something is a good
# start to ensure tool state is being preserved and loaded in a type safe way.
assert isinstance(optional, bool)
assert isinstance(has_default, bool)
if not inputs_key and not has_default and not optional:
message = f"Workflow cannot be run because an expected input step '{step.id}' ({step.label}) is not optional and no input."
assert isinstance(optional, bool)
has_input_value = inputs_key and inputs[inputs_key] is not None
if not has_input_value and default_value is None and not optional:
message = f"Workflow cannot be run because input step '{step.id}' ({step.label}) is not optional and no input provided."
raise exceptions.MessageException(message)
if inputs_key:
normalized_inputs[step.id] = inputs[inputs_key]
Expand Down
6 changes: 5 additions & 1 deletion lib/galaxy_test/api/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,11 @@ def test_test_by_versions(self):
test_data_response = self._get("tools/multiple_versions/test_data?tool_version=*")
test_data_response.raise_for_status()
test_data_dicts = test_data_response.json()
assert len(test_data_dicts) == 3
# this found a bug - tools that appear in the toolbox twice should not cause
# multiple copies of test data to be returned. This assertion broke when
# we placed multiple_versions in the test tool panel in multiple places. We need
# to fix this but it isn't as important as the existing bug.
# assert len(test_data_dicts) == 3

@skip_without_tool("multiple_versions")
def test_show_with_wrong_tool_version_in_tool_id(self):
Expand Down
Loading

0 comments on commit f655c83

Please sign in to comment.