1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { Dispatch } from "redux";
6 import { RootState } from "store/store";
7 import { ServiceRepository } from "services/services";
8 import { updateResources } from "store/resources/resources-actions";
9 import { dialogActions } from "store/dialog/dialog-actions";
10 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
11 import { projectPanelRunActions } from "store/project-panel/project-panel-action-bind";
12 import { navigateTo, navigateToRunProcess } from "store/navigation/navigation-action";
13 import { goToStep, runProcessPanelActions } from "store/run-process-panel/run-process-panel-actions";
14 import { getResource } from "store/resources/resources";
15 import { initialize } from "redux-form";
16 import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from "store/run-process-panel/run-process-panel-actions";
17 import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "store/run-process-panel/run-process-panel-actions";
18 import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from "models/process";
19 import { CommandInputParameter, getWorkflow, getWorkflowInputs, getWorkflowOutputs, WorkflowInputsData } from "models/workflow";
20 import { ProjectResource } from "models/project";
21 import { UserResource } from "models/user";
22 import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
23 import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
24 import { FilterBuilder } from "services/api/filter-builder";
25 import { selectedToArray } from "components/multiselect-toolbar/MultiselectToolbar";
26 import { Resource, ResourceKind } from "models/resource";
27 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
28 import { CommonResourceServiceError, getCommonResourceServiceError } from "services/common-service/common-resource-service";
29 import { getProcessPanelCurrentUuid } from "store/process-panel/process-panel";
30 import { getProjectPanelCurrentUuid } from "store/project-panel/project-panel";
32 export const loadContainers =
33 (containerUuids: string[], loadMounts: boolean = true) =>
34 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
36 filters: new FilterBuilder().addIn("uuid", containerUuids).getFilters(),
37 limit: containerUuids.length,
40 args.select = containerFieldsNoMounts;
42 const { items } = await services.containerService.list(args);
43 dispatch<any>(updateResources(items));
47 // Until the api supports unselecting fields, we need a list of all other fields to omit mounts
48 export const containerFieldsNoMounts = [
60 "interactive_session_started",
66 "modified_by_user_uuid",
69 "output_storage_classes",
74 "runtime_auth_scopes",
75 "runtime_constraints",
78 "scheduling_parameters",
85 export const cancelRunningWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
87 const process = await services.containerRequestService.update(uuid, { priority: 0 });
88 dispatch<any>(updateResources([process]));
89 if (process.containerUuid) {
90 const container = await services.containerService.get(process.containerUuid, false);
91 dispatch<any>(updateResources([container]));
95 throw new Error("Could not cancel the process.");
99 export const resumeOnHoldWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
101 const process = await services.containerRequestService.update(uuid, { priority: 500 });
102 dispatch<any>(updateResources([process]));
103 if (process.containerUuid) {
104 const container = await services.containerService.get(process.containerUuid, false);
105 dispatch<any>(updateResources([container]));
109 throw new Error("Could not resume the process.");
113 export const startWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
115 const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
117 dispatch<any>(updateResources([process]));
118 dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Process started", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
120 dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
123 dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
127 export const reRunProcess =
128 (processUuid: string, workflowUuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
129 const process = getResource<any>(processUuid)(getState().resources);
130 const workflows = getState().runProcessPanel.searchWorkflows;
131 const workflow = workflows.find(workflow => workflow.uuid === workflowUuid);
132 if (workflow && process) {
133 const mainWf = getWorkflow(process.mounts[MOUNT_PATH_CWL_WORKFLOW]);
135 mainWf.inputs = getInputs(process);
137 const stringifiedDefinition = JSON.stringify(process.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
138 const newWorkflow = { ...workflow, definition: stringifiedDefinition };
140 const owner = getResource<ProjectResource | UserResource>(workflow.ownerUuid)(getState().resources);
141 const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, owner };
142 dispatch<any>(initialize(RUN_PROCESS_BASIC_FORM, basicInitialData));
144 const advancedInitialData: RunProcessAdvancedFormData = {
145 description: process.description,
146 output: process.outputName,
147 runtime: process.schedulingParameters.max_run_time,
148 ram: process.runtimeConstraints.ram,
149 vcpus: process.runtimeConstraints.vcpus,
150 keep_cache_ram: process.runtimeConstraints.keep_cache_ram,
151 acr_container_image: process.containerImage,
153 dispatch<any>(initialize(RUN_PROCESS_ADVANCED_FORM, advancedInitialData));
155 dispatch<any>(navigateToRunProcess);
156 dispatch<any>(goToStep(1));
157 dispatch(runProcessPanelActions.SET_STEP_CHANGED(true));
158 dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(newWorkflow));
160 dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `You can't re-run this process`, kind: SnackbarKind.ERROR }));
165 * Fetches raw inputs from containerRequest mounts with fallback to properties
166 * Returns undefined if containerRequest not loaded
167 * Returns {} if inputs not found in mounts or props
169 export const getRawInputs = (data: any): WorkflowInputsData | undefined => {
173 const mountInput = data.mounts?.[MOUNT_PATH_CWL_INPUT]?.content;
174 const propsInput = data.properties?.cwl_input;
175 if (!mountInput && !propsInput) {
178 return mountInput || propsInput;
181 export const getInputs = (data: any): CommandInputParameter[] => {
182 // Definitions from mounts are needed so we return early if missing
183 if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
186 const content = getRawInputs(data) as any;
187 // Only escape if content is falsy to allow displaying definitions if no inputs are present
188 // (Don't check raw content length)
193 const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
195 ? inputs.map((it: any) => ({
199 default: content[it.id],
200 value: content[it.id.split("/").pop()] || [],
207 * Fetches raw outputs from containerRequest properties
208 * Assumes containerRequest is loaded
210 export const getRawOutputs = (data: any): any | undefined => {
211 if (!data || !data.properties || !data.properties.cwl_output) {
214 return data.properties.cwl_output;
217 export type InputCollectionMount = {
222 export const getInputCollectionMounts = (data: any): InputCollectionMount[] => {
223 if (!data || !data.mounts) {
226 return Object.keys(data.mounts)
231 .filter(mount => mount.kind === "collection" && mount.portable_data_hash && mount.path)
234 pdh: mount.portable_data_hash,
238 export const getOutputParameters = (data: any): CommandOutputParameter[] => {
239 if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
242 const outputs = getWorkflowOutputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
244 ? outputs.map((it: any) => ({
253 export const openRemoveProcessDialog =
254 (resource: ContextMenuResource, numOfProcesses: Number) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
255 const confirmationText =
257 ? "Are you sure you want to remove this process?"
258 : `Are you sure you want to remove these ${numOfProcesses} processes?`;
259 const titleText = numOfProcesses === 1 ? "Remove process permanently" : "Remove processes permanently";
262 dialogActions.OPEN_DIALOG({
263 id: REMOVE_PROCESS_DIALOG,
266 text: confirmationText,
267 confirmButtonLabel: "Remove",
275 export const REMOVE_PROCESS_DIALOG = "removeProcessDialog";
277 export const removeProcessPermanently = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
278 const currentProcessPanelUuid = getProcessPanelCurrentUuid(getState().router);
279 const currentProjectUuid = getProjectPanelCurrentUuid(getState());
280 const resource = getState().dialog.removeProcessDialog.data.resource;
281 const checkedList = getState().multiselect.checkedList;
283 const uuidsToRemove: string[] = resource.fromContextMenu ? [resource.uuid] : selectedToArray(checkedList);
285 //if no items in checkedlist, default to normal context menu behavior
286 if (!uuidsToRemove.length) uuidsToRemove.push(uuid);
288 const processesToRemove = uuidsToRemove
289 .map(uuid => getResource(uuid)(getState().resources) as Resource)
290 .filter(resource => resource.kind === ResourceKind.PROCESS);
292 await Promise.allSettled(processesToRemove.map(process => services.containerRequestService.delete(process.uuid, false)))
294 const failed = res.filter((promiseResult): promiseResult is PromiseRejectedResult => promiseResult.status === 'rejected');
295 const succeeded = res.filter((promiseResult): promiseResult is PromiseFulfilledResult<ContainerRequestResource> => promiseResult.status === 'fulfilled');
298 const accessDeniedError = failed.filter((promiseResult) => {
299 return getCommonResourceServiceError(promiseResult.reason) === CommonResourceServiceError.PERMISSION_ERROR_FORBIDDEN;
301 const genericError = failed.filter((promiseResult) => {
302 return getCommonResourceServiceError(promiseResult.reason) !== CommonResourceServiceError.PERMISSION_ERROR_FORBIDDEN;
305 // Show grouped errors for access or generic error
306 if (accessDeniedError.length) {
307 if (accessDeniedError.length > 1) {
308 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Access denied: ${accessDeniedError.length} items`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
310 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Access denied`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
313 if (genericError.length) {
314 if (genericError.length > 1) {
315 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Deletion failed: ${genericError.length} items`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
317 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Deletion failed`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
320 if (succeeded.length) {
321 if (succeeded.length > 1) {
322 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Removed ${succeeded.length} items`, hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
324 dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removed", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
328 // If currently viewing any of the deleted runs, navigate to home
329 if (currentProcessPanelUuid) {
330 const currentProcessDeleted = succeeded.find((promiseResult) => promiseResult.value.uuid === currentProcessPanelUuid);
331 if (currentProcessDeleted) {
332 dispatch<any>(navigateTo(currentProcessDeleted.value.ownerUuid));
336 // If currently viewing the parent project of any of the deleted runs, refresh project runs tab
337 if (currentProjectUuid && succeeded.find((promiseResult) => promiseResult.value.ownerUuid === currentProjectUuid)) {
338 dispatch(projectPanelRunActions.REQUEST_ITEMS());