Web UI: Improve "..." menu (#9631)

master
darkdragon-001 4 years ago committed by GitHub
parent eba423bb9d
commit 60791d6dd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      browser/app/js/objects/ObjectActions.js
  2. 95
      browser/app/js/objects/PrefixActions.js
  3. 16
      browser/app/js/objects/PrefixContainer.js
  4. 16
      browser/app/js/objects/__tests__/ObjectActions.test.js
  5. 84
      browser/app/js/objects/__tests__/PrefixActions.test.js
  6. 18
      browser/app/js/objects/__tests__/PrefixContainer.test.js
  7. 31
      browser/app/js/objects/__tests__/actions.test.js
  8. 46
      browser/app/js/objects/actions.js

@ -47,6 +47,11 @@ export class ObjectActions extends React.Component {
SHARE_OBJECT_EXPIRY_MINUTES SHARE_OBJECT_EXPIRY_MINUTES
) )
} }
handleDownload(e) {
e.preventDefault()
const { object, downloadObject } = this.props
downloadObject(object.name)
}
deleteObject() { deleteObject() {
const { object, deleteObject } = this.props const { object, deleteObject } = this.props
deleteObject(object.name) deleteObject(object.name)
@ -82,6 +87,7 @@ export class ObjectActions extends React.Component {
<a <a
href="" href=""
className="fiad-action" className="fiad-action"
title="Share"
onClick={this.shareObject.bind(this)} onClick={this.shareObject.bind(this)}
> >
<i className="fas fa-share-alt" /> <i className="fas fa-share-alt" />
@ -90,6 +96,7 @@ export class ObjectActions extends React.Component {
<a <a
href="" href=""
className="fiad-action" className="fiad-action"
title="Preview"
onClick={this.showPreviewModal.bind(this)} onClick={this.showPreviewModal.bind(this)}
> >
<i className="far fa-file-image" /> <i className="far fa-file-image" />
@ -98,6 +105,15 @@ export class ObjectActions extends React.Component {
<a <a
href="" href=""
className="fiad-action" className="fiad-action"
title="Download"
onClick={this.handleDownload.bind(this)}
>
<i className="fas fa-cloud-download-alt" />
</a>
<a
href=""
className="fiad-action"
title="Delete"
onClick={this.showDeleteConfirmModal.bind(this)} onClick={this.showDeleteConfirmModal.bind(this)}
> >
<i className="fas fa-trash-alt" /> <i className="fas fa-trash-alt" />
@ -134,6 +150,7 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
downloadObject: object => dispatch(objectsActions.downloadObject(object)),
shareObject: (object, days, hours, minutes) => shareObject: (object, days, hours, minutes) =>
dispatch(objectsActions.shareObject(object, days, hours, minutes)), dispatch(objectsActions.shareObject(object, days, hours, minutes)),
deleteObject: (object) => dispatch(objectsActions.deleteObject(object)), deleteObject: (object) => dispatch(objectsActions.deleteObject(object)),

@ -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 (
<Dropdown id={`obj-actions-${object.name}`}>
<Dropdown.Toggle noCaret className="fia-toggle" />
<Dropdown.Menu>
<a
href=""
className="fiad-action"
title="Download as zip"
onClick={this.handleDownload.bind(this)}
>
<i className="fas fa-cloud-download-alt" />
</a>
<a
href=""
className="fiad-action"
title="Delete"
onClick={this.showDeleteConfirmModal.bind(this)}
>
<i className="fas fa-trash-alt" />
</a>
</Dropdown.Menu>
{this.state.showDeleteConfirmation && (
<DeleteObjectConfirmModal
deleteObject={this.deleteObject.bind(this)}
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
/>
)}
</Dropdown>
)
}
}
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)

@ -17,22 +17,32 @@
import React from "react" import React from "react"
import { connect } from "react-redux" import { connect } from "react-redux"
import ObjectItem from "./ObjectItem" import ObjectItem from "./ObjectItem"
import PrefixActions from "./PrefixActions"
import * as actionsObjects from "./actions" import * as actionsObjects from "./actions"
import { getCheckedList } from "./selectors"
export const PrefixContainer = ({ object, currentPrefix, selectPrefix }) => { export const PrefixContainer = ({
object,
currentPrefix,
checkedObjectsCount,
selectPrefix
}) => {
const props = { const props = {
name: object.name, name: object.name,
contentType: object.contentType, contentType: object.contentType,
onClick: () => selectPrefix(`${currentPrefix}${object.name}`) onClick: () => selectPrefix(`${currentPrefix}${object.name}`)
} }
if (checkedObjectsCount == 0) {
props.actionButtons = <PrefixActions object={object} />
}
return <ObjectItem {...props} /> return <ObjectItem {...props} />
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
return { return {
object: ownProps.object, object: ownProps.object,
currentPrefix: state.objects.currentPrefix currentPrefix: state.objects.currentPrefix,
checkedObjectsCount: getCheckedList(state).length
} }
} }

@ -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(
<ObjectActions
object={{ name: "obj1" }}
currentPrefix={"pre1/"}
downloadObject={downloadObject} />
)
wrapper
.find("a")
.at(1)
.simulate("click", { preventDefault: jest.fn() })
expect(downloadObject).toHaveBeenCalled()
})
it("should show PreviewObjectModal when preview action is clicked", () => { it("should show PreviewObjectModal when preview action is clicked", () => {
@ -106,7 +120,7 @@ describe("ObjectActions", () => {
) )
expect(wrapper expect(wrapper
.find("a") .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", () => { it("should call shareObject with object and expiry", () => {

@ -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(<PrefixActions object={{ name: "abc/" }} currentPrefix={"pre1/"} />)
})
it("should show DeleteObjectConfirmModal when delete action is clicked", () => {
const wrapper = shallow(
<PrefixActions object={{ name: "abc/" }} 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(
<PrefixActions object={{ name: "abc/" }} 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(
<PrefixActions
object={{ name: "abc/" }}
currentPrefix={"pre1/"}
deleteObject={deleteObject}
/>
)
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(
<PrefixActions
object={{ name: "abc/" }}
currentPrefix={"pre1/"}
downloadPrefix={downloadPrefix} />
)
wrapper
.find("a")
.first()
.simulate("click", { preventDefault: jest.fn() })
expect(downloadPrefix).toHaveBeenCalled()
})
})

@ -41,4 +41,22 @@ describe("PrefixContainer", () => {
wrapper.find("Connect(ObjectItem)").prop("onClick")() wrapper.find("Connect(ObjectItem)").prop("onClick")()
expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/") expect(selectPrefix).toHaveBeenCalledWith("xyz/abc/")
}) })
it("should pass actions to ObjectItem", () => {
const wrapper = shallow(
<PrefixContainer object={{ name: "abc/" }} checkedObjectsCount={0} />
)
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(
<PrefixContainer object={{ name: "abc/" }} checkedObjectsCount={1} />
)
expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).toBe(
undefined
)
})
}) })

