Refactor make bucket and upload components (#5521)

This commit is contained in:
Kanagaraj M
2018-02-15 08:04:59 +05:30
committed by Harshavardhana
parent 9bfa07ecf5
commit 44f8f7059c
29 changed files with 1329 additions and 213 deletions

View 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)

View 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)

View 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()
})
})

View 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()
})
})

View 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)
})
})

View 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()
})
})

View 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)
}
}

View 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
}
}