mirror of
https://github.com/minio/minio.git
synced 2025-11-21 10:16:03 -05:00
Revert "deprecate embedded browser (#12163)"
This reverts commit 736d8cbac4.
Bring contrib files for older contributions
This commit is contained in:
36
browser/app/js/objects/DeleteObjectConfirmModal.js
Normal file
36
browser/app/js/objects/DeleteObjectConfirmModal.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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="fas 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
|
||||
162
browser/app/js/objects/ObjectActions.js
Normal file
162
browser/app/js/objects/ObjectActions.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 PreviewObjectModal from "./PreviewObjectModal"
|
||||
|
||||
import * as objectsActions from "./actions"
|
||||
import { getDataType } from "../mime.js"
|
||||
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,
|
||||
showPreview: 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
|
||||
)
|
||||
}
|
||||
handleDownload(e) {
|
||||
e.preventDefault()
|
||||
const { object, downloadObject } = this.props
|
||||
downloadObject(object.name)
|
||||
}
|
||||
deleteObject() {
|
||||
const { object, deleteObject } = this.props
|
||||
deleteObject(object.name)
|
||||
}
|
||||
showDeleteConfirmModal(e) {
|
||||
e.preventDefault()
|
||||
this.setState({ showDeleteConfirmation: true })
|
||||
}
|
||||
hideDeleteConfirmModal() {
|
||||
this.setState({
|
||||
showDeleteConfirmation: false,
|
||||
})
|
||||
}
|
||||
getObjectURL(objectname, callback) {
|
||||
const { getObjectURL } = this.props
|
||||
getObjectURL(objectname, callback)
|
||||
}
|
||||
showPreviewModal(e) {
|
||||
e.preventDefault()
|
||||
this.setState({ showPreview: true })
|
||||
}
|
||||
hidePreviewModal() {
|
||||
this.setState({
|
||||
showPreview: 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="Share"
|
||||
onClick={this.shareObject.bind(this)}
|
||||
>
|
||||
<i className="fas fa-share-alt" />
|
||||
</a>
|
||||
{getDataType(object.name, object.contentType) == "image" && (
|
||||
<a
|
||||
href=""
|
||||
className="fiad-action"
|
||||
title="Preview"
|
||||
onClick={this.showPreviewModal.bind(this)}
|
||||
>
|
||||
<i className="far fa-file-image" />
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href=""
|
||||
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)}
|
||||
>
|
||||
<i className="fas fa-trash-alt" />
|
||||
</a>
|
||||
</Dropdown.Menu>
|
||||
{showShareObjectModal && shareObjectName === object.name && (
|
||||
<ShareObjectModal object={object} />
|
||||
)}
|
||||
{this.state.showDeleteConfirmation && (
|
||||
<DeleteObjectConfirmModal
|
||||
deleteObject={this.deleteObject.bind(this)}
|
||||
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
|
||||
/>
|
||||
)}
|
||||
{this.state.showPreview && (
|
||||
<PreviewObjectModal
|
||||
object={object}
|
||||
hidePreviewModal={this.hidePreviewModal.bind(this)}
|
||||
getObjectURL={this.getObjectURL.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
object: ownProps.object,
|
||||
showShareObjectModal: state.objects.shareObject.show,
|
||||
shareObjectName: state.objects.shareObject.object,
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
getObjectURL: (object, callback) =>
|
||||
dispatch(objectsActions.getObjectURL(object, callback)),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ObjectActions)
|
||||
49
browser/app/js/objects/ObjectContainer.js
Normal file
49
browser/app/js/objects/ObjectContainer.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 humanize from "humanize"
|
||||
import Moment from "moment"
|
||||
import ObjectItem from "./ObjectItem"
|
||||
import ObjectActions from "./ObjectActions"
|
||||
import * as actionsObjects from "./actions"
|
||||
import { getCheckedList } from "./selectors"
|
||||
|
||||
export const ObjectContainer = ({
|
||||
object,
|
||||
checkedObjectsCount,
|
||||
downloadObject
|
||||
}) => {
|
||||
let props = {
|
||||
name: object.name,
|
||||
contentType: object.contentType,
|
||||
size: humanize.filesize(object.size),
|
||||
lastModified: Moment(object.lastModified).format("lll")
|
||||
}
|
||||
if (checkedObjectsCount == 0) {
|
||||
props.actionButtons = <ObjectActions object={object} />
|
||||
}
|
||||
return <ObjectItem {...props} />
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
checkedObjectsCount: getCheckedList(state).length
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ObjectContainer)
|
||||
88
browser/app/js/objects/ObjectItem.js
Normal file
88
browser/app/js/objects/ObjectItem.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 humanize from "humanize"
|
||||
import Moment from "moment"
|
||||
import { getDataType } from "../mime"
|
||||
import * as actions from "./actions"
|
||||
import { getCheckedList } from "./selectors"
|
||||
|
||||
export const ObjectItem = ({
|
||||
name,
|
||||
contentType,
|
||||
size,
|
||||
lastModified,
|
||||
checked,
|
||||
checkObject,
|
||||
uncheckObject,
|
||||
actionButtons,
|
||||
onClick
|
||||
}) => {
|
||||
return (
|
||||
<div className={"fesl-row"} data-type={getDataType(name, contentType)}>
|
||||
<div className="fesl-item fesl-item-icon">
|
||||
<div className="fi-select">
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
onChange={() => {
|
||||
checked ? uncheckObject(name) : checkObject(name)
|
||||
}}
|
||||
/>
|
||||
<i className="fis-icon" />
|
||||
<i className="fis-helper" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-name">
|
||||
<a
|
||||
href={getDataType(name, contentType) === "folder" ? name : "#"}
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
// onclick function is passed only when we have a prefix
|
||||
if (onClick) {
|
||||
onClick()
|
||||
} else {
|
||||
checked ? uncheckObject(name) : checkObject(name)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-size">{size}</div>
|
||||
<div className="fesl-item fesl-item-modified">{lastModified}</div>
|
||||
<div className="fesl-item fesl-item-actions">{actionButtons}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
checked: getCheckedList(state).indexOf(ownProps.name) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
checkObject: name => dispatch(actions.checkObject(name)),
|
||||
uncheckObject: name => dispatch(actions.uncheckObject(name))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ObjectItem)
|
||||
116
browser/app/js/objects/ObjectsBulkActions.js
Normal file
116
browser/app/js/objects/ObjectsBulkActions.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 classNames from "classnames"
|
||||
import * as actions from "./actions"
|
||||
import { getCheckedList } from "./selectors"
|
||||
import DeleteObjectConfirmModal from "./DeleteObjectConfirmModal"
|
||||
|
||||
export class ObjectsBulkActions extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
showDeleteConfirmation: false
|
||||
}
|
||||
}
|
||||
handleDownload() {
|
||||
const { checkedObjects, clearChecked, downloadChecked, downloadObject } = this.props
|
||||
if (checkedObjects.length === 1 && !checkedObjects[0].endsWith('/')) {
|
||||
downloadObject(checkedObjects[0])
|
||||
clearChecked()
|
||||
} else {
|
||||
downloadChecked()
|
||||
}
|
||||
}
|
||||
deleteChecked() {
|
||||
const { deleteChecked } = this.props
|
||||
deleteChecked()
|
||||
this.hideDeleteConfirmModal()
|
||||
}
|
||||
hideDeleteConfirmModal() {
|
||||
this.setState({
|
||||
showDeleteConfirmation: false
|
||||
})
|
||||
}
|
||||
render() {
|
||||
const { checkedObjects, clearChecked } = this.props
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"list-actions" +
|
||||
classNames({
|
||||
" list-actions-toggled": checkedObjects.length > 0
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="la-label">
|
||||
<i className="fas fa-check-circle" /> {checkedObjects.length}
|
||||
{checkedObjects.length === 1 ? " Object " : " Objects "}
|
||||
selected
|
||||
</span>
|
||||
<span className="la-actions pull-right">
|
||||
<button id="download-checked" onClick={this.handleDownload.bind(this)}>
|
||||
{" "}
|
||||
Download
|
||||
{(checkedObjects.length === 1 && !checkedObjects[0].endsWith('/')) ?
|
||||
" object" : " all as zip" }{" "}
|
||||
</button>
|
||||
</span>
|
||||
<span className="la-actions pull-right">
|
||||
<button
|
||||
id="delete-checked"
|
||||
onClick={() => this.setState({ showDeleteConfirmation: true })}
|
||||
>
|
||||
{" "}
|
||||
Delete selected{" "}
|
||||
</button>
|
||||
</span>
|
||||
<i
|
||||
className="la-close fas fa-times"
|
||||
id="close-bulk-actions"
|
||||
onClick={clearChecked}
|
||||
/>
|
||||
{this.state.showDeleteConfirmation && (
|
||||
<DeleteObjectConfirmModal
|
||||
deleteObject={this.deleteChecked.bind(this)}
|
||||
hideDeleteConfirmModal={this.hideDeleteConfirmModal.bind(this)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
checkedObjects: getCheckedList(state)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
downloadObject: object => dispatch(actions.downloadObject(object)),
|
||||
downloadChecked: () => dispatch(actions.downloadCheckedObjects()),
|
||||
downloadObject: object => dispatch(actions.downloadObject(object)),
|
||||
resetCheckedList: () => dispatch(actions.resetCheckedList()),
|
||||
clearChecked: () => dispatch(actions.resetCheckedList()),
|
||||
deleteChecked: () => dispatch(actions.deleteCheckedObjects())
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ObjectsBulkActions)
|
||||
116
browser/app/js/objects/ObjectsHeader.js
Normal file
116
browser/app/js/objects/ObjectsHeader.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 * as actionsObjects from "./actions"
|
||||
import {
|
||||
SORT_BY_NAME,
|
||||
SORT_BY_SIZE,
|
||||
SORT_BY_LAST_MODIFIED,
|
||||
SORT_ORDER_DESC,
|
||||
SORT_ORDER_ASC
|
||||
} from "../constants"
|
||||
|
||||
export const ObjectsHeader = ({
|
||||
sortedByName,
|
||||
sortedBySize,
|
||||
sortedByLastModified,
|
||||
sortOrder,
|
||||
sortObjects
|
||||
}) => (
|
||||
<div className="feb-container">
|
||||
<header className="fesl-row" data-type="folder">
|
||||
<div className="fesl-item fesl-item-icon" />
|
||||
<div
|
||||
className="fesl-item fesl-item-name"
|
||||
id="sort-by-name"
|
||||
onClick={() => sortObjects(SORT_BY_NAME)}
|
||||
data-sort="name"
|
||||
>
|
||||
Name
|
||||
<i
|
||||
className={classNames({
|
||||
"fesli-sort": true,
|
||||
"fesli-sort--active": sortedByName,
|
||||
fas: true,
|
||||
"fa-sort-alpha-down-alt": sortedByName && sortOrder === SORT_ORDER_DESC,
|
||||
"fa-sort-alpha-down": sortedByName && sortOrder === SORT_ORDER_ASC
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="fesl-item fesl-item-size"
|
||||
id="sort-by-size"
|
||||
onClick={() => sortObjects(SORT_BY_SIZE)}
|
||||
data-sort="size"
|
||||
>
|
||||
Size
|
||||
<i
|
||||
className={classNames({
|
||||
"fesli-sort": true,
|
||||
"fesli-sort--active": sortedBySize,
|
||||
fas: true,
|
||||
"fa-sort-amount-down":
|
||||
sortedBySize && sortOrder === SORT_ORDER_DESC,
|
||||
"fa-sort-amount-down-alt": sortedBySize && sortOrder === SORT_ORDER_ASC
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="fesl-item fesl-item-modified"
|
||||
id="sort-by-last-modified"
|
||||
onClick={() => sortObjects(SORT_BY_LAST_MODIFIED)}
|
||||
data-sort="last-modified"
|
||||
>
|
||||
Last Modified
|
||||
<i
|
||||
className={classNames({
|
||||
"fesli-sort": true,
|
||||
"fesli-sort--active": sortedByLastModified,
|
||||
fas: true,
|
||||
"fa-sort-numeric-down-alt":
|
||||
sortedByLastModified && sortOrder === SORT_ORDER_DESC,
|
||||
"fa-sort-numeric-down":
|
||||
sortedByLastModified && sortOrder === SORT_ORDER_ASC
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="fesl-item fesl-item-actions" />
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
sortedByName: state.objects.sortBy == SORT_BY_NAME,
|
||||
sortedBySize: state.objects.sortBy == SORT_BY_SIZE,
|
||||
sortedByLastModified: state.objects.sortBy == SORT_BY_LAST_MODIFIED,
|
||||
sortOrder: state.objects.sortOrder
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
sortObjects: sortBy => dispatch(actionsObjects.sortObjects(sortBy))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ObjectsHeader)
|
||||
32
browser/app/js/objects/ObjectsList.js
Normal file
32
browser/app/js/objects/ObjectsList.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 ObjectContainer from "./ObjectContainer"
|
||||
import PrefixContainer from "./PrefixContainer"
|
||||
|
||||
export const ObjectsList = ({ objects }) => {
|
||||
const list = objects.map(object => {
|
||||
if (object.name.endsWith("/")) {
|
||||
return <PrefixContainer object={object} key={object.name} />
|
||||
} else {
|
||||
return <ObjectContainer object={object} key={object.name} />
|
||||
}
|
||||
})
|
||||
return <div>{list}</div>
|
||||
}
|
||||
|
||||
export default ObjectsList
|
||||
89
browser/app/js/objects/ObjectsListContainer.js
Normal file
89
browser/app/js/objects/ObjectsListContainer.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 InfiniteScroll from "react-infinite-scroller"
|
||||
import ObjectsList from "./ObjectsList"
|
||||
import { getFilteredObjects } from "./selectors"
|
||||
|
||||
export class ObjectsListContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
page: 1
|
||||
}
|
||||
this.loadNextPage = this.loadNextPage.bind(this)
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
nextProps.currentBucket !== this.props.currentBucket ||
|
||||
nextProps.currentPrefix !== this.props.currentPrefix ||
|
||||
nextProps.sortBy !== this.props.sortBy ||
|
||||
nextProps.sortOrder !== this.props.sortOrder
|
||||
) {
|
||||
this.setState({
|
||||
page: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.filter !== prevProps.filter) {
|
||||
this.setState({
|
||||
page: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
loadNextPage() {
|
||||
this.setState(state => {
|
||||
return { page: state.page + 1 }
|
||||
})
|
||||
}
|
||||
render() {
|
||||
const { filteredObjects, listLoading } = this.props
|
||||
|
||||
const visibleObjects = filteredObjects.slice(0, this.state.page * 100)
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={this.loadNextPage}
|
||||
hasMore={filteredObjects.length > visibleObjects.length}
|
||||
useWindow={true}
|
||||
initialLoad={false}
|
||||
>
|
||||
<ObjectsList objects={visibleObjects} />
|
||||
</InfiniteScroll>
|
||||
{listLoading && <div className="loading" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
currentBucket: state.buckets.currentBucket,
|
||||
currentPrefix: state.objects.currentPrefix,
|
||||
filteredObjects: getFilteredObjects(state),
|
||||
filter: state.objects.filter,
|
||||
sortBy: state.objects.sortBy,
|
||||
sortOrder: state.objects.sortOrder,
|
||||
listLoading: state.objects.listLoading
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ObjectsListContainer)
|
||||
43
browser/app/js/objects/ObjectsSearch.js
Normal file
43
browser/app/js/objects/ObjectsSearch.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 * as actionsObjects from "./actions"
|
||||
|
||||
export const ObjectsSearch = ({ onChange }) => (
|
||||
<div
|
||||
className="input-group ig-left ig-search-dark"
|
||||
style={{ display: "block" }}
|
||||
>
|
||||
<input
|
||||
className="ig-text"
|
||||
type="input"
|
||||
placeholder="Search Objects..."
|
||||
onChange={e => onChange(e.target.value)}
|
||||
/>
|
||||
<i className="ig-helpers" />
|
||||
</div>
|
||||
)
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChange: filter =>
|
||||
dispatch(actionsObjects.setFilter(filter))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(ObjectsSearch)
|
||||
28
browser/app/js/objects/ObjectsSection.js
Normal file
28
browser/app/js/objects/ObjectsSection.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 ObjectsHeader from "./ObjectsHeader"
|
||||
import ObjectsListContainer from "./ObjectsListContainer"
|
||||
|
||||
export const ObjectsSection = () => (
|
||||
<div>
|
||||
<ObjectsHeader />
|
||||
<ObjectsListContainer />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default ObjectsSection
|
||||
176
browser/app/js/objects/Path.js
Normal file
176
browser/app/js/objects/Path.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 ClickOutHandler from "react-onclickout"
|
||||
import { OverlayTrigger, Tooltip } from "react-bootstrap"
|
||||
import { getCurrentBucket } from "../buckets/selectors"
|
||||
import * as actionsObjects from "./actions"
|
||||
import * as actionsBuckets from "../buckets/actions"
|
||||
|
||||
export class Path extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
path: ""
|
||||
}
|
||||
}
|
||||
stopEditing() {
|
||||
this.setState({
|
||||
isEditing: false
|
||||
})
|
||||
}
|
||||
onPrefixClick(e, prefix) {
|
||||
e.preventDefault()
|
||||
const { selectPrefix } = this.props
|
||||
selectPrefix(prefix)
|
||||
}
|
||||
onEditClick(e) {
|
||||
e.preventDefault()
|
||||
const { currentBucket, currentPrefix } = this.props
|
||||
this.setState(
|
||||
{
|
||||
isEditing: true,
|
||||
path: `${currentBucket}/${currentPrefix}`
|
||||
},
|
||||
() => {
|
||||
// focus on input and move cursor to the end
|
||||
this.pathInput.focus()
|
||||
this.pathInput.setSelectionRange(
|
||||
this.state.path.length,
|
||||
this.state.path.length
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
onKeyDown(e) {
|
||||
// When Esc key is pressed
|
||||
if (e.keyCode === 27) {
|
||||
this.stopEditing()
|
||||
}
|
||||
}
|
||||
onInputClickOut() {
|
||||
this.stopEditing()
|
||||
}
|
||||
bucketExists(bucketName) {
|
||||
const { buckets } = this.props
|
||||
return buckets.includes(bucketName)
|
||||
}
|
||||
async onSubmit(e) {
|
||||
e.preventDefault()
|
||||
const { makeBucket, selectBucket } = this.props
|
||||
// all paths need to end in slash to display contents properly
|
||||
let path = this.state.path
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/"
|
||||
}
|
||||
const splittedPath = path.split("/")
|
||||
if (splittedPath.length > 0) {
|
||||
// prevent bucket name from being empty
|
||||
if (splittedPath[0]) {
|
||||
const bucketName = splittedPath[0]
|
||||
const prefix = splittedPath.slice(1).join("/")
|
||||
if (!this.bucketExists(bucketName)) {
|
||||
await makeBucket(bucketName)
|
||||
}
|
||||
// check updated buckets and don't proceed on invalid inputs
|
||||
if (this.bucketExists(bucketName)) {
|
||||
// then select bucket with prefix
|
||||
selectBucket(bucketName, prefix)
|
||||
}
|
||||
this.stopEditing()
|
||||
}
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const pathTooltip = <Tooltip id="tt-path">Choose or create new path</Tooltip>
|
||||
const { currentBucket, currentPrefix } = this.props
|
||||
let dirPath = []
|
||||
let path = ""
|
||||
if (currentPrefix) {
|
||||
path = currentPrefix.split("/").map((dir, i) => {
|
||||
if (dir) {
|
||||
dirPath.push(dir)
|
||||
let dirPath_ = dirPath.join("/") + "/"
|
||||
return (
|
||||
<span key={i}>
|
||||
<a href="" onClick={e => this.onPrefixClick(e, dirPath_)}>
|
||||
{dir}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<h2>
|
||||
{this.state.isEditing ? (
|
||||
<ClickOutHandler onClickOut={() => this.onInputClickOut()}>
|
||||
<form onSubmit={e => this.onSubmit(e)}>
|
||||
<input
|
||||
className="form-control form-control--path"
|
||||
type="text"
|
||||
placeholder="Choose or create new path"
|
||||
ref={node => (this.pathInput = node)}
|
||||
onKeyDown={e => this.onKeyDown(e)}
|
||||
value={this.state.path}
|
||||
onChange={e => this.setState({ path: e.target.value })}
|
||||
/>
|
||||
</form>
|
||||
</ClickOutHandler>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<span className="main">
|
||||
<a href="" onClick={e => this.onPrefixClick(e, "")}>
|
||||
{currentBucket}
|
||||
</a>
|
||||
</span>
|
||||
{path}
|
||||
<OverlayTrigger placement="bottom" overlay={pathTooltip}>
|
||||
<a href="" onClick={e => this.onEditClick(e)} className="fe-edit">
|
||||
<i className="fas fa-folder-plus" />
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
buckets: state.buckets.list,
|
||||
currentBucket: getCurrentBucket(state),
|
||||
currentPrefix: state.objects.currentPrefix
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
makeBucket: bucket => dispatch(actionsBuckets.makeBucket(bucket)),
|
||||
selectBucket: (bucket, prefix) =>
|
||||
dispatch(actionsBuckets.selectBucket(bucket, prefix)),
|
||||
selectPrefix: prefix => dispatch(actionsObjects.selectPrefix(prefix))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Path)
|
||||
95
browser/app/js/objects/PrefixActions.js
Normal file
95
browser/app/js/objects/PrefixActions.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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)
|
||||
55
browser/app/js/objects/PrefixContainer.js
Normal file
55
browser/app/js/objects/PrefixContainer.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 ObjectItem from "./ObjectItem"
|
||||
import PrefixActions from "./PrefixActions"
|
||||
import * as actionsObjects from "./actions"
|
||||
import { getCheckedList } from "./selectors"
|
||||
|
||||
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 = <PrefixActions object={object} />
|
||||
}
|
||||
return <ObjectItem {...props} />
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
object: ownProps.object,
|
||||
currentPrefix: state.objects.currentPrefix,
|
||||
checkedObjectsCount: getCheckedList(state).length
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
selectPrefix: prefix => dispatch(actionsObjects.selectPrefix(prefix))
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PrefixContainer)
|
||||
68
browser/app/js/objects/PreviewObjectModal.js
Normal file
68
browser/app/js/objects/PreviewObjectModal.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { Modal, ModalHeader, ModalBody } from "react-bootstrap"
|
||||
|
||||
class PreviewObjectModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
url: "",
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getObjectURL(this.props.object.name, (url) => {
|
||||
this.setState({
|
||||
url: url,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hidePreviewModal } = this.props
|
||||
return (
|
||||
<Modal
|
||||
show={true}
|
||||
animation={false}
|
||||
onHide={hidePreviewModal}
|
||||
bsSize="large"
|
||||
>
|
||||
<ModalHeader>Preview</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="input-group">
|
||||
{this.state.url && (
|
||||
<object data={this.state.url} style={{ display: "block", width: "100%" }}>
|
||||
<h3 style={{ textAlign: "center", display: "block", width: "100%" }}>
|
||||
Do not have read permissions to preview "{this.props.object.name}"
|
||||
</h3>
|
||||
</object>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<div className="modal-footer">
|
||||
{
|
||||
<button className="btn btn-link" onClick={hidePreviewModal}>
|
||||
Cancel
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default PreviewObjectModal
|
||||
216
browser/app/js/objects/ShareObjectModal.js
Normal file
216
browser/app/js/objects/ShareObjectModal.js
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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"
|
||||
import QRCode from "react-qr-code";
|
||||
|
||||
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, hideShareObject } = this.props
|
||||
const url = `${window.location.protocol}//${shareObjectDetails.url}`
|
||||
return (
|
||||
<Modal
|
||||
show={true}
|
||||
animation={false}
|
||||
onHide={hideShareObject}
|
||||
bsSize="small"
|
||||
>
|
||||
<ModalHeader>Share Object</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="input-group copy-text">
|
||||
<QRCode value={url} size={128}/>
|
||||
<label>Shareable Link</label>
|
||||
<input
|
||||
type="text"
|
||||
ref={node => (this.copyTextInput = node)}
|
||||
readOnly="readOnly"
|
||||
value={url}
|
||||
onClick={() => this.copyTextInput.select()}
|
||||
/>
|
||||
</div>
|
||||
{shareObjectDetails.showExpiryDate && (
|
||||
<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={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)
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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()
|
||||
})
|
||||
})
|
||||
165
browser/app/js/objects/__tests__/ObjectActions.test.js
Normal file
165
browser/app/js/objects/__tests__/ObjectActions.test.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 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", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectActions
|
||||
object={{ name: "obj1", contentType: "image/jpeg"}}
|
||||
currentPrefix={"pre1/"} />
|
||||
)
|
||||
wrapper
|
||||
.find("a")
|
||||
.at(1)
|
||||
.simulate("click", { preventDefault: jest.fn() })
|
||||
expect(wrapper.state("showPreview")).toBeTruthy()
|
||||
expect(wrapper.find("PreviewObjectModal").length).toBe(1)
|
||||
})
|
||||
|
||||
it("should hide PreviewObjectModal when cancel button is clicked", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectActions
|
||||
object={{ name: "obj1" , contentType: "image/jpeg"}}
|
||||
currentPrefix={"pre1/"} />
|
||||
)
|
||||
wrapper
|
||||
.find("a")
|
||||
.at(1)
|
||||
.simulate("click", { preventDefault: jest.fn() })
|
||||
wrapper.find("PreviewObjectModal").prop("hidePreviewModal")()
|
||||
wrapper.update()
|
||||
expect(wrapper.state("showPreview")).toBeFalsy()
|
||||
expect(wrapper.find("PreviewObjectModal").length).toBe(0)
|
||||
})
|
||||
it("should not show PreviewObjectModal when preview action is clicked if object is not an image", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectActions
|
||||
object={{ name: "obj1"}}
|
||||
currentPrefix={"pre1/"} />
|
||||
)
|
||||
expect(wrapper
|
||||
.find("a")
|
||||
.length).toBe(3) // find only the other 2
|
||||
})
|
||||
|
||||
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}
|
||||
shareObjectName={"obj1"}
|
||||
/>
|
||||
)
|
||||
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(1)
|
||||
})
|
||||
|
||||
it("shouldn't render ShareObjectModal when the names of the objects don't match", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectActions
|
||||
object={{ name: "obj1" }}
|
||||
currentPrefix={"pre1/"}
|
||||
showShareObjectModal={true}
|
||||
shareObjectName={"obj2"}
|
||||
/>
|
||||
)
|
||||
expect(wrapper.find("Connect(ShareObjectModal)").length).toBe(0)
|
||||
})
|
||||
})
|
||||
49
browser/app/js/objects/__tests__/ObjectContainer.test.js
Normal file
49
browser/app/js/objects/__tests__/ObjectContainer.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectContainer } from "../ObjectContainer"
|
||||
|
||||
describe("ObjectContainer", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectContainer object={{ name: "test1.jpg" }} />)
|
||||
})
|
||||
|
||||
it("should render ObjectItem with props", () => {
|
||||
const wrapper = shallow(<ObjectContainer object={{ name: "test1.jpg" }} />)
|
||||
expect(wrapper.find("Connect(ObjectItem)").length).toBe(1)
|
||||
expect(wrapper.find("Connect(ObjectItem)").prop("name")).toBe("test1.jpg")
|
||||
})
|
||||
|
||||
it("should pass actions to ObjectItem", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectContainer object={{ name: "test1.jpg" }} 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(
|
||||
<ObjectContainer object={{ name: "test1.jpg" }} checkedObjectsCount={1} />
|
||||
)
|
||||
expect(wrapper.find("Connect(ObjectItem)").prop("actionButtons")).toBe(
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
76
browser/app/js/objects/__tests__/ObjectItem.test.js
Normal file
76
browser/app/js/objects/__tests__/ObjectItem.test.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectItem } from "../ObjectItem"
|
||||
|
||||
describe("ObjectItem", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectItem name={"test"} />)
|
||||
})
|
||||
|
||||
it("should render with content type", () => {
|
||||
const wrapper = shallow(<ObjectItem name={"test.jpg"} contentType={""} />)
|
||||
expect(wrapper.prop("data-type")).toBe("image")
|
||||
})
|
||||
|
||||
it("shouldn't call onClick when the object isclicked", () => {
|
||||
const onClick = jest.fn()
|
||||
const checkObject = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectItem name={"test"} checkObject={checkObject} />
|
||||
)
|
||||
wrapper.find("a").simulate("click", { preventDefault: jest.fn() })
|
||||
expect(onClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call onClick when the folder isclicked", () => {
|
||||
const onClick = jest.fn()
|
||||
const wrapper = shallow(<ObjectItem name={"test/"} onClick={onClick} />)
|
||||
wrapper.find("a").simulate("click", { preventDefault: jest.fn() })
|
||||
expect(onClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call checkObject when the object/prefix is checked", () => {
|
||||
const checkObject = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectItem name={"test"} checked={false} checkObject={checkObject} />
|
||||
)
|
||||
wrapper.find("input[type='checkbox']").simulate("change")
|
||||
expect(checkObject).toHaveBeenCalledWith("test")
|
||||
})
|
||||
|
||||
it("should render checked checkbox", () => {
|
||||
const wrapper = shallow(<ObjectItem name={"test"} checked={true} />)
|
||||
expect(wrapper.find("input[type='checkbox']").prop("checked")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should call uncheckObject when the object/prefix is unchecked", () => {
|
||||
const checkObject = jest.fn()
|
||||
const uncheckObject = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectItem
|
||||
name={"test"}
|
||||
checked={true}
|
||||
checkObject={checkObject}
|
||||
uncheckObject={uncheckObject}
|
||||
/>
|
||||
)
|
||||
wrapper.find("input[type='checkbox']").simulate("change")
|
||||
expect(uncheckObject).toHaveBeenCalledWith("test")
|
||||
})
|
||||
})
|
||||
100
browser/app/js/objects/__tests__/ObjectsBulkActions.test.js
Normal file
100
browser/app/js/objects/__tests__/ObjectsBulkActions.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectsBulkActions } from "../ObjectsBulkActions"
|
||||
|
||||
describe("ObjectsBulkActions", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectsBulkActions checkedObjects={[]} />)
|
||||
})
|
||||
|
||||
it("should show actions when checkObjectsCount is more than 0", () => {
|
||||
const wrapper = shallow(<ObjectsBulkActions checkedObjects={["test"]} />)
|
||||
expect(wrapper.hasClass("list-actions-toggled")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should call downloadObject when single object is selected and download button is clicked", () => {
|
||||
const downloadObject = jest.fn()
|
||||
const clearChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
checkedObjects={["test"]}
|
||||
downloadObject={downloadObject}
|
||||
clearChecked={clearChecked}
|
||||
/>
|
||||
)
|
||||
wrapper.find("#download-checked").simulate("click")
|
||||
expect(downloadObject).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call downloadChecked when a folder is selected and download button is clicked", () => {
|
||||
const downloadChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
checkedObjects={["test/"]}
|
||||
downloadChecked={downloadChecked}
|
||||
/>
|
||||
)
|
||||
wrapper.find("#download-checked").simulate("click")
|
||||
expect(downloadChecked).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call downloadChecked when multiple objects are selected and download button is clicked", () => {
|
||||
const downloadChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
checkedObjects={["test1", "test2"]}
|
||||
downloadChecked={downloadChecked}
|
||||
/>
|
||||
)
|
||||
wrapper.find("#download-checked").simulate("click")
|
||||
expect(downloadChecked).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call clearChecked when close button is clicked", () => {
|
||||
const clearChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions checkedObjects={["test"]} clearChecked={clearChecked} />
|
||||
)
|
||||
wrapper.find("#close-bulk-actions").simulate("click")
|
||||
expect(clearChecked).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("shoud show DeleteObjectConfirmModal when delete-checked button is clicked", () => {
|
||||
const wrapper = shallow(<ObjectsBulkActions checkedObjects={["test"]} />)
|
||||
wrapper.find("#delete-checked").simulate("click")
|
||||
wrapper.update()
|
||||
expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(1)
|
||||
})
|
||||
|
||||
it("shoud call deleteChecked when Delete is clicked on confirmation modal", () => {
|
||||
const deleteChecked = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsBulkActions
|
||||
checkedObjects={["test"]}
|
||||
deleteChecked={deleteChecked}
|
||||
/>
|
||||
)
|
||||
wrapper.find("#delete-checked").simulate("click")
|
||||
wrapper.update()
|
||||
wrapper.find("DeleteObjectConfirmModal").prop("deleteObject")()
|
||||
expect(deleteChecked).toHaveBeenCalled()
|
||||
wrapper.update()
|
||||
expect(wrapper.find("DeleteObjectConfirmModal").length).toBe(0)
|
||||
})
|
||||
})
|
||||
122
browser/app/js/objects/__tests__/ObjectsHeader.test.js
Normal file
122
browser/app/js/objects/__tests__/ObjectsHeader.test.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectsHeader } from "../ObjectsHeader"
|
||||
import { SORT_ORDER_ASC, SORT_ORDER_DESC } from "../../constants"
|
||||
|
||||
describe("ObjectsHeader", () => {
|
||||
it("should render without crashing", () => {
|
||||
const sortObjects = jest.fn()
|
||||
shallow(<ObjectsHeader sortObjects={sortObjects} />)
|
||||
})
|
||||
|
||||
it("should render the name column with asc class when objects are sorted by name asc", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsHeader
|
||||
sortObjects={sortObjects}
|
||||
sortedByName={true}
|
||||
sortOrder={SORT_ORDER_ASC}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
wrapper.find("#sort-by-name i").hasClass("fa-sort-alpha-down")
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should render the name column with desc class when objects are sorted by name desc", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsHeader
|
||||
sortObjects={sortObjects}
|
||||
sortedByName={true}
|
||||
sortOrder={SORT_ORDER_DESC}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
wrapper.find("#sort-by-name i").hasClass("fa-sort-alpha-down-alt")
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should render the size column with asc class when objects are sorted by size asc", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsHeader
|
||||
sortObjects={sortObjects}
|
||||
sortedBySize={true}
|
||||
sortOrder={SORT_ORDER_ASC}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
wrapper.find("#sort-by-size i").hasClass("fa-sort-amount-down-alt")
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should render the size column with desc class when objects are sorted by size desc", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsHeader
|
||||
sortObjects={sortObjects}
|
||||
sortedBySize={true}
|
||||
sortOrder={SORT_ORDER_DESC}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
wrapper.find("#sort-by-size i").hasClass("fa-sort-amount-down")
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should render the date column with asc class when objects are sorted by date asc", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsHeader
|
||||
sortObjects={sortObjects}
|
||||
sortedByLastModified={true}
|
||||
sortOrder={SORT_ORDER_ASC}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
wrapper.find("#sort-by-last-modified i").hasClass("fa-sort-numeric-down")
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should render the date column with desc class when objects are sorted by date desc", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<ObjectsHeader
|
||||
sortObjects={sortObjects}
|
||||
sortedByLastModified={true}
|
||||
sortOrder={SORT_ORDER_DESC}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
wrapper.find("#sort-by-last-modified i").hasClass("fa-sort-numeric-down-alt")
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should call sortObjects when a column is clicked", () => {
|
||||
const sortObjects = jest.fn()
|
||||
const wrapper = shallow(<ObjectsHeader sortObjects={sortObjects} />)
|
||||
wrapper.find("#sort-by-name").simulate("click")
|
||||
expect(sortObjects).toHaveBeenCalledWith("name")
|
||||
wrapper.find("#sort-by-size").simulate("click")
|
||||
expect(sortObjects).toHaveBeenCalledWith("size")
|
||||
wrapper.find("#sort-by-last-modified").simulate("click")
|
||||
expect(sortObjects).toHaveBeenCalledWith("last-modified")
|
||||
})
|
||||
})
|
||||
39
browser/app/js/objects/__tests__/ObjectsList.test.js
Normal file
39
browser/app/js/objects/__tests__/ObjectsList.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectsList } from "../ObjectsList"
|
||||
|
||||
describe("ObjectsList", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectsList objects={[]} />)
|
||||
})
|
||||
|
||||
it("should render ObjectContainer for every object", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectsList objects={[{ name: "test1.jpg" }, { name: "test2.jpg" }]} />
|
||||
)
|
||||
expect(wrapper.find("Connect(ObjectContainer)").length).toBe(2)
|
||||
})
|
||||
|
||||
it("should render PrefixContainer for every prefix", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectsList objects={[{ name: "abc/" }, { name: "xyz/" }]} />
|
||||
)
|
||||
expect(wrapper.find("Connect(PrefixContainer)").length).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectsListContainer } from "../ObjectsListContainer"
|
||||
|
||||
describe("ObjectsList", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectsListContainer filteredObjects={[]} />)
|
||||
})
|
||||
|
||||
it("should render ObjectsList with objects", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectsListContainer
|
||||
filteredObjects={[{ name: "test1.jpg" }, { name: "test2.jpg" }]}
|
||||
/>
|
||||
)
|
||||
expect(wrapper.find("ObjectsList").length).toBe(1)
|
||||
expect(wrapper.find("ObjectsList").prop("objects")).toEqual([
|
||||
{ name: "test1.jpg" },
|
||||
{ name: "test2.jpg" }
|
||||
])
|
||||
})
|
||||
|
||||
it("should show the loading indicator when the objects are being loaded", () => {
|
||||
const wrapper = shallow(
|
||||
<ObjectsListContainer
|
||||
currentBucket="test1"
|
||||
filteredObjects={[]}
|
||||
listLoading={true}
|
||||
/>
|
||||
)
|
||||
expect(wrapper.find(".loading").exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
32
browser/app/js/objects/__tests__/ObjectsSearch.test.js
Normal file
32
browser/app/js/objects/__tests__/ObjectsSearch.test.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectsSearch } from "../ObjectsSearch"
|
||||
|
||||
describe("ObjectsSearch", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectsSearch />)
|
||||
})
|
||||
|
||||
it("should call onChange with search text", () => {
|
||||
const onChange = jest.fn()
|
||||
const wrapper = shallow(<ObjectsSearch onChange={onChange} />)
|
||||
wrapper.find("input").simulate("change", { target: { value: "test" } })
|
||||
expect(onChange).toHaveBeenCalledWith("test")
|
||||
})
|
||||
})
|
||||
25
browser/app/js/objects/__tests__/ObjectsSection.test.js
Normal file
25
browser/app/js/objects/__tests__/ObjectsSection.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { ObjectsSection } from "../ObjectsSection"
|
||||
|
||||
describe("ObjectsSection", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<ObjectsSection />)
|
||||
})
|
||||
})
|
||||
143
browser/app/js/objects/__tests__/Path.test.js
Normal file
143
browser/app/js/objects/__tests__/Path.test.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { Path } from "../Path"
|
||||
|
||||
describe("Path", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<Path currentBucket={"test1"} currentPrefix={"test2"} />)
|
||||
})
|
||||
|
||||
it("should render only bucket if there is no prefix", () => {
|
||||
const wrapper = shallow(<Path currentBucket={"test1"} currentPrefix={""} />)
|
||||
expect(wrapper.find("span").length).toBe(1)
|
||||
expect(
|
||||
wrapper
|
||||
.find("span")
|
||||
.at(0)
|
||||
.text()
|
||||
).toBe("test1")
|
||||
})
|
||||
|
||||
it("should render bucket and prefix", () => {
|
||||
const wrapper = shallow(
|
||||
<Path currentBucket={"test1"} currentPrefix={"a/b/"} />
|
||||
)
|
||||
expect(wrapper.find("span").length).toBe(3)
|
||||
expect(
|
||||
wrapper
|
||||
.find("span")
|
||||
.at(0)
|
||||
.text()
|
||||
).toBe("test1")
|
||||
expect(
|
||||
wrapper
|
||||
.find("span")
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe("a")
|
||||
expect(
|
||||
wrapper
|
||||
.find("span")
|
||||
.at(2)
|
||||
.text()
|
||||
).toBe("b")
|
||||
})
|
||||
|
||||
it("should call selectPrefix when a prefix part is clicked", () => {
|
||||
const selectPrefix = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<Path
|
||||
currentBucket={"test1"}
|
||||
currentPrefix={"a/b/"}
|
||||
selectPrefix={selectPrefix}
|
||||
/>
|
||||
)
|
||||
wrapper
|
||||
.find("a")
|
||||
.at(2)
|
||||
.simulate("click", { preventDefault: jest.fn() })
|
||||
expect(selectPrefix).toHaveBeenCalledWith("a/b/")
|
||||
})
|
||||
|
||||
it("should switch to input mode when edit icon is clicked", () => {
|
||||
const wrapper = mount(<Path currentBucket={"test1"} currentPrefix={""} />)
|
||||
wrapper.find(".fe-edit").simulate("click", { preventDefault: jest.fn() })
|
||||
expect(wrapper.find(".form-control--path").exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should navigate to prefix when user types path for existing bucket", () => {
|
||||
const selectBucket = jest.fn()
|
||||
const buckets = ["test1", "test2"]
|
||||
const wrapper = mount(
|
||||
<Path
|
||||
buckets={buckets}
|
||||
currentBucket={"test1"}
|
||||
currentPrefix={""}
|
||||
selectBucket={selectBucket}
|
||||
/>
|
||||
)
|
||||
wrapper.setState({
|
||||
isEditing: true,
|
||||
path: "test2/dir1/"
|
||||
})
|
||||
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() })
|
||||
expect(selectBucket).toHaveBeenCalledWith("test2", "dir1/")
|
||||
})
|
||||
|
||||
it("should create a new bucket if bucket typed in path doesn't exist", () => {
|
||||
const makeBucket = jest.fn()
|
||||
const buckets = ["test1", "test2"]
|
||||
const wrapper = mount(
|
||||
<Path
|
||||
buckets={buckets}
|
||||
currentBucket={"test1"}
|
||||
currentPrefix={""}
|
||||
makeBucket={makeBucket}
|
||||
/>
|
||||
)
|
||||
wrapper.setState({
|
||||
isEditing: true,
|
||||
path: "test3/dir1/"
|
||||
})
|
||||
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() })
|
||||
expect(makeBucket).toHaveBeenCalledWith("test3")
|
||||
})
|
||||
|
||||
it("should not make or select bucket if path doesn't point to bucket", () => {
|
||||
const makeBucket = jest.fn()
|
||||
const selectBucket = jest.fn()
|
||||
const buckets = ["test1", "test2"]
|
||||
const wrapper = mount(
|
||||
<Path
|
||||
buckets={buckets}
|
||||
currentBucket={"test1"}
|
||||
currentPrefix={""}
|
||||
makeBucket={makeBucket}
|
||||
selectBucket={selectBucket}
|
||||
/>
|
||||
)
|
||||
wrapper.setState({
|
||||
isEditing: true,
|
||||
path: "//dir1/dir2/"
|
||||
})
|
||||
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() })
|
||||
expect(makeBucket).not.toHaveBeenCalled()
|
||||
expect(selectBucket).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
84
browser/app/js/objects/__tests__/PrefixActions.test.js
Normal file
84
browser/app/js/objects/__tests__/PrefixActions.test.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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()
|
||||
})
|
||||
})
|
||||
62
browser/app/js/objects/__tests__/PrefixContainer.test.js
Normal file
62
browser/app/js/objects/__tests__/PrefixContainer.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { PrefixContainer } from "../PrefixContainer"
|
||||
|
||||
describe("PrefixContainer", () => {
|
||||
it("should render without crashing", () => {
|
||||
shallow(<PrefixContainer object={{ name: "abc/" }} />)
|
||||
})
|
||||
|
||||
it("should render ObjectItem with props", () => {
|
||||
const wrapper = shallow(<PrefixContainer object={{ name: "abc/" }} />)
|
||||
expect(wrapper.find("Connect(ObjectItem)").length).toBe(1)
|
||||
expect(wrapper.find("Connect(ObjectItem)").prop("name")).toBe("abc/")
|
||||
})
|
||||
|
||||
it("should call selectPrefix when the prefix is clicked", () => {
|
||||
const selectPrefix = jest.fn()
|
||||
const wrapper = shallow(
|
||||
<PrefixContainer
|
||||
object={{ name: "abc/" }}
|
||||
currentPrefix={"xyz/"}
|
||||
selectPrefix={selectPrefix}
|
||||
/>
|
||||
)
|
||||
wrapper.find("Connect(ObjectItem)").prop("onClick")()
|
||||
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
|
||||
)
|
||||
})
|
||||
})
|
||||
211
browser/app/js/objects/__tests__/ShareObjectModal.test.js
Normal file
211
browser/app/js/objects/__tests__/ShareObjectModal.test.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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", showExpiryDate: true }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
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", showExpiryDate: true }}
|
||||
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", showExpiryDate: true }}
|
||||
/>
|
||||
)
|
||||
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", showExpiryDate: true }}
|
||||
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", showExpiryDate: true }
|
||||
}
|
||||
|
||||
it("should not show expiry values if shared with public link", () => {
|
||||
const shareObjectDetails = { show: true, object: "obj1", url: "test", showExpiryDate: false }
|
||||
const wrapper = shallow(<ShareObjectModal {...props} shareObjectDetails={shareObjectDetails} />)
|
||||
expect(wrapper.find('.set-expire').exists()).toEqual(false)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
584
browser/app/js/objects/__tests__/actions.test.js
Normal file
584
browser/app/js/objects/__tests__/actions.test.js
Normal file
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 actionsObjects from "../actions"
|
||||
import * as alertActions from "../../alert/actions"
|
||||
import {
|
||||
minioBrowserPrefix,
|
||||
SORT_BY_NAME,
|
||||
SORT_ORDER_ASC,
|
||||
SORT_BY_LAST_MODIFIED,
|
||||
SORT_ORDER_DESC
|
||||
} from "../../constants"
|
||||
import history from "../../history"
|
||||
|
||||
jest.mock("../../web", () => ({
|
||||
LoggedIn: jest
|
||||
.fn(() => true)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false),
|
||||
ListObjects: jest.fn(({ bucketName }) => {
|
||||
if (bucketName === "test-deny") {
|
||||
return Promise.reject({
|
||||
message: "listobjects is denied"
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve({
|
||||
objects: [{ name: "test1" }, { name: "test2" }],
|
||||
writable: false
|
||||
})
|
||||
}
|
||||
}),
|
||||
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" })
|
||||
}),
|
||||
CreateURLToken: jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ token: "test" })
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.reject({ message: "Error in creating token" })
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ token: "test" })
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ token: "test" })
|
||||
}),
|
||||
GetBucketPolicy: jest.fn(({ bucketName, prefix }) => {
|
||||
if (!bucketName) {
|
||||
return Promise.reject({ message: "Invalid bucket" })
|
||||
}
|
||||
if (bucketName === 'test-public') return Promise.resolve({ policy: 'readonly' })
|
||||
return Promise.resolve({})
|
||||
})
|
||||
}))
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureStore(middlewares)
|
||||
|
||||
describe("Objects actions", () => {
|
||||
it("creates objects/SET_LIST action", () => {
|
||||
const store = mockStore()
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_LIST",
|
||||
objects: [{ name: "test1" }, { name: "test2" }]
|
||||
}
|
||||
]
|
||||
store.dispatch(
|
||||
actionsObjects.setList([{ name: "test1" }, { name: "test2" }])
|
||||
)
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("creates objects/SET_SORT_BY action", () => {
|
||||
const store = mockStore()
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SORT_BY",
|
||||
sortBy: SORT_BY_NAME
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.setSortBy(SORT_BY_NAME))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("creates objects/SET_SORT_ORDER action", () => {
|
||||
const store = mockStore()
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SORT_ORDER",
|
||||
sortOrder: SORT_ORDER_ASC
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.setSortOrder(SORT_ORDER_ASC))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("creates objects/SET_LIST after fetching the objects", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "bk1" },
|
||||
objects: { currentPrefix: "" }
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/RESET_LIST"
|
||||
},
|
||||
{ listLoading: true, type: "objects/SET_LIST_LOADING" },
|
||||
{
|
||||
type: "objects/SET_SORT_BY",
|
||||
sortBy: SORT_BY_LAST_MODIFIED
|
||||
},
|
||||
{
|
||||
type: "objects/SET_SORT_ORDER",
|
||||
sortOrder: SORT_ORDER_DESC
|
||||
},
|
||||
{
|
||||
type: "objects/SET_LIST",
|
||||
objects: [{ name: "test2" }, { name: "test1" }]
|
||||
},
|
||||
{
|
||||
type: "objects/SET_PREFIX_WRITABLE",
|
||||
prefixWritable: false
|
||||
},
|
||||
{ listLoading: false, type: "objects/SET_LIST_LOADING" }
|
||||
]
|
||||
return store.dispatch(actionsObjects.fetchObjects()).then(() => {
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
})
|
||||
|
||||
it("creates objects/RESET_LIST after failing to fetch the objects from bucket with ListObjects denied for LoggedIn users", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "test-deny" },
|
||||
objects: { currentPrefix: "" }
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/RESET_LIST"
|
||||
},
|
||||
{ listLoading: true, type: "objects/SET_LIST_LOADING" },
|
||||
{
|
||||
type: "alert/SET",
|
||||
alert: {
|
||||
type: "danger",
|
||||
message: "listobjects is denied",
|
||||
id: alertActions.alertId,
|
||||
autoClear: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "objects/RESET_LIST"
|
||||
},
|
||||
{ listLoading: false, type: "objects/SET_LIST_LOADING" }
|
||||
]
|
||||
return store.dispatch(actionsObjects.fetchObjects()).then(() => {
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
})
|
||||
|
||||
it("redirect to login after failing to fetch the objects from bucket for non-LoggedIn users", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "test-deny" },
|
||||
objects: { currentPrefix: "" }
|
||||
})
|
||||
return store.dispatch(actionsObjects.fetchObjects()).then(() => {
|
||||
expect(history.location.pathname.endsWith("/login")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
it("creates objects/SET_SORT_BY and objects/SET_SORT_ORDER when sortObjects is called", () => {
|
||||
const store = mockStore({
|
||||
objects: {
|
||||
list: [],
|
||||
sortBy: "",
|
||||
sortOrder: SORT_ORDER_ASC
|
||||
}
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SORT_BY",
|
||||
sortBy: SORT_BY_NAME
|
||||
},
|
||||
{
|
||||
type: "objects/SET_SORT_ORDER",
|
||||
sortOrder: SORT_ORDER_ASC
|
||||
},
|
||||
{
|
||||
type: "objects/SET_LIST",
|
||||
objects: []
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.sortObjects(SORT_BY_NAME))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("should update browser url and creates objects/SET_CURRENT_PREFIX and objects/CHECKED_LIST_RESET actions when selectPrefix is called", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "test" },
|
||||
objects: { currentPrefix: "" }
|
||||
})
|
||||
const expectedActions = [
|
||||
{ type: "objects/SET_CURRENT_PREFIX", prefix: "abc/" },
|
||||
{
|
||||
type: "objects/RESET_LIST"
|
||||
},
|
||||
{ listLoading: true, type: "objects/SET_LIST_LOADING" },
|
||||
{ type: "objects/CHECKED_LIST_RESET" }
|
||||
]
|
||||
store.dispatch(actionsObjects.selectPrefix("abc/"))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
expect(window.location.pathname.endsWith("/test/abc/")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("create objects/SET_PREFIX_WRITABLE action", () => {
|
||||
const store = mockStore()
|
||||
const expectedActions = [
|
||||
{ type: "objects/SET_PREFIX_WRITABLE", prefixWritable: true }
|
||||
]
|
||||
store.dispatch(actionsObjects.setPrefixWritable(true))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
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: alertActions.alertId
|
||||
}
|
||||
}
|
||||
]
|
||||
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",
|
||||
showExpiryDate: true
|
||||
}
|
||||
]
|
||||
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/" },
|
||||
browser: { serverInfo: {} },
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SHARE_OBJECT",
|
||||
show: true,
|
||||
object: "a.txt",
|
||||
url: "https://test.com/bk1/pre1/b.txt",
|
||||
showExpiryDate: true
|
||||
},
|
||||
{
|
||||
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 objects/SET_SHARE_OBJECT when object is shared with public link", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "test-public" },
|
||||
objects: { currentPrefix: "pre1/" },
|
||||
browser: { serverInfo: { info: { domains: ['public.com'] }} },
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SHARE_OBJECT",
|
||||
show: true,
|
||||
object: "a.txt",
|
||||
url: "public.com/test-public/pre1/a.txt",
|
||||
showExpiryDate: false
|
||||
},
|
||||
{
|
||||
type: "alert/SET",
|
||||
alert: {
|
||||
type: "success",
|
||||
message: "Object shared.",
|
||||
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/" },
|
||||
browser: { serverInfo: {} },
|
||||
})
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Download object", () => {
|
||||
it("should download the object non-LoggedIn users", () => {
|
||||
const setLocation = jest.fn()
|
||||
Object.defineProperty(window, "location", {
|
||||
set(url) {
|
||||
setLocation(url)
|
||||
},
|
||||
get() {
|
||||
return {
|
||||
origin: "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
})
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "bk1" },
|
||||
objects: { currentPrefix: "pre1/" }
|
||||
})
|
||||
store.dispatch(actionsObjects.downloadObject("obj1"))
|
||||
const url = `${
|
||||
window.location.origin
|
||||
}${minioBrowserPrefix}/download/bk1/${encodeURI("pre1/obj1")}?token=`
|
||||
expect(setLocation).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it("should download the object for LoggedIn users", () => {
|
||||
const setLocation = jest.fn()
|
||||
Object.defineProperty(window, "location", {
|
||||
set(url) {
|
||||
setLocation(url)
|
||||
},
|
||||
get() {
|
||||
return {
|
||||
origin: "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
})
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "bk1" },
|
||||
objects: { currentPrefix: "pre1/" }
|
||||
})
|
||||
return store.dispatch(actionsObjects.downloadObject("obj1")).then(() => {
|
||||
const url = `${
|
||||
window.location.origin
|
||||
}${minioBrowserPrefix}/download/bk1/${encodeURI(
|
||||
"pre1/obj1"
|
||||
)}?token=test`
|
||||
expect(setLocation).toHaveBeenCalledWith(url)
|
||||
})
|
||||
})
|
||||
|
||||
it("create alert/SET action when CreateUrlToken fails", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "bk1" },
|
||||
objects: { currentPrefix: "pre1/" }
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "alert/SET",
|
||||
alert: {
|
||||
type: "danger",
|
||||
message: "Error in creating token",
|
||||
id: alertActions.alertId
|
||||
}
|
||||
}
|
||||
]
|
||||
return store.dispatch(actionsObjects.downloadObject("obj1")).then(() => {
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 = [
|
||||
{
|
||||
type: "objects/CHECKED_LIST_ADD",
|
||||
object: "obj1"
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.checkObject("obj1"))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("creates objects/CHECKED_LIST_REMOVE action", () => {
|
||||
const store = mockStore()
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/CHECKED_LIST_REMOVE",
|
||||
object: "obj1"
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.uncheckObject("obj1"))
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("creates objects/CHECKED_LIST_RESET action", () => {
|
||||
const store = mockStore()
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/CHECKED_LIST_RESET"
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.resetCheckedList())
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
|
||||
it("should download checked objects", () => {
|
||||
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/", checkedList: ["obj1"] }
|
||||
})
|
||||
return store.dispatch(actionsObjects.downloadCheckedObjects()).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: ["obj1"]
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
148
browser/app/js/objects/__tests__/reducer.test.js
Normal file
148
browser/app/js/objects/__tests__/reducer.test.js
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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"
|
||||
import { SORT_ORDER_ASC, SORT_BY_NAME } from "../../constants"
|
||||
|
||||
describe("objects reducer", () => {
|
||||
it("should return the initial state", () => {
|
||||
const initialState = reducer(undefined, {})
|
||||
expect(initialState).toEqual({
|
||||
list: [],
|
||||
filter: "",
|
||||
listLoading: false,
|
||||
sortBy: "",
|
||||
sortOrder: SORT_ORDER_ASC,
|
||||
currentPrefix: "",
|
||||
prefixWritable: false,
|
||||
shareObject: {
|
||||
show: false,
|
||||
object: "",
|
||||
url: ""
|
||||
},
|
||||
checkedList: []
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle SET_LIST", () => {
|
||||
const newState = reducer(undefined, {
|
||||
type: actions.SET_LIST,
|
||||
objects: [{ name: "obj1" }, { name: "obj2" }]
|
||||
})
|
||||
expect(newState.list).toEqual([{ name: "obj1" }, { name: "obj2" }])
|
||||
})
|
||||
|
||||
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,
|
||||
sortBy: SORT_BY_NAME
|
||||
})
|
||||
expect(newState.sortBy).toEqual(SORT_BY_NAME)
|
||||
})
|
||||
|
||||
it("should handle SET_SORT_ORDER", () => {
|
||||
const newState = reducer(undefined, {
|
||||
type: actions.SET_SORT_ORDER,
|
||||
sortOrder: SORT_ORDER_ASC
|
||||
})
|
||||
expect(newState.sortOrder).toEqual(SORT_ORDER_ASC)
|
||||
})
|
||||
|
||||
it("should handle SET_CURRENT_PREFIX", () => {
|
||||
const newState = reducer(
|
||||
{ currentPrefix: "test1/" },
|
||||
{
|
||||
type: actions.SET_CURRENT_PREFIX,
|
||||
prefix: "test2/"
|
||||
}
|
||||
)
|
||||
expect(newState.currentPrefix).toEqual("test2/")
|
||||
})
|
||||
|
||||
it("should handle SET_PREFIX_WRITABLE", () => {
|
||||
const newState = reducer(undefined, {
|
||||
type: actions.SET_PREFIX_WRITABLE,
|
||||
prefixWritable: true
|
||||
})
|
||||
expect(newState.prefixWritable).toBeTruthy()
|
||||
})
|
||||
|
||||
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"
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle CHECKED_LIST_ADD", () => {
|
||||
const newState = reducer(undefined, {
|
||||
type: actions.CHECKED_LIST_ADD,
|
||||
object: "obj1"
|
||||
})
|
||||
expect(newState.checkedList).toEqual(["obj1"])
|
||||
})
|
||||
|
||||
it("should handle SELECTED_LIST_REMOVE", () => {
|
||||
const newState = reducer(
|
||||
{ checkedList: ["obj1", "obj2"] },
|
||||
{
|
||||
type: actions.CHECKED_LIST_REMOVE,
|
||||
object: "obj1"
|
||||
}
|
||||
)
|
||||
expect(newState.checkedList).toEqual(["obj2"])
|
||||
})
|
||||
|
||||
it("should handle CHECKED_LIST_RESET", () => {
|
||||
const newState = reducer(
|
||||
{ checkedList: ["obj1", "obj2"] },
|
||||
{
|
||||
type: actions.CHECKED_LIST_RESET
|
||||
}
|
||||
)
|
||||
expect(newState.checkedList).toEqual([])
|
||||
})
|
||||
})
|
||||
464
browser/app/js/objects/actions.js
Normal file
464
browser/app/js/objects/actions.js
Normal file
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 web from "../web"
|
||||
import history from "../history"
|
||||
import {
|
||||
sortObjectsByName,
|
||||
sortObjectsBySize,
|
||||
sortObjectsByDate,
|
||||
} from "../utils"
|
||||
import { getCurrentBucket } from "../buckets/selectors"
|
||||
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
||||
import * as alertActions from "../alert/actions"
|
||||
import {
|
||||
minioBrowserPrefix,
|
||||
SORT_BY_NAME,
|
||||
SORT_BY_SIZE,
|
||||
SORT_BY_LAST_MODIFIED,
|
||||
SORT_ORDER_ASC,
|
||||
SORT_ORDER_DESC,
|
||||
} from "../constants"
|
||||
import { getServerInfo, hasServerPublicDomain } from '../browser/selectors'
|
||||
|
||||
export const SET_LIST = "objects/SET_LIST"
|
||||
export const RESET_LIST = "objects/RESET_LIST"
|
||||
export const SET_FILTER = "objects/SET_FILTER"
|
||||
export const APPEND_LIST = "objects/APPEND_LIST"
|
||||
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_PREFIX_WRITABLE = "objects/SET_PREFIX_WRITABLE"
|
||||
export const SET_SHARE_OBJECT = "objects/SET_SHARE_OBJECT"
|
||||
export const CHECKED_LIST_ADD = "objects/CHECKED_LIST_ADD"
|
||||
export const CHECKED_LIST_REMOVE = "objects/CHECKED_LIST_REMOVE"
|
||||
export const CHECKED_LIST_RESET = "objects/CHECKED_LIST_RESET"
|
||||
export const SET_LIST_LOADING = "objects/SET_LIST_LOADING"
|
||||
|
||||
export const setList = (objects) => ({
|
||||
type: SET_LIST,
|
||||
objects,
|
||||
})
|
||||
|
||||
export const resetList = () => ({
|
||||
type: RESET_LIST,
|
||||
})
|
||||
|
||||
export const setFilter = filter => {
|
||||
return {
|
||||
type: SET_FILTER,
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
export const setListLoading = (listLoading) => ({
|
||||
type: SET_LIST_LOADING,
|
||||
listLoading,
|
||||
})
|
||||
|
||||
export const fetchObjects = () => {
|
||||
return function (dispatch, getState) {
|
||||
dispatch(resetList())
|
||||
const {
|
||||
buckets: { currentBucket },
|
||||
objects: { currentPrefix },
|
||||
} = getState()
|
||||
if (currentBucket) {
|
||||
dispatch(setListLoading(true))
|
||||
return web
|
||||
.ListObjects({
|
||||
bucketName: currentBucket,
|
||||
prefix: currentPrefix,
|
||||
})
|
||||
.then((res) => {
|
||||
// we need to check if the bucket name and prefix are the same as
|
||||
// when the request was made before updating the displayed objects
|
||||
if (
|
||||
currentBucket === getCurrentBucket(getState()) &&
|
||||
currentPrefix === getCurrentPrefix(getState())
|
||||
) {
|
||||
let objects = []
|
||||
if (res.objects) {
|
||||
objects = res.objects.map((object) => {
|
||||
return {
|
||||
...object,
|
||||
name: object.name.replace(currentPrefix, ""),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sortBy = SORT_BY_LAST_MODIFIED
|
||||
const sortOrder = SORT_ORDER_DESC
|
||||
dispatch(setSortBy(sortBy))
|
||||
dispatch(setSortOrder(sortOrder))
|
||||
const sortedList = sortObjectsList(objects, sortBy, sortOrder)
|
||||
dispatch(setList(sortedList))
|
||||
|
||||
dispatch(setPrefixWritable(res.writable))
|
||||
dispatch(setListLoading(false))
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (web.LoggedIn()) {
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "danger",
|
||||
message: err.message,
|
||||
autoClear: true,
|
||||
})
|
||||
)
|
||||
dispatch(resetList())
|
||||
} else {
|
||||
history.push("/login")
|
||||
}
|
||||
dispatch(setListLoading(false))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sortObjects = (sortBy) => {
|
||||
return function (dispatch, getState) {
|
||||
const { objects } = getState()
|
||||
let sortOrder = SORT_ORDER_ASC
|
||||
// Reverse sort order if the list is already sorted on same field
|
||||
if (objects.sortBy === sortBy && objects.sortOrder === SORT_ORDER_ASC) {
|
||||
sortOrder = SORT_ORDER_DESC
|
||||
}
|
||||
dispatch(setSortBy(sortBy))
|
||||
dispatch(setSortOrder(sortOrder))
|
||||
const sortedList = sortObjectsList(objects.list, sortBy, sortOrder)
|
||||
dispatch(setList(sortedList))
|
||||
}
|
||||
}
|
||||
|
||||
const sortObjectsList = (list, sortBy, sortOrder) => {
|
||||
switch (sortBy) {
|
||||
case SORT_BY_NAME:
|
||||
return sortObjectsByName(list, sortOrder)
|
||||
case SORT_BY_SIZE:
|
||||
return sortObjectsBySize(list, sortOrder)
|
||||
case SORT_BY_LAST_MODIFIED:
|
||||
return sortObjectsByDate(list, sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
export const setSortBy = (sortBy) => ({
|
||||
type: SET_SORT_BY,
|
||||
sortBy,
|
||||
})
|
||||
|
||||
export const setSortOrder = (sortOrder) => ({
|
||||
type: SET_SORT_ORDER,
|
||||
sortOrder,
|
||||
})
|
||||
|
||||
export const selectPrefix = (prefix) => {
|
||||
return function (dispatch, getState) {
|
||||
dispatch(setCurrentPrefix(prefix))
|
||||
dispatch(fetchObjects())
|
||||
dispatch(resetCheckedList())
|
||||
const currentBucket = getCurrentBucket(getState())
|
||||
history.replace(`/${currentBucket}/${prefix}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const setCurrentPrefix = (prefix) => {
|
||||
return {
|
||||
type: SET_CURRENT_PREFIX,
|
||||
prefix,
|
||||
}
|
||||
}
|
||||
|
||||
export const setPrefixWritable = (prefixWritable) => ({
|
||||
type: SET_PREFIX_WRITABLE,
|
||||
prefixWritable,
|
||||
})
|
||||
|
||||
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 deleteCheckedObjects = () => {
|
||||
return function (dispatch, getState) {
|
||||
const checkedObjects = getCheckedList(getState())
|
||||
for (let i = 0; i < checkedObjects.length; i++) {
|
||||
dispatch(deleteObject(checkedObjects[i]))
|
||||
}
|
||||
dispatch(resetCheckedList())
|
||||
}
|
||||
}
|
||||
|
||||
export const shareObject = (object, days, hours, minutes) => {
|
||||
return function (dispatch, getState) {
|
||||
const hasServerDomain = hasServerPublicDomain(getState())
|
||||
const currentBucket = getCurrentBucket(getState())
|
||||
const currentPrefix = getCurrentPrefix(getState())
|
||||
const objectName = `${currentPrefix}${object}`
|
||||
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||
if (web.LoggedIn()) {
|
||||
return web
|
||||
.GetBucketPolicy({ bucketName: currentBucket, prefix: currentPrefix })
|
||||
.catch(() => ({ policy: null }))
|
||||
.then(({ policy }) => {
|
||||
if (hasServerDomain && ['readonly', 'readwrite'].includes(policy)) {
|
||||
const domain = getServerInfo(getState()).info.domains[0]
|
||||
const url = `${domain}/${currentBucket}/${encodeURI(objectName)}`
|
||||
dispatch(showShareObject(object, url, false))
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "success",
|
||||
message: "Object shared."
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return web
|
||||
.PresignedGet({
|
||||
host: location.host,
|
||||
bucket: currentBucket,
|
||||
object: objectName,
|
||||
expiry: expiry
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((obj) => {
|
||||
if (!obj) return
|
||||
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,
|
||||
})
|
||||
)
|
||||
})
|
||||
} else {
|
||||
dispatch(
|
||||
showShareObject(
|
||||
object,
|
||||
`${location.host}` +
|
||||
"/" +
|
||||
`${currentBucket}` +
|
||||
"/" +
|
||||
encodeURI(objectName)
|
||||
)
|
||||
)
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "success",
|
||||
message: `Object shared.`,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const showShareObject = (object, url, showExpiryDate = true) => ({
|
||||
type: SET_SHARE_OBJECT,
|
||||
show: true,
|
||||
object,
|
||||
url,
|
||||
showExpiryDate,
|
||||
})
|
||||
|
||||
export const hideShareObject = (object, url) => ({
|
||||
type: SET_SHARE_OBJECT,
|
||||
show: false,
|
||||
object: "",
|
||||
url: "",
|
||||
})
|
||||
export const getObjectURL = (object, callback) => {
|
||||
return function (dispatch, getState) {
|
||||
const currentBucket = getCurrentBucket(getState())
|
||||
const currentPrefix = getCurrentPrefix(getState())
|
||||
const objectName = `${currentPrefix}${object}`
|
||||
const encObjectName = encodeURI(objectName)
|
||||
if (web.LoggedIn()) {
|
||||
return web
|
||||
.CreateURLToken()
|
||||
.then((res) => {
|
||||
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${res.token}`
|
||||
callback(url)
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "danger",
|
||||
message: err.message,
|
||||
})
|
||||
)
|
||||
})
|
||||
} else {
|
||||
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
|
||||
callback(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
export const downloadObject = (object) => {
|
||||
return function (dispatch, getState) {
|
||||
const currentBucket = getCurrentBucket(getState())
|
||||
const currentPrefix = getCurrentPrefix(getState())
|
||||
const objectName = `${currentPrefix}${object}`
|
||||
const encObjectName = encodeURI(objectName)
|
||||
if (web.LoggedIn()) {
|
||||
return web
|
||||
.CreateURLToken()
|
||||
.then((res) => {
|
||||
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${res.token}`
|
||||
window.location = url
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "danger",
|
||||
message: err.message,
|
||||
})
|
||||
)
|
||||
})
|
||||
} else {
|
||||
const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
|
||||
window.location = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
export const uncheckObject = (object) => ({
|
||||
type: CHECKED_LIST_REMOVE,
|
||||
object,
|
||||
})
|
||||
|
||||
export const resetCheckedList = () => ({
|
||||
type: CHECKED_LIST_RESET,
|
||||
})
|
||||
|
||||
export const downloadCheckedObjects = () => {
|
||||
return function (dispatch, getState) {
|
||||
return downloadObjects(
|
||||
getCurrentBucket(getState()),
|
||||
getCurrentPrefix(getState()),
|
||||
getCheckedList(getState()),
|
||||
null,
|
||||
dispatch
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadObjects = (bucketName, prefix, objects, filename, dispatch) => {
|
||||
const req = {
|
||||
bucketName: bucketName,
|
||||
prefix: prefix,
|
||||
objects: objects,
|
||||
}
|
||||
if (web.LoggedIn()) {
|
||||
return web
|
||||
.CreateURLToken()
|
||||
.then((res) => {
|
||||
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}`
|
||||
downloadZip(requestUrl, req, filename, dispatch)
|
||||
})
|
||||
.catch((err) =>
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "danger",
|
||||
message: err.message,
|
||||
})
|
||||
)
|
||||
)
|
||||
} else {
|
||||
const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=`
|
||||
downloadZip(requestUrl, req, filename, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadZip = (url, req, filename, dispatch) => {
|
||||
var anchor = document.createElement("a")
|
||||
document.body.appendChild(anchor)
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open("POST", url, true)
|
||||
xhr.responseType = "blob"
|
||||
|
||||
xhr.onload = function (e) {
|
||||
if (this.status == 200) {
|
||||
dispatch(resetCheckedList())
|
||||
var blob = new Blob([this.response], {
|
||||
type: "octet/stream",
|
||||
})
|
||||
var blobUrl = window.URL.createObjectURL(blob)
|
||||
var separator = req.prefix.length > 1 ? "-" : ""
|
||||
|
||||
anchor.href = blobUrl
|
||||
anchor.download = filename ||
|
||||
req.bucketName + separator + req.prefix.slice(0, -1) + ".zip"
|
||||
|
||||
anchor.click()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
anchor.remove()
|
||||
}
|
||||
}
|
||||
xhr.send(JSON.stringify(req))
|
||||
}
|
||||
124
browser/app/js/objects/reducer.js
Normal file
124
browser/app/js/objects/reducer.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 actionsObjects from "./actions"
|
||||
import { SORT_ORDER_ASC } from "../constants"
|
||||
|
||||
const removeObject = (list, objectToRemove, lookup) => {
|
||||
const idx = list.findIndex(object => lookup(object) === objectToRemove)
|
||||
if (idx == -1) {
|
||||
return list
|
||||
}
|
||||
return [...list.slice(0, idx), ...list.slice(idx + 1)]
|
||||
}
|
||||
|
||||
export default (
|
||||
state = {
|
||||
list: [],
|
||||
filter: "",
|
||||
listLoading: false,
|
||||
sortBy: "",
|
||||
sortOrder: SORT_ORDER_ASC,
|
||||
currentPrefix: "",
|
||||
prefixWritable: false,
|
||||
shareObject: {
|
||||
show: false,
|
||||
object: "",
|
||||
url: ""
|
||||
},
|
||||
checkedList: []
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case actionsObjects.SET_LIST:
|
||||
return {
|
||||
...state,
|
||||
list: action.objects
|
||||
}
|
||||
case actionsObjects.RESET_LIST:
|
||||
return {
|
||||
...state,
|
||||
list: []
|
||||
}
|
||||
case actionsObjects.SET_FILTER:
|
||||
return {
|
||||
...state,
|
||||
filter: action.filter
|
||||
}
|
||||
case actionsObjects.SET_LIST_LOADING:
|
||||
return {
|
||||
...state,
|
||||
listLoading: action.listLoading
|
||||
}
|
||||
case actionsObjects.REMOVE:
|
||||
return {
|
||||
...state,
|
||||
list: removeObject(state.list, action.object, object => object.name)
|
||||
}
|
||||
case actionsObjects.SET_SORT_BY:
|
||||
return {
|
||||
...state,
|
||||
sortBy: action.sortBy
|
||||
}
|
||||
case actionsObjects.SET_SORT_ORDER:
|
||||
return {
|
||||
...state,
|
||||
sortOrder: action.sortOrder
|
||||
}
|
||||
case actionsObjects.SET_CURRENT_PREFIX:
|
||||
return {
|
||||
...state,
|
||||
currentPrefix: action.prefix
|
||||
}
|
||||
case actionsObjects.SET_PREFIX_WRITABLE:
|
||||
return {
|
||||
...state,
|
||||
prefixWritable: action.prefixWritable
|
||||
}
|
||||
case actionsObjects.SET_SHARE_OBJECT:
|
||||
return {
|
||||
...state,
|
||||
shareObject: {
|
||||
show: action.show,
|
||||
object: action.object,
|
||||
url: action.url,
|
||||
showExpiryDate: action.showExpiryDate
|
||||
}
|
||||
}
|
||||
case actionsObjects.CHECKED_LIST_ADD:
|
||||
return {
|
||||
...state,
|
||||
checkedList: [...state.checkedList, action.object]
|
||||
}
|
||||
case actionsObjects.CHECKED_LIST_REMOVE:
|
||||
return {
|
||||
...state,
|
||||
checkedList: removeObject(
|
||||
state.checkedList,
|
||||
action.object,
|
||||
object => object
|
||||
)
|
||||
}
|
||||
case actionsObjects.CHECKED_LIST_RESET:
|
||||
return {
|
||||
...state,
|
||||
checkedList: []
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
33
browser/app/js/objects/selectors.js
Normal file
33
browser/app/js/objects/selectors.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 { createSelector } from "reselect"
|
||||
|
||||
export const getCurrentPrefix = state => state.objects.currentPrefix
|
||||
|
||||
export const getCheckedList = state => state.objects.checkedList
|
||||
|
||||
export const getPrefixWritable = state => state.objects.prefixWritable
|
||||
|
||||
const objectsSelector = state => state.objects.list
|
||||
const objectsFilterSelector = state => state.objects.filter
|
||||
|
||||
export const getFilteredObjects = createSelector(
|
||||
objectsSelector,
|
||||
objectsFilterSelector,
|
||||
(objects, filter) => objects.filter(
|
||||
object => object.name.toLowerCase().startsWith(filter.toLowerCase()))
|
||||
)
|
||||
Reference in New Issue
Block a user