@ -68,6 +68,9 @@ jest.mock("../../web", () => ({
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
return Promise.reject({ message: "Error in creating token" }) return Promise.reject({ message: "Error in creating token" })
}) })
.mockImplementationOnce(() => {
return Promise.resolve({ token: "test" })
})
.mockImplementationOnce(() => { .mockImplementationOnce(() => {
return Promise.resolve({ token: "test" }) 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", () => { it("creates objects/CHECKED_LIST_ADD action", () => {
const store = mockStore() const store = mockStore()
const expectedActions = [ const expectedActions = [

@ -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) => ({ export const checkObject = (object) => ({
type: CHECKED_LIST_ADD, type: CHECKED_LIST_ADD,
object, object,
@ -376,21 +389,28 @@ export const resetCheckedList = () => ({
export const downloadCheckedObjects = () => { export const downloadCheckedObjects = () => {
return function (dispatch, getState) { 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 = { const req = {
bucketName: getCurrentBucket(state), bucketName: bucketName,
prefix: getCurrentPrefix(state), prefix: prefix,
objects: getCheckedList(state), objects: objects,
} }
if (!web.LoggedIn()) { if (web.LoggedIn()) {
const requestUrl = location.origin + "/minio/zip?token="
downloadZip(requestUrl, req, dispatch)
} else {
return web return web
.CreateURLToken() .CreateURLToken()
.then((res) => { .then((res) => {
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}` const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}`
downloadZip(requestUrl, req, dispatch) downloadZip(requestUrl, req, filename, dispatch)
}) })
.catch((err) => .catch((err) =>
dispatch( 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") var anchor = document.createElement("a")
document.body.appendChild(anchor) document.body.appendChild(anchor)
@ -422,7 +444,7 @@ const downloadZip = (url, req, dispatch) => {
var separator = req.prefix.length > 1 ? "-" : "" var separator = req.prefix.length > 1 ? "-" : ""
anchor.href = blobUrl anchor.href = blobUrl
anchor.download = anchor.download = filename ||
req.bucketName + separator + req.prefix.slice(0, -1) + ".zip" req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
anchor.click() anchor.click()

Loading…
Cancel
Save