diff --git a/browser/app/js/mime.js b/browser/app/js/mime.js
index ccf4bfaa0..b7160be0b 100644
--- a/browser/app/js/mime.js
+++ b/browser/app/js/mime.js
@@ -14,30 +14,67 @@
* limitations under the License.
*/
-import mimedb from 'mime-types'
+import mimedb from "mime-types"
const isFolder = (name, contentType) => {
- if (name.endsWith('/')) return true
+ if (name.endsWith("/")) return true
return false
}
const isPdf = (name, contentType) => {
- if (contentType === 'application/pdf') return true
+ if (contentType === "application/pdf") return true
+ return false
+}
+const isImage = (name, contentType) => {
+ if (
+ contentType === "image/jpeg" ||
+ contentType === "image/gif" ||
+ contentType === "image/x-icon" ||
+ contentType === "image/png" ||
+ contentType === "image/svg+xml" ||
+ contentType === "image/tiff" ||
+ contentType === "image/webp"
+ )
+ return true
return false
}
const isZip = (name, contentType) => {
- if (!contentType || !contentType.includes('/')) return false
- if (contentType.split('/')[1].includes('zip')) return true
+ if (!contentType || !contentType.includes("/")) return false
+ if (contentType.split("/")[1].includes("zip")) return true
return false
}
const isCode = (name, contentType) => {
- const codeExt = ['c', 'cpp', 'go', 'py', 'java', 'rb', 'js', 'pl', 'fs',
- 'php', 'css', 'less', 'scss', 'coffee', 'net', 'html',
- 'rs', 'exs', 'scala', 'hs', 'clj', 'el', 'scm', 'lisp',
- 'asp', 'aspx']
- const ext = name.split('.').reverse()[0]
+ const codeExt = [
+ "c",
+ "cpp",
+ "go",
+ "py",
+ "java",
+ "rb",
+ "js",
+ "pl",
+ "fs",
+ "php",
+ "css",
+ "less",
+ "scss",
+ "coffee",
+ "net",
+ "html",
+ "rs",
+ "exs",
+ "scala",
+ "hs",
+ "clj",
+ "el",
+ "scm",
+ "lisp",
+ "asp",
+ "aspx",
+ ]
+ const ext = name.split(".").reverse()[0]
for (var i in codeExt) {
if (ext === codeExt[i]) return true
}
@@ -45,9 +82,9 @@ const isCode = (name, contentType) => {
}
const isExcel = (name, contentType) => {
- if (!contentType || !contentType.includes('/')) return false
- const types = ['excel', 'spreadsheet']
- const subType = contentType.split('/')[1]
+ if (!contentType || !contentType.includes("/")) return false
+ const types = ["excel", "spreadsheet"]
+ const subType = contentType.split("/")[1]
for (var i in types) {
if (subType.includes(types[i])) return true
}
@@ -55,9 +92,9 @@ const isExcel = (name, contentType) => {
}
const isDoc = (name, contentType) => {
- if (!contentType || !contentType.includes('/')) return false
- const types = ['word', '.document']
- const subType = contentType.split('/')[1]
+ if (!contentType || !contentType.includes("/")) return false
+ const types = ["word", ".document"]
+ const subType = contentType.split("/")[1]
for (var i in types) {
if (subType.includes(types[i])) return true
}
@@ -65,9 +102,9 @@ const isDoc = (name, contentType) => {
}
const isPresentation = (name, contentType) => {
- if (!contentType || !contentType.includes('/')) return false
- var types = ['powerpoint', 'presentation']
- const subType = contentType.split('/')[1]
+ if (!contentType || !contentType.includes("/")) return false
+ var types = ["powerpoint", "presentation"]
+ const subType = contentType.split("/")[1]
for (var i in types) {
if (subType.includes(types[i])) return true
}
@@ -76,31 +113,32 @@ const isPresentation = (name, contentType) => {
const typeToIcon = (type) => {
return (name, contentType) => {
- if (!contentType || !contentType.includes('/')) return false
- if (contentType.split('/')[0] === type) return true
+ if (!contentType || !contentType.includes("/")) return false
+ if (contentType.split("/")[0] === type) return true
return false
}
}
export const getDataType = (name, contentType) => {
if (contentType === "") {
- contentType = mimedb.lookup(name) || 'application/octet-stream'
+ contentType = mimedb.lookup(name) || "application/octet-stream"
}
const check = [
- ['folder', isFolder],
- ['code', isCode],
- ['audio', typeToIcon('audio')],
- ['image', typeToIcon('image')],
- ['video', typeToIcon('video')],
- ['text', typeToIcon('text')],
- ['pdf', isPdf],
- ['zip', isZip],
- ['excel', isExcel],
- ['doc', isDoc],
- ['presentation', isPresentation]
+ ["folder", isFolder],
+ ["code", isCode],
+ ["audio", typeToIcon("audio")],
+ ["image", typeToIcon("image")],
+ ["video", typeToIcon("video")],
+ ["text", typeToIcon("text")],
+ ["pdf", isPdf],
+ ["image", isImage],
+ ["zip", isZip],
+ ["excel", isExcel],
+ ["doc", isDoc],
+ ["presentation", isPresentation],
]
for (var i in check) {
if (check[i][1](name, contentType)) return check[i][0]
}
- return 'other'
+ return "other"
}
diff --git a/browser/app/js/objects/ObjectActions.js b/browser/app/js/objects/ObjectActions.js
index 394f84613..a1660a630 100644
--- a/browser/app/js/objects/ObjectActions.js
+++ b/browser/app/js/objects/ObjectActions.js
@@ -19,18 +19,22 @@ 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
+ SHARE_OBJECT_EXPIRY_MINUTES,
} from "../constants"
export class ObjectActions extends React.Component {
constructor(props) {
super(props)
this.state = {
- showDeleteConfirmation: false
+ showDeleteConfirmation: false,
+ showPreview: false,
}
}
shareObject(e) {
@@ -53,7 +57,20 @@ export class ObjectActions extends React.Component {
}
hideDeleteConfirmModal() {
this.setState({
- showDeleteConfirmation: false
+ 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() {
@@ -69,6 +86,15 @@ export class ObjectActions extends React.Component {
>
+ {getDataType(object.name, object.contentType) == "image" && (
+
+
+
+ )}
- {(showShareObjectModal && shareObjectName === object.name) &&
- }
+ {showShareObjectModal && shareObjectName === object.name && (
+
+ )}
{this.state.showDeleteConfirmation && (
)}
+ {this.state.showPreview && (
+
+ )}
)
}
@@ -94,15 +128,17 @@ const mapStateToProps = (state, ownProps) => {
return {
object: ownProps.object,
showShareObjectModal: state.objects.shareObject.show,
- shareObjectName: state.objects.shareObject.object
+ shareObjectName: state.objects.shareObject.object,
}
}
-const mapDispatchToProps = dispatch => {
+const mapDispatchToProps = (dispatch) => {
return {
shareObject: (object, days, hours, minutes) =>
dispatch(objectsActions.shareObject(object, days, hours, minutes)),
- deleteObject: object => dispatch(objectsActions.deleteObject(object))
+ deleteObject: (object) => dispatch(objectsActions.deleteObject(object)),
+ getObjectURL: (object, callback) =>
+ dispatch(objectsActions.getObjectURL(object, callback)),
}
}
diff --git a/browser/app/js/objects/PreviewObjectModal.js b/browser/app/js/objects/PreviewObjectModal.js
new file mode 100644
index 000000000..12b4e016b
--- /dev/null
+++ b/browser/app/js/objects/PreviewObjectModal.js
@@ -0,0 +1,65 @@
+/*
+ * MinIO Cloud Storage (C) 2020 MinIO, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+import { Modal, ModalHeader, ModalBody } from "react-bootstrap"
+
+class PreviewObjectModal extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ url: "",
+ }
+ props.getObjectURL(props.object.name, (url) => {
+ this.setState({
+ url: url,
+ })
+ })
+ }
+
+ render() {
+ const { hidePreviewModal } = this.props
+ return (
+
+ Object Preview
+
+
+ {this.state.url && (
+
+ )}
+
+
+
+ {
+
+ }
+
+
+ )
+ }
+}
+export default PreviewObjectModal
diff --git a/browser/app/js/objects/__tests__/ObjectActions.test.js b/browser/app/js/objects/__tests__/ObjectActions.test.js
index 6c90c02c4..7339b1a5d 100644
--- a/browser/app/js/objects/__tests__/ObjectActions.test.js
+++ b/browser/app/js/objects/__tests__/ObjectActions.test.js
@@ -66,6 +66,49 @@ describe("ObjectActions", () => {
expect(deleteObject).toHaveBeenCalledWith("obj1")
})
+
+
+
+ it("should show PreviewObjectModal when preview action is clicked", () => {
+ const wrapper = shallow(
+
+ )
+ 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(
+
+ )
+ 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(
+
+ )
+ expect(wrapper
+ .find("a")
+ .length).toBe(2) // find only the other 2
+ })
+
it("should call shareObject with object and expiry", () => {
const shareObject = jest.fn()
const wrapper = shallow(
diff --git a/browser/app/js/objects/actions.js b/browser/app/js/objects/actions.js
index c1ea37def..371beac27 100644
--- a/browser/app/js/objects/actions.js
+++ b/browser/app/js/objects/actions.js
@@ -19,7 +19,7 @@ import history from "../history"
import {
sortObjectsByName,
sortObjectsBySize,
- sortObjectsByDate
+ sortObjectsByDate,
} from "../utils"
import { getCurrentBucket } from "../buckets/selectors"
import { getCurrentPrefix, getCheckedList } from "./selectors"
@@ -31,7 +31,7 @@ import {
SORT_BY_SIZE,
SORT_BY_LAST_MODIFIED,
SORT_ORDER_ASC,
- SORT_ORDER_DESC
+ SORT_ORDER_DESC,
} from "../constants"
export const SET_LIST = "objects/SET_LIST"
@@ -48,35 +48,35 @@ 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 => ({
+export const setList = (objects) => ({
type: SET_LIST,
- objects
+ objects,
})
export const resetList = () => ({
- type: RESET_LIST
+ type: RESET_LIST,
})
-export const setListLoading = listLoading => ({
+export const setListLoading = (listLoading) => ({
type: SET_LIST_LOADING,
- listLoading
+ listLoading,
})
export const fetchObjects = () => {
- return function(dispatch, getState) {
+ return function (dispatch, getState) {
dispatch(resetList())
const {
buckets: { currentBucket },
- objects: { currentPrefix }
+ objects: { currentPrefix },
} = getState()
if (currentBucket) {
dispatch(setListLoading(true))
return web
.ListObjects({
bucketName: currentBucket,
- prefix: currentPrefix
+ prefix: currentPrefix,
})
- .then(res => {
+ .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 (
@@ -85,10 +85,10 @@ export const fetchObjects = () => {
) {
let objects = []
if (res.objects) {
- objects = res.objects.map(object => {
+ objects = res.objects.map((object) => {
return {
...object,
- name: object.name.replace(currentPrefix, "")
+ name: object.name.replace(currentPrefix, ""),
}
})
}
@@ -104,13 +104,13 @@ export const fetchObjects = () => {
dispatch(setListLoading(false))
}
})
- .catch(err => {
+ .catch((err) => {
if (web.LoggedIn()) {
dispatch(
alertActions.set({
type: "danger",
message: err.message,
- autoClear: true
+ autoClear: true,
})
)
dispatch(resetList())
@@ -123,8 +123,8 @@ export const fetchObjects = () => {
}
}
-export const sortObjects = sortBy => {
- return function(dispatch, getState) {
+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
@@ -149,18 +149,18 @@ const sortObjectsList = (list, sortBy, sortOrder) => {
}
}
-export const setSortBy = sortBy => ({
+export const setSortBy = (sortBy) => ({
type: SET_SORT_BY,
- sortBy
+ sortBy,
})
-export const setSortOrder = sortOrder => ({
+export const setSortOrder = (sortOrder) => ({
type: SET_SORT_ORDER,
- sortOrder
+ sortOrder,
})
-export const selectPrefix = prefix => {
- return function(dispatch, getState) {
+export const selectPrefix = (prefix) => {
+ return function (dispatch, getState) {
dispatch(setCurrentPrefix(prefix))
dispatch(fetchObjects())
dispatch(resetCheckedList())
@@ -169,49 +169,49 @@ export const selectPrefix = prefix => {
}
}
-export const setCurrentPrefix = prefix => {
+export const setCurrentPrefix = (prefix) => {
return {
type: SET_CURRENT_PREFIX,
- prefix
+ prefix,
}
}
-export const setPrefixWritable = prefixWritable => ({
+export const setPrefixWritable = (prefixWritable) => ({
type: SET_PREFIX_WRITABLE,
- prefixWritable
+ prefixWritable,
})
-export const deleteObject = object => {
- return function(dispatch, getState) {
+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]
+ objects: [objectName],
})
.then(() => {
dispatch(removeObject(object))
})
- .catch(e => {
+ .catch((e) => {
dispatch(
alertActions.set({
type: "danger",
- message: e.message
+ message: e.message,
})
)
})
}
}
-export const removeObject = object => ({
+export const removeObject = (object) => ({
type: REMOVE,
- object
+ object,
})
export const deleteCheckedObjects = () => {
- return function(dispatch, getState) {
+ return function (dispatch, getState) {
const checkedObjects = getCheckedList(getState())
for (let i = 0; i < checkedObjects.length; i++) {
dispatch(deleteObject(checkedObjects[i]))
@@ -221,7 +221,7 @@ export const deleteCheckedObjects = () => {
}
export const shareObject = (object, days, hours, minutes) => {
- return function(dispatch, getState) {
+ return function (dispatch, getState) {
const currentBucket = getCurrentBucket(getState())
const currentPrefix = getCurrentPrefix(getState())
const objectName = `${currentPrefix}${object}`
@@ -232,22 +232,22 @@ export const shareObject = (object, days, hours, minutes) => {
host: location.host,
bucket: currentBucket,
object: objectName,
- expiry: expiry
+ expiry: expiry,
})
- .then(obj => {
+ .then((obj) => {
dispatch(showShareObject(object, obj.url))
dispatch(
alertActions.set({
type: "success",
- message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`
+ message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes`,
})
)
})
- .catch(err => {
+ .catch((err) => {
dispatch(
alertActions.set({
type: "danger",
- message: err.message
+ message: err.message,
})
)
})
@@ -265,7 +265,7 @@ export const shareObject = (object, days, hours, minutes) => {
dispatch(
alertActions.set({
type: "success",
- message: `Object shared.`
+ message: `Object shared.`,
})
)
}
@@ -276,18 +276,44 @@ export const showShareObject = (object, url) => ({
type: SET_SHARE_OBJECT,
show: true,
object,
- url
+ url,
})
export const hideShareObject = (object, url) => ({
type: SET_SHARE_OBJECT,
show: false,
object: "",
- url: ""
+ url: "",
})
-
-export const downloadObject = object => {
- return function(dispatch, getState) {
+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}`
@@ -295,52 +321,46 @@ export const downloadObject = object => {
if (web.LoggedIn()) {
return web
.CreateURLToken()
- .then(res => {
- const url = `${
- window.location.origin
- }${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${
- res.token
- }`
+ .then((res) => {
+ const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=${res.token}`
window.location = url
})
- .catch(err => {
+ .catch((err) => {
dispatch(
alertActions.set({
type: "danger",
- message: err.message
+ message: err.message,
})
)
})
} else {
- const url = `${
- window.location.origin
- }${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
+ const url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encObjectName}?token=`
window.location = url
}
}
}
-export const checkObject = object => ({
+export const checkObject = (object) => ({
type: CHECKED_LIST_ADD,
- object
+ object,
})
-export const uncheckObject = object => ({
+export const uncheckObject = (object) => ({
type: CHECKED_LIST_REMOVE,
- object
+ object,
})
export const resetCheckedList = () => ({
- type: CHECKED_LIST_RESET
+ type: CHECKED_LIST_RESET,
})
export const downloadCheckedObjects = () => {
- return function(dispatch, getState) {
+ return function (dispatch, getState) {
const state = getState()
const req = {
bucketName: getCurrentBucket(state),
prefix: getCurrentPrefix(state),
- objects: getCheckedList(state)
+ objects: getCheckedList(state),
}
if (!web.LoggedIn()) {
const requestUrl = location.origin + "/minio/zip?token="
@@ -348,17 +368,15 @@ export const downloadCheckedObjects = () => {
} else {
return web
.CreateURLToken()
- .then(res => {
- const requestUrl = `${
- location.origin
- }${minioBrowserPrefix}/zip?token=${res.token}`
+ .then((res) => {
+ const requestUrl = `${location.origin}${minioBrowserPrefix}/zip?token=${res.token}`
downloadZip(requestUrl, req, dispatch)
})
- .catch(err =>
+ .catch((err) =>
dispatch(
alertActions.set({
type: "danger",
- message: err.message
+ message: err.message,
})
)
)
@@ -374,11 +392,11 @@ const downloadZip = (url, req, dispatch) => {
xhr.open("POST", url, true)
xhr.responseType = "blob"
- xhr.onload = function(e) {
+ xhr.onload = function (e) {
if (this.status == 200) {
dispatch(resetCheckedList())
var blob = new Blob([this.response], {
- type: "octet/stream"
+ type: "octet/stream",
})
var blobUrl = window.URL.createObjectURL(blob)
var separator = req.prefix.length > 1 ? "-" : ""