Integrate existing remove bucket functionality from newux to current UI (#5289)

This commit takes the existing remove bucket functionality written by
brendanashworth, integrates it to the current UI with a dropdown for
each bucket, and fixes small issues that were present, like the dropdown
not disappearing after the user clicks on 'Delete' for certain buckets.
This feature only deletes a bucket that is empty (that has no objects).

Fixes #4166
master
Kaan Kabalak 7 years ago committed by Nitish Tiwari
parent baaf67d82e
commit 659f724f4c
  1. 2
      browser/app/index.js
  2. 48
      browser/app/js/actions.js
  3. 32
      browser/app/js/components/Browse.js
  4. 18
      browser/app/js/components/SideBar.js
  5. 9
      browser/app/js/reducers.js
  6. 3
      browser/app/js/web.js
  7. 39
      browser/app/less/inc/sidebar.less
  8. 1
      browser/package.json
  9. 102
      browser/ui-assets.go
  10. 30
      cmd/web-handlers.go
  11. 101
      cmd/web-handlers_test.go

@ -41,6 +41,8 @@ import _Login from './js/components/Login.js'
import _Browse from './js/components/Browse.js' import _Browse from './js/components/Browse.js'
import fontAwesome from 'font-awesome/css/font-awesome.css' import fontAwesome from 'font-awesome/css/font-awesome.css'
import MaterialDesignIconicFonts from 'material-design-iconic-font/dist/css/material-design-iconic-font.min.css'
import Web from './js/web' import Web from './js/web'
window.Web = Web window.Web = Web

@ -24,6 +24,8 @@ export const SET_CURRENT_BUCKET = 'SET_CURRENT_BUCKET'
export const SET_CURRENT_PATH = 'SET_CURRENT_PATH' export const SET_CURRENT_PATH = 'SET_CURRENT_PATH'
export const SET_BUCKETS = 'SET_BUCKETS' export const SET_BUCKETS = 'SET_BUCKETS'
export const ADD_BUCKET = 'ADD_BUCKET' export const ADD_BUCKET = 'ADD_BUCKET'
export const REMOVE_BUCKET = 'REMOVE_BUCKET'
export const SHOW_BUCKET_DROPDOWN = 'SHOW_BUCKET_DROPDOWN'
export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS' export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS'
export const SET_OBJECTS = 'SET_OBJECTS' export const SET_OBJECTS = 'SET_OBJECTS'
export const APPEND_OBJECTS = 'APPEND_OBJECTS' export const APPEND_OBJECTS = 'APPEND_OBJECTS'
@ -173,6 +175,27 @@ export const addBucket = bucket => {
} }
} }
export const removeBucket = bucket => {
return {
type: REMOVE_BUCKET,
bucket
}
}
export const showBucketDropdown = bucket => {
return {
type: SHOW_BUCKET_DROPDOWN,
showBucketDropdown: true
}
}
export const hideBucketDropdown = bucket => {
return {
type: SHOW_BUCKET_DROPDOWN,
showBucketDropdown: false
}
}
export const showMakeBucketModal = () => { export const showMakeBucketModal = () => {
return { return {
type: SHOW_MAKEBUCKET_MODAL, type: SHOW_MAKEBUCKET_MODAL,
@ -314,6 +337,31 @@ export const selectBucket = (newCurrentBucket, prefix) => {
} }
} }
export const deleteBucket = (bucket) => {
return (dispatch, getState) => {
// DeleteBucket() RPC call will ONLY delete a bucket if it is empty of
// objects. This means a call can just be sent, as it is entirely reversable
// and won't do any permanent damage.
web.DeleteBucket({
bucketName: bucket
})
.then(() => {
dispatch(showAlert({
type: 'info',
message: `Bucket '${bucket}' has been deleted.`
}))
dispatch(removeBucket(bucket))
})
.catch(err => {
let message = err.message
dispatch(showAlert({
type: 'danger',
message: message
}))
})
}
}
export const listObjects = () => { export const listObjects = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
const {buckets, currentBucket, currentPath, marker, objects, istruncated, web} = getState() const {buckets, currentBucket, currentPath, marker, objects, istruncated, web} = getState()

@ -210,9 +210,19 @@ export default class Browse extends React.Component {
dispatch(actions.hideAbout()) dispatch(actions.hideAbout())
} }
toggleBucketDropdown(e) {
const {dispatch, showBucketDropdown} = this.props
if (showBucketDropdown) {
dispatch(actions.hideBucketDropdown())
} else {
dispatch(actions.showBucketDropdown())
}
}
showBucketPolicy(e) { showBucketPolicy(e) {
e.preventDefault() e.preventDefault()
const {dispatch} = this.props const {dispatch} = this.props
this.toggleBucketDropdown(e)
dispatch(actions.showBucketPolicy()) dispatch(actions.showBucketPolicy())
} }
@ -222,10 +232,17 @@ export default class Browse extends React.Component {
dispatch(actions.hideBucketPolicy()) dispatch(actions.hideBucketPolicy())
} }
uploadFile(e) { deleteBucket(e, bucket) {
e.preventDefault() e.preventDefault()
const {dispatch, buckets} = this.props const {dispatch} = this.props
this.toggleBucketDropdown(e)
dispatch(actions.deleteBucket(bucket))
browserHistory.push(`${minioBrowserPrefix}/`)
}
uploadFile(e) {
e.preventDefault()
const {dispatch, buckets, currentBucket} = this.props
if (buckets.length === 0) { if (buckets.length === 0) {
dispatch(actions.showAlert({ dispatch(actions.showAlert({
type: 'danger', type: 'danger',
@ -233,6 +250,13 @@ export default class Browse extends React.Component {
})) }))
return return
} }
if (currentBucket === '') {
dispatch(actions.showAlert({
type: 'danger',
message: "Please choose a bucket before trying to upload files."
}))
return
}
let file = e.target.files[0] let file = e.target.files[0]
e.target.value = null e.target.value = null
this.xhr = new XMLHttpRequest() this.xhr = new XMLHttpRequest()
@ -577,7 +601,9 @@ export default class Browse extends React.Component {
<SideBar searchBuckets={ this.searchBuckets.bind(this) } <SideBar searchBuckets={ this.searchBuckets.bind(this) }
selectBucket={ this.selectBucket.bind(this) } selectBucket={ this.selectBucket.bind(this) }
clickOutside={ this.hideSidebar.bind(this) } clickOutside={ this.hideSidebar.bind(this) }
showPolicy={ this.showBucketPolicy.bind(this) } /> showPolicy={ this.showBucketPolicy.bind(this) }
deleteBucket={ this.deleteBucket.bind(this) }
toggleBucketDropdown={ this.toggleBucketDropdown.bind(this) } />
<div className="fe-body"> <div className="fe-body">
<div className={ 'list-actions' + (classNames({ <div className={ 'list-actions' + (classNames({
' list-actions-toggled': checkedObjects.length > 0 ' list-actions-toggled': checkedObjects.length > 0

@ -21,8 +21,9 @@ import Scrollbars from 'react-custom-scrollbars/lib/Scrollbars'
import connect from 'react-redux/lib/components/connect' import connect from 'react-redux/lib/components/connect'
import logo from '../../img/logo.svg' import logo from '../../img/logo.svg'
import Dropdown from 'react-bootstrap/lib/Dropdown'
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy}) => { let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy, deleteBucket, toggleBucketDropdown, showBucketDropdown}) => {
const list = visibleBuckets.map((bucket, i) => { const list = visibleBuckets.map((bucket, i) => {
return <li className={ classNames({ return <li className={ classNames({
@ -33,7 +34,19 @@ let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchB
}) }> }) }>
{ bucket } { bucket }
</a> </a>
<i className="fesli-trigger" onClick={ showPolicy }></i> <Dropdown open={bucket === currentBucket && showBucketDropdown} onToggle={toggleBucketDropdown} className="bucket-dropdown" id="bucket-dropdown">
<Dropdown.Toggle noCaret>
<i className="zmdi zmdi-more-vert" />
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<li>
<a onClick={ showPolicy }>Edit policy</a>
</li>
<li>
<a onClick={ (e) => deleteBucket(e, bucket) }>Delete</a>
</li>
</Dropdown.Menu>
</Dropdown>
</li> </li>
}) })
@ -80,6 +93,7 @@ export default connect(state => {
visibleBuckets: state.visibleBuckets, visibleBuckets: state.visibleBuckets,
loadBucket: state.loadBucket, loadBucket: state.loadBucket,
currentBucket: state.currentBucket, currentBucket: state.currentBucket,
showBucketDropdown: state.showBucketDropdown,
sidebarStatus: state.sidebarStatus sidebarStatus: state.sidebarStatus
} }
})(SideBar) })(SideBar)

@ -25,6 +25,7 @@ export default (state = {
storageInfo: {}, storageInfo: {},
serverInfo: {}, serverInfo: {},
currentBucket: '', currentBucket: '',
showBucketDropdown: false,
currentPath: '', currentPath: '',
showMakeBucketModal: false, showMakeBucketModal: false,
uploads: {}, uploads: {},
@ -71,6 +72,14 @@ export default (state = {
newState.buckets = [action.bucket, ...newState.buckets] newState.buckets = [action.bucket, ...newState.buckets]
newState.visibleBuckets = [action.bucket, ...newState.visibleBuckets] newState.visibleBuckets = [action.bucket, ...newState.visibleBuckets]
break break
case actions.REMOVE_BUCKET:
newState.buckets = newState.buckets.filter(bucket => bucket != action.bucket)
newState.visibleBuckets = newState.visibleBuckets.filter(bucket => bucket != action.bucket)
newState.currentBucket = ""
break
case actions.SHOW_BUCKET_DROPDOWN:
newState.showBucketDropdown = action.showBucketDropdown
break
case actions.SET_VISIBLE_BUCKETS: case actions.SET_VISIBLE_BUCKETS:
newState.visibleBuckets = action.visibleBuckets newState.visibleBuckets = action.visibleBuckets
break break

@ -87,6 +87,9 @@ export default class Web {
MakeBucket(args) { MakeBucket(args) {
return this.makeCall('MakeBucket', args) return this.makeCall('MakeBucket', args)
} }
DeleteBucket(args) {
return this.makeCall('DeleteBucket', args)
}
ListObjects(args) { ListObjects(args) {
return this.makeCall('ListObjects', args) return this.makeCall('ListObjects', args)
} }

@ -111,7 +111,7 @@
} }
&:hover { &:hover {
.fesli-trigger { .bucket-dropdown .dropdown-toggle {
.opacity(0.6); .opacity(0.6);
&:hover { &:hover {
@ -132,17 +132,40 @@
} }
} }
.fesli-trigger { /* Dropdown */
.opacity(0); .bucket-dropdown {
.transition(all);
.transition-duration(200ms);
position: absolute; position: absolute;
top: 0; top: 0px;
right: 0; right: 0px;
width: 35px; width: 35px;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
background: url(../../img/more-h-light.svg) no-repeat top 20px left; color: @white;
.dropdown-toggle {
.opacity(0);
.transition(all);
.transition-duration(200ms);
font-size: 20px;
background-color: transparent;
}
.dropdown-menu-right {
padding: 15px 0;
margin-top: -1px;
& li {
a:before {
content: none;
-webkit-box-shadow: @dropdown-shadow;
box-shadow: @dropdown-shadow;
}
&:not(.active):hover {
& > a {
color: @dropdown-link-hover-color;
}
}
}
}
} }
/* Scrollbar */ /* Scrollbar */

@ -60,6 +60,7 @@
"humanize": "0.0.9", "humanize": "0.0.9",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"local-storage-fallback": "^1.3.0", "local-storage-fallback": "^1.3.0",
"material-design-iconic-font": "^2.2.0",
"mime-db": "^1.25.0", "mime-db": "^1.25.0",
"mime-types": "^2.1.13", "mime-types": "^2.1.13",
"moment": "^2.15.1", "moment": "^2.15.1",

File diff suppressed because one or more lines are too long

@ -145,6 +145,36 @@ func (web *webAPIHandlers) MakeBucket(r *http.Request, args *MakeBucketArgs, rep
return nil return nil
} }
// RemoveBucketArgs - remove bucket args.
type RemoveBucketArgs struct {
BucketName string `json:"bucketName"`
}
// DeleteBucket - removes a bucket, must be empty.
func (web *webAPIHandlers) DeleteBucket(r *http.Request, args *RemoveBucketArgs, reply *WebGenericRep) error {
objectAPI := web.ObjectAPI()
if objectAPI == nil {
return toJSONError(errServerNotInitialized)
}
if !isHTTPRequestValid(r) {
return toJSONError(errAuthentication)
}
bucketLock := globalNSMutex.NewNSLock(args.BucketName, "")
if err := bucketLock.GetLock(globalObjectTimeout); err != nil {
return toJSONError(errOperationTimedOut)
}
defer bucketLock.Unlock()
err := objectAPI.DeleteBucket(args.BucketName)
if err != nil {
return toJSONError(err, args.BucketName)
}
reply.UIVersion = browser.UIVersion
return nil
}
// ListBucketsRep - list buckets response // ListBucketsRep - list buckets response
type ListBucketsRep struct { type ListBucketsRep struct {
Buckets []WebBucketInfo `json:"buckets"` Buckets []WebBucketInfo `json:"buckets"`

@ -305,6 +305,107 @@ func testMakeBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrHan
} }
} }
// Wrapper for calling DeleteBucket handler
func TestWebHandlerDeleteBucket(t *testing.T) {
ExecObjectLayerTest(t, testDeleteBucketWebHandler)
}
// testDeleteBucketWebHandler - Test DeleteBucket web handler
func testDeleteBucketWebHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
apiRouter := initTestWebRPCEndPoint(obj)
credentials := globalServerConfig.GetCredential()
token, err := getWebRPCToken(apiRouter, credentials.AccessKey, credentials.SecretKey)
if err != nil {
t.Fatalf("could not get RPC token, %s", err.Error())
}
bucketName := getRandomBucketName()
err = obj.MakeBucketWithLocation(bucketName, "")
if err != nil {
t.Fatalf("failed to create bucket: %s (%s)", err.Error(), instanceType)
}
testCases := []struct {
bucketName string
// Whether or not to put an object into the bucket.
initWithObject bool
token string
// Expected error (error must only contain this string to pass test)
// Empty string = no error
expect string
}{
{"", false, token, "Bucket Name is invalid"},
{".", false, "auth", "Authentication failed"},
{".", false, token, "Bucket Name . is invalid"},
{"ab", false, token, "Bucket Name ab is invalid"},
{"minio", false, "false token", "Authentication failed"},
{"minio", false, token, "specified bucket minio does not exist"},
{bucketName, false, token, ""},
{bucketName, true, token, "Bucket not empty"},
{bucketName, false, "", "Authentication failed"},
}
for _, test := range testCases {
if test.initWithObject {
data := bytes.NewBufferString("hello")
_, err = obj.PutObject(test.bucketName, "object", mustGetHashReader(t, data, int64(data.Len()), "", ""), nil)
// _, err = obj.PutObject(test.bucketName, "object", int64(data.Len()), data, nil, "")
if err != nil {
t.Fatalf("could not put object to %s, %s", test.bucketName, err.Error())
}
}
rec := httptest.NewRecorder()
makeBucketRequest := MakeBucketArgs{BucketName: test.bucketName}
makeBucketReply := &WebGenericRep{}
req, err := newTestWebRPCRequest("Web.DeleteBucket", test.token, makeBucketRequest)
if err != nil {
t.Errorf("failed to create HTTP request: <ERROR> %v", err)
}
apiRouter.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected the response status to be `%d`, but instead found `%d`", http.StatusOK, rec.Code)
}
err = getTestWebRPCResponse(rec, &makeBucketReply)
if test.expect != "" {
if err == nil {
// If we expected an error, but didn't get one.
t.Errorf("expected `..%s..` but got nil error", test.expect)
} else if !strings.Contains(err.Error(), test.expect) {
// If we got an error that wasn't what we expected.
t.Errorf("expected `..%s..` but got `%s`", test.expect, err.Error())
}
} else if test.expect == "" && err != nil {
t.Errorf("expected test success, but got `%s`", err.Error())
}
// If we created the bucket with an object, now delete the object to cleanup.
if test.initWithObject {
err = obj.DeleteObject(test.bucketName, "object")
if err != nil {
t.Fatalf("could not delete object, %s", err.Error())
}
}
// If it did not succeed in deleting the bucket, don't try and recreate it.
// Or, it'll fail if there was an object.
if err != nil || test.initWithObject {
continue
}
err = obj.MakeBucketWithLocation(bucketName, "")
if err != nil {
// failed to create new bucket, abort.
t.Fatalf("failed to create new bucket (%s): %s", instanceType, err.Error())
}
}
}
// Wrapper for calling ListBuckets Web Handler // Wrapper for calling ListBuckets Web Handler
func TestWebHandlerListBuckets(t *testing.T) { func TestWebHandlerListBuckets(t *testing.T) {
ExecObjectLayerTest(t, testListBucketsWebHandler) ExecObjectLayerTest(t, testListBucketsWebHandler)

Loading…
Cancel
Save