1c0b2dea4eb4680b08adb6c56d3cb52bb7d00f9e
[arvados.git] / src / store / tree-picker / tree-picker-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 { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, TreePickerId, Tree } from 'models/tree';
7 import { createCollectionFilesTree } from "models/collection-file";
8 import { Dispatch } from 'redux';
9 import { RootState } from 'store/store';
10 import { getUserUuid } from "common/getuser";
11 import { ServiceRepository } from 'services/services';
12 import { FilterBuilder } from 'services/api/filter-builder';
13 import { pipe, values } from 'lodash/fp';
14 import { ResourceKind } from 'models/resource';
15 import { GroupContentsResource } from 'services/groups-service/groups-service';
16 import { getTreePicker, TreePicker } from './tree-picker';
17 import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
18 import { OrderBuilder } from 'services/api/order-builder';
19 import { ProjectResource } from 'models/project';
20 import { mapTree } from '../../models/tree';
21 import { LinkResource, LinkClass } from "models/link";
22 import { mapTreeValues } from "models/tree";
23 import { sortFilesTree } from "services/collection-service/collection-service-files-response";
24 import { GroupClass, GroupResource } from "models/group";
25
26 export const treePickerActions = unionize({
27     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
28     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ id: string, nodes: Array<TreeNode<any>>, pickerId: string }>(),
29     APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
30     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
31     ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
32     DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
33     TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
34     SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
35     DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
36     EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
37     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
38 });
39
40 export type TreePickerAction = UnionOf<typeof treePickerActions>;
41
42 export const getProjectsTreePickerIds = (pickerId: string) => ({
43     home: `${pickerId}_home`,
44     shared: `${pickerId}_shared`,
45     favorites: `${pickerId}_favorites`,
46     publicFavorites: `${pickerId}_publicFavorites`
47 });
48
49 export const getAllNodes = <Value>(pickerId: string, filter = (node: TreeNode<Value>) => true) => (state: TreePicker) =>
50     pipe(
51         () => values(getProjectsTreePickerIds(pickerId)),
52
53         ids => ids
54             .map(id => getTreePicker<Value>(id)(state)),
55
56         trees => trees
57             .map(getNodeDescendants(''))
58             .reduce((allNodes, nodes) => allNodes.concat(nodes), []),
59
60         allNodes => allNodes
61             .reduce((map, node) =>
62                 filter(node)
63                     ? map.set(node.id, node)
64                     : map, new Map<string, TreeNode<Value>>())
65             .values(),
66
67         uniqueNodes => Array.from(uniqueNodes),
68     )();
69 export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
70     getAllNodes<Value>(pickerId, node => node.selected)(state);
71
72 export const initProjectsTreePicker = (pickerId: string) =>
73     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
74         const { home, shared, favorites, publicFavorites } = getProjectsTreePickerIds(pickerId);
75         dispatch<any>(initUserProject(home));
76         dispatch<any>(initSharedProject(shared));
77         dispatch<any>(initFavoritesProject(favorites));
78         dispatch<any>(initPublicFavoritesProject(publicFavorites));
79     };
80
81 interface ReceiveTreePickerDataParams<T> {
82     data: T[];
83     extractNodeData: (value: T) => { id: string, value: T, status?: TreeNodeStatus };
84     id: string;
85     pickerId: string;
86 }
87
88 export const receiveTreePickerData = <T>(params: ReceiveTreePickerDataParams<T>) =>
89     (dispatch: Dispatch) => {
90         const { data, extractNodeData, id, pickerId, } = params;
91         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
92             id,
93             nodes: data.map(item => initTreeNode(extractNodeData(item))),
94             pickerId,
95         }));
96         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
97     };
98
99 interface LoadProjectParams {
100     id: string;
101     pickerId: string;
102     includeCollections?: boolean;
103     includeFiles?: boolean;
104     includeFilterGroups?: boolean;
105     loadShared?: boolean;
106     options?: { showOnlyOwned: boolean; showOnlyWritable: boolean; };
107 }
108 export const loadProject = (params: LoadProjectParams) =>
109     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
110         const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false, options } = params;
111
112         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
113
114         const filters = pipe(
115             (fb: FilterBuilder) => includeCollections
116                 ? fb.addIsA('uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
117                 : fb.addIsA('uuid', [ResourceKind.PROJECT]),
118             fb => fb.addNotIn("collections.properties.type", ["intermediate", "log"]),
119             fb => fb.getFilters(),
120         )(new FilterBuilder());
121
122         const { items, itemsAvailable } = await services.groupsService.contents(loadShared ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: 1000 });
123
124         if (itemsAvailable > 1000) {
125             items.push({
126                 uuid: "more-items-available",
127                 kind: ResourceKind.WORKFLOW,
128                 name: "*** Not all items were loaded (limit 1000 items) ***",
129                 description: "",
130                 definition: "",
131                 ownerUuid: "",
132                 createdAt: "",
133                 modifiedByClientUuid: "",
134                 modifiedByUserUuid: "",
135                 modifiedAt: "",
136                 href: "",
137                 etag: ""
138             });
139         }
140
141         dispatch<any>(receiveTreePickerData<GroupContentsResource>({
142             id,
143             pickerId,
144             data: items.filter((item) => {
145                 if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
146                     return false;
147                 }
148
149                 if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
150                     return false;
151                 }
152
153                 return true;
154             }),
155             extractNodeData: item => (
156                 item.uuid === "more-items-available" ?
157                     {
158                         id: item.uuid,
159                         value: item,
160                         status: TreeNodeStatus.LOADED
161                     }
162                     : {
163                         id: item.uuid,
164                         value: item,
165                         status: item.kind === ResourceKind.PROJECT
166                             ? TreeNodeStatus.INITIAL
167                             : includeFiles
168                                 ? TreeNodeStatus.INITIAL
169                                 : TreeNodeStatus.LOADED
170                     }),
171         }));
172     };
173
174 export const loadCollection = (id: string, pickerId: string) =>
175     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
176         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
177
178         const picker = getTreePicker<ProjectsTreePickerItem>(pickerId)(getState().treePicker);
179         if (picker) {
180
181             const node = getNode(id)(picker);
182             if (node && 'kind' in node.value && node.value.kind === ResourceKind.COLLECTION) {
183                 const files = await services.collectionService.files(node.value.portableDataHash);
184                 const tree = createCollectionFilesTree(files);
185                 const sorted = sortFilesTree(tree);
186                 const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
187
188                 dispatch(
189                     treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
190                         id,
191                         pickerId,
192                         subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
193                     }));
194
195                 dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
196             }
197         }
198     };
199
200
201 export const initUserProject = (pickerId: string) =>
202     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
203         const uuid = getUserUuid(getState());
204         if (uuid) {
205             dispatch(receiveTreePickerData({
206                 id: '',
207                 pickerId,
208                 data: [{ uuid, name: 'Projects' }],
209                 extractNodeData: value => ({
210                     id: value.uuid,
211                     status: TreeNodeStatus.INITIAL,
212                     value,
213                 }),
214             }));
215         }
216     };
217 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false, options?: { showOnlyOwned: boolean, showOnlyWritable: boolean }) =>
218     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
219         const uuid = getUserUuid(getState());
220         if (uuid) {
221             dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles, options }));
222         }
223     };
224
225 export const SHARED_PROJECT_ID = 'Shared with me';
226 export const initSharedProject = (pickerId: string) =>
227     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
228         dispatch(receiveTreePickerData({
229             id: '',
230             pickerId,
231             data: [{ uuid: SHARED_PROJECT_ID, name: SHARED_PROJECT_ID }],
232             extractNodeData: value => ({
233                 id: value.uuid,
234                 status: TreeNodeStatus.INITIAL,
235                 value,
236             }),
237         }));
238     };
239
240 export const FAVORITES_PROJECT_ID = 'Favorites';
241 export const initFavoritesProject = (pickerId: string) =>
242     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
243         dispatch(receiveTreePickerData({
244             id: '',
245             pickerId,
246             data: [{ uuid: FAVORITES_PROJECT_ID, name: FAVORITES_PROJECT_ID }],
247             extractNodeData: value => ({
248                 id: value.uuid,
249                 status: TreeNodeStatus.INITIAL,
250                 value,
251             }),
252         }));
253     };
254
255 export const PUBLIC_FAVORITES_PROJECT_ID = 'Public Favorites';
256 export const initPublicFavoritesProject = (pickerId: string) =>
257     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
258         dispatch(receiveTreePickerData({
259             id: '',
260             pickerId,
261             data: [{ uuid: PUBLIC_FAVORITES_PROJECT_ID, name: PUBLIC_FAVORITES_PROJECT_ID }],
262             extractNodeData: value => ({
263                 id: value.uuid,
264                 status: TreeNodeStatus.INITIAL,
265                 value,
266             }),
267         }));
268     };
269
270 interface LoadFavoritesProjectParams {
271     pickerId: string;
272     includeCollections?: boolean;
273     includeFiles?: boolean;
274     options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
275 }
276
277 export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
278     options: { showOnlyOwned: boolean, showOnlyWritable: boolean } = { showOnlyOwned: true, showOnlyWritable: false }) =>
279     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
280         const { pickerId, includeCollections = false, includeFiles = false } = params;
281         const uuid = getUserUuid(getState());
282         if (uuid) {
283             const filters = pipe(
284                 (fb: FilterBuilder) => includeCollections
285                     ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
286                     : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
287                 fb => fb.getFilters(),
288             )(new FilterBuilder());
289
290             const { items } = await services.favoriteService.list(uuid, { filters }, options.showOnlyOwned);
291
292             dispatch<any>(receiveTreePickerData<GroupContentsResource>({
293                 id: 'Favorites',
294                 pickerId,
295                 data: items.filter((item) => {
296                     if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
297                         return false;
298                     }
299
300                     if (options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
301                         return false;
302                     }
303
304                     return true;
305                 }),
306                 extractNodeData: item => ({
307                     id: item.uuid,
308                     value: item,
309                     status: item.kind === ResourceKind.PROJECT
310                         ? TreeNodeStatus.INITIAL
311                         : includeFiles
312                             ? TreeNodeStatus.INITIAL
313                             : TreeNodeStatus.LOADED
314                 }),
315             }));
316         }
317     };
318
319 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
320     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
321         const { pickerId, includeCollections = false, includeFiles = false } = params;
322         const uuidPrefix = getState().auth.config.uuidPrefix;
323         const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
324
325         const filters = pipe(
326             (fb: FilterBuilder) => includeCollections
327                 ? fb.addIsA('head_uuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
328                 : fb.addIsA('head_uuid', [ResourceKind.PROJECT]),
329             fb => fb
330                 .addEqual('link_class', LinkClass.STAR)
331                 .addEqual('owner_uuid', publicProjectUuid)
332                 .getFilters(),
333         )(new FilterBuilder());
334
335         const { items } = await services.linkService.list({ filters });
336
337         dispatch<any>(receiveTreePickerData<LinkResource>({
338             id: 'Public Favorites',
339             pickerId,
340             data: items.filter(item => {
341                 if (params.options && params.options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as any).frozenByUuid) {
342                     return false;
343                 }
344
345                 return true;
346             }),
347             extractNodeData: item => ({
348                 id: item.headUuid,
349                 value: item,
350                 status: item.headKind === ResourceKind.PROJECT
351                     ? TreeNodeStatus.INITIAL
352                     : includeFiles
353                         ? TreeNodeStatus.INITIAL
354                         : TreeNodeStatus.LOADED
355             }),
356         }));
357     };
358
359 export const receiveTreePickerProjectsData = (id: string, projects: ProjectResource[], pickerId: string) =>
360     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
361         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
362             id,
363             nodes: projects.map(project => initTreeNode({ id: project.uuid, value: project })),
364             pickerId,
365         }));
366
367         dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
368     };
369
370 export const loadProjectTreePickerProjects = (id: string) =>
371     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
372         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
373
374
375         const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
376         const { items } = await services.projectService.list(buildParams(ownerUuid));
377
378         dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
379     };
380
381 export const loadFavoriteTreePickerProjects = (id: string) =>
382     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
383         const parentId = getUserUuid(getState()) || '';
384
385         if (id === '') {
386             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
387             const { items } = await services.favoriteService.list(parentId);
388             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.FAVORITES));
389         } else {
390             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.FAVORITES }));
391             const { items } = await services.projectService.list(buildParams(id));
392             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.FAVORITES));
393         }
394
395     };
396
397 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
398     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
399         const parentId = getUserUuid(getState()) || '';
400
401         if (id === '') {
402             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
403             const { items } = await services.favoriteService.list(parentId);
404             dispatch<any>(receiveTreePickerProjectsData(parentId, items as ProjectResource[], TreePickerId.PUBLIC_FAVORITES));
405         } else {
406             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PUBLIC_FAVORITES }));
407             const { items } = await services.projectService.list(buildParams(id));
408             dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PUBLIC_FAVORITES));
409         }
410
411     };
412
413 const buildParams = (ownerUuid: string) => {
414     return {
415         filters: new FilterBuilder()
416             .addEqual('owner_uuid', ownerUuid)
417             .getFilters(),
418         order: new OrderBuilder<ProjectResource>()
419             .addAsc('name')
420             .getOrder()
421     };
422 };