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