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/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";
28 export const contextMenuActions = unionize({
29 OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
30 CLOSE_CONTEXT_MENU: ofType<{}>(),
33 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
35 export type ContextMenuResource = {
41 menuKind: ContextMenuKind | string;
45 workflowUuid?: string;
48 storageClassesDesired?: string[];
49 properties?: { [key: string]: string | string[] };
54 export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
56 export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
57 event.preventDefault();
58 const { left, top } = event.currentTarget.getBoundingClientRect();
60 contextMenuActions.OPEN_CONTEXT_MENU({
62 x: event.clientX || left,
63 y: event.clientY || top,
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');
74 openContextMenu(event, {
79 kind: ResourceKind.COLLECTION,
80 menuKind: isCollectionFileSelected
82 ? ContextMenuKind.COLLECTION_FILES
83 : ContextMenuKind.READONLY_COLLECTION_FILES
84 : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED,
89 export const openRepositoryContextMenu =
90 (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => {
92 openContextMenu(event, {
94 uuid: repository.uuid,
95 ownerUuid: repository.ownerUuid,
96 kind: ResourceKind.REPOSITORY,
97 menuKind: ContextMenuKind.REPOSITORY,
102 export const openVirtualMachinesContextMenu =
103 (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) => (dispatch: Dispatch, getState: () => RootState) => {
105 openContextMenu(event, {
107 uuid: repository.uuid,
108 ownerUuid: repository.ownerUuid,
109 kind: ResourceKind.VIRTUAL_MACHINE,
110 menuKind: ContextMenuKind.VIRTUAL_MACHINE,
115 export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => (dispatch: Dispatch) => {
117 openContextMenu(event, {
120 ownerUuid: sshKey.ownerUuid,
121 kind: ResourceKind.SSH_KEY,
122 menuKind: ContextMenuKind.SSH_KEY,
127 export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => (dispatch: Dispatch) => {
129 openContextMenu(event, {
131 uuid: keepService.uuid,
132 ownerUuid: keepService.ownerUuid,
133 kind: ResourceKind.KEEP_SERVICE,
134 menuKind: ContextMenuKind.KEEP_SERVICE,
139 export const openApiClientAuthorizationContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch) => {
141 openContextMenu(event, {
145 kind: ResourceKind.API_CLIENT_AUTHORIZATION,
146 menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION,
151 export const openRootProjectContextMenu =
152 (event: React.MouseEvent<HTMLElement>, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
153 const res = getResource<UserResource>(projectUuid)(getState().resources);
156 openContextMenu(event, {
161 menuKind: ContextMenuKind.ROOT_PROJECT,
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) {
174 openContextMenu(event, {
179 description: res.description,
180 ownerUuid: res.ownerUuid,
181 isTrashed: "isTrashed" in res ? res.isTrashed : false,
182 isFrozen: !!(res as ProjectResource).frozenByUuid,
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));
199 export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
200 const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
203 openContextMenu(event, {
205 ownerUuid: res.ownerUuid,
206 kind: ResourceKind.PROCESS,
208 description: res.description,
209 outputUuid: res.outputUuid || "",
210 workflowUuid: res.properties.template_uuid || "",
211 menuKind: ContextMenuKind.PROCESS_RESOURCE,
217 export const openPermissionEditContextMenu =
218 (event: React.MouseEvent<HTMLElement>, link: LinkResource) => (dispatch: Dispatch, getState: () => RootState) => {
221 openContextMenu(event, {
225 menuKind: ContextMenuKind.PERMISSION_EDIT,
226 ownerUuid: link.ownerUuid,
232 export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) => (dispatch: Dispatch, getState: () => RootState) => {
234 openContextMenu(event, {
237 ownerUuid: user.ownerUuid,
239 menuKind: ContextMenuKind.USER,
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;
254 case ResourceKind.PROJECT:
256 return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT;
259 return isAdminUser && !readonly
260 ? resource && resource.groupClass !== GroupClass.FILTER
261 ? ContextMenuKind.PROJECT_ADMIN
262 : ContextMenuKind.FILTER_GROUP_ADMIN
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) {
273 const isOldVersion = c.uuid !== c.currentVersionUuid;
274 const isTrashed = c.isTrashed;
276 ? ContextMenuKind.OLD_VERSION_COLLECTION
277 : isTrashed && isEditable
278 ? ContextMenuKind.TRASHED_COLLECTION
279 : isAdminUser && isEditable
280 ? ContextMenuKind.COLLECTION_ADMIN
282 ? ContextMenuKind.COLLECTION
283 : ContextMenuKind.READONLY_COLLECTION;
284 case ResourceKind.PROCESS:
285 return isAdminUser && isEditable
286 ? ContextMenuKind.PROCESS_ADMIN
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;
301 export const openSearchResultsContextMenu =
302 (event: React.MouseEvent<HTMLElement>, uuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
303 const res = getResource<Resource>(uuid)(getState().resources);
306 openContextMenu(event, {
311 menuKind: ContextMenuKind.SEARCH_RESULTS,