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