1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { unionize, ofType, UnionOf } from "common/unionize";
6 import { ContextMenuPosition } from "./context-menu-reducer";
7 import { ContextMenuKind } from "views-components/context-menu/menu-item-sort";
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 import { selectOne, deselectAllOthers } from "store/multiselect/multiselect-actions";
30 export const contextMenuActions = unionize({
31 OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
32 CLOSE_CONTEXT_MENU: ofType<{}>(),
35 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
37 export type ContextMenuResource = {
43 menuKind: ContextMenuKind | string;
47 workflowUuid?: string;
50 storageClassesDesired?: string[];
51 properties?: { [key: string]: string | string[] };
53 fromContextMenu?: boolean;
56 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
58 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
59 event.preventDefault();
60 dispatch<any>(selectOne(resource.uuid));
61 dispatch<any>(deselectAllOthers(resource.uuid));
62 const { left, top } = event.currentTarget.getBoundingClientRect();
64 contextMenuActions.OPEN_CONTEXT_MENU({
66 x: event.clientX || left,
67 y: event.clientY || top,
74 export const openCollectionFilesContextMenu =
75 (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => (dispatch: Dispatch, getState: () => RootState) => {
76 const selectedCount = filterCollectionFilesBySelection(getState().collectionPanelFiles, true).length;
77 const multiple = selectedCount > 1;
79 openContextMenu(event, {
84 kind: ResourceKind.COLLECTION,
89 ? ContextMenuKind.COLLECTION_FILES_MULTIPLE
90 : ContextMenuKind.COLLECTION_FILES
92 ? ContextMenuKind.READONLY_COLLECTION_FILES_MULTIPLE
93 : ContextMenuKind.READONLY_COLLECTION_FILES
94 : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED,
99 export const openRepositoryContextMenu =
100 (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => {
102 openContextMenu(event, {
104 uuid: repository.uuid,
105 ownerUuid: repository.ownerUuid,
106 kind: ResourceKind.REPOSITORY,
107 menuKind: ContextMenuKind.REPOSITORY,
112 export const openVirtualMachinesContextMenu =
113 (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) => (dispatch: Dispatch, getState: () => RootState) => {
115 openContextMenu(event, {
117 uuid: repository.uuid,
118 ownerUuid: repository.ownerUuid,
119 kind: ResourceKind.VIRTUAL_MACHINE,
120 menuKind: ContextMenuKind.VIRTUAL_MACHINE,
125 export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => (dispatch: Dispatch) => {
127 openContextMenu(event, {
130 ownerUuid: sshKey.ownerUuid,
131 kind: ResourceKind.SSH_KEY,
132 menuKind: ContextMenuKind.SSH_KEY,
137 export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => (dispatch: Dispatch) => {
139 openContextMenu(event, {
141 uuid: keepService.uuid,
142 ownerUuid: keepService.ownerUuid,
143 kind: ResourceKind.KEEP_SERVICE,
144 menuKind: ContextMenuKind.KEEP_SERVICE,
149 export const openApiClientAuthorizationContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch) => {
151 openContextMenu(event, {
155 kind: ResourceKind.API_CLIENT_AUTHORIZATION,
156 menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION,
161 export const openRootProjectContextMenu =
162 (event: React.MouseEvent<HTMLElement>, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
163 const res = getResource<UserResource>(projectUuid)(getState().resources);
166 openContextMenu(event, {
171 menuKind: ContextMenuKind.ROOT_PROJECT,
178 export const openProjectContextMenu =
179 (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
180 const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
181 const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
182 if (res && menuKind) {
184 openContextMenu(event, {
189 description: res.description,
190 ownerUuid: res.ownerUuid,
191 isTrashed: "isTrashed" in res ? res.isTrashed : false,
192 isFrozen: !!(res as ProjectResource).frozenByUuid,
198 export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) => (dispatch: Dispatch, getState: () => RootState) => {
199 if (!isSidePanelTreeCategory(id)) {
200 const kind = extractUuidKind(id);
201 if (kind === ResourceKind.USER) {
202 dispatch<any>(openRootProjectContextMenu(event, id));
203 } else if (kind === ResourceKind.PROJECT) {
204 dispatch<any>(openProjectContextMenu(event, id));
209 export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
210 const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
213 openContextMenu(event, {
215 ownerUuid: res.ownerUuid,
216 kind: ResourceKind.PROCESS,
218 description: res.description,
219 outputUuid: res.outputUuid || "",
220 workflowUuid: res.properties.template_uuid || "",
221 menuKind: isProcessCancelable(process) ? ContextMenuKind.RUNNING_PROCESS_RESOURCE : ContextMenuKind.PROCESS_RESOURCE
227 export const openPermissionEditContextMenu =
228 (event: React.MouseEvent<HTMLElement>, link: LinkResource) => (dispatch: Dispatch, getState: () => RootState) => {
231 openContextMenu(event, {
235 menuKind: ContextMenuKind.PERMISSION_EDIT,
236 ownerUuid: link.ownerUuid,
242 export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) => (dispatch: Dispatch, getState: () => RootState) => {
244 openContextMenu(event, {
247 ownerUuid: user.ownerUuid,
249 menuKind: ContextMenuKind.USER,
254 export const resourceUuidToContextMenuKind =
255 (uuid: string, readonly = false) =>
256 (dispatch: Dispatch, getState: () => RootState) => {
257 const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
258 const kind = extractUuidKind(uuid);
259 const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
260 const isFrozen = resourceIsFrozen(resource, getState().resources);
261 const isEditable = (isAdminUser || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
264 case ResourceKind.PROJECT:
266 return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN
268 ? ContextMenuKind.FROZEN_PROJECT
269 : ContextMenuKind.READONLY_PROJECT;
272 return isAdminUser && !readonly
273 ? resource && resource.groupClass !== GroupClass.FILTER
274 ? ContextMenuKind.PROJECT_ADMIN
275 : ContextMenuKind.FILTER_GROUP_ADMIN
277 ? resource && resource.groupClass !== GroupClass.FILTER
278 ? ContextMenuKind.PROJECT
279 : ContextMenuKind.FILTER_GROUP
280 : ContextMenuKind.READONLY_PROJECT;
281 case ResourceKind.COLLECTION:
282 const c = getResource<CollectionResource>(uuid)(getState().resources);
283 if (c === undefined) {
286 const isOldVersion = c.uuid !== c.currentVersionUuid;
287 const isTrashed = c.isTrashed;
289 ? ContextMenuKind.OLD_VERSION_COLLECTION
290 : isTrashed && isEditable
291 ? ContextMenuKind.TRASHED_COLLECTION
292 : isAdminUser && isEditable
293 ? ContextMenuKind.COLLECTION_ADMIN
295 ? ContextMenuKind.COLLECTION
296 : ContextMenuKind.READONLY_COLLECTION;
297 case ResourceKind.PROCESS:
299 ? ContextMenuKind.READONLY_PROCESS_RESOURCE
301 ? resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
302 ? ContextMenuKind.RUNNING_PROCESS_ADMIN
303 : ContextMenuKind.PROCESS_ADMIN
304 : resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
305 ? ContextMenuKind.RUNNING_PROCESS_RESOURCE
306 : ContextMenuKind.PROCESS_RESOURCE;
307 case ResourceKind.USER:
308 return ContextMenuKind.ROOT_PROJECT;
309 case ResourceKind.LINK:
310 return ContextMenuKind.LINK;
311 case ResourceKind.WORKFLOW:
312 return isEditable ? ContextMenuKind.WORKFLOW : ContextMenuKind.READONLY_WORKFLOW;
318 export const openSearchResultsContextMenu =
319 (event: React.MouseEvent<HTMLElement>, uuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
320 const res = getResource<Resource>(uuid)(getState().resources);
323 openContextMenu(event, {
328 menuKind: ContextMenuKind.SEARCH_RESULTS,