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