mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -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 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…
Reference in New Issue
Block a user