Skip to content

Commit

Permalink
Display workflow invocation counts.
Browse files Browse the repository at this point in the history
Performance: Workflows render fine without the data, data is fetched asynchronously from other queries, joins only happen across a couple of fields that are all indexed, avoiding serializing objects and such - the SQL is quite low-level.
  • Loading branch information
jmchilton committed Feb 17, 2024
1 parent 85ba4d5 commit 194889f
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 11 deletions.
39 changes: 39 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,10 @@ export interface paths {
/** Add the deleted flag to a workflow. */
delete: operations["delete_workflow_api_workflows__workflow_id__delete"];
};
"/api/workflows/{workflow_id}/counts": {
/** Get state counts for accessible workflow. */
get: operations["invocation_state_counts_api_workflows__workflow_id__counts_get"];
};
"/api/workflows/{workflow_id}/disable_link_access": {
/**
* Makes this item inaccessible by a URL link.
Expand Down Expand Up @@ -9559,6 +9563,10 @@ export interface components {
*/
url: string;
};
/** RootModel[Dict[str, int]] */
RootModel_Dict_str__int__: {
[key: string]: number | undefined;
};
/** SearchJobsPayload */
SearchJobsPayload: {
/**
Expand Down Expand Up @@ -21503,6 +21511,37 @@ export interface operations {
};
};
};
invocation_state_counts_api_workflows__workflow_id__counts_get: {
/** Get state counts for accessible workflow. */
parameters: {
/** @description Is provided workflow id for Workflow instead of StoredWorkflow? */
query?: {
instance?: boolean | null;
};
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
header?: {
"run-as"?: string | null;
};
/** @description The encoded database identifier of the Stored Workflow. */
path: {
workflow_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["RootModel_Dict_str__int__"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
disable_link_access_api_workflows__workflow_id__disable_link_access_put: {
/**
* Makes this item inaccessible by a URL link.
Expand Down
2 changes: 2 additions & 0 deletions client/src/api/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fetcher } from "@/api/schema";

export const workflowsFetcher = fetcher.path("/api/workflows").method("get").create();

export const invocationCountsFetcher = fetcher.path("/api/workflows/{workflow_id}/counts").method("get").create();
33 changes: 33 additions & 0 deletions client/src/components/Workflow/WorkflowInvocationsCount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faClock, faSitemap } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { onMounted,ref } from "vue";
import { useRouter } from "vue-router/composables";
import { invocationCountsFetcher } from "@/api/workflows";
import localize from "@/utils/localization";
library.add(faClock, faSitemap);
Expand All @@ -16,14 +18,45 @@ const props = defineProps<Props>();
const router = useRouter();
const count = ref<number | undefined>(undefined);
async function initCounts() {
const { data } = await invocationCountsFetcher({workflow_id: props.workflow.id});
let allCounts = 0;
for (const stateCount of Object.values(data)) {
if (stateCount ) {
allCounts += stateCount;
}
}
count.value = allCounts;
}
onMounted(initCounts)
function onInvocations() {
router.push(`/workflows/${props.workflow.id}/invocations`);
}
</script>

<template>
<div class="workflow-invocations-count d-flex align-items-center flex-gapx-1">
<BBadge
v-if="count != undefined"
v-b-tooltip.hover
pill
:title="localize('View workflow invocations')"
class="outline-badge cursor-pointer list-view"
@click="onInvocations">
<FontAwesomeIcon :icon="faSitemap" />
<span v-if="count > 0">
workflow runs:
{{ count }}
</span>
<span v-else> workflow never run </span>
</BBadge>

<BButton
v-else
v-b-tooltip.hover
:title="localize('View workflow invocations')"
class="inline-icon-button"
Expand Down
15 changes: 15 additions & 0 deletions lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
DatasetCollectionPopulatedState,
DatasetState,
DatasetValidatedState,
InvocationsStateCounts,
JobState,
)
from galaxy.schema.workflow.comments import WorkflowCommentModel
Expand Down Expand Up @@ -7476,6 +7477,20 @@ def copy_tags_from(self, target_user, source_workflow):
new_swta.user = target_user
self.tags.append(new_swta)

def invocation_counts(self) -> InvocationsStateCounts:
sa_session = object_session(self)
stmt = (
select([WorkflowInvocation.state, func.count(WorkflowInvocation.state)])
.select_from(StoredWorkflow)
.join(Workflow, Workflow.stored_workflow_id == StoredWorkflow.id)
.join(WorkflowInvocation, WorkflowInvocation.workflow_id == Workflow.id)
.group_by(WorkflowInvocation.state)
.where(StoredWorkflow.id == self.id)
)
rows = sa_session.execute(stmt).all()
rows_as_dict = dict(r for r in rows if r[0] is not None)
return InvocationsStateCounts(rows_as_dict)

def to_dict(self, view="collection", value_mapper=None):
rval = super().to_dict(view=view, value_mapper=value_mapper)
rval["latest_workflow_uuid"] = (lambda uuid: str(uuid) if self.latest_workflow.uuid else None)(
Expand Down
3 changes: 3 additions & 0 deletions lib/galaxy/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2404,6 +2404,9 @@ class WorkflowStepLayoutPosition(Model):
width: int = Field(..., title="Width", description="Width of the box in pixels.")


InvocationsStateCounts = RootModel[Dict[str, int]]


class WorkflowStepToExportBase(Model):
id: int = Field(
...,
Expand Down
31 changes: 23 additions & 8 deletions lib/galaxy/webapps/galaxy/api/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
AsyncFile,
AsyncTaskResultSummary,
InvocationSortByEnum,
InvocationsStateCounts,
SetSlugPayload,
ShareWithPayload,
ShareWithStatus,
Expand Down Expand Up @@ -883,6 +884,14 @@ def __get_stored_workflow(self, trans, workflow_id, **kwd):
),
]

InvocationsInstanceQueryParam = Annotated[
Optional[bool],
Query(
title="Instance",
description="Is provided workflow id for Workflow instead of StoredWorkflow?",
),
]

DeletedQueryParam: bool = Query(
default=False, title="Display deleted", description="Whether to restrict result to deleted workflows."
)
Expand Down Expand Up @@ -1127,6 +1136,20 @@ def show_versions(
):
return self.service.get_versions(trans, workflow_id, instance)

@router.get(
"/api/workflows/{workflow_id}/counts",
summary="Get state counts for accessible workflow.",
name="invocation_state_counts",
operation_id="workflows__invocation_counts",
)
def invocation_counts(
self,
workflow_id: StoredWorkflowIDPathParam,
instance: InvocationsInstanceQueryParam = False,
trans: ProvidesUserContext = DependsOnTrans,
) -> InvocationsStateCounts:
return self.service.invocation_counts(trans, workflow_id, instance)

@router.get(
"/api/workflows/menu",
summary="Get workflows present in the tools panel.",
Expand Down Expand Up @@ -1248,14 +1271,6 @@ def get_workflow_menu(
]


InvocationsInstanceQueryParam = Annotated[
Optional[bool],
Query(
title="Instance",
description="Is provided workflow id for Workflow instead of StoredWorkflow?",
),
]

CreateInvocationsFromStoreBody = Annotated[
CreateInvocationsFromStorePayload,
Body(
Expand Down
16 changes: 13 additions & 3 deletions lib/galaxy/webapps/galaxy/services/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
WorkflowSerializer,
WorkflowsManager,
)
from galaxy.schema.schema import WorkflowIndexQueryPayload
from galaxy.model import StoredWorkflow
from galaxy.schema.schema import (
InvocationsStateCounts,
WorkflowIndexQueryPayload,
)
from galaxy.util.tool_shed.tool_shed_registry import Registry
from galaxy.webapps.galaxy.services.base import ServiceBase
from galaxy.webapps.galaxy.services.sharable import ShareableService
Expand Down Expand Up @@ -111,15 +115,21 @@ def undelete(self, trans, workflow_id):
self._workflows_manager.check_security(trans, workflow_to_undelete)
self._workflows_manager.undelete(workflow_to_undelete)

def get_versions(self, trans, workflow_id, instance):
stored_workflow = self._workflows_manager.get_stored_accessible_workflow(
def get_versions(self, trans, workflow_id, instance: bool):
stored_workflow: StoredWorkflow = self._workflows_manager.get_stored_accessible_workflow(
trans, workflow_id, by_stored_id=not instance
)
return [
{"version": i, "update_time": w.update_time.isoformat(), "steps": len(w.steps)}
for i, w in enumerate(reversed(stored_workflow.workflows))
]

def invocation_counts(self, trans, workflow_id, instance: bool) -> InvocationsStateCounts:
stored_workflow: StoredWorkflow = self._workflows_manager.get_stored_accessible_workflow(
trans, workflow_id, by_stored_id=not instance
)
return stored_workflow.invocation_counts()

def get_workflow_menu(self, trans, payload):
ids_in_menu = [x.stored_workflow_id for x in trans.user.stored_workflow_menu_entries]
workflows = self._get_workflows_list(
Expand Down
18 changes: 18 additions & 0 deletions test/unit/data/test_galaxy_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,23 @@ def test_workflows(self):
annotations = copied_workflow.steps[0].annotations
assert len(annotations) == 1

stored_workflow = loaded_workflow.stored_workflow
counts = stored_workflow.invocation_counts()
assert counts

workflow_invocation_0 = _invocation_for_workflow(user, loaded_workflow)
workflow_invocation_1 = _invocation_for_workflow(user, loaded_workflow)
workflow_invocation_1.state = "scheduled"
self.model.session.add(workflow_invocation_0)
self.model.session.add(workflow_invocation_1)
# self.persist(workflow_invocation_0)
# self.persist(workflow_invocation_1)
self.model.session.flush()
counts = stored_workflow.invocation_counts()
print(counts)
assert counts.root["new"] == 2
assert counts.root["scheduled"] == 1

def test_role_creation(self):
security_agent = GalaxyRBACAgent(self.model)

Expand Down Expand Up @@ -1164,6 +1181,7 @@ def _invocation_for_workflow(user, workflow):
workflow_invocation = galaxy.model.WorkflowInvocation()
workflow_invocation.workflow = workflow
workflow_invocation.history = h1
workflow_invocation.state = "new"
return workflow_invocation


Expand Down

0 comments on commit 194889f

Please sign in to comment.