599 lines
18 KiB
TypeScript
599 lines
18 KiB
TypeScript
import {
|
|
Button,
|
|
Checkbox,
|
|
CircularProgress,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Divider,
|
|
FormControl,
|
|
FormControlLabel,
|
|
FormGroup,
|
|
FormLabel,
|
|
IconButton,
|
|
Link,
|
|
List,
|
|
ListItem,
|
|
ListItemSecondaryAction,
|
|
ListItemText,
|
|
Tab,
|
|
Tabs,
|
|
TextField,
|
|
Theme,
|
|
Tooltip,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { green, red } from "@mui/material/colors";
|
|
import {
|
|
Add as AddIcon,
|
|
Link as LinkIcon,
|
|
LinkOff,
|
|
RemoveCircle as RemoveCircleIcon
|
|
} from "@mui/icons-material";
|
|
import { MobileDateTimePicker } from '@mui/x-date-pickers';
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { usePermissionAPI, usePrevious, useSnackbar } from "hooks";
|
|
import { cloneDeep } from "lodash";
|
|
import { binScope, Scope, ShareableLink, User } from "models";
|
|
import moment, { Moment } from "moment";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import { openSnackbar } from "providers/Snackbar";
|
|
import { useBinAPI, useGateAPI } from "providers";
|
|
import { makeStyles } from "@mui/styles";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => ({
|
|
dialogContent: {
|
|
marginTop: theme.spacing(2)
|
|
},
|
|
formContainer: {
|
|
display: "flex",
|
|
flexWrap: "wrap"
|
|
},
|
|
formControl: {
|
|
margin: theme.spacing(1),
|
|
minWidth: 160,
|
|
width: "100%"
|
|
},
|
|
emailForm: {
|
|
paddingBottom: theme.spacing(2)
|
|
},
|
|
fabContainer: {
|
|
position: "relative",
|
|
minHeight: "64px"
|
|
},
|
|
addIcon: {
|
|
color: green[500],
|
|
"&:hover": {
|
|
color: green[600]
|
|
}
|
|
},
|
|
removeIcon: {
|
|
color: red[500],
|
|
"&:hover": {
|
|
color: red[600]
|
|
}
|
|
},
|
|
grey: {
|
|
color: theme.palette.text.secondary
|
|
}
|
|
}));
|
|
|
|
interface Props {
|
|
scope: Scope;
|
|
label: string;
|
|
permissions: pond.Permission[];
|
|
isDialogOpen: boolean;
|
|
closeDialogCallback: Function;
|
|
useImitation?: boolean;
|
|
sharedUsers?: User[]; //including the users that the object/team is already shared to will prevent users from sharing again to an existing user
|
|
}
|
|
|
|
export default function ShareObject(props: Props) {
|
|
const binAPI = useBinAPI();
|
|
const classes = useStyles();
|
|
const permissionAPI = usePermissionAPI();
|
|
const { info, success, error } = useSnackbar();
|
|
const { scope, label, permissions, isDialogOpen, closeDialogCallback, sharedUsers } = props;
|
|
const [email, setEmail] = useState<string>("");
|
|
const [sharedPermissions, setSharedPermissions] = useState<pond.Permission[]>([
|
|
pond.Permission.PERMISSION_READ
|
|
]);
|
|
const [tab, setTab] = useState<number>(0);
|
|
const prevTab = usePrevious(tab);
|
|
const [isLoadingLinks, setIsLoadingLinks] = useState<boolean>(false);
|
|
const [shareableLinks, setShareableLinks] = useState<ShareableLink[]>([]);
|
|
const [newLinkExpiration, setNewLinkExpiration] = useState<Moment>(moment().add(1, "week"));
|
|
const [isNewLinkInfinite, setIsNewLinkInfinite] = useState<boolean>(false);
|
|
const [openExistingUser, setOpenExistingUser] = useState(false);
|
|
const [useImitation] = useState<boolean>(
|
|
props.useImitation !== undefined ? props.useImitation : true
|
|
);
|
|
const gateAPI = useGateAPI();
|
|
|
|
const share = () => {
|
|
permissionAPI
|
|
.shareObject(scope, email, sharedPermissions, useImitation)
|
|
.then((result: any) => {
|
|
let shareBins = true;
|
|
if (result && result.data && result.data.existing) {
|
|
success(label + " was shared with " + email);
|
|
} else {
|
|
success(email + " was sent an email to sign up");
|
|
shareBins = false;
|
|
}
|
|
let successBins = true;
|
|
if (scope.kind === "binyard" && shareBins) {
|
|
binAPI.listBins(1000, 0, "asc", "name", scope.key).then(resp => {
|
|
resp.data.bins.forEach(bin => {
|
|
if (bin.settings) {
|
|
let newScope = binScope(bin.settings?.key);
|
|
permissionAPI.shareObject(newScope, email, sharedPermissions).catch(_err => {
|
|
successBins = false;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
if (!successBins) {
|
|
openSnackbar("error", "One or more bins failed to share with " + email);
|
|
} else {
|
|
success("Bins shared with " + email);
|
|
|
|
}
|
|
}
|
|
let successGates = true;
|
|
if (scope.kind === "terminal" && shareBins) {
|
|
gateAPI
|
|
.listGates(
|
|
1000,
|
|
0,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
[scope.key],
|
|
[scope.kind]
|
|
)
|
|
.then(resp => {
|
|
resp.data.gates.forEach(gate => {
|
|
if (gate) {
|
|
let newScope = { key: gate.key, kind: "gate" } as Scope;
|
|
permissionAPI.shareObject(newScope, email, sharedPermissions).catch(_err => {
|
|
successGates = false;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
if (!successGates) {
|
|
openSnackbar("error", "One or more Gates failed to share");
|
|
} else {
|
|
success("Gates shared");
|
|
}
|
|
}
|
|
closeAfterShare();
|
|
})
|
|
.catch((_err: any) => {
|
|
error("Unable to share " + label + " with " + email);
|
|
close();
|
|
});
|
|
};
|
|
|
|
const loadShareableLinks = useCallback(() => {
|
|
setIsLoadingLinks(true);
|
|
permissionAPI
|
|
.listShareableLinks(scope)
|
|
.then((response: any) => {
|
|
let rawShareableLinks = response.data.links ? response.data.links : [];
|
|
let rShareableLinks = rawShareableLinks.map((rawShareableLink: any) =>
|
|
ShareableLink.any(rawShareableLink)
|
|
);
|
|
setShareableLinks(rShareableLinks);
|
|
})
|
|
.catch((_error: any) => {
|
|
setShareableLinks([]);
|
|
console.log("Error occured while loading shareable links");
|
|
})
|
|
.finally(() => {
|
|
setIsLoadingLinks(false);
|
|
});
|
|
}, [permissionAPI, scope]);
|
|
|
|
useEffect(() => {
|
|
if (tab === 1 && prevTab !== tab) {
|
|
loadShareableLinks();
|
|
}
|
|
}, [loadShareableLinks, prevTab, tab]);
|
|
|
|
const getNeverExpires = () => {
|
|
return "";
|
|
};
|
|
|
|
const createShareableLink = () => {
|
|
const expiration = isNewLinkInfinite ? getNeverExpires() : newLinkExpiration.toISOString();
|
|
permissionAPI
|
|
.addShareableLink(scope, expiration)
|
|
.then((_response: any) => {
|
|
loadShareableLinks();
|
|
})
|
|
.catch((_error: any) => {
|
|
console.log("Error occured while creating shareable link");
|
|
});
|
|
};
|
|
|
|
const revokeShareableLink = (code: string) => {
|
|
permissionAPI
|
|
.removeShareableLink(scope, code)
|
|
.then((_response: any) => {
|
|
loadShareableLinks();
|
|
})
|
|
.catch((_error: any) => {
|
|
console.log("Error occured while removing the shareable link");
|
|
});
|
|
};
|
|
|
|
const getShareableLinkURL = (code: string): string => {
|
|
const base = window.location.origin;
|
|
return base + "/" + scope.kind + "s/" + code;
|
|
};
|
|
|
|
const copyShareableLinkToClipboard = (code: string) => {
|
|
const url = getShareableLinkURL(code);
|
|
navigator.clipboard.writeText(url);
|
|
info("Copied link to clipboard");
|
|
};
|
|
|
|
const closeAfterShare = () => {
|
|
closeDialogCallback(true);
|
|
};
|
|
|
|
const close = () => {
|
|
closeDialogCallback(false);
|
|
};
|
|
|
|
const changeEmail = (event: any) => {
|
|
setEmail(event.target.value);
|
|
};
|
|
|
|
const changePermissions = (event: any) => {
|
|
let updatedSharedPermissions: Array<pond.Permission> = cloneDeep(sharedPermissions);
|
|
let permission: pond.Permission;
|
|
switch (event.target.value) {
|
|
case "1":
|
|
permission = pond.Permission.PERMISSION_USERS;
|
|
break;
|
|
case "2":
|
|
permission = pond.Permission.PERMISSION_READ;
|
|
break;
|
|
case "3":
|
|
permission = pond.Permission.PERMISSION_WRITE;
|
|
break;
|
|
case "4":
|
|
permission = pond.Permission.PERMISSION_SHARE;
|
|
break;
|
|
case "5":
|
|
permission = pond.Permission.PERMISSION_BILLING;
|
|
break;
|
|
case "6":
|
|
permission = pond.Permission.PERMISSION_FILE_MANAGEMENT;
|
|
break;
|
|
default:
|
|
permission = pond.Permission.PERMISSION_INVALID;
|
|
break;
|
|
}
|
|
|
|
if (updatedSharedPermissions.includes(permission)) {
|
|
updatedSharedPermissions = updatedSharedPermissions.filter(
|
|
(curPermission: pond.Permission) => curPermission !== permission
|
|
);
|
|
} else {
|
|
updatedSharedPermissions.push(permission);
|
|
}
|
|
|
|
setSharedPermissions(updatedSharedPermissions);
|
|
};
|
|
|
|
const changeTab = (_event: any, newTab: number) => {
|
|
setTab(newTab);
|
|
};
|
|
|
|
const isValid = () => {
|
|
return email.trim() !== "" && sharedPermissions.length > 0;
|
|
};
|
|
|
|
// UI BEGINS
|
|
|
|
const emailForm = () => {
|
|
return (
|
|
<TextField
|
|
id="email"
|
|
name="email"
|
|
label="Email"
|
|
value={email}
|
|
onChange={changeEmail}
|
|
margin="normal"
|
|
fullWidth
|
|
variant="outlined"
|
|
className={classes.emailForm}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const permissionsForm = () => {
|
|
return (
|
|
<FormControl component="fieldset" fullWidth variant="outlined">
|
|
<FormLabel component="legend">Permissions</FormLabel>
|
|
<FormGroup>
|
|
{permissions.includes(pond.Permission.PERMISSION_READ) && (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
disabled={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
|
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
|
|
onChange={changePermissions}
|
|
value={pond.Permission.PERMISSION_READ as pond.Permission}
|
|
/>
|
|
}
|
|
label="View"
|
|
labelPlacement="end"
|
|
/>
|
|
)}
|
|
|
|
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_WRITE)}
|
|
onChange={changePermissions}
|
|
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
|
|
/>
|
|
}
|
|
label="Edit"
|
|
labelPlacement="end"
|
|
/>
|
|
)}
|
|
|
|
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_SHARE)}
|
|
onChange={changePermissions}
|
|
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
|
|
/>
|
|
}
|
|
label="Share"
|
|
labelPlacement="end"
|
|
/>
|
|
)}
|
|
|
|
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_USERS)}
|
|
onChange={changePermissions}
|
|
value={pond.Permission.PERMISSION_USERS as pond.Permission}
|
|
/>
|
|
}
|
|
label="Manage Users"
|
|
labelPlacement="end"
|
|
/>
|
|
)}
|
|
|
|
{permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) && (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
|
|
onChange={changePermissions}
|
|
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
|
|
/>
|
|
}
|
|
label="Files"
|
|
labelPlacement="end"
|
|
/>
|
|
)}
|
|
|
|
{permissions.includes(pond.Permission.PERMISSION_BILLING) && (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={sharedPermissions.includes(pond.Permission.PERMISSION_BILLING)}
|
|
onChange={changePermissions}
|
|
value={pond.Permission.PERMISSION_BILLING as pond.Permission}
|
|
/>
|
|
}
|
|
label="Billing"
|
|
labelPlacement="end"
|
|
/>
|
|
)}
|
|
</FormGroup>
|
|
</FormControl>
|
|
);
|
|
};
|
|
|
|
const emailSharing = () => {
|
|
return (
|
|
<React.Fragment>
|
|
{emailForm()}
|
|
{permissionsForm()}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const existingUserDialog = () => {
|
|
return (
|
|
<ResponsiveDialog
|
|
open={openExistingUser}
|
|
onClose={() => {
|
|
setOpenExistingUser(false);
|
|
close();
|
|
}}>
|
|
<DialogTitle>User Already exists</DialogTitle>
|
|
<DialogContent>This user is already part of {label}.</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={() => {
|
|
setOpenExistingUser(false);
|
|
close();
|
|
}}>
|
|
Close
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
const shareableLinksList = () => {
|
|
const now = moment();
|
|
let items: any[] = [];
|
|
shareableLinks.forEach((link: ShareableLink) => {
|
|
const expiration = moment(link.settings.expiration);
|
|
const neverExpires = link.settings.expiration === getNeverExpires();
|
|
const expired = !neverExpires && expiration < now;
|
|
items.push(
|
|
<ListItem key={link.key()}>
|
|
{expired ? (
|
|
<IconButton disabled>
|
|
<LinkOff />
|
|
</IconButton>
|
|
) : (
|
|
<Tooltip title="Click to copy link">
|
|
<IconButton onClick={() => copyShareableLinkToClipboard(link.key())}>
|
|
<LinkIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
|
|
<ListItemText
|
|
primary={
|
|
expired ? (
|
|
<span className={classes.grey}>{"/devices/" + link.key()}</span>
|
|
) : (
|
|
<Link href={getShareableLinkURL(link.key())} target="_blank">
|
|
{"/devices/" + link.key()}
|
|
</Link>
|
|
)
|
|
}
|
|
secondary={
|
|
neverExpires ? (
|
|
"Never expires"
|
|
) : (
|
|
<Tooltip title={expiration.calendar()}>
|
|
<span>{(expired ? "Expired " : "Expires ") + expiration.fromNow()}</span>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
/>
|
|
<ListItemSecondaryAction>
|
|
<Tooltip title="Revoke shareable link">
|
|
<IconButton onClick={() => revokeShareableLink(link.key())}>
|
|
<RemoveCircleIcon className={classes.removeIcon} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
);
|
|
});
|
|
return (
|
|
<List>
|
|
{isLoadingLinks ? <CircularProgress /> : <React.Fragment>{items}</React.Fragment>}
|
|
{shareableLinks.length > 0 && <Divider variant="middle" />}
|
|
<ListItem>
|
|
<FormGroup row>
|
|
<MobileDateTimePicker
|
|
disabled={isNewLinkInfinite}
|
|
// inputRef={props => <TextField {...props} helperText="" />}
|
|
label="Expiration Date"
|
|
value={newLinkExpiration}
|
|
// onChange={(date: moment.Moment) => setNewLinkExpiration(date as Moment)}
|
|
onChange={(value) => setNewLinkExpiration(value as Moment)}
|
|
disablePast
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={isNewLinkInfinite}
|
|
onChange={(_, checked) => setIsNewLinkInfinite(checked)}
|
|
value="isNewLinkInfinite"
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label="Never expires"
|
|
/>
|
|
</FormGroup>
|
|
<ListItemSecondaryAction>
|
|
<Tooltip title="Create new shareable link">
|
|
<IconButton
|
|
className={classes.addIcon}
|
|
color="default"
|
|
onClick={() => createShareableLink()}>
|
|
<AddIcon className={classes.addIcon} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
</List>
|
|
);
|
|
};
|
|
|
|
const content = () => {
|
|
switch (tab) {
|
|
case 1:
|
|
return <React.Fragment>{shareableLinksList()}</React.Fragment>;
|
|
default:
|
|
return <React.Fragment>{emailSharing()}</React.Fragment>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{existingUserDialog()}
|
|
<ResponsiveDialog
|
|
maxWidth="sm"
|
|
fullWidth
|
|
open={isDialogOpen}
|
|
onClose={close}
|
|
aria-labelledby="share-dialog-title">
|
|
<DialogTitle id="share-dialog-title">
|
|
Share
|
|
<Typography variant="body2" color="textSecondary">
|
|
{label}
|
|
</Typography>
|
|
</DialogTitle>
|
|
{scope.kind === "device" && (
|
|
<Tabs value={tab} onChange={changeTab}>
|
|
<Tab label="Share by Email" />
|
|
<Tab label="Shareable Links" />
|
|
</Tabs>
|
|
)}
|
|
<Divider />
|
|
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={close} color="primary">
|
|
Cancel
|
|
</Button>
|
|
{tab === 0 && (
|
|
<Button
|
|
onClick={() => {
|
|
//check here if the email they entered is already in the list of shared users if it was passed in
|
|
let hasUser = false;
|
|
sharedUsers?.forEach(user => {
|
|
if (user.settings.email === email) hasUser = true;
|
|
});
|
|
if (hasUser) {
|
|
setOpenExistingUser(true);
|
|
} else {
|
|
share();
|
|
}
|
|
}}
|
|
color="primary"
|
|
disabled={!isValid()}>
|
|
Share
|
|
</Button>
|
|
)}
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
</React.Fragment>
|
|
);
|
|
}
|