Merge branch '17782-react-scripts-ts-migration' into main. Closes #17782
[arvados-workbench2.git] / src / store / groups-panel / groups-panel-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { Dispatch } from 'redux';
6 import { reset, startSubmit, stopSubmit, FormErrors } from 'redux-form';
7 import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
8 import { dialogActions } from 'store/dialog/dialog-actions';
9 import { Participant } from 'views-components/sharing-dialog/participant-select';
10 import { RootState } from 'store/store';
11 import { ServiceRepository } from 'services/services';
12 import { getResource } from 'store/resources/resources';
13 import { GroupResource } from 'models/group';
14 import { getCommonResourceServiceError, CommonResourceServiceError } from 'services/common-service/common-resource-service';
15 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
16 import { PermissionLevel } from 'models/permission';
17 import { PermissionService } from 'services/permission-service/permission-service';
18 import { FilterBuilder } from 'services/api/filter-builder';
19
20 export const GROUPS_PANEL_ID = "groupsPanel";
21 export const CREATE_GROUP_DIALOG = "createGroupDialog";
22 export const CREATE_GROUP_FORM = "createGroupForm";
23 export const CREATE_GROUP_NAME_FIELD_NAME = 'name';
24 export const CREATE_GROUP_USERS_FIELD_NAME = 'users';
25 export const GROUP_ATTRIBUTES_DIALOG = 'groupAttributesDialog';
26 export const GROUP_REMOVE_DIALOG = 'groupRemoveDialog';
27
28 export const GroupsPanelActions = bindDataExplorerActions(GROUPS_PANEL_ID);
29
30 export const loadGroupsPanel = () => GroupsPanelActions.REQUEST_ITEMS();
31
32 export const openCreateGroupDialog = () =>
33     (dispatch: Dispatch) => {
34         dispatch(dialogActions.OPEN_DIALOG({ id: CREATE_GROUP_DIALOG, data: {} }));
35         dispatch(reset(CREATE_GROUP_FORM));
36     };
37
38 export const openGroupAttributes = (uuid: string) =>
39     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
40         const { resources } = getState();
41         const data = getResource<GroupResource>(uuid)(resources);
42         dispatch(dialogActions.OPEN_DIALOG({ id: GROUP_ATTRIBUTES_DIALOG, data }));
43     };
44
45 export const removeGroup = (uuid: string) =>
46     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
47         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
48         await services.groupsService.delete(uuid);
49         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
50         dispatch<any>(loadGroupsPanel());
51     };
52
53 export const openRemoveGroupDialog = (uuid: string) =>
54     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
55         dispatch(dialogActions.OPEN_DIALOG({
56             id: GROUP_REMOVE_DIALOG,
57             data: {
58                 title: 'Remove group',
59                 text: 'Are you sure you want to remove this group?',
60                 confirmButtonLabel: 'Remove',
61                 uuid
62             }
63         }));
64     };
65
66 export interface CreateGroupFormData {
67     [CREATE_GROUP_NAME_FIELD_NAME]: string;
68     [CREATE_GROUP_USERS_FIELD_NAME]?: Participant[];
69 }
70
71 export const createGroup = ({ name, users = [] }: CreateGroupFormData) =>
72     async (dispatch: Dispatch, _: {}, { groupsService, permissionService }: ServiceRepository) => {
73         dispatch(startSubmit(CREATE_GROUP_FORM));
74         try {
75             const newGroup = await groupsService.create({ name });
76             for (const user of users) {
77                 await addGroupMember({
78                     user,
79                     group: newGroup,
80                     dispatch,
81                     permissionService,
82                 });
83             }
84             dispatch(dialogActions.CLOSE_DIALOG({ id: CREATE_GROUP_DIALOG }));
85             dispatch(reset(CREATE_GROUP_FORM));
86             dispatch(loadGroupsPanel());
87             dispatch(snackbarActions.OPEN_SNACKBAR({
88                 message: `${newGroup.name} group has been created`,
89                 kind: SnackbarKind.SUCCESS
90             }));
91             return newGroup;
92         } catch (e) {
93             const error = getCommonResourceServiceError(e);
94             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
95                 dispatch(stopSubmit(CREATE_GROUP_FORM, { name: 'Group with the same name already exists.' } as FormErrors));
96             }
97             return;
98         }
99     };
100
101 interface AddGroupMemberArgs {
102     user: { uuid: string, name: string };
103     group: { uuid: string, name: string };
104     dispatch: Dispatch;
105     permissionService: PermissionService;
106 }
107
108 /**
109  * Group membership is determined by whether the group has can_read permission on an object.
110  * If a group G can_read an object A, then we say A is a member of G.
111  *
112  * [Permission model docs](https://doc.arvados.org/api/permission-model.html)
113  */
114 export const addGroupMember = async ({ user, group, ...args }: AddGroupMemberArgs) => {
115     await createPermission({
116         head: { ...user },
117         tail: { ...group },
118         permissionLevel: PermissionLevel.CAN_READ,
119         ...args,
120     });
121 };
122
123 interface CreatePermissionLinkArgs {
124     head: { uuid: string, name: string };
125     tail: { uuid: string, name: string };
126     permissionLevel: PermissionLevel;
127     dispatch: Dispatch;
128     permissionService: PermissionService;
129 }
130
131 const createPermission = async ({ head, tail, permissionLevel, dispatch, permissionService }: CreatePermissionLinkArgs) => {
132     try {
133         await permissionService.create({
134             tailUuid: tail.uuid,
135             headUuid: head.uuid,
136             name: permissionLevel,
137         });
138     } catch (e) {
139         dispatch(snackbarActions.OPEN_SNACKBAR({
140             message: `Could not add ${tail.name} -> ${head.name} relation`,
141             kind: SnackbarKind.ERROR,
142         }));
143     }
144 };
145
146 interface DeleteGroupMemberArgs {
147     user: { uuid: string, name: string };
148     group: { uuid: string, name: string };
149     dispatch: Dispatch;
150     permissionService: PermissionService;
151 }
152
153 export const deleteGroupMember = async ({ user, group, ...args }: DeleteGroupMemberArgs) => {
154     await deletePermission({
155         tail: group,
156         head: user,
157         ...args,
158     });
159 };
160
161 interface DeletePermissionLinkArgs {
162     head: { uuid: string, name: string };
163     tail: { uuid: string, name: string };
164     dispatch: Dispatch;
165     permissionService: PermissionService;
166 }
167
168 export const deletePermission = async ({ head, tail, dispatch, permissionService }: DeletePermissionLinkArgs) => {
169     try {
170         const permissionsResponse = await permissionService.list({
171             filters: new FilterBuilder()
172                 .addEqual('tail_uuid', tail.uuid)
173                 .addEqual('head_uuid', head.uuid)
174                 .getFilters()
175         });
176         const [permission] = permissionsResponse.items;
177         if (permission) {
178             await permissionService.delete(permission.uuid);
179         } else {
180             throw new Error('Permission not found');
181         }
182     } catch (e) {
183         dispatch(snackbarActions.OPEN_SNACKBAR({
184             message: `Could not delete ${tail.name} -> ${head.name} relation`,
185             kind: SnackbarKind.ERROR,
186         }));
187     }
188 };