diff --git a/browser/app/js/alert/actions.js b/browser/app/js/alert/actions.js index 4bb73b352..3ce11661d 100644 --- a/browser/app/js/alert/actions.js +++ b/browser/app/js/alert/actions.js @@ -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++ diff --git a/browser/app/js/constants.js b/browser/app/js/constants.js index a2cf3097b..310b959a1 100644 --- a/browser/app/js/constants.js +++ b/browser/app/js/constants.js @@ -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 diff --git a/browser/app/js/objects/DeleteObjectConfirmModal.js b/browser/app/js/objects/DeleteObjectConfirmModal.js new file mode 100644 index 000000000..af88ec98b --- /dev/null +++ b/browser/app/js/objects/DeleteObjectConfirmModal.js @@ -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 +}) => ( + +) + +export default DeleteObjectConfirmModal diff --git a/browser/app/js/objects/ObjectActions.js b/browser/app/js/objects/ObjectActions.js new file mode 100644 index 000000000..9937ecf6f --- /dev/null +++ b/browser/app/js/objects/ObjectActions.js @@ -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 ( + + + + + + + + + + + {showShareObjectModal && } + {this.state.showDeleteConfirmation && ( + + )} + + ) + } +} + +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) diff --git a/browser/app/js/objects/ObjectContainer.js b/browser/app/js/objects/ObjectContainer.js index 17014d6a5..4fdf938c5 100644 --- a/browser/app/js/objects/ObjectContainer.js +++ b/browser/app/js/objects/ObjectContainer.js @@ -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 = const props = { name: object.name, contentType: object.contentType, size: humanize.filesize(object.size), lastModified: Moment(object.lastModified).format("lll"), - actionButtons: [] + actionButtons: actionButtons } return } diff --git a/browser/app/js/objects/ShareObjectModal.js b/browser/app/js/objects/ShareObjectModal.js new file mode 100644 index 000000000..5c82eb1b2 --- /dev/null +++ b/browser/app/js/objects/ShareObjectModal.js @@ -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 ( + + Share Object + +
+ + (this.copyTextInput = node)} + readOnly="readOnly" + value={window.location.protocol + "//" + shareObjectDetails.url} + onClick={() => this.copyTextInput.select()} + /> +
+
+ +
+
+ this.updateExpireValue("days", 1)} + /> +
Days
+
+ +
+ this.updateExpireValue("days", -1)} + /> +
+
+ this.updateExpireValue("hours", 1)} + /> +
Hours
+
+ +
+ this.updateExpireValue("hours", -1)} + /> +
+
+ this.updateExpireValue("minutes", 1)} + /> +
Minutes
+
+ +
+ this.updateExpireValue("minutes", -1)} + /> +
+
+
+
+
+ + + + +
+
+ ) + } +} + +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) diff --git a/browser/app/js/objects/__tests__/DeleteObjectConfirmModal.test.js b/browser/app/js/objects/__tests__/DeleteObjectConfirmModal.test.js new file mode 100644 index 000000000..d5536a225 --- /dev/null +++ b/browser/app/js/objects/__tests__/DeleteObjectConfirmModal.test.js @@ -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() + }) + + it("should call deleteObject when Delete is clicked", () => { + const deleteObject = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("ConfirmModal").prop("okHandler")() + expect(deleteObject).toHaveBeenCalled() + }) + + it("should call hideDeleteConfirmModal when Cancel is clicked", () => { + const hideDeleteConfirmModal = jest.fn() + const wrapper = shallow( + + ) + wrapper.find("ConfirmModal").prop("cancelHandler")() + expect(hideDeleteConfirmModal).toHaveBeenCalled() + }) +}) diff --git a/browser/app/js/objects/__tests__/ObjectActions.test.js b/browser/app/js/objects/__tests__/ObjectActions.test.js new file mode 100644 index 000000000..213a88290 --- /dev/null +++ b/browser/app/js/objects/__tests__/ObjectActions.test.js @@ -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() + }) + + it("should show DeleteObjectConfirmModal when delete action is clicked", () => { + const wrapper = shallow( + + ) + 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( + + ) + 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( + + ) + 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( + + ) + 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( + + ) + expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1) + }) +}) diff --git a/browser/app/js/objects/__tests__/ShareObjectModal.test.js b/browser/app/js/objects/__tests__/ShareObjectModal.test.js new file mode 100644 index 000000000..793a4bcce --- /dev/null +++ b/browser/app/js/objects/__tests__/ShareObjectModal.test.js @@ -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( + + ) + }) + + it("shoud call hideShareObject when Cancel is clicked", () => { + const hideShareObject = jest.fn() + const wrapper = shallow( + + ) + wrapper + .find("button") + .last() + .simulate("click") + expect(hideShareObject).toHaveBeenCalled() + }) + + it("should show the shareable link", () => { + const wrapper = shallow( + + ) + 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( + + ) + 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() + 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( + + ) + 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( + + ) + 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( + + ) + 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( + + ) + 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( + + ) + 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) + }) + }) +}) diff --git a/browser/app/js/objects/__tests__/actions.test.js b/browser/app/js/objects/__tests__/actions.test.js index a5d52b3b1..26d33a37c 100644 --- a/browser/app/js/objects/__tests__/actions.test.js +++ b/browser/app/js/objects/__tests__/actions.test.js @@ -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) + }) + }) }) diff --git a/browser/app/js/objects/__tests__/reducer.test.js b/browser/app/js/objects/__tests__/reducer.test.js index 5d9ec924a..732b227ba 100644 --- a/browser/app/js/objects/__tests__/reducer.test.js +++ b/browser/app/js/objects/__tests__/reducer.test.js @@ -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" + }) + }) }) diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js index a8f29c44c..3ae6fe606 100644 --- a/browser/app/js/objects/actions.js +++ b/browser/app/js/objects/actions.js @@ -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: "" +}) diff --git a/browser/app/js/objects/reducer.js b/browser/app/js/objects/reducer.js index 952a3fcb1..301b9aa83 100644 --- a/browser/app/js/objects/reducer.js +++ b/browser/app/js/objects/reducer.js @@ -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 }