mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge remote-tracking branch 'upstream/release_24.0' into dev
This commit is contained in:
@@ -6852,10 +6852,11 @@ export interface components {
|
||||
tool_version?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @description The type of workflow module.
|
||||
* @default data_collection_input
|
||||
* @constant
|
||||
*/
|
||||
type?: components["schemas"]["WorkflowModuleType"];
|
||||
type: "data_collection_input";
|
||||
/** When */
|
||||
when: string | null;
|
||||
};
|
||||
/** InputDataStep */
|
||||
InputDataStep: {
|
||||
@@ -6893,10 +6894,11 @@ export interface components {
|
||||
tool_version?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @description The type of workflow module.
|
||||
* @default data_input
|
||||
* @constant
|
||||
*/
|
||||
type?: components["schemas"]["WorkflowModuleType"];
|
||||
type: "data_input";
|
||||
/** When */
|
||||
when: string | null;
|
||||
};
|
||||
/** InputParameterStep */
|
||||
InputParameterStep: {
|
||||
@@ -6934,10 +6936,11 @@ export interface components {
|
||||
tool_version?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @description The type of workflow module.
|
||||
* @default parameter_input
|
||||
* @constant
|
||||
*/
|
||||
type?: components["schemas"]["WorkflowModuleType"];
|
||||
type: "parameter";
|
||||
/** When */
|
||||
when: string | null;
|
||||
};
|
||||
/** InputReferenceByLabel */
|
||||
InputReferenceByLabel: {
|
||||
@@ -9704,11 +9707,27 @@ export interface components {
|
||||
[key: string]: components["schemas"]["InputStep"] | undefined;
|
||||
};
|
||||
/**
|
||||
* Type
|
||||
* @description The type of workflow module.
|
||||
* @default pause
|
||||
* Tool ID
|
||||
* @description The unique name of the tool associated with this step.
|
||||
*/
|
||||
type?: components["schemas"]["WorkflowModuleType"];
|
||||
tool_id?: string | null;
|
||||
/**
|
||||
* Tool Inputs
|
||||
* @description TODO
|
||||
*/
|
||||
tool_inputs?: Record<string, never>;
|
||||
/**
|
||||
* Tool Version
|
||||
* @description The version of the tool associated with this step.
|
||||
*/
|
||||
tool_version?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
*/
|
||||
type: "pause";
|
||||
/** When */
|
||||
when: string | null;
|
||||
};
|
||||
/** Person */
|
||||
Person: {
|
||||
@@ -11110,11 +11129,27 @@ export interface components {
|
||||
[key: string]: components["schemas"]["InputStep"] | undefined;
|
||||
};
|
||||
/**
|
||||
* Type
|
||||
* @description The type of workflow module.
|
||||
* @default subworkflow
|
||||
* Tool ID
|
||||
* @description The unique name of the tool associated with this step.
|
||||
*/
|
||||
type?: components["schemas"]["WorkflowModuleType"];
|
||||
tool_id?: string | null;
|
||||
/**
|
||||
* Tool Inputs
|
||||
* @description TODO
|
||||
*/
|
||||
tool_inputs?: Record<string, never>;
|
||||
/**
|
||||
* Tool Version
|
||||
* @description The version of the tool associated with this step.
|
||||
*/
|
||||
tool_version?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @constant
|
||||
*/
|
||||
type: "subworkflow";
|
||||
/** When */
|
||||
when: string | null;
|
||||
/**
|
||||
* Workflow ID
|
||||
* @description The encoded ID of the workflow that will be run on this step.
|
||||
@@ -11301,10 +11336,11 @@ export interface components {
|
||||
tool_version?: string | null;
|
||||
/**
|
||||
* Type
|
||||
* @description The type of workflow module.
|
||||
* @default tool
|
||||
* @constant
|
||||
*/
|
||||
type?: components["schemas"]["WorkflowModuleType"];
|
||||
type: "tool";
|
||||
/** When */
|
||||
when: string | null;
|
||||
};
|
||||
/** Tour */
|
||||
Tour: {
|
||||
@@ -12303,18 +12339,6 @@ export interface components {
|
||||
[key: string]: number | undefined;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* WorkflowModuleType
|
||||
* @description Available types of modules that represent a step in a Workflow.
|
||||
* @enum {string}
|
||||
*/
|
||||
WorkflowModuleType:
|
||||
| "data_input"
|
||||
| "data_collection_input"
|
||||
| "parameter_input"
|
||||
| "subworkflow"
|
||||
| "tool"
|
||||
| "pause";
|
||||
/** WriteInvocationStoreToPayload */
|
||||
WriteInvocationStoreToPayload: {
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,7 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
activeList: "my",
|
||||
username: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -65,8 +66,16 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
<BNavItem id="histories-published-tab" :active="activeList === 'published'" to="/histories/list_published">
|
||||
Public Histories
|
||||
</BNavItem>
|
||||
<BNavItem id="histories-published-tab" :active="activeList === 'archived'" to="/histories/archived">
|
||||
<BNavItem
|
||||
id="histories-archived-tab"
|
||||
:active="activeList === 'archived'"
|
||||
:disabled="userStore.isAnonymous"
|
||||
to="/histories/archived">
|
||||
Archived Histories
|
||||
<LoginRequired
|
||||
v-if="userStore.isAnonymous"
|
||||
target="histories-archived-tab"
|
||||
title="Manage your Histories" />
|
||||
</BNavItem>
|
||||
</BNav>
|
||||
<GridList v-if="activeList === 'my'" :grid-config="historiesGridConfig" embedded />
|
||||
|
||||
@@ -363,7 +363,7 @@ watch(operationMessage, () => {
|
||||
</table>
|
||||
<div class="flex-grow-1 h-100" />
|
||||
<div class="grid-footer">
|
||||
<div v-if="isAvailable && gridConfig.batch" class="d-flex justify-content-between pt-3">
|
||||
<div v-if="isAvailable" class="d-flex justify-content-between pt-3">
|
||||
<div class="d-flex">
|
||||
<div v-for="(batchOperation, batchIndex) in gridConfig.batch" :key="batchIndex">
|
||||
<BButton
|
||||
@@ -383,9 +383,6 @@ watch(operationMessage, () => {
|
||||
</div>
|
||||
<BPagination v-model="currentPage" :total-rows="totalRows" :per-page="limit" class="m-0" size="sm" />
|
||||
</div>
|
||||
<div v-else-if="isAvailable" class="d-flex justify-content-center pt-3">
|
||||
<BPagination v-model="currentPage" :total-rows="totalRows" :per-page="limit" class="m-0" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -402,6 +399,7 @@ watch(operationMessage, () => {
|
||||
top: 0;
|
||||
}
|
||||
.grid-sticky {
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: $white;
|
||||
opacity: 0.95;
|
||||
|
||||
@@ -241,7 +241,7 @@ const fields: FieldArray = [
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "hid_counter",
|
||||
key: "count",
|
||||
title: "Items",
|
||||
type: "text",
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ type SortKeyLiteral = "name" | "update_time" | undefined;
|
||||
*/
|
||||
async function getData(offset: number, limit: number, search: string, sort_by: string, sort_desc: boolean) {
|
||||
const { data, headers } = await historiesFetcher({
|
||||
view: "summary",
|
||||
limit,
|
||||
offset,
|
||||
search,
|
||||
|
||||
@@ -43,7 +43,16 @@ const dsc = computed(() => {
|
||||
}
|
||||
return currentCollection;
|
||||
});
|
||||
const collectionElements = computed(() => collectionElementsStore.getCollectionElements(dsc.value, offset.value));
|
||||
|
||||
watch(
|
||||
() => [dsc.value, offset.value],
|
||||
() => {
|
||||
collectionElementsStore.fetchMissingElements(dsc.value, offset.value);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const collectionElements = computed(() => collectionElementsStore.getCollectionElements(dsc.value) ?? []);
|
||||
const loading = computed(() => collectionElementsStore.isLoadingCollectionElements(dsc.value));
|
||||
const jobState = computed(() => ("job_state_summary" in dsc.value ? dsc.value.job_state_summary : undefined));
|
||||
const populatedStateMsg = computed(() =>
|
||||
@@ -99,7 +108,8 @@ watch(
|
||||
watch(
|
||||
jobState,
|
||||
() => {
|
||||
collectionElementsStore.loadCollectionElements(dsc.value);
|
||||
collectionElementsStore.invalidateCollectionElements(dsc.value);
|
||||
collectionElementsStore.fetchMissingElements(dsc.value, offset.value);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
@@ -22,24 +22,30 @@ const preferredObjectStoreId = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BPopover :target="`history-storage-${historyId}`" triggers="hover" placement="bottomleft">
|
||||
<BPopover :target="`history-storage-${historyId}`" triggers="hover" placement="bottomleft" boundary="window">
|
||||
<template v-slot:title>Preferred Target Object Store</template>
|
||||
<div class="popover-wide">
|
||||
<p v-if="historyPreferredObjectStoreId" class="history-preferred-object-store-inherited">
|
||||
This target object store has been set at the history level.
|
||||
</p>
|
||||
<p v-else class="history-preferred-object-store-not-inherited">
|
||||
This target object store has been inherited from your user preferences (set in User -> Preferences ->
|
||||
Preferred Object Store). If that option is updated, this history will target that new default.
|
||||
</p>
|
||||
|
||||
<p v-if="historyPreferredObjectStoreId" class="history-preferred-object-store-inherited">
|
||||
This target object store has been set at the history level.
|
||||
</p>
|
||||
<p v-else class="history-preferred-object-store-not-inherited">
|
||||
This target object store has been inherited from your user preferences (set in User -> Preferences ->
|
||||
Preferred Object Store). If that option is updated, this history will target that new default.
|
||||
</p>
|
||||
<ShowSelectedObjectStore
|
||||
v-if="preferredObjectStoreId"
|
||||
:preferred-object-store-id="preferredObjectStoreId"
|
||||
for-what="Galaxy will default to storing this history's datasets in " />
|
||||
|
||||
<ShowSelectedObjectStore
|
||||
v-if="preferredObjectStoreId"
|
||||
:preferred-object-store-id="preferredObjectStoreId"
|
||||
for-what="Galaxy will default to storing this history's datasets in " />
|
||||
|
||||
<div v-localize>
|
||||
Change this preference object store target by clicking on the storage button in the history panel.
|
||||
<div v-localize>
|
||||
Change this preference object store target by clicking on the storage button in the history panel.
|
||||
</div>
|
||||
</div>
|
||||
</BPopover>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.popover-wide {
|
||||
max-width: 30rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -100,6 +100,17 @@ function selectHistories() {
|
||||
function setFilterValue(newFilter: string, newValue: string) {
|
||||
filter.value = HistoriesFilters.setFilterValue(filter.value, newFilter, newValue);
|
||||
}
|
||||
|
||||
// hacky workaround for popovers in date pickers being cutoff
|
||||
// https://github.com/galaxyproject/galaxy/issues/17711
|
||||
const modalBodyClasses = computed(() => {
|
||||
return [
|
||||
"history-selector-modal-body",
|
||||
showAdvanced.value
|
||||
? "history-selector-modal-body-allow-overflow"
|
||||
: "history-selector-modal-body-prevent-overflow",
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -107,9 +118,9 @@ function setFilterValue(newFilter: string, newValue: string) {
|
||||
<BModal
|
||||
ref="modal"
|
||||
v-model="propShowModal"
|
||||
body-class="history-selector-modal-body"
|
||||
content-class="history-selector-modal-content"
|
||||
v-bind="$attrs"
|
||||
:body-class="modalBodyClasses"
|
||||
static
|
||||
centered
|
||||
hide-footer
|
||||
@@ -174,11 +185,18 @@ function setFilterValue(newFilter: string, newValue: string) {
|
||||
with scoped or lang="scss" */
|
||||
|
||||
.history-selector-modal-body {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.history-selector-modal-body-allow-overflow {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.history-selector-modal-body-prevent-overflow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.history-selector-modal-content {
|
||||
max-height: 80vh !important;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<template>
|
||||
<b-popover target="tool-storage" triggers="hover" placement="bottomleft">
|
||||
<b-popover target="tool-storage" triggers="hover" placement="bottomleft" boundary="window">
|
||||
<template v-slot:title>{{ title }}</template>
|
||||
<p v-if="toolPreferredObjectStoreId">
|
||||
This target object store has been set at the tool level, by default history or user preferences will be used
|
||||
and if those are not set Galaxy will pick an adminstrator configured default.
|
||||
</p>
|
||||
<ShowSelectedObjectStore
|
||||
v-if="toolPreferredObjectStoreId"
|
||||
:preferred-object-store-id="toolPreferredObjectStoreId"
|
||||
for-what="Galaxy will default to storing this tool run's output in">
|
||||
</ShowSelectedObjectStore>
|
||||
<div v-else>
|
||||
No selection has been made for this tool execution. Defaults from history, user, or Galaxy will be used.
|
||||
</div>
|
||||
<div v-localize>
|
||||
Change this preference object store target by clicking on the storage button in the tool header.
|
||||
<div class="popover-wide">
|
||||
<p v-if="toolPreferredObjectStoreId">
|
||||
This target object store has been set at the tool level, by default history or user preferences will be
|
||||
used and if those are not set Galaxy will pick an adminstrator configured default.
|
||||
</p>
|
||||
<ShowSelectedObjectStore
|
||||
v-if="toolPreferredObjectStoreId"
|
||||
:preferred-object-store-id="toolPreferredObjectStoreId"
|
||||
for-what="Galaxy will default to storing this tool run's output in">
|
||||
</ShowSelectedObjectStore>
|
||||
<div v-else>
|
||||
No selection has been made for this tool execution. Defaults from history, user, or Galaxy will be used.
|
||||
</div>
|
||||
<div v-localize>
|
||||
Change this preference object store target by clicking on the storage button in the tool header.
|
||||
</div>
|
||||
</div>
|
||||
</b-popover>
|
||||
</template>
|
||||
@@ -33,3 +35,8 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.popover-wide {
|
||||
max-width: 30rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<b-popover :target="target" triggers="hover" placement="bottomleft">
|
||||
<b-popover :target="target" triggers="hover" placement="bottomleft" boundary="window">
|
||||
<template v-slot:title>{{ title }}</template>
|
||||
<p v-if="invocationPreferredObjectStoreId">This target object store has been set at the invocation level.</p>
|
||||
<ShowSelectedObjectStore
|
||||
v-if="invocationPreferredObjectStoreId"
|
||||
:preferred-object-store-id="invocationPreferredObjectStoreId"
|
||||
for-what="Galaxy will default to storing this tool run's output in">
|
||||
</ShowSelectedObjectStore>
|
||||
<div v-else>
|
||||
No selection has been made for this worklfow invocation. Defaults from history, user, or Galaxy will be
|
||||
used.
|
||||
</div>
|
||||
<div v-localize>
|
||||
Change this preference object store target by clicking on the storage button in the worklfow run header.
|
||||
<div class="popover-wide">
|
||||
<p v-if="invocationPreferredObjectStoreId">
|
||||
This target object store has been set at the invocation level.
|
||||
</p>
|
||||
<ShowSelectedObjectStore
|
||||
v-if="invocationPreferredObjectStoreId"
|
||||
:preferred-object-store-id="invocationPreferredObjectStoreId"
|
||||
for-what="Galaxy will default to storing this tool run's output in">
|
||||
</ShowSelectedObjectStore>
|
||||
<div v-else>
|
||||
No selection has been made for this worklfow invocation. Defaults from history, user, or Galaxy will be
|
||||
used.
|
||||
</div>
|
||||
<div v-localize>
|
||||
Change this preference object store target by clicking on the storage button in the worklfow run header.
|
||||
</div>
|
||||
</div>
|
||||
</b-popover>
|
||||
</template>
|
||||
@@ -34,3 +38,9 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popover-wide {
|
||||
max-width: 30rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -242,7 +242,7 @@ const menuActions: ComputedRef<BAction[]> = computed(() => {
|
||||
:data-workflow-actions-dropdown="workflow.id"
|
||||
right
|
||||
no-caret
|
||||
class="workflow-actions-dropdown show-in-card"
|
||||
class="workflow-actions-dropdown"
|
||||
toggle-class="inline-icon-button"
|
||||
title="Workflow actions"
|
||||
variant="link">
|
||||
|
||||
@@ -123,38 +123,34 @@ async function onTagClick(tag: string) {
|
||||
:class="{
|
||||
'workflow-shared': workflow.published,
|
||||
}">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<div>
|
||||
<WorkflowIndicators :workflow="workflow" :published-view="publishedView" />
|
||||
<div class="workflow-card-header">
|
||||
<WorkflowIndicators :workflow="workflow" :published-view="publishedView" />
|
||||
|
||||
<span class="workflow-name font-weight-bold">
|
||||
{{ workflow.name }}
|
||||
<BButton
|
||||
v-if="!shared && !workflow.deleted"
|
||||
v-b-tooltip.hover.noninteractive
|
||||
:data-workflow-rename="workflow.id"
|
||||
class="inline-icon-button workflow-rename"
|
||||
variant="link"
|
||||
size="sm"
|
||||
title="Rename"
|
||||
@click="showRename = !showRename">
|
||||
<FontAwesomeIcon :icon="faPen" fixed-width />
|
||||
</BButton>
|
||||
</span>
|
||||
</div>
|
||||
<div class="workflow-count-actions">
|
||||
<WorkflowInvocationsCount v-if="!isAnonymous && !shared" class="mx-1" :workflow="workflow" />
|
||||
|
||||
<div class="workflow-count-actions">
|
||||
<WorkflowInvocationsCount v-if="!isAnonymous && !shared" class="mx-1" :workflow="workflow" />
|
||||
|
||||
<WorkflowActions
|
||||
:workflow="workflow"
|
||||
:published="publishedView"
|
||||
@refreshList="emit('refreshList', true)"
|
||||
@toggleShowPreview="toggleShowPreview" />
|
||||
</div>
|
||||
<WorkflowActions
|
||||
:workflow="workflow"
|
||||
:published="publishedView"
|
||||
@refreshList="emit('refreshList', true)"
|
||||
@toggleShowPreview="toggleShowPreview" />
|
||||
</div>
|
||||
|
||||
<span class="workflow-name font-weight-bold">
|
||||
{{ workflow.name }}
|
||||
<BButton
|
||||
v-if="!shared && !workflow.deleted"
|
||||
v-b-tooltip.hover.noninteractive
|
||||
:data-workflow-rename="workflow.id"
|
||||
class="inline-icon-button workflow-rename"
|
||||
variant="link"
|
||||
size="sm"
|
||||
title="Rename"
|
||||
@click="showRename = !showRename">
|
||||
<FontAwesomeIcon :icon="faPen" fixed-width />
|
||||
</BButton>
|
||||
</span>
|
||||
|
||||
<TextSummary
|
||||
v-if="description"
|
||||
class="my-1"
|
||||
@@ -272,47 +268,53 @@ async function onTagClick(tag: string) {
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
.workflow-count-actions {
|
||||
display: flex;
|
||||
align-self: baseline;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
&.workflow-shared {
|
||||
border-left: 0.25rem solid $brand-primary;
|
||||
}
|
||||
|
||||
.text-summary {
|
||||
height: inherit;
|
||||
}
|
||||
.workflow-card-header {
|
||||
display: grid;
|
||||
|
||||
.workflow-name {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.workflow-count-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.workflow-card-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
.workflow-name {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.workflow-card-tags {
|
||||
max-width: 60%;
|
||||
.workflow-card-tags {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.workflow-card-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
@container workflow-card (max-width: #{$breakpoint-sm}) {
|
||||
.workflow-card-footer {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.workflow-card-tags {
|
||||
max-width: 100%;
|
||||
.workflow-card-tags {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,19 +331,7 @@ async function onTagClick(tag: string) {
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-shared {
|
||||
border-left: 0.25rem solid $brand-primary;
|
||||
}
|
||||
|
||||
@container workflow-card (max-width: #{$breakpoint-md}) {
|
||||
.hide-in-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.show-in-card {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.workflow-count-actions {
|
||||
align-items: baseline;
|
||||
justify-content: end;
|
||||
@@ -353,14 +343,6 @@ async function onTagClick(tag: string) {
|
||||
align-items: end;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.hide-in-card {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.show-in-card {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -291,6 +291,7 @@ export function getRouter(Galaxy) {
|
||||
props: {
|
||||
activeList: "archived",
|
||||
},
|
||||
redirect: redirectAnon(),
|
||||
},
|
||||
{
|
||||
path: "histories/list",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Vue from "vue";
|
||||
import { set } from "vue";
|
||||
|
||||
/* This function merges the existing data with new incoming data. */
|
||||
export function mergeArray(id, payload, items, itemKey) {
|
||||
if (!items[id]) {
|
||||
Vue.set(items, id, []);
|
||||
set(items, id, []);
|
||||
}
|
||||
const itemArray = items[id];
|
||||
for (const item of payload) {
|
||||
@@ -16,7 +16,7 @@ export function mergeArray(id, payload, items, itemKey) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Vue.set(itemArray, itemIndex, item);
|
||||
set(itemArray, itemIndex, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,15 @@ describe("useCollectionElementsStore", () => {
|
||||
expect(store.storedCollectionElements).toEqual({});
|
||||
expect(store.isLoadingCollectionElements(collection)).toEqual(false);
|
||||
|
||||
const offset = 0;
|
||||
const limit = 5;
|
||||
// Getting collection elements should trigger a fetch and change the loading state
|
||||
store.getCollectionElements(collection, offset, limit);
|
||||
expect(store.isLoadingCollectionElements(collection)).toEqual(true);
|
||||
await flushPromises();
|
||||
// Getting collection elements should be side effect free
|
||||
store.getCollectionElements(collection);
|
||||
expect(store.isLoadingCollectionElements(collection)).toEqual(false);
|
||||
await flushPromises();
|
||||
expect(fetchCollectionElements).not.toHaveBeenCalled();
|
||||
|
||||
const limit = 5;
|
||||
store.fetchMissingElements(collection, 0, limit);
|
||||
await flushPromises();
|
||||
expect(fetchCollectionElements).toHaveBeenCalled();
|
||||
|
||||
const collectionKey = store.getCollectionKey(collection);
|
||||
@@ -70,18 +72,20 @@ describe("useCollectionElementsStore", () => {
|
||||
const offset = 0;
|
||||
const limit = storedCount;
|
||||
// Getting the same collection elements range should not trigger a fetch
|
||||
store.getCollectionElements(collection, offset, limit);
|
||||
store.fetchMissingElements(collection, offset, limit);
|
||||
expect(store.isLoadingCollectionElements(collection)).toEqual(false);
|
||||
expect(fetchCollectionElements).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fetch only missing elements if the requested range is not already stored", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const totalElements = 10;
|
||||
const collection: HDCASummary = mockCollection("1", totalElements);
|
||||
const store = useCollectionElementsStore();
|
||||
|
||||
const initialElements = 3;
|
||||
store.getCollectionElements(collection, 0, initialElements);
|
||||
store.fetchMissingElements(collection, 0, initialElements);
|
||||
await flushPromises();
|
||||
expect(fetchCollectionElements).toHaveBeenCalled();
|
||||
const collectionKey = store.getCollectionKey(collection);
|
||||
@@ -92,11 +96,10 @@ describe("useCollectionElementsStore", () => {
|
||||
|
||||
const offset = 2;
|
||||
const limit = 5;
|
||||
// Getting collection elements should trigger a fetch in this case
|
||||
store.getCollectionElements(collection, offset, limit);
|
||||
expect(store.isLoadingCollectionElements(collection)).toEqual(true);
|
||||
// Fetching collection elements should trigger a fetch in this case
|
||||
store.fetchMissingElements(collection, offset, limit);
|
||||
jest.runAllTimers();
|
||||
await flushPromises();
|
||||
expect(store.isLoadingCollectionElements(collection)).toEqual(false);
|
||||
expect(fetchCollectionElements).toHaveBeenCalled();
|
||||
|
||||
elements = store.storedCollectionElements[collectionKey];
|
||||
|
||||
@@ -4,6 +4,8 @@ import { computed, del, ref, set } from "vue";
|
||||
import type { CollectionEntry, DCESummary, HDCASummary, HistoryContentItemBase } from "@/api";
|
||||
import { isHDCA } from "@/api";
|
||||
import { fetchCollectionDetails, fetchElementsFromCollection } from "@/api/datasetCollections";
|
||||
import { ensureDefined } from "@/utils/assertions";
|
||||
import { ActionSkippedError, LastQueue } from "@/utils/lastQueue";
|
||||
|
||||
/**
|
||||
* Represents an element in a collection that has not been fetched yet.
|
||||
@@ -24,7 +26,11 @@ export interface ContentPlaceholder {
|
||||
fetching?: boolean;
|
||||
}
|
||||
|
||||
export type DCEEntry = ContentPlaceholder | DCESummary;
|
||||
export type InvalidDCEEntry = (ContentPlaceholder | DCESummary) & {
|
||||
valid: false;
|
||||
};
|
||||
|
||||
export type DCEEntry = ContentPlaceholder | DCESummary | InvalidDCEEntry;
|
||||
|
||||
const FETCH_LIMIT = 50;
|
||||
|
||||
@@ -46,11 +52,8 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore",
|
||||
}
|
||||
|
||||
const getCollectionElements = computed(() => {
|
||||
return (collection: CollectionEntry, offset = 0, limit = FETCH_LIMIT) => {
|
||||
const storedElements =
|
||||
storedCollectionElements.value[getCollectionKey(collection)] ?? initWithPlaceholderElements(collection);
|
||||
fetchMissingElements({ collection, storedElements, offset, limit });
|
||||
return storedElements;
|
||||
return (collection: CollectionEntry) => {
|
||||
return storedCollectionElements.value[getCollectionKey(collection)];
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,47 +63,91 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore",
|
||||
};
|
||||
});
|
||||
|
||||
async function fetchMissingElements(params: {
|
||||
collection: CollectionEntry;
|
||||
type FetchParams = {
|
||||
storedElements: DCEEntry[];
|
||||
collection: CollectionEntry;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}) {
|
||||
const collectionKey = getCollectionKey(params.collection);
|
||||
try {
|
||||
// We should fetch only missing (placeholder) elements from the range
|
||||
const firstMissingIndexInRange = params.storedElements
|
||||
.slice(params.offset, params.offset + params.limit)
|
||||
.findIndex((element) => isPlaceholder(element) && !element.fetching);
|
||||
};
|
||||
|
||||
if (firstMissingIndexInRange === -1) {
|
||||
// All elements in the range are already stored or being fetched
|
||||
return;
|
||||
async function fetchMissing({ storedElements, collection, offset, limit = FETCH_LIMIT }: FetchParams) {
|
||||
const collectionKey = getCollectionKey(collection);
|
||||
|
||||
try {
|
||||
if (collection.element_count !== null) {
|
||||
// We should fetch only missing (placeholder) elements from the range
|
||||
const firstMissingIndexInRange = storedElements
|
||||
.slice(offset, offset + limit)
|
||||
.findIndex((element) => (isPlaceholder(element) && !element.fetching) || isInvalid(element));
|
||||
|
||||
if (firstMissingIndexInRange === -1) {
|
||||
// All elements in the range are already stored or being fetched
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust the offset to the first missing element
|
||||
offset += firstMissingIndexInRange;
|
||||
} else {
|
||||
// Edge case where element_count is incorrect
|
||||
// TODO: remove me once element_count is reported reliably
|
||||
offset = 0;
|
||||
}
|
||||
// Adjust the offset to the first missing element
|
||||
params.offset += firstMissingIndexInRange;
|
||||
|
||||
set(loadingCollectionElements.value, collectionKey, true);
|
||||
// Mark all elements in the range as fetching
|
||||
params.storedElements
|
||||
.slice(params.offset, params.offset + params.limit)
|
||||
storedElements
|
||||
.slice(offset, offset + limit)
|
||||
.forEach((element) => isPlaceholder(element) && (element.fetching = true));
|
||||
|
||||
const fetchedElements = await fetchElementsFromCollection({
|
||||
entry: params.collection,
|
||||
offset: params.offset,
|
||||
limit: params.limit,
|
||||
entry: collection,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
});
|
||||
// Update only the elements that were fetched
|
||||
params.storedElements.splice(params.offset, fetchedElements.length, ...fetchedElements);
|
||||
set(storedCollectionElements.value, collectionKey, params.storedElements);
|
||||
|
||||
return { fetchedElements, elementOffset: offset };
|
||||
} finally {
|
||||
del(loadingCollectionElements.value, collectionKey);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCollectionElements(collection: CollectionEntry) {
|
||||
const elements = await fetchElementsFromCollection({ entry: collection });
|
||||
set(storedCollectionElements.value, getCollectionKey(collection), elements);
|
||||
const lastQueue = new LastQueue<typeof fetchMissing>(1000, true);
|
||||
|
||||
async function fetchMissingElements(collection: CollectionEntry, offset: number, limit = FETCH_LIMIT) {
|
||||
const key = getCollectionKey(collection);
|
||||
let storedElements = storedCollectionElements.value[key];
|
||||
|
||||
if (!storedElements) {
|
||||
storedElements = initWithPlaceholderElements(collection);
|
||||
set(storedCollectionElements.value, key, storedElements);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await lastQueue.enqueue(fetchMissing, { storedElements, collection, offset, limit }, key);
|
||||
|
||||
if (data) {
|
||||
const from = data.elementOffset;
|
||||
const to = from + data.fetchedElements.length;
|
||||
|
||||
for (let index = from; index < to; index++) {
|
||||
const element = ensureDefined(data.fetchedElements[index - from]);
|
||||
set(storedElements, index, element);
|
||||
}
|
||||
|
||||
set(storedCollectionElements.value, key, storedElements);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof ActionSkippedError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function invalidateCollectionElements(collection: CollectionEntry) {
|
||||
const storedElements = storedCollectionElements.value[getCollectionKey(collection)] ?? [];
|
||||
storedElements.forEach((element) => {
|
||||
(element as InvalidDCEEntry).valid = false;
|
||||
});
|
||||
}
|
||||
|
||||
function saveCollections(historyContentsPayload: HistoryContentItemBase[]) {
|
||||
@@ -135,6 +182,10 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore",
|
||||
return "id" in element === false;
|
||||
}
|
||||
|
||||
function isInvalid(element: DCEEntry): element is InvalidDCEEntry {
|
||||
return (element as InvalidDCEEntry)["valid"] === false;
|
||||
}
|
||||
|
||||
function initWithPlaceholderElements(collection: CollectionEntry): ContentPlaceholder[] {
|
||||
const totalElements = collection.element_count ?? 0;
|
||||
const placeholderElements = new Array<ContentPlaceholder>(totalElements);
|
||||
@@ -151,8 +202,9 @@ export const useCollectionElementsStore = defineStore("collectionElementsStore",
|
||||
isLoadingCollectionElements,
|
||||
getCollection,
|
||||
fetchCollection,
|
||||
loadCollectionElements,
|
||||
invalidateCollectionElements,
|
||||
saveCollections,
|
||||
getCollectionKey,
|
||||
fetchMissingElements,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type QueuedAction<T extends (...args: any) => R, R = unknown> = {
|
||||
type QueuedAction<T extends (...args: any) => R, R = ReturnType<T>> = {
|
||||
action: T;
|
||||
arg: Parameters<T>[0];
|
||||
resolve: (value: R) => void;
|
||||
@@ -13,7 +13,7 @@ export class ActionSkippedError extends Error {}
|
||||
* This is useful when promises earlier enqueued become obsolete.
|
||||
* See also: https://stackoverflow.com/questions/53540348/js-async-await-tasks-queue
|
||||
*/
|
||||
export class LastQueue<T extends (arg: any) => R, R = unknown> {
|
||||
export class LastQueue<T extends (arg: any) => R, R = ReturnType<T>> {
|
||||
throttlePeriod: number;
|
||||
/** Throw an error if a queued action is skipped. This avoids dangling promises */
|
||||
rejectSkipped: boolean;
|
||||
@@ -34,7 +34,7 @@ export class LastQueue<T extends (arg: any) => R, R = unknown> {
|
||||
promise?.reject(new ActionSkippedError());
|
||||
}
|
||||
|
||||
async enqueue(action: T, arg: Parameters<T>[0], key: string | number = 0) {
|
||||
async enqueue(action: T, arg: Parameters<T>[0], key: string | number = 0): Promise<R> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.skipPromise(key);
|
||||
this.queuedPromises[key] = { action, arg, resolve, reject };
|
||||
|
||||
@@ -1615,9 +1615,9 @@ class WorkflowContentsManager(UsesAnnotations):
|
||||
def _workflow_to_dict_instance(self, trans, stored, workflow, legacy=True):
|
||||
encode = self.app.security.encode_id
|
||||
sa_session = self.app.model.context
|
||||
item = stored.to_dict(view="element", value_mapper={"id": encode})
|
||||
item = stored.to_dict(view="element")
|
||||
item["name"] = workflow.name
|
||||
item["url"] = trans.url_builder("workflow", id=item["id"])
|
||||
item["url"] = trans.url_builder("workflow", id=encode(stored.id))
|
||||
item["owner"] = stored.user.username
|
||||
item["email_hash"] = md5_hash_str(stored.user.email)
|
||||
item["slug"] = stored.slug
|
||||
@@ -1668,7 +1668,7 @@ class WorkflowContentsManager(UsesAnnotations):
|
||||
del step_dict["tool_id"]
|
||||
del step_dict["tool_version"]
|
||||
del step_dict["tool_inputs"]
|
||||
step_dict["workflow_id"] = encode(step.subworkflow.id)
|
||||
step_dict["workflow_id"] = step.subworkflow.id
|
||||
|
||||
for conn in step.input_connections:
|
||||
step_id = step.id if legacy else step.order_index
|
||||
|
||||
@@ -7730,7 +7730,7 @@ class WorkflowStep(Base, RepresentById):
|
||||
uuid = Column(UUIDType)
|
||||
label = Column(Unicode(255))
|
||||
temp_input_connections: Optional[InputConnDictType]
|
||||
parent_comment_id = Column(Integer, ForeignKey("workflow_comment.id"), nullable=True)
|
||||
parent_comment_id = Column(Integer, ForeignKey("workflow_comment.id"), index=True, nullable=True)
|
||||
|
||||
parent_comment = relationship(
|
||||
"WorkflowComment",
|
||||
@@ -8177,7 +8177,7 @@ class WorkflowComment(Base, RepresentById):
|
||||
type = Column(String(16))
|
||||
color = Column(String(16))
|
||||
data = Column(JSONType)
|
||||
parent_comment_id = Column(Integer, ForeignKey("workflow_comment.id"), nullable=True)
|
||||
parent_comment_id = Column(Integer, ForeignKey("workflow_comment.id"), index=True, nullable=True)
|
||||
|
||||
workflow = relationship(
|
||||
"Workflow",
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
"""add_indexes_for_workflow_comment_foreign_keys
|
||||
|
||||
Revision ID: 2dc3386d091f
|
||||
Revises: 8a19186a6ee7
|
||||
Create Date: 2024-03-13 15:25:52.587488
|
||||
|
||||
"""
|
||||
|
||||
from galaxy.model.database_object_names import build_index_name
|
||||
from galaxy.model.migrations.util import (
|
||||
create_index,
|
||||
drop_index,
|
||||
)
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2dc3386d091f"
|
||||
down_revision = "8a19186a6ee7"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
workflow_comment_table_name = "workflow_comment"
|
||||
workflow_step_table_name = "workflow_step"
|
||||
workflow_id_column_name = "workflow_id"
|
||||
parent_comment_id_column_name = "parent_comment_id"
|
||||
|
||||
workflow_step_parent_comment_index_name = build_index_name(workflow_step_table_name, parent_comment_id_column_name)
|
||||
workflow_comment_workflow_id_index_name = build_index_name(workflow_comment_table_name, workflow_id_column_name)
|
||||
workflow_comment_parent_comment_index_name = build_index_name(
|
||||
workflow_comment_table_name, parent_comment_id_column_name
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
create_index(workflow_step_parent_comment_index_name, workflow_step_table_name, [parent_comment_id_column_name])
|
||||
create_index(workflow_comment_workflow_id_index_name, workflow_comment_table_name, [workflow_id_column_name])
|
||||
create_index(
|
||||
workflow_comment_parent_comment_index_name, workflow_comment_table_name, [parent_comment_id_column_name]
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
drop_index(workflow_step_parent_comment_index_name, workflow_step_table_name)
|
||||
drop_index(workflow_comment_workflow_id_index_name, workflow_comment_table_name)
|
||||
drop_index(workflow_comment_parent_comment_index_name, workflow_comment_table_name)
|
||||
+15
-35
@@ -2099,7 +2099,7 @@ class JobFullDetails(JobDetails):
|
||||
|
||||
|
||||
class StoredWorkflowSummary(Model, WithModelClass):
|
||||
id: DecodedDatabaseIdField
|
||||
id: EncodedDatabaseIdField
|
||||
model_class: STORED_WORKFLOW_MODEL_CLASS = ModelClassField(STORED_WORKFLOW_MODEL_CLASS)
|
||||
create_time: datetime = CreateTimeField
|
||||
update_time: datetime = UpdateTimeField
|
||||
@@ -2203,33 +2203,21 @@ class InputStep(Model):
|
||||
)
|
||||
|
||||
|
||||
class WorkflowModuleType(str, Enum):
|
||||
"""Available types of modules that represent a step in a Workflow."""
|
||||
|
||||
data_input = "data_input"
|
||||
data_collection_input = "data_collection_input"
|
||||
parameter_input = "parameter_input"
|
||||
subworkflow = "subworkflow"
|
||||
tool = "tool"
|
||||
pause = "pause" # Experimental
|
||||
|
||||
|
||||
class WorkflowStepBase(Model):
|
||||
id: int = Field(
|
||||
...,
|
||||
title="ID",
|
||||
description="The identifier of the step. It matches the index order of the step inside the workflow.",
|
||||
)
|
||||
type: WorkflowModuleType = Field(..., title="Type", description="The type of workflow module.")
|
||||
annotation: Optional[str] = AnnotationField
|
||||
input_steps: Dict[str, InputStep] = Field(
|
||||
...,
|
||||
title="Input Steps",
|
||||
description="A dictionary containing information about the inputs connected to this workflow step.",
|
||||
)
|
||||
|
||||
|
||||
class ToolBasedWorkflowStep(WorkflowStepBase):
|
||||
when: Optional[str]
|
||||
# TODO: these should move to ToolStep, however we might be breaking scripts that iterate over steps and
|
||||
# assume tool_id is a valid key for every step.
|
||||
tool_id: Optional[str] = Field(
|
||||
None, title="Tool ID", description="The unique name of the tool associated with this step."
|
||||
)
|
||||
@@ -2239,37 +2227,29 @@ class ToolBasedWorkflowStep(WorkflowStepBase):
|
||||
tool_inputs: Any = Field(None, title="Tool Inputs", description="TODO")
|
||||
|
||||
|
||||
class InputDataStep(ToolBasedWorkflowStep):
|
||||
type: WorkflowModuleType = Field(
|
||||
WorkflowModuleType.data_input, title="Type", description="The type of workflow module."
|
||||
)
|
||||
class InputDataStep(WorkflowStepBase):
|
||||
type: Literal["data_input"]
|
||||
|
||||
|
||||
class InputDataCollectionStep(ToolBasedWorkflowStep):
|
||||
type: WorkflowModuleType = Field(
|
||||
WorkflowModuleType.data_collection_input, title="Type", description="The type of workflow module."
|
||||
)
|
||||
class InputDataCollectionStep(WorkflowStepBase):
|
||||
type: Literal["data_collection_input"]
|
||||
|
||||
|
||||
class InputParameterStep(ToolBasedWorkflowStep):
|
||||
type: WorkflowModuleType = Field(
|
||||
WorkflowModuleType.parameter_input, title="Type", description="The type of workflow module."
|
||||
)
|
||||
class InputParameterStep(WorkflowStepBase):
|
||||
type: Literal["parameter"]
|
||||
|
||||
|
||||
class PauseStep(WorkflowStepBase):
|
||||
type: WorkflowModuleType = Field(WorkflowModuleType.pause, title="Type", description="The type of workflow module.")
|
||||
type: Literal["pause"]
|
||||
|
||||
|
||||
class ToolStep(ToolBasedWorkflowStep):
|
||||
type: WorkflowModuleType = Field(WorkflowModuleType.tool, title="Type", description="The type of workflow module.")
|
||||
class ToolStep(WorkflowStepBase):
|
||||
type: Literal["tool"]
|
||||
|
||||
|
||||
class SubworkflowStep(WorkflowStepBase):
|
||||
type: WorkflowModuleType = Field(
|
||||
WorkflowModuleType.subworkflow, title="Type", description="The type of workflow module."
|
||||
)
|
||||
workflow_id: DecodedDatabaseIdField = Field(
|
||||
type: Literal["subworkflow"]
|
||||
workflow_id: EncodedDatabaseIdField = Field(
|
||||
..., title="Workflow ID", description="The encoded ID of the workflow that will be run on this step."
|
||||
)
|
||||
|
||||
|
||||
@@ -197,7 +197,12 @@ class StoredWorkflowDetailed(StoredWorkflowSummary):
|
||||
ToolStep,
|
||||
SubworkflowStep,
|
||||
],
|
||||
] = Field({}, title="Steps", description="A dictionary with information about all the steps of the workflow.")
|
||||
] = Field(
|
||||
{},
|
||||
title="Steps",
|
||||
description="A dictionary with information about all the steps of the workflow.",
|
||||
discriminator="type",
|
||||
)
|
||||
importable: Optional[bool] = Field(
|
||||
...,
|
||||
title="Importable",
|
||||
|
||||
@@ -328,6 +328,15 @@ class TestWorkflowsApi(BaseWorkflowsApiTestCase, ChangeDatatypeTests):
|
||||
# workflow was created first in this instance.
|
||||
assert sorted(step["id"] for step in workflow["steps"].values()) != [0, 1, 2]
|
||||
|
||||
def test_show_subworkflow(self):
|
||||
workflow_id = self.workflow_populator.upload_yaml_workflow(WORKFLOW_NESTED_SIMPLE)
|
||||
workflow = self._get(f"workflows/{workflow_id}", {"style": "instance"}).json()
|
||||
assert isinstance(workflow["id"], str)
|
||||
subworkflow_step = workflow["steps"]["2"]
|
||||
assert subworkflow_step["type"] == "subworkflow"
|
||||
assert isinstance(subworkflow_step["workflow_id"], str)
|
||||
self._get(f"workflows/{subworkflow_step['workflow_id']}", {"style": "instance"}).json()
|
||||
|
||||
def test_show_invalid_key_is_400(self):
|
||||
show_response = self._get(f"workflows/{self._random_key()}")
|
||||
self._assert_status_code_is(show_response, 400)
|
||||
|
||||
Reference in New Issue
Block a user