Merge branch 'main' into 21842-improve-sharing
[arvados.git] / services / workbench2 / src / views-components / sharing-dialog / sharing-dialog-component.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 {
7     Dialog,
8     DialogTitle,
9     Button,
10     Grid,
11     DialogContent,
12     CircularProgress,
13     Paper,
14     Tabs,
15     Tab,
16     Checkbox,
17     FormControlLabel,
18     Typography,
19 } from '@mui/material';
20 import { CustomStyleRulesCallback } from 'common/custom-theme';
21 import { WithStyles } from '@mui/styles';
22 import withStyles from '@mui/styles/withStyles';
23 import { DialogActions } from 'components/dialog-actions/dialog-actions';
24 import { SharingURLsContent } from './sharing-urls';
25 import {
26     extractUuidObjectType,
27     ResourceObjectType
28 } from 'models/resource';
29 import { SharingInvitationForm } from './sharing-invitation-form';
30 import { SharingManagementForm } from './sharing-management-form';
31 import moment, { Moment } from 'moment';
32 import { SharingPublicAccessForm } from './sharing-public-access-form';
33 import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
34 import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
35 import { StaticDateTimePicker } from '@mui/x-date-pickers/StaticDateTimePicker';
36
37 export interface SharingDialogDataProps {
38     open: boolean;
39     loading: boolean;
40     saveEnabled: boolean;
41     sharedResourceUuid: string;
42     sharingURLsNr: number;
43     privateAccess: boolean;
44     sharingURLsDisabled: boolean;
45     permissions: any[];
46 }
47 export interface SharingDialogActionProps {
48     onClose: () => void;
49     onSave: () => void;
50     onCreateSharingToken: (d: Date | undefined) => () => void;
51     refreshPermissions: () => void;
52 }
53 enum SharingDialogTab {
54     PERMISSIONS = 0,
55     URLS = 1,
56 }
57 export type SharingDialogComponentProps = SharingDialogDataProps & SharingDialogActionProps;
58
59 export const SharingDialogComponent = (props: SharingDialogComponentProps) => {
60     const { open, loading, saveEnabled, sharedResourceUuid,
61         sharingURLsNr, privateAccess, sharingURLsDisabled,
62         onClose, onSave, onCreateSharingToken, refreshPermissions } = props;
63     const showTabs = !sharingURLsDisabled && extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
64     const [tabNr, setTabNr] = React.useState<number>(SharingDialogTab.PERMISSIONS);
65     const [expDate, setExpDate] = React.useState<Moment>();
66     const [withExpiration, setWithExpiration] = React.useState<boolean>(false);
67
68     const handleChange = (newValue: moment.Moment) => setExpDate(newValue);
69     const handleClose = (ev, reason) => {
70         if (reason !== 'backdropClick') {
71             onClose();
72         }
73     }
74
75     // Sets up the dialog depending on the resource type
76     if (!showTabs && tabNr !== SharingDialogTab.PERMISSIONS) {
77         setTabNr(SharingDialogTab.PERMISSIONS);
78     }
79
80     React.useEffect(() => {
81         if (!withExpiration) {
82             setExpDate(undefined);
83         } else {
84             setExpDate(moment().add(2, 'hour'));
85         }
86     }, [withExpiration]);
87
88     return (
89         <Dialog {...{ open, onClose }} className="sharing-dialog" onClose={handleClose} fullWidth maxWidth='sm' >
90             <DialogTitle>
91                 Sharing settings
92             </DialogTitle>
93             {showTabs &&
94                 <Tabs value={tabNr}
95                     onChange={(_, tb) => {
96                         if (tb === SharingDialogTab.PERMISSIONS) {
97                             refreshPermissions();
98                         }
99                         setTabNr(tb)
100                     }
101                     }>
102                     <Tab label="With users/groups" />
103                     <Tab label={`Sharing URLs ${sharingURLsNr > 0 ? '(' + sharingURLsNr + ')' : ''}`} disabled={saveEnabled} />
104                 </Tabs>
105             }
106             <DialogContent>
107                 {tabNr === SharingDialogTab.PERMISSIONS &&
108                     <Grid container direction='column' spacing={3}>
109                         <Grid item>
110                             <SharingInvitationForm onSave={onSave} />
111                         </Grid>
112                         <Grid item>
113                             <SharingManagementForm onSave={onSave} />
114                         </Grid>
115                         <Grid item>
116                             <SharingPublicAccessForm onSave={onSave} />
117                         </Grid>
118                     </Grid>
119                 }
120                 {tabNr === SharingDialogTab.URLS &&
121                     <SharingURLsContent uuid={sharedResourceUuid} />
122                 }
123             </DialogContent>
124             <DialogActions>
125                 <Grid container spacing={1} style={{ display: 'flex', width: '100%', flexDirection: 'column', alignItems: 'center'}}>
126                     {tabNr === SharingDialogTab.URLS && withExpiration && 
127                         <>
128                             <section style={{minHeight: '42dvh', display: 'flex', flexDirection: 'column' }}>
129                                 <LocalizationProvider dateAdapter={AdapterMoment}>
130                                     <StaticDateTimePicker 
131                                         orientation="landscape" 
132                                         onChange={handleChange} 
133                                         value={expDate || moment().add(2, 'hour')} 
134                                         disablePast
135                                         minutesStep={5}
136                                         ampm={false}
137                                         slots={{
138                                             //removes redundant action bar
139                                             actionBar: () => null,
140                                         }}
141                                     />
142                                 </LocalizationProvider>
143                             </section>
144                             <Typography variant='caption' align='center' marginBottom='1rem'>
145                                 Maximum expiration date may be limited by the cluster configuration.
146                             </Typography>
147                         </>
148                         }
149                     {tabNr === SharingDialogTab.PERMISSIONS && !sharingURLsDisabled &&
150                         privateAccess && sharingURLsNr > 0 &&
151                         <Grid item md={12}>
152                             <Typography variant='caption' align='center' color='error'>
153                                 Although there aren't specific permissions set, this is publicly accessible via Sharing URL(s).
154                             </Typography>
155                         </Grid>
156                     }
157                     <Grid style={{display: 'flex', justifyContent: 'end', flexDirection: 'row', width: '100%', marginBottom: '-0.5rem'}}>
158                         {tabNr === SharingDialogTab.URLS && 
159                             <Grid container style={{ display: 'flex', justifyContent: 'space-between'}}>
160                                 <Grid display='flex'>
161                                     <Grid item>
162                                         <FormControlLabel
163                                             control={<Checkbox color="primary" checked={withExpiration}
164                                                 onChange={(e) => setWithExpiration(e.target.checked)} />}
165                                             label="With expiration" />
166                                     </Grid>
167                                     <Grid item>
168                                         <Button variant="contained" color="primary"
169                                             disabled={expDate !== undefined && expDate.toDate() <= new Date()}
170                                             onClick={onCreateSharingToken(expDate?.toDate())}>
171                                             Create sharing URL
172                                         </Button>
173                                     </Grid>
174                                 </Grid>
175                             </Grid>
176                         }
177                         <Grid>
178                             <Grid style={{display: 'flex'}}>
179                                 <Button onClick={() => {
180                                     onClose();
181                                     setWithExpiration(false);
182                                     }}
183                                     disabled={saveEnabled}
184                                     color='primary'
185                                     size='small'
186                                     style={{ marginLeft: '10px' }}
187                                     >
188                                         Close
189                                 </Button>
190                                 {tabNr !== SharingDialogTab.URLS && 
191                                     <Button onClick={() => {
192                                             onSave();
193                                         }}
194                                         data-cy="add-invited-people"
195                                         disabled={!saveEnabled}
196                                         color='primary'
197                                         variant='contained'
198                                         size='small'
199                                         style={{ marginLeft: '10px' }}
200                                         >
201                                             Save
202                                     </Button>
203                                 }
204                             </Grid>
205                         </Grid>
206                     </Grid>
207                 </Grid>
208             </DialogActions>
209             {
210                 loading && <LoadingIndicator />
211             }
212         </Dialog>
213     );
214 };
215
216 const loadingIndicatorStyles: CustomStyleRulesCallback<'root'> = theme => ({
217     root: {
218         position: 'absolute',
219         top: 0,
220         right: 0,
221         bottom: 0,
222         left: 0,
223         display: 'flex',
224         alignItems: 'center',
225         justifyContent: 'center',
226         backgroundColor: 'rgba(255, 255, 255, 0.8)',
227     },
228 });
229
230 const LoadingIndicator = withStyles(loadingIndicatorStyles)(
231     (props: WithStyles<'root'>) =>
232         <Paper classes={props.classes}>
233             <CircularProgress />
234         </Paper>
235 );