diff --git a/client/src/api/invocations.ts b/client/src/api/invocations.ts index 495a009c660c..bd2e1ec0fe1b 100644 --- a/client/src/api/invocations.ts +++ b/client/src/api/invocations.ts @@ -6,12 +6,16 @@ import { ApiResponse, components, fetcher } from "./schema"; export type WorkflowInvocationElementView = components["schemas"]["WorkflowInvocationElementView"]; export type WorkflowInvocationCollectionView = components["schemas"]["WorkflowInvocationCollectionView"]; +export type WorkflowInvocationStepStatesView = components["schemas"]["WorkflowInvocationStepStatesView"]; export type InvocationJobsSummary = components["schemas"]["InvocationJobsResponse"]; export type InvocationStep = components["schemas"]["InvocationStep"]; export const invocationsFetcher = fetcher.path("/api/invocations").method("get").create(); -export type WorkflowInvocation = WorkflowInvocationElementView | WorkflowInvocationCollectionView; +export type WorkflowInvocation = + | WorkflowInvocationElementView + | WorkflowInvocationCollectionView + | WorkflowInvocationStepStatesView; export interface WorkflowInvocationStep { id: string; @@ -34,6 +38,15 @@ export async function fetchInvocationDetails(params: { id: string }): Promise; } +export async function fetchInvocationStepStateDetails(params: { + id: string; +}): Promise> { + const { data } = await axios.get(`${getAppRoot()}api/invocations/${params.id}?view=step_states`); + return { + data, + } as ApiResponse; +} + export async function fetchInvocationJobsSummary(params: { id: string }): Promise> { const { data } = await axios.get(`${getAppRoot()}api/invocations/${params.id}/jobs_summary`); return { diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index e4de5ba426b9..562f7502b71e 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -7869,7 +7869,7 @@ export interface components { * InvocationSerializationView * @enum {string} */ - InvocationSerializationView: "element" | "collection"; + InvocationSerializationView: "element" | "collection" | "step_states"; /** * InvocationSortByEnum * @enum {string} @@ -12917,7 +12917,8 @@ export interface components { WorkflowInvocationResponse: | components["schemas"]["WorkflowInvocationElementView"] | components["schemas"]["LegacyWorkflowInvocationElementView"] - | components["schemas"]["WorkflowInvocationCollectionView"]; + | components["schemas"]["WorkflowInvocationCollectionView"] + | components["schemas"]["WorkflowInvocationStepStatesView"]; /** WorkflowInvocationStateSummary */ WorkflowInvocationStateSummary: { /** @@ -12945,6 +12946,60 @@ export interface components { [key: string]: number | undefined; }; }; + /** WorkflowInvocationStepStatesView */ + WorkflowInvocationStepStatesView: { + /** + * Create Time + * Format: date-time + * @description The time and date this item was created. + */ + create_time: string; + /** + * History ID + * @description The encoded ID of the history associated with the invocation. + * @example 0123456789ABCDEF + */ + history_id: string; + /** + * ID + * @description The encoded ID of the workflow invocation. + * @example 0123456789ABCDEF + */ + id: string; + /** + * Model class + * @description The name of the database model class. + * @constant + */ + model_class: "WorkflowInvocation"; + /** + * Invocation state + * @description State of workflow invocation. + */ + state: components["schemas"]["InvocationState"]; + /** + * Steps + * @description Steps of the workflow invocation. + */ + steps: components["schemas"]["InvocationStep"][]; + /** + * Update Time + * Format: date-time + * @description The last time and date this item was updated. + */ + update_time: string; + /** + * UUID + * @description Universal unique identifier of the workflow invocation. + */ + uuid?: string | string | null; + /** + * Workflow ID + * @description The encoded Workflow ID associated with the invocation. + * @example 0123456789ABCDEF + */ + workflow_id: string; + }; /** WriteInvocationStoreToPayload */ WriteInvocationStoreToPayload: { /** @@ -18602,6 +18657,7 @@ export interface operations { show_invocation_api_invocations__invocation_id__get: { /** Get detailed description of a workflow invocation. */ parameters: { + /** @description View to be passed to the serializer */ /** @description Include details for individual invocation steps and populate a steps attribute in the resulting dictionary. */ /** * @description Populate the invocation step state with the job state instead of the invocation step state. @@ -18610,6 +18666,7 @@ export interface operations { * are not the mapped over step outputs but the individual job outputs. */ query?: { + view?: string | null; step_details?: boolean; legacy_job_state?: boolean; }; diff --git a/client/src/components/Workflow/InvocationsListState.vue b/client/src/components/Workflow/InvocationsListState.vue index 138a0f32ea0e..32b6d1a34844 100644 --- a/client/src/components/Workflow/InvocationsListState.vue +++ b/client/src/components/Workflow/InvocationsListState.vue @@ -22,7 +22,7 @@ const { jobStatesSummary, monitorState, clearStateMonitor, -} = useInvocationState(toRef(props, "invocationId")); +} = useInvocationState(toRef(props, "invocationId"), true); onMounted(monitorState); onBeforeUnmount(clearStateMonitor); diff --git a/client/src/components/WorkflowInvocationState/usesInvocationState.ts b/client/src/components/WorkflowInvocationState/usesInvocationState.ts index f59f3bc4357c..f77aeb05ddde 100644 --- a/client/src/components/WorkflowInvocationState/usesInvocationState.ts +++ b/client/src/components/WorkflowInvocationState/usesInvocationState.ts @@ -8,11 +8,15 @@ import { isTerminal, jobCount } from "./util"; type OptionalInterval = ReturnType | null; -export function useInvocationState(invocationId: Ref) { +export function useInvocationState(invocationId: Ref, fetchMinimal: boolean = false) { const invocationStore = useInvocationStore(); const invocation = computed(() => { - return invocationStore.getInvocationById(invocationId.value); + if (fetchMinimal) { + return invocationStore.getInvocationWithStepStatesById(invocationId.value); + } else { + return invocationStore.getInvocationById(invocationId.value); + } }); let stepStatesInterval: OptionalInterval = null; @@ -48,7 +52,11 @@ export function useInvocationState(invocationId: Ref) { async function pollStepStatesUntilTerminal() { if (!invocation.value || !invocationSchedulingTerminal.value) { - await invocationStore.fetchInvocationForId({ id: invocationId.value }); + if (fetchMinimal) { + await invocationStore.fetchInvocationWithStepStatesForId({ id: invocationId.value }); + } else { + await invocationStore.fetchInvocationForId({ id: invocationId.value }); + } stepStatesInterval = setTimeout(pollStepStatesUntilTerminal, 3000); } } diff --git a/client/src/stores/invocationStore.ts b/client/src/stores/invocationStore.ts index 836667d22661..a20c431d90e8 100644 --- a/client/src/stores/invocationStore.ts +++ b/client/src/stores/invocationStore.ts @@ -2,6 +2,7 @@ import { defineStore } from "pinia"; import { fetchInvocationDetails, + fetchInvocationStepStateDetails, fetchInvocationJobsSummary, fetchInvocationStep, type WorkflowInvocation, @@ -14,6 +15,9 @@ export const useInvocationStore = defineStore("invocationStore", () => { const { getItemById: getInvocationById, fetchItemById: fetchInvocationForId } = useKeyedCache(fetchInvocationDetails); + const { getItemById: getInvocationWithStepStatesById, fetchItemById: fetchInvocationWithStepStatesForId } = + useKeyedCache(fetchInvocationStepStateDetails); + const { getItemById: getInvocationJobsSummaryById, fetchItemById: fetchInvocationJobsSummaryForId } = useKeyedCache(fetchInvocationJobsSummary); @@ -27,5 +31,7 @@ export const useInvocationStore = defineStore("invocationStore", () => { fetchInvocationJobsSummaryForId, getInvocationStepById, fetchInvocationStepById, + getInvocationWithStepStatesById, + fetchInvocationWithStepStatesForId, }; }); diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 235eb89ee3e8..1f76aa4e6853 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -8653,11 +8653,12 @@ def _serialize(self, id_encoder, serialization_options): return invocation_attrs def to_dict(self, view="collection", value_mapper=None, step_details=False, legacy_job_state=False): - rval = super().to_dict(view=view, value_mapper=value_mapper) + base_view = view if view != "step_states" else "collection" + rval = super().to_dict(view=base_view, value_mapper=value_mapper) if rval["state"] is None: # bugs could result in no state being set rval["state"] = self.states.FAILED - if view == "element": + if view in ["element", "step_states"]: steps = [] for step in self.steps: if step_details: @@ -8678,7 +8679,7 @@ def to_dict(self, view="collection", value_mapper=None, step_details=False, lega else: steps.append(v) rval["steps"] = steps - + if view == "element": inputs = {} for input_item_association in self.input_datasets + self.input_dataset_collections: if input_item_association.history_content_type == "dataset": diff --git a/lib/galaxy/schema/invocation.py b/lib/galaxy/schema/invocation.py index dffb7873bd00..f87bd6f67c5b 100644 --- a/lib/galaxy/schema/invocation.py +++ b/lib/galaxy/schema/invocation.py @@ -566,6 +566,10 @@ class WorkflowInvocationCollectionView(Model, WithModelClass): model_class: INVOCATION_MODEL_CLASS = ModelClassField(INVOCATION_MODEL_CLASS) +class WorkflowInvocationStepStatesView(WorkflowInvocationCollectionView): + steps: List[InvocationStep] = Field(default=..., title="Steps", description="Steps of the workflow invocation.") + + class BaseWorkflowInvocationElementView(WorkflowInvocationCollectionView): inputs: Dict[str, InvocationInput] = Field( default=..., title="Inputs", description="Input datasets/dataset collections of the workflow invocation." @@ -603,7 +607,12 @@ class WorkflowInvocationElementView(BaseWorkflowInvocationElementView): class WorkflowInvocationResponse(RootModel): root: Annotated[ - Union[WorkflowInvocationElementView, LegacyWorkflowInvocationElementView, WorkflowInvocationCollectionView], + Union[ + WorkflowInvocationElementView, + LegacyWorkflowInvocationElementView, + WorkflowInvocationCollectionView, + WorkflowInvocationStepStatesView, + ], Field(union_mode="left_to_right"), ] @@ -614,6 +623,8 @@ def from_dict(as_dict: Dict[str, Any], view: "InvocationSerializationView", lega # performant, and will likely yield clearer error messages. if view == InvocationSerializationView.collection: root = WorkflowInvocationCollectionView(**as_dict) + elif view == InvocationSerializationView.step_states: + root = WorkflowInvocationStepStatesView(**as_dict) elif legacy_job_state: root = LegacyWorkflowInvocationElementView(**as_dict) else: @@ -658,6 +669,7 @@ class CreateInvocationFromStore(StoreContentSource): class InvocationSerializationView(str, Enum): element = "element" collection = "collection" + step_states = "step_states" # collection + steps - for monitoring, lighter than element class InvocationSerializationParams(BaseModel): diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index b29263c70a5c..7d117e555848 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -1405,11 +1405,12 @@ def show_invocation( self, invocation_id: InvocationIDPathParam, trans: ProvidesUserContext = DependsOnTrans, + view: SerializationViewQueryParam = None, step_details: StepDetailQueryParam = False, legacy_job_state: LegacyJobStateQueryParam = False, ) -> WorkflowInvocationResponse: serialization_params = InvocationSerializationParams( - step_details=step_details, legacy_job_state=legacy_job_state + view=view, step_details=step_details, legacy_job_state=legacy_job_state ) return self.invocations_service.show(trans, invocation_id, serialization_params, eager=True)