21225: Add project workflow runs tab.
[arvados.git] / services / workbench2 / src / store / resource-type-filters / resource-type-filters.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { difference, pipe, values, includes, __ } from 'lodash/fp';
6 import { createTree, setNode, TreeNodeStatus, TreeNode, Tree } from 'models/tree';
7 import { DataTableFilterItem, DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
8 import { ResourceKind } from 'models/resource';
9 import { FilterBuilder } from 'services/api/filter-builder';
10 import { getSelectedNodes } from 'models/tree';
11 import { CollectionType } from 'models/collection';
12 import { GroupContentsResourcePrefix } from 'services/groups-service/groups-service';
13 import { ContainerState } from 'models/container';
14 import { ContainerRequestState } from 'models/container-request';
15
16 export enum ProcessStatusFilter {
17     ALL = 'All',
18     RUNNING = 'Running',
19     FAILED = 'Failed',
20     COMPLETED = 'Completed',
21     CANCELLED = 'Cancelled',
22     ONHOLD = 'On hold',
23     QUEUED = 'Queued'
24 }
25
26 export enum ObjectTypeFilter {
27     PROJECT = 'Project',
28     WORKFLOW = 'Workflow',
29     COLLECTION = 'Data collection',
30     DEFINITION = 'Definition',
31 }
32
33 export enum GroupTypeFilter {
34     PROJECT = 'Project (normal)',
35     FILTER_GROUP = 'Filter group',
36 }
37
38 export enum CollectionTypeFilter {
39     GENERAL_COLLECTION = 'General',
40     OUTPUT_COLLECTION = 'Output',
41     LOG_COLLECTION = 'Log',
42     INTERMEDIATE_COLLECTION = 'Intermediate',
43 }
44
45 export enum ProcessTypeFilter {
46     MAIN_PROCESS = 'Runs',
47     CHILD_PROCESS = 'Intermediate Steps',
48 }
49
50 const initFilter = (name: string, parent = '', isSelected?: boolean, isExpanded?: boolean) =>
51     setNode<DataTableFilterItem>({
52         id: name,
53         value: { name },
54         parent,
55         children: [],
56         active: false,
57         selected: isSelected !== undefined ? isSelected : true,
58         initialState: isSelected !== undefined ? isSelected : true,
59         expanded: isExpanded !== undefined ? isExpanded : false,
60         status: TreeNodeStatus.LOADED,
61     });
62
63 export const getSimpleObjectTypeFilters = pipe(
64     (): DataTableFilters => createTree<DataTableFilterItem>(),
65     initFilter(ObjectTypeFilter.PROJECT),
66     initFilter(ObjectTypeFilter.WORKFLOW),
67     initFilter(ObjectTypeFilter.COLLECTION),
68     initFilter(ObjectTypeFilter.DEFINITION),
69 );
70
71 // Using pipe() with more than 7 arguments makes the return type be 'any',
72 // causing compile issues.
73 export const getInitialResourceTypeFilters = pipe(
74     (): DataTableFilters => createTree<DataTableFilterItem>(),
75     pipe(
76         initFilter(ObjectTypeFilter.PROJECT, '', true, true),
77         initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT),
78         initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT),
79     ),
80     pipe(
81         initFilter(ObjectTypeFilter.WORKFLOW, '', false, true),
82         initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.WORKFLOW),
83         initFilter(ProcessTypeFilter.CHILD_PROCESS, ObjectTypeFilter.WORKFLOW, false),
84         initFilter(ObjectTypeFilter.DEFINITION, ObjectTypeFilter.WORKFLOW),
85     ),
86     pipe(
87         initFilter(ObjectTypeFilter.COLLECTION, '', true, true),
88         initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
89         initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
90         initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION, false),
91         initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION, false),
92     ),
93
94 );
95
96 /**
97  * Resource type filters for Data tab (excludes main/sub process runs)
98  */
99 export const getInitialDataResourceTypeFilters = pipe(
100     (): DataTableFilters => createTree<DataTableFilterItem>(),
101     pipe(
102         initFilter(ObjectTypeFilter.PROJECT, '', true, true),
103         initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT),
104         initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT),
105     ),
106     pipe(
107         initFilter(ObjectTypeFilter.WORKFLOW, '', false, true),
108         initFilter(ObjectTypeFilter.DEFINITION, ObjectTypeFilter.WORKFLOW),
109     ),
110     pipe(
111         initFilter(ObjectTypeFilter.COLLECTION, '', true, true),
112         initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
113         initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
114         initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION, false),
115         initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION, false),
116     ),
117
118 );
119
120 // Using pipe() with more than 7 arguments makes the return type be 'any',
121 // causing compile issues.
122 export const getInitialSearchTypeFilters = pipe(
123     (): DataTableFilters => createTree<DataTableFilterItem>(),
124     pipe(
125         initFilter(ObjectTypeFilter.PROJECT, '', true, true),
126         initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT),
127         initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT),
128     ),
129     pipe(
130         initFilter(ObjectTypeFilter.WORKFLOW, '', false, true),
131         initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.WORKFLOW, false),
132         initFilter(ProcessTypeFilter.CHILD_PROCESS, ObjectTypeFilter.WORKFLOW, false),
133         initFilter(ObjectTypeFilter.DEFINITION, ObjectTypeFilter.WORKFLOW, false),
134     ),
135     pipe(
136         initFilter(ObjectTypeFilter.COLLECTION, '', true, true),
137         initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
138         initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
139         initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION, false),
140         initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION, false),
141     ),
142 );
143
144 export const getInitialProcessTypeFilters = pipe(
145     (): DataTableFilters => createTree<DataTableFilterItem>(),
146     initFilter(ProcessTypeFilter.MAIN_PROCESS),
147     initFilter(ProcessTypeFilter.CHILD_PROCESS, '', false)
148 );
149
150 export const getInitialProcessStatusFilters = pipe(
151     (): DataTableFilters => createTree<DataTableFilterItem>(),
152     pipe(
153         initFilter(ProcessStatusFilter.ALL, '', true),
154         initFilter(ProcessStatusFilter.ONHOLD, '', false),
155         initFilter(ProcessStatusFilter.QUEUED, '', false),
156         initFilter(ProcessStatusFilter.RUNNING, '', false),
157         initFilter(ProcessStatusFilter.COMPLETED, '', false),
158         initFilter(ProcessStatusFilter.CANCELLED, '', false),
159         initFilter(ProcessStatusFilter.FAILED, '', false),
160     ),
161 );
162
163 export const getTrashPanelTypeFilters = pipe(
164     (): DataTableFilters => createTree<DataTableFilterItem>(),
165     initFilter(ObjectTypeFilter.PROJECT),
166     initFilter(ObjectTypeFilter.COLLECTION),
167     initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
168     initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
169     initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION),
170     initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
171 );
172
173 const createFiltersBuilder = (filters: DataTableFilters) =>
174     ({ fb: new FilterBuilder(), selectedFilters: getSelectedNodes(filters) });
175
176 const getMatchingFilters = (values: string[], filters: TreeNode<DataTableFilterItem>[]) =>
177     filters
178         .map(f => f.id)
179         .filter(includes(__, values));
180
181 const objectTypeToResourceKind = (type: ObjectTypeFilter) => {
182     switch (type) {
183         case ObjectTypeFilter.PROJECT:
184             return ResourceKind.PROJECT;
185         case ObjectTypeFilter.WORKFLOW:
186             return ResourceKind.PROCESS;
187         case ObjectTypeFilter.COLLECTION:
188             return ResourceKind.COLLECTION;
189         case ObjectTypeFilter.DEFINITION:
190             return ResourceKind.WORKFLOW;
191     }
192 };
193
194 const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => {
195     const groupFilters = getMatchingFilters(values(GroupTypeFilter), selectedFilters);
196     const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters);
197     const processFilters = getMatchingFilters(values(ProcessTypeFilter), selectedFilters);
198     const typeFilters = pipe(
199         () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)),
200         set => groupFilters.length > 0
201             ? set.add(ObjectTypeFilter.PROJECT)
202             : set,
203         set => collectionFilters.length > 0
204             ? set.add(ObjectTypeFilter.COLLECTION)
205             : set,
206         set => processFilters.length > 0
207             ? set.add(ObjectTypeFilter.WORKFLOW)
208             : set,
209         set => Array.from(set)
210     )();
211
212     return {
213         fb: typeFilters.length > 0
214             ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind))
215             : fb.addIsA('uuid', ResourceKind.NONE),
216         selectedFilters,
217     };
218 };
219
220 const collectionTypeToPropertyValue = (type: CollectionTypeFilter) => {
221     switch (type) {
222         case CollectionTypeFilter.GENERAL_COLLECTION:
223             return CollectionType.GENERAL;
224         case CollectionTypeFilter.OUTPUT_COLLECTION:
225             return CollectionType.OUTPUT;
226         case CollectionTypeFilter.LOG_COLLECTION:
227             return CollectionType.LOG;
228         case CollectionTypeFilter.INTERMEDIATE_COLLECTION:
229             return CollectionType.INTERMEDIATE;
230         default:
231             return CollectionType.GENERAL;
232     }
233 };
234
235 const serializeCollectionTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
236     () => getMatchingFilters(values(CollectionTypeFilter), selectedFilters),
237     filters => filters.map(collectionTypeToPropertyValue),
238     mappedFilters => ({
239         fb: buildCollectionTypeFilters({ fb, filters: mappedFilters }),
240         selectedFilters
241     })
242 )();
243
244 const COLLECTION_TYPES = values(CollectionType);
245
246 const NON_GENERAL_COLLECTION_TYPES = difference(COLLECTION_TYPES, [CollectionType.GENERAL]);
247
248 const COLLECTION_PROPERTIES_PREFIX = `${GroupContentsResourcePrefix.COLLECTION}.properties`;
249
250 const buildCollectionTypeFilters = ({ fb, filters }: { fb: FilterBuilder, filters: CollectionType[] }) => {
251     switch (true) {
252         case filters.length === 0 || filters.length === COLLECTION_TYPES.length:
253             return fb;
254         case includes(CollectionType.GENERAL, filters):
255             return fb.addNotIn('type', difference(NON_GENERAL_COLLECTION_TYPES, filters), COLLECTION_PROPERTIES_PREFIX);
256         default:
257             return fb.addIn('type', filters, COLLECTION_PROPERTIES_PREFIX);
258     }
259 };
260
261 const serializeGroupTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
262     () => getMatchingFilters(values(GroupTypeFilter), selectedFilters),
263     filters => filters,
264     mappedFilters => ({
265         fb: buildGroupTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
266         selectedFilters
267     })
268 )();
269
270 const GROUP_TYPES = values(GroupTypeFilter);
271
272 const buildGroupTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilder, filters: string[], use_prefix: boolean }) => {
273     switch (true) {
274         case filters.length === 0 || filters.length === GROUP_TYPES.length:
275             return fb;
276         case includes(GroupTypeFilter.PROJECT, filters):
277             return fb.addEqual('groups.group_class', 'project');
278         case includes(GroupTypeFilter.FILTER_GROUP, filters):
279             return fb.addEqual('groups.group_class', 'filter');
280         default:
281             return fb;
282     }
283 };
284
285 const serializeProcessTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
286     () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
287     filters => filters,
288     mappedFilters => ({
289         fb: buildProcessTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
290         selectedFilters
291     })
292 )();
293
294 const PROCESS_TYPES = values(ProcessTypeFilter);
295 const PROCESS_PREFIX = GroupContentsResourcePrefix.PROCESS;
296
297 const buildProcessTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilder, filters: string[], use_prefix: boolean }) => {
298     switch (true) {
299         case filters.length === 0 || filters.length === PROCESS_TYPES.length:
300             return fb;
301         case includes(ProcessTypeFilter.MAIN_PROCESS, filters):
302             return fb.addEqual('requesting_container_uuid', null, use_prefix ? PROCESS_PREFIX : '');
303         case includes(ProcessTypeFilter.CHILD_PROCESS, filters):
304             return fb.addDistinct('requesting_container_uuid', null, use_prefix ? PROCESS_PREFIX : '');
305         default:
306             return fb;
307     }
308 };
309
310 /**
311  * Serializes general resource type filters with prefix for group contents API
312  */
313 export const serializeResourceTypeFilters = pipe(
314     createFiltersBuilder,
315     serializeObjectTypeFilters,
316     serializeGroupTypeFilters,
317     serializeCollectionTypeFilters,
318     serializeProcessTypeFilters,
319     ({ fb }) => fb.getFilters(),
320 );
321
322 export const serializeOnlyProcessTypeFilters = pipe(
323     createFiltersBuilder,
324     ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
325         () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
326         filters => filters,
327         mappedFilters => ({
328             fb: buildProcessTypeFilters({ fb, filters: mappedFilters, use_prefix: false }),
329             selectedFilters
330         })
331     )(),
332     ({ fb }) => fb.getFilters(),
333 );
334
335 /**
336  * Serializes process type filters with prefix for group contents request
337  * Uses buildProcessTypeFilters to disable filters when no process type is selected
338  */
339 export const serializeProcessTypeGroupContentsFilters = pipe(
340     createFiltersBuilder,
341     ({fb, selectedFilters }): ReturnType<typeof createFiltersBuilder> => ({
342             fb: fb.addIsA('uuid', [ResourceKind.PROCESS]),
343             selectedFilters,
344     }),
345     ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
346         () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
347         filters => filters,
348         mappedFilters => ({
349             fb: buildProcessTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
350             selectedFilters
351         })
352     )(),
353     ({ fb }) => fb.getFilters(),
354 );
355
356 export const serializeSimpleObjectTypeFilters = (filters: Tree<DataTableFilterItem>) => {
357     return getSelectedNodes(filters)
358         .map(f => f.id)
359         .map(objectTypeToResourceKind);
360 };
361
362 export const buildProcessStatusFilters = (fb: FilterBuilder, activeStatusFilter: string, resourcePrefix?: string): FilterBuilder => {
363     switch (activeStatusFilter) {
364         case ProcessStatusFilter.ONHOLD: {
365             fb.addDistinct('state', ContainerRequestState.FINAL, resourcePrefix);
366             fb.addEqual('priority', '0', resourcePrefix);
367             fb.addIn('container.state', [ContainerState.QUEUED, ContainerState.LOCKED], resourcePrefix);
368             break;
369         }
370         case ProcessStatusFilter.COMPLETED: {
371             fb.addEqual('container.state', ContainerState.COMPLETE, resourcePrefix);
372             fb.addEqual('container.exit_code', '0', resourcePrefix);
373             break;
374         }
375         case ProcessStatusFilter.FAILED: {
376             fb.addEqual('container.state', ContainerState.COMPLETE, resourcePrefix);
377             fb.addDistinct('container.exit_code', '0', resourcePrefix);
378             break;
379         }
380         case ProcessStatusFilter.QUEUED: {
381             fb.addIn('container.state', [ContainerState.QUEUED, ContainerState.LOCKED], resourcePrefix);
382             fb.addDistinct('priority', '0', resourcePrefix);
383             break;
384         }
385         case ProcessStatusFilter.CANCELLED:
386         case ProcessStatusFilter.RUNNING: {
387             fb.addEqual('container.state', activeStatusFilter, resourcePrefix);
388             break;
389         }
390     }
391     return fb;
392 };