diff --git a/browser/app/js/actions.js b/browser/app/js/actions.js index 979f1ad97..2acf688e1 100644 --- a/browser/app/js/actions.js +++ b/browser/app/js/actions.js @@ -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`) }) } } diff --git a/browser/app/js/components/Browse.js b/browser/app/js/components/Browse.js index 128b6c8c2..6458ad22b 100644 --- a/browser/app/js/components/Browse.js +++ b/browser/app/js/components/Browse.js @@ -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 { - } return ( @@ -545,10 +546,18 @@ export default class Browse extends React.Component {
- + + + +
+ Loading... +
{ createButton } diff --git a/browser/app/js/components/Path.js b/browser/app/js/components/Path.js index 6ca85869b..901f094aa 100644 --- a/browser/app/js/components/Path.js +++ b/browser/app/js/components/Path.js @@ -29,7 +29,7 @@ let Path = ({currentBucket, currentPath, selectPrefix}) => { } return ( -

selectPrefix(e, '') } href="">{ currentBucket }{ path }

+

selectPrefix(e, '') } href="">{ currentBucket }{ path }

) } diff --git a/browser/app/js/reducers.js b/browser/app/js/reducers.js index 00482e1ff..ca312fbe7 100644 --- a/browser/app/js/reducers.js +++ b/browser/app/js/reducers.js @@ -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 } diff --git a/browser/package.json b/browser/package.json index 23e8145c6..583d87621 100644 --- a/browser/package.json +++ b/browser/package.json @@ -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" } } diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go index e11743e94..c988ab835 100644 --- a/cmd/web-handlers.go +++ b/cmd/web-handlers.go @@ -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 }