16115: Further sharing dialog cleanup.
[arvados-workbench2.git] / src / store / sharing-dialog / sharing-dialog-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { dialogActions } from "store/dialog/dialog-actions";
6 import { withDialog } from "store/dialog/with-dialog";
7 import {
8     SHARING_DIALOG_NAME,
9     SHARING_INVITATION_FORM_NAME,
10     SharingManagementFormData,
11     SharingInvitationFormData,
12     getSharingMangementFormData,
13 } from './sharing-dialog-types';
14 import { Dispatch } from 'redux';
15 import { ServiceRepository } from "services/services";
16 import { FilterBuilder } from 'services/api/filter-builder';
17 import { initialize, getFormValues, reset } from 'redux-form';
18 import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
19 import { RootState } from 'store/store';
20 import { getDialog } from 'store/dialog/dialog-reducer';
21 import { PermissionLevel } from 'models/permission';
22 import { PermissionResource } from 'models/permission';
23 import { differenceWith } from "lodash";
24 import { withProgress } from "store/progress-indicator/with-progress";
25 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
26 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
27 import {
28     extractUuidKind,
29     extractUuidObjectType,
30     ResourceKind,
31     ResourceObjectType
32 } from "models/resource";
33 import { resourcesActions } from "store/resources/resources-actions";
34
35 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
36     (dispatch: Dispatch) => {
37         dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: {resourceUuid, refresh} }));
38         dispatch<any>(loadSharingDialog);
39     };
40
41 export const closeSharingDialog = () =>
42     dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME });
43
44 export const connectSharingDialog = withDialog(SHARING_DIALOG_NAME);
45 export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
46
47
48 export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => {
49     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
50     await dispatch<any>(saveManagementChanges);
51     await dispatch<any>(sendInvitations);
52     dispatch(reset(SHARING_INVITATION_FORM_NAME));
53     await dispatch<any>(loadSharingDialog);
54     dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
55
56     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
57     if (dialog && dialog.data.refresh) {
58         dialog.data.refresh();
59     }
60 };
61
62 export const sendSharingInvitations = async (dispatch: Dispatch, getState: () => RootState) => {
63     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
64     await dispatch<any>(sendInvitations);
65     dispatch(closeSharingDialog());
66     dispatch(snackbarActions.OPEN_SNACKBAR({
67         message: 'Resource has been shared',
68         kind: SnackbarKind.SUCCESS,
69     }));
70     dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
71
72     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
73     if (dialog && dialog.data.refresh) {
74         dialog.data.refresh();
75     }
76 };
77
78 export interface SharingDialogData {
79     resourceUuid: string;
80     refresh: () => void;
81 }
82
83 export const createSharingToken = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
84     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
85     if (dialog) {
86         const resourceUuid = dialog.data.resourceUuid;
87         if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
88             dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
89             try {
90                 const sharingToken = await apiClientAuthorizationService.createCollectionSharingToken(resourceUuid);
91                 dispatch(resourcesActions.SET_RESOURCES([sharingToken]));
92                 dispatch(snackbarActions.OPEN_SNACKBAR({
93                     message: 'Sharing URL created',
94                     hideDuration: 2000,
95                     kind: SnackbarKind.SUCCESS,
96                 }));
97             } catch (e) {
98                 dispatch(snackbarActions.OPEN_SNACKBAR({
99                     message: 'Failed to create sharing URL',
100                     hideDuration: 2000,
101                     kind: SnackbarKind.ERROR,
102                 }));
103             } finally {
104                 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
105             }
106         }
107     }
108 };
109
110 export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
111     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
112     try {
113         await apiClientAuthorizationService.delete(uuid);
114         dispatch(resourcesActions.DELETE_RESOURCES([uuid]));
115         dispatch(snackbarActions.OPEN_SNACKBAR({
116             message: 'Sharing URL removed',
117             hideDuration: 2000,
118             kind: SnackbarKind.SUCCESS,
119         }));
120     } catch (e) {
121         dispatch(snackbarActions.OPEN_SNACKBAR({
122             message: 'Failed to remove sharing URL',
123             hideDuration: 2000,
124             kind: SnackbarKind.ERROR,
125         }));
126     } finally {
127         dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
128     }
129 };
130
131 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService, apiClientAuthorizationService }: ServiceRepository) => {
132
133     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
134     if (dialog) {
135         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
136         try {
137             const resourceUuid = dialog.data.resourceUuid;
138             const { items } = await permissionService.listResourcePermissions(resourceUuid);
139             await dispatch<any>(initializeManagementForm(items));
140             // For collections, we need to load the public sharing tokens
141             if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
142                 const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
143                 dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items]));
144             }
145         } catch (e) {
146             dispatch(snackbarActions.OPEN_SNACKBAR({
147                 message: 'You do not have access to share this item',
148                 hideDuration: 2000,
149                 kind: SnackbarKind.ERROR }));
150             dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
151         } finally {
152             dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
153         }
154     }
155 };
156
157 const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
158     async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService }: ServiceRepository) => {
159
160         const filters = new FilterBuilder()
161             .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
162             .getFilters();
163
164         const { items: users } = await userService.list({ filters, count: "none" });
165         const { items: groups } = await groupsService.list({ filters, count: "none" });
166
167         const getEmail = (tailUuid: string) => {
168             const user = users.find(({ uuid }) => uuid === tailUuid);
169             const group = groups.find(({ uuid }) => uuid === tailUuid);
170             return user
171                 ? user.email
172                 : group
173                     ? group.name
174                     : tailUuid;
175         };
176
177         const managementPermissions = permissionLinks
178             .map(({ tailUuid, name, uuid }) => ({
179                 email: getEmail(tailUuid),
180                 permissions: name as PermissionLevel,
181                 permissionUuid: uuid,
182             }));
183
184         const managementFormData: SharingManagementFormData = {
185             permissions: managementPermissions,
186             initialPermissions: managementPermissions,
187         };
188
189         dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
190     };
191
192 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
193     const state = getState();
194     const { user } = state.auth;
195     const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
196     if (dialog && user) {
197         const { initialPermissions, permissions } = getSharingMangementFormData(state);
198         const cancelledPermissions = differenceWith(
199             initialPermissions,
200             permissions,
201             (a, b) => a.permissionUuid === b.permissionUuid
202         );
203
204         for (const { permissionUuid } of cancelledPermissions) {
205             await permissionService.delete(permissionUuid);
206         }
207
208         for (const permission of permissions) {
209             await permissionService.update(permission.permissionUuid, { name: permission.permissions });
210         }
211     }
212 };
213
214 const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
215     const state = getState();
216     const { user } = state.auth;
217     const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
218     if (dialog && user) {
219         const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData;
220
221         const getGroupsFromForm = invitations.invitedPeople.filter((invitation) => extractUuidKind(invitation.uuid) === ResourceKind.GROUP);
222         const getUsersFromForm = invitations.invitedPeople.filter((invitation) => extractUuidKind(invitation.uuid) === ResourceKind.USER);
223
224         const invitationDataUsers = getUsersFromForm
225             .map(person => ({
226                 ownerUuid: user.uuid,
227                 headUuid: dialog.data.resourceUuid,
228                 tailUuid: person.uuid,
229                 name: invitations.permissions
230             }));
231
232         const invitationsDataGroups = getGroupsFromForm.map(
233             group => ({
234                 ownerUuid: user.uuid,
235                 headUuid: dialog.data.resourceUuid,
236                 tailUuid: group.uuid,
237                 name: invitations.permissions
238             })
239         );
240
241         const data = invitationDataUsers.concat(invitationsDataGroups);
242
243         for (const invitation of data) {
244             await permissionService.create(invitation);
245         }
246     }
247 };