17119: Merge branch 'master' into 17119-support-filter-groups
[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 } 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 { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action';
24
25 export const contextMenuActions = unionize({
26     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
27     CLOSE_CONTEXT_MENU: ofType<{}>()
28 });
29
30 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
31
32 export type ContextMenuResource = {
33     name: string;
34     uuid: string;
35     ownerUuid: string;
36     description?: string;
37     kind: ResourceKind,
38     menuKind: ContextMenuKind;
39     isTrashed?: boolean;
40     isEditable?: boolean;
41     outputUuid?: string;
42     workflowUuid?: string;
43 };
44
45 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
46
47 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
48     (dispatch: Dispatch) => {
49         event.preventDefault();
50         const { left, top } = event.currentTarget.getBoundingClientRect();
51         dispatch(
52             contextMenuActions.OPEN_CONTEXT_MENU({
53                 position: {
54                     x: event.clientX || left,
55                     y: event.clientY || top,
56                 },
57                 resource
58             })
59         );
60     };
61
62 export const openCollectionFilesContextMenu = (event: React.MouseEvent<HTMLElement>, isWritable: boolean) =>
63     (dispatch: Dispatch, getState: () => RootState) => {
64         const isCollectionFileSelected = JSON.stringify(getState().collectionPanelFiles).includes('"selected":true');
65         dispatch<any>(openContextMenu(event, {
66             name: '',
67             uuid: '',
68             ownerUuid: '',
69             kind: ResourceKind.COLLECTION,
70             menuKind: isCollectionFileSelected
71                 ? isWritable
72                     ? ContextMenuKind.COLLECTION_FILES
73                     : ContextMenuKind.READONLY_COLLECTION_FILES
74                 : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED
75         }));
76     };
77
78 export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) =>
79     (dispatch: Dispatch, getState: () => RootState) => {
80         dispatch<any>(openContextMenu(event, {
81             name: '',
82             uuid: repository.uuid,
83             ownerUuid: repository.ownerUuid,
84             kind: ResourceKind.REPOSITORY,
85             menuKind: ContextMenuKind.REPOSITORY
86         }));
87     };
88
89 export const openVirtualMachinesContextMenu = (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) =>
90     (dispatch: Dispatch, getState: () => RootState) => {
91         dispatch<any>(openContextMenu(event, {
92             name: '',
93             uuid: repository.uuid,
94             ownerUuid: repository.ownerUuid,
95             kind: ResourceKind.VIRTUAL_MACHINE,
96             menuKind: ContextMenuKind.VIRTUAL_MACHINE
97         }));
98     };
99
100 export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) =>
101     (dispatch: Dispatch) => {
102         dispatch<any>(openContextMenu(event, {
103             name: '',
104             uuid: sshKey.uuid,
105             ownerUuid: sshKey.ownerUuid,
106             kind: ResourceKind.SSH_KEY,
107             menuKind: ContextMenuKind.SSH_KEY
108         }));
109     };
110
111 export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) =>
112     (dispatch: Dispatch) => {
113         dispatch<any>(openContextMenu(event, {
114             name: '',
115             uuid: keepService.uuid,
116             ownerUuid: keepService.ownerUuid,
117             kind: ResourceKind.KEEP_SERVICE,
118             menuKind: ContextMenuKind.KEEP_SERVICE
119         }));
120     };
121
122 export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
123     (dispatch: Dispatch) => {
124         dispatch<any>(openContextMenu(event, {
125             name: '',
126             uuid: resourceUuid,
127             ownerUuid: '',
128             kind: ResourceKind.NODE,
129             menuKind: ContextMenuKind.NODE
130         }));
131     };
132
133 export const openApiClientAuthorizationContextMenu =
134     (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
135         (dispatch: Dispatch) => {
136             dispatch<any>(openContextMenu(event, {
137                 name: '',
138                 uuid: resourceUuid,
139                 ownerUuid: '',
140                 kind: ResourceKind.API_CLIENT_AUTHORIZATION,
141                 menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION
142             }));
143         };
144
145 export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
146     (dispatch: Dispatch, getState: () => RootState) => {
147         const res = getResource<UserResource>(projectUuid)(getState().resources);
148         if (res) {
149             dispatch<any>(openContextMenu(event, {
150                 name: '',
151                 uuid: res.uuid,
152                 ownerUuid: res.uuid,
153                 kind: res.kind,
154                 menuKind: ContextMenuKind.ROOT_PROJECT,
155                 isTrashed: false
156             }));
157         }
158     };
159
160 export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
161     (dispatch: Dispatch, getState: () => RootState) => {
162         const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
163         const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
164         if (res && menuKind) {
165             dispatch<any>(openContextMenu(event, {
166                 name: res.name,
167                 uuid: res.uuid,
168                 kind: res.kind,
169                 menuKind,
170                 ownerUuid: res.ownerUuid,
171                 isTrashed: ('isTrashed' in res) ? res.isTrashed: false,
172             }));
173         }
174     };
175
176 export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
177     (dispatch: Dispatch, getState: () => RootState) => {
178         if (!isSidePanelTreeCategory(id)) {
179             const kind = extractUuidKind(id);
180             if (kind === ResourceKind.USER) {
181                 dispatch<any>(openRootProjectContextMenu(event, id));
182             } else if (kind === ResourceKind.PROJECT) {
183                 dispatch<any>(openProjectContextMenu(event, id));
184             }
185         }
186     };
187
188 export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
189     (dispatch: Dispatch, getState: () => RootState) => {
190         const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
191         if (res) {
192             dispatch<any>(openContextMenu(event, {
193                 uuid: res.uuid,
194                 ownerUuid: res.ownerUuid,
195                 kind: ResourceKind.PROCESS,
196                 name: res.name,
197                 description: res.description,
198                 outputUuid: res.outputUuid || '',
199                 workflowUuid: res.properties.workflowUuid || '',
200                 menuKind: ContextMenuKind.PROCESS
201             }));
202         }
203     };
204
205 export const resourceUuidToContextMenuKind = (uuid: string) =>
206     (dispatch: Dispatch, getState: () => RootState) => {
207         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
208         const kind = extractUuidKind(uuid);
209         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
210         // When viewing the contents of a filter group, all contents should be treated as read only.
211         let inFilterGroup = false;
212         const projectUuid = getProjectPanelCurrentUuid(getState());
213         if (projectUuid !== undefined) {
214           const project = getResource<GroupResource>(projectUuid)(getState().resources);
215           if (project) {
216             if (project.groupClass === GroupClass.FILTER) {
217               inFilterGroup = true;
218             }
219           }
220         }
221         const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !inFilterGroup;
222
223         switch (kind) {
224             case ResourceKind.PROJECT:
225                 return (isAdminUser && !inFilterGroup)
226                     ? (resource && resource.groupClass === GroupClass.PROJECT)
227                         ? ContextMenuKind.PROJECT_ADMIN
228                         : ContextMenuKind.READONLY_PROJECT
229                     : isEditable
230                         ? ContextMenuKind.PROJECT
231                         : ContextMenuKind.READONLY_PROJECT;
232             case ResourceKind.COLLECTION:
233                 const c = getResource<CollectionResource>(uuid)(getState().resources);
234                 if (c === undefined) { return; }
235                 const isOldVersion = c.uuid !== c.currentVersionUuid;
236                 const isTrashed = c.isTrashed;
237                 return isOldVersion
238                     ? ContextMenuKind.OLD_VERSION_COLLECTION
239                     : (isTrashed && isEditable)
240                         ? ContextMenuKind.TRASHED_COLLECTION
241                         : (isAdminUser && !inFilterGroup)
242                             ? ContextMenuKind.COLLECTION_ADMIN
243                             : isEditable
244                                 ? ContextMenuKind.COLLECTION
245                                 : ContextMenuKind.READONLY_COLLECTION;
246             case ResourceKind.PROCESS:
247                 return (isAdminUser && !inFilterGroup)
248                     ? ContextMenuKind.PROCESS_ADMIN
249                     : isEditable
250                         ? ContextMenuKind.PROCESS_RESOURCE
251                         : ContextMenuKind.READONLY_PROCESS_RESOURCE;
252             case ResourceKind.USER:
253                 return ContextMenuKind.ROOT_PROJECT;
254             case ResourceKind.LINK:
255                 return ContextMenuKind.LINK;
256             default:
257                 return;
258         }
259     };