Refactor delete object and share object components (#5537)

This commit is contained in:
Kanagaraj M 2018-02-19 09:37:59 +05:30 committed by Harshavardhana
parent 566ac78b8b
commit da4558a8f7
13 changed files with 988 additions and 6 deletions

View file

@ -17,7 +17,7 @@
export const SET = "alert/SET"
export const CLEAR = "alert/CLEAR"
let alertId = 0
export let alertId = 0
export const set = alert => {
const id = alertId++

View file

@ -23,3 +23,7 @@ export const minioBrowserPrefix = p.slice(0, p.indexOf("/", 1))
export const READ_ONLY = "readonly"
export const WRITE_ONLY = "writeonly"
export const READ_WRITE = "readwrite"
export const SHARE_OBJECT_EXPIRY_DAYS = 5
export const SHARE_OBJECT_EXPIRY_HOURS = 0
export const SHARE_OBJECT_EXPIRY_MINUTES = 0

View file

@ -0,0 +1,36 @@
/*
* 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 ConfirmModal from "../browser/ConfirmModal"
export const DeleteObjectConfirmModal = ({
deleteObject,
hideDeleteConfirmModal
}) => (
<ConfirmModal
show={true}
icon="fa fa-exclamation-triangle mci-red"
text="Are you sure you want to delete?"
sub="This cannot be undone!"
okText="Delete"
cancelText="Cancel"
okHandler={deleteObject}
cancelHandler={hideDeleteConfirmModal}
/>
)
export default DeleteObjectConfirmModal

View file

@ -0,0 +1,107 @@
/*
* 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 } from "react-bootstrap"
import ShareObjectModal from "./ShareObjectModal"
import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal"
import * as objectsActions from "./actions"
import {
SHARE_OBJECT_EXPIRY_DAYS,
SHARE_OBJECT_EXPIRY_HOURS,
SHARE_OBJECT_EXPIRY_MINUTES
} from "../constants"
export class ObjectActions extends React.Component {
constructor(props) {
super(props)
this.state = {
showDeleteConfirmation: false
}
}
shareObject(e) {
e.preventDefault()
const { object, shareObject } = this.props
shareObject(
object.name,
SHARE_OBJECT_EXPIRY_DAYS,
SHARE_OBJECT_EXPIRY_HOURS,
SHARE_OBJECT_EXPIRY_MINUTES
)
}
deleteObject() {
const { object, deleteObject } = this.props
deleteObject(object.name)
}
showDeleteConfirmModal(e) {
e.preventDefault()
this.setState({ showDeleteConfirmation: true })
}
hideDeleteConfirmModal() {
this.setState({
showDeleteConfirmation: false
})
}
render() {
const { object, showShareObjectModal } = this.props
return (
<Dropdown id={`obj-actions-${object.name}`}>
<Dropdown.Toggle noCaret className="fia-toggle" />
<Dropdown.Menu>
<a
href=""
className="fiad-action"
onClick={this.shareObject.bind(this)}
>
<i className="fa fa-copy" />
</a>
<a
href=""
className="fiad-action"
onClick={this.showDeleteConfirmModal.bind(this)}
>
<i className="fa fa-trash" />
</a>
</Dropdown.Menu>
{showShareObjectModal && <ShareObjectModal object={object} />}
{this.state.showDeleteConfirmation && (
<DeleteObjectConfirmModal
deleteObject={this.deleteObject.bind(this)}
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
/>
)}
</Dropdown>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
object: ownProps.object,
showShareObjectModal: state.objects.shareObject.show
}
}
const mapDispatchToProps = dispatch => {
return {
shareObject: (object, days, hours, minutes) =>
dispatch(objectsActions.shareObject(object, days, hours, minutes)),
deleteObject: object => dispatch(objectsActions.deleteObject(object))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ObjectActions)

View file

@ -18,16 +18,17 @@ import React from "react"
import humanize from "humanize"
import Moment from "moment"
import ObjectItem from "./ObjectItem"
import ObjectActions from "./ObjectActions"
import * as actionsObjects from "./actions"
export const ObjectContainer = ({ object }) => {
const actionButtons = []
const actionButtons = <ObjectActions object={object} />
const props = {
name: object.name,
contentType: object.contentType,
size: humanize.filesize(object.size),
lastModified: Moment(object.lastModified).format("lll"),
actionButtons: []
actionButtons: actionButtons
}
return <ObjectItem {...props} />
}

View file

@ -0,0 +1,211 @@
/*
* 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, ModalBody } from "react-bootstrap"
import CopyToClipboard from "react-copy-to-clipboard"
import web from "../web"
import * as objectsActions from "./actions"
import * as alertActions from "../alert/actions"
import {
SHARE_OBJECT_EXPIRY_DAYS,
SHARE_OBJECT_EXPIRY_HOURS,
SHARE_OBJECT_EXPIRY_MINUTES
} from "../constants"
export class ShareObjectModal extends React.Component {
constructor(props) {
super(props)
this.state = {
expiry: {
days: SHARE_OBJECT_EXPIRY_DAYS,
hours: SHARE_OBJECT_EXPIRY_HOURS,
minutes: SHARE_OBJECT_EXPIRY_MINUTES
}
}
this.expiryRange = {
days: { min: 0, max: 7 },
hours: { min: 0, max: 23 },
minutes: { min: 0, max: 59 }
}
}
updateExpireValue(param, inc) {
let expiry = Object.assign({}, this.state.expiry)
// Not allowing any increments if days is already max
if (expiry.days == this.expiryRange["days"].max && inc > 0) {
return
}
const { min, max } = this.expiryRange[param]
expiry[param] = expiry[param] + inc
if (expiry[param] < min || expiry[param] > max) {
return
}
if (expiry.days == this.expiryRange["days"].max) {
expiry.hours = 0
expiry.minutes = 0
} else if (expiry.days + expiry.hours + expiry.minutes == 0) {
expiry.days = this.expiryRange["days"].max
}
this.setState({
expiry
})
const { object, shareObject } = this.props
shareObject(object.name, expiry.days, expiry.hours, expiry.minutes)
}
onUrlCopied() {
const { showCopyAlert, hideShareObject } = this.props
showCopyAlert("Link copied to clipboard!")
hideShareObject()
}
render() {
const { shareObjectDetails, shareObject, hideShareObject } = this.props
return (
<Modal
show={true}
animation={false}
onHide={hideShareObject}
bsSize="small"
>
<ModalHeader>Share Object</ModalHeader>
<ModalBody>
<div className="input-group copy-text">
<label>Shareable Link</label>
<input
type="text"
ref={node => (this.copyTextInput = node)}
readOnly="readOnly"
value={window.location.protocol + "//" + shareObjectDetails.url}
onClick={() => this.copyTextInput.select()}
/>
</div>
<div
className="input-group"
style={{ display: web.LoggedIn() ? "block" : "none" }}
>
<label>Expires in (Max 7 days)</label>
<div className="set-expire">
<div className="set-expire-item">
<i
id="increase-days"
className="set-expire-increase"
onClick={() => this.updateExpireValue("days", 1)}
/>
<div className="set-expire-title">Days</div>
<div className="set-expire-value">
<input
ref="expireDays"
type="number"
min={0}
max={7}
value={this.state.expiry.days}
readOnly="readOnly"
/>
</div>
<i
id="decrease-days"
className="set-expire-decrease"
onClick={() => this.updateExpireValue("days", -1)}
/>
</div>
<div className="set-expire-item">
<i
id="increase-hours"
className="set-expire-increase"
onClick={() => this.updateExpireValue("hours", 1)}
/>
<div className="set-expire-title">Hours</div>
<div className="set-expire-value">
<input
ref="expireHours"
type="number"
min={0}
max={23}
value={this.state.expiry.hours}
readOnly="readOnly"
/>
</div>
<i
className="set-expire-decrease"
id="decrease-hours"
onClick={() => this.updateExpireValue("hours", -1)}
/>
</div>
<div className="set-expire-item">
<i
id="increase-minutes"
className="set-expire-increase"
onClick={() => this.updateExpireValue("minutes", 1)}
/>
<div className="set-expire-title">Minutes</div>
<div className="set-expire-value">
<input
ref="expireMins"
type="number"
min={0}
max={59}
value={this.state.expiry.minutes}
readOnly="readOnly"
/>
</div>
<i
id="decrease-minutes"
className="set-expire-decrease"
onClick={() => this.updateExpireValue("minutes", -1)}
/>
</div>
</div>
</div>
</ModalBody>
<div className="modal-footer">
<CopyToClipboard
text={window.location.protocol + "//" + shareObjectDetails.url}
onCopy={this.onUrlCopied.bind(this)}
>
<button className="btn btn-success">Copy Link</button>
</CopyToClipboard>
<button className="btn btn-link" onClick={hideShareObject}>
Cancel
</button>
</div>
</Modal>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
object: ownProps.object,
shareObjectDetails: state.objects.shareObject
}
}
const mapDispatchToProps = dispatch => {
return {
shareObject: (object, days, hours, minutes) =>
dispatch(objectsActions.shareObject(object, days, hours, minutes)),
hideShareObject: () => dispatch(objectsActions.hideShareObject()),
showCopyAlert: message =>
dispatch(alertActions.set({ type: "success", message: message }))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ShareObjectModal)

View file

@ -0,0 +1,45 @@
/*
* 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 { DeleteObjectConfirmModal } from "../DeleteObjectConfirmModal"
describe("DeleteObjectConfirmModal", () => {
it("should render without crashing", () => {
shallow(<DeleteObjectConfirmModal />)
})
it("should call deleteObject when Delete is clicked", () => {
const deleteObject = jest.fn()
const wrapper = shallow(
<DeleteObjectConfirmModal deleteObject={deleteObject} />
)
wrapper.find("ConfirmModal").prop("okHandler")()
expect(deleteObject).toHaveBeenCalled()
})
it("should call hideDeleteConfirmModal when Cancel is clicked", () => {
const hideDeleteConfirmModal = jest.fn()
const wrapper = shallow(
<DeleteObjectConfirmModal
hideDeleteConfirmModal={hideDeleteConfirmModal}
/>
)
wrapper.find("ConfirmModal").prop("cancelHandler")()
expect(hideDeleteConfirmModal).toHaveBeenCalled()
})
})

View file

@ -0,0 +1,95 @@
/*
* 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 { ObjectActions } from "../ObjectActions"
describe("ObjectActions", () => {
it("should render without crashing", () => {
shallow(<ObjectActions object={{ name: "obj1" }} currentPrefix={"pre1/"} />)
})
it("should show DeleteObjectConfirmModal when delete action is clicked", () => {
const wrapper = shallow(
<ObjectActions object={{ name: "obj1" }} currentPrefix={"pre1/"} />
)
wrapper
.find("a")
.last()
.simulate("click", { preventDefault: jest.fn() })
expect(wrapper.state("showDeleteConfirmation")).toBeTruthy()
expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(1)
})
it("should hide DeleteObjectConfirmModal when Cancel button is clicked", () => {
const wrapper = shallow(
<ObjectActions object={{ name: "obj1" }} currentPrefix={"pre1/"} />
)
wrapper
.find("a")
.last()
.simulate("click", { preventDefault: jest.fn() })
wrapper.find("DeleteObjectConfirmModal").prop("hideDeleteConfirmModal")()
wrapper.update()
expect(wrapper.state("showDeleteConfirmation")).toBeFalsy()
expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(0)
})
it("should call deleteObject with object name", () => {
const deleteObject = jest.fn()
const wrapper = shallow(
<ObjectActions
object={{ name: "obj1" }}
currentPrefix={"pre1/"}
deleteObject={deleteObject}
/>
)
wrapper
.find("a")
.last()
.simulate("click", { preventDefault: jest.fn() })
wrapper.find("DeleteObjectConfirmModal").prop("deleteObject")()
expect(deleteObject).toHaveBeenCalledWith("obj1")
})
it("should call shareObject with object and expiry", () => {
const shareObject = jest.fn()
const wrapper = shallow(
<ObjectActions
object={{ name: "obj1" }}
currentPrefix={"pre1/"}
shareObject={shareObject}
/>
)
wrapper
.find("a")
.first()
.simulate("click", { preventDefault: jest.fn() })
expect(shareObject).toHaveBeenCalledWith("obj1", 5, 0, 0)
})
it("should render ShareObjectModal when an object is shared", () => {
const wrapper = shallow(
<ObjectActions
object={{ name: "obj1" }}
currentPrefix={"pre1/"}
showShareObjectModal={true}
/>
)
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1)
})
})

View file

@ -0,0 +1,204 @@
/*
* 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 { ShareObjectModal } from "../ShareObjectModal"
import {
SHARE_OBJECT_EXPIRY_DAYS,
SHARE_OBJECT_EXPIRY_HOURS,
SHARE_OBJECT_EXPIRY_MINUTES
} from "../../constants"
jest.mock("../../web", () => ({
LoggedIn: jest.fn(() => {
return true
})
}))
describe("ShareObjectModal", () => {
it("should render without crashing", () => {
shallow(
<ShareObjectModal
object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
/>
)
})
it("shoud call hideShareObject when Cancel is clicked", () => {
const hideShareObject = jest.fn()
const wrapper = shallow(
<ShareObjectModal
object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
hideShareObject={hideShareObject}
/>
)
wrapper
.find("button")
.last()
.simulate("click")
expect(hideShareObject).toHaveBeenCalled()
})
it("should show the shareable link", () => {
const wrapper = shallow(
<ShareObjectModal
object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
/>
)
expect(
wrapper
.find("input")
.first()
.prop("value")
).toBe(`${window.location.protocol}//test`)
})
it("should call showCopyAlert and hideShareObject when Copy button is clicked", () => {
const hideShareObject = jest.fn()
const showCopyAlert = jest.fn()
const wrapper = shallow(
<ShareObjectModal
object={{ name: "obj1" }}
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
hideShareObject={hideShareObject}
showCopyAlert={showCopyAlert}
/>
)
wrapper.find("CopyToClipboard").prop("onCopy")()
expect(showCopyAlert).toHaveBeenCalledWith("Link copied to clipboard!")
expect(hideShareObject).toHaveBeenCalled()
})
describe("Update expiry values", () => {
const props = {
object: { name: "obj1" },
shareObjectDetails: { show: true, object: "obj1", url: "test" }
}
it("should have default expiry values", () => {
const wrapper = shallow(<ShareObjectModal {...props} />)
expect(wrapper.state("expiry")).toEqual({
days: SHARE_OBJECT_EXPIRY_DAYS,
hours: SHARE_OBJECT_EXPIRY_HOURS,
minutes: SHARE_OBJECT_EXPIRY_MINUTES
})
})
it("should not allow any increments when days is already max", () => {
const shareObject = jest.fn()
const wrapper = shallow(
<ShareObjectModal {...props} shareObject={shareObject} />
)
wrapper.setState({
expiry: {
days: 7,
hours: 0,
minutes: 0
}
})
wrapper.find("#increase-hours").simulate("click")
expect(wrapper.state("expiry")).toEqual({
days: 7,
hours: 0,
minutes: 0
})
expect(shareObject).not.toHaveBeenCalled()
})
it("should not allow expiry values less than minimum value", () => {
const shareObject = jest.fn()
const wrapper = shallow(
<ShareObjectModal {...props} shareObject={shareObject} />
)
wrapper.setState({
expiry: {
days: 5,
hours: 0,
minutes: 0
}
})
wrapper.find("#decrease-hours").simulate("click")
expect(wrapper.state("expiry").hours).toBe(0)
wrapper.find("#decrease-minutes").simulate("click")
expect(wrapper.state("expiry").minutes).toBe(0)
expect(shareObject).not.toHaveBeenCalled()
})
it("should not allow expiry values more than maximum value", () => {
const shareObject = jest.fn()
const wrapper = shallow(
<ShareObjectModal {...props} shareObject={shareObject} />
)
wrapper.setState({
expiry: {
days: 1,
hours: 23,
minutes: 59
}
})
wrapper.find("#increase-hours").simulate("click")
expect(wrapper.state("expiry").hours).toBe(23)
wrapper.find("#increase-minutes").simulate("click")
expect(wrapper.state("expiry").minutes).toBe(59)
expect(shareObject).not.toHaveBeenCalled()
})
it("should set hours and minutes to 0 when days reaches max", () => {
const shareObject = jest.fn()
const wrapper = shallow(
<ShareObjectModal {...props} shareObject={shareObject} />
)
wrapper.setState({
expiry: {
days: 6,
hours: 5,
minutes: 30
}
})
wrapper.find("#increase-days").simulate("click")
expect(wrapper.state("expiry")).toEqual({
days: 7,
hours: 0,
minutes: 0
})
expect(shareObject).toHaveBeenCalled()
})
it("should set days to MAX when all of them becomes 0", () => {
const shareObject = jest.fn()
const wrapper = shallow(
<ShareObjectModal {...props} shareObject={shareObject} />
)
wrapper.setState({
expiry: {
days: 0,
hours: 1,
minutes: 0
}
})
wrapper.find("#decrease-hours").simulate("click")
expect(wrapper.state("expiry")).toEqual({
days: 7,
hours: 0,
minutes: 0
})
expect(shareObject).toHaveBeenCalledWith("obj1", 7, 0, 0)
})
})
})

View file

@ -17,6 +17,7 @@
import configureStore from "redux-mock-store"
import thunk from "redux-thunk"
import * as actionsObjects from "../actions"
import * as alertActions from "../../alert/actions"
jest.mock("../../web", () => ({
ListObjects: jest.fn(() => {
@ -25,6 +26,18 @@ jest.mock("../../web", () => ({
istruncated: false,
nextmarker: "test2"
})
}),
RemoveObject: jest.fn(({ bucketName, objects }) => {
if (!bucketName) {
return Promise.reject({ message: "Invalid bucket" })
}
return Promise.resolve({})
}),
PresignedGet: jest.fn(({ bucket, object }) => {
if (!bucket) {
return Promise.reject({ message: "Invalid bucket" })
}
return Promise.resolve({ url: "https://test.com/bk1/pre1/b.txt" })
})
}))
@ -169,4 +182,123 @@ describe("Objects actions", () => {
expect(actions).toEqual(expectedActions)
expect(window.location.pathname.endsWith("/test/abc/")).toBeTruthy()
})
it("creates objects/REMOVE action", () => {
const store = mockStore()
const expectedActions = [{ type: "objects/REMOVE", object: "obj1" }]
store.dispatch(actionsObjects.removeObject("obj1"))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates objects/REMOVE action when object is deleted", () => {
const store = mockStore({
buckets: { currentBucket: "test" },
objects: { currentPrefix: "pre1/" }
})
const expectedActions = [{ type: "objects/REMOVE", object: "obj1" }]
store.dispatch(actionsObjects.deleteObject("obj1")).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
it("creates alert/SET action when invalid bucket is provided", () => {
const store = mockStore({
buckets: { currentBucket: "" },
objects: { currentPrefix: "pre1/" }
})
const expectedActions = [
{
type: "alert/SET",
alert: { type: "danger", message: "Invalid bucket", id: 0 }
}
]
return store.dispatch(actionsObjects.deleteObject("obj1")).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
it("creates objects/SET_SHARE_OBJECT action for showShareObject", () => {
const store = mockStore()
const expectedActions = [
{
type: "objects/SET_SHARE_OBJECT",
show: true,
object: "b.txt",
url: "test"
}
]
store.dispatch(actionsObjects.showShareObject("b.txt", "test"))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates objects/SET_SHARE_OBJECT action for hideShareObject", () => {
const store = mockStore()
const expectedActions = [
{
type: "objects/SET_SHARE_OBJECT",
show: false,
object: "",
url: ""
}
]
store.dispatch(actionsObjects.hideShareObject())
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates objects/SET_SHARE_OBJECT when object is shared", () => {
const store = mockStore({
buckets: { currentBucket: "bk1" },
objects: { currentPrefix: "pre1/" }
})
const expectedActions = [
{
type: "objects/SET_SHARE_OBJECT",
show: true,
object: "a.txt",
url: "https://test.com/bk1/pre1/b.txt"
},
{
type: "alert/SET",
alert: {
type: "success",
message: "Object shared. Expires in 1 days 0 hours 0 minutes",
id: alertActions.alertId
}
}
]
return store
.dispatch(actionsObjects.shareObject("a.txt", 1, 0, 0))
.then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
it("creates alert/SET when shareObject is failed", () => {
const store = mockStore({
buckets: { currentBucket: "" },
objects: { currentPrefix: "pre1/" }
})
const expectedActions = [
{
type: "alert/SET",
alert: {
type: "danger",
message: "Invalid bucket",
id: alertActions.alertId
}
}
]
return store
.dispatch(actionsObjects.shareObject("a.txt", 1, 0, 0))
.then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
})

View file

@ -26,7 +26,12 @@ describe("objects reducer", () => {
sortOrder: false,
currentPrefix: "",
marker: "",
isTruncated: false
isTruncated: false,
shareObject: {
show: false,
object: "",
url: ""
}
})
})
@ -66,6 +71,28 @@ describe("objects reducer", () => {
expect(newState.isTruncated).toBeFalsy()
})
it("should handle REMOVE", () => {
const newState = reducer(
{ list: [{ name: "obj1" }, { name: "obj2" }] },
{
type: actions.REMOVE,
object: "obj1"
}
)
expect(newState.list).toEqual([{ name: "obj2" }])
})
it("should handle REMOVE with non-existent object", () => {
const newState = reducer(
{ list: [{ name: "obj1" }, { name: "obj2" }] },
{
type: actions.REMOVE,
object: "obj3"
}
)
expect(newState.list).toEqual([{ name: "obj1" }, { name: "obj2" }])
})
it("should handle SET_SORT_BY", () => {
const newState = reducer(undefined, {
type: actions.SET_SORT_BY,
@ -94,4 +121,18 @@ describe("objects reducer", () => {
expect(newState.marker).toEqual("")
expect(newState.isTruncated).toBeFalsy()
})
it("should handle SET_SHARE_OBJECT", () => {
const newState = reducer(undefined, {
type: actions.SET_SHARE_OBJECT,
show: true,
object: "a.txt",
url: "test"
})
expect(newState.shareObject).toEqual({
show: true,
object: "a.txt",
url: "test"
})
})
})

View file

@ -22,13 +22,16 @@ import {
sortObjectsByDate
} from "../utils"
import { getCurrentBucket } from "../buckets/selectors"
import { getCurrentPrefix } from "./selectors"
import * as alertActions from "../alert/actions"
export const SET_LIST = "objects/SET_LIST"
export const APPEND_LIST = "objects/APPEND_LIST"
export const RESET = "objects/RESET"
export const REMOVE = "objects/REMOVE"
export const SET_SORT_BY = "objects/SET_SORT_BY"
export const SET_SORT_ORDER = "objects/SET_SORT_ORDER"
export const SET_CURRENT_PREFIX = "objects/SET_CURRENT_PREFIX"
export const SET_SHARE_OBJECT = "objects/SET_SHARE_OBJECT"
export const setList = (objects, marker, isTruncated) => ({
type: SET_LIST,
@ -127,3 +130,79 @@ export const setCurrentPrefix = prefix => {
prefix
}
}
export const deleteObject = object => {
return function(dispatch, getState) {
const currentBucket = getCurrentBucket(getState())
const currentPrefix = getCurrentPrefix(getState())
const objectName = `${currentPrefix}${object}`
return web
.RemoveObject({
bucketName: currentBucket,
objects: [objectName]
})
.then(() => {
dispatch(removeObject(object))
})
.catch(e => {
dispatch(
alertActions.set({
type: "danger",
message: e.message
})
)
})
}
}
export const removeObject = object => ({
type: REMOVE,
object
})
export const shareObject = (object, days, hours, minutes) => {
return function(dispatch, getState) {
const currentBucket = getCurrentBucket(getState())
const currentPrefix = getCurrentPrefix(getState())
const objectName = `${currentPrefix}${object}`
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
return web
.PresignedGet({
host: location.host,
bucket: currentBucket,
object: objectName,
expiry
})
.then(obj => {
dispatch(showShareObject(object, obj.url))
dispatch(
alertActions.set({
type: "success",
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`
})
)
})
.catch(err => {
dispatch(
alertActions.set({
type: "danger",
message: err.message
})
)
})
}
}
export const showShareObject = (object, url) => ({
type: SET_SHARE_OBJECT,
show: true,
object,
url
})
export const hideShareObject = (object, url) => ({
type: SET_SHARE_OBJECT,
show: false,
object: "",
url: ""
})

View file

@ -16,6 +16,14 @@
import * as actionsObjects from "./actions"
const removeObject = (list, action) => {
const idx = list.findIndex(object => object.name === action.object)
if (idx == -1) {
return list
}
return [...list.slice(0, idx), ...list.slice(idx + 1)]
}
export default (
state = {
list: [],
@ -23,7 +31,12 @@ export default (
sortOrder: false,
currentPrefix: "",
marker: "",
isTruncated: false
isTruncated: false,
shareObject: {
show: false,
object: "",
url: ""
}
},
action
) => {
@ -42,6 +55,11 @@ export default (
marker: action.marker,
isTruncated: action.isTruncated
}
case actionsObjects.REMOVE:
return {
...state,
list: removeObject(state.list, action)
}
case actionsObjects.SET_SORT_BY:
return {
...state,
@ -59,6 +77,15 @@ export default (
marker: "",
isTruncated: false
}
case actionsObjects.SET_SHARE_OBJECT:
return {
...state,
shareObject: {
show: action.show,
object: action.object,
url: action.url
}
}
default:
return state
}