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

This commit is contained in:
Dannon Baker
2024-03-15 14:12:11 -04:00
23 changed files with 408 additions and 249 deletions
+56 -32
View File
@@ -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: {
/**
+10 -1
View File
@@ -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 />
+2 -4
View File
@@ -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">
+54 -72
View File
@@ -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>
+1
View File
@@ -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];
+84 -32
View File
@@ -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,
};
});
+3 -3
View File
@@ -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 };
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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",
@@ -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
View File
@@ -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."
)
+6 -1
View File
@@ -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",
+9
View File
@@ -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)