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

Feat/files progress feedback #1495

Merged
merged 10 commits into from
May 22, 2020
15 changes: 12 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"i18next-xhr-backend": "^3.2.2",
"internal-nav-helper": "^3.1.0",
"ip": "^1.1.5",
"ipfs-css": "^1.0.0",
"ipfs-css": "^1.1.0",
"ipfs-geoip": "^4.0.0",
"ipfs-redux-bundle": "^7.0.0",
"ipld-explorer-components": "^1.5.1",
Expand Down Expand Up @@ -75,6 +75,7 @@
"react-overlays": "^2.1.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
"react-spring": "^8.0.27",
"react-test-renderer": "^16.12.0",
"react-virtualized": "^9.21.2",
"redux-bundler": "^26.0.0",
Expand Down
7 changes: 7 additions & 0 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@
"filesList": {
"noFiles": "<0>No files in this directory. Click the “Add” button to add some.</0>"
},
"filesImportStatus": {
"imported": "{count, plural, one {Imported 1 item} other {Imported {count} items}}",
"importing": "{count, plural, one {Importing 1 item} other {Importing {count} items}}",
"toggleDropdown": "Toggle dropdown",
"closeDropdown": "Close dropdown",
"count": "{count} of {count}"
},
"addFilesInfo": "<0>Add files to your local IPFS node by clicking the <1>Add to IPFS</1> button above.</0>",
"companionInfo": "<0>As you are using <1>IPFS Companion</1>, the files view is limited to files added while using the extension.</0>",
"tour": {
Expand Down
13 changes: 12 additions & 1 deletion src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,17 @@ export default () => ({
}
})

const paths = files.map(f => ({ path: f.path, size: f.size }))

const updateProgress = (sent) => {
dispatch({ type: 'FILES_WRITE_UPDATED', payload: { id: id, progress: sent / totalSize * 100 } })
dispatch({
type: 'FILES_WRITE_UPDATED',
payload: {
id,
paths,
progress: sent / totalSize * 100
}
})
}

