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