Merge branch 'main' into 22235-toolbar-access-fixes
[arvados.git] / services / workbench2 / src / store / context-menu / context-menu-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { unionize, ofType, UnionOf } from "common/unionize";
6 import { ContextMenuPosition } from "./context-menu-reducer";
7 import { ContextMenuKind } from "views-components/context-menu/menu-item-sort";
8 import { Dispatch } from "redux";
9 import { RootState } from "store/store";
10 import { getResource, getResourceWithEditableStatus } from "../resources/resources";
11 import { UserResource } from "models/user";
12 import { isSidePanelTreeCategory } from "store/side-panel-tree/side-panel-tree-actions";
13 import { extractUuidKind, ResourceKind, EditableResource, Resource } from "models/resource";
14 import { Process, isProcessCancelable } from "store/processes/process";
15 import { RepositoryResource } from "models/repositories";
16 import { SshKeyResource } from "models/ssh-key";
17 import { VirtualMachinesResource } from "models/virtual-machines";
18 import { KeepServiceResource } from "models/keep-services";
19 import { ProcessResource } from "models/process";
20 import { CollectionResource } from "models/collection";
21 import { GroupClass, GroupResource } from "models/group";
22 import { GroupContentsResource } from "services/groups-service/groups-service";
23 import { LinkResource } from "models/link";
24 import { resourceIsFrozen } from "common/frozen-resources";
25 import { ProjectResource } from "models/project";
26 import { getProcess } from "store/processes/process";
27 import { filterCollectionFilesBySelection } from "store/collection-panel/collection-panel-files/collection-panel-files-state";
28 import { selectOne, deselectAllOthers } from "store/multiselect/multiselect-actions";
29
30 export const contextMenuActions = unionize({
31     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
32     CLOSE_CONTEXT_MENU: ofType<{}>(),
33 });
34
35 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
36
37 export type ContextMenuResource = {
38     name: string;
39     uuid: string;
40     ownerUuid: string;
41     description?: string;
42     kind: ResourceKind;
43     menuKind: ContextMenuKind | string;
44     isTrashed?: boolean;
45     isEditable?: boolean;
46     outputUuid?: string;
47     workflowUuid?: string;
48     isAdmin?: boolean;
49     isFrozen?: boolean;
50     storageClassesDesired?: string[];
51     properties?: { [key: string]: string | string[] };
52     isMulti?: boolean;
53     fromContextMenu?: boolean;
54 };
55
56 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
57
58 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
59     event.preventDefault();
60     dispatch<any>(selectOne(resource.uuid));
61     dispatch<any>(deselectAllOthers(resource.uuid));
62     const { left, top } = event.currentTarget.getBoundingClientRect();
63     dispatch(
64         contextMenuActions.OPEN_CONTEXT_MENU({
65             position: {
66                 x: event.clientX || left,
67                 y: event.clientY || top,
68             },
69             resource,
70         })
71     );
72 };
73
74 export const openCollectionFilesContextMenu =
75     (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => (dispatch: Dispatch, getState: () => RootState) => {
76         const selectedCount = filterCollectionFilesBySelection(getState().collectionPanelFiles, true).length;
77         const multiple = selectedCount > 1;
78         dispatch<any>(
79             openContextMenu(event, {
80                 name: "",
81                 uuid: "",
82                 ownerUuid: "",
83                 description: "",
84                 kind: ResourceKind.COLLECTION,
85                 menuKind:
86                     selectedCount > 0
87                         ? isWritable
88                             ? multiple
89                                 ? ContextMenuKind.COLLECTION_FILES_MULTIPLE
90                                 : ContextMenuKind.COLLECTION_FILES
91                             : multiple
92                             ? ContextMenuKind.READONLY_COLLECTION_FILES_MULTIPLE
93                             : ContextMenuKind.READONLY_COLLECTION_FILES
94                         : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED,
95             })
96         );
97     };
98
99 export const openRepositoryContextMenu =
100     (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => {
101         dispatch<any>(
102             openContextMenu(event, {
103                 name: "",
104                 uuid: repository.uuid,
105                 ownerUuid: repository.ownerUuid,
106                 kind: ResourceKind.REPOSITORY,
107                 menuKind: ContextMenuKind.REPOSITORY,
108             })
109         );
110     };
111
112 export const openVirtualMachinesContextMenu =
113     (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) => (dispatch: Dispatch, getState: () => RootState) => {
114         dispatch<any>(
115             openContextMenu(event, {
116                 name: "",
117                 uuid: repository.uuid,
118                 ownerUuid: repository.ownerUuid,
119                 kind: ResourceKind.VIRTUAL_MACHINE,
120                 menuKind: ContextMenuKind.VIRTUAL_MACHINE,
121             })
122         );
123     };
124
125 export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => (dispatch: Dispatch) => {
126     dispatch<any>(
127         openContextMenu(event, {
128             name: "",
129             uuid: sshKey.uuid,
130             ownerUuid: sshKey.ownerUuid,
131             kind: ResourceKind.SSH_KEY,
132             menuKind: ContextMenuKind.SSH_KEY,
133         })
134     );
135 };
136
137 export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => (dispatch: Dispatch) => {
138     dispatch<any>(
139         openContextMenu(event, {
140             name: "",
141             uuid: keepService.uuid,
142             ownerUuid: keepService.ownerUuid,
143             kind: ResourceKind.KEEP_SERVICE,
144             menuKind: ContextMenuKind.KEEP_SERVICE,
145         })
146     );
147 };
148
149 export const openApiClientAuthorizationContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch) => {
150     dispatch<any>(
151         openContextMenu(event, {
152             name: "",
153             uuid: resourceUuid,
154             ownerUuid: "",
155             kind: ResourceKind.API_CLIENT_AUTHORIZATION,
156             menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION,
157         })
158     );
159 };
160
161 export const openRootProjectContextMenu =
162     (event: React.MouseEvent<HTMLElement>, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
163         const res = getResource<UserResource>(projectUuid)(getState().resources);
164         if (res) {
165             dispatch<any>(
166                 openContextMenu(event, {
167                     name: "",
168                     uuid: res.uuid,
169                     ownerUuid: res.uuid,
170                     kind: res.kind,
171                     menuKind: ContextMenuKind.ROOT_PROJECT,
172                     isTrashed: false,
173                 })
174             );
175         }
176     };
177
178 export const openProjectContextMenu =
179     (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
180         const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
181         const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
182         if (res && menuKind) {
183             dispatch<any>(
184                 openContextMenu(event, {
185                     name: res.name,
186                     uuid: res.uuid,
187                     kind: res.kind,
188                     menuKind,
189                     description: res.description,
190                     ownerUuid: res.ownerUuid,
191                     isTrashed: "isTrashed" in res ? res.isTrashed : false,
192                     isFrozen: !!(res as ProjectResource).frozenByUuid,
193                 })
194             );
195         }
196     };
197
198 export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) => (dispatch: Dispatch, getState: () => RootState) => {
199     if (!isSidePanelTreeCategory(id)) {
200         const kind = extractUuidKind(id);
201         if (kind === ResourceKind.USER) {
202             dispatch<any>(openRootProjectContextMenu(event, id));
203         } else if (kind === ResourceKind.PROJECT) {
204             dispatch<any>(openProjectContextMenu(event, id));
205         }
206     }
207 };
208
209 export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
210     const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
211     if (res) {
212         dispatch<any>(
213             openContextMenu(event, {
214                 uuid: res.uuid,
215                 ownerUuid: res.ownerUuid,
216                 kind: ResourceKind.PROCESS,
217                 name: res.name,
218                 description: res.description,
219                 outputUuid: res.outputUuid || "",
220                 workflowUuid: res.properties.template_uuid || "",
221                 menuKind: isProcessCancelable(process) ? ContextMenuKind.RUNNING_PROCESS_RESOURCE : ContextMenuKind.PROCESS_RESOURCE
222             })
223         );
224     }
225 };
226
227 export const openPermissionEditContextMenu =
228     (event: React.MouseEvent<HTMLElement>, link: LinkResource) => (dispatch: Dispatch, getState: () => RootState) => {
229         if (link) {
230             dispatch<any>(
231                 openContextMenu(event, {
232                     name: link.name,
233                     uuid: link.uuid,
234                     kind: link.kind,
235                     menuKind: ContextMenuKind.PERMISSION_EDIT,
236                     ownerUuid: link.ownerUuid,
237                 })
238             );
239         }
240     };
241
242 export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) => (dispatch: Dispatch, getState: () => RootState) => {
243     dispatch<any>(
244         openContextMenu(event, {
245             name: "",
246             uuid: user.uuid,
247             ownerUuid: user.ownerUuid,
248             kind: user.kind,
249             menuKind: ContextMenuKind.USER,
250         })
251     );
252 };
253
254 export const resourceUuidToContextMenuKind =
255     (uuid: string, readonly = false) =>
256     (dispatch: Dispatch, getState: () => RootState) => {
257         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
258         const kind = extractUuidKind(uuid);
259         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
260         const isFrozen = resourceIsFrozen(resource, getState().resources);
261         const isEditable = (isAdminUser || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
262
263         switch (kind) {
264             case ResourceKind.PROJECT:
265                 if (isFrozen) {
266                     return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN 
267                     : isEditable 
268                     ? ContextMenuKind.FROZEN_PROJECT
269                     : ContextMenuKind.READONLY_PROJECT;
270                 }
271
272                 return isAdminUser && !readonly
273                     ? resource && resource.groupClass !== GroupClass.FILTER
274                         ? ContextMenuKind.PROJECT_ADMIN
275                         : ContextMenuKind.FILTER_GROUP_ADMIN
276                     : isEditable
277                     ? resource && resource.groupClass !== GroupClass.FILTER
278                         ? ContextMenuKind.PROJECT
279                         : ContextMenuKind.FILTER_GROUP
280                     : ContextMenuKind.READONLY_PROJECT;
281             case ResourceKind.COLLECTION:
282                 const c = getResource<CollectionResource>(uuid)(getState().resources);
283                 if (c === undefined) {
284                     return;
285                 }
286                 const isOldVersion = c.uuid !== c.currentVersionUuid;
287                 const isTrashed = c.isTrashed;
288                 return isOldVersion
289                     ? ContextMenuKind.OLD_VERSION_COLLECTION
290                     : isTrashed && isEditable
291                     ? ContextMenuKind.TRASHED_COLLECTION
292                     : isAdminUser && isEditable
293                     ? ContextMenuKind.COLLECTION_ADMIN
294                     : isEditable
295                     ? ContextMenuKind.COLLECTION
296                     : ContextMenuKind.READONLY_COLLECTION;
297             case ResourceKind.PROCESS:
298                 return !isEditable
299                     ? ContextMenuKind.READONLY_PROCESS_RESOURCE
300                     : isAdminUser 
301                         ? resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
302                             ? ContextMenuKind.RUNNING_PROCESS_ADMIN
303                             : ContextMenuKind.PROCESS_ADMIN
304                         : resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
305                             ? ContextMenuKind.RUNNING_PROCESS_RESOURCE
306                             : ContextMenuKind.PROCESS_RESOURCE;
307             case ResourceKind.USER:
308                 return ContextMenuKind.ROOT_PROJECT;
309             case ResourceKind.LINK:
310                 return ContextMenuKind.LINK;
311             case ResourceKind.WORKFLOW:
312                 return isEditable ? ContextMenuKind.WORKFLOW : ContextMenuKind.READONLY_WORKFLOW;
313             default:
314                 return;
315         }
316     };
317
318 export const openSearchResultsContextMenu =
319     (event: React.MouseEvent<HTMLElement>, uuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
320         const res = getResource<Resource>(uuid)(getState().resources);
321         if (res) {
322             dispatch<any>(
323                 openContextMenu(event, {
324                     name: "",
325                     uuid: res.uuid,
326                     ownerUuid: "",
327                     kind: res.kind,
328                     menuKind: ContextMenuKind.SEARCH_RESULTS,
329                 })
330             );
331         }
332     };