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