21128: Merge commit 'adbbc9e3c7a36d39b30f403555ee5889e32adcc0' into 21128-toolbar...
[arvados.git] / services / workbench2 / src / store / workbench / workbench-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 { getUserUuid } from "common/getuser";
8 import { loadDetailsPanel } from "store/details-panel/details-panel-action";
9 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
10 import { favoritePanelActions, loadFavoritePanel } from "store/favorite-panel/favorite-panel-action";
11 import { getProjectPanelCurrentUuid, setIsProjectPanelTrashed } from "store/project-panel/project-panel-action";
12 import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
13 import {
14     activateSidePanelTreeItem,
15     initSidePanelTree,
16     loadSidePanelTreeProjects,
17     SidePanelTreeCategory,
18     SIDE_PANEL_TREE, 
19 } from "store/side-panel-tree/side-panel-tree-actions";
20 import { updateResources } from "store/resources/resources-actions";
21 import { projectPanelColumns } from "views/project-panel/project-panel";
22 import { favoritePanelColumns } from "views/favorite-panel/favorite-panel";
23 import { matchRootRoute } from "routes/routes";
24 import {
25     setGroupDetailsBreadcrumbs,
26     setGroupsBreadcrumbs,
27     setProcessBreadcrumbs,
28     setSharedWithMeBreadcrumbs,
29     setSidePanelBreadcrumbs,
30     setTrashBreadcrumbs,
31     setUsersBreadcrumbs,
32     setMyAccountBreadcrumbs,
33     setUserProfileBreadcrumbs,
34     setInstanceTypesBreadcrumbs,
35     setVirtualMachinesBreadcrumbs,
36     setVirtualMachinesAdminBreadcrumbs,
37     setRepositoriesBreadcrumbs,
38 } from "store/breadcrumbs/breadcrumbs-actions";
39 import { navigateTo, navigateToRootProject } from "store/navigation/navigation-action";
40 import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
41 import { ServiceRepository } from "services/services";
42 import { getResource } from "store/resources/resources";
43 import * as projectCreateActions from "store/projects/project-create-actions";
44 import * as projectMoveActions from "store/projects/project-move-actions";
45 import * as projectUpdateActions from "store/projects/project-update-actions";
46 import * as collectionCreateActions from "store/collections/collection-create-actions";
47 import * as collectionCopyActions from "store/collections/collection-copy-actions";
48 import * as collectionMoveActions from "store/collections/collection-move-actions";
49 import * as processesActions from "store/processes/processes-actions";
50 import * as processMoveActions from "store/processes/process-move-actions";
51 import * as processUpdateActions from "store/processes/process-update-actions";
52 import * as processCopyActions from "store/processes/process-copy-actions";
53 import { trashPanelColumns } from "views/trash-panel/trash-panel";
54 import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
55 import { loadProcessPanel } from "store/process-panel/process-panel-actions";
56 import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
57 import { sharedWithMePanelColumns } from "views/shared-with-me-panel/shared-with-me-panel";
58 import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
59 import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
60 import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
61 import { loadLinkAccountPanel, linkAccountPanelActions } from "store/link-account-panel/link-account-panel-actions";
62 import { loadSiteManagerPanel } from "store/auth/auth-action-session";
63 import { workflowPanelColumns } from "views/workflow-panel/workflow-panel-view";
64 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
65 import { getProgressIndicator } from "store/progress-indicator/progress-indicator-reducer";
66 import { extractUuidKind, Resource, ResourceKind } from "models/resource";
67 import { FilterBuilder } from "services/api/filter-builder";
68 import { GroupContentsResource } from "services/groups-service/groups-service";
69 import { MatchCases, ofType, unionize, UnionOf } from "common/unionize";
70 import { loadRunProcessPanel } from "store/run-process-panel/run-process-panel-actions";
71 import { collectionPanelActions, loadCollectionPanel } from "store/collection-panel/collection-panel-action";
72 import { CollectionResource } from "models/collection";
73 import { WorkflowResource } from "models/workflow";
74 import { loadSearchResultsPanel, searchResultsPanelActions } from "store/search-results-panel/search-results-panel-actions";
75 import { searchResultsPanelColumns } from "views/search-results-panel/search-results-panel-view";
76 import { loadVirtualMachinesPanel } from "store/virtual-machines/virtual-machines-actions";
77 import { loadRepositoriesPanel } from "store/repositories/repositories-actions";
78 import { loadKeepServicesPanel } from "store/keep-services/keep-services-actions";
79 import { loadUsersPanel, userBindedActions } from "store/users/users-actions";
80 import * as userProfilePanelActions from "store/user-profile/user-profile-actions";
81 import { linkPanelActions, loadLinkPanel } from "store/link-panel/link-panel-actions";
82 import { linkPanelColumns } from "views/link-panel/link-panel-root";
83 import { userPanelColumns } from "views/user-panel/user-panel";
84 import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from "store/api-client-authorizations/api-client-authorizations-actions";
85 import { apiClientAuthorizationPanelColumns } from "views/api-client-authorization-panel/api-client-authorization-panel-root";
86 import * as groupPanelActions from "store/groups-panel/groups-panel-actions";
87 import { groupsPanelColumns } from "views/groups-panel/groups-panel";
88 import * as groupDetailsPanelActions from "store/group-details-panel/group-details-panel-actions";
89 import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from "views/group-details-panel/group-details-panel";
90 import { DataTableFetchMode } from "components/data-table/data-table";
91 import { loadPublicFavoritePanel, publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
92 import { publicFavoritePanelColumns } from "views/public-favorites-panel/public-favorites-panel";
93 import {
94     loadCollectionsContentAddressPanel,
95     collectionsContentAddressActions,
96 } from "store/collections-content-address-panel/collections-content-address-panel-actions";
97 import { collectionContentAddressPanelColumns } from "views/collection-content-address-panel/collection-content-address-panel";
98 import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-actions";
99 import { subprocessPanelColumns } from "views/subprocess-panel/subprocess-panel-root";
100 import { loadAllProcessesPanel, allProcessesPanelActions } from "../all-processes-panel/all-processes-panel-action";
101 import { allProcessesPanelColumns } from "views/all-processes-panel/all-processes-panel";
102 import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
103 import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
104 import { multiselectActions } from "store/multiselect/multiselect-actions";
105 import { treePickerActions } from "store/tree-picker/tree-picker-actions";
106
107 export const WORKBENCH_LOADING_SCREEN = "workbenchLoadingScreen";
108
109 export const isWorkbenchLoading = (state: RootState) => {
110     const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
111     return progress ? progress.working : false;
112 };
113
114 export const handleFirstTimeLoad = (action: any) => async (dispatch: Dispatch<any>, getState: () => RootState) => {
115     try {
116         await dispatch(action);
117     } catch (e) {
118         snackbarActions.OPEN_SNACKBAR({
119             message: "Error " + e,
120             hideDuration: 8000,
121             kind: SnackbarKind.WARNING,
122         })
123     } finally {
124         if (isWorkbenchLoading(getState())) {
125             dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
126         }
127     }
128 };
129
130 export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
131     dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
132     const { auth, router } = getState();
133     const { user } = auth;
134     if (user) {
135         dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
136         dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
137         dispatch(
138             allProcessesPanelActions.SET_COLUMNS({
139                 columns: allProcessesPanelColumns,
140             })
141         );
142         dispatch(
143             publicFavoritePanelActions.SET_COLUMNS({
144                 columns: publicFavoritePanelColumns,
145             })
146         );
147         dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
148         dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: sharedWithMePanelColumns }));
149         dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
150         dispatch(
151             searchResultsPanelActions.SET_FETCH_MODE({
152                 fetchMode: DataTableFetchMode.INFINITE,
153             })
154         );
155         dispatch(
156             searchResultsPanelActions.SET_COLUMNS({
157                 columns: searchResultsPanelColumns,
158             })
159         );
160         dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
161         dispatch(
162             groupPanelActions.GroupsPanelActions.SET_COLUMNS({
163                 columns: groupsPanelColumns,
164             })
165         );
166         dispatch(
167             groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
168                 columns: groupDetailsMembersPanelColumns,
169             })
170         );
171         dispatch(
172             groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
173                 columns: groupDetailsPermissionsPanelColumns,
174             })
175         );
176         dispatch(
177             userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
178                 columns: userProfileGroupsColumns,
179             })
180         );
181         dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
182         dispatch(
183             apiClientAuthorizationsActions.SET_COLUMNS({
184                 columns: apiClientAuthorizationPanelColumns,
185             })
186         );
187         dispatch(
188             collectionsContentAddressActions.SET_COLUMNS({
189                 columns: collectionContentAddressPanelColumns,
190             })
191         );
192         dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
193
194         if (services.linkAccountService.getAccountToLink()) {
195             dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
196         }
197
198         dispatch<any>(initSidePanelTree());
199         if (router.location) {
200             const match = matchRootRoute(router.location.pathname);
201             if (match) {
202                 dispatch<any>(navigateToRootProject);
203             }
204         }
205     } else {
206         dispatch(userIsNotAuthenticated);
207     }
208 };
209
210 export const loadFavorites = () =>
211     handleFirstTimeLoad((dispatch: Dispatch) => {
212         dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
213         dispatch<any>(loadFavoritePanel());
214         dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
215     });
216
217 export const loadCollectionContentAddress = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
218     await dispatch(loadCollectionsContentAddressPanel());
219 });
220
221 export const loadTrash = () =>
222     handleFirstTimeLoad((dispatch: Dispatch) => {
223         dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
224         dispatch<any>(loadTrashPanel());
225         dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
226     });
227
228 export const loadAllProcesses = () =>
229     handleFirstTimeLoad((dispatch: Dispatch) => {
230         dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES));
231         dispatch<any>(loadAllProcessesPanel());
232         dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
233     });
234
235 export const loadProject = (uuid: string) =>
236     handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
237         const userUuid = getUserUuid(getState());
238         dispatch(setIsProjectPanelTrashed(false));
239         if (!userUuid) {
240             return;
241         }
242         try {
243             dispatch(progressIndicatorActions.START_WORKING(uuid));
244             if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
245                 // Load another users home projects
246                 dispatch(finishLoadingProject(uuid));
247             } else if (userUuid !== uuid) {
248                 await dispatch(finishLoadingProject(uuid));
249                 const match = await loadGroupContentsResource({
250                     uuid,
251                     userUuid,
252                     services,
253                 });
254                 match({
255                     OWNED: async () => {
256                         await dispatch(activateSidePanelTreeItem(uuid));
257                         dispatch<any>(setSidePanelBreadcrumbs(uuid));
258                     },
259                     SHARED: async () => {
260                         await dispatch(activateSidePanelTreeItem(uuid));
261                         dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
262                     },
263                     TRASHED: async () => {
264                         await dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
265                         dispatch<any>(setTrashBreadcrumbs(uuid));
266                         dispatch(setIsProjectPanelTrashed(true));
267                     },
268                 });
269             } else {
270                 await dispatch(finishLoadingProject(userUuid));
271                 await dispatch(activateSidePanelTreeItem(userUuid));
272                 dispatch<any>(setSidePanelBreadcrumbs(userUuid));
273             }
274         } finally {
275             dispatch(progressIndicatorActions.STOP_WORKING(uuid));
276         }
277     });
278
279 export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) => async (dispatch: Dispatch) => {
280     const newProject = await dispatch<any>(projectCreateActions.createProject(data));
281     if (newProject) {
282         dispatch(
283             snackbarActions.OPEN_SNACKBAR({
284                 message: "Project has been successfully created.",
285                 hideDuration: 2000,
286                 kind: SnackbarKind.SUCCESS,
287             })
288         );
289         await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
290         dispatch<any>(navigateTo(newProject.uuid));
291     }
292 };
293
294 export const moveProject =
295     (data: MoveToFormDialogData, isSecondaryMove = false) =>
296         async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
297             const checkedList = getState().multiselect.checkedList;
298             const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
299
300             //if no items in checkedlist default to normal context menu behavior
301             if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
302
303             const sourceUuid = getResource(data.uuid)(getState().resources)?.ownerUuid;
304             const destinationUuid = data.ownerUuid;
305
306             const projectsToMove: MoveableResource[] = uuidsToMove
307                 .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
308                 .filter(resource => resource.kind === ResourceKind.PROJECT);
309
310             for (const project of projectsToMove) {
311                 await moveSingleProject(project);
312             }
313
314             //omly propagate if this call is the original
315             if (!isSecondaryMove) {
316                 const kindsToMove: Set<string> = selectedToKindSet(checkedList);
317                 kindsToMove.delete(ResourceKind.PROJECT);
318
319                 kindsToMove.forEach(kind => {
320                     secondaryMove[kind](data, true)(dispatch, getState, services);
321                 });
322             }
323
324             async function moveSingleProject(project: MoveableResource) {
325                 try {
326                     const oldProject: MoveToFormDialogData = { name: project.name, uuid: project.uuid, ownerUuid: data.ownerUuid };
327                     const oldOwnerUuid = oldProject ? oldProject.ownerUuid : "";
328                     const movedProject = await dispatch<any>(projectMoveActions.moveProject(oldProject));
329                     if (movedProject) {
330                         dispatch(
331                             snackbarActions.OPEN_SNACKBAR({
332                                 message: "Project has been moved",
333                                 hideDuration: 2000,
334                                 kind: SnackbarKind.SUCCESS,
335                             })
336                         );
337                         await dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
338                     }
339                 } catch (e) {
340                     dispatch(
341                         snackbarActions.OPEN_SNACKBAR({
342                             message: !!(project as any).frozenByUuid ? 'Could not move frozen project.' : e.message,
343                             hideDuration: 2000,
344                             kind: SnackbarKind.ERROR,
345                         })
346                     );
347                 }
348             }
349             if (sourceUuid) await dispatch<any>(loadSidePanelTreeProjects(sourceUuid));
350             await dispatch<any>(loadSidePanelTreeProjects(destinationUuid));
351         };
352
353 export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
354     const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
355     if (updatedProject) {
356         dispatch(
357             snackbarActions.OPEN_SNACKBAR({
358                 message: "Project has been successfully updated.",
359                 hideDuration: 2000,
360                 kind: SnackbarKind.SUCCESS,
361             })
362         );
363         await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
364         dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
365     }
366 };
367
368 export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
369     const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
370     if (updatedGroup) {
371         dispatch(
372             snackbarActions.OPEN_SNACKBAR({
373                 message: "Group has been successfully updated.",
374                 hideDuration: 2000,
375                 kind: SnackbarKind.SUCCESS,
376             })
377         );
378         await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
379         dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
380     }
381 };
382
383 export const loadCollection = (uuid: string) =>
384     handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
385         const userUuid = getUserUuid(getState());
386         try {
387             dispatch(progressIndicatorActions.START_WORKING(uuid));
388             if (userUuid) {
389                 const match = await loadGroupContentsResource({
390                     uuid,
391                     userUuid,
392                     services,
393                 });
394                 let collection: CollectionResource | undefined;
395                 let breadcrumbfunc:
396                     | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
397                     | undefined;
398                 let sidepanel: string | undefined;
399                 match({
400                     OWNED: thecollection => {
401                         collection = thecollection as CollectionResource;
402                         sidepanel = collection.ownerUuid;
403                         breadcrumbfunc = setSidePanelBreadcrumbs;
404                     },
405                     SHARED: thecollection => {
406                         collection = thecollection as CollectionResource;
407                         sidepanel = collection.ownerUuid;
408                         breadcrumbfunc = setSharedWithMeBreadcrumbs;
409                     },
410                     TRASHED: thecollection => {
411                         collection = thecollection as CollectionResource;
412                         sidepanel = SidePanelTreeCategory.TRASH;
413                         breadcrumbfunc = () => setTrashBreadcrumbs("");
414                     },
415                 });
416                 if (collection && breadcrumbfunc && sidepanel) {
417                     dispatch(updateResources([collection]));
418                     await dispatch<any>(finishLoadingProject(collection.ownerUuid));
419                     dispatch(collectionPanelActions.SET_COLLECTION(collection));
420                     await dispatch(activateSidePanelTreeItem(sidepanel));
421                     dispatch(breadcrumbfunc(collection.ownerUuid));
422                     dispatch(loadCollectionPanel(collection.uuid));
423                 }
424             }
425         } finally {
426             dispatch(progressIndicatorActions.STOP_WORKING(uuid));
427         }
428     });
429
430 export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) => async (dispatch: Dispatch) => {
431     const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
432     if (collection) {
433         dispatch(
434             snackbarActions.OPEN_SNACKBAR({
435                 message: "Collection has been successfully created.",
436                 hideDuration: 2000,
437                 kind: SnackbarKind.SUCCESS,
438             })
439         );
440         dispatch<any>(updateResources([collection]));
441         dispatch<any>(navigateTo(collection.uuid));
442     }
443 };
444
445 export const copyCollection = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
446     const checkedList = getState().multiselect.checkedList;
447     const uuidsToCopy: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
448
449     //if no items in checkedlist && no items passed in, default to normal context menu behavior
450     if (!uuidsToCopy.length) uuidsToCopy.push(data.uuid);
451
452     const collectionsToCopy: CollectionCopyResource[] = uuidsToCopy
453         .map(uuid => getResource(uuid)(getState().resources) as CollectionCopyResource)
454         .filter(resource => resource.kind === ResourceKind.COLLECTION);
455
456     for (const collection of collectionsToCopy) {
457         await copySingleCollection({ ...collection, ownerUuid: data.ownerUuid } as CollectionCopyResource);
458     }
459
460     async function copySingleCollection(copyToProject: CollectionCopyResource) {
461         const newName = data.fromContextMenu || collectionsToCopy.length === 1 ? data.name : `Copy of: ${copyToProject.name}`;
462         try {
463             const collection = await dispatch<any>(
464                 collectionCopyActions.copyCollection({
465                     ...copyToProject,
466                     name: newName,
467                     fromContextMenu: collectionsToCopy.length === 1 ? true : data.fromContextMenu,
468                 })
469             );
470             if (copyToProject && collection) {
471                 await dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
472                 dispatch(
473                     snackbarActions.OPEN_SNACKBAR({
474                         message: "Collection has been copied.",
475                         hideDuration: 3000,
476                         kind: SnackbarKind.SUCCESS,
477                         link: collection.ownerUuid,
478                     })
479                 );
480                 dispatch<any>(multiselectActions.deselectOne(copyToProject.uuid));
481             }
482         } catch (e) {
483             dispatch(
484                 snackbarActions.OPEN_SNACKBAR({
485                     message: e.message,
486                     hideDuration: 2000,
487                     kind: SnackbarKind.ERROR,
488                 })
489             );
490         }
491     }
492     dispatch(projectPanelActions.REQUEST_ITEMS());
493 };
494
495 export const moveCollection =
496     (data: MoveToFormDialogData, isSecondaryMove = false) =>
497         async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
498             const checkedList = getState().multiselect.checkedList;
499             const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
500
501             //if no items in checkedlist && no items passed in, default to normal context menu behavior
502             if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
503
504             const collectionsToMove: MoveableResource[] = uuidsToMove
505                 .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
506                 .filter(resource => resource.kind === ResourceKind.COLLECTION);
507
508             for (const collection of collectionsToMove) {
509                 await moveSingleCollection(collection);
510             }
511
512             //omly propagate if this call is the original
513             if (!isSecondaryMove) {
514                 const kindsToMove: Set<string> = selectedToKindSet(checkedList);
515                 kindsToMove.delete(ResourceKind.COLLECTION);
516
517                 kindsToMove.forEach(kind => {
518                     secondaryMove[kind](data, true)(dispatch, getState, services);
519                 });
520             }
521
522             async function moveSingleCollection(collection: MoveableResource) {
523                 try {
524                     const oldCollection: MoveToFormDialogData = { name: collection.name, uuid: collection.uuid, ownerUuid: data.ownerUuid };
525                     const movedCollection = await dispatch<any>(collectionMoveActions.moveCollection(oldCollection));
526                     dispatch<any>(updateResources([movedCollection]));
527                     dispatch<any>(reloadProjectMatchingUuid([movedCollection.ownerUuid]));
528                     dispatch(
529                         snackbarActions.OPEN_SNACKBAR({
530                             message: "Collection has been moved.",
531                             hideDuration: 2000,
532                             kind: SnackbarKind.SUCCESS,
533                         })
534                     );
535                 } catch (e) {
536                     dispatch(
537                         snackbarActions.OPEN_SNACKBAR({
538                             message: e.message,
539                             hideDuration: 2000,
540                             kind: SnackbarKind.ERROR,
541                         })
542                     );
543                 }
544             }
545         };
546
547 export const loadProcess = (uuid: string) =>
548     handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
549         try {
550             dispatch(progressIndicatorActions.START_WORKING(uuid));
551             dispatch<any>(loadProcessPanel(uuid));
552             const process = await dispatch<any>(processesActions.loadProcess(uuid));
553             if (process) {
554                 await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
555                 await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
556                 dispatch<any>(setProcessBreadcrumbs(uuid));
557                 dispatch<any>(loadDetailsPanel(uuid));
558             }
559         } finally {
560             dispatch(progressIndicatorActions.STOP_WORKING(uuid));
561         }
562     });
563
564 export const loadRegisteredWorkflow = (uuid: string) =>
565     handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
566         const userUuid = getUserUuid(getState());
567         if (userUuid) {
568             const match = await loadGroupContentsResource({
569                 uuid,
570                 userUuid,
571                 services,
572             });
573             let workflow: WorkflowResource | undefined;
574             let breadcrumbfunc:
575                 | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
576                 | undefined;
577             match({
578                 OWNED: async theworkflow => {
579                     workflow = theworkflow as WorkflowResource;
580                     breadcrumbfunc = setSidePanelBreadcrumbs;
581                 },
582                 SHARED: async theworkflow => {
583                     workflow = theworkflow as WorkflowResource;
584                     breadcrumbfunc = setSharedWithMeBreadcrumbs;
585                 },
586                 TRASHED: () => { },
587             });
588             if (workflow && breadcrumbfunc) {
589                 dispatch(updateResources([workflow]));
590                 await dispatch<any>(finishLoadingProject(workflow.ownerUuid));
591                 await dispatch<any>(activateSidePanelTreeItem(workflow.ownerUuid));
592                 dispatch<any>(breadcrumbfunc(workflow.ownerUuid));
593             }
594         }
595     });
596
597 export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) => async (dispatch: Dispatch) => {
598     try {
599         const process = await dispatch<any>(processUpdateActions.updateProcess(data));
600         if (process) {
601             dispatch(
602                 snackbarActions.OPEN_SNACKBAR({
603                     message: "Process has been successfully updated.",
604                     hideDuration: 2000,
605                     kind: SnackbarKind.SUCCESS,
606                 })
607             );
608             dispatch<any>(updateResources([process]));
609             dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
610         }
611     } catch (e) {
612         dispatch(
613             snackbarActions.OPEN_SNACKBAR({
614                 message: e.message,
615                 hideDuration: 2000,
616                 kind: SnackbarKind.ERROR,
617             })
618         );
619     }
620 };
621
622 export const moveProcess =
623     (data: MoveToFormDialogData, isSecondaryMove = false) =>
624         async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
625             const checkedList = getState().multiselect.checkedList;
626             const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
627
628             //if no items in checkedlist && no items passed in, default to normal context menu behavior
629             if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
630
631             const processesToMove: MoveableResource[] = uuidsToMove
632                 .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
633                 .filter(resource => resource.kind === ResourceKind.PROCESS);
634
635             for (const process of processesToMove) {
636                 await moveSingleProcess(process);
637             }
638
639             //omly propagate if this call is the original
640             if (!isSecondaryMove) {
641                 const kindsToMove: Set<string> = selectedToKindSet(checkedList);
642                 kindsToMove.delete(ResourceKind.PROCESS);
643
644                 kindsToMove.forEach(kind => {
645                     secondaryMove[kind](data, true)(dispatch, getState, services);
646                 });
647             }
648
649             async function moveSingleProcess(process: MoveableResource) {
650                 try {
651                     const oldProcess: MoveToFormDialogData = { name: process.name, uuid: process.uuid, ownerUuid: data.ownerUuid };
652                     const movedProcess = await dispatch<any>(processMoveActions.moveProcess(oldProcess));
653                     dispatch<any>(updateResources([movedProcess]));
654                     dispatch<any>(reloadProjectMatchingUuid([movedProcess.ownerUuid]));
655                     dispatch(
656                         snackbarActions.OPEN_SNACKBAR({
657                             message: "Process has been moved.",
658                             hideDuration: 2000,
659                             kind: SnackbarKind.SUCCESS,
660                         })
661                     );
662                 } catch (e) {
663                     dispatch(
664                         snackbarActions.OPEN_SNACKBAR({
665                             message: e.message,
666                             hideDuration: 2000,
667                             kind: SnackbarKind.ERROR,
668                         })
669                     );
670                 }
671             }
672         };
673
674 export const copyProcess = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
675     try {
676         const process = await dispatch<any>(processCopyActions.copyProcess(data));
677         dispatch<any>(updateResources([process]));
678         dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
679         dispatch(
680             snackbarActions.OPEN_SNACKBAR({
681                 message: "Process has been copied.",
682                 hideDuration: 2000,
683                 kind: SnackbarKind.SUCCESS,
684             })
685         );
686         dispatch<any>(navigateTo(process.uuid));
687     } catch (e) {
688         dispatch(
689             snackbarActions.OPEN_SNACKBAR({
690                 message: e.message,
691                 hideDuration: 2000,
692                 kind: SnackbarKind.ERROR,
693             })
694         );
695     }
696 };
697
698 export const resourceIsNotLoaded = (uuid: string) =>
699     snackbarActions.OPEN_SNACKBAR({
700         message: `Resource identified by ${uuid} is not loaded.`,
701         kind: SnackbarKind.ERROR,
702     });
703
704 export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
705     message: "User is not authenticated",
706     kind: SnackbarKind.ERROR,
707 });
708
709 export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
710     message: "Could not load user",
711     kind: SnackbarKind.ERROR,
712 });
713
714 export const reloadProjectMatchingUuid =
715     (matchingUuids: string[]) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
716         const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
717         if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
718             dispatch<any>(loadProject(currentProjectPanelUuid));
719         }
720     };
721
722 export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
723     dispatch<any>(loadSharedWithMePanel());
724     await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
725     await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
726 });
727
728 export const loadRunProcess = handleFirstTimeLoad(async (dispatch: Dispatch) => {
729     await dispatch<any>(loadRunProcessPanel());
730 });
731
732 export const loadPublicFavorites = () =>
733     handleFirstTimeLoad((dispatch: Dispatch) => {
734         dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES));
735         dispatch<any>(loadPublicFavoritePanel());
736         dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES));
737     });
738
739 export const loadSearchResults = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
740     await dispatch(loadSearchResultsPanel());
741 });
742
743 export const loadLinks = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
744     await dispatch(loadLinkPanel());
745 });
746
747 export const loadVirtualMachines = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
748     await dispatch(loadVirtualMachinesPanel());
749     dispatch(setVirtualMachinesBreadcrumbs());
750     dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHELL_ACCESS));
751 });
752
753 export const loadVirtualMachinesAdmin = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
754     await dispatch(loadVirtualMachinesPanel());
755     dispatch(setVirtualMachinesAdminBreadcrumbs());
756     dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({pickerId: SIDE_PANEL_TREE} ))
757 });
758
759 export const loadRepositories = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
760     await dispatch(loadRepositoriesPanel());
761     dispatch(setRepositoriesBreadcrumbs());
762 });
763
764 export const loadSshKeys = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
765     await dispatch(loadSshKeysPanel());
766 });
767
768 export const loadInstanceTypes = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
769     dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.INSTANCE_TYPES));
770     dispatch(setInstanceTypesBreadcrumbs());
771 });
772
773 export const loadSiteManager = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
774     await dispatch(loadSiteManagerPanel());
775 });
776
777 export const loadUserProfile = (userUuid?: string) =>
778     handleFirstTimeLoad((dispatch: Dispatch<any>) => {
779         if (userUuid) {
780             dispatch(setUserProfileBreadcrumbs(userUuid));
781             dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
782         } else {
783             dispatch(setMyAccountBreadcrumbs());
784             dispatch(userProfilePanelActions.loadUserProfilePanel());
785         }
786     });
787
788 export const loadLinkAccount = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
789     dispatch(loadLinkAccountPanel());
790 });
791
792 export const loadKeepServices = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
793     await dispatch(loadKeepServicesPanel());
794 });
795
796 export const loadUsers = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
797     await dispatch(loadUsersPanel());
798     dispatch(setUsersBreadcrumbs());
799 });
800
801 export const loadApiClientAuthorizations = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
802     await dispatch(loadApiClientAuthorizationsPanel());
803 });
804
805 export const loadGroupsPanel = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
806     dispatch(setGroupsBreadcrumbs());
807     dispatch(groupPanelActions.loadGroupsPanel());
808 });
809
810 export const loadGroupDetailsPanel = (groupUuid: string) =>
811     handleFirstTimeLoad((dispatch: Dispatch<any>) => {
812         dispatch(setGroupDetailsBreadcrumbs(groupUuid));
813         dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
814     });
815
816 const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch<any>) => {
817     const uuid = typeof project === "string" ? project : project.uuid;
818     dispatch(loadDetailsPanel(uuid));
819     if (typeof project !== "string") {
820         dispatch(updateResources([project]));
821     }
822 };
823
824 const loadGroupContentsResource = async (params: { uuid: string; userUuid: string; services: ServiceRepository }) => {
825     const filters = new FilterBuilder().addEqual("uuid", params.uuid).getFilters();
826     const { items } = await params.services.groupsService.contents(params.userUuid, {
827         filters,
828         recursive: true,
829         includeTrash: true,
830     });
831     const resource = items.shift();
832     let handler: GroupContentsHandler;
833     if (resource) {
834         handler =
835             (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
836                 ? groupContentsHandlers.TRASHED(resource)
837                 : groupContentsHandlers.OWNED(resource);
838     } else {
839         const kind = extractUuidKind(params.uuid);
840         let resource: GroupContentsResource;
841         if (kind === ResourceKind.COLLECTION) {
842             resource = await params.services.collectionService.get(params.uuid);
843         } else if (kind === ResourceKind.PROJECT) {
844             resource = await params.services.projectService.get(params.uuid);
845         } else if (kind === ResourceKind.WORKFLOW) {
846             resource = await params.services.workflowService.get(params.uuid);
847         } else if (kind === ResourceKind.CONTAINER_REQUEST) {
848             resource = await params.services.containerRequestService.get(params.uuid);
849         } else {
850             throw new Error("loadGroupContentsResource unsupported kind " + kind);
851         }
852         handler = groupContentsHandlers.SHARED(resource);
853     }
854     return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) => groupContentsHandlers.match(handler, cases);
855 };
856
857 const groupContentsHandlersRecord = {
858     TRASHED: ofType<GroupContentsResource>(),
859     SHARED: ofType<GroupContentsResource>(),
860     OWNED: ofType<GroupContentsResource>(),
861 };
862
863 const groupContentsHandlers = unionize(groupContentsHandlersRecord);
864
865 type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;
866
867 type CollectionCopyResource = Resource & { name: string; fromContextMenu: boolean };
868
869 type MoveableResource = Resource & { name: string };
870
871 type MoveFunc = (
872     data: MoveToFormDialogData,
873     isSecondaryMove?: boolean
874 ) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>;
875
876 const secondaryMove: Record<string, MoveFunc> = {
877     [ResourceKind.PROJECT]: moveProject,
878     [ResourceKind.PROCESS]: moveProcess,
879     [ResourceKind.COLLECTION]: moveCollection,
880 };