Merge branch 'main' into 21720-material-ui-upgrade
[arvados.git] / services / workbench2 / src / store / subprocess-panel / subprocess-panel-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { Dispatch } from 'redux';
6 import { RootState } from 'store/store';
7 import { ServiceRepository } from 'services/services';
8 import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-action';
9 import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
10 import { ProgressBarStatus, ProgressBarCounts } from 'components/subprocess-progress-bar/subprocess-progress-bar';
11 import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
12 import { Process } from 'store/processes/process';
13 import { ProjectResource } from 'models/project';
14 import { getResource } from 'store/resources/resources';
15 import { ContainerRequestResource } from 'models/container-request';
16 import { Resource } from 'models/resource';
17
18 export const SUBPROCESS_PANEL_ID = "subprocessPanel";
19 export const SUBPROCESS_ATTRIBUTES_DIALOG = 'subprocessAttributesDialog';
20 export const subprocessPanelActions = bindDataExplorerActions(SUBPROCESS_PANEL_ID);
21
22 export const loadSubprocessPanel = () =>
23     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
24         dispatch(subprocessPanelActions.REQUEST_ITEMS());
25     };
26
27 /**
28  * Holds a ProgressBarData status type and process count result
29  */
30 type ProcessStatusCount = {
31     status: keyof ProgressBarCounts;
32     count: number;
33 };
34
35 /**
36  * Associates each of the limited progress bar segment types with an array of
37  * ProcessStatusFilterTypes to be combined when displayed
38  */
39 type ProcessStatusMap = Record<keyof ProgressBarCounts, ProcessStatusFilter[]>;
40
41 const statusMap: ProcessStatusMap = {
42         [ProcessStatusFilter.COMPLETED]: [ProcessStatusFilter.COMPLETED],
43         [ProcessStatusFilter.RUNNING]: [ProcessStatusFilter.RUNNING],
44         [ProcessStatusFilter.FAILED]: [ProcessStatusFilter.FAILED, ProcessStatusFilter.CANCELLED],
45         [ProcessStatusFilter.QUEUED]: [ProcessStatusFilter.QUEUED, ProcessStatusFilter.ONHOLD],
46 };
47
48 /**
49  * Utility type to hold a pair of associated progress bar status and process status
50  */
51 type ProgressBarStatusPair = {
52     barStatus: keyof ProcessStatusMap;
53     processStatus: ProcessStatusFilter;
54 };
55
56 /**
57  * Type guard to distinguish Processes from other Resources
58  * @param resource The item to check
59  * @returns if the resource is a Process
60  */
61 export const isProcess = <T extends Resource>(resource: T | Process | undefined): resource is Process => {
62     return !!resource && 'containerRequest' in resource;
63 };
64
65 /**
66  * Type guard to distinguish ContainerRequestResources from Resources
67  * @param resource The item to check
68  * @returns if the resource is a ContainerRequestResource
69  */
70 const isContainerRequest = <T extends Resource>(resource: T | ContainerRequestResource | undefined): resource is ContainerRequestResource => {
71     return !!resource && 'containerUuid' in resource;
72 };
73
74 export const fetchProcessProgressBarStatus = (parentResourceUuid: string, typeFilter?: string) =>
75     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<ProgressBarStatus | undefined> => {
76         const resources = getState().resources;
77         const parentResource = getResource<ProjectResource | ContainerRequestResource>(parentResourceUuid)(resources);
78
79         const requestContainerStatusCount = async (fb: FilterBuilder) => {
80             return await services.containerRequestService.list({
81                 limit: 0,
82                 offset: 0,
83                 filters: fb.getFilters(),
84             });
85         }
86
87         let baseFilter: string = "";
88         if (isContainerRequest(parentResource) && parentResource.containerUuid) {
89             // Prevent CR without containerUuid from generating baseFilter
90             baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', parentResource.containerUuid).getFilters();
91         } else if (parentResource && !isContainerRequest(parentResource)) {
92             // isCR type narrowing needed since CR without container may fall through
93             baseFilter = new FilterBuilder().addEqual('owner_uuid', parentResource.uuid).getFilters();
94         }
95
96         if (parentResource && baseFilter) {
97             // Add type filters from consumers that want to sync progress stats with filters
98             if (typeFilter) {
99                 baseFilter = joinFilters(baseFilter, typeFilter);
100             }
101
102             try {
103                 // Create return object
104                 let result: ProgressBarCounts = {
105                     [ProcessStatusFilter.COMPLETED]: 0,
106                     [ProcessStatusFilter.RUNNING]: 0,
107                     [ProcessStatusFilter.FAILED]: 0,
108                     [ProcessStatusFilter.QUEUED]: 0,
109                 }
110
111                 // Create array of promises that returns the status associated with the item count
112                 // Helps to make the requests simultaneously while preserving the association with the status key as a typed key
113                 const promises = (Object.keys(statusMap) as Array<keyof ProcessStatusMap>)
114                     // Split statusMap into pairs of progress bar status and process status
115                     .reduce((acc, curr) => [...acc, ...statusMap[curr].map(processStatus => ({barStatus: curr, processStatus}))], [] as ProgressBarStatusPair[])
116                     .map(async (statusPair: ProgressBarStatusPair): Promise<ProcessStatusCount> => {
117                         // For each status pair, request count and return bar status and count
118                         const { barStatus, processStatus } = statusPair;
119                         const filter = buildProcessStatusFilters(new FilterBuilder(baseFilter), processStatus);
120                         const count = (await requestContainerStatusCount(filter)).itemsAvailable;
121                         if (count === undefined) return Promise.reject();
122                         return {status: barStatus, count};
123                     });
124
125                 // Simultaneously requests each status count and apply them to the return object
126                 (await Promise.all(promises)).forEach((singleResult) => {
127                     result[singleResult.status] += singleResult.count;
128                 });
129
130                 // CR polling is handled in progress bar based on store updates
131                 // This bool triggers polling without causing a final fetch when disabled
132                 // The shouldPoll logic here differs slightly from shouldPollProcess:
133                 //   * Process gets websocket updates through the store so using isProcessRunning
134                 //     ignores Queued
135                 //   * In projects, we get no websocket updates on CR state changes so we treat
136                 //     Queued processes as running in order to let polling keep us up to date
137                 //     when anything transitions to Running. This also means that a project with
138                 //     CRs in a stopped state won't start polling if CRs are started elsewhere
139                 const shouldPollProject = isContainerRequest(parentResource)
140                     ? false
141                     : (result[ProcessStatusFilter.RUNNING] + result[ProcessStatusFilter.QUEUED]) > 0;
142
143                 return {counts: result, shouldPollProject};
144             } catch (e) {
145                 return undefined;
146             }
147         }
148         return undefined;
149     };