merge master
[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";
7 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
8 import { snackbarActions } from '../snackbar/snackbar-actions';
9 import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action';
10 import { openProjectPanel, projectPanelActions } from '~/store/project-panel/project-panel-action';
11 import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory, loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
12 import { loadResource, updateResources } from '../resources/resources-actions';
13 import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
14 import { projectPanelColumns } from '~/views/project-panel/project-panel';
15 import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
16 import { matchRootRoute } from '~/routes/routes';
17 import { setSidePanelBreadcrumbs, setProcessBreadcrumbs, setSharedWithMeBreadcrumbs, setTrashBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
18 import { navigateToProject } from '../navigation/navigation-action';
19 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
20 import { ServiceRepository } from '~/services/services';
21 import { getResource } from '../resources/resources';
22 import { getProjectPanelCurrentUuid } from '../project-panel/project-panel-action';
23 import * as projectCreateActions from '~/store/projects/project-create-actions';
24 import * as projectMoveActions from '~/store/projects/project-move-actions';
25 import * as projectUpdateActions from '~/store/projects/project-update-actions';
26 import * as collectionCreateActions from '~/store/collections/collection-create-actions';
27 import * as collectionCopyActions from '~/store/collections/collection-copy-actions';
28 import * as collectionUpdateActions from '~/store/collections/collection-update-actions';
29 import * as collectionMoveActions from '~/store/collections/collection-move-actions';
30 import * as processesActions from '../processes/processes-actions';
31 import * as processMoveActions from '~/store/processes/process-move-actions';
32 import * as processUpdateActions from '~/store/processes/process-update-actions';
33 import * as processCopyActions from '~/store/processes/process-copy-actions';
34 import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
35 import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
36 import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
37 import { loadProcessPanel } from '~/store/process-panel/process-panel-actions';
38 import { sharedWithMePanelActions } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
39 import { loadSharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel-actions';
40 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
41 import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
42 import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
43 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
44 import { getProgressIndicator } from '../progress-indicator/progress-indicator-reducer';
45 import { ResourceKind, extractUuidKind } from '~/models/resource';
46 import { FilterBuilder } from '~/services/api/filter-builder';
47 import { GroupContentsResource } from '~/services/groups-service/groups-service';
48 import { unionize, ofType, UnionOf, MatchCases } from '~/common/unionize';
49 import { loadRunProcessPanel } from '~/store/run-process-panel/run-process-panel-actions';
50 import { loadCollectionFiles } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
51 import { SnackbarKind } from '~/store/snackbar/snackbar-actions';
52
53 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
54
55 export const isWorkbenchLoading = (state: RootState) => {
56     const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
57     return progress ? progress.working : false;
58 };
59
60 const handleFirstTimeLoad = (action: any) =>
61     async (dispatch: Dispatch<any>, getState: () => RootState) => {
62         try {
63             await dispatch(action);
64         } finally {
65             if (isWorkbenchLoading(getState())) {
66                 dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
67             }
68         }
69     };
70
71
72 export const loadWorkbench = () =>
73     async (dispatch: Dispatch, getState: () => RootState) => {
74         dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
75         const { auth, router } = getState();
76         const { user } = auth;
77         if (user) {
78             const userResource = await dispatch<any>(loadResource(user.uuid));
79             if (userResource) {
80                 dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
81                 dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
82                 dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
83                 dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
84                 dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
85                 dispatch<any>(initSidePanelTree());
86                 if (router.location) {
87                     const match = matchRootRoute(router.location.pathname);
88                     if (match) {
89                         dispatch(navigateToProject(userResource.uuid));
90                     }
91                 }
92             } else {
93                 dispatch(userIsNotAuthenticated);
94             }
95         } else {
96             dispatch(userIsNotAuthenticated);
97         }
98     };
99
100 export const loadFavorites = () =>
101     handleFirstTimeLoad(
102         (dispatch: Dispatch) => {
103             dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
104             dispatch<any>(loadFavoritePanel());
105             dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
106         });
107
108 export const loadTrash = () =>
109     handleFirstTimeLoad(
110         (dispatch: Dispatch) => {
111             dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
112             dispatch<any>(loadTrashPanel());
113             dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
114         });
115
116 export const loadProject = (uuid: string) =>
117     handleFirstTimeLoad(
118         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
119             const userUuid = services.authService.getUuid();
120             if (userUuid) {
121                 if (userUuid !== uuid) {
122                     const match = await loadGroupContentsResource({ uuid, userUuid, services });
123                     match({
124                         OWNED: async project => {
125                             await dispatch(activateSidePanelTreeItem(uuid));
126                             dispatch<any>(setSidePanelBreadcrumbs(uuid));
127                             dispatch(finishLoadingProject(project));
128                         },
129                         SHARED: project => {
130                             dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
131                             dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
132                             dispatch(finishLoadingProject(project));
133                         },
134                         TRASHED: project => {
135                             dispatch<any>(setTrashBreadcrumbs(uuid));
136                             dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
137                             dispatch(finishLoadingProject(project));
138                         }
139                     });
140                 } else {
141                     await dispatch(activateSidePanelTreeItem(userUuid));
142                     dispatch<any>(setSidePanelBreadcrumbs(userUuid));
143                     dispatch(finishLoadingProject(userUuid));
144                 }
145             }
146         });
147
148 export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) =>
149     async (dispatch: Dispatch) => {
150         const newProject = await dispatch<any>(projectCreateActions.createProject(data));
151         if (newProject) {
152             dispatch(snackbarActions.OPEN_SNACKBAR({
153                 message: "Project has been successfully created.",
154                 hideDuration: 2000
155             }));
156             await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
157             dispatch<any>(reloadProjectMatchingUuid([newProject.ownerUuid]));
158         }
159     };
160
161 export const moveProject = (data: MoveToFormDialogData) =>
162     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
163         try {
164             const oldProject = getResource(data.uuid)(getState().resources);
165             const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
166             const movedProject = await dispatch<any>(projectMoveActions.moveProject(data));
167             if (movedProject) {
168                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Project has been moved', hideDuration: 2000 }));
169                 if (oldProject) {
170                     await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
171                 }
172                 dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
173             }
174         } catch (e) {
175             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
176         }
177     };
178
179 export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
180     async (dispatch: Dispatch) => {
181         const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
182         if (updatedProject) {
183             dispatch(snackbarActions.OPEN_SNACKBAR({
184                 message: "Project has been successfully updated.",
185                 hideDuration: 2000
186             }));
187             await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
188             dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
189         }
190     };
191
192 export const loadCollection = (uuid: string) =>
193     handleFirstTimeLoad(
194         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
195             const userUuid = services.authService.getUuid();
196             if (userUuid) {
197                 const match = await loadGroupContentsResource({ uuid, userUuid, services });
198                 match({
199                     OWNED: async collection => {
200                         dispatch(updateResources([collection]));
201                         await dispatch(activateSidePanelTreeItem(collection.ownerUuid));
202                         dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
203                         dispatch(loadCollectionFiles(collection.uuid));
204                     },
205                     SHARED: collection => {
206                         dispatch(updateResources([collection]));
207                         dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
208                         dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
209                         dispatch(loadCollectionFiles(collection.uuid));
210                     },
211                     TRASHED: collection => {
212                         dispatch(updateResources([collection]));
213                         dispatch(setTrashBreadcrumbs(''));
214                         dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
215                         dispatch(loadCollectionFiles(collection.uuid));
216                     },
217
218                 });
219             }
220         });
221
222 export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) =>
223     async (dispatch: Dispatch) => {
224         const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
225         if (collection) {
226             dispatch(snackbarActions.OPEN_SNACKBAR({
227                 message: "Collection has been successfully created.",
228                 hideDuration: 2000
229             }));
230             dispatch<any>(updateResources([collection]));
231             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
232         }
233     };
234
235 export const updateCollection = (data: collectionUpdateActions.CollectionUpdateFormDialogData) =>
236     async (dispatch: Dispatch) => {
237         const collection = await dispatch<any>(collectionUpdateActions.updateCollection(data));
238         if (collection) {
239             dispatch(snackbarActions.OPEN_SNACKBAR({
240                 message: "Collection has been successfully updated.",
241                 hideDuration: 2000
242             }));
243             dispatch<any>(updateResources([collection]));
244             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
245         }
246     };
247
248 export const copyCollection = (data: CopyFormDialogData) =>
249     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
250         try {
251             const collection = await dispatch<any>(collectionCopyActions.copyCollection(data));
252             dispatch<any>(updateResources([collection]));
253             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
254             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied.', hideDuration: 2000 }));
255         } catch (e) {
256             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
257         }
258     };
259
260 export const moveCollection = (data: MoveToFormDialogData) =>
261     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
262         try {
263             const collection = await dispatch<any>(collectionMoveActions.moveCollection(data));
264             dispatch<any>(updateResources([collection]));
265             dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
266             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
267         } catch (e) {
268             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
269         }
270     };
271
272 export const loadProcess = (uuid: string) =>
273     handleFirstTimeLoad(
274         async (dispatch: Dispatch, getState: () => RootState) => {
275             dispatch<any>(loadProcessPanel(uuid));
276             const process = await dispatch<any>(processesActions.loadProcess(uuid));
277             await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
278             dispatch<any>(setProcessBreadcrumbs(uuid));
279             dispatch(loadDetailsPanel(uuid));
280         });
281
282 export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) =>
283     async (dispatch: Dispatch) => {
284         try {
285             const process = await dispatch<any>(processUpdateActions.updateProcess(data));
286             if (process) {
287                 dispatch(snackbarActions.OPEN_SNACKBAR({
288                     message: "Process has been successfully updated.",
289                     hideDuration: 2000
290                 }));
291                 dispatch<any>(updateResources([process]));
292                 dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
293             }
294         } catch (e) {
295             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
296         }
297     };
298
299 export const moveProcess = (data: MoveToFormDialogData) =>
300     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
301         try {
302             const process = await dispatch<any>(processMoveActions.moveProcess(data));
303             dispatch<any>(updateResources([process]));
304             dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
305             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been moved.', hideDuration: 2000 }));
306         } catch (e) {
307             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
308         }
309     };
310
311 export const copyProcess = (data: CopyFormDialogData) =>
312     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
313         try {
314             const process = await dispatch<any>(processCopyActions.copyProcess(data));
315             dispatch<any>(updateResources([process]));
316             dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
317             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process has been copied.', hideDuration: 2000 }));
318         } catch (e) {
319             dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000 }));
320         }
321     };
322
323 export const loadProcessLog = (uuid: string) =>
324     handleFirstTimeLoad(
325         async (dispatch: Dispatch) => {
326             const process = await dispatch<any>(processesActions.loadProcess(uuid));
327             dispatch<any>(setProcessBreadcrumbs(uuid));
328             dispatch<any>(initProcessLogsPanel(uuid));
329             await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
330         });
331
332 export const resourceIsNotLoaded = (uuid: string) =>
333     snackbarActions.OPEN_SNACKBAR({
334         message: `Resource identified by ${uuid} is not loaded.`
335     });
336
337 export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
338     message: 'User is not authenticated'
339 });
340
341 export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
342     message: 'Could not load user'
343 });
344
345 export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
346     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
347         const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
348         if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
349             dispatch<any>(loadProject(currentProjectPanelUuid));
350         }
351     };
352
353 export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
354     dispatch<any>(loadSharedWithMePanel());
355     await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
356     await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
357 });
358
359 export const loadRunProcess = handleFirstTimeLoad(
360     async (dispatch: Dispatch) => {
361         await dispatch<any>(loadRunProcessPanel());
362     }
363 );
364
365 export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
366     dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.WORKFLOWS));
367     await dispatch(loadWorkflowPanel());
368     dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
369 });
370
371 const finishLoadingProject = (project: GroupContentsResource | string) =>
372     async (dispatch: Dispatch<any>) => {
373         const uuid = typeof project === 'string' ? project : project.uuid;
374         dispatch(openProjectPanel(uuid));
375         dispatch(loadDetailsPanel(uuid));
376         if (typeof project !== 'string') {
377             dispatch(updateResources([project]));
378         }
379     };
380
381 const loadGroupContentsResource = async (params: {
382     uuid: string,
383     userUuid: string,
384     services: ServiceRepository
385 }) => {
386     const filters = new FilterBuilder()
387         .addEqual('uuid', params.uuid)
388         .getFilters();
389     const { items } = await params.services.groupsService.contents(params.userUuid, {
390         filters,
391         recursive: true,
392         includeTrash: true,
393     });
394     const resource = items.shift();
395     let handler: GroupContentsHandler;
396     if (resource) {
397         handler = (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
398             ? groupContentsHandlers.TRASHED(resource)
399             : groupContentsHandlers.OWNED(resource);
400     } else {
401         const kind = extractUuidKind(params.uuid);
402         let resource: GroupContentsResource;
403         if (kind === ResourceKind.COLLECTION) {
404             resource = await params.services.collectionService.get(params.uuid);
405         } else if (kind === ResourceKind.PROJECT) {
406             resource = await params.services.projectService.get(params.uuid);
407         } else {
408             resource = await params.services.containerRequestService.get(params.uuid);
409         }
410         handler = groupContentsHandlers.SHARED(resource);
411     }
412     return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) =>
413         groupContentsHandlers.match(handler, cases);
414
415 };
416
417 const groupContentsHandlersRecord = {
418     TRASHED: ofType<GroupContentsResource>(),
419     SHARED: ofType<GroupContentsResource>(),
420     OWNED: ofType<GroupContentsResource>(),
421 };
422
423 const groupContentsHandlers = unionize(groupContentsHandlersRecord);
424
425 type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;