updateProgress(0)
Expand Down Expand Up @@ -273,6 +282,8 @@ export default () => ({
dispatch({ type: 'FILES_UPDATE_SORT', payload: { by, asc } })
},

doFilesClear: () => async ({ dispatch }) => dispatch({ type: 'FILES_CLEAR_ALL' }),

doFilesSizeGet: make(ACTIONS.FILES_SIZE_GET, async (ipfs) => {
const stat = await ipfs.files.stat('/')
return { size: stat.cumulativeSize }
Expand Down
14 changes: 13 additions & 1 deletion src/bundles/files/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export default () => {
}
}

if (action.type === 'FILES_CLEAR_ALL') {
return {
...state,
failed: [],
finished: [],
pending: []
}
}

if (action.type === 'FILES_UPDATE_SORT') {
const pageContent = state.pageContent

Expand Down Expand Up @@ -65,7 +74,10 @@ export default () => {
...state.pending.filter(a => a.id !== id),
{
...pendingAction,
data: data
data: {
...data,
hasError: true
}
}
]
}
Expand Down
11 changes: 2 additions & 9 deletions src/bundles/files/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,9 @@ export default () => ({

selectFilesSorting: (state) => state.files.sorting,

selectWriteFilesProgress: (state) => {
const writes = state.files.pending.filter(s => s.type === ACTIONS.WRITE && s.data.progress)
selectFilesPending: (state) => state.files.pending.filter(s => s.type === ACTIONS.WRITE && s.data.progress),

if (writes.length === 0) {
return null
}

const sum = writes.reduce((acc, s) => s.data.progress + acc, 0)
return sum / writes.length
},
selectFilesFinished: (state) => state.files.finished.filter(s => s.type === ACTIONS.WRITE),

selectFilesHasError: (state) => state.files.failed.length > 0,

Expand Down
12 changes: 11 additions & 1 deletion src/bundles/files/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ export const make = (basename, action, options = {}) => (...args) => async (args

try {
data = await action(getIpfs(), ...args, id, args2)
dispatch({ type: `FILES_${basename}_FINISHED`, payload: { id, ...data } })

const paths = args[0] ? args[0].flat() : []

dispatch({
type: `FILES_${basename}_FINISHED`,
payload: {
id,
...data,
paths
}
})

// Rename specific logic
if (basename === ACTIONS.MOVE) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/notify/Toast.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import CancelIcon from '../../icons/GlyphCancel'
import CancelIcon from '../../icons/GlyphSmallCancel'

const Toast = ({ error, children, onDismiss }) => {
const bg = error ? 'bg-yellow' : 'bg-green'
Expand All @@ -9,7 +9,7 @@ const Toast = ({ error, children, onDismiss }) => {
{children}
<CancelIcon
className='dib fill-current-color ph3 glow o-80 pointer'
style={{ height: '28px', verticalAlign: '-8px' }}
style={{ height: '28px', transform: 'scale(1.5)', verticalAlign: 'bottom' }}
onClick={onDismiss} />
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getJoyrideLocales } from '../helpers/i8n'
// Icons
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_PATH } from './modals/Modals'
import Header from './header/Header'
import FileImportStatus from './file-import-status/FileImportStatus'

const defaultState = {
downloadAbort: null,
Expand Down Expand Up @@ -258,6 +259,8 @@ class FilesPage extends React.Component {
onAddByPath={this.onAddByPath}
{ ...this.state.modals } />

<FileImportStatus />

<ReactJoyride
run={toursEnabled}
steps={filesTour.getSteps({ t, Trans })}
Expand Down
63 changes: 63 additions & 0 deletions src/files/file-import-status/FileImportStatus.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.fileImportStatus {
left: 50%;
transform: translateX(-50%);
}

.fileImportStatusButton {
max-height: 3rem;
}

.fileImportStatusButton .fileImportStatusArrow {
transition: transform 0.2s ease-in-out;
}

.fileImportStatusButton[aria-expanded="false"] .fileImportStatusArrow {
transform: rotate(180deg) translateY(-2px);
}

.fileImportStatusArrow {
margin-left: auto;
width: 1rem;
}

.fileImportStatusCancel {
height: 3rem;
margin-left: 0.5rem;
margin-right: -1.2rem;
}

.fileImportStatusRow {
height: 153px;
overflow: auto;

transition: height 0.2s ease-in-out;
}

.fileImportStatusRow[aria-hidden="true"] {
height: 0
}

.fileImportStatusIcon {
width: 36px;
}

.fileLoadingIndicator {
height: 4px;
overflow: hidden;
}

.fileLoadingIndicatorBar {
width: 25%;
height: 100%;
animation: fileLoadingIndicatorBar 2s ease-in-out infinite;
}

@keyframes fileLoadingIndicatorBar {
0% { transform: translateX(-100%) }
99.9% { transform: translateX(450%) }
100% { transform: translateX(-100%) }
}

.fileImportStatusName {
flex: 1 1;
}
116 changes: 116 additions & 0 deletions src/files/file-import-status/FileImportStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useMemo, useState, useCallback } from 'react'
import classNames from 'classnames'
import filesize from 'filesize'
import PropTypes from 'prop-types'
import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
// Icons
import DocumentIcon from '../../icons/GlyphDocGeneric'
import FolderIcon from '../../icons/GlyphFolder'
import './FileImportStatus.css'
import GlyphSmallArrows from '../../icons/GlyphSmallArrow'
import GlyphTick from '../../icons/GlyphTick'
import GlyphCancel from '../../icons/GlyphCancel'
import GlyphSmallCancel from '../../icons/GlyphSmallCancel'

const File = ({ paths = [], hasError }, t) => {
const pathsByFolder = paths.reduce((prev, currentPath) => {
const isFolder = currentPath.path.includes('/')
if (!isFolder) {
return [...prev, currentPath]
}

const baseFolder = currentPath.path.split('/')[0]

const alreadyExistentBaseFolder = prev.find(previousPath => previousPath.path.startsWith(`${baseFolder}/`))

if (alreadyExistentBaseFolder) {
alreadyExistentBaseFolder.count = alreadyExistentBaseFolder.count + 1

return prev
}

return [...prev, { ...currentPath, name: baseFolder, count: 1 }]
}, [])

return pathsByFolder.map(({ count, name, path, size, progress }) => (
<li className="flex w-100 bb b--light-gray items-center f6 charcoal" key={ path || name }>
{ count ? <FolderIcon className='fileImportStatusIcon fill-aqua pa1'/> : <DocumentIcon className='fileImportStatusIcon fill-aqua pa1'/> }
<span className="fileImportStatusName truncate">{ name || path }</span>
<span className='gray mh2'> |
{ count && (<span> { t('filesImportStatus.count', { count }) } | </span>) }
<span className='ml2'>{ filesize(size) }</span>
</span>
{ hasError ? <GlyphCancel className="dark-red w2 ph1" fill="currentColor"/> : <LoadingIndicator complete={ !progress }/> }
</li>
))
}

const LoadingIndicator = ({ complete }) => (
<>
<div className={ classNames('fileLoadingIndicator bg-light-gray mh4 flex-auto relative', complete && 'dn') }>
<div className='fileLoadingIndicatorBar bg-blue absolute left-0'></div>
</div>
{ complete && <GlyphTick className="green w2 ph1" fill="currentColor"/>}
</>
)

const FileImportStatus = ({ filesFinished, filesPending, filesErrors, doFilesClear, t }) => {
const sortedFilesFinished = useMemo(() => filesFinished.sort((fileA, fileB) => fileB.start - fileA.start), [filesFinished])
const [expanded, setExpanded] = useState(true)

const handleImportStatusClose = useCallback((ev) => {
doFilesClear()
ev.stopPropagation() // Prevent setExpanded from being called
}, [doFilesClear])

if (!filesFinished.length && !filesPending.length && !filesErrors.length) {
return null
}

const numberOfImportedFiles = !filesFinished.length ? 0 : filesFinished.reduce((prev, finishedFile) => prev + finishedFile?.data?.paths?.length, 0)

return (
<div className='fileImportStatus fixed bottom-1 w-100 flex justify-center' style={{ zIndex: 14, pointerEvents: 'none' }}>
<div className="br1 dark-gray w-40 center ba b--light-gray bg-white" style={{ pointerEvents: 'auto' }}>
<div className="fileImportStatusButton pv2 ph3 relative flex items-center no-select pointer charcoal" style={{ background: '#F0F6FA' }}
onClick={() => setExpanded(!expanded)} aria-expanded={expanded} aria-label={ t('filesImportStatus.toggleDropdown') } role="button">
{ filesPending.length
? t('filesImportStatus.importing', { count: filesPending.length })
: t('filesImportStatus.imported', { count: numberOfImportedFiles })
}
<GlyphSmallArrows className='fileImportStatusArrow' fill="currentColor" opacity="0.7"/>
<div onClick={ handleImportStatusClose } aria-label={ t('filesImportStatus.closeDropdown') } role="button">
<GlyphSmallCancel className='fileImportStatusCancel' fill="currentColor" opacity="0.7"/>
</div>
</div>
<ul className='fileImportStatusRow pa0 ma0' aria-hidden={!expanded}>
{ filesPending.map(file => File(file.data, t)) }
{ sortedFilesFinished.map(file => File(file.data, t)) }
{ filesErrors.map(file => File(file.data, t)) }
</ul>
</div>
</div>
)
}

FileImportStatus.propTypes = {
filesFinished: PropTypes.array,
filesPending: PropTypes.array,
filesErrors: PropTypes.array,
doFilesClear: PropTypes.func
}

FileImportStatus.defaultProps = {
filesFinished: [],
filesPending: [],
filesErrors: []
}

export default connect(
'selectFilesFinished',
'selectFilesPending',
'selectFilesErrors',
'doFilesClear',
withTranslation('files')(FileImportStatus)
)
Loading