Merge branch '21764-project-picker-crash' into main. Closes #21764
[arvados.git] / services / workbench2 / src / store / processes / process.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { ContainerRequestResource, ContainerRequestState } from '../../models/container-request';
6 import { ContainerResource, ContainerState } from '../../models/container';
7 import { ResourcesState, getResource } from 'store/resources/resources';
8 import { filterResources } from '../resources/resources';
9 import { ResourceKind, Resource, extractUuidKind } from 'models/resource';
10 import { getTimeDiff } from 'common/formatters';
11 import { ArvadosTheme } from 'common/custom-theme';
12
13 export interface Process {
14     containerRequest: ContainerRequestResource;
15     container?: ContainerResource;
16 }
17
18 export enum ProcessStatus {
19     CANCELLED = 'Cancelled',
20     COMPLETED = 'Completed',
21     DRAFT = 'Draft',
22     FAILING = 'Failing',
23     FAILED = 'Failed',
24     ONHOLD = 'On hold',
25     QUEUED = 'Queued',
26     RUNNING = 'Running',
27     WARNING = 'Warning',
28     UNKNOWN = 'Unknown',
29     REUSED = 'Reused',
30     CANCELLING = 'Cancelling',
31 }
32
33 /**
34  * Gets a process from the store using container request uuid
35  * @param uuid container request associated with process
36  * @returns a Process object with containerRequest and optional container or undefined
37  */
38 export const getProcess = (uuid: string) => (resources: ResourcesState): Process | undefined => {
39     if (extractUuidKind(uuid) === ResourceKind.CONTAINER_REQUEST) {
40         const containerRequest = getResource<ContainerRequestResource>(uuid)(resources);
41         if (containerRequest) {
42             if (containerRequest.containerUuid) {
43                 const container = getResource<ContainerResource>(containerRequest.containerUuid)(resources);
44                 if (container) {
45                     return { containerRequest, container };
46                 }
47             }
48             return { containerRequest };
49         }
50     }
51     return;
52 };
53
54 export const getSubprocesses = (uuid: string) => (resources: ResourcesState) => {
55     const process = getProcess(uuid)(resources);
56     if (process && process.container) {
57         const containerRequests = filterResources(isSubprocess(process.container.uuid))(resources) as ContainerRequestResource[];
58         return containerRequests.reduce((subprocesses, { uuid }) => {
59             const process = getProcess(uuid)(resources);
60             return process
61                 ? [...subprocesses, process]
62                 : subprocesses;
63         }, []);
64     }
65     return [];
66 };
67
68 export const getProcessRuntime = ({ container }: Process) => {
69     if (container) {
70         if (container.startedAt === null) {
71             return 0;
72         }
73         if (container.finishedAt === null) {
74             // Count it from now
75             return new Date().getTime() - new Date(container.startedAt).getTime();
76         }
77         return getTimeDiff(container.finishedAt, container.startedAt);
78     } else {
79         return 0;
80     }
81 };
82
83
84 export const getProcessStatusStyles = (status: string, theme: ArvadosTheme): React.CSSProperties => {
85     let color = theme.customs.colors.grey500;
86     let running = false;
87     switch (status) {
88         case ProcessStatus.RUNNING:
89             color = theme.customs.colors.green800;
90             running = true;
91             break;
92         case ProcessStatus.COMPLETED:
93         case ProcessStatus.REUSED:
94             color = theme.customs.colors.green800;
95             break;
96         case ProcessStatus.WARNING:
97             color = theme.customs.colors.green800;
98             running = true;
99             break;
100         case ProcessStatus.FAILING:
101             color = theme.customs.colors.red900;
102             running = true;
103             break;
104         case ProcessStatus.CANCELLING:
105             color = theme.customs.colors.red900;
106             running = true;
107             break;
108         case ProcessStatus.CANCELLED:
109         case ProcessStatus.FAILED:
110             color = theme.customs.colors.red900;
111             break;
112         case ProcessStatus.QUEUED:
113             color = theme.customs.colors.grey600;
114             running = true;
115             break;
116         default:
117             color = theme.customs.colors.grey600;
118             break;
119     }
120
121     // Using color and running we build the text, border, and background style properties
122     return {
123         // Set background color when not running, otherwise use white
124         backgroundColor: running ? theme.palette.common.white : color,
125         // Set text color to status color when running, else use white text for solid button
126         color: running ? color : theme.palette.common.white,
127         // Set border color when running, else omit the style entirely
128         ...(running ? { border: `2px solid ${color}` } : {}),
129     };
130 };
131
132 export const getProcessStatus = ({ containerRequest, container }: Process): ProcessStatus => {
133     switch (true) {
134         case containerRequest.containerUuid && !container:
135             return ProcessStatus.UNKNOWN;
136
137         case containerRequest.state === ContainerRequestState.UNCOMMITTED:
138             return ProcessStatus.DRAFT;
139
140         case containerRequest.state === ContainerRequestState.FINAL &&
141             container?.state === ContainerState.RUNNING:
142             // It is about to be completed but we haven't
143             // gotten the updated container record yet,
144             // if we don't catch this and show it as "Running"
145             // it will flicker "Cancelled" briefly
146             return ProcessStatus.RUNNING;
147
148         case containerRequest.state === ContainerRequestState.FINAL &&
149             container?.state !== ContainerState.COMPLETE:
150             // Request was finalized before its container started (or the
151             // container was cancelled)
152             return ProcessStatus.CANCELLED;
153
154         case container && container.state === ContainerState.COMPLETE:
155             if (container?.exitCode === 0) {
156                 if (containerRequest && container.finishedAt) {
157                     // don't compare on createdAt because the container can
158                     // have a slightly earlier creation time when it is created
159                     // in the same transaction as the container request.
160                     // use finishedAt because most people will assume "reused" means
161                     // no additional work needed to be done, it's possible
162                     // to share a running container but calling it "reused" in that case
163                     // is more likely to just be confusing.
164                     const finishedAt = new Date(container.finishedAt).getTime();
165                     const createdAt = new Date(containerRequest.createdAt).getTime();
166                     if (finishedAt < createdAt) {
167                         return ProcessStatus.REUSED;
168                     }
169                 }
170                 return ProcessStatus.COMPLETED;
171             }
172             return ProcessStatus.FAILED;
173
174         case container?.state === ContainerState.CANCELLED:
175             return ProcessStatus.CANCELLED;
176
177         case container?.state === ContainerState.QUEUED ||
178             container?.state === ContainerState.LOCKED:
179             if (containerRequest.priority === 0) {
180                 return ProcessStatus.ONHOLD;
181             }
182             return ProcessStatus.QUEUED;
183
184         case container?.state === ContainerState.RUNNING:
185             if (container?.priority === 0) {
186                 return ProcessStatus.CANCELLING;
187             }
188             if (!!container?.runtimeStatus.error) {
189                 return ProcessStatus.FAILING;
190             }
191             if (!!container?.runtimeStatus.warning) {
192                 return ProcessStatus.WARNING;
193             }
194             return ProcessStatus.RUNNING;
195
196         default:
197             return ProcessStatus.UNKNOWN;
198     }
199 };
200
201 export const isProcessRunning = ({ container }: Process): boolean => (
202     container?.state === ContainerState.RUNNING
203 );
204
205 export const isProcessRunnable = ({ containerRequest }: Process): boolean => (
206     containerRequest.state === ContainerRequestState.UNCOMMITTED
207 );
208
209 export const isProcessResumable = ({ containerRequest, container }: Process): boolean => (
210     containerRequest.state === ContainerRequestState.COMMITTED &&
211     containerRequest.priority === 0 &&
212     // Don't show run button when container is present & running or cancelled
213     !(container && (container.state === ContainerState.RUNNING ||
214         container.state === ContainerState.CANCELLED ||
215         container.state === ContainerState.COMPLETE))
216 );
217
218 export const isProcessCancelable = ({ containerRequest, container }: Process): boolean => (
219     containerRequest.priority !== null &&
220     containerRequest.priority > 0 &&
221     container !== undefined &&
222     (container.state === ContainerState.QUEUED ||
223         container.state === ContainerState.LOCKED ||
224         container.state === ContainerState.RUNNING)
225 );
226
227 const isSubprocess = (containerUuid: string) => (resource: Resource) =>
228     resource.kind === ResourceKind.CONTAINER_REQUEST
229     && (resource as ContainerRequestResource).requestingContainerUuid === containerUuid;