mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
Refactor make bucket and upload components (#5521)
This commit is contained in:
parent
9bfa07ecf5
commit
44f8f7059c
41
browser/app/js/alert/AlertContainer.js
Normal file
41
browser/app/js/alert/AlertContainer.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 Alert from "./Alert"
|
||||||
|
import * as alertActions from "./actions"
|
||||||
|
|
||||||
|
export const AlertContainer = ({ alert, clearAlert }) => {
|
||||||
|
if (!alert.message) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return <Alert {...alert} onDismiss={clearAlert} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
alert: state.alert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
clearAlert: () => dispatch(alertActions.clear())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AlertContainer)
|
@ -26,7 +26,7 @@ describe("Alert", () => {
|
|||||||
it("should call onDismiss when close button is clicked", () => {
|
it("should call onDismiss when close button is clicked", () => {
|
||||||
const onDismiss = jest.fn()
|
const onDismiss = jest.fn()
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<Alert show={true} type="danger" message="test" onDismiss={onDismiss}/>
|
<Alert show={true} type="danger" message="test" onDismiss={onDismiss} />
|
||||||
)
|
)
|
||||||
wrapper.find("button").simulate("click", { preventDefault: jest.fn() })
|
wrapper.find("button").simulate("click", { preventDefault: jest.fn() })
|
||||||
expect(onDismiss).toHaveBeenCalled()
|
expect(onDismiss).toHaveBeenCalled()
|
||||||
|
34
browser/app/js/alert/__tests___/AlertContainer.test.js
Normal file
34
browser/app/js/alert/__tests___/AlertContainer.test.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AlertContainer } from "../AlertContainer"
|
||||||
|
|
||||||
|
describe("Alert", () => {
|
||||||
|
it("should render without crashing", () => {
|
||||||
|
shallow(
|
||||||
|
<AlertContainer alert={{ show: true, type: "danger", message: "Test" }} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render nothing if message is empty", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<AlertContainer alert={{ show: true, type: "danger", message: "" }} />
|
||||||
|
)
|
||||||
|
expect(wrapper.find("Alert").length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
@ -19,6 +19,7 @@ import classNames from "classnames"
|
|||||||
import { connect } from "react-redux"
|
import { connect } from "react-redux"
|
||||||
import SideBar from "./SideBar"
|
import SideBar from "./SideBar"
|
||||||
import MainContent from "./MainContent"
|
import MainContent from "./MainContent"
|
||||||
|
import AlertContainer from "../alert/AlertContainer"
|
||||||
|
|
||||||
class Browser extends React.Component {
|
class Browser extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
@ -30,6 +31,7 @@ class Browser extends React.Component {
|
|||||||
>
|
>
|
||||||
<SideBar />
|
<SideBar />
|
||||||
<MainContent />
|
<MainContent />
|
||||||
|
<AlertContainer />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
* Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,33 +14,40 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import Modal from 'react-bootstrap/lib/Modal'
|
import { Modal, ModalBody } from "react-bootstrap"
|
||||||
import ModalBody from 'react-bootstrap/lib/ModalBody'
|
|
||||||
|
|
||||||
let ConfirmModal = ({baseClass, icon, text, sub, okText, cancelText, okHandler, cancelHandler, show}) => {
|
let ConfirmModal = ({
|
||||||
|
baseClass,
|
||||||
|
icon,
|
||||||
|
text,
|
||||||
|
sub,
|
||||||
|
okText,
|
||||||
|
cancelText,
|
||||||
|
okHandler,
|
||||||
|
cancelHandler,
|
||||||
|
show
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Modal bsSize="small"
|
<Modal
|
||||||
animation={ false }
|
bsSize="small"
|
||||||
show={ show }
|
animation={false}
|
||||||
className={ "modal-confirm " + (baseClass || '') }>
|
show={show}
|
||||||
|
className={"modal-confirm " + (baseClass || "")}
|
||||||
|
>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="mc-icon">
|
<div className="mc-icon">
|
||||||
<i className={ icon }></i>
|
<i className={icon} />
|
||||||
</div>
|
|
||||||
<div className="mc-text">
|
|
||||||
{ text }
|
|
||||||
</div>
|
|
||||||
<div className="mc-sub">
|
|
||||||
{ sub }
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mc-text">{text}</div>
|
||||||
|
<div className="mc-sub">{sub}</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button className="btn btn-danger" onClick={ okHandler }>
|
<button className="btn btn-danger" onClick={okHandler}>
|
||||||
{ okText }
|
{okText}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-link" onClick={ cancelHandler }>
|
<button className="btn btn-link" onClick={cancelHandler}>
|
||||||
{ cancelText }
|
{cancelText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
83
browser/app/js/browser/MainActions.js
Normal file
83
browser/app/js/browser/MainActions.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Dropdown, OverlayTrigger, Tooltip } from "react-bootstrap"
|
||||||
|
import * as actionsBuckets from "../buckets/actions"
|
||||||
|
import * as uploadsActions from "../uploads/actions"
|
||||||
|
|
||||||
|
export const MainActions = ({ uploadFile, showMakeBucketModal }) => {
|
||||||
|
const uploadTooltip = <Tooltip id="tt-upload-file">Upload file</Tooltip>
|
||||||
|
const makeBucketTooltip = (
|
||||||
|
<Tooltip id="tt-create-bucket">Create bucket</Tooltip>
|
||||||
|
)
|
||||||
|
const onFileUpload = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
uploadFile(e.target.files[0])
|
||||||
|
e.target.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown dropup className="feb-actions" id="fe-action-toggle">
|
||||||
|
<Dropdown.Toggle noCaret className="feba-toggle">
|
||||||
|
<span>
|
||||||
|
<i className="fa fa-plus" />
|
||||||
|
</span>
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<OverlayTrigger placement="left" overlay={uploadTooltip}>
|
||||||
|
<a href="#" className="feba-btn feba-upload">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={onFileUpload}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
id="file-input"
|
||||||
|
/>
|
||||||
|
<label htmlFor="file-input">
|
||||||
|
{" "}
|
||||||
|
<i className="fa fa-cloud-upload" />{" "}
|
||||||
|
</label>
|
||||||
|
</a>
|
||||||
|
</OverlayTrigger>
|
||||||
|
<OverlayTrigger placement="left" overlay={makeBucketTooltip}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
id="show-make-bucket"
|
||||||
|
className="feba-btn feba-bucket"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
showMakeBucketModal()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="fa fa-hdd-o" />
|
||||||
|
</a>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => state
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
uploadFile: file => dispatch(uploadsActions.uploadFile(file)),
|
||||||
|
showMakeBucketModal: () => dispatch(actionsBuckets.showMakeBucketModal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(MainActions)
|
@ -18,12 +18,18 @@ import React from "react"
|
|||||||
import MobileHeader from "./MobileHeader"
|
import MobileHeader from "./MobileHeader"
|
||||||
import Header from "./Header"
|
import Header from "./Header"
|
||||||
import ObjectsSection from "../objects/ObjectsSection"
|
import ObjectsSection from "../objects/ObjectsSection"
|
||||||
|
import MainActions from "./MainActions"
|
||||||
|
import MakeBucketModal from "../buckets/MakeBucketModal"
|
||||||
|
import UploadModal from "../uploads/UploadModal"
|
||||||
|
|
||||||
export const MainContent = () => (
|
export const MainContent = () => (
|
||||||
<div className="fe-body">
|
<div className="fe-body">
|
||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
<Header />
|
<Header />
|
||||||
<ObjectsSection />
|
<ObjectsSection />
|
||||||
|
<MainActions />
|
||||||
|
<MakeBucketModal />
|
||||||
|
<UploadModal />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
47
browser/app/js/browser/__tests__/MainActions.test.js
Normal file
47
browser/app/js/browser/__tests__/MainActions.test.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MainActions } from "../MainActions"
|
||||||
|
|
||||||
|
describe("MainActions", () => {
|
||||||
|
it("should render without crashing", () => {
|
||||||
|
shallow(<MainActions />)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call showMakeBucketModal when create bucket icon is clicked", () => {
|
||||||
|
const showMakeBucketModal = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<MainActions showMakeBucketModal={showMakeBucketModal} />
|
||||||
|
)
|
||||||
|
wrapper
|
||||||
|
.find("#show-make-bucket")
|
||||||
|
.simulate("click", { preventDefault: jest.fn() })
|
||||||
|
expect(showMakeBucketModal).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call uploadFile when a file is selected for upload", () => {
|
||||||
|
const uploadFile = jest.fn()
|
||||||
|
const wrapper = shallow(<MainActions uploadFile={uploadFile} />)
|
||||||
|
const file = new Blob(["file content"], { type: "text/plain" })
|
||||||
|
wrapper.find("#file-input").simulate("change", {
|
||||||
|
preventDefault: jest.fn(),
|
||||||
|
target: { files: [file] }
|
||||||
|
})
|
||||||
|
expect(uploadFile).toHaveBeenCalledWith(file)
|
||||||
|
})
|
||||||
|
})
|
90
browser/app/js/buckets/MakeBucketModal.js
Normal file
90
browser/app/js/buckets/MakeBucketModal.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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, ModalBody } from "react-bootstrap"
|
||||||
|
import * as actionsBuckets from "./actions"
|
||||||
|
|
||||||
|
export class MakeBucketModal extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
bucketName: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const { makeBucket } = this.props
|
||||||
|
const bucket = this.state.bucketName
|
||||||
|
if (bucket) {
|
||||||
|
makeBucket(bucket)
|
||||||
|
this.hideModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hideModal() {
|
||||||
|
this.setState({
|
||||||
|
bucketName: ""
|
||||||
|
})
|
||||||
|
this.props.hideMakeBucketModal()
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { showMakeBucketModal } = this.props
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="modal-create-bucket"
|
||||||
|
bsSize="small"
|
||||||
|
animation={false}
|
||||||
|
show={showMakeBucketModal}
|
||||||
|
onHide={this.hideModal.bind(this)}
|
||||||
|
>
|
||||||
|
<button className="close close-alt" onClick={this.hideModal.bind(this)}>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
<ModalBody>
|
||||||
|
<form onSubmit={this.onSubmit.bind(this)}>
|
||||||
|
<div className="input-group">
|
||||||
|
<input
|
||||||
|
className="ig-text"
|
||||||
|
type="text"
|
||||||
|
placeholder="Bucket Name"
|
||||||
|
value={this.state.bucketName}
|
||||||
|
onChange={e => this.setState({ bucketName: e.target.value })}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<i className="ig-helpers" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
showMakeBucketModal: state.buckets.showMakeBucketModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
makeBucket: bucket => dispatch(actionsBuckets.makeBucket(bucket)),
|
||||||
|
hideMakeBucketModal: () => dispatch(actionsBuckets.hideMakeBucketModal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(MakeBucketModal)
|
80
browser/app/js/buckets/__tests__/MakeBucketModal.test.js
Normal file
80
browser/app/js/buckets/__tests__/MakeBucketModal.test.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MakeBucketModal } from "../MakeBucketModal"
|
||||||
|
|
||||||
|
describe("MakeBucketModal", () => {
|
||||||
|
it("should render without crashing", () => {
|
||||||
|
shallow(<MakeBucketModal />)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call hideMakeBucketModal when close button is clicked", () => {
|
||||||
|
const hideMakeBucketModal = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<MakeBucketModal hideMakeBucketModal={hideMakeBucketModal} />
|
||||||
|
)
|
||||||
|
wrapper.find("button").simulate("click")
|
||||||
|
expect(hideMakeBucketModal).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("bucketName should be cleared before hiding the modal", () => {
|
||||||
|
const hideMakeBucketModal = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<MakeBucketModal hideMakeBucketModal={hideMakeBucketModal} />
|
||||||
|
)
|
||||||
|
wrapper.find("input").simulate("change", {
|
||||||
|
target: { value: "test" }
|
||||||
|
})
|
||||||
|
expect(wrapper.state("bucketName")).toBe("test")
|
||||||
|
wrapper.find("button").simulate("click")
|
||||||
|
expect(wrapper.state("bucketName")).toBe("")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call makeBucket when the form is submitted", () => {
|
||||||
|
const makeBucket = jest.fn()
|
||||||
|
const hideMakeBucketModal = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<MakeBucketModal
|
||||||
|
makeBucket={makeBucket}
|
||||||
|
hideMakeBucketModal={hideMakeBucketModal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
wrapper.find("input").simulate("change", {
|
||||||
|
target: { value: "test" }
|
||||||
|
})
|
||||||
|
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() })
|
||||||
|
expect(makeBucket).toHaveBeenCalledWith("test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call hideMakeBucketModal and clear bucketName after the form is submited", () => {
|
||||||
|
const makeBucket = jest.fn()
|
||||||
|
const hideMakeBucketModal = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<MakeBucketModal
|
||||||
|
makeBucket={makeBucket}
|
||||||
|
hideMakeBucketModal={hideMakeBucketModal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
wrapper.find("input").simulate("change", {
|
||||||
|
target: { value: "test" }
|
||||||
|
})
|
||||||
|
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() })
|
||||||
|
expect(hideMakeBucketModal).toHaveBeenCalled()
|
||||||
|
expect(wrapper.state("bucketName")).toBe("")
|
||||||
|
})
|
||||||
|
})
|
@ -17,13 +17,21 @@
|
|||||||
import configureStore from "redux-mock-store"
|
import configureStore from "redux-mock-store"
|
||||||
import thunk from "redux-thunk"
|
import thunk from "redux-thunk"
|
||||||
import * as actionsBuckets from "../actions"
|
import * as actionsBuckets from "../actions"
|
||||||
|
import * as objectActions from "../../objects/actions"
|
||||||
|
|
||||||
jest.mock("../../web", () => ({
|
jest.mock("../../web", () => ({
|
||||||
ListBuckets: jest.fn(() => {
|
ListBuckets: jest.fn(() => {
|
||||||
return Promise.resolve({ buckets: [{ name: "test1" }, { name: "test2" }] })
|
return Promise.resolve({ buckets: [{ name: "test1" }, { name: "test2" }] })
|
||||||
|
}),
|
||||||
|
MakeBucket: jest.fn(() => {
|
||||||
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock("../../objects/actions", () => ({
|
||||||
|
selectPrefix: () => dispatch => {}
|
||||||
|
}))
|
||||||
|
|
||||||
const middlewares = [thunk]
|
const middlewares = [thunk]
|
||||||
const mockStore = configureStore(middlewares)
|
const mockStore = configureStore(middlewares)
|
||||||
|
|
||||||
@ -40,24 +48,6 @@ describe("Buckets actions", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates buckets/SET_LIST directly", () => {
|
|
||||||
const store = mockStore()
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] }
|
|
||||||
]
|
|
||||||
store.dispatch(actionsBuckets.setList(["test1", "test2"]))
|
|
||||||
const actions = store.getActions()
|
|
||||||
expect(actions).toEqual(expectedActions)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("creates buckets/SET_FILTER directly", () => {
|
|
||||||
const store = mockStore()
|
|
||||||
const expectedActions = [{ type: "buckets/SET_FILTER", filter: "test" }]
|
|
||||||
store.dispatch(actionsBuckets.setFilter("test"))
|
|
||||||
const actions = store.getActions()
|
|
||||||
expect(actions).toEqual(expectedActions)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should update browser url and creates buckets/SET_CURRENT_BUCKET action when selectBucket is called", () => {
|
it("should update browser url and creates buckets/SET_CURRENT_BUCKET action when selectBucket is called", () => {
|
||||||
const store = mockStore()
|
const store = mockStore()
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
@ -68,4 +58,44 @@ describe("Buckets actions", () => {
|
|||||||
expect(actions).toEqual(expectedActions)
|
expect(actions).toEqual(expectedActions)
|
||||||
expect(window.location.pathname).toBe("/test1")
|
expect(window.location.pathname).toBe("/test1")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("creates buckets/SHOW_MAKE_BUCKET_MODAL for showMakeBucketModal", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: "buckets/SHOW_MAKE_BUCKET_MODAL", show: true }
|
||||||
|
]
|
||||||
|
store.dispatch(actionsBuckets.showMakeBucketModal())
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates buckets/SHOW_MAKE_BUCKET_MODAL for hideMakeBucketModal", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: "buckets/SHOW_MAKE_BUCKET_MODAL", show: false }
|
||||||
|
]
|
||||||
|
store.dispatch(actionsBuckets.hideMakeBucketModal())
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates buckets/ADD action", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [{ type: "buckets/ADD", bucket: "test" }]
|
||||||
|
store.dispatch(actionsBuckets.addBucket("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 = [
|
||||||
|
{ type: "buckets/ADD", bucket: "test1" },
|
||||||
|
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" }
|
||||||
|
]
|
||||||
|
return store.dispatch(actionsBuckets.makeBucket("test1")).then(() => {
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -23,7 +23,8 @@ describe("buckets reducer", () => {
|
|||||||
expect(initialState).toEqual({
|
expect(initialState).toEqual({
|
||||||
list: [],
|
list: [],
|
||||||
filter: "",
|
filter: "",
|
||||||
currentBucket: ""
|
currentBucket: "",
|
||||||
|
showMakeBucketModal: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -35,6 +36,17 @@ describe("buckets reducer", () => {
|
|||||||
expect(newState.list).toEqual(["bk1", "bk2"])
|
expect(newState.list).toEqual(["bk1", "bk2"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should handle ADD", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{ list: ["test1", "test2"] },
|
||||||
|
{
|
||||||
|
type: actions.ADD,
|
||||||
|
bucket: "test3"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(newState.list).toEqual(["test3", "test1", "test2"])
|
||||||
|
})
|
||||||
|
|
||||||
it("should handle SET_FILTER", () => {
|
it("should handle SET_FILTER", () => {
|
||||||
const newState = reducer(undefined, {
|
const newState = reducer(undefined, {
|
||||||
type: actions.SET_FILTER,
|
type: actions.SET_FILTER,
|
||||||
@ -50,4 +62,12 @@ describe("buckets reducer", () => {
|
|||||||
})
|
})
|
||||||
expect(newState.currentBucket).toEqual("test")
|
expect(newState.currentBucket).toEqual("test")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should handle SHOW_MAKE_BUCKET_MODAL", () => {
|
||||||
|
const newState = reducer(undefined, {
|
||||||
|
type: actions.SHOW_MAKE_BUCKET_MODAL,
|
||||||
|
show: true
|
||||||
|
})
|
||||||
|
expect(newState.showMakeBucketModal).toBeTruthy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -16,10 +16,14 @@
|
|||||||
|
|
||||||
import web from "../web"
|
import web from "../web"
|
||||||
import history from "../history"
|
import history from "../history"
|
||||||
|
import * as alertActions from "../alert/actions"
|
||||||
|
import * as objectsActions from "../objects/actions"
|
||||||
|
|
||||||
export const SET_LIST = "buckets/SET_LIST"
|
export const SET_LIST = "buckets/SET_LIST"
|
||||||
|
export const ADD = "buckets/ADD"
|
||||||
export const SET_FILTER = "buckets/SET_FILTER"
|
export const SET_FILTER = "buckets/SET_FILTER"
|
||||||
export const SET_CURRENT_BUCKET = "buckets/SET_CURRENT_BUCKET"
|
export const SET_CURRENT_BUCKET = "buckets/SET_CURRENT_BUCKET"
|
||||||
|
export const SHOW_MAKE_BUCKET_MODAL = "buckets/SHOW_MAKE_BUCKET_MODAL"
|
||||||
|
|
||||||
export const fetchBuckets = () => {
|
export const fetchBuckets = () => {
|
||||||
return function(dispatch) {
|
return function(dispatch) {
|
||||||
@ -50,6 +54,7 @@ export const setFilter = filter => {
|
|||||||
export const selectBucket = bucket => {
|
export const selectBucket = bucket => {
|
||||||
return function(dispatch) {
|
return function(dispatch) {
|
||||||
dispatch(setCurrentBucket(bucket))
|
dispatch(setCurrentBucket(bucket))
|
||||||
|
dispatch(objectsActions.selectPrefix(""))
|
||||||
history.push(`/${bucket}`)
|
history.push(`/${bucket}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,3 +65,39 @@ export const setCurrentBucket = bucket => {
|
|||||||
bucket
|
bucket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const makeBucket = bucket => {
|
||||||
|
return function(dispatch) {
|
||||||
|
return web
|
||||||
|
.MakeBucket({
|
||||||
|
bucketName: bucket
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(addBucket(bucket))
|
||||||
|
dispatch(selectBucket(bucket))
|
||||||
|
})
|
||||||
|
.catch(err =>
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addBucket = bucket => ({
|
||||||
|
type: ADD,
|
||||||
|
bucket
|
||||||
|
})
|
||||||
|
|
||||||
|
export const showMakeBucketModal = () => ({
|
||||||
|
type: SHOW_MAKE_BUCKET_MODAL,
|
||||||
|
show: true
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hideMakeBucketModal = () => ({
|
||||||
|
type: SHOW_MAKE_BUCKET_MODAL,
|
||||||
|
show: false
|
||||||
|
})
|
||||||
|
@ -17,7 +17,12 @@
|
|||||||
import * as actionsBuckets from "./actions"
|
import * as actionsBuckets from "./actions"
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
state = { list: [], filter: "", currentBucket: "" },
|
state = {
|
||||||
|
list: [],
|
||||||
|
filter: "",
|
||||||
|
currentBucket: "",
|
||||||
|
showMakeBucketModal: false
|
||||||
|
},
|
||||||
action
|
action
|
||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -26,6 +31,11 @@ export default (
|
|||||||
...state,
|
...state,
|
||||||
list: action.buckets
|
list: action.buckets
|
||||||
}
|
}
|
||||||
|
case actionsBuckets.ADD:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
list: [action.bucket, ...state.list]
|
||||||
|
}
|
||||||
case actionsBuckets.SET_FILTER:
|
case actionsBuckets.SET_FILTER:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -36,6 +46,11 @@ export default (
|
|||||||
...state,
|
...state,
|
||||||
currentBucket: action.bucket
|
currentBucket: action.bucket
|
||||||
}
|
}
|
||||||
|
case actionsBuckets.SHOW_MAKE_BUCKET_MODAL:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showMakeBucketModal: action.show
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -1,141 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage (C) 2016 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 humanize from 'humanize'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import connect from 'react-redux/lib/components/connect'
|
|
||||||
|
|
||||||
import ProgressBar from 'react-bootstrap/lib/ProgressBar'
|
|
||||||
import ConfirmModal from './ConfirmModal'
|
|
||||||
|
|
||||||
import * as actions from '../actions'
|
|
||||||
|
|
||||||
// UploadModal is a modal that handles multiple file uploads.
|
|
||||||
// During the upload, it displays a progress bar, and can transform into an
|
|
||||||
// abort modal if the user decides to abort the uploads.
|
|
||||||
class UploadModal extends React.Component {
|
|
||||||
|
|
||||||
// Abort all the current uploads.
|
|
||||||
abortUploads(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
const {dispatch, uploads} = this.props
|
|
||||||
|
|
||||||
for (var slug in uploads) {
|
|
||||||
let upload = uploads[slug]
|
|
||||||
upload.xhr.abort()
|
|
||||||
dispatch(actions.stopUpload({
|
|
||||||
slug
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hideAbort(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the abort modal instead of the progress modal.
|
|
||||||
showAbort(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
const {dispatch} = this.props
|
|
||||||
|
|
||||||
dispatch(actions.setShowAbortModal(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the progress modal instead of the abort modal.
|
|
||||||
hideAbort(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
const {dispatch} = this.props
|
|
||||||
|
|
||||||
dispatch(actions.setShowAbortModal(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {uploads, showAbortModal} = this.props
|
|
||||||
|
|
||||||
// Show the abort modal.
|
|
||||||
if (showAbortModal) {
|
|
||||||
let baseClass = classNames({
|
|
||||||
'abort-upload': true
|
|
||||||
})
|
|
||||||
let okIcon = classNames({
|
|
||||||
'fa': true,
|
|
||||||
'fa-times': true
|
|
||||||
})
|
|
||||||
let cancelIcon = classNames({
|
|
||||||
'fa': true,
|
|
||||||
'fa-cloud-upload': true
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfirmModal show={ true }
|
|
||||||
baseClass={ baseClass }
|
|
||||||
text='Abort uploads in progress?'
|
|
||||||
icon='fa fa-info-circle mci-amber'
|
|
||||||
sub='This cannot be undone!'
|
|
||||||
okText='Abort'
|
|
||||||
okIcon={ okIcon }
|
|
||||||
cancelText='Upload'
|
|
||||||
cancelIcon={ cancelIcon }
|
|
||||||
okHandler={ this.abortUploads.bind(this) }
|
|
||||||
cancelHandler={ this.hideAbort.bind(this) }>
|
|
||||||
</ConfirmModal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't have any files uploading, don't show anything.
|
|
||||||
let numberUploading = Object.keys(uploads).length
|
|
||||||
if (numberUploading == 0)
|
|
||||||
return ( <noscript></noscript> )
|
|
||||||
|
|
||||||
let totalLoaded = 0
|
|
||||||
let totalSize = 0
|
|
||||||
|
|
||||||
// Iterate over each upload, adding together the total size and that
|
|
||||||
// which has been uploaded.
|
|
||||||
for (var slug in uploads) {
|
|
||||||
let upload = uploads[slug]
|
|
||||||
totalLoaded += upload.loaded
|
|
||||||
totalSize += upload.size
|
|
||||||
}
|
|
||||||
|
|
||||||
let percent = (totalLoaded / totalSize) * 100
|
|
||||||
|
|
||||||
// If more than one: "Uploading files (5)..."
|
|
||||||
// If only one: "Uploading myfile.txt..."
|
|
||||||
let text = 'Uploading ' + (numberUploading == 1 ? `'${uploads[Object.keys(uploads)[0]].name}'` : `files (${numberUploading})`) + '...'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="alert alert-info progress animated fadeInUp ">
|
|
||||||
<button type="button" className="close" onClick={ this.showAbort.bind(this) }>
|
|
||||||
<span>×</span>
|
|
||||||
</button>
|
|
||||||
<div className="text-center">
|
|
||||||
<small>{ text }</small>
|
|
||||||
</div>
|
|
||||||
<ProgressBar now={ percent } />
|
|
||||||
<div className="text-center">
|
|
||||||
<small>{ humanize.filesize(totalLoaded) } ({ percent.toFixed(2) } %)</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(state => {
|
|
||||||
return {
|
|
||||||
uploads: state.uploads,
|
|
||||||
showAbortModal: state.showAbortModal
|
|
||||||
}
|
|
||||||
})(UploadModal)
|
|
@ -22,15 +22,6 @@ import * as actionsObjects from "./actions"
|
|||||||
import ObjectsList from "./ObjectsList"
|
import ObjectsList from "./ObjectsList"
|
||||||
|
|
||||||
export class ObjectsListContainer extends React.Component {
|
export class ObjectsListContainer extends React.Component {
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
const { currentBucket, currentPrefix, loadObjects } = this.props
|
|
||||||
if (
|
|
||||||
currentBucket != nextProps.currentBucket ||
|
|
||||||
currentPrefix != nextProps.currentPrefix
|
|
||||||
) {
|
|
||||||
loadObjects()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render() {
|
render() {
|
||||||
const { objects, isTruncated, currentBucket, loadObjects } = this.props
|
const { objects, isTruncated, currentBucket, loadObjects } = this.props
|
||||||
return (
|
return (
|
||||||
|
@ -36,22 +36,4 @@ describe("ObjectsList", () => {
|
|||||||
{ name: "test2.jpg" }
|
{ name: "test2.jpg" }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should call loadObjects when currentBucket is changed", () => {
|
|
||||||
const loadObjects = jest.fn()
|
|
||||||
const wrapper = shallow(
|
|
||||||
<ObjectsListContainer currentBucket="test1" loadObjects={loadObjects} />
|
|
||||||
)
|
|
||||||
wrapper.setProps({ currentBucket: "test2" })
|
|
||||||
expect(loadObjects).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should call loadObjects when currentPrefix is changed", () => {
|
|
||||||
const loadObjects = jest.fn()
|
|
||||||
const wrapper = shallow(
|
|
||||||
<ObjectsListContainer currentPrefix="abc/" loadObjects={loadObjects} />
|
|
||||||
)
|
|
||||||
wrapper.setProps({ currentPrefix: "abc/xyz/" })
|
|
||||||
expect(loadObjects).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -158,7 +158,8 @@ describe("Objects actions", () => {
|
|||||||
|
|
||||||
it("should update browser url and creates objects/SET_CURRENT_PREFIX action when selectPrefix is called", () => {
|
it("should update browser url and creates objects/SET_CURRENT_PREFIX action when selectPrefix is called", () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
buckets: { currentBucket: "test" }
|
buckets: { currentBucket: "test" },
|
||||||
|
objects: { currentPrefix: "" }
|
||||||
})
|
})
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{ type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" }
|
{ type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" }
|
||||||
|
@ -21,9 +21,11 @@ import {
|
|||||||
sortObjectsBySize,
|
sortObjectsBySize,
|
||||||
sortObjectsByDate
|
sortObjectsByDate
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
|
import { getCurrentBucket } from "../buckets/selectors"
|
||||||
|
|
||||||
export const SET_LIST = "objects/SET_LIST"
|
export const SET_LIST = "objects/SET_LIST"
|
||||||
export const APPEND_LIST = "objects/APPEND_LIST"
|
export const APPEND_LIST = "objects/APPEND_LIST"
|
||||||
|
export const RESET = "objects/RESET"
|
||||||
export const SET_SORT_BY = "objects/SET_SORT_BY"
|
export const SET_SORT_BY = "objects/SET_SORT_BY"
|
||||||
export const SET_SORT_ORDER = "objects/SET_SORT_ORDER"
|
export const SET_SORT_ORDER = "objects/SET_SORT_ORDER"
|
||||||
export const SET_CURRENT_PREFIX = "objects/SET_CURRENT_PREFIX"
|
export const SET_CURRENT_PREFIX = "objects/SET_CURRENT_PREFIX"
|
||||||
@ -52,7 +54,7 @@ export const fetchObjects = append => {
|
|||||||
.ListObjects({
|
.ListObjects({
|
||||||
bucketName: currentBucket,
|
bucketName: currentBucket,
|
||||||
prefix: currentPrefix,
|
prefix: currentPrefix,
|
||||||
marker: marker
|
marker: append ? marker : ""
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
let objects = []
|
let objects = []
|
||||||
@ -113,7 +115,8 @@ export const setSortOrder = sortOrder => ({
|
|||||||
export const selectPrefix = prefix => {
|
export const selectPrefix = prefix => {
|
||||||
return function(dispatch, getState) {
|
return function(dispatch, getState) {
|
||||||
dispatch(setCurrentPrefix(prefix))
|
dispatch(setCurrentPrefix(prefix))
|
||||||
const currentBucket = getState().buckets.currentBucket
|
dispatch(fetchObjects())
|
||||||
|
const currentBucket = getCurrentBucket(getState())
|
||||||
history.replace(`/${currentBucket}/${prefix}`)
|
history.replace(`/${currentBucket}/${prefix}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
browser/app/js/objects/selectors.js
Normal file
19
browser/app/js/objects/selectors.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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 { createSelector } from "reselect"
|
||||||
|
|
||||||
|
export const getCurrentPrefix = state => state.objects.currentPrefix
|
@ -19,12 +19,14 @@ import browser from "./browser/reducer"
|
|||||||
import alert from "./alert/reducer"
|
import alert from "./alert/reducer"
|
||||||
import buckets from "./buckets/reducer"
|
import buckets from "./buckets/reducer"
|
||||||
import objects from "./objects/reducer"
|
import objects from "./objects/reducer"
|
||||||
|
import uploads from "./uploads/reducer"
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
browser,
|
browser,
|
||||||
alert,
|
alert,
|
||||||
buckets,
|
buckets,
|
||||||
objects
|
objects,
|
||||||
|
uploads
|
||||||
})
|
})
|
||||||
|
|
||||||
export default rootReducer
|
export default rootReducer
|
||||||
|
75
browser/app/js/uploads/AbortConfirmModal.js
Normal file
75
browser/app/js/uploads/AbortConfirmModal.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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 ConfirmModal from "../browser/ConfirmModal"
|
||||||
|
import * as uploadsActions from "./actions"
|
||||||
|
|
||||||
|
export class AbortConfirmModal extends React.Component {
|
||||||
|
abortUploads() {
|
||||||
|
const { abort, uploads } = this.props
|
||||||
|
for (var slug in uploads) {
|
||||||
|
abort(slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const { hideAbort } = this.props
|
||||||
|
let baseClass = classNames({
|
||||||
|
"abort-upload": true
|
||||||
|
})
|
||||||
|
let okIcon = classNames({
|
||||||
|
fa: true,
|
||||||
|
"fa-times": true
|
||||||
|
})
|
||||||
|
let cancelIcon = classNames({
|
||||||
|
fa: true,
|
||||||
|
"fa-cloud-upload": true
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
show={true}
|
||||||
|
baseClass={baseClass}
|
||||||
|
text="Abort uploads in progress?"
|
||||||
|
icon="fa fa-info-circle mci-amber"
|
||||||
|
sub="This cannot be undone!"
|
||||||
|
okText="Abort"
|
||||||
|
okIcon={okIcon}
|
||||||
|
cancelText="Upload"
|
||||||
|
cancelIcon={cancelIcon}
|
||||||
|
okHandler={this.abortUploads.bind(this)}
|
||||||
|
cancelHandler={hideAbort}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
uploads: state.uploads.files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
abort: slug => dispatch(uploadsActions.abortUpload(slug)),
|
||||||
|
hideAbort: () => dispatch(uploadsActions.hideAbortModal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AbortConfirmModal)
|
91
browser/app/js/uploads/UploadModal.js
Normal file
91
browser/app/js/uploads/UploadModal.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* 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 humanize from "humanize"
|
||||||
|
import classNames from "classnames"
|
||||||
|
import { connect } from "react-redux"
|
||||||
|
|
||||||
|
import { ProgressBar } from "react-bootstrap"
|
||||||
|
import AbortConfirmModal from "./AbortConfirmModal"
|
||||||
|
import * as uploadsActions from "./actions"
|
||||||
|
|
||||||
|
export class UploadModal extends React.Component {
|
||||||
|
render() {
|
||||||
|
const { uploads, showAbort, showAbortModal } = this.props
|
||||||
|
if (showAbort) {
|
||||||
|
return <AbortConfirmModal />
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have any files uploading, don't show anything.
|
||||||
|
let numberUploading = Object.keys(uploads).length
|
||||||
|
if (numberUploading == 0) return <noscript />
|
||||||
|
|
||||||
|
let totalLoaded = 0
|
||||||
|
let totalSize = 0
|
||||||
|
|
||||||
|
// Iterate over each upload, adding together the total size and that
|
||||||
|
// which has been uploaded.
|
||||||
|
for (var slug in uploads) {
|
||||||
|
let upload = uploads[slug]
|
||||||
|
totalLoaded += upload.loaded
|
||||||
|
totalSize += upload.size
|
||||||
|
}
|
||||||
|
|
||||||
|
let percent = totalLoaded / totalSize * 100
|
||||||
|
|
||||||
|
// If more than one: "Uploading files (5)..."
|
||||||
|
// If only one: "Uploading myfile.txt..."
|
||||||
|
let text =
|
||||||
|
"Uploading " +
|
||||||
|
(numberUploading == 1
|
||||||
|
? `'${uploads[Object.keys(uploads)[0]].name}'`
|
||||||
|
: `files (${numberUploading})`) +
|
||||||
|
"..."
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alert alert-info progress animated fadeInUp ">
|
||||||
|
<button type="button" className="close" onClick={showAbortModal}>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
<div className="text-center">
|
||||||
|
<small>{text}</small>
|
||||||
|
</div>
|
||||||
|
<ProgressBar now={percent} />
|
||||||
|
<div className="text-center">
|
||||||
|
<small>
|
||||||
|
{humanize.filesize(totalLoaded)} ({percent.toFixed(2)} %)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
uploads: state.uploads.files,
|
||||||
|
showAbort: state.uploads.showAbortModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
showAbortModal: () => dispatch(uploadsActions.showAbortModal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(UploadModal)
|
49
browser/app/js/uploads/__tests__/AbortConfirmModal.test.js
Normal file
49
browser/app/js/uploads/__tests__/AbortConfirmModal.test.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 } from "enzyme"
|
||||||
|
import { AbortConfirmModal } from "../AbortConfirmModal"
|
||||||
|
|
||||||
|
describe("AbortConfirmModal", () => {
|
||||||
|
it("should render without crashing", () => {
|
||||||
|
shallow(<AbortConfirmModal />)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call abort for every upload when Abort is clicked", () => {
|
||||||
|
const abort = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<AbortConfirmModal
|
||||||
|
uploads={{
|
||||||
|
"a-b/-test1": { size: 100, loaded: 50, name: "test1" },
|
||||||
|
"a-b/-test2": { size: 100, loaded: 50, name: "test2" }
|
||||||
|
}}
|
||||||
|
abort={abort}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
wrapper.instance().abortUploads()
|
||||||
|
expect(abort.mock.calls.length).toBe(2)
|
||||||
|
expect(abort.mock.calls[0][0]).toBe("a-b/-test1")
|
||||||
|
expect(abort.mock.calls[1][0]).toBe("a-b/-test2")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call hideAbort when cancel is clicked", () => {
|
||||||
|
const hideAbort = jest.fn()
|
||||||
|
const wrapper = shallow(<AbortConfirmModal hideAbort={hideAbort} />)
|
||||||
|
wrapper.find("ConfirmModal").prop("cancelHandler")()
|
||||||
|
expect(hideAbort).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
56
browser/app/js/uploads/__tests__/UploadModal.test.js
Normal file
56
browser/app/js/uploads/__tests__/UploadModal.test.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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 } from "enzyme"
|
||||||
|
import { UploadModal } from "../UploadModal"
|
||||||
|
|
||||||
|
describe("UploadModal", () => {
|
||||||
|
it("should render without crashing", () => {
|
||||||
|
shallow(<UploadModal uploads={{}} />)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render AbortConfirmModal when showAbort is true", () => {
|
||||||
|
const wrapper = shallow(<UploadModal uploads={{}} showAbort={true} />)
|
||||||
|
expect(wrapper.find("Connect(AbortConfirmModal)").length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should render nothing when there are no files being uploaded", () => {
|
||||||
|
const wrapper = shallow(<UploadModal uploads={{}} />)
|
||||||
|
expect(wrapper.find("noscript").length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should show upload progress when one or more files are being uploaded", () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<UploadModal
|
||||||
|
uploads={{ "a-b/-test": { size: 100, loaded: 50, name: "test" } }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(wrapper.find("ProgressBar").length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call showAbortModal when close button is clicked", () => {
|
||||||
|
const showAbortModal = jest.fn()
|
||||||
|
const wrapper = shallow(
|
||||||
|
<UploadModal
|
||||||
|
uploads={{ "a-b/-test": { size: 100, loaded: 50, name: "test" } }}
|
||||||
|
showAbortModal={showAbortModal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
wrapper.find("button").simulate("click")
|
||||||
|
expect(showAbortModal).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
166
browser/app/js/uploads/__tests__/actions.test.js
Normal file
166
browser/app/js/uploads/__tests__/actions.test.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* 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 configureStore from "redux-mock-store"
|
||||||
|
import thunk from "redux-thunk"
|
||||||
|
import * as uploadsActions from "../actions"
|
||||||
|
|
||||||
|
const middlewares = [thunk]
|
||||||
|
const mockStore = configureStore(middlewares)
|
||||||
|
|
||||||
|
describe("Uploads actions", () => {
|
||||||
|
it("creates uploads/ADD action", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "uploads/ADD",
|
||||||
|
slug: "a-b-c",
|
||||||
|
size: 100,
|
||||||
|
name: "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
store.dispatch(uploadsActions.add("a-b-c", 100, "test"))
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates uploads/UPDATE_PROGRESS action", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "uploads/UPDATE_PROGRESS",
|
||||||
|
slug: "a-b-c",
|
||||||
|
loaded: 50
|
||||||
|
}
|
||||||
|
]
|
||||||
|
store.dispatch(uploadsActions.updateProgress("a-b-c", 50))
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates uploads/STOP action", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "uploads/STOP",
|
||||||
|
slug: "a-b-c"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
store.dispatch(uploadsActions.stop("a-b-c"))
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates uploads/SHOW_ABORT_MODAL action", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "uploads/SHOW_ABORT_MODAL",
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
store.dispatch(uploadsActions.showAbortModal())
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("uploadFile", () => {
|
||||||
|
const file = new Blob(["file content"], {
|
||||||
|
type: "text/plain"
|
||||||
|
})
|
||||||
|
file.name = "file1"
|
||||||
|
|
||||||
|
it("creates alerts/SET action when currentBucket is not present", () => {
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "" }
|
||||||
|
})
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "alert/SET",
|
||||||
|
alert: {
|
||||||
|
id: 0,
|
||||||
|
type: "danger",
|
||||||
|
message: "Please choose a bucket before trying to upload files."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const file = new Blob(["file content"], { type: "text/plain" })
|
||||||
|
store.dispatch(uploadsActions.uploadFile(file))
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates uploads/ADD action before uploading the file", () => {
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "test1" },
|
||||||
|
objects: { currentPrefix: "pre1/" }
|
||||||
|
})
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "uploads/ADD",
|
||||||
|
slug: "test1-pre1/-file1",
|
||||||
|
size: file.size,
|
||||||
|
name: file.name
|
||||||
|
}
|
||||||
|
]
|
||||||
|
store.dispatch(uploadsActions.uploadFile(file))
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should open and send XMLHttpRequest", () => {
|
||||||
|
const open = jest.fn()
|
||||||
|
const send = jest.fn()
|
||||||
|
const xhrMockClass = () => ({
|
||||||
|
open: open,
|
||||||
|
send: send,
|
||||||
|
setRequestHeader: jest.fn(),
|
||||||
|
upload: {
|
||||||
|
addEventListener: jest.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass)
|
||||||
|
const store = mockStore({
|
||||||
|
buckets: { currentBucket: "test1" },
|
||||||
|
objects: { currentPrefix: "pre1/" }
|
||||||
|
})
|
||||||
|
store.dispatch(uploadsActions.uploadFile(file))
|
||||||
|
expect(open).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"https://localhost:8080/upload/test1/pre1/file1",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(send).toHaveBeenCalledWith(file)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates uploads/STOP and uploads/SHOW_ABORT_MODAL after abortUpload", () => {
|
||||||
|
const store = mockStore()
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
type: "uploads/STOP",
|
||||||
|
slug: "a-b/-c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "uploads/SHOW_ABORT_MODAL",
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
store.dispatch(uploadsActions.abortUpload("a-b/-c"))
|
||||||
|
const actions = store.getActions()
|
||||||
|
expect(actions).toEqual(expectedActions)
|
||||||
|
})
|
||||||
|
})
|
87
browser/app/js/uploads/__tests__/reducer.test.js
Normal file
87
browser/app/js/uploads/__tests__/reducer.test.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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 reducer from "../reducer"
|
||||||
|
import * as actions from "../actions"
|
||||||
|
|
||||||
|
describe("uploads reducer", () => {
|
||||||
|
it("should return the initial state", () => {
|
||||||
|
const initialState = reducer(undefined, {})
|
||||||
|
expect(initialState).toEqual({
|
||||||
|
files: {},
|
||||||
|
showAbortModal: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle ADD", () => {
|
||||||
|
const newState = reducer(undefined, {
|
||||||
|
type: actions.ADD,
|
||||||
|
slug: "a-b-c",
|
||||||
|
size: 100,
|
||||||
|
name: "test"
|
||||||
|
})
|
||||||
|
expect(newState.files).toEqual({
|
||||||
|
"a-b-c": { loaded: 0, size: 100, name: "test" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle UPDATE_PROGRESS", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{
|
||||||
|
files: { "a-b-c": { loaded: 0, size: 100, name: "test" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: actions.UPDATE_PROGRESS,
|
||||||
|
slug: "a-b-c",
|
||||||
|
loaded: 50
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(newState.files).toEqual({
|
||||||
|
"a-b-c": { loaded: 50, size: 100, name: "test" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle STOP", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{
|
||||||
|
files: {
|
||||||
|
"a-b-c": { loaded: 70, size: 100, name: "test1" },
|
||||||
|
"x-y-z": { loaded: 50, size: 100, name: "test2" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: actions.STOP,
|
||||||
|
slug: "a-b-c"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(newState.files).toEqual({
|
||||||
|
"x-y-z": { loaded: 50, size: 100, name: "test2" }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle SHOW_ABORT_MODAL", () => {
|
||||||
|
const newState = reducer(
|
||||||
|
{
|
||||||
|
showAbortModal: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: actions.SHOW_ABORT_MODAL,
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(newState.showAbortModal).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
172
browser/app/js/uploads/actions.js
Normal file
172
browser/app/js/uploads/actions.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* 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 Moment from "moment"
|
||||||
|
import storage from "local-storage-fallback"
|
||||||
|
import * as alertActions from "../alert/actions"
|
||||||
|
import * as objectsActions from "../objects/actions"
|
||||||
|
import { getCurrentBucket } from "../buckets/selectors"
|
||||||
|
import { getCurrentPrefix } from "../objects/selectors"
|
||||||
|
import { minioBrowserPrefix } from "../constants"
|
||||||
|
|
||||||
|
export const ADD = "uploads/ADD"
|
||||||
|
export const UPDATE_PROGRESS = "uploads/UPDATE_PROGRESS"
|
||||||
|
export const STOP = "uploads/STOP"
|
||||||
|
export const SHOW_ABORT_MODAL = "uploads/SHOW_ABORT_MODAL"
|
||||||
|
|
||||||
|
export const add = (slug, size, name) => ({
|
||||||
|
type: ADD,
|
||||||
|
slug,
|
||||||
|
size,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateProgress = (slug, loaded) => ({
|
||||||
|
type: UPDATE_PROGRESS,
|
||||||
|
slug,
|
||||||
|
loaded
|
||||||
|
})
|
||||||
|
|
||||||
|
export const stop = slug => ({
|
||||||
|
type: STOP,
|
||||||
|
slug
|
||||||
|
})
|
||||||
|
|
||||||
|
export const showAbortModal = () => ({
|
||||||
|
type: SHOW_ABORT_MODAL,
|
||||||
|
show: true
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hideAbortModal = () => ({
|
||||||
|
type: SHOW_ABORT_MODAL,
|
||||||
|
show: false
|
||||||
|
})
|
||||||
|
|
||||||
|
let requests = {}
|
||||||
|
|
||||||
|
export const addUpload = (xhr, slug, size, name) => {
|
||||||
|
return function(dispatch) {
|
||||||
|
requests[slug] = xhr
|
||||||
|
dispatch(add(slug, size, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const abortUpload = slug => {
|
||||||
|
return function(dispatch) {
|
||||||
|
const xhr = requests[slug]
|
||||||
|
if (xhr) {
|
||||||
|
xhr.abort()
|
||||||
|
}
|
||||||
|
dispatch(stop(slug))
|
||||||
|
dispatch(hideAbortModal())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadFile = file => {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const state = getState()
|
||||||
|
const currentBucket = getCurrentBucket(state)
|
||||||
|
if (!currentBucket) {
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: "Please choose a bucket before trying to upload files."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentPrefix = getCurrentPrefix(state)
|
||||||
|
const objectName = `${currentPrefix}${file.name}`
|
||||||
|
const uploadUrl = `${
|
||||||
|
window.location.origin
|
||||||
|
}${minioBrowserPrefix}/upload/${currentBucket}/${objectName}`
|
||||||
|
const slug = `${currentBucket}-${currentPrefix}-${file.name}`
|
||||||
|
|
||||||
|
let xhr = new XMLHttpRequest()
|
||||||
|
xhr.open("PUT", uploadUrl, true)
|
||||||
|
xhr.withCredentials = false
|
||||||
|
const token = storage.getItem("token")
|
||||||
|
if (token) {
|
||||||
|
xhr.setRequestHeader(
|
||||||
|
"Authorization",
|
||||||
|
"Bearer " + storage.getItem("token")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
xhr.setRequestHeader(
|
||||||
|
"x-amz-date",
|
||||||
|
Moment()
|
||||||
|
.utc()
|
||||||
|
.format("YYYYMMDDTHHmmss") + "Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatch(addUpload(xhr, slug, file.size, file.name))
|
||||||
|
|
||||||
|
xhr.onload = function(event) {
|
||||||
|
if (xhr.status == 401 || xhr.status == 403) {
|
||||||
|
dispatch(hideAbortModal())
|
||||||
|
dispatch(stop(slug))
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: "Unauthorized request."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (xhr.status == 500) {
|
||||||
|
dispatch(hideAbortModal())
|
||||||
|
dispatch(stop(slug))
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: xhr.responseText
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (xhr.status == 200) {
|
||||||
|
dispatch(hideAbortModal())
|
||||||
|
dispatch(stop(slug))
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "success",
|
||||||
|
message: "File '" + file.name + "' uploaded successfully."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
dispatch(objectsActions.selectPrefix(currentPrefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.upload.addEventListener("error", event => {
|
||||||
|
dispatch(stop(slug))
|
||||||
|
dispatch(
|
||||||
|
alertActions.set({
|
||||||
|
type: "danger",
|
||||||
|
message: "Error occurred uploading '" + file.name + "'."
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
xhr.upload.addEventListener("progress", event => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
let loaded = event.loaded
|
||||||
|
let total = event.total
|
||||||
|
// Update the counter
|
||||||
|
dispatch(updateProgress(slug, loaded))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
xhr.send(file)
|
||||||
|
}
|
||||||
|
}
|
67
browser/app/js/uploads/reducer.js
Normal file
67
browser/app/js/uploads/reducer.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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 * as uploadsActions from "./actions"
|
||||||
|
|
||||||
|
const add = (files, action) => ({
|
||||||
|
...files,
|
||||||
|
[action.slug]: {
|
||||||
|
loaded: 0,
|
||||||
|
size: action.size,
|
||||||
|
name: action.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateProgress = (files, action) => ({
|
||||||
|
...files,
|
||||||
|
[action.slug]: {
|
||||||
|
...files[action.slug],
|
||||||
|
loaded: action.loaded
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const stop = (files, action) => {
|
||||||
|
const newFiles = Object.assign({}, files)
|
||||||
|
delete newFiles[action.slug]
|
||||||
|
return newFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (state = { files: {}, showAbortModal: false }, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case uploadsActions.ADD:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
files: add(state.files, action)
|
||||||
|
}
|
||||||
|
case uploadsActions.UPDATE_PROGRESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
files: updateProgress(state.files, action)
|
||||||
|
}
|
||||||
|
case uploadsActions.STOP:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
files: stop(state.files, action)
|
||||||
|
}
|
||||||
|
case uploadsActions.SHOW_ABORT_MODAL:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showAbortModal: action.show
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user