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