Merge branch '21225-project-panel-tabs' into main. Closes #21225
[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 = 'Workflow Runs',
47     CHILD_PROCESS = 'Workflow 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, '', true, 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 /**
195  * object to resource which clasifies workflow category as only registered workflows, not processes
196  * Used for data tab that excludes process runs
197  */
198 const dataObjectTypeToResourceKind = (type: ObjectTypeFilter) => {
199     switch (type) {
200         case ObjectTypeFilter.PROJECT:
201             return ResourceKind.PROJECT;
202         case ObjectTypeFilter.COLLECTION:
203             return ResourceKind.COLLECTION;
204         case ObjectTypeFilter.WORKFLOW:
205         case ObjectTypeFilter.DEFINITION:
206             return ResourceKind.WORKFLOW;
207     }
208 };
209
210 const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => {
211     const groupFilters = getMatchingFilters(values(GroupTypeFilter), selectedFilters);
212     const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters);
213     const processFilters = getMatchingFilters(values(ProcessTypeFilter), selectedFilters);
214     const typeFilters = pipe(
215         () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)),
216         set => groupFilters.length > 0
217             ? set.add(ObjectTypeFilter.PROJECT)
218             : set,
219         set => collectionFilters.length > 0
220             ? set.add(ObjectTypeFilter.COLLECTION)
221             : set,
222         set => processFilters.length > 0
223             ? set.add(ObjectTypeFilter.WORKFLOW)
224             : set,
225         set => Array.from(set)
226     )();
227
228     return {
229         fb: typeFilters.length > 0
230             ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind))
231             : fb.addIsA('uuid', ResourceKind.NONE),
232         selectedFilters,
233     };
234 };
235
236 /**
237  * Serialize only data object types, excludes processes
238  */
239 const serializeDataObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => {
240     const groupFilters = getMatchingFilters(values(GroupTypeFilter), selectedFilters);
241     const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters);
242     const typeFilters = pipe(
243         () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)),
244         set => groupFilters.length > 0
245             ? set.add(ObjectTypeFilter.PROJECT)
246             : set,
247         set => collectionFilters.length > 0
248             ? set.add(ObjectTypeFilter.COLLECTION)
249             : set,
250         set => Array.from(set)
251     )();
252
253     return {
254         fb: typeFilters.length > 0
255             ? fb.addIsA('uuid', typeFilters.map(dataObjectTypeToResourceKind))
256             : fb.addIsA('uuid', ResourceKind.NONE),
257         selectedFilters,
258     };
259 };
260
261 const collectionTypeToPropertyValue = (type: CollectionTypeFilter) => {
262     switch (type) {
263         case CollectionTypeFilter.GENERAL_COLLECTION:
264             return CollectionType.GENERAL;
265         case CollectionTypeFilter.OUTPUT_COLLECTION:
266             return CollectionType.OUTPUT;
267         case CollectionTypeFilter.LOG_COLLECTION:
268             return CollectionType.LOG;
269         case CollectionTypeFilter.INTERMEDIATE_COLLECTION:
270             return CollectionType.INTERMEDIATE;
271         default:
272             return CollectionType.GENERAL;
273     }
274 };
275
276 const serializeCollectionTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
277     () => getMatchingFilters(values(CollectionTypeFilter), selectedFilters),
278     filters => filters.map(collectionTypeToPropertyValue),
279     mappedFilters => ({
280         fb: buildCollectionTypeFilters({ fb, filters: mappedFilters }),
281         selectedFilters
282     })
283 )();
284
285 const COLLECTION_TYPES = values(CollectionType);
286
287 const NON_GENERAL_COLLECTION_TYPES = difference(COLLECTION_TYPES, [CollectionType.GENERAL]);
288
289 const COLLECTION_PROPERTIES_PREFIX = `${GroupContentsResourcePrefix.COLLECTION}.properties`;
290
291 const buildCollectionTypeFilters = ({ fb, filters }: { fb: FilterBuilder, filters: CollectionType[] }) => {
292     switch (true) {
293         case filters.length === 0 || filters.length === COLLECTION_TYPES.length:
294             return fb;
295         case includes(CollectionType.GENERAL, filters):
296             return fb.addNotIn('type', difference(NON_GENERAL_COLLECTION_TYPES, filters), COLLECTION_PROPERTIES_PREFIX);
297         default:
298             return fb.addIn('type', filters, COLLECTION_PROPERTIES_PREFIX);
299     }
300 };
301
302 const serializeGroupTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
303     () => getMatchingFilters(values(GroupTypeFilter), selectedFilters),
304     filters => filters,
305     mappedFilters => ({
306         fb: buildGroupTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
307         selectedFilters
308     })
309 )();
310
311 const GROUP_TYPES = values(GroupTypeFilter);
312
313 const buildGroupTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilder, filters: string[], use_prefix: boolean }) => {
314     switch (true) {
315         case filters.length === 0 || filters.length === GROUP_TYPES.length:
316             return fb;
317         case includes(GroupTypeFilter.PROJECT, filters):
318             return fb.addEqual('groups.group_class', 'project');
319         case includes(GroupTypeFilter.FILTER_GROUP, filters):
320             return fb.addEqual('groups.group_class', 'filter');
321         default:
322             return fb;
323     }
324 };
325
326 const serializeProcessTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
327     () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
328     filters => filters,
329     mappedFilters => ({
330         fb: buildProcessTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
331         selectedFilters
332     })
333 )();
334
335 const PROCESS_TYPES = values(ProcessTypeFilter);
336 const PROCESS_PREFIX = GroupContentsResourcePrefix.PROCESS;
337
338 const buildProcessTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilder, filters: string[], use_prefix: boolean }) => {
339     switch (true) {
340         case filters.length === 0 || filters.length === PROCESS_TYPES.length:
341             return fb;
342         case includes(ProcessTypeFilter.MAIN_PROCESS, filters):
343             return fb.addEqual('requesting_container_uuid', null, use_prefix ? PROCESS_PREFIX : '');
344         case includes(ProcessTypeFilter.CHILD_PROCESS, filters):
345             return fb.addDistinct('requesting_container_uuid', null, use_prefix ? PROCESS_PREFIX : '');
346         default:
347             return fb;
348     }
349 };
350
351 /**
352  * Serializes general resource type filters with prefix for group contents API
353  */
354 export const serializeResourceTypeFilters = pipe(
355     createFiltersBuilder,
356     serializeObjectTypeFilters,
357     serializeGroupTypeFilters,
358     serializeCollectionTypeFilters,
359     serializeProcessTypeFilters,
360     ({ fb }) => fb.getFilters(),
361 );
362
363 /**
364  * Serializes data tab resource type filters with prefix for group contents API
365  */
366 export const serializeDataResourceTypeFilters = pipe(
367     createFiltersBuilder,
368     serializeDataObjectTypeFilters,
369     serializeGroupTypeFilters,
370     serializeCollectionTypeFilters,
371     ({ fb }) => fb.getFilters(),
372 );
373
374 export const serializeOnlyProcessTypeFilters = pipe(
375     createFiltersBuilder,
376     ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
377         () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
378         filters => filters,
379         mappedFilters => ({
380             fb: buildProcessTypeFilters({ fb, filters: mappedFilters, use_prefix: false }),
381             selectedFilters
382         })
383     )(),
384     ({ fb }) => fb.getFilters(),
385 );
386
387 /**
388  * Serializes process type filters with prefix for group contents request
389  * Uses buildProcessTypeFilters to disable filters when no process type is selected
390  */
391 export const serializeProcessTypeGroupContentsFilters = pipe(
392     createFiltersBuilder,
393     ({fb, selectedFilters }): ReturnType<typeof createFiltersBuilder> => ({
394             fb: fb.addIsA('uuid', [ResourceKind.PROCESS]),
395             selectedFilters,
396     }),
397     ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
398         () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
399         filters => filters,
400         mappedFilters => ({
401             fb: buildProcessTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
402             selectedFilters
403         })
404     )(),
405     ({ fb }) => fb.getFilters(),
406 );
407
408 export const serializeSimpleObjectTypeFilters = (filters: Tree<DataTableFilterItem>) => {
409     return getSelectedNodes(filters)
410         .map(f => f.id)
411         .map(objectTypeToResourceKind);
412 };
413
414 export const buildProcessStatusFilters = (fb: FilterBuilder, activeStatusFilter: string, resourcePrefix?: string): FilterBuilder => {
415     switch (activeStatusFilter) {
416         case ProcessStatusFilter.ONHOLD: {
417             fb.addDistinct('state', ContainerRequestState.FINAL, resourcePrefix);
418             fb.addEqual('priority', '0', resourcePrefix);
419             fb.addIn('container.state', [ContainerState.QUEUED, ContainerState.LOCKED], resourcePrefix);
420             break;
421         }
422         case ProcessStatusFilter.COMPLETED: {
423             fb.addEqual('container.state', ContainerState.COMPLETE, resourcePrefix);
424             fb.addEqual('container.exit_code', '0', resourcePrefix);
425             break;
426         }
427         case ProcessStatusFilter.FAILED: {
428             fb.addEqual('container.state', ContainerState.COMPLETE, resourcePrefix);
429             fb.addDistinct('container.exit_code', '0', resourcePrefix);
430             break;
431         }
432         case ProcessStatusFilter.QUEUED: {
433             fb.addIn('container.state', [ContainerState.QUEUED, ContainerState.LOCKED], resourcePrefix);
434             fb.addDistinct('priority', '0', resourcePrefix);
435             break;
436         }
437         case ProcessStatusFilter.CANCELLED:
438         case ProcessStatusFilter.RUNNING: {
439             fb.addEqual('container.state', activeStatusFilter, resourcePrefix);
440             break;
441         }
442     }
443     return fb;
444 };