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

fixes #2831
master
Krishna Srinivas 8 years ago committed by Harshavardhana
parent 8f66cfa316
commit 25b936c369
  1. 55
      browser/app/js/actions.js
  2. 23
      browser/app/js/components/Browse.js
  3. 16
      browser/app/js/reducers.js
  4. 2
      browser/package.json
  5. 15
      cmd/web-handlers.go

@ -56,6 +56,7 @@ export const SET_POLICIES = 'SET_POLICIES'
export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT' export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION' export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE' export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
export const REMOVE_OBJECT = 'REMOVE_OBJECT'
export const showDeleteConfirmation = (object) => { export const showDeleteConfirmation = (object) => {
return { return {
@ -206,6 +207,13 @@ export const showAlert = alert => {
} }
} }
export const removeObject = object => {
return {
type: REMOVE_OBJECT,
object
}
}
export const setSidebarStatus = (status) => { export const setSidebarStatus = (status) => {
return { return {
type: SET_SIDEBAR_STATUS, type: SET_SIDEBAR_STATUS,
@ -227,10 +235,12 @@ export const setVisibleBuckets = visibleBuckets => {
} }
} }
export const setObjects = (objects) => { export const setObjects = (objects, marker, istruncated) => {
return { return {
type: SET_OBJECTS, 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 => { export const selectPrefix = prefix => {
return (dispatch, getState) => { return (dispatch, getState) => {
const {currentBucket, web} = getState() const {currentBucket, web} = getState()
dispatch(setObjects([], "", true))
dispatch(setLoadPath(prefix)) dispatch(setLoadPath(prefix))
web.ListObjects({ web.ListObjects({
bucketName: currentBucket, bucketName: currentBucket,
prefix prefix,
marker: ""
}) })
.then(res => { .then(res => {
let objects = res.objects let objects = res.objects
if (!objects) if (!objects)
objects = [] objects = []
dispatch(setObjects( dispatch(setObjects(
utils.sortObjectsByName(objects.map(object => { objects,
object.name = object.name.replace(`${prefix}`, ''); return object res.nextmarker,
})) res.istruncated
)) ))
dispatch(setPrefixWritable(res.writable)) dispatch(setPrefixWritable(res.writable))
dispatch(setSortNameOrder(false)) dispatch(setSortNameOrder(false))

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

@ -21,6 +21,7 @@ export default (state = {
buckets: [], buckets: [],
visibleBuckets: [], visibleBuckets: [],
objects: [], objects: [],
istruncated: true,
storageInfo: {}, storageInfo: {},
serverInfo: {}, serverInfo: {},
currentBucket: '', currentBucket: '',
@ -76,7 +77,15 @@ export default (state = {
newState.currentBucket = action.currentBucket newState.currentBucket = action.currentBucket
break break
case actions.SET_OBJECTS: 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 break
case actions.SET_CURRENT_PATH: case actions.SET_CURRENT_PATH:
newState.currentPath = action.currentPath newState.currentPath = action.currentPath
@ -171,6 +180,11 @@ export default (state = {
case actions.SET_PREFIX_WRITABLE: case actions.SET_PREFIX_WRITABLE:
newState.prefixWritable = action.prefixWritable newState.prefixWritable = action.prefixWritable
break 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 return newState
} }

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

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

Loading…
Cancel
Save