mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
browser: Implement infinite scrolling for object listing. (#3720)
fixes #2831
This commit is contained in:
parent
8f66cfa316
commit
25b936c369
@ -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`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user