Skip to content

Commit 7aff982

Browse files
Add create time filters to invocation grid
Fixes galaxyproject#15863
1 parent feaa3b0 commit 7aff982

6 files changed

Lines changed: 105 additions & 6 deletions

File tree

client/src/components/Grid/GridInvocation.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ function refreshTable() {
110110
:grid-message="props.headerMessage"
111111
:no-data-message="effectiveNoInvocationsMessage"
112112
:extra-props="extraProps"
113+
standalone
113114
:embedded="forStoredWorkflow || forHistory || forBatch">
114115
<template v-slot:expanded="{ rowData }">
115116
<span class="mb-2" :data-invocation-id="rowData.id">

client/src/components/Grid/GridList.vue

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ interface Props {
4747
usernameSearch?: string;
4848
// any extra props to be passed to `getData`
4949
extraProps?: Record<string, unknown>;
50+
/** Whether the filter menu should be standalone or not */
51+
standalone?: boolean;
5052
}
5153
5254
const props = withDefaults(defineProps<Props>(), {
@@ -168,14 +170,19 @@ async function getGridData() {
168170
return;
169171
}
170172
try {
173+
// TODO: Feels a little hacky, better to type `getData` to accept `filterQueryParams` as an argument
174+
const extraProps = {
175+
...props.extraProps,
176+
filterQueryParams: validatedFilterQueryParamDict(),
177+
};
171178
const offset = props.limit * (currentPage.value ? currentPage.value - 1 : 0);
172179
const [responseData, responseTotal] = await props.gridConfig.getData(
173180
offset,
174181
props.limit,
175182
validatedFilterText(),
176183
sortBy.value,
177184
sortDesc.value,
178-
props.extraProps
185+
extraProps
179186
);
180187
gridData.value = responseData;
181188
totalRows.value = responseTotal;
@@ -250,6 +257,14 @@ function onFilter(filter?: string) {
250257
}
251258
}
252259
260+
function onSearch(filters: Record<string, string | boolean>, text?: string) {
261+
if (props.standalone) {
262+
filterText.value = text || "";
263+
}
264+
265+
// TODO: We can handle other on-search variables here if needed
266+
}
267+
253268
// Select multiple rows
254269
function onSelect(rowData: RowData) {
255270
if (selected.value.has(rowData)) {
@@ -295,6 +310,21 @@ function validatedFilterText() {
295310
return filterClass?.getFilterText(validFilters.value || {}, false) || "";
296311
}
297312
313+
/**
314+
* Returns an object with filter keys ("q") and values ("qv") for backend queries.
315+
* Example: { q: ["name", "create_time"], qv: ["test", "2023-01-01"] }
316+
*/
317+
function validatedFilterQueryParamDict() {
318+
const initDict = filterClass?.getQueryDict(filterText.value) || {};
319+
if (Object.keys(initDict).length === 0) {
320+
return {};
321+
}
322+
return {
323+
q: Object.keys(initDict),
324+
qv: Object.values(initDict),
325+
};
326+
}
327+
298328
/**
299329
* Initialize grid data
300330
*/
@@ -355,9 +385,11 @@ watch(operationMessage, () => {
355385
:placeholder="`search ${gridConfig.plural.toLowerCase()}`"
356386
:filter-class="filterClass"
357387
:filter-text.sync="filterText"
388+
:menu-type="props.standalone ? 'standalone' : undefined"
358389
:loading="initDataLoading || resultsLoading"
359390
:show-advanced.sync="showAdvanced"
360-
view="compact" />
391+
view="compact"
392+
@on-search="onSearch" />
361393
</div>
362394
<LoadingSpan v-if="initDataLoading" />
363395
<span v-else-if="!isAvailable || hasInvalidFilters">

client/src/components/Grid/configs/invocations.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { WorkflowInvocation } from "@/api/invocations";
66
import type { StoredWorkflowDetailed } from "@/api/workflows";
77
import { useHistoryStore } from "@/stores/historyStore";
88
import { useWorkflowStore } from "@/stores/workflowStore";
9+
import Filtering, { compare, toDate, type ValidFilter } from "@/utils/filtering";
910
import _l from "@/utils/localization";
1011
import { rethrowSimple } from "@/utils/simple-error";
1112

@@ -29,6 +30,7 @@ export async function getData(
2930
sort_desc: boolean,
3031
extraProps?: Record<string, unknown>
3132
) {
33+
const filterQueryParams = extraProps?.filterQueryParams || {};
3234
const params = {
3335
limit,
3436
offset,
@@ -37,6 +39,10 @@ export async function getData(
3739
include_nested_invocations: false,
3840
} as Record<string, unknown>;
3941

42+
if (Object.keys(filterQueryParams).length > 0) {
43+
Object.assign(params, filterQueryParams);
44+
}
45+
4046
if (extraProps && "include_terminal" in extraProps) {
4147
params["include_terminal"] = extraProps["include_terminal"];
4248
}
@@ -150,13 +156,27 @@ const fields: FieldArray = [
150156
},
151157
];
152158

159+
/**
160+
* Declare filter options
161+
*/
162+
const validFilters: Record<string, ValidFilter<number>> = {
163+
create_time: {
164+
placeholder: "creation time",
165+
type: Date,
166+
handler: compare("create_time", "le", toDate),
167+
isRangeInput: true,
168+
menuItem: true,
169+
},
170+
};
171+
153172
/**
154173
* Grid configuration
155174
*/
156175
const gridConfig: GridConfig = {
157176
id: "invocations-grid",
158177
actions: actions,
159178
fields: fields,
179+
filtering: new Filtering(validFilters, undefined, true, false),
160180
getData: getData,
161181
plural: "Workflow Invocations",
162182
sortBy: "create_time",

lib/galaxy/managers/workflows.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
)
5656
from galaxy.job_execution.actions.post import ActionBox
5757
from galaxy.managers import (
58+
base,
5859
deletable,
5960
sharable,
6061
)
@@ -517,6 +518,7 @@ def build_invocations_query(
517518
sort_desc=None,
518519
include_nested_invocations=True,
519520
check_ownership=True,
521+
filters=[],
520522
) -> Tuple[List, int]:
521523
"""Get invocations owned by the current user."""
522524

@@ -545,6 +547,11 @@ def build_invocations_query(
545547
)
546548
stmt = stmt.where(~subquery)
547549

550+
# Apply any filters from the filters array
551+
for filter_item in filters:
552+
if hasattr(filter_item, "filter_type") and filter_item.filter_type == "orm":
553+
stmt = stmt.where(filter_item.filter)
554+
548555
total_matches = get_count(trans.sa_session, stmt)
549556

550557
if sort_by:
@@ -2142,6 +2149,19 @@ def get_workflow_by_trs_id_and_version(
21422149
return sa_session.execute(stmnt.order_by(model.StoredWorkflow.id.desc()).limit(1)).scalar()
21432150

21442151

2152+
class InvocationFilters(base.ModelFilterParser):
2153+
model_class = model.WorkflowInvocation
2154+
model_manager_class = WorkflowsManager
2155+
2156+
def _add_parsers(self):
2157+
super()._add_parsers()
2158+
self.orm_filter_parsers.update(
2159+
{
2160+
"create_time": {"op": ("le", "ge", "gt", "lt"), "val": self.parse_date},
2161+
}
2162+
)
2163+
2164+
21452165
class RefactorRequest(RefactorActions):
21462166
style: str = "export"
21472167

lib/galaxy/webapps/galaxy/api/workflows.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from fastapi import (
1818
Body,
19+
Depends,
1920
Path,
2021
Query,
2122
Response,
@@ -51,6 +52,7 @@
5152
WorkflowUpdateOptions,
5253
)
5354
from galaxy.model.item_attrs import UsesAnnotations
55+
from galaxy.schema import FilterQueryParams
5456
from galaxy.schema.fields import DecodedDatabaseIdField
5557
from galaxy.schema.invocation import (
5658
CreateInvocationFromStore,
@@ -112,7 +114,10 @@
112114
Router,
113115
search_query_param,
114116
)
115-
from galaxy.webapps.galaxy.api.common import SerializationViewQueryParam
117+
from galaxy.webapps.galaxy.api.common import (
118+
get_filter_query_params,
119+
SerializationViewQueryParam,
120+
)
116121
from galaxy.webapps.galaxy.services.base import (
117122
ConsumesModelStores,
118123
ServesExportStores,
@@ -1335,6 +1340,7 @@ def index_invocations(
13351340
user_id: UserIdQueryParam = None,
13361341
sort_by: InvocationsSortByQueryParam = None,
13371342
sort_desc: InvocationsSortDescQueryParam = False,
1343+
filter_query_params: FilterQueryParams = Depends(get_filter_query_params),
13381344
include_terminal: InvocationsIncludeTerminalQueryParam = True,
13391345
limit: InvocationsLimitQueryParam = 20,
13401346
offset: InvocationsOffsetQueryParam = None,
@@ -1365,7 +1371,12 @@ def index_invocations(
13651371
view=view,
13661372
step_details=step_details,
13671373
)
1368-
invocations, total_matches = self.invocations_service.index(trans, invocation_payload, serialization_params)
1374+
invocations, total_matches = self.invocations_service.index(
1375+
trans,
1376+
invocation_payload,
1377+
serialization_params,
1378+
filter_query_params=filter_query_params,
1379+
)
13691380
response.headers["total_matches"] = str(total_matches)
13701381
return invocations
13711382

lib/galaxy/webapps/galaxy/services/invocations.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@
2929
invocation_job_source_iter,
3030
summarize_metrics,
3131
)
32-
from galaxy.managers.workflows import WorkflowsManager
32+
from galaxy.managers.workflows import (
33+
InvocationFilters,
34+
WorkflowsManager,
35+
)
3336
from galaxy.model import (
3437
HistoryDatasetAssociation,
3538
HistoryDatasetCollectionAssociation,
3639
WorkflowInvocation,
3740
WorkflowInvocationStep,
3841
WorkflowRequestInputParameter,
3942
)
43+
from galaxy.schema import FilterQueryParams
4044
from galaxy.schema.fields import DecodedDatabaseIdField
4145
from galaxy.schema.invocation import (
4246
CreateInvocationFromStore,
@@ -89,15 +93,21 @@ def __init__(
8993
security: IdEncodingHelper,
9094
histories_manager: HistoryManager,
9195
workflows_manager: WorkflowsManager,
96+
filters: InvocationFilters,
9297
short_term_storage_allocator: ShortTermStorageAllocator,
9398
):
9499
super().__init__(security=security)
95100
self._histories_manager = histories_manager
96101
self._workflows_manager = workflows_manager
102+
self.filters = filters
97103
self.short_term_storage_allocator = short_term_storage_allocator
98104

99105
def index(
100-
self, trans, invocation_payload: InvocationIndexPayload, serialization_params: InvocationSerializationParams
106+
self,
107+
trans,
108+
invocation_payload: InvocationIndexPayload,
109+
serialization_params: InvocationSerializationParams,
110+
filter_query_params: FilterQueryParams,
101111
) -> Tuple[List[WorkflowInvocationResponse], int]:
102112
workflow_id = invocation_payload.workflow_id
103113
if invocation_payload.instance:
@@ -121,6 +131,10 @@ def index(
121131
# Get all invocations if user is admin (and user_id is None).
122132
# xref https://github.com/galaxyproject/galaxy/pull/13862#discussion_r865732297
123133
user_id = invocation_payload.user_id
134+
135+
filter_params = self.filters.build_filter_params(filter_query_params)
136+
filters = self.filters.parse_filters(filter_params)
137+
124138
invocations, total_matches = self._workflows_manager.build_invocations_query(
125139
trans,
126140
stored_workflow_id=invocation_payload.workflow_id,
@@ -134,6 +148,7 @@ def index(
134148
sort_desc=invocation_payload.sort_desc,
135149
include_nested_invocations=invocation_payload.include_nested_invocations,
136150
check_ownership=False,
151+
filters=filters,
137152
)
138153
invocation_dict = self.serialize_workflow_invocations(invocations, serialization_params)
139154
return invocation_dict, total_matches

0 commit comments

Comments
 (0)