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