21316: Merge commit '1416f698b72de4b09350d9c2fb25c1405c3247bc' into 21316-left-panel...
[arvados.git] / services / workbench2 / src / store / breadcrumbs / breadcrumbs-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 { getResource } from 'store/resources/resources';
9 import { propertiesActions } from '../properties/properties-actions';
10 import { getProcess } from 'store/processes/process';
11 import { ServiceRepository } from 'services/services';
12 import { SidePanelTreeCategory, activateSidePanelTreeItem } from 'store/side-panel-tree/side-panel-tree-actions';
13 import { updateResources } from '../resources/resources-actions';
14 import { ResourceKind } from 'models/resource';
15 import { GroupResource } from 'models/group';
16 import { extractUuidKind } from 'models/resource';
17 import { UserResource } from 'models/user';
18 import { FilterBuilder } from 'services/api/filter-builder';
19 import { ProcessResource } from 'models/process';
20 import { OrderBuilder } from 'services/api/order-builder';
21 import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
22 import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
23 import { AdminMenuIcon, CollectionIcon, IconType, ProcessIcon, ProjectIcon, ResourceIcon, TerminalIcon, WorkflowIcon } from 'components/icon/icon';
24 import { CollectionResource } from 'models/collection';
25 import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree';
26 import { WorkflowResource } from 'models/workflow';
27 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
28
29 export const BREADCRUMBS = 'breadcrumbs';
30
31 export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource) => {
32     if (currentItem) {
33         const currentCrumb = resourceToBreadcrumb(currentItem)
34         if (currentCrumb.label.length) breadcrumbs.push(currentCrumb);
35     }
36     return propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
37 };
38
39 const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource): IconType | undefined => {
40     switch (resource.kind) {
41         case ResourceKind.PROJECT:
42             return ProjectIcon;
43         case ResourceKind.PROCESS:
44             return ProcessIcon;
45         case ResourceKind.COLLECTION:
46             return CollectionIcon;
47         case ResourceKind.WORKFLOW:
48             return WorkflowIcon;
49         default:
50             return undefined;
51     }
52 }
53
54 const resourceToBreadcrumb = (resource: (CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource) & {fullName?: string}  ): Breadcrumb => ({
55     label: resource.name || resource.fullName || '',
56     uuid: resource.uuid,
57     icon: resourceToBreadcrumbIcon(resource),
58 })
59
60 export const setSidePanelBreadcrumbs = (uuid: string) =>
61     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
62         try {
63             dispatch(progressIndicatorActions.START_WORKING(uuid + "-breadcrumbs"));
64             const ancestors = await services.ancestorsService.ancestors(uuid, '');
65             dispatch(updateResources(ancestors));
66
67             let breadcrumbs: Breadcrumb[] = [];
68             const { collectionPanel: { item } } = getState();
69
70             const path = getState().router.location!.pathname;
71             const currentUuid = path.split('/')[2];
72             const uuidKind = extractUuidKind(currentUuid);
73             const rootUuid = getUserUuid(getState());
74
75             if (ancestors.find(ancestor => ancestor.uuid === rootUuid)) {
76                 // Handle home project uuid root
77                 breadcrumbs.push({
78                     label: SidePanelTreeCategory.PROJECTS,
79                     uuid: SidePanelTreeCategory.PROJECTS,
80                     icon: getSidePanelIcon(SidePanelTreeCategory.PROJECTS)
81                 });
82             } else if (Object.values(SidePanelTreeCategory).includes(uuid as SidePanelTreeCategory)) {
83                 // Handle SidePanelTreeCategory root
84                 breadcrumbs.push({
85                     label: uuid,
86                     uuid: uuid,
87                     icon: getSidePanelIcon(uuid)
88                 });
89             }
90
91             breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
92                 ancestor.kind === ResourceKind.GROUP
93                     ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
94                     : breadcrumbs,
95                 breadcrumbs);
96
97             if (uuidKind === ResourceKind.COLLECTION) {
98                 const collectionItem = item ? item : await services.collectionService.get(currentUuid);
99                 const parentProcessItem = await getCollectionParent(collectionItem)(services);
100                 if (parentProcessItem) {
101                     const mainProcessItem = await getProcessParent(parentProcessItem)(services);
102                     mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
103                     breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
104                 }
105                 dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
106             } else if (uuidKind === ResourceKind.PROCESS) {
107                 const processItem = await services.containerRequestService.get(currentUuid);
108                 const parentProcessItem = await getProcessParent(processItem)(services);
109                 if (parentProcessItem) {
110                     breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
111                 }
112                 dispatch(setBreadcrumbs(breadcrumbs, processItem));
113             } else if (uuidKind === ResourceKind.WORKFLOW) {
114                 const workflowItem = await services.workflowService.get(currentUuid);
115                 dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
116             }
117             dispatch(setBreadcrumbs(breadcrumbs));
118         } finally {
119             dispatch(progressIndicatorActions.STOP_WORKING(uuid + "-breadcrumbs"));
120         }
121     };
122
123 export const setSharedWithMeBreadcrumbs = (uuid: string) =>
124     setCategoryBreadcrumbs(uuid, SidePanelTreeCategory.SHARED_WITH_ME);
125
126 export const setTrashBreadcrumbs = (uuid: string) =>
127     setCategoryBreadcrumbs(uuid, SidePanelTreeCategory.TRASH);
128
129 export const setCategoryBreadcrumbs = (uuid: string, category: string) =>
130     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
131         try {
132             dispatch(progressIndicatorActions.START_WORKING(uuid + "-breadcrumbs"));
133             const ancestors = await services.ancestorsService.ancestors(uuid, '');
134             dispatch(updateResources(ancestors));
135             const initialBreadcrumbs: Breadcrumb[] = [
136                 {
137                     label: category,
138                     uuid: category,
139                     icon: getSidePanelIcon(category)
140                 }
141             ];
142             const { collectionPanel: { item } } = getState();
143             const path = getState().router.location!.pathname;
144             const currentUuid = path.split('/')[2];
145             const uuidKind = extractUuidKind(currentUuid);
146             let breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
147                 ancestor.kind === ResourceKind.GROUP
148                     ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
149                     : breadcrumbs,
150                 initialBreadcrumbs);
151             if (uuidKind === ResourceKind.COLLECTION) {
152                 const collectionItem = item ? item : await services.collectionService.get(currentUuid);
153                 const parentProcessItem = await getCollectionParent(collectionItem)(services);
154                 if (parentProcessItem) {
155                     const mainProcessItem = await getProcessParent(parentProcessItem)(services);
156                     mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
157                     breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
158                 }
159                 dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
160             } else if (uuidKind === ResourceKind.PROCESS) {
161                 const processItem = await services.containerRequestService.get(currentUuid);
162                 const parentProcessItem = await getProcessParent(processItem)(services);
163                 if (parentProcessItem) {
164                     breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
165                 }
166                 dispatch(setBreadcrumbs(breadcrumbs, processItem));
167             } else if (uuidKind === ResourceKind.WORKFLOW) {
168                 const workflowItem = await services.workflowService.get(currentUuid);
169                 dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
170             }
171             dispatch(setBreadcrumbs(breadcrumbs));
172         } finally {
173             dispatch(progressIndicatorActions.STOP_WORKING(uuid + "-breadcrumbs"));
174         }
175     };
176
177 const getProcessParent = (childProcess: ContainerRequestResource) =>
178     async (services: ServiceRepository): Promise<ContainerRequestResource | undefined> => {
179         if (childProcess.requestingContainerUuid) {
180             const parentProcesses = await services.containerRequestService.list({
181                 order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
182                 filters: new FilterBuilder().addEqual('container_uuid', childProcess.requestingContainerUuid).getFilters(),
183                 select: containerRequestFieldsNoMounts,
184             });
185             if (parentProcesses.items.length > 0) {
186                 return parentProcesses.items[0];
187             } else {
188                 return undefined;
189             }
190         } else {
191             return undefined;
192         }
193     }
194
195 const getCollectionParent = (collection: CollectionResource) =>
196     async (services: ServiceRepository): Promise<ContainerRequestResource | undefined> => {
197         const parentOutputPromise = services.containerRequestService.list({
198             order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
199             filters: new FilterBuilder().addEqual('output_uuid', collection.uuid).getFilters(),
200             select: containerRequestFieldsNoMounts,
201         });
202         const parentLogPromise = services.containerRequestService.list({
203             order: new OrderBuilder<ProcessResource>().addAsc('createdAt').getOrder(),
204             filters: new FilterBuilder().addEqual('log_uuid', collection.uuid).getFilters(),
205             select: containerRequestFieldsNoMounts,
206         });
207         const [parentOutput, parentLog] = await Promise.all([parentOutputPromise, parentLogPromise]);
208         return parentOutput.items.length > 0 ?
209             parentOutput.items[0] :
210             parentLog.items.length > 0 ?
211                 parentLog.items[0] :
212                 undefined;
213     }
214
215
216 export const setProjectBreadcrumbs = (uuid: string) =>
217     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
218         const ancestors = await services.ancestorsService.ancestors(uuid, '');
219         const rootUuid = getUserUuid(getState());
220         if (uuid === rootUuid || ancestors.find(ancestor => ancestor.uuid === rootUuid)) {
221             dispatch(setSidePanelBreadcrumbs(uuid));
222         } else {
223             dispatch(setSharedWithMeBreadcrumbs(uuid));
224             dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
225         }
226     };
227
228 export const setProcessBreadcrumbs = (processUuid: string) =>
229     (dispatch: Dispatch, getState: () => RootState) => {
230         const { resources } = getState();
231         const process = getProcess(processUuid)(resources);
232         if (process) {
233             dispatch<any>(setProjectBreadcrumbs(process.containerRequest.ownerUuid));
234         }
235     };
236
237 export const setGroupsBreadcrumbs = () =>
238     setBreadcrumbs([{
239         label: SidePanelTreeCategory.GROUPS,
240         uuid: SidePanelTreeCategory.GROUPS,
241         icon: getSidePanelIcon(SidePanelTreeCategory.GROUPS)
242     }]);
243
244 export const setGroupDetailsBreadcrumbs = (groupUuid: string) =>
245     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
246
247         const group = getResource<GroupResource>(groupUuid)(getState().resources);
248
249         const breadcrumbs: Breadcrumb[] = [
250             {
251                 label: SidePanelTreeCategory.GROUPS,
252                 uuid: SidePanelTreeCategory.GROUPS,
253                 icon: getSidePanelIcon(SidePanelTreeCategory.GROUPS)
254             },
255             { label: group ? group.name : (await services.groupsService.get(groupUuid)).name, uuid: groupUuid },
256         ];
257
258         dispatch(setBreadcrumbs(breadcrumbs));
259
260     };
261
262 export const USERS_PANEL_LABEL = 'Users';
263
264 export const setUsersBreadcrumbs = () =>
265     setBreadcrumbs([{ label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL }]);
266
267 export const setUserProfileBreadcrumbs = (userUuid: string) =>
268     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
269         try {
270             const user = getResource<UserResource>(userUuid)(getState().resources)
271                 || await services.userService.get(userUuid, false);
272             const currentCrumbs = getState().properties.breadcrumbs as Breadcrumb[]
273             const userProfileBreadcrumbs: Breadcrumb[] = [
274                 { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
275                 { label: user ? `${user.firstName} ${user.lastName}` : userUuid, uuid: userUuid },
276             ];    
277             const breadcrumbsWithPreviousCrumbs: Breadcrumb[] = [
278                 ...currentCrumbs,
279                 { label: user ? `${user.firstName} ${user.lastName}` : userUuid, uuid: userUuid },
280             ];
281             dispatch(setBreadcrumbs(currentCrumbs.some((crumb) => crumb.label === SidePanelTreeCategory.GROUPS) ? breadcrumbsWithPreviousCrumbs : userProfileBreadcrumbs));
282         } catch (e) {
283             const breadcrumbs: Breadcrumb[] = [
284                 { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
285                 { label: userUuid, uuid: userUuid },
286             ];
287             dispatch(setBreadcrumbs(breadcrumbs));
288         }
289     };
290
291 export const MY_ACCOUNT_PANEL_LABEL = 'My Account';
292
293 export const setMyAccountBreadcrumbs = () =>
294     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
295         dispatch(setBreadcrumbs([
296             { label: MY_ACCOUNT_PANEL_LABEL, uuid: MY_ACCOUNT_PANEL_LABEL },
297         ]));
298     };
299
300 export const INSTANCE_TYPES_PANEL_LABEL = 'Instance Types';
301
302 export const setInstanceTypesBreadcrumbs = () =>
303     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
304         dispatch(setBreadcrumbs([
305             { label: INSTANCE_TYPES_PANEL_LABEL, uuid: INSTANCE_TYPES_PANEL_LABEL, icon: ResourceIcon },
306         ]));
307     };
308
309 export const setVirtualMachinesBreadcrumbs = () =>
310     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
311         dispatch(setBreadcrumbs([
312             { label: SidePanelTreeCategory.SHELL_ACCESS, uuid: SidePanelTreeCategory.SHELL_ACCESS, icon: TerminalIcon },
313         ]));
314     };
315
316 export const VIRTUAL_MACHINES_ADMIN_PANEL_LABEL = 'Shell Access Admin';
317
318 export const setVirtualMachinesAdminBreadcrumbs = () =>
319     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
320         dispatch(setBreadcrumbs([
321             { label: VIRTUAL_MACHINES_ADMIN_PANEL_LABEL, uuid: VIRTUAL_MACHINES_ADMIN_PANEL_LABEL, icon: AdminMenuIcon },
322         ]));
323     };
324
325 export const REPOSITORIES_PANEL_LABEL = 'Repositories';
326
327 export const setRepositoriesBreadcrumbs = () =>
328     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
329         dispatch(setBreadcrumbs([
330             { label: REPOSITORIES_PANEL_LABEL, uuid: REPOSITORIES_PANEL_LABEL, icon: AdminMenuIcon },
331         ]));
332     };