Refactor bucket delete and bucket policy (#5580)

This commit adds the bucket delete and bucket policy functionalities
to the browser.

Part of rewriting the browser code to follow best practices and
guidelines of React (issues #5409 and #5410)

The backend code has been modified by @krishnasrinivas to prevent
issue #4498 from occuring. The relevant changes have been made to the
code according to the latest commit and the unit tests in the backend.
This commit also addresses issue #5449.
This commit is contained in:
Kaan Kabalak
2018-02-27 19:14:49 -08:00
committed by Harshavardhana
parent 416841869a
commit a6adef0bdf
23 changed files with 836 additions and 204 deletions

View File

@@ -16,6 +16,7 @@
import React from "react"
import classNames from "classnames"
import BucketDropdown from "./BucketDropdown"
export const Bucket = ({ bucket, isActive, selectBucket }) => {
return (
@@ -36,6 +37,7 @@ export const Bucket = ({ bucket, isActive, selectBucket }) => {
>
{bucket}
</a>
<BucketDropdown bucket={bucket}/>
</li>
)
}

View File

@@ -0,0 +1,92 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import classNames from "classnames"
import { connect } from "react-redux"
import * as actionsBuckets from "./actions"
import { getCurrentBucket } from "./selectors"
import Dropdown from "react-bootstrap/lib/Dropdown"
export class BucketDropdown extends React.Component {
constructor(props) {
super(props)
this.state = {
showBucketDropdown: false
}
}
toggleDropdown() {
if (this.state.showBucketDropdown) {
this.setState({
showBucketDropdown: false
})
} else {
this.setState({
showBucketDropdown: true
})
}
}
render() {
const { bucket, showBucketPolicy, deleteBucket, currentBucket } = this.props
return (
<Dropdown
open = {this.state.showBucketDropdown}
onToggle = {this.toggleDropdown.bind(this)}
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={e => {
e.stopPropagation()
this.toggleDropdown()
showBucketPolicy()
}}
>
Edit policy
</a>
</li>
<li>
<a
onClick={e => {
e.stopPropagation()
this.toggleDropdown()
deleteBucket(bucket)
}}
>
Delete
</a>
</li>
</Dropdown.Menu>
</Dropdown>
)
}
}
const mapDispatchToProps = dispatch => {
return {
deleteBucket: bucket => dispatch(actionsBuckets.deleteBucket(bucket)),
showBucketPolicy: () => dispatch(actionsBuckets.showBucketPolicy())
}
}
export default connect(state => state, mapDispatchToProps)(BucketDropdown)

View File

@@ -28,7 +28,7 @@ export class BucketList extends React.Component {
componentWillMount() {
const { fetchBuckets, setBucketList, selectBucket } = this.props
if (web.LoggedIn()) {
fetchBuckets()
fetchBuckets("list")
} else {
const { bucket, prefix } = pathSlice(history.location.pathname)
if (bucket) {
@@ -63,7 +63,7 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => {
return {
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()),
fetchBuckets: action => dispatch(actionsBuckets.fetchBuckets(action)),
setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)),
selectBucket: bucket => dispatch(actionsBuckets.selectBucket(bucket))
}

View File

@@ -0,0 +1,61 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { connect } from "react-redux"
import { Modal, ModalHeader } from "react-bootstrap"
import * as actionsBuckets from "./actions"
import PolicyInput from "./PolicyInput"
import Policy from "./Policy"
export const BucketPolicyModal = ({ showBucketPolicy, currentBucket, hideBucketPolicy, policies }) => {
return (
<Modal className="modal-policy"
animation={ false }
show={ showBucketPolicy }
onHide={ hideBucketPolicy }
>
<ModalHeader>
Bucket Policy (
{ currentBucket })
<button className="close close-alt" onClick={ hideBucketPolicy }>
<span>×</span>
</button>
</ModalHeader>
<div className="pm-body">
<PolicyInput />
{ policies.map((policy, i) => <Policy key={ i } prefix={ policy.prefix } policy={ policy.policy } />
) }
</div>
</Modal>
)
}
const mapStateToProps = state => {
return {
currentBucket: state.buckets.currentBucket,
showBucketPolicy: state.buckets.showBucketPolicy,
policies: state.buckets.policies
}
}
const mapDispatchToProps = dispatch => {
return {
hideBucketPolicy: () => dispatch(actionsBuckets.hideBucketPolicy())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BucketPolicyModal)

View File

@@ -0,0 +1,93 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
import React from "react"
import { connect } from "react-redux"
import classnames from "classnames"
import * as actionsBuckets from "./actions"
import * as actionsAlert from "../alert/actions"
import web from "../web"
export class Policy extends React.Component {
removePolicy(e) {
e.preventDefault()
const {currentBucket, prefix, fetchPolicies, showAlert} = this.props
web.
SetBucketPolicy({
bucketName: currentBucket,
prefix: prefix,
policy: 'none'
})
.then(() => {
fetchPolicies(currentBucket)
})
.catch(e => showAlert('danger', e.message))
}
render() {
const {policy, prefix} = this.props
let newPrefix = prefix
if (newPrefix === '')
newPrefix = '*'
return (
<div className="pmb-list">
<div className="pmbl-item">
{ newPrefix }
</div>
<div className="pmbl-item">
<select className="form-control"
disabled
value={ policy }>
<option value={ READ_ONLY }>
Read Only
</option>
<option value={ WRITE_ONLY }>
Write Only
</option>
<option value={ READ_WRITE }>
Read and Write
</option>
</select>
</div>
<div className="pmbl-item">
<button className="btn btn-block btn-danger" onClick={ this.removePolicy.bind(this) }>
Remove
</button>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return {
currentBucket: state.buckets.currentBucket
}
}
const mapDispatchToProps = dispatch => {
return {
fetchPolicies: bucket => dispatch(actionsBuckets.fetchPolicies(bucket)),
showAlert: (type, message) =>
dispatch(actionsAlert.set({ type: type, message: message }))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Policy)

View File

@@ -0,0 +1,115 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
import React from "react"
import { connect } from "react-redux"
import classnames from "classnames"
import * as actionsBuckets from "./actions"
import * as actionsAlert from "../alert/actions"
import web from "../web"
export class PolicyInput extends React.Component {
componentDidMount() {
const { currentBucket, fetchPolicies } = this.props
fetchPolicies(currentBucket)
}
componentWillUnmount() {
const { setPolicies } = this.props
setPolicies([])
}
handlePolicySubmit(e) {
e.preventDefault()
const { currentBucket, fetchPolicies, showAlert } = this.props
if (this.prefix.value === "*")
this.prefix.value = ""
let policyAlreadyExists = this.props.policies.some(
elem => this.prefix.value === elem.prefix && this.policy.value === elem.policy
)
if (policyAlreadyExists) {
showAlert("danger", "Policy for this prefix already exists.")
return
}
web.
SetBucketPolicy({
bucketName: currentBucket,
prefix: this.prefix.value,
policy: this.policy.value
})
.then(() => {
fetchPolicies(currentBucket)
this.prefix.value = ''
})
.catch(e => showAlert("danger", e.message))
}
render() {
return (
<header className="pmb-list">
<div className="pmbl-item">
<input
type="text"
ref={ prefix => this.prefix = prefix }
className="form-control"
placeholder="Prefix"
/>
</div>
<div className="pmbl-item">
<select ref={ policy => this.policy = policy } className="form-control">
<option value={ READ_ONLY }>
Read Only
</option>
<option value={ WRITE_ONLY }>
Write Only
</option>
<option value={ READ_WRITE }>
Read and Write
</option>
</select>
</div>
<div className="pmbl-item">
<button className="btn btn-block btn-primary" onClick={ this.handlePolicySubmit.bind(this) }>
Add
</button>
</div>
</header>
)
}
}
const mapStateToProps = state => {
return {
currentBucket: state.buckets.currentBucket,
policies: state.buckets.policies
}
}
const mapDispatchToProps = dispatch => {
return {
fetchPolicies: bucket => dispatch(actionsBuckets.fetchPolicies(bucket)),
setPolicies: policies => dispatch(actionsBuckets.setPolicies(policies)),
showAlert: (type, message) =>
dispatch(actionsAlert.set({ type: type, message: message }))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(PolicyInput)

View File

@@ -0,0 +1,62 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow, mount } from "enzyme"
import { BucketDropdown } from "../BucketDropdown"
describe("BucketDropdown", () => {
it("should render without crashing", () => {
shallow(<BucketDropdown />)
})
it("should call toggleDropdown on dropdown toggle", () => {
const spy = jest.spyOn(BucketDropdown.prototype, 'toggleDropdown')
const wrapper = shallow(
<BucketDropdown />
)
wrapper
.find("Uncontrolled(Dropdown)")
.simulate("toggle")
expect(spy).toHaveBeenCalled()
spy.mockReset()
spy.mockRestore()
})
it("should call showBucketPolicy when Edit Policy link is clicked", () => {
const showBucketPolicy = jest.fn()
const wrapper = shallow(
<BucketDropdown showBucketPolicy={showBucketPolicy} />
)
wrapper
.find("li a")
.at(0)
.simulate("click", { stopPropagation: jest.fn() })
expect(showBucketPolicy).toHaveBeenCalled()
})
it("should call deleteBucket when Delete link is clicked", () => {
const deleteBucket = jest.fn()
const wrapper = shallow(
<BucketDropdown bucket={"test"} deleteBucket={deleteBucket} />
)
wrapper
.find("li a")
.at(1)
.simulate("click", { stopPropagation: jest.fn() })
expect(deleteBucket).toHaveBeenCalledWith("test")
})
})

View File

@@ -0,0 +1,43 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow, mount } from "enzyme"
import { BucketPolicyModal } from "../BucketPolicyModal"
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants"
describe("BucketPolicyModal", () => {
it("should render without crashing", () => {
shallow(<BucketPolicyModal policies={[]}/>)
})
it("should call hideBucketPolicy when close button is clicked", () => {
const hideBucketPolicy = jest.fn()
const wrapper = shallow(
<BucketPolicyModal hideBucketPolicy={hideBucketPolicy} policies={[]} />
)
wrapper.find("button").simulate("click")
expect(hideBucketPolicy).toHaveBeenCalled()
})
it("should include the PolicyInput and Policy components when there are any policies", () => {
const wrapper = shallow(
<BucketPolicyModal policies={ [{prefix: "test", policy: READ_ONLY}] } />
)
expect(wrapper.find("Connect(PolicyInput)").length).toBe(1)
expect(wrapper.find("Connect(Policy)").length).toBe(1)
})
})

View File

@@ -0,0 +1,63 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow, mount } from "enzyme"
import { Policy } from "../Policy"
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants"
import web from "../../web"
jest.mock("../../web", () => ({
SetBucketPolicy: jest.fn(() => {
return Promise.resolve()
})
}))
describe("Policy", () => {
it("should render without crashing", () => {
shallow(<Policy currentBucket={"bucket"} prefix={"foo"} policy={READ_ONLY} />)
})
it("should call web.setBucketPolicy and fetchPolicies on submit", () => {
const fetchPolicies = jest.fn()
const wrapper = shallow(
<Policy
currentBucket={"bucket"}
prefix={"foo"}
policy={READ_ONLY}
fetchPolicies={fetchPolicies}
/>
)
wrapper.find("button").simulate("click", { preventDefault: jest.fn() })
expect(web.SetBucketPolicy).toHaveBeenCalledWith({
bucketName: "bucket",
prefix: "foo",
policy: "none"
})
setImmediate(() => {
expect(fetchPolicies).toHaveBeenCalledWith("bucket")
})
})
it("should change the empty string to '*' while displaying prefixes", () => {
const wrapper = shallow(
<Policy currentBucket={"bucket"} prefix={""} policy={READ_ONLY} />
)
expect(wrapper.find(".pmbl-item").at(0).text()).toEqual("*")
})
})

View File

@@ -0,0 +1,77 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow, mount } from "enzyme"
import { PolicyInput } from "../PolicyInput"
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from "../../constants"
import web from "../../web"
jest.mock("../../web", () => ({
SetBucketPolicy: jest.fn(() => {
return Promise.resolve()
})
}))
describe("PolicyInput", () => {
it("should render without crashing", () => {
const fetchPolicies = jest.fn()
shallow(<PolicyInput currentBucket={"bucket"} fetchPolicies={fetchPolicies}/>)
})
it("should call fetchPolicies after the component has mounted", () => {
const fetchPolicies = jest.fn()
const wrapper = shallow(
<PolicyInput currentBucket={"bucket"} fetchPolicies={fetchPolicies} />
)
setImmediate(() => {
expect(fetchPolicies).toHaveBeenCalled()
})
})
it("should call web.setBucketPolicy and fetchPolicies on submit", () => {
const fetchPolicies = jest.fn()
const wrapper = shallow(
<PolicyInput currentBucket={"bucket"} policies={[]} fetchPolicies={fetchPolicies}/>
)
wrapper.instance().prefix = { value: "baz" }
wrapper.instance().policy = { value: READ_ONLY }
wrapper.find("button").simulate("click", { preventDefault: jest.fn() })
expect(web.SetBucketPolicy).toHaveBeenCalledWith({
bucketName: "bucket",
prefix: "baz",
policy: READ_ONLY
})
setImmediate(() => {
expect(fetchPolicies).toHaveBeenCalledWith("bucket")
})
})
it("should change the prefix '*' to an empty string", () => {
const fetchPolicies = jest.fn()
const wrapper = shallow(
<PolicyInput currentBucket={"bucket"} policies={[]} fetchPolicies={fetchPolicies}/>
)
wrapper.instance().prefix = { value: "*" }
wrapper.instance().policy = { value: READ_ONLY }
wrapper.find("button").simulate("click", { preventDefault: jest.fn() })
expect(wrapper.instance().prefix).toEqual({ value: "" })
})
})

View File

@@ -26,6 +26,9 @@ jest.mock("../../web", () => ({
}),
MakeBucket: jest.fn(() => {
return Promise.resolve()
}),
DeleteBucket: jest.fn(() => {
return Promise.resolve()
})
}))
@@ -43,7 +46,7 @@ describe("Buckets actions", () => {
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] },
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" }
]
return store.dispatch(actionsBuckets.fetchBuckets()).then(() => {
return store.dispatch(actionsBuckets.fetchBuckets("list")).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
@@ -57,7 +60,7 @@ describe("Buckets actions", () => {
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test2" }
]
window.location
return store.dispatch(actionsBuckets.fetchBuckets()).then(() => {
return store.dispatch(actionsBuckets.fetchBuckets("list")).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
@@ -71,7 +74,7 @@ describe("Buckets actions", () => {
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" }
]
window.location
return store.dispatch(actionsBuckets.fetchBuckets()).then(() => {
return store.dispatch(actionsBuckets.fetchBuckets("list")).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
@@ -107,6 +110,36 @@ describe("Buckets actions", () => {
expect(actions).toEqual(expectedActions)
})
it("creates buckets/SHOW_BUCKET_POLICY for showBucketPolicy", () => {
const store = mockStore()
const expectedActions = [
{ type: "buckets/SHOW_BUCKET_POLICY", show: true }
]
store.dispatch(actionsBuckets.showBucketPolicy())
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates buckets/SHOW_BUCKET_POLICY for hideBucketPolicy", () => {
const store = mockStore()
const expectedActions = [
{ type: "buckets/SHOW_BUCKET_POLICY", show: false }
]
store.dispatch(actionsBuckets.hideBucketPolicy())
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates buckets/SET_POLICIES action", () => {
const store = mockStore()
const expectedActions = [
{ type: "buckets/SET_POLICIES", policies: ["test1", "test2"] }
]
store.dispatch(actionsBuckets.setPolicies(["test1", "test2"]))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates buckets/ADD action", () => {
const store = mockStore()
const expectedActions = [{ type: "buckets/ADD", bucket: "test" }]
@@ -115,6 +148,14 @@ describe("Buckets actions", () => {
expect(actions).toEqual(expectedActions)
})
it("creates buckets/REMOVE action", () => {
const store = mockStore()
const expectedActions = [{ type: "buckets/REMOVE", bucket: "test" }]
store.dispatch(actionsBuckets.removeBucket("test"))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates buckets/ADD and buckets/SET_CURRENT_BUCKET after creating the bucket", () => {
const store = mockStore()
const expectedActions = [
@@ -126,4 +167,19 @@ describe("Buckets actions", () => {
expect(actions).toEqual(expectedActions)
})
})
it("creates alert/SET, buckets/REMOVE, buckets/SET_LIST and buckets/SET_CURRENT_BUCKET " +
"after deleting the bucket", () => {
const store = mockStore()
const expectedActions = [
{ type: "alert/SET", alert: {id: 0, message: "Bucket 'test3' has been deleted.", type: "info"} },
{ type: "buckets/REMOVE", bucket: "test3" },
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] },
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" }
]
return store.dispatch(actionsBuckets.deleteBucket("test3")).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
})

View File

@@ -22,8 +22,10 @@ describe("buckets reducer", () => {
const initialState = reducer(undefined, {})
expect(initialState).toEqual({
list: [],
policies: [],
filter: "",
currentBucket: "",
showBucketPolicy: false,
showMakeBucketModal: false
})
})
@@ -47,6 +49,17 @@ describe("buckets reducer", () => {
expect(newState.list).toEqual(["test3", "test1", "test2"])
})
it("should handle REMOVE", () => {
const newState = reducer(
{ list: ["test1", "test2"] },
{
type: actions.REMOVE,
bucket: "test2"
}
)
expect(newState.list).toEqual(["test1"])
})
it("should handle SET_FILTER", () => {
const newState = reducer(undefined, {
type: actions.SET_FILTER,
@@ -63,6 +76,22 @@ describe("buckets reducer", () => {
expect(newState.currentBucket).toEqual("test")
})
it("should handle SET_POLICIES", () => {
const newState = reducer(undefined, {
type: actions.SET_POLICIES,
policies: ["test1", "test2"]
})
expect(newState.policies).toEqual(["test1", "test2"])
})
it("should handle SHOW_BUCKET_POLICY", () => {
const newState = reducer(undefined, {
type: actions.SHOW_BUCKET_POLICY,
show: true
})
expect(newState.showBucketPolicy).toBeTruthy()
})
it("should handle SHOW_MAKE_BUCKET_MODAL", () => {
const newState = reducer(undefined, {
type: actions.SHOW_MAKE_BUCKET_MODAL,

View File

@@ -22,11 +22,14 @@ import { pathSlice } from "../utils"
export const SET_LIST = "buckets/SET_LIST"
export const ADD = "buckets/ADD"
export const REMOVE = "buckets/REMOVE"
export const SET_FILTER = "buckets/SET_FILTER"
export const SET_CURRENT_BUCKET = "buckets/SET_CURRENT_BUCKET"
export const SHOW_MAKE_BUCKET_MODAL = "buckets/SHOW_MAKE_BUCKET_MODAL"
export const SHOW_BUCKET_POLICY = "buckets/SHOW_BUCKET_POLICY"
export const SET_POLICIES = "buckets/SET_POLICIES"
export const fetchBuckets = () => {
export const fetchBuckets = action => {
return function(dispatch) {
return web.ListBuckets().then(res => {
const buckets = res.buckets ? res.buckets.map(bucket => bucket.name) : []
@@ -38,6 +41,9 @@ export const fetchBuckets = () => {
} else {
dispatch(selectBucket(buckets[0]))
}
} else if (action === "delete") {
dispatch(selectBucket(""))
history.replace("/")
}
})
}
@@ -92,11 +98,43 @@ export const makeBucket = bucket => {
}
}
export const deleteBucket = bucket => {
return function(dispatch) {
return web
.DeleteBucket({
bucketName: bucket
})
.then(() => {
dispatch(
alertActions.set({
type: "info",
message: "Bucket '" + bucket + "' has been deleted."
})
)
dispatch(removeBucket(bucket))
dispatch(fetchBuckets("delete"))
})
.catch(err => {
dispatch(
alertActions.set({
type: "danger",
message: err.message
})
)
})
}
}
export const addBucket = bucket => ({
type: ADD,
bucket
})
export const removeBucket = bucket => ({
type: REMOVE,
bucket
})
export const showMakeBucketModal = () => ({
type: SHOW_MAKE_BUCKET_MODAL,
show: true
@@ -106,3 +144,42 @@ export const hideMakeBucketModal = () => ({
type: SHOW_MAKE_BUCKET_MODAL,
show: false
})
export const fetchPolicies = bucket => {
return function(dispatch) {
return web
.ListAllBucketPolicies({
bucketName: bucket
})
.then(res => {
let policies = res.policies
if(policies)
dispatch(setPolicies(policies))
else
dispatch(setPolicies([]))
})
.catch(err => {
dispatch(
alertActions.set({
type: "danger",
message: err.message
})
)
})
}
}
export const setPolicies = policies => ({
type: SET_POLICIES,
policies
})
export const showBucketPolicy = () => ({
type: SHOW_BUCKET_POLICY,
show: true
})
export const hideBucketPolicy = () => ({
type: SHOW_BUCKET_POLICY,
show: false
})

View File

@@ -16,12 +16,22 @@
import * as actionsBuckets from "./actions"
const removeBucket = (list, action) => {
const idx = list.findIndex(bucket => bucket === action.bucket)
if (idx == -1) {
return list
}
return [...list.slice(0, idx), ...list.slice(idx + 1)]
}
export default (
state = {
list: [],
filter: "",
currentBucket: "",
showMakeBucketModal: false
showMakeBucketModal: false,
policies: [],
showBucketPolicy: false
},
action
) => {
@@ -36,6 +46,11 @@ export default (
...state,
list: [action.bucket, ...state.list]
}
case actionsBuckets.REMOVE:
return {
...state,
list: removeBucket(state.list, action),
}
case actionsBuckets.SET_FILTER:
return {
...state,
@@ -51,6 +66,16 @@ export default (
...state,
showMakeBucketModal: action.show
}
case actionsBuckets.SET_POLICIES:
return {
...state,
policies: action.policies
}
case actionsBuckets.SHOW_BUCKET_POLICY:
return {
...state,
showBucketPolicy: action.show
}
default:
return state
}