21700: Install Bundler system-wide in Rails postinst
[arvados.git] / services / workbench2 / src / components / multiselect-toolbar / MultiselectToolbar.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from "react";
6 import { connect } from "react-redux";
7 import { StyleRulesCallback, withStyles, WithStyles, Toolbar, Tooltip, IconButton } from "@material-ui/core";
8 import { ArvadosTheme } from "common/custom-theme";
9 import { RootState } from "store/store";
10 import { Dispatch } from "redux";
11 import { TCheckedList } from "components/data-table/data-table";
12 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
13 import { Resource, ResourceKind, extractUuidKind } from "models/resource";
14 import { getResource } from "store/resources/resources";
15 import { ResourcesState } from "store/resources/resources";
16 import { MultiSelectMenuAction, MultiSelectMenuActionSet } from "views-components/multiselect-toolbar/ms-menu-actions";
17 import { ContextMenuAction, ContextMenuActionNames } from "views-components/context-menu/context-menu-action-set";
18 import { multiselectActionsFilters, TMultiselectActionsFilters } from "./ms-toolbar-action-filters";
19 import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
20 import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set";
21 import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
22 import { ContainerRequestResource } from "models/container-request";
23 import { FavoritesState } from "store/favorites/favorites-reducer";
24 import { resourceIsFrozen } from "common/frozen-resources";
25 import { getResourceWithEditableStatus } from "store/resources/resources";
26 import { GroupResource } from "models/group";
27 import { EditableResource } from "models/resource";
28 import { User } from "models/user";
29 import { GroupClass } from "models/group";
30 import { isProcessCancelable } from "store/processes/process";
31 import { CollectionResource } from "models/collection";
32 import { getProcess } from "store/processes/process";
33 import { Process } from "store/processes/process";
34 import { PublicFavoritesState } from "store/public-favorites/public-favorites-reducer";
35 import { isExactlyOneSelected } from "store/multiselect/multiselect-actions";
36 import { IntersectionObserverWrapper } from "./ms-toolbar-overflow-wrapper";
37 import { ContextMenuKind, sortMenuItems, menuDirection } from 'views-components/context-menu/menu-item-sort';
38
39 type CssRules = "root" | "button" | "iconContainer" | "icon" | "divider";
40
41 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
42     root: {
43         display: "flex",
44         flexDirection: "row",
45         width: 0,
46         height: '2.7rem',
47         padding: 0,
48         margin: "1rem auto auto 0.3rem",
49         overflow: 'hidden',
50     },
51     button: {
52         width: "2.5rem",
53         height: "2.5rem ",
54         paddingLeft: 0,
55         border: "1px solid transparent",
56     },
57     iconContainer: {
58         height: '100%',
59     },
60     icon: {
61         marginLeft: '-0.5rem',
62     },
63     divider: {
64         display: "flex",
65         alignItems: "center",
66     },
67 });
68
69 export type MultiselectToolbarProps = {
70     checkedList: TCheckedList;
71     singleSelectedUuid: string | null
72     iconProps: IconProps
73     user: User | null
74     disabledButtons: Set<string>
75     executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
76 };
77
78 type IconProps = {
79     resources: ResourcesState;
80     favorites: FavoritesState;
81     publicFavorites: PublicFavoritesState;
82 }
83
84 export const MultiselectToolbar = connect(
85     mapStateToProps,
86     mapDispatchToProps
87 )(
88     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
89         const { classes, checkedList, singleSelectedUuid, iconProps, user, disabledButtons } = props;
90         const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null
91         const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
92         const currentPathIsTrash = window.location.pathname === "/trash";
93
94         const rawActions =
95             currentPathIsTrash && selectedToKindSet(checkedList).size
96                 ? [msToggleTrashAction]
97                 : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) =>
98                         singleSelectedUuid === null ? action.isForMulti : true
99                     );
100                     
101         const actions: ContextMenuAction[] | MultiSelectMenuAction[] = sortMenuItems(
102             singleResourceKind && singleResourceKind.length ? (singleResourceKind[0] as ContextMenuKind) : ContextMenuKind.MULTI,
103             rawActions,
104             menuDirection.HORIZONTAL
105         ); 
106
107         return (
108             <React.Fragment>
109                 <Toolbar
110                     className={classes.root}
111                     style={{ width: `${(actions.length * 2.5) + 6}rem`}}
112                     data-cy='multiselect-toolbar'
113                     >
114                     {actions.length ? (
115                         <IntersectionObserverWrapper menuLength={actions.length}>
116                             {actions.map((action, i) =>{
117                                 const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
118                             return action.name === ContextMenuActionNames.DIVIDER ? (
119                                 action.component && (
120                                     <div
121                                         className={classes.divider}
122                                         data-targetid={`${name}${i}`}
123                                         key={i}
124                                     >
125                                         <action.component />
126                                     </div>
127                                 )
128                             ) : hasAlts ? (
129                                 <Tooltip
130                                     className={classes.button}
131                                     data-targetid={name}
132                                     title={currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altName : name}
133                                     key={i}
134                                     disableFocusListener
135                                 >
136                                     <span className={classes.iconContainer}>
137                                         <IconButton
138                                             data-cy='multiselect-button'
139                                             disabled={disabledButtons.has(name)}
140                                             onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
141                                             className={classes.icon}
142                                         >
143                                             {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
144                                         </IconButton>
145                                     </span>
146                                 </Tooltip>
147                             ) : (
148                                 <Tooltip
149                                     className={classes.button}
150                                     data-targetid={name}
151                                     title={action.name}
152                                     key={i}
153                                     disableFocusListener
154                                 >
155                                     <span className={classes.iconContainer}>
156                                         <IconButton
157                                             data-cy='multiselect-button'
158                                             onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
159                                             className={classes.icon}
160                                         >
161                                             {action.icon({})}
162                                         </IconButton>
163                                     </span>
164                                 </Tooltip>
165                             );
166                             })}
167                         </IntersectionObserverWrapper>
168                     ) : (
169                         <></>
170                     )}
171                 </Toolbar>
172             </React.Fragment>
173         )
174     })
175 );
176
177 export function selectedToArray(checkedList: TCheckedList): Array<string> {
178     const arrayifiedSelectedList: Array<string> = [];
179     for (const [key, value] of Object.entries(checkedList)) {
180         if (value === true) {
181             arrayifiedSelectedList.push(key);
182         }
183     }
184     return arrayifiedSelectedList;
185 }
186
187 export function selectedToKindSet(checkedList: TCheckedList): Set<string> {
188     const setifiedList = new Set<string>();
189     for (const [key, value] of Object.entries(checkedList)) {
190         if (value === true) {
191             setifiedList.add(extractUuidKind(key) as string);
192         }
193     }
194     return setifiedList;
195 }
196
197 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
198     const result = {};
199     selectedToArray(checkedList).forEach(uuid => {
200         const resource = getResource(uuid)(resources) as ContainerRequestResource | Resource;
201         if (!result[resource.kind]) result[resource.kind] = [];
202         result[resource.kind].push(resource);
203     });
204     return result;
205 }
206
207 function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
208     return actionArray[0].filter(action => filters.has(action.name as string));
209 }
210
211 const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (ContextMenuKind | ResourceKind) | undefined => {
212     if (!user) return;
213     const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, user.uuid)(resources);
214     const { isAdmin } = user;
215     const kind = extractUuidKind(uuid);
216
217     const isFrozen = resourceIsFrozen(resource, resources);
218     const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
219
220     switch (kind) {
221         case ResourceKind.PROJECT:
222             if (isFrozen) {
223                 return isAdmin ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT;
224             }
225
226             return isAdmin && !readonly
227                 ? resource && resource.groupClass !== GroupClass.FILTER
228                     ? ContextMenuKind.PROJECT_ADMIN
229                     : ContextMenuKind.FILTER_GROUP_ADMIN
230                 : isEditable
231                 ? resource && resource.groupClass !== GroupClass.FILTER
232                     ? ContextMenuKind.PROJECT
233                     : ContextMenuKind.FILTER_GROUP
234                 : ContextMenuKind.READONLY_PROJECT;
235         case ResourceKind.COLLECTION:
236             const c = getResource<CollectionResource>(uuid)(resources);
237             if (c === undefined) {
238                 return;
239             }
240             const isOldVersion = c.uuid !== c.currentVersionUuid;
241             const isTrashed = c.isTrashed;
242             return isOldVersion
243                 ? ContextMenuKind.OLD_VERSION_COLLECTION
244                 : isTrashed && isEditable
245                 ? ContextMenuKind.TRASHED_COLLECTION
246                 : isAdmin && isEditable
247                 ? ContextMenuKind.COLLECTION_ADMIN
248                 : isEditable
249                 ? ContextMenuKind.COLLECTION
250                 : ContextMenuKind.READONLY_COLLECTION;
251         case ResourceKind.PROCESS:
252             return isAdmin && isEditable
253                 ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
254                     ? ContextMenuKind.RUNNING_PROCESS_ADMIN
255                     : ContextMenuKind.PROCESS_ADMIN
256                 : readonly
257                 ? ContextMenuKind.READONLY_PROCESS_RESOURCE
258                 : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
259                 ? ContextMenuKind.RUNNING_PROCESS_RESOURCE
260                 : ContextMenuKind.PROCESS_RESOURCE;
261         case ResourceKind.USER:
262             return ContextMenuKind.ROOT_PROJECT;
263         case ResourceKind.LINK:
264             return ContextMenuKind.LINK;
265         case ResourceKind.WORKFLOW:
266             return isEditable ? ContextMenuKind.WORKFLOW : ContextMenuKind.READONLY_WORKFLOW;
267         default:
268             return;
269     }
270 }; 
271
272 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters): MultiSelectMenuAction[] {
273     const rawResult: Set<MultiSelectMenuAction> = new Set();
274     const resultNames = new Set();
275     const allFiltersArray: MultiSelectMenuAction[][] = []
276     currentResourceKinds.forEach(kind => {
277         if (filterSet[kind]) {
278             const actions = filterActions(...filterSet[kind]);
279             allFiltersArray.push(actions);
280             actions.forEach(action => {
281                 if (!resultNames.has(action.name)) {
282                     rawResult.add(action);
283                     resultNames.add(action.name);
284                 }
285             });
286         }
287     });
288
289     const filteredNameSet = allFiltersArray.map(filterArray => {
290         const resultSet = new Set<string>();
291         filterArray.forEach(action => resultSet.add(action.name as string || ""));
292         return resultSet;
293     });
294
295     const filteredResult = Array.from(rawResult).filter(action => {
296         for (let i = 0; i < filteredNameSet.length; i++) {
297             if (!filteredNameSet[i].has(action.name as string)) return false;
298         }
299         return true;
300     });
301
302     return filteredResult;
303 }
304
305
306 //--------------------------------------------------//
307
308 function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) {
309     return {
310         checkedList: multiselect.checkedList as TCheckedList,
311         singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList),
312         user: auth && auth.user ? auth.user : null,
313         disabledButtons: new Set<string>(multiselect.disabledButtons),
314         iconProps: {
315             resources,
316             favorites,
317             publicFavorites
318         }
319     }
320 }
321
322 function mapDispatchToProps(dispatch: Dispatch) {
323     return {
324         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
325             const kindGroups = groupByKind(checkedList, resources);
326             switch (selectedAction.name) {
327                 case ContextMenuActionNames.MOVE_TO:
328                 case ContextMenuActionNames.REMOVE:
329                     const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
330                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
331                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
332                     break;
333                 case ContextMenuActionNames.COPY_LINK_TO_CLIPBOARD:
334                     const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
335                     dispatch<any>(copyToClipboardAction(selectedResources));
336                     break;
337                 default:
338                     for (const kind in kindGroups) {
339                         const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
340                         if (action) action.execute(dispatch, kindGroups[kind]);
341                     }
342                     break;
343             }
344         },
345     };
346 }