From 60791d6dd11802197bc3a1303dff540a73129c56 Mon Sep 17 00:00:00 2001 From: darkdragon-001 Date: Mon, 25 May 2020 19:08:19 +0200 Subject: [PATCH] Web UI: Improve "..." menu (#9631) --- browser/app/js/objects/ObjectActions.js | 17 ++++ browser/app/js/objects/PrefixActions.js | 95 +++++++++++++++++++ browser/app/js/objects/PrefixContainer.js | 16 +++- .../objects/__tests__/ObjectActions.test.js | 16 +++- .../objects/__tests__/PrefixActions.test.js | 84 ++++++++++++++++ .../objects/__tests__/PrefixContainer.test.js | 18 ++++ .../app/js/objects/__tests__/actions.test.js | 31 ++++++ browser/app/js/objects/actions.js | 46 ++++++--- 8 files changed, 307 insertions(+), 16 deletions(-) create mode 100644 browser/app/js/objects/PrefixActions.js create mode 100644 browser/app/js/objects/__tests__/PrefixActions.test.js diff --git a/browser/app/js/objects/ObjectActions.js b/browser/app/js/objects/ObjectActions.js index a1660a630..2915eb426 100644 --- a/browser/app/js/objects/ObjectActions.js +++ b/browser/app/js/objects/ObjectActions.js @@ -47,6 +47,11 @@ export class ObjectActions extends React.Component { SHARE_OBJECT_EXPIRY_MINUTES ) } + handleDownload(e) { + e.preventDefault() + const { object, downloadObject } = this.props + downloadObject(object.name) + } deleteObject() { const { object, deleteObject } = this.props deleteObject(object.name) @@ -82,6 +87,7 @@ export class ObjectActions extends React.Component { @@ -90,6 +96,7 @@ export class ObjectActions extends React.Component { @@ -98,6 +105,15 @@ export class ObjectActions extends React.Component { + + + @@ -134,6 +150,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { + downloadObject: object => dispatch(objectsActions.downloadObject(object)), shareObject: (object, days, hours, minutes) => dispatch(objectsActions.shareObject(object, days, hours, minutes)), deleteObject: (object) => dispatch(objectsActions.deleteObject(object)), diff --git a/browser/app/js/objects/PrefixActions.js b/browser/app/js/objects/PrefixActions.js new file mode 100644 index 000000000..b6ebcb0e7 --- /dev/null +++ b/browser/app/js/objects/PrefixActions.js @@ -0,0 +1,95 @@ +/* + * MinIO Cloud Storage (C) 2020 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 DeleteObjectConfirmModal from "./DeleteObjectConfirmModal" +import * as actions from "./actions" + +export class PrefixActions extends React.Component { + constructor(props) { + super(props) + this.state = { + showDeleteConfirmation: false, + } + } + handleDownload(e) { + e.preventDefault() + const { object, downloadPrefix } = this.props + downloadPrefix(object.name) + } + 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, shareObjectName } = this.props + return ( + + + + + + + + + + + {this.state.showDeleteConfirmation && ( + + )} + + ) + } +} + +const mapStateToProps = (state, ownProps) => { + return { + object: ownProps.object, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + downloadPrefix: object => dispatch(actions.downloadPrefix(object)), + deleteObject: (object) => dispatch(actions.deleteObject(object)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(PrefixActions) diff --git a/browser/app/js/objects/PrefixContainer.js b/browser/app/js/objects/PrefixContainer.js index d88fc894e..5f8ccfb7b 100644 --- a/browser/app/js/objects/PrefixContainer.js +++ b/browser/app/js/objects/PrefixContainer.js @@ -17,22 +17,32 @@ import React from "react" import { connect } from "react-redux" import ObjectItem from "./ObjectItem" +import PrefixActions from "./PrefixActions" import * as actionsObjects from "./actions" +import { getCheckedList } from "./selectors" -export const PrefixContainer = ({ object, currentPrefix, selectPrefix }) => { +export const PrefixContainer = ({ + object, + currentPrefix, + checkedObjectsCount, + selectPrefix +}) => { const props = { name: object.name, contentType: object.contentType, onClick: () => selectPrefix(`${currentPrefix}${object.name}`) } - + if (checkedObjectsCount == 0) { + props.actionButtons = + } return } const mapStateToProps = (state, ownProps) => { return { object: ownProps.object, - currentPrefix: state.objects.currentPrefix + currentPrefix: state.objects.currentPrefix, + checkedObjectsCount: getCheckedList(state).length } } diff --git a/browser/app/js/objects/__tests__/ObjectActions.test.js b/browser/app/js/objects/__tests__/ObjectActions.test.js index 7339b1a5d..0f45bb881 100644 --- a/browser/app/js/objects/__tests__/ObjectActions.test.js +++ b/browser/app/js/objects/__tests__/ObjectActions.test.js @@ -67,6 +67,20 @@ describe("ObjectActions", () => { }) + it("should call downloadObject when single object is selected and download button is clicked", () => { + const downloadObject = jest.fn() + const wrapper = shallow( + + ) + wrapper + .find("a") + .at(1) + .simulate("click", { preventDefault: jest.fn() }) + expect(downloadObject).toHaveBeenCalled() + }) it("should show PreviewObjectModal when preview action is clicked", () => { @@ -106,7 +120,7 @@ describe("ObjectActions", () => { ) expect(wrapper .find("a") - .length).toBe(2) // find only the other 2 + .length).toBe(3) // find only the other 2 }) it("should call shareObject with object and expiry", () => { diff --git a/browser/app/js/objects/__tests__/PrefixActions.test.js b/browser/app/js/objects/__tests__/PrefixActions.test.js new file mode 100644 index 000000000..82befde4a --- /dev/null +++ b/browser/app/js/objects/__tests__/PrefixActions.test.js @@ -0,0 +1,84 @@ +/* + * 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 { PrefixActions } from "../PrefixActions" + +describe("PrefixActions", () => { + 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("abc/") + }) + + + it("should call downloadPrefix when single object is selected and download button is clicked", () => { + const downloadPrefix = jest.fn() + const wrapper = shallow( + + ) + wrapper + .find("a") + .first() + .simulate("click", { preventDefault: jest.fn() }) + expect(downloadPrefix).toHaveBeenCalled() + }) +}) diff --git a/browser/app/js/objects/__tests__/PrefixContainer.test.js b/browser/app/js/objects/__tests__/PrefixContainer.test.js index fef61a186..299615811 100644 --- a/browser/app/js/objects/__tests__/PrefixContainer.test.js +++ b/browser/app/js/objects/__tests__/PrefixContainer.test.js @@ -41,4 +41,22 @@ describe("PrefixContainer", () => { wrapper.find("Connect(ObjectItem)").prop("onClick")() expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/") }) + + it("should pass actions to ObjectItem", () => { + const wrapper = shallow( + + ) + expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).not.toBe( + undefined + ) + }) + + it("should pass empty actions to ObjectItem when checkedObjectCount is more than 0", () => { + const wrapper = shallow( + + ) + expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).toBe( + undefined + ) + }) }) diff --git a/browser/app/js/objects/__tests__/actions.test.js b/browser/app/js/objects/__tests__/actions.test.js index 421ebc6f4..e667f5aea 100644 --- a/browser/app/js/objects/__tests__/actions.test.js +++ b/browser/app/js/objects/__tests__/actions.test.js @@ -68,6 +68,9 @@ jest.mock("../../web", () => ({ .mockImplementationOnce(() => { return Promise.reject({ message: "Error in creating token" }) }) + .mockImplementationOnce(() => { + return Promise.resolve({ token: "test" }) + }) .mockImplementationOnce(() => { return Promise.resolve({ token: "test" }) }), @@ -485,6 +488,34 @@ describe("Objects actions", () => { }) }) + it("should download prefix", () => { + const open = jest.fn() + const send = jest.fn() + const xhrMockClass = () => ({ + open: open, + send: send + }) + window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass) + + const store = mockStore({ + buckets: { currentBucket: "bk1" }, + objects: { currentPrefix: "pre1/" } + }) + return store.dispatch(actionsObjects.downloadPrefix("pre2/")).then(() => { + const requestUrl = `${ + location.origin + }${minioBrowserPrefix}/zip?token=test` + expect(open).toHaveBeenCalledWith("POST", requestUrl, true) + expect(send).toHaveBeenCalledWith( + JSON.stringify({ + bucketName: "bk1", + prefix: "pre1/", + objects: ["pre2/"] + }) + ) + }) + }) + it("creates objects/CHECKED_LIST_ADD action", () => { const store = mockStore() const expectedActions = [ diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js index 8e71d2d1f..4c6eff340 100644 --- a/browser/app/js/objects/actions.js +++ b/browser/app/js/objects/actions.js @@ -360,6 +360,19 @@ export const downloadObject = (object) => { } } +export const downloadPrefix = (object) => { + return function (dispatch, getState) { + return downloadObjects( + getCurrentBucket(getState()), + getCurrentPrefix(getState()), + [object], + `${object.slice(0, -1)}.zip`, + dispatch + ) + } +} + + export const checkObject = (object) => ({ type: CHECKED_LIST_ADD, object, @@ -376,21 +389,28 @@ export const resetCheckedList = () => ({ export const downloadCheckedObjects = () => { return function (dispatch, getState) { - const state = getState() + return downloadObjects( + getCurrentBucket(getState()), + getCurrentPrefix(getState()), + getCheckedList(getState()), + null, + dispatch + ) + } +} + +const downloadObjects = (bucketName, prefix, objects, filename, dispatch) => { const req = { - bucketName: getCurrentBucket(state), - prefix: getCurrentPrefix(state), - objects: getCheckedList(state), + bucketName: bucketName, + prefix: prefix, + objects: objects, } - if (!web.LoggedIn()) { - const requestUrl = location.origin + "/minio/zip?token=" - downloadZip(requestUrl, req, dispatch) - } else { + if (web.LoggedIn()) { return web .CreateURLToken() .then((res) => { const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}` - downloadZip(requestUrl, req, dispatch) + downloadZip(requestUrl, req, filename, dispatch) }) .catch((err) => dispatch( @@ -400,11 +420,13 @@ export const downloadCheckedObjects = () => { }) ) ) + } else { + const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=` + downloadZip(requestUrl, req, filename, dispatch) } - } } -const downloadZip = (url, req, dispatch) => { +const downloadZip = (url, req, filename, dispatch) => { var anchor = document.createElement("a") document.body.appendChild(anchor) @@ -422,7 +444,7 @@ const downloadZip = (url, req, dispatch) => { var separator = req.prefix.length > 1 ? "-" : "" anchor.href = blobUrl - anchor.download = + anchor.download = filename || req.bucketName + separator + req.prefix.slice(0, -1) + ".zip" anchor.click()