browser: Implement infinite scrolling for object listing. (#3720)

fixes #2831
This commit is contained in:
Krishna Srinivas 2017-02-10 22:54:42 -08:00 committed by Harshavardhana
parent 8f66cfa316
commit 25b936c369
6 changed files with 114 additions and 47 deletions

View File

@ -56,6 +56,7 @@ export const SET_POLICIES = 'SET_POLICIES'
export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
export const REMOVE_OBJECT = 'REMOVE_OBJECT'
export const showDeleteConfirmation = (object) => {
return {
@ -206,6 +207,13 @@ export const showAlert = alert => {
}
}
export const removeObject = object => {
return {
type: REMOVE_OBJECT,
object
}
}
export const setSidebarStatus = (status) => {
return {
type: SET_SIDEBAR_STATUS,
@ -227,10 +235,12 @@ export const setVisibleBuckets = visibleBuckets => {
}
}
export const setObjects = (objects) => {
export const setObjects = (objects, marker, istruncated) => {
return {
type: SET_OBJECTS,
objects
objects,
marker,
istruncated
}
}
@ -284,22 +294,55 @@ export const selectBucket = (newCurrentBucket, prefix) => {
}
}
export const listObjects = () => {
return (dispatch, getState) => {
const {currentBucket, currentPath, marker, objects, istruncated, web} = getState()
if (!istruncated) return
web.ListObjects({
bucketName: currentBucket,
prefix: currentPath,
marker: marker
})
.then(res => {
let objects = res.objects
if (!objects.length)
objects = []
dispatch(setObjects(objects, res.nextmarker, res.istruncated))
dispatch(setPrefixWritable(res.writable))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}
export const selectPrefix = prefix => {
return (dispatch, getState) => {
const {currentBucket, web} = getState()
dispatch(setObjects([], "", true))
dispatch(setLoadPath(prefix))
web.ListObjects({
bucketName: currentBucket,
prefix
prefix,
marker: ""
})
.then(res => {
let objects = res.objects
if (!objects)
objects = []
dispatch(setObjects(
utils.sortObjectsByName(objects.map(object => {
object.name = object.name.replace(`${prefix}`, ''); return object
}))
objects,
res.nextmarker,
res.istruncated
))
dispatch(setPrefixWritable(res.writable))
dispatch(setSortNameOrder(false))
@ -314,8 +357,8 @@ export const selectPrefix = prefix => {
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}

View File

@ -47,6 +47,7 @@ import * as mime from '../mime'
import { minioBrowserPrefix } from '../constants'
import CopyToClipboard from 'react-copy-to-clipboard'
import storage from 'local-storage-fallback'
import InfiniteScroll from 'react-infinite-scroller';
export default class Browse extends React.Component {
componentDidMount() {
@ -110,9 +111,6 @@ export default class Browse extends React.Component {
if (!decPathname.endsWith('/'))
decPathname += '/'
if (decPathname === minioBrowserPrefix + '/') {
dispatch(actions.setCurrentBucket(''))
dispatch(actions.setCurrentPath(''))
dispatch(actions.setObjects([]))
return
}
let obj = utils.pathSlice(decPathname)
@ -140,6 +138,11 @@ export default class Browse extends React.Component {
this.props.dispatch(actions.setVisibleBuckets(buckets.filter(bucket => bucket.indexOf(e.target.value) > -1)))
}
listObjects() {
const {dispatch} = this.props
dispatch(actions.listObjects())
}
selectPrefix(e, prefix) {
e.preventDefault()
const {dispatch, currentPath, web, currentBucket} = this.props
@ -231,7 +234,7 @@ export default class Browse extends React.Component {
})
.then(() => {
this.hideDeleteConfirmation()
dispatch(actions.selectPrefix(currentPath))
dispatch(actions.removeObject(deleteConfirmation.object))
})
.catch(e => dispatch(actions.showAlert({
type: 'danger',
@ -360,7 +363,6 @@ export default class Browse extends React.Component {
}
}
render() {
const {total, free} = this.props.storageInfo
const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy} = this.props
@ -370,7 +372,7 @@ export default class Browse extends React.Component {
const {policies, currentBucket, currentPath} = this.props
const {deleteConfirmation} = this.props
const {shareObject} = this.props
const {web, prefixWritable} = this.props
const {web, prefixWritable, istruncated} = this.props
// Don't always show the SettingsModal. This is done here instead of in
// SettingsModal.js so as to allow for #componentWillMount to handle
@ -476,7 +478,6 @@ export default class Browse extends React.Component {
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
}
return (
@ -545,10 +546,18 @@ export default class Browse extends React.Component {
</header>
</div>
<div className="feb-container">
<ObjectsList dataType={ this.dataType.bind(this) }
selectPrefix={ this.selectPrefix.bind(this) }
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
shareObject={ this.shareObject.bind(this) } />
<InfiniteScroll loadMore={ this.listObjects.bind(this) }
hasMore={ istruncated }
useWindow={ true }
initialLoad={ false }>
<ObjectsList dataType={ this.dataType.bind(this) }
selectPrefix={ this.selectPrefix.bind(this) }
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
shareObject={ this.shareObject.bind(this) } />
</InfiniteScroll>
<div className="text-center" style={ { display: istruncated ? 'block' : 'none' } }>
<span>Loading...</span>
</div>
</div>
<UploadModal />
{ createButton }

View File

@ -29,7 +29,7 @@ let Path = ({currentBucket, currentPath, selectPrefix}) => {
}
return (
<h2><span className="main"><a onClick={ (e) => selectPrefix(e, '') } href="">{ currentBucket }</a></span>{ path }</h2>
<h2><span className="main"><a onClick={ (e) => selectPrefix(e, '') } href="">{ currentBucket }</a></span>{ path }</h2>
)
}

View File

@ -21,6 +21,7 @@ export default (state = {
buckets: [],
visibleBuckets: [],
objects: [],
istruncated: true,
storageInfo: {},
serverInfo: {},
currentBucket: '',
@ -76,7 +77,15 @@ export default (state = {
newState.currentBucket = action.currentBucket
break
case actions.SET_OBJECTS:
newState.objects = action.objects
if (!action.objects.length) {
newState.objects = []
newState.marker = ""
newState.istruncated = action.istruncated
} else {
newState.objects = [...newState.objects, ...action.objects]
newState.marker = action.marker
newState.istruncated = action.istruncated
}
break
case actions.SET_CURRENT_PATH:
newState.currentPath = action.currentPath
@ -171,6 +180,11 @@ export default (state = {
case actions.SET_PREFIX_WRITABLE:
newState.prefixWritable = action.prefixWritable
break
case actions.REMOVE_OBJECT:
let idx = newState.objects.findIndex(object => object.name === action.object)
if (idx == -1) break
newState.objects = [...newState.objects.slice(0, idx), ...newState.objects.slice(idx + 1)]
break
}
return newState
}

View File

@ -32,6 +32,7 @@
"copy-webpack-plugin": "^0.3.3",
"css-loader": "^0.23.1",
"esformatter": "^0.10.0",
"esformatter-jsx": "^7.4.1",
"esformatter-jsx-ignore": "^1.0.6",
"expect": "^1.20.2",
"history": "^1.17.0",
@ -77,6 +78,7 @@
"react-custom-scrollbars": "^2.2.2",
"react-dom": "^0.14.6",
"react-dropzone": "^3.5.3",
"react-infinite-scroller": "^1.0.6",
"react-onclickout": "2.0.4"
}
}

View File

@ -178,13 +178,16 @@ func (web *webAPIHandlers) ListBuckets(r *http.Request, args *WebGenericArgs, re
type ListObjectsArgs struct {
BucketName string `json:"bucketName"`
Prefix string `json:"prefix"`
Marker string `json:"marker"`
}
// ListObjectsRep - list objects response.
type ListObjectsRep struct {
Objects []WebObjectInfo `json:"objects"`
Writable bool `json:"writable"` // Used by client to show "upload file" button.
UIVersion string `json:"uiVersion"`
Objects []WebObjectInfo `json:"objects"`
NextMarker string `json:"nextmarker"`
IsTruncated bool `json:"istruncated"`
Writable bool `json:"writable"` // Used by client to show "upload file" button.
UIVersion string `json:"uiVersion"`
}
// WebObjectInfo container for list objects metadata.
@ -226,30 +229,26 @@ func (web *webAPIHandlers) ListObjects(r *http.Request, args *ListObjectsArgs, r
default:
return errAuthentication
}
marker := ""
for {
lo, err := objectAPI.ListObjects(args.BucketName, args.Prefix, marker, "/", 1000)
if err != nil {
return &json2.Error{Message: err.Error()}
}
marker = lo.NextMarker
for _, obj := range lo.Objects {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: obj.Name,
LastModified: obj.ModTime,
Size: obj.Size,
ContentType: obj.ContentType,
})
}
for _, prefix := range lo.Prefixes {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: prefix,
})
}
if !lo.IsTruncated {
break
}
lo, err := objectAPI.ListObjects(args.BucketName, args.Prefix, args.Marker, slashSeparator, 1000)
if err != nil {
return &json2.Error{Message: err.Error()}
}
reply.NextMarker = lo.NextMarker
reply.IsTruncated = lo.IsTruncated
for _, obj := range lo.Objects {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: obj.Name,
LastModified: obj.ModTime,
Size: obj.Size,
ContentType: obj.ContentType,
})
}
for _, prefix := range lo.Prefixes {
reply.Objects = append(reply.Objects, WebObjectInfo{
Key: prefix,
})
}
return nil
}