Cluster search fixes, infinite data explorer mode
[arvados-workbench2.git] / 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 { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
8 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
9 import { favoritePanelActions, loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
10 import {
11     getProjectPanelCurrentUuid,
12     openProjectPanel,
13     projectPanelActions,
14     setIsProjectPanelTrashed
15 } from '~/store/project-panel/project-panel-action';
16 import {
17     activateSidePanelTreeItem,
18     initSidePanelTree,
19     loadSidePanelTreeProjects,
20     SidePanelTreeCategory
21 } from '~/store/side-panel-tree/side-panel-tree-actions';
22 import { loadResource, updateResources } from '~/store/resources/resources-actions';
23 import { projectPanelColumns } from '~/views/project-panel/project-panel';
24 import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
25 import { matchRootRoute } from '~/routes/routes';
26 import {
27     setBreadcrumbs,
28     setGroupDetailsBreadcrumbs,
29     setGroupsBreadcrumbs,
30     setProcessBreadcrumbs,
31     setSharedWithMeBreadcrumbs,
32     setSidePanelBreadcrumbs,
33     setTrashBreadcrumbs
34 } from '~/store/breadcrumbs/breadcrumbs-actions';
35 import { navigateToProject } from '~/store/navigation/navigation-action';
36 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
37 import { ServiceRepository } from '~/services/services';
38 import { getResource } from '~/store/resources/resources';
39 import * as projectCreateActions from '~/store/projects/project-create-actions';
40 import * as projectMoveActions from '~/store/projects/project-move-actions';
41 import * as projectUpdateActions from '~/store/projects/project-update-actions';
42 import * as collectionCreateActions from '~/store/collections/collection-create-actions';
43 import * as collectionCopyActions from '~/store/collections/collection-copy-actions';
44 import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
45 import * as collectionMoveActions from '~/store/collections/collection-move-actions';
46 import * as processesActions from '~/store/processes/processes-actions';
47 import * as processMoveActions from '~/store/processes/process-move-actions';
48 import * as processUpdateActions from '~/store/processes/process-update-actions';
49 import * as processCopyActions from '~/store/processes/process-copy-actions';
50 import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
51 import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
52 import { initProcessLogsPanel } from '~/store/process-logs-panel/process-logs-panel-actions';
53 import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
54 import {
55     loadSharedWithMePanel,
56     sharedWithMePanelActions
57 } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
58 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
59 import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
60 import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
61 import { loadMyAccountPanel } from '~/store/my-account/my-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, 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 { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
72 import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
73 import { CollectionResource } from "~/models/collection";
74 import {
75     loadSearchResultsPanel,
76     searchResultsPanelActions
77 } from '~/store/search-results-panel/search-results-panel-actions';
78 import { searchResultsPanelColumns } from '~/views/search-results-panel/search-results-panel-view';
79 import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
80 import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
81 import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
82 import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
83 import { linkPanelActions, loadLinkPanel } from '~/store/link-panel/link-panel-actions';
84 import { computeNodesActions, loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
85 import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
86 import { userPanelColumns } from '~/views/user-panel/user-panel';
87 import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
88 import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
89 import * as groupPanelActions from '~/store/groups-panel/groups-panel-actions';
90 import { groupsPanelColumns } from '~/views/groups-panel/groups-panel';
91 import * as groupDetailsPanelActions from '~/store/group-details-panel/group-details-panel-actions';
92 import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-details-panel';
93 import { DataTableFetchMode } from "~/components/data-table/data-table";
94
95 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
96
97 export const isWorkbenchLoading = (state: RootState) => {
98     const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
99     return progress ? progress.working : false;
100 };
101
102 const handleFirstTimeLoad = (action: any) =>
103     async (dispatch: Dispatch<any>, getState: () => RootState) => {
104         try {
105             await dispatch(action);
106         } finally {
107             if (isWorkbenchLoading(getState())) {
108                 dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
109             }
110         }
111     };
112
113 export const loadWorkbench = () =>
114     async (dispatch: Dispatch, getState: () => RootState) => {
115         dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
116         const { auth, router } = getState();
117         const { user } = auth;
118         if (user) {
119             const userResource = await dispatch<any>(loadResource(user.uuid));
120             if (userResource) {
121                 dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
122                 dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
123                 dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
124                 dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
125                 dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
126                 dispatch(searchResultsPanelActions.SET_FETCH_MODE({ fetchMode: DataTableFetchMode.INFINITE }));
127                 dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
128                 dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
129                 dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
130                 dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({columns: groupDetailsPanelColumns}));
131                 dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
132                 dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
133                 dispatch<any>(initSidePanelTree());
134                 if (router.location) {
135                     const match = matchRootRoute(router.location.pathname);
136                     if (match) {
137                         dispatch(navigateToProject(userResource.uuid));
138                     }
139                 }
140             } else {
141                 dispatch(userIsNotAuthenticated);
142             }
143         } else {
144             dispatch(userIsNotAuthenticated);
145         }
146     };
147
148 export const loadFavorites = () =>
149     handleFirstTimeLoad(
150         (dispatch: Dispatch) => {
151             dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
152             dispatch<any>(loadFavoritePanel());
153             dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
154         });
155
156 export const loadTrash = () =>
157     handleFirstTimeLoad(
158         (dispatch: Dispatch) => {
159             dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
160             dispatch<any>(loadTrashPanel());
161             dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
162         });
163
164 export const loadProject = (uuid: string) =>
165     handleFirstTimeLoad(
166         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
167             const userUuid = services.authService.getUuid();
168             dispatch(setIsProjectPanelTrashed(false));
169             if (userUuid) {
170                 if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
171                     // Load another users home projects
172                     dispatch(finishLoadingProject(uuid));
173                 } else if (userUuid !== uuid) {
174                     const match = await loadGroupContentsResource({ uuid, userUuid, services });
175                     match({
176                         OWNED: async project => {
177                             await dispatch(activateSidePanelTreeItem(uuid));
178                             dispatch<any>(setSidePanelBreadcrumbs(uuid));
179                             dispatch(finishLoadingProject(project));
180                         },
181                         SHARED: project => {
182                             dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
183                             dispatch(activateSidePanelTreeItem(uuid));
184                             dispatch(finishLoadingProject(project));
185                         },
186                         TRASHED: project => {
187                             dispatch<any>(setTrashBreadcrumbs(uuid));
188                             dispatch(setIsProjectPanelTrashed(true));
189                             dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
190                             dispatch(finishLoadingProject(project));
191                         }
192                     });
193                 } else {
194                     await dispatch(activateSidePanelTreeItem(userUuid));
195                     dispatch<any>(setSidePanelBreadcrumbs(userUuid));
196                     dispatch(finishLoadingProject(userUuid));
197                 }
198             }
199         });
200
201 export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) =>
202     async (dispatch: Dispatch) => {
203         const newProject = await dispatch<any>(projectCreateActions.createProject(data));
204         if (newProject) {
205             dispatch(snackbarActions.OPEN_SNACKBAR({
206                 message: "Project has been successfully created.",
207                 hideDuration: 2000
208             }));
209             await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
210             dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
211         }
212     };
213
214 export const moveProject = (data: MoveToFormDialogData) =>
215     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
216         try {
217             const oldProject = getResource(data.uuid)(getState().resources);
218             const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
219             const movedProject = await dispatch<any>(projectMoveActions.moveProject(data));
220             if (movedProject) {
221                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000 }));
222                 if (oldProject) {
223                     await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
224                 }
225                 dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
226             }
227         } catch (e) {
228             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
229         }
230     };
231
232 export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
233     async (dispatch: Dispatch) => {
234         const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
235         if (updatedProject) {
236             dispatch(snackbarActions.OPEN_SNACKBAR({
237                 message: "Project has been successfully updated.",
238                 hideDuration: 2000
239             }));
240             await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
241             dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
242         }
243     };
244
245 export const loadCollection = (uuid: string) =>
246     handleFirstTimeLoad(
247         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
248             const userUuid = services.authService.getUuid();
249             if (userUuid) {
250                 const match = await loadGroupContentsResource({ uuid, userUuid, services });
251                 match({
252                     OWNED: async collection => {
253                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
254                         dispatch(updateResources([collection]));
255                         await dispatch(activateSidePanelTreeItem(collection.ownerUuid));
256                         dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
257                         dispatch(loadCollectionFiles(collection.uuid));
258                     },
259                     SHARED: collection => {
260                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
261                         dispatch(updateResources([collection]));
262                         dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
263                         dispatch(activateSidePanelTreeItem(collection.ownerUuid));
264                         dispatch(loadCollectionFiles(collection.uuid));
265                     },
266                     TRASHED: collection => {
267                         dispatch(collectionPanelActions.SET_COLLECTION(collection as CollectionResource));
268                         dispatch(updateResources([collection]));
269                         dispatch(setTrashBreadcrumbs(''));
270                         dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
271                         dispatch(loadCollectionFiles(collection.uuid));
272                     },
273
274                 });
275             }
276         });
277
278 export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) =>
279     async (dispatch: Dispatch) => {
280         const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
281         if (collection) {
282             dispatch(snackbarActions.OPEN_SNACKBAR({
283                 message: "Collection has been successfully created.",
284                 hideDuration: 2000
285             }));
286             dispatch<any>(updateResources([collection]));
287             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
288         }
289     };
290
291 export const updateCollection = (data: collectionUpdateActions.CollectionUpdateFormDialogData) =>
292     async (dispatch: Dispatch) => {
293         const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
294         if (collection) {
295             dispatch(snackbarActions.OPEN_SNACKBAR({
296                 message: "Collection has been successfully updated.",
297                 hideDuration: 2000
298             }));
299             dispatch<any>(updateResources([collection]));
300             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
301         }
302     };
303
304 export const copyCollection = (data: CopyFormDialogData) =>
305     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
306         try {
307             const copyToProject = getResource(data.ownerUuid)(getState().resources);
308             const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
309             if (copyToProject && collection) {
310                 dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
311                 dispatch(snackbarActions.OPEN_SNACKBAR({
312                     message: 'Collection has been copied.',
313                     hideDuration: 3000,
314                     kind: SnackbarKind.SUCCESS,
315                     link: collection.ownerUuid
316                 }));
317             }
318         } catch (e) {
319             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
320         }
321     };
322
323 export const moveCollection = (data: MoveToFormDialogData) =>
324     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
325         try {
326             const collection = await dispatch<any>(collectionMoveActions.moveCollection(data));
327             dispatch<any>(updateResources([collection]));
328             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
329             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
330         } catch (e) {
331             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
332         }
333     };
334
335 export const loadProcess = (uuid: string) =>
336     handleFirstTimeLoad(
337         async (dispatch: Dispatch, getState: () => RootState) => {
338             dispatch<any>(loadProcessPanel(uuid));
339             const process = await dispatch<any>(processesActions.loadProcess(uuid));
340             await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
341             dispatch<any>(setProcessBreadcrumbs(uuid));
342             dispatch(loadDetailsPanel(uuid));
343         });
344
345 export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
346     async (dispatch: Dispatch) => {
347         try {
348             const process = await dispatch<any>(processUpdateActions.updateProcess(data));
349             if (process) {
350                 dispatch(snackbarActions.OPEN_SNACKBAR({
351                     message: "Process has been successfully updated.",
352                     hideDuration: 2000
353                 }));
354                 dispatch<any>(updateResources([process]));
355                 dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
356             }
357         } catch (e) {
358             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
359         }
360     };
361
362 export const moveProcess = (data: MoveToFormDialogData) =>
363     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
364         try {
365             const process = await dispatch<any>(processMoveActions.moveProcess(data));
366             dispatch<any>(updateResources([process]));
367             dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
368             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000 }));
369         } catch (e) {
370             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
371         }
372     };
373
374 export const copyProcess = (data: CopyFormDialogData) =>
375     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
376         try {
377             const process = await dispatch<any>(processCopyActions.copyProcess(data));
378             dispatch<any>(updateResources([process]));
379             dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
380             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
381         } catch (e) {
382             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
383         }
384     };
385
386 export const loadProcessLog = (uuid: string) =>
387     handleFirstTimeLoad(
388         async (dispatch: Dispatch) => {
389             const process = await dispatch<any>(processesActions.loadProcess(uuid));
390             dispatch<any>(setProcessBreadcrumbs(uuid));
391             dispatch<any>(initProcessLogsPanel(uuid));
392             await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
393         });
394
395 export const resourceIsNotLoaded = (uuid: string) =>
396     snackbarActions.OPEN_SNACKBAR({
397         message: `Resource identified by ${uuid} is not loaded.`
398     });
399
400 export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
401     message: 'User is not authenticated'
402 });
403
404 export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
405     message: 'Could not load user'
406 });
407
408 export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
409     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
410         const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
411         if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
412             dispatch<any>(loadProject(currentProjectPanelUuid));
413         }
414     };
415
416 export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
417     dispatch<any>(loadSharedWithMePanel());
418     await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
419     await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
420 });
421
422 export const loadRunProcess = handleFirstTimeLoad(
423     async (dispatch: Dispatch) => {
424         await dispatch<any>(loadRunProcessPanel());
425     }
426 );
427
428 export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
429     dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.WORKFLOWS));
430     await dispatch(loadWorkflowPanel());
431     dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
432 });
433
434 export const loadSearchResults = handleFirstTimeLoad(
435     async (dispatch: Dispatch<any>) => {
436         await dispatch(loadSearchResultsPanel());
437     });
438
439 export const loadLinks = handleFirstTimeLoad(
440     async (dispatch: Dispatch<any>) => {
441         await dispatch(loadLinkPanel());
442     });
443
444 export const loadVirtualMachines = handleFirstTimeLoad(
445     async (dispatch: Dispatch<any>) => {
446         await dispatch(loadVirtualMachinesPanel());
447         dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
448     });
449
450 export const loadRepositories = handleFirstTimeLoad(
451     async (dispatch: Dispatch<any>) => {
452         await dispatch(loadRepositoriesPanel());
453         dispatch(setBreadcrumbs([{ label: 'Repositories' }]));
454     });
455
456 export const loadSshKeys = handleFirstTimeLoad(
457     async (dispatch: Dispatch<any>) => {
458         await dispatch(loadSshKeysPanel());
459     });
460
461 export const loadSiteManager = handleFirstTimeLoad(
462 async (dispatch: Dispatch<any>) => {
463     await dispatch(loadSiteManagerPanel());
464 });
465
466 export const loadMyAccount = handleFirstTimeLoad(
467     (dispatch: Dispatch<any>) => {
468         dispatch(loadMyAccountPanel());
469     });
470
471 export const loadKeepServices = handleFirstTimeLoad(
472     async (dispatch: Dispatch<any>) => {
473         await dispatch(loadKeepServicesPanel());
474     });
475
476 export const loadUsers = handleFirstTimeLoad(
477     async (dispatch: Dispatch<any>) => {
478         await dispatch(loadUsersPanel());
479         dispatch(setBreadcrumbs([{ label: 'Users' }]));
480     });
481
482 export const loadComputeNodes = handleFirstTimeLoad(
483     async (dispatch: Dispatch<any>) => {
484         await dispatch(loadComputeNodesPanel());
485     });
486
487 export const loadApiClientAuthorizations = handleFirstTimeLoad(
488     async (dispatch: Dispatch<any>) => {
489         await dispatch(loadApiClientAuthorizationsPanel());
490     });
491
492 export const loadGroupsPanel = handleFirstTimeLoad(
493     (dispatch: Dispatch<any>) => {
494         dispatch(setGroupsBreadcrumbs());
495         dispatch(groupPanelActions.loadGroupsPanel());
496     });
497
498
499 export const loadGroupDetailsPanel = (groupUuid: string) =>
500     handleFirstTimeLoad(
501         (dispatch: Dispatch<any>) => {
502             dispatch(setGroupDetailsBreadcrumbs(groupUuid));
503             dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
504         });
505
506 const finishLoadingProject = (project: GroupContentsResource | string) =>
507     async (dispatch: Dispatch<any>) => {
508         const uuid = typeof project === 'string' ? project : project.uuid;
509         dispatch(openProjectPanel(uuid));
510         dispatch(loadDetailsPanel(uuid));
511         if (typeof project !== 'string') {
512             dispatch(updateResources([project]));
513         }
514     };
515
516 const loadGroupContentsResource = async (params: {
517     uuid: string,
518     userUuid: string,
519     services: ServiceRepository
520 }) => {
521     const filters = new FilterBuilder()
522         .addEqual('uuid', params.uuid)
523         .getFilters();
524     const { items } = await params.services.groupsService.contents(params.userUuid, {
525         filters,
526         recursive: true,
527         includeTrash: true,
528     });
529     const resource = items.shift();
530     let handler: GroupContentsHandler;
531     if (resource) {
532         handler = (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
533             ? groupContentsHandlers.TRASHED(resource)
534             : groupContentsHandlers.OWNED(resource);
535     } else {
536         const kind = extractUuidKind(params.uuid);
537         let resource: GroupContentsResource;
538         if (kind === ResourceKind.COLLECTION) {
539             resource = await params.services.collectionService.get(params.uuid);
540         } else if (kind === ResourceKind.PROJECT) {
541             resource = await params.services.projectService.get(params.uuid);
542         } else {
543             resource = await params.services.containerRequestService.get(params.uuid);
544         }
545         handler = groupContentsHandlers.SHARED(resource);
546     }
547     return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) =>
548         groupContentsHandlers.match(handler, cases);
549
550 };
551
552 const groupContentsHandlersRecord = {
553     TRASHED: ofType<GroupContentsResource>(),
554     SHARED: ofType<GroupContentsResource>(),
555     OWNED: ofType<GroupContentsResource>(),
556 };
557
558 const groupContentsHandlers = unionize(groupContentsHandlersRecord);
559
560 type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;