From 92c727e5f5772f612fe04b750ef5373780ccba5c Mon Sep 17 00:00:00 2001 From: Vojtech Simetka Date: Wed, 2 Jun 2021 13:25:49 +0200 Subject: [PATCH] feat: upload files with postage stamps (#126) * chore: release 0.3.0 * feat: added postage stamp table to list all stamps * feat: postage stamp modal to purchase stamps * feat: postage stamps provider * chore: added formik * chore: proper form state handling * chore: revert accidental release inclusion * chore: polishing identified when developing the upload functionality * feat: upload files with postage stamps * style: tabs styles are defined in theme now, addressed other PR comments * style: removed unused styles * fix: enable encrypted hashes to download Co-authored-by: bee-worker <70210089+bee-worker@users.noreply.github.com> --- src/components/PeerDetail.tsx | 5 +- src/components/TabsContainer.tsx | 71 ++++++++++++++++++++++ src/pages/files/Download.tsx | 65 ++++++++++++++++++++ src/pages/files/SelectStamp.tsx | 48 +++++++++++++++ src/pages/files/Upload.tsx | 101 +++++++++++++++++++++++++++++++ src/pages/files/index.tsx | 87 ++++++-------------------- src/theme.tsx | 62 ++++++++++++++++++- 7 files changed, 365 insertions(+), 74 deletions(-) create mode 100644 src/components/TabsContainer.tsx create mode 100644 src/pages/files/Download.tsx create mode 100644 src/pages/files/SelectStamp.tsx create mode 100644 src/pages/files/Upload.tsx diff --git a/src/components/PeerDetail.tsx b/src/components/PeerDetail.tsx index afa415c..a685399 100644 --- a/src/components/PeerDetail.tsx +++ b/src/components/PeerDetail.tsx @@ -7,9 +7,10 @@ function truncStringPortion(str: string, firstCharCount = 10, endCharCount = 10) interface Props { peerId: string + characterLength?: number } -export default function PeerDetail(props: Props): ReactElement { +export default function PeerDetail({ peerId, characterLength }: Props): ReactElement { return ( - {truncStringPortion(props.peerId)} + {truncStringPortion(peerId, characterLength, characterLength)} ) } diff --git a/src/components/TabsContainer.tsx b/src/components/TabsContainer.tsx new file mode 100644 index 0000000..0100f1e --- /dev/null +++ b/src/components/TabsContainer.tsx @@ -0,0 +1,71 @@ +import React, { ReactElement, ReactNode } from 'react' +import { makeStyles } from '@material-ui/core/styles' +import Tabs from '@material-ui/core/Tabs' +import Tab from '@material-ui/core/Tab' +import Typography from '@material-ui/core/Typography' +import Box from '@material-ui/core/Box' + +interface TabPanelProps { + children?: ReactNode + index: number + value: number +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props + + return ( + + ) +} + +const useStyles = makeStyles(() => ({ + root: { + flexGrow: 1, + }, +})) + +interface TabsValues { + component: ReactNode + label: string +} + +interface Props { + values: TabsValues[] +} + +export default function SimpleTabs({ values }: Props): ReactElement { + const classes = useStyles() + const [value, setValue] = React.useState(0) + + const handleChange = (event: React.ChangeEvent>, newValue: number) => { + setValue(newValue) + } + + return ( +
+ + {values.map(({ label }, index) => ( + + ))} + + {values.map(({ component }, index) => ( + + {component} + + ))} +
+ ) +} diff --git a/src/pages/files/Download.tsx b/src/pages/files/Download.tsx new file mode 100644 index 0000000..5641e12 --- /dev/null +++ b/src/pages/files/Download.tsx @@ -0,0 +1,65 @@ +import { ReactElement, useState } from 'react' +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' +import { Paper, InputBase, IconButton, FormHelperText } from '@material-ui/core' +import { Search } from '@material-ui/icons' +import { apiHost } from '../../constants' +import { Utils } from '@ethersphere/bee-js' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + padding: theme.spacing(0.25), + display: 'flex', + alignItems: 'center', + }, + input: { + marginLeft: theme.spacing(1), + flex: 1, + }, + iconButton: { + padding: 10, + }, + divider: { + height: 28, + margin: 4, + }, + }), +) + +export default function Files(): ReactElement { + const classes = useStyles() + + const [referenceInput, setReferenceInput] = useState('') + const [referenceError, setReferenceError] = useState(null) + + const handleReferenceChange = (e: React.ChangeEvent) => { + setReferenceInput(e.target.value) + + if (Utils.Hex.isHexString(e.target.value, 64) || Utils.Hex.isHexString(e.target.value, 128)) setReferenceError(null) + else setReferenceError(new Error('Incorrect format of swarm hash')) + } + + return ( + <> + + + + + + + {referenceError && {referenceError.message}} + + ) +} diff --git a/src/pages/files/SelectStamp.tsx b/src/pages/files/SelectStamp.tsx new file mode 100644 index 0000000..7449343 --- /dev/null +++ b/src/pages/files/SelectStamp.tsx @@ -0,0 +1,48 @@ +import React, { ReactElement } from 'react' +import Button from '@material-ui/core/Button' +import Menu from '@material-ui/core/Menu' +import MenuItem from '@material-ui/core/MenuItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import { PostageBatch } from '@ethersphere/bee-js' +import PeerDetailDrawer from '../../components/PeerDetail' + +interface Props { + stamps: PostageBatch[] | null + selectedStamp: PostageBatch | null + setSelected: (stamp: PostageBatch) => void +} + +export default function SimpleMenu({ stamps, selectedStamp, setSelected }: Props): ReactElement | null { + const [anchorEl, setAnchorEl] = React.useState(null) + + if (!stamps) return null + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => setAnchorEl(null) + + return ( +
+ + + {stamps.map(stamp => ( + { + setSelected(stamp) + handleClose() + }} + selected={stamp.batchID === selectedStamp?.batchID} + > + {stamp.utilization} + + + ))} + +
+ ) +} diff --git a/src/pages/files/Upload.tsx b/src/pages/files/Upload.tsx new file mode 100644 index 0000000..4349197 --- /dev/null +++ b/src/pages/files/Upload.tsx @@ -0,0 +1,101 @@ +import { ReactElement, useContext, useEffect, useState } from 'react' +import { beeApi } from '../../services/bee' + +import { Button, Container, CircularProgress, FormHelperText } from '@material-ui/core' +import { DropzoneArea } from 'material-ui-dropzone' +import ClipboardCopy from '../../components/ClipboardCopy' +import { PostageBatch } from '@ethersphere/bee-js' +import { Context } from '../../providers/Stamps' +import PeerDetailDrawer from '../../components/PeerDetail' +import Chip from '@material-ui/core/Chip' +import Avatar from '@material-ui/core/Avatar' +import SelectStamp from './SelectStamp' +import CreatePostageStamp from '../stamps/CreatePostageStampModal' + +export default function Files(): ReactElement { + const [file, setFile] = useState(null) + const [uploadReference, setUploadReference] = useState('') + const [uploadError, setUploadError] = useState(null) + const [isUploadingFile, setIsUploadingFile] = useState(false) + + const [selectedStamp, setSelectedStamp] = useState(null) + + const { isLoading, error, stamps } = useContext(Context) + + // Choose a postage stamp that has the lowest utilization + useEffect(() => { + if (!selectedStamp && stamps && stamps.length > 0) { + const stamp = stamps.reduce((prev, curr) => { + if (curr.utilization < prev.utilization) return curr + + return prev + }, stamps[0]) + + setSelectedStamp(stamp) + } + }, [isLoading, error, stamps, selectedStamp]) + + const uploadFile = () => { + if (file === null || selectedStamp === null) return + setIsUploadingFile(true) + setUploadError(null) + beeApi.files + .uploadFile(selectedStamp.batchID, file) + .then(hash => { + setUploadReference(hash) + setFile(null) + }) + .catch(setUploadError) // FIXME: should instead trigger notification + .finally(() => { + setIsUploadingFile(false) + }) + } + + const handleChange = (files?: File[]) => { + if (files) { + setFile(files[0]) + setUploadReference('') + } + } + + return ( +
+
+ +
+ {selectedStamp && ( +
+ + with Postage Stamp{' '} + {selectedStamp.utilization}} + label={} + deleteIcon={} + onDelete={() => {} /* eslint-disable-line*/} + variant="outlined" + /> + + +
+ )} + {!selectedStamp && } + + {isUploadingFile && ( + + + + )} + {uploadReference && ( +
+ {uploadReference} + +
+ )} + {uploadError && {uploadError.message}} +
+
+
+ ) +} diff --git a/src/pages/files/index.tsx b/src/pages/files/index.tsx index 6f421ab..1f16a10 100644 --- a/src/pages/files/index.tsx +++ b/src/pages/files/index.tsx @@ -1,58 +1,17 @@ -import { ReactElement, useState } from 'react' +import { ReactElement } from 'react' -import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' -import { - Paper, - InputBase, - IconButton, - Typography, - Container, - CircularProgress, - FormHelperText, -} from '@material-ui/core' -import { Search } from '@material-ui/icons' +import { Container, CircularProgress } from '@material-ui/core' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' -import { apiHost } from '../../constants' -import { Utils } from '@ethersphere/bee-js' - -const useStyles = makeStyles((theme: Theme) => - createStyles({ - root: { - padding: '2px 4px', - display: 'flex', - alignItems: 'center', - }, - input: { - marginLeft: theme.spacing(1), - flex: 1, - }, - iconButton: { - padding: 10, - }, - divider: { - height: 28, - margin: 4, - }, - }), -) +import Download from './Download' +import Upload from './Upload' +import TabsContainer from '../../components/TabsContainer' export default function Files(): ReactElement { - const classes = useStyles() - - const [referenceInput, setReferenceInput] = useState('') - const [referenceError, setReferenceError] = useState(null) const { health, isLoadingHealth } = useApiHealth() const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() - const handleReferenceChange = (e: React.ChangeEvent) => { - setReferenceInput(e.target.value) - - if (Utils.Hex.isHexString(e.target.value, 64)) setReferenceError(null) - else setReferenceError(new Error('Incorrect format of swarm hash')) - } - if (isLoadingHealth || isLoadingNodeHealth) { return ( @@ -65,30 +24,18 @@ export default function Files(): ReactElement { return ( -
- - Download - -
- - - - - - - {referenceError && {referenceError.message}} + , + }, + { + label: 'upload', + component: , + }, + ]} + />
) } diff --git a/src/theme.tsx b/src/theme.tsx index 133f713..5d11889 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -1,4 +1,5 @@ -import { createMuiTheme } from '@material-ui/core/styles' +import { createMuiTheme, Theme } from '@material-ui/core/styles' +import { orange } from '@material-ui/core/colors' declare module '@material-ui/core/styles/createPalette' { interface TypeBackground { @@ -6,6 +7,54 @@ declare module '@material-ui/core/styles/createPalette' { } } +// Overwriting default components styles +const componentsOverrides = (theme: Theme) => ({ + MuiTab: { + root: { + backgroundColor: 'transparent', + fontWeight: theme.typography.fontWeightRegular, + marginRight: theme.spacing(4), + fontFamily: [ + '-apple-system', + 'BlinkMacSystemFont', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + '"Apple Color Emoji"', + '"Segoe UI Emoji"', + '"Segoe UI Symbol"', + ].join(','), + '&:hover': { + color: theme.palette.secondary, + opacity: 1, + }, + '&$selected': { + color: theme.palette.secondary, + fontWeight: theme.typography.fontWeightMedium, + }, + '&:focus': { + color: theme.palette.secondary, + }, + }, + }, + MuiTabs: { + root: { + borderBottom: 'none', + }, + indicator: { + backgroundColor: theme.palette.primary.main, + }, + }, +}) + +const propsOverrides = { + MuiTab: { + disableRipple: true, + }, +} + export const lightTheme = createMuiTheme({ palette: { type: 'light', @@ -13,7 +62,9 @@ export const lightTheme = createMuiTheme({ default: '#fafafa', }, primary: { - main: '#6a6a6a', + light: orange.A200, + main: '#dd7700', + dark: orange[800], }, secondary: { main: '#333333', @@ -32,7 +83,9 @@ export const darkTheme = createMuiTheme({ paper: '#161b22', }, primary: { + light: orange.A200, main: '#dd7700', + dark: orange[800], }, secondary: { main: '#1f2937', @@ -42,3 +95,8 @@ export const darkTheme = createMuiTheme({ fontFamily: ['Work Sans', 'Montserrat', 'Nunito', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'].join(','), }, }) + +darkTheme.overrides = componentsOverrides(darkTheme) +darkTheme.props = propsOverrides +lightTheme.overrides = componentsOverrides(lightTheme) +lightTheme.props = propsOverrides