Skip to content

Commit

Permalink
Feat/files progress feedback (#1495)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelramalho19 authored May 22, 2020
1 parent 9c89d6d commit 4cc5e80
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 55 deletions.
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

0 comments on commit 4cc5e80

Please sign in to comment.