Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a link sharing from a shared folder manage access #1619

Merged
merged 16 commits into from
Oct 14, 2021
5 changes: 3 additions & 2 deletions packages/common-components/src/Router/ConditionalRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ConditionalRoute: React.FC<IConditionalRouteProps> = ({
exact,
...rest
}) => {
const { state, pathname } = useLocation<{from?: string} | undefined>()
const { state, pathname, hash } = useLocation<{from?: string} | undefined>()
const from = (state as any)?.from

return <Route
Expand All @@ -34,7 +34,8 @@ const ConditionalRoute: React.FC<IConditionalRouteProps> = ({
? <Redirect
to={{
pathname: redirectToSource && from ? from : redirectPath,
state: { from: pathname }
state: { from: pathname },
hash
}}
/>
// this may be converted into loading
Expand Down
5 changes: 4 additions & 1 deletion packages/files-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"@babel/core": "^7.12.10",
"@babel/runtime": "^7.0.0",
"@chainsafe/browser-storage-hooks": "^1.0.1",
"@chainsafe/files-api-client": "^1.18.11",
"@chainsafe/files-api-client": "^1.18.14",
"@chainsafe/web3-context": "1.1.4",
"@lingui/core": "^3.7.2",
"@lingui/react": "^3.7.2",
Expand All @@ -31,6 +31,8 @@
"ethers": "^5.4.3",
"fflate": "^0.7.1",
"formik": "^2.2.5",
"jsrsasign": "^10.4.1",
"key-encoder": "^2.0.3",
"mime-matcher": "^1.0.5",
"posthog-js": "^1.13.10",
"react": "^16.14.0",
Expand Down Expand Up @@ -61,6 +63,7 @@
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.5.0",
"@types/jest": "^26.0.16",
"@types/jsrsasign": "^8.0.13",
"@types/node": "^14.14.10",
"@types/react": "^17.0.0",
"@types/react-beforeunload": "^2.1.0",
Expand Down
15 changes: 14 additions & 1 deletion packages/files-ui/src/Components/FilesRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import PurchasePlanPage from "./Pages/PurchasePlanPage"
import { useThresholdKey } from "../Contexts/ThresholdKeyContext"
import ShareFilesPage from "./Pages/SharedFilesPage"
import SharedFoldersOverview from "./Modules/FileBrowsers/SharedFoldersOverview"
import { NonceResponsePermission } from "@chainsafe/files-api-client"
import LinkSharingLanding from "./Pages/LinkSharingLanding"

export const SETTINGS_BASE = "/settings"
export const LINK_SHARING_BASE = "/link-sharing"

export const ROUTE_LINKS = {
Landing: "/",
PrivacyPolicy: "https://files.chainsafe.io/privacy-policy",
Expand All @@ -27,6 +31,8 @@ export const ROUTE_LINKS = {
UserSurvey: "https://calendly.com/colinschwarz/chainsafe-files-chat",
SharedFolders: "/shared-overview",
SharedFolderBrowserRoot: "/shared",
SharingLink: (permission: NonceResponsePermission, jwt: string, bucketEncryptionKey: string) =>
`${LINK_SHARING_BASE}/${permissionPath(permission)}/${encodeURIComponent(jwt)}#${encodeURIComponent(bucketEncryptionKey)}`,
SharedFolderExplorer: (bucketId: string, rawCurrentPath: string) => {
// bucketId should not have a / at the end
// rawCurrentPath can be empty, or /
Expand All @@ -37,6 +43,7 @@ export const ROUTE_LINKS = {
TeamSignup: "https://shrl.ink/cgQy"
}

export const permissionPath = (permission: NonceResponsePermission) => permission === "read" ? "read" : "edit"
export const SETTINGS_PATHS = ["profile", "plan", "security"] as const
export type SettingsPath = typeof SETTINGS_PATHS[number]

Expand All @@ -48,6 +55,12 @@ const FilesRoutes = () => {
[isLoggedIn, isNewDevice, publicKey, secured, shouldInitializeAccount])
return (
<Switch>
<ConditionalRoute
path={LINK_SHARING_BASE}
isAuthorized={isAuthorized}
component={LinkSharingLanding}
redirectPath={ROUTE_LINKS.Landing}
/>
<ConditionalRoute
exact
path={ROUTE_LINKS.SharedFolders}
Expand Down Expand Up @@ -101,7 +114,7 @@ const FilesRoutes = () => {
redirectPath={ROUTE_LINKS.Landing}
/>
<ConditionalRoute
path='/'
path={ROUTE_LINKS.Landing}
isAuthorized={!isAuthorized}
component={LoginPage}
redirectPath={ROUTE_LINKS.Drive("/")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useCreateOrEditSharedFolder } from "./hooks/useCreateOrEditSharedFolder
import { useLookupSharedFolderUser } from "./hooks/useLookupUser"
import { nameValidator } from "../../../Utils/validationSchema"
import { getUserDisplayName } from "../../../Utils/getUserDisplayName"
import LinkList from "./LinkSharing/LinkList"
import clsx from "clsx"

const useStyles = makeStyles(
({ breakpoints, constants, typography, zIndex, palette }: CSFTheme) => {
Expand Down Expand Up @@ -102,6 +104,9 @@ const useStyles = makeStyles(
errorText: {
marginLeft: constants.generalUnit * 1.5,
color: palette.error.main
},
sharingLink: {
padding: constants.generalUnit * 1.25
}
})
}
Expand Down Expand Up @@ -275,6 +280,17 @@ const CreateOrEditSharedFolderModal = ({ mode, isModalOpen, onClose, bucketToEdi
noOptionsMessage={t`No user found for this query.`}
/>
</div>
{mode === "edit" && !!bucketToEdit && (
<div className={clsx(classes.modalFlexItem, classes.sharingLink)}>
<Typography className={classes.inputLabel}>
<Trans>Sharing link</Trans>
</Typography>
<LinkList
bucketEncryptionKey={bucketToEdit.encryptionKey}
bucketId={bucketToEdit.id}
/>
</div>
)}
<Grid
item
flexDirection="row"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Button, Loading, MenuDropdown } from "@chainsafe/common-components"
import { createStyles, makeStyles } from "@chainsafe/common-theme"
import { NonceResponse, NonceResponsePermission } from "@chainsafe/files-api-client"
import { t, Trans } from "@lingui/macro"
import React, { useCallback, useEffect, useState } from "react"
import { useFilesApi } from "../../../../Contexts/FilesApiContext"
import { CSFTheme } from "../../../../Themes/types"
import SharingLink from "./SharingLink"

const useStyles = makeStyles(
({ constants, palette }: CSFTheme) => {
return createStyles({
root: {
},
options: {
backgroundColor: constants.header.optionsBackground,
color: constants.header.optionsTextColor,
border: `1px solid ${constants.header.optionsBorder}`,
minWidth: 145
tanmoyAtb marked this conversation as resolved.
Show resolved Hide resolved
},
menuItem: {
width: "100%",
display: "flex",
flexDirection: "row",
alignItems: "center",
color: constants.header.menuItemTextColor,
"& svg": {
width: constants.generalUnit * 2,
height: constants.generalUnit * 2,
marginRight: constants.generalUnit,
fill: palette.additional["gray"][7],
stroke: palette.additional["gray"][7]
}
},
icon: {
"& svg": {
fill: constants.header.iconColor
}
},
menuIcon: {
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 20,
marginRight: constants.generalUnit * 1.5,
fill: constants.fileSystemItemRow.menuIcon
},
permissionDropdown: {
padding: `0px ${constants.generalUnit}px`,
backgroundColor: palette.additional["gray"][5],
marginLeft: constants.generalUnit
},
createLink: {
display: "flex",
alignItems: "center",
margin: `${constants.generalUnit * 2.5}px 0`
},
createLinkButton: {
marginRight: constants.generalUnit
},
dropdownTitle: {
padding: `${constants.generalUnit * 0.75}px ${constants.generalUnit}px`
}
})
}
)

interface Props {
bucketId: string
bucketEncryptionKey: string
}

const readRights = t`read rights`
const editRights = t`edit rights`
export const translatedPermission = (permission: NonceResponsePermission) => permission === "read" ? readRights : editRights

const LinkList = ({ bucketId, bucketEncryptionKey }: Props) => {
const classes = useStyles()
const { filesApiClient } = useFilesApi()
const [nonces, setNonces] = useState<NonceResponse[]>([])
const [isLoading, setIsLoading] = useState(false)
const [newLinkPermission, setNewLinkPermission] = useState<NonceResponsePermission>("read")

const refreshNonces = useCallback(() => {
setIsLoading(true)
filesApiClient.getAllNonces()
.then((res) => {
const noncesForCurrentBucket = res.filter(n => n.bucket_id === bucketId)
setNonces(noncesForCurrentBucket)
})
.catch(console.error)
.finally(() => setIsLoading(false))
}, [bucketId, filesApiClient])

useEffect(() => {
refreshNonces()
}, [filesApiClient, refreshNonces])

const onCreateNonce = useCallback(() => {

setIsLoading(true)

return filesApiClient
.createNonce({ bucket_id: bucketId, permission: newLinkPermission })
.catch(console.error)
.finally(() => {
setIsLoading(false)
refreshNonces()
})
}, [bucketId, filesApiClient, newLinkPermission, refreshNonces])

return (
<div className={classes.root}>
<div className={classes.createLink}>
<Button
className={classes.createLinkButton}
onClick={onCreateNonce}
disabled={isLoading}
>
<Trans>Create new link</Trans>
</Button>
<Trans>with</Trans>
<MenuDropdown
title={translatedPermission(newLinkPermission)}
anchor="bottom-right"
className={classes.permissionDropdown}
classNames={{
icon: classes.icon,
options: classes.options,
title: classes.dropdownTitle
}}
testId="permission"
menuItems={[
{
onClick: () => setNewLinkPermission("read"),
contents: (
<div
data-cy="menu-read"
className={classes.menuItem}
>
{readRights}
</div>
)
},
{
onClick: () => setNewLinkPermission("write"),
contents: (
<div
data-cy="menu-write"
className={classes.menuItem}
>
{editRights}
</div>
)
}
]}
/>
</div>
{
isLoading && <Loading size={16} />
}
{
!isLoading && nonces.length > 0 && nonces.map((nonce) =>
<SharingLink
key={nonce.id}
refreshNonces={refreshNonces}
bucketEncryptionKey={bucketEncryptionKey}
nonce={nonce}
/>
)
}
</div>
)
}

export default LinkList
Loading