upgrading react and refactoring components (#5409) (#5444)

- upgraded react from v16.2.0
- upgraded react-router to v4.2.0 and re-writen the routes
- using prettier to format the code
- added jest to unit test components/reducers/selectors

This provides a skeleton to start of with. Only basic unit test
cases are added, remaining needs to be added.

In terms of functionality, it provides login, listing and searching
buckets. Remaining functionalities will be added in upcoming patches.
master
Kanagaraj M 7 years ago committed by Harshavardhana
parent 76be54551f
commit 9ab6a8035f
  1. 2
      browser/.editorconfig
  2. 3
      browser/.prettierrc
  3. 2
      browser/app/css/loader.css
  4. BIN
      browser/app/img/favicon.ico
  5. 67
      browser/app/img/logo.svg
  6. 131
      browser/app/index.js
  7. 40
      browser/app/js/__tests__/jsonrpc-test.js
  8. 695
      browser/app/js/actions.js
  9. 57
      browser/app/js/actions/__tests__/alert.test.js
  10. 41
      browser/app/js/actions/__tests__/buckets.test.js
  11. 46
      browser/app/js/actions/alert.js
  12. 51
      browser/app/js/actions/buckets.js
  13. 30
      browser/app/js/components/Alert.js
  14. 46
      browser/app/js/components/App.js
  15. 869
      browser/app/js/components/Browse.js
  16. 36
      browser/app/js/components/Browser.js
  17. 43
      browser/app/js/components/Bucket.js
  18. 35
      browser/app/js/components/BucketContainer.js
  19. 59
      browser/app/js/components/BucketList.js
  20. 44
      browser/app/js/components/BucketSearch.js
  21. 26
      browser/app/js/components/Host.js
  22. 129
      browser/app/js/components/Login.js
  23. 93
      browser/app/js/components/SideBar.js
  24. 39
      browser/app/js/components/__tests__/Bucket.test.js
  25. 29
      browser/app/js/components/__tests__/BucketList.test.js
  26. 32
      browser/app/js/components/__tests__/BucketSearch.test.js
  27. 54
      browser/app/js/components/__tests__/Login-test.js
  28. 21
      browser/app/js/jest/setup.js
  29. 30
      browser/app/js/loader.js
  30. 215
      browser/app/js/reducers.js
  31. 87
      browser/app/js/reducers/__tests__/alert.test.js
  32. 64
      browser/app/js/reducers/__tests__/buckets.test.js
  33. 41
      browser/app/js/reducers/alert.js
  34. 42
      browser/app/js/reducers/buckets.js
  35. 26
      browser/app/js/reducers/index.js
  36. 38
      browser/app/js/selectors/__tests__/buckets.test.js
  37. 28
      browser/app/js/selectors/buckets.js
  38. 26
      browser/app/js/store/configure-store.js
  39. 15
      browser/app/js/web.js
  40. 46
      browser/package.json
  41. 2518
      browser/yarn.lock

@ -3,7 +3,7 @@ root = true
[*]
indent_style = space
indent_size = 4
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

@ -0,0 +1,3 @@
{
"semi": false
}

@ -54,7 +54,7 @@
.pl-inner > img {
width: 30px;
margin-top: 28px;
margin-top: 21px;
}
@-webkit-keyframes fade-in {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,57 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewbox="0 0 160 256"
version="1.1"
id="svg3092"
height="218.14844"
width="137">
<defs
id="defs3094" />
<metadata
id="metadata3097">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0.99999967,-982.85149)"
id="layer1">
<g
transform="matrix(1.0112586,0,0,1.0112586,5.4603732,-13.223714)"
id="g4144">
<g
id="g4140">
<g
style="image-rendering:auto"
id="minio"
transform="matrix(1.0000023,0,0,0.99999799,-739.31646,295.2269)">
<title
id="title3337">Minio Logo</title>
<path
d="m 803.42903,801.80813 c 12.40802,4.17067 27.0499,9.11665 37.95186,12.80906 -12.01295,-21.20683 -27.84305,-34.11687 -37.95186,-40.78578 l 0,27.97672 z m 0,93.72113 -14.22303,8.96217 0,-92.45864 c -1.52985,-0.5139 -2.97948,-0.9981 -4.33405,-1.45259 -19.98593,-6.67189 -32.7207,-17.95703 -35.85168,-31.77904 -2.54577,-11.21386 1.55064,-23.02184 11.24654,-32.39691 8.84929,-8.55225 21.22761,-18.39964 31.17304,-26.31619 3.60329,-2.86658 6.73129,-5.3173 9.2028,-7.39669 2.31406,-1.93977 1.61598,-4.95488 0.57033,-6.21441 -1.74073,-2.09127 -4.61921,-1.74669 -6.56195,-0.32379 -0.10398,0.0802 -5.65595,4.40832 -5.65595,4.40832 l -8.58195,-11.57033 c 0,0 5.60843,-4.14096 5.8223,-4.30137 8.39777,-6.155 19.54034,-4.98758 25.92406,2.71509 3.19039,3.84093 4.68459,8.69779 4.20929,13.67051 -0.47232,4.9549 -2.84579,9.43153 -6.68078,12.5922 -2.58439,2.12988 -5.73912,4.64298 -9.39291,7.54522 -9.70779,7.72641 -21.78905,17.33915 -30.14226,25.41907 -6.1253,5.9233 -8.70671,12.67834 -7.26896,19.02345 1.9873,8.75424 11.33268,16.34105 26.32213,21.37911 l 0,-46.22486 c 40.29563,13.62298 68.76248,61.22321 78.20589,87.64039 0,0 -41.76308,-14.15768 -63.98286,-21.64051 l 0,78.7198"
style="fill:#f8f8f8;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path48" />
<path
d="m 803.42903,826.12513 -14.22303,-4.78261 0,-9.30973 14.22303,4.77667 0,9.31567"
style="fill:#cdccca;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path50" />
<path
d="m 734.75566,745.06155 c -0.23469,0.16636 -0.54956,0.14853 -0.73077,-0.0743 -0.17823,-0.22576 -0.13063,-0.53766 0.0802,-0.73373 4.93113,-4.51525 24.45661,-23.86844 46.30805,-45.2624 l 8.58193,11.57033 c 0,0 -53.54135,34.01288 -54.23942,34.50007"
style="fill:#f14621;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path52" />
<?xml version="1.0" encoding="UTF-8"?>
<svg width="302px" height="579px" viewBox="0 0 302 579" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Untitled 2</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Minio_Logo_White" fill-rule="nonzero">
<g id="Group">
<path d="M225.835,28.882 C225.835,28.882 282.569,120.259 301.531,152.735 C301.837,153.264 301.707,153.963 301.251,154.419 C300.676,154.994 299.81,155.056 299.24,154.481 L201.484,52.243 L225.835,28.882 Z" id="Shape" fill="#F15A29"></path>
<path d="M58.706,361.854 C75.299,326.75 97.681,294.989 125.38,267.29 C135.915,256.76 147.036,247.002 158.701,238.037 L158.701,310.385 L58.706,361.854 Z M0.971,428.672 L158.7,348.375 L158.7,531.932 L194.244,578.623 L194.244,330.092 L215.833,318.987 C225.778,313.991 235.105,307.343 243.402,299.041 C284.517,257.931 285.17,191.574 245.226,149.749 L163.742,64.855 C160.493,61.383 158.788,56.838 158.938,52.06 C159.088,47.251 161.099,42.784 164.597,39.488 C171.811,32.704 183.305,33.161 190.088,40.369 L201.484,52.247 L225.835,28.881 C197.064,-8.431 161.861,-3.746 141.47,14.92 C131.417,24.383 125.644,37.198 125.208,51.003 C124.773,64.829 129.753,77.981 139.221,88.04 L139.33,88.159 L221.239,173.483 C248.176,202.151 247.637,247.235 219.617,275.255 C215.248,279.623 210.46,283.324 205.387,286.36 L194.245,292.091 L194.245,176.126 C160.167,193.688 129.09,216.251 101.692,243.655 C67.568,277.774 40.776,317.516 22.058,361.772 C12.875,383.479 5.837,405.829 0.971,428.672 Z" id="Shape" fill="#FFFFFF"></path>
<polygon id="Shape" fill="#BCBEC0" points="194.246 330.09 194.246 367.609 158.701 385.648 158.701 348.373"></polygon>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -1,5 +1,5 @@
/*
* Minio Cloud Storage (C) 2016 Minio, Inc.
* Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,106 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'babel-polyfill'
import './less/main.less'
import React from 'react'
import ReactDOM from 'react-dom'
import thunkMiddleware from 'redux-thunk'
import createStore from 'redux/lib/createStore'
import applyMiddleware from 'redux/lib/applyMiddleware'
import Route from 'react-router/lib/Route'
import Router from 'react-router/lib/Router'
import browserHistory from 'react-router/lib/browserHistory'
import IndexRoute from 'react-router/lib/IndexRoute'
import Provider from 'react-redux/lib/components/Provider'
import connect from 'react-redux/lib/components/connect'
import Moment from 'moment'
import { minioBrowserPrefix } from './js/constants.js'
import * as actions from './js/actions.js'
import reducer from './js/reducers.js'
import _Login from './js/components/Login.js'
import _Browse from './js/components/Browse.js'
import fontAwesome from 'font-awesome/css/font-awesome.css'
import MaterialDesignIconicFonts from 'material-design-iconic-font/dist/css/material-design-iconic-font.min.css'
import Web from './js/web'
window.Web = Web
import storage from 'local-storage-fallback'
const store = applyMiddleware(thunkMiddleware)(createStore)(reducer)
const Browse = connect(state => state)(_Browse)
const Login = connect(state => state)(_Login)
let web = new Web(`${window.location.protocol}//${window.location.host}${minioBrowserPrefix}/webrpc`, store.dispatch)
window.web = web
store.dispatch(actions.setWeb(web))
function authNeeded(nextState, replace, cb) {
if (web.LoggedIn()) {
return cb()
}
if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') {
replace(`${minioBrowserPrefix}/login`)
}
return cb()
}
function authNotNeeded(nextState, replace) {
if (web.LoggedIn()) {
replace(`${minioBrowserPrefix}`)
}
}
const App = (props) => {
return <div>
{ props.children }
</div>
}
ReactDOM.render((
<Provider store={ store } web={ web }>
<Router history={ browserHistory }>
<Route path='/' component={ App }>
<Route path={ minioBrowserPrefix } component={ App }>
<IndexRoute component={ Browse } onEnter={ authNeeded } />
<Route path='login' component={ Login } onEnter={ authNotNeeded } />
<Route path=':bucket' component={ Browse } onEnter={ authNeeded } />
<Route path=':bucket/*' component={ Browse } onEnter={ authNeeded } />
</Route>
</Route>
</Router>
</Provider>
), document.getElementById('root'))
//Page loader
let delay = [0, 400]
let i = 0
function handleLoader() {
if (i < 2) {
setTimeout(function() {
document.querySelector('.page-load').classList.add('pl-' + i)
i++
handleLoader()
}, delay[i])
}
}
handleLoader()
if (storage.getItem('newlyUpdated')) {
store.dispatch(actions.showAlert({
type: 'success',
message: "Updated to the latest UI Version."
}))
storage.removeItem('newlyUpdated')
}
import "babel-polyfill"
import "./less/main.less"
import "font-awesome/css/font-awesome.css"
import "material-design-iconic-font/dist/css/material-design-iconic-font.min.css"
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter, Route } from "react-router-dom"
import { Provider } from "react-redux"
import { minioBrowserPrefix } from "./js/constants"
import configureStore from "./js/store/configure-store"
import hideLoader from "./js/loader"
import App from "./js/components/App"
const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Route path={minioBrowserPrefix} component={App} />
</BrowserRouter>
</Provider>,
document.getElementById("root")
)
hideLoader()

@ -14,30 +14,28 @@
* limitations under the License.
*/
import expect from 'expect';
import JSONrpc from '../jsonrpc';
import JSONrpc from "../jsonrpc"
describe('jsonrpc', () => {
it('should fail with invalid endpoint', (done) => {
describe("jsonrpc", () => {
it("should fail with invalid endpoint", done => {
try {
let jsonRPC = new JSONrpc({
endpoint: 'htt://localhost:9000',
namespace: 'Test'
});
endpoint: "htt://localhost:9000",
namespace: "Test"
})
} catch (e) {
done();
done()
}
});
it('should succeed with valid endpoint', () => {
})
it("should succeed with valid endpoint", () => {
let jsonRPC = new JSONrpc({
endpoint: 'http://localhost:9000/webrpc',
namespace: 'Test'
});
expect(jsonRPC.version).toEqual('2.0');
expect(jsonRPC.host).toEqual('localhost');
expect(jsonRPC.port).toEqual('9000');
expect(jsonRPC.path).toEqual('/webrpc');
expect(jsonRPC.scheme).toEqual('http');
});
});
endpoint: "http://localhost:9000/webrpc",
namespace: "Test"
})
expect(jsonRPC.version).toEqual("2.0")
expect(jsonRPC.host).toEqual("localhost")
expect(jsonRPC.port).toEqual("9000")
expect(jsonRPC.path).toEqual("/webrpc")
expect(jsonRPC.scheme).toEqual("http")
})
})

@ -1,695 +0,0 @@
/*
* Minio Cloud Storage (C) 2016 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 Moment from 'moment'
import browserHistory from 'react-router/lib/browserHistory'
import storage from 'local-storage-fallback'
import { minioBrowserPrefix } from './constants'
export const SET_WEB = 'SET_WEB'
export const SET_CURRENT_BUCKET = 'SET_CURRENT_BUCKET'
export const SET_CURRENT_PATH = 'SET_CURRENT_PATH'
export const SET_BUCKETS = 'SET_BUCKETS'
export const ADD_BUCKET = 'ADD_BUCKET'
export const REMOVE_BUCKET = 'REMOVE_BUCKET'
export const SHOW_BUCKET_DROPDOWN = 'SHOW_BUCKET_DROPDOWN'
export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS'
export const SET_OBJECTS = 'SET_OBJECTS'
export const APPEND_OBJECTS = 'APPEND_OBJECTS'
export const RESET_OBJECTS = 'RESET_OBJECTS'
export const SET_STORAGE_INFO = 'SET_STORAGE_INFO'
export const SET_SERVER_INFO = 'SET_SERVER_INFO'
export const SHOW_MAKEBUCKET_MODAL = 'SHOW_MAKEBUCKET_MODAL'
export const ADD_UPLOAD = 'ADD_UPLOAD'
export const STOP_UPLOAD = 'STOP_UPLOAD'
export const UPLOAD_PROGRESS = 'UPLOAD_PROGRESS'
export const SET_ALERT = 'SET_ALERT'
export const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR'
export const SET_SHOW_ABORT_MODAL = 'SET_SHOW_ABORT_MODAL'
export const SHOW_ABOUT = 'SHOW_ABOUT'
export const SET_SORT_NAME_ORDER = 'SET_SORT_NAME_ORDER'
export const SET_SORT_SIZE_ORDER = 'SET_SORT_SIZE_ORDER'
export const SET_SORT_DATE_ORDER = 'SET_SORT_DATE_ORDER'
export const SET_LATEST_UI_VERSION = 'SET_LATEST_UI_VERSION'
export const SET_SIDEBAR_STATUS = 'SET_SIDEBAR_STATUS'
export const SET_LOGIN_REDIRECT_PATH = 'SET_LOGIN_REDIRECT_PATH'
export const SET_LOAD_BUCKET = 'SET_LOAD_BUCKET'
export const SET_LOAD_PATH = 'SET_LOAD_PATH'
export const SHOW_SETTINGS = 'SHOW_SETTINGS'
export const SET_SETTINGS = 'SET_SETTINGS'
export const SHOW_BUCKET_POLICY = 'SHOW_BUCKET_POLICY'
export const SET_POLICIES = 'SET_POLICIES'
export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
export const REMOVE_OBJECT = 'REMOVE_OBJECT'
export const CHECKED_OBJECTS_ADD = 'CHECKED_OBJECTS_ADD'
export const CHECKED_OBJECTS_REMOVE = 'CHECKED_OBJECTS_REMOVE'
export const CHECKED_OBJECTS_RESET = 'CHECKED_OBJECTS_RESET'
export const showDeleteConfirmation = (object) => {
return {
type: DELETE_CONFIRMATION,
payload: {
object,
show: true
}
}
}
export const hideDeleteConfirmation = () => {
return {
type: DELETE_CONFIRMATION,
payload: {
object: '',
show: false
}
}
}
export const showShareObject = (object, url) => {
return {
type: SET_SHARE_OBJECT,
shareObject: {
object,
url,
show: true
}
}
}
export const hideShareObject = () => {
return {
type: SET_SHARE_OBJECT,
shareObject: {
url: '',
show: false
}
}
}
export const shareObject = (object, days, hours, minutes) => (dispatch, getState) => {
const {currentBucket, web} = getState()
let host = location.host
let bucket = currentBucket
if (!web.LoggedIn()) {
dispatch(showShareObject(object, `${host}/${bucket}/${object}`))
return
}
let expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
web.PresignedGet({
host,
bucket,
object,
expiry
})
.then(obj => {
dispatch(showShareObject(object, obj.url))
dispatch(showAlert({
type: 'success',
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes.`
}))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
})
}
export const setLoginRedirectPath = (path) => {
return {
type: SET_LOGIN_REDIRECT_PATH,
path
}
}
export const setLoadPath = (loadPath) => {
return {
type: SET_LOAD_PATH,
loadPath
}
}
export const setLoadBucket = (loadBucket) => {
return {
type: SET_LOAD_BUCKET,
loadBucket
}
}
export const setWeb = web => {
return {
type: SET_WEB,
web
}
}
export const setBuckets = buckets => {
return {
type: SET_BUCKETS,
buckets
}
}
export const addBucket = bucket => {
return {
type: ADD_BUCKET,
bucket
}
}
export const removeBucket = bucket => {
return {
type: REMOVE_BUCKET,
bucket
}
}
export const showBucketDropdown = bucket => {
return {
type: SHOW_BUCKET_DROPDOWN,
showBucketDropdown: true
}
}
export const hideBucketDropdown = bucket => {
return {
type: SHOW_BUCKET_DROPDOWN,
showBucketDropdown: false
}
}
export const showMakeBucketModal = () => {
return {
type: SHOW_MAKEBUCKET_MODAL,
showMakeBucketModal: true
}
}
export const hideAlert = () => {
return {
type: SET_ALERT,
alert: {
show: false,
message: '',
type: ''
}
}
}
export const showAlert = alert => {
return (dispatch, getState) => {
let alertTimeout = null
if (alert.type !== 'danger') {
alertTimeout = setTimeout(() => {
dispatch({
type: SET_ALERT,
alert: {
show: false
}
})
}, 5000)
}
dispatch({
type: SET_ALERT,
alert: Object.assign({}, alert, {
show: true,
alertTimeout
})
})
}
}
export const removeObject = object => {
return {
type: REMOVE_OBJECT,
object
}
}
export const setSidebarStatus = (status) => {
return {
type: SET_SIDEBAR_STATUS,
sidebarStatus: status
}
}
export const hideMakeBucketModal = () => {
return {
type: SHOW_MAKEBUCKET_MODAL,
showMakeBucketModal: false
}
}
export const setVisibleBuckets = visibleBuckets => {
return {
type: SET_VISIBLE_BUCKETS,
visibleBuckets
}
}
const appendObjects = (objects, marker, istruncated) => {
return {
type: APPEND_OBJECTS,
objects,
marker,
istruncated
}
}
export const setObjects = (objects) => {
return {
type: SET_OBJECTS,
objects,
}
}
export const resetObjects = () => {
return {
type: RESET_OBJECTS
}
}
export const setCurrentBucket = currentBucket => {
return {
type: SET_CURRENT_BUCKET,
currentBucket
}
}
export const setCurrentPath = currentPath => {
return {
type: SET_CURRENT_PATH,
currentPath
}
}
export const setStorageInfo = storageInfo => {
return {
type: SET_STORAGE_INFO,
storageInfo
}
}
export const setServerInfo = serverInfo => {
return {
type: SET_SERVER_INFO,
serverInfo
}
}
const setPrefixWritable = prefixWritable => {
return {
type: SET_PREFIX_WRITABLE,
prefixWritable,
}
}
export const selectBucket = (newCurrentBucket, prefix) => {
if (!prefix)
prefix = ''
return (dispatch, getState) => {
let web = getState().web
let currentBucket = getState().currentBucket
if (currentBucket !== newCurrentBucket) dispatch(setLoadBucket(newCurrentBucket))
dispatch(setCurrentBucket(newCurrentBucket))
dispatch(selectPrefix(prefix))
return
}
}
export const deleteBucket = (bucket) => {
return (dispatch, getState) => {
// DeleteBucket() RPC call will ONLY delete a bucket if it is empty of
// objects. This means a call can just be sent, as it is entirely reversable
// and won't do any permanent damage.
web.DeleteBucket({
bucketName: bucket
})
.then(() => {
dispatch(showAlert({
type: 'info',
message: `Bucket '${bucket}' has been deleted.`
}))
dispatch(removeBucket(bucket))
})
.catch(err => {
let message = err.message
dispatch(showAlert({
type: 'danger',
message: message
}))
})
}
}
export const listObjects = () => {
return (dispatch, getState) => {
const {buckets, currentBucket, currentPath, marker, objects, istruncated, web} = getState()
if (!istruncated || buckets.length === 0) return
web.ListObjects({
bucketName: currentBucket,
prefix: currentPath,
marker: marker
})
.then(res => {
let objects = res.objects
if (!objects)
objects = []
objects = objects.map(object => {
object.name = object.name.replace(`${currentPath}`, '');
return object
})
dispatch(appendObjects(objects, res.nextmarker, res.istruncated))
dispatch(setPrefixWritable(res.writable))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}
export const selectPrefix = prefix => {
return (dispatch, getState) => {
const {currentBucket, web} = getState()
dispatch(setLoadPath(prefix))
web.ListObjects({
bucketName: currentBucket,
prefix,
marker: ""
})
.then(res => {
let objects = res.objects
if (!objects)
objects = []
objects = objects.map(object => {
object.name = object.name.replace(`${prefix}`, '');
return object
})
dispatch(resetObjects())
dispatch(appendObjects(
objects,
res.nextmarker,
res.istruncated
))
dispatch(setPrefixWritable(res.writable))
dispatch(setSortNameOrder(false))
dispatch(setCurrentPath(prefix))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
})
.catch(err => {
dispatch(showAlert({
type: 'danger',
message: err.message
}))
dispatch(setLoadBucket(''))
dispatch(setLoadPath(''))
// Use browserHistory.replace instead of push so that browser back button works fine.
browserHistory.replace(`${minioBrowserPrefix}/login`)
})
}
}
export const addUpload = options => {
return {
type: ADD_UPLOAD,
slug: options.slug,
size: options.size,
xhr: options.xhr,
name: options.name
}
}
export const stopUpload = options => {
return {
type: STOP_UPLOAD,
slug: options.slug
}
}
export const uploadProgress = options => {
return {
type: UPLOAD_PROGRESS,
slug: options.slug,
loaded: options.loaded
}
}
export const setShowAbortModal = showAbortModal => {
return {
type: SET_SHOW_ABORT_MODAL,
showAbortModal
}
}
export const setLoginError = () => {
return {
type: SET_LOGIN_ERROR,
loginError: true
}
}
export const downloadSelected = (url, req, xhr) => {
return (dispatch) => {
var anchor = document.createElement('a')
document.body.appendChild(anchor);
xhr.open('POST', url, true)
xhr.responseType = 'blob'
xhr.onload = function(e) {
if (this.status == 200) {
dispatch(checkedObjectsReset())
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 = req.bucketName+separator+req.prefix.slice(0, -1)+'.zip';
anchor.click()
window.URL.revokeObjectURL(blobUrl)
anchor.remove()
}
};
xhr.send(JSON.stringify(req));
}
}
export const uploadFile = (file, xhr) => {
return (dispatch, getState) => {
const {currentBucket, currentPath} = getState()
const objectName = `${currentPath}${file.name}`
const uploadUrl = `${window.location.origin}${minioBrowserPrefix}/upload/${currentBucket}/${objectName}`
// The slug is a unique identifer for the file upload.
const slug = `${currentBucket}-${currentPath}-${file.name}`
xhr.open('PUT', uploadUrl, true)
xhr.withCredentials = false
const token = storage.getItem('token')
if (token) xhr.setRequestHeader("Authorization", 'Bearer ' + storage.getItem('token'))
xhr.setRequestHeader('x-amz-date', Moment().utc().format('YYYYMMDDTHHmmss') + 'Z')
dispatch(addUpload({
slug,
xhr,
size: file.size,
name: file.name
}))
xhr.onload = function(event) {
if (xhr.status == 401 || xhr.status == 403) {
setShowAbortModal(false)
dispatch(stopUpload({
slug
}))
dispatch(showAlert({
type: 'danger',
message: 'Unauthorized request.'
}))
}
if (xhr.status == 500) {
setShowAbortModal(false)
dispatch(stopUpload({
slug
}))
dispatch(showAlert({
type: 'danger',
message: xhr.responseText
}))
}
if (xhr.status == 200) {
setShowAbortModal(false)
dispatch(stopUpload({
slug
}))
dispatch(showAlert({
type: 'success',
message: 'File \'' + file.name + '\' uploaded successfully.'
}))
dispatch(selectPrefix(currentPath))
}
}
xhr.upload.addEventListener('error', event => {
dispatch(showAlert({
type: 'danger',
message: 'Error occurred uploading \'' + file.name + '\'.'
}))
dispatch(stopUpload({
slug
}))
})
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
let loaded = event.loaded
let total = event.total
// Update the counter.
dispatch(uploadProgress({
slug,
loaded
}))
}
})
xhr.send(file)
}
}
export const showAbout = () => {
return {
type: SHOW_ABOUT,
showAbout: true
}
}
export const hideAbout = () => {
return {
type: SHOW_ABOUT,
showAbout: false
}
}
export const setSortNameOrder = (sortNameOrder) => {
return {
type: SET_SORT_NAME_ORDER,
sortNameOrder
}
}
export const setSortSizeOrder = (sortSizeOrder) => {
return {
type: SET_SORT_SIZE_ORDER,
sortSizeOrder
}
}
export const setSortDateOrder = (sortDateOrder) => {
return {
type: SET_SORT_DATE_ORDER,
sortDateOrder
}
}
export const setLatestUIVersion = (latestUiVersion) => {
return {
type: SET_LATEST_UI_VERSION,
latestUiVersion
}
}
export const showSettings = () => {
return {
type: SHOW_SETTINGS,
showSettings: true
}
}
export const hideSettings = () => {
return {
type: SHOW_SETTINGS,
showSettings: false
}
}
export const setSettings = (settings) => {
return {
type: SET_SETTINGS,
settings
}
}
export const showBucketPolicy = () => {
return {
type: SHOW_BUCKET_POLICY,
showBucketPolicy: true
}
}
export const hideBucketPolicy = () => {
return {
type: SHOW_BUCKET_POLICY,
showBucketPolicy: false
}
}
export const setPolicies = (policies) => {
return {
type: SET_POLICIES,
policies
}
}
export const checkedObjectsAdd = (objectName) => {
return {
type: CHECKED_OBJECTS_ADD,
objectName
}
}
export const checkedObjectsRemove = (objectName) => {
return {
type: CHECKED_OBJECTS_REMOVE,
objectName
}
}
export const checkedObjectsReset = (objectName) => {
return {
type: CHECKED_OBJECTS_RESET,
objectName
}
}

@ -0,0 +1,57 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import configureStore from "redux-mock-store"
import thunk from "redux-thunk"
import * as actionsAlert from "../alert"
const middlewares = [thunk]
const mockStore = configureStore(middlewares)
jest.useFakeTimers()
describe("Alert actions", () => {
it("creates alert/SET action", () => {
const store = mockStore()
const expectedActions = [
{
type: "alert/SET",
alert: { id: 0, message: "Test alert", type: "danger" }
}
]
store.dispatch(actionsAlert.set({ message: "Test alert", type: "danger" }))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates alert/CLEAR action for non danger alerts", () => {
const store = mockStore()
const expectedActions = [
{
type: "alert/SET",
alert: { id: 1, message: "Test alert" }
},
{
type: "alert/CLEAR",
alert: { id: 1 }
}
]
store.dispatch(actionsAlert.set({ message: "Test alert" }))
jest.runAllTimers()
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})

@ -0,0 +1,41 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import configureStore from "redux-mock-store"
import thunk from "redux-thunk"
import * as actionsBuckets from "../buckets"
jest.mock("../../web", () => ({
ListBuckets: jest.fn(() => {
return Promise.resolve({ buckets: [{ name: "test1" }, { name: "test2" }] })
})
}))
const middlewares = [thunk]
const mockStore = configureStore(middlewares)
describe("Buckets actions", () => {
it("creates buckets/SET_LIST after fetching the buckets", () => {
const store = mockStore()
const expectedActions = [
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] }
]
return store.dispatch(actionsBuckets.fetchBuckets()).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
})

@ -0,0 +1,46 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const SET = "alert/SET"
export const CLEAR = "alert/CLEAR"
let alertId = 0
export const set = alert => {
const id = alertId++
return (dispatch, getState) => {
if (alert.type !== "danger") {
setTimeout(() => {
dispatch({
type: CLEAR,
alert: {
id
}
})
}, 5000)
}
dispatch({
type: SET,
alert: Object.assign({}, alert, {
id
})
})
}
}
export const clear = () => {
return { type: CLEAR }
}

@ -0,0 +1,51 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import web from "../web"
export const SET_LIST = "buckets/SET_LIST"
export const SET_FILTER = "buckets/SET_FILTER"
export const SET_CURRENT_BUCKET = "buckets/SET_CURRENT_BUCKET"
export const fetchBuckets = () => {
return function(dispatch) {
return web.ListBuckets().then(res => {
const buckets = res.buckets ? res.buckets.map(bucket => bucket.name) : []
dispatch(setList(buckets))
})
}
}
export const setList = buckets => {
return {
type: SET_LIST,
buckets
}
}
export const setFilter = filter => {
return {
type: SET_FILTER,
filter
}
}
export const setCurrentBucket = bucket => {
return {
type: SET_CURRENT_BUCKET,
bucket
}
}

@ -0,0 +1,30 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import AlertComponent from "react-bootstrap/lib/Alert"
const Alert = ({ show, type, message, onDismiss }) => (
<AlertComponent
className={"alert animated " + (show ? "fadeInDown" : "fadeOutUp")}
bsStyle={type}
onDismiss={onDismiss}
>
<div className="text-center">{message}</div>
</AlertComponent>
)
export default Alert

@ -0,0 +1,46 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { Route, Switch, Redirect } from "react-router-dom"
import Browser from "../components/Browser"
import Login from "../components/Login"
import web from "../web"
import { minioBrowserPrefix } from "../constants"
const AuthorizedRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
web.LoggedIn() ? (
<Component {...props} />
) : (
<Redirect to={`${minioBrowserPrefix}/login`} />
)
}
/>
)
export const App = ({ match }) => (
<Switch>
<AuthorizedRoute exact path={match.url} component={Browser} />
<Route path={`${match.url}/login`} component={Login} />
<AuthorizedRoute path={`${match.url}/:bucket/*`} component={Browser} />
<AuthorizedRoute path={`${match.url}/:bucket`} component={Browser} />
</Switch>
)
export default App

@ -1,869 +0,0 @@
/*
* Minio Cloud Storage (C) 2016 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 browserHistory from 'react-router/lib/browserHistory'
import humanize from 'humanize'
import Moment from 'moment'
import Modal from 'react-bootstrap/lib/Modal'
import ModalBody from 'react-bootstrap/lib/ModalBody'
import ModalHeader from 'react-bootstrap/lib/ModalHeader'
import Alert from 'react-bootstrap/lib/Alert'
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
import Tooltip from 'react-bootstrap/lib/Tooltip'
import Dropdown from 'react-bootstrap/lib/Dropdown'
import MenuItem from 'react-bootstrap/lib/MenuItem'
import InputGroup from '../components/InputGroup'
import Dropzone from '../components/Dropzone'
import ObjectsList from '../components/ObjectsList'
import SideBar from '../components/SideBar'
import Path from '../components/Path'
import BrowserUpdate from '../components/BrowserUpdate'
import UploadModal from '../components/UploadModal'
import SettingsModal from '../components/SettingsModal'
import PolicyInput from '../components/PolicyInput'
import Policy from '../components/Policy'
import BrowserDropdown from '../components/BrowserDropdown'
import ConfirmModal from './ConfirmModal'
import logo from '../../img/logo.svg'
import * as actions from '../actions'
import * as utils from '../utils'
import * as mime from '../mime'
import { minioBrowserPrefix } from '../constants'
import CopyToClipboard from 'react-copy-to-clipboard'
import storage from 'local-storage-fallback'
import InfiniteScroll from 'react-infinite-scroller';
export default class Browse extends React.Component {
componentDidMount() {
const {web, dispatch, currentBucket} = this.props
if (!web.LoggedIn()) return
web.StorageInfo()
.then(res => {
let storageInfo = Object.assign({}, {
total: res.storageInfo.Total,
free: res.storageInfo.Free
})
storageInfo.used = storageInfo.total - storageInfo.free
dispatch(actions.setStorageInfo(storageInfo))
return web.ServerInfo()
})
.then(res => {
let serverInfo = Object.assign({}, {
version: res.MinioVersion,
memory: res.MinioMemory,
platform: res.MinioPlatform,
runtime: res.MinioRuntime,
info: res.MinioGlobalInfo
})
dispatch(actions.setServerInfo(serverInfo))
})
.catch(err => {
dispatch(actions.showAlert({
type: 'danger',
message: err.message
}))
})
}
componentWillMount() {
const {dispatch} = this.props
// Clear out any stale message in the alert of Login page
dispatch(actions.showAlert({
type: 'danger',
message: ''
}))
if (web.LoggedIn()) {
web.ListBuckets()
.then(res => {
let buckets
if (!res.buckets)
buckets = []
else
buckets = res.buckets.map(bucket => bucket.name)
if (buckets.length) {
dispatch(actions.setBuckets(buckets))
dispatch(actions.setVisibleBuckets(buckets))
if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') {
browserHistory.push(utils.pathJoin(buckets[0]))
}
}
})
}
this.history = browserHistory.listen(({pathname}) => {
let decPathname = decodeURI(pathname)
if (decPathname === `${minioBrowserPrefix}/login`) return // FIXME: better organize routes and remove this
if (!decPathname.endsWith('/'))
decPathname += '/'
if (decPathname === minioBrowserPrefix + '/') {
return
}
let obj = utils.pathSlice(decPathname)
if (!web.LoggedIn()) {
dispatch(actions.setBuckets([obj.bucket]))
dispatch(actions.setVisibleBuckets([obj.bucket]))
}
dispatch(actions.selectBucket(obj.bucket, obj.prefix))
})
}
componentWillUnmount() {
this.history()
}
selectBucket(e, bucket) {
e.preventDefault()
if (bucket === this.props.currentBucket) return
browserHistory.push(utils.pathJoin(bucket))
}
searchBuckets(e) {
e.preventDefault()
let {buckets} = this.props
this.props.dispatch(actions.setVisibleBuckets(buckets.filter(bucket => bucket.indexOf(e.target.value) > -1)))
}
listObjects() {
const {dispatch} = this.props
dispatch(actions.listObjects())
}
selectPrefix(e, prefix) {
e.preventDefault()
const {dispatch, currentPath, web, currentBucket} = this.props
const encPrefix = encodeURI(prefix)
if (prefix.endsWith('/') || prefix === '') {
if (prefix === currentPath) return
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
} else {
if (!web.LoggedIn()) {
let url = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=''`
window.location = url
} else {
// Download the selected file.
web.CreateURLToken()
.then(res => {
let url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encPrefix}?token=${res.token}`
window.location = url
})
.catch(err => dispatch(actions.showAlert({
type: 'danger',
message: err.message
})))
}
}
}
makeBucket(e) {
e.preventDefault()
const bucketName = this.refs.makeBucketRef.value
this.refs.makeBucketRef.value = ''
const {web, dispatch} = this.props
this.hideMakeBucketModal()
web.MakeBucket({
bucketName
})
.then(() => {
dispatch(actions.addBucket(bucketName))
dispatch(actions.selectBucket(bucketName))
})
.catch(err => dispatch(actions.showAlert({
type: 'danger',
message: err.message
})))
}
hideMakeBucketModal() {
const {dispatch} = this.props
dispatch(actions.hideMakeBucketModal())
}
showMakeBucketModal(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showMakeBucketModal())
}
showAbout(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showAbout())
}
hideAbout(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideAbout())
}
toggleBucketDropdown(e) {
const {dispatch, showBucketDropdown} = this.props
if (showBucketDropdown) {
dispatch(actions.hideBucketDropdown())
} else {
dispatch(actions.showBucketDropdown())
}
}
showBucketPolicy(e) {
e.preventDefault()
const {dispatch} = this.props
this.toggleBucketDropdown(e)
dispatch(actions.showBucketPolicy())
}
hideBucketPolicy(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideBucketPolicy())
}
deleteBucket(e, bucket) {
e.preventDefault()
const {dispatch} = this.props
this.toggleBucketDropdown(e)
dispatch(actions.deleteBucket(bucket))
browserHistory.push(`${minioBrowserPrefix}/`)
}
uploadFile(e) {
e.preventDefault()
const {dispatch, buckets, currentBucket} = this.props
if (buckets.length === 0) {
dispatch(actions.showAlert({
type: 'danger',
message: "Bucket needs to be created before trying to upload files."
}))
return
}
if (currentBucket === '') {
dispatch(actions.showAlert({
type: 'danger',
message: "Please choose a bucket before trying to upload files."
}))
return
}
let file = e.target.files[0]
e.target.value = null
this.xhr = new XMLHttpRequest()
dispatch(actions.uploadFile(file, this.xhr))
}
removeObject() {
const {web, dispatch, currentPath, currentBucket, deleteConfirmation, checkedObjects} = this.props
let objects = []
if (checkedObjects.length > 0) {
objects = checkedObjects.map(obj => `${currentPath}${obj}`)
} else {
objects = [deleteConfirmation.object]
}
web.RemoveObject({
bucketname: currentBucket,
objects: objects
})
.then(() => {
this.hideDeleteConfirmation()
if (checkedObjects.length > 0) {
for (let i = 0; i < checkedObjects.length; i++) {
dispatch(actions.removeObject(checkedObjects[i].replace(currentPath, '')))
}
dispatch(actions.checkedObjectsReset())
} else {
let delObject = deleteConfirmation.object.replace(currentPath, '')
dispatch(actions.removeObject(delObject))
}
})
.catch(e => dispatch(actions.showAlert({
type: 'danger',
message: e.message
})))
}
hideAlert(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.hideAlert())
}
showDeleteConfirmation(e, object) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showDeleteConfirmation(object))
}
hideDeleteConfirmation() {
const {dispatch} = this.props
dispatch(actions.hideDeleteConfirmation())
}
shareObject(e, object) {
e.preventDefault()
const {dispatch} = this.props
// let expiry = 5 * 24 * 60 * 60 // 5 days expiry by default
dispatch(actions.shareObject(object, 5, 0, 0))
}
hideShareObjectModal() {
const {dispatch} = this.props
dispatch(actions.hideShareObject())
}
dataType(name, contentType) {
return mime.getDataType(name, contentType)
}
sortObjectsByName(e) {
const {dispatch, objects, sortNameOrder} = this.props
dispatch(actions.setObjects(utils.sortObjectsByName(objects, !sortNameOrder)))
dispatch(actions.setSortNameOrder(!sortNameOrder))
}
sortObjectsBySize() {
const {dispatch, objects, sortSizeOrder} = this.props
dispatch(actions.setObjects(utils.sortObjectsBySize(objects, !sortSizeOrder)))
dispatch(actions.setSortSizeOrder(!sortSizeOrder))
}
sortObjectsByDate() {
const {dispatch, objects, sortDateOrder} = this.props
dispatch(actions.setObjects(utils.sortObjectsByDate(objects, !sortDateOrder)))
dispatch(actions.setSortDateOrder(!sortDateOrder))
}
logout(e) {
const {web} = this.props
e.preventDefault()
web.Logout()
browserHistory.push(`${minioBrowserPrefix}/login`)
}
fullScreen(e) {
e.preventDefault()
let el = document.documentElement
if (el.requestFullscreen) {
el.requestFullscreen()
}
if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
}
if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen()
}
if (el.msRequestFullscreen) {
el.msRequestFullscreen()
}
}
toggleSidebar(status) {
this.props.dispatch(actions.setSidebarStatus(status))
}
hideSidebar(event) {
let e = event || window.event;
// Support all browsers.
let target = e.srcElement || e.target;
if (target.nodeType === 3) // Safari support.
target = target.parentNode;
let targetID = target.id;
if (!(targetID === 'feh-trigger')) {
this.props.dispatch(actions.setSidebarStatus(false))
}
}
showSettings(e) {
e.preventDefault()
const {dispatch} = this.props
dispatch(actions.showSettings())
}
showMessage() {
const {dispatch} = this.props
dispatch(actions.showAlert({
type: 'success',
message: 'Link copied to clipboard!'
}))
this.hideShareObjectModal()
}
selectTexts() {
this.refs.copyTextInput.select()
}
handleExpireValue(targetInput, inc, object) {
let value = this.refs[targetInput].value
let maxValue = (targetInput == 'expireHours') ? 23 : (targetInput == 'expireMins') ? 59 : (targetInput == 'expireDays') ? 7 : 0
value = isNaN(value) ? 0 : value
// Use custom step count to support browser Edge
if((inc === -1)) {
if(value != 0) {
value--
}
}
else {
if(value != maxValue) {
value++
}
}
this.refs[targetInput].value = value
// Reset hours and mins when days reaches it's max value
if (this.refs.expireDays.value == 7) {
this.refs.expireHours.value = 0
this.refs.expireMins.value = 0
}
if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) {
this.refs.expireDays.value = 7
}
const {dispatch} = this.props
dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value))
}
checkObject(e, objectName) {
const {dispatch} = this.props
e.target.checked ? dispatch(actions.checkedObjectsAdd(objectName)) : dispatch(actions.checkedObjectsRemove(objectName))
}
downloadSelected() {
const {dispatch, web} = this.props
let req = {
bucketName: this.props.currentBucket,
objects: this.props.checkedObjects,
prefix: this.props.currentPath
}
if (!web.LoggedIn()) {
let requestUrl = location.origin + "/minio/zip?token=''"
this.xhr = new XMLHttpRequest()
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
} else {
web.CreateURLToken()
.then(res => {
let requestUrl = location.origin + minioBrowserPrefix + "/zip?token=" + res.token
this.xhr = new XMLHttpRequest()
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
})
.catch(err => dispatch(actions.showAlert({
type: 'danger',
message: err.message
})))
}
}
clearSelected() {
const {dispatch} = this.props
dispatch(actions.checkedObjectsReset())
}
render() {
const {total, free} = this.props.storageInfo
const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy, checkedObjects} = this.props
const {version, memory, platform, runtime} = this.props.serverInfo
const {sidebarStatus} = this.props
const {showSettings} = this.props
const {policies, currentBucket, currentPath} = this.props
const {deleteConfirmation} = this.props
const {shareObject} = this.props
const {web, prefixWritable, istruncated} = this.props
// Don't always show the SettingsModal. This is done here instead of in
// SettingsModal.js so as to allow for #componentWillMount to handle
// the loading of the settings.
let settingsModal = showSettings ? <SettingsModal /> : <noscript></noscript>
let alertBox = <Alert className={ classNames({
'alert': true,
'animated': true,
'fadeInDown': alert.show,
'fadeOutUp': !alert.show
}) } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }>
<div className='text-center'>
{ alert.message }
</div>
</Alert>
// Make sure you don't show a fading out alert box on the initial web-page load.
if (!alert.message)
alertBox = ''
let signoutTooltip = <Tooltip id="tt-sign-out">
Sign out
</Tooltip>
let uploadTooltip = <Tooltip id="tt-upload-file">
Upload file
</Tooltip>
let makeBucketTooltip = <Tooltip id="tt-create-bucket">
Create bucket
</Tooltip>
let loginButton = ''
let browserDropdownButton = ''
let storageUsageDetails = ''
let used = total - free
let usedPercent = (used / total) * 100 + '%'
let freePercent = free * 100 / total
if (web.LoggedIn()) {
browserDropdownButton = <BrowserDropdown fullScreenFunc={ this.fullScreen.bind(this) }
aboutFunc={ this.showAbout.bind(this) }
settingsFunc={ this.showSettings.bind(this) }
logoutFunc={ this.logout.bind(this) } />
} else {
loginButton = <a className='btn btn-danger' href={minioBrowserPrefix+'/login'}>Login</a>
}
if (web.LoggedIn()) {
if (!(used === 0 && free === 0)) {
storageUsageDetails = <div className="feh-usage">
<div className="fehu-chart">
<div style={ { width: usedPercent } }></div>
</div>
<ul>
<li>
<span>Used: </span>
{ humanize.filesize(total - free) }
</li>
<li className="pull-right">
<span>Free: </span>
{ humanize.filesize(total - used) }
</li>
</ul>
</div>
}
}
let createButton = ''
if (web.LoggedIn()) {
createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle">
<Dropdown.Toggle noCaret className="feba-toggle">
<span><i className="fa fa-plus"></i></span>
</Dropdown.Toggle>
<Dropdown.Menu>
<OverlayTrigger placement="left" overlay={ uploadTooltip }>
<a href="#" className="feba-btn feba-upload">
<input type="file"
onChange={ this.uploadFile.bind(this) }
style={ { display: 'none' } }
id="file-input"></input>
<label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label>
</a>
</OverlayTrigger>
<OverlayTrigger placement="left" overlay={ makeBucketTooltip }>
<a href="#" className="feba-btn feba-bucket" onClick={ this.showMakeBucketModal.bind(this) }><i className="fa fa-hdd-o"></i></a>
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
} else {
if (prefixWritable)
createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle">
<Dropdown.Toggle noCaret className="feba-toggle">
<span><i className="fa fa-plus"></i></span>
</Dropdown.Toggle>
<Dropdown.Menu>
<OverlayTrigger placement="left" overlay={ uploadTooltip }>
<a href="#" className="feba-btn feba-upload">
<input type="file"
onChange={ this.uploadFile.bind(this) }
style={ { display: 'none' } }
id="file-input"></input>
<label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label>
</a>
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
}
return (
<div className={ classNames({
'file-explorer': true,
'toggled': sidebarStatus
}) }>
<SideBar searchBuckets={ this.searchBuckets.bind(this) }
selectBucket={ this.selectBucket.bind(this) }
clickOutside={ this.hideSidebar.bind(this) }
showPolicy={ this.showBucketPolicy.bind(this) }
deleteBucket={ this.deleteBucket.bind(this) }
toggleBucketDropdown={ this.toggleBucketDropdown.bind(this) } />
<div className="fe-body">
<div className={ 'list-actions' + (classNames({
' list-actions-toggled': checkedObjects.length > 0
})) }>
<span className="la-label"><i className="fa fa-check-circle" /> { checkedObjects.length } Objects selected</span>
<span className="la-actions pull-right"><button onClick={ this.downloadSelected.bind(this) }> Download all as zip </button></span>
<span className="la-actions pull-right"><button onClick={ this.showDeleteConfirmation.bind(this) }> Delete selected </button></span>
<i className="la-close fa fa-times" onClick={ this.clearSelected.bind(this) }></i>
</div>
<Dropzone>
{ alertBox }
<header className="fe-header-mobile hidden-lg hidden-md">
<div id="feh-trigger" className={ 'feh-trigger ' + (classNames({
'feht-toggled': sidebarStatus
})) } onClick={ this.toggleSidebar.bind(this, !sidebarStatus) }>
<div className="feht-lines">
<div className="top"></div>
<div className="center"></div>
<div className="bottom"></div>
</div>
</div>
<img className="mh-logo" src={ logo } alt="" />
</header>
<header className="fe-header">
<Path selectPrefix={ this.selectPrefix.bind(this) } />
{ storageUsageDetails }
<ul className="feh-actions">
<BrowserUpdate />
{ loginButton }
{ browserDropdownButton }
</ul>
</header>
<div className="feb-container">
<header className="fesl-row" data-type="folder">
<div className="fesl-item fesl-item-icon"></div>
<div className="fesl-item fesl-item-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name">
Name
<i className={ classNames({
'fesli-sort': true,
'fa': true,
'fa-sort-alpha-desc': sortNameOrder,
'fa-sort-alpha-asc': !sortNameOrder
}) } />
</div>
<div className="fesl-item fesl-item-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
Size
<i className={ classNames({
'fesli-sort': true,
'fa': true,
'fa-sort-amount-desc': sortSizeOrder,
'fa-sort-amount-asc': !sortSizeOrder
}) } />
</div>
<div className="fesl-item fesl-item-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
Last Modified
<i className={ classNames({
'fesli-sort': true,
'fa': true,
'fa-sort-numeric-desc': sortDateOrder,
'fa-sort-numeric-asc': !sortDateOrder
}) } />
</div>
<div className="fesl-item fesl-item-actions"></div>
</header>
</div>
<div className="feb-container">
<InfiniteScroll loadMore={ this.listObjects.bind(this) }
hasMore={ istruncated }
useWindow={ true }
initialLoad={ false }>
<ObjectsList dataType={ this.dataType.bind(this) }
selectPrefix={ this.selectPrefix.bind(this) }
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
shareObject={ this.shareObject.bind(this) }
checkObject={ this.checkObject.bind(this) }
checkedObjectsArray={ checkedObjects } />
</InfiniteScroll>
<div className="text-center" style={ { display: (istruncated && currentBucket) ? 'block' : 'none' } }>
<span>Loading...</span>
</div>
</div>
<UploadModal />
{ createButton }
<Modal className="modal-create-bucket"
bsSize="small"
animation={ false }
show={ showMakeBucketModal }
onHide={ this.hideMakeBucketModal.bind(this) }>
<button className="close close-alt" onClick={ this.hideMakeBucketModal.bind(this) }>
<span>×</span>
</button>
<ModalBody>
<form onSubmit={ this.makeBucket.bind(this) }>
<div className="input-group">
<input className="ig-text"
type="text"
ref="makeBucketRef"
placeholder="Bucket Name"
autoFocus/>
<i className="ig-helpers"></i>
</div>
</form>
</ModalBody>
</Modal>
<Modal className="modal-about modal-dark"
animation={ false }
show={ showAbout }
onHide={ this.hideAbout.bind(this) }>
<button className="close" onClick={ this.hideAbout.bind(this) }>
<span>×</span>
</button>
<div className="ma-inner">
<div className="mai-item hidden-xs">
<a href="https://minio.io" target="_blank"><img className="maii-logo" src={ logo } alt="" /></a>
</div>
<div className="mai-item">
<ul className="maii-list">
<li>
<div>
Version
</div>
<small>{ version }</small>
</li>
<li>
<div>
Memory
</div>
<small>{ memory }</small>
</li>
<li>
<div>
Platform
</div>
<small>{ platform }</small>
</li>
<li>
<div>
Runtime
</div>
<small>{ runtime }</small>
</li>
</ul>
</div>
</div>
</Modal>
<Modal className="modal-policy"
animation={ false }
show={ showBucketPolicy }
onHide={ this.hideBucketPolicy.bind(this) }>
<ModalHeader>
Bucket Policy (
{ currentBucket })
<button className="close close-alt" onClick={ this.hideBucketPolicy.bind(this) }>
<span>×</span>
</button>
</ModalHeader>
<div className="pm-body">
<PolicyInput bucket={ currentBucket } />
{ policies.map((policy, i) => <Policy key={ i } prefix={ policy.prefix } policy={ policy.policy } />
) }
</div>
</Modal>
<ConfirmModal show={ deleteConfirmation.show }
icon='fa fa-exclamation-triangle mci-red'
text='Are you sure you want to delete?'
sub='This cannot be undone!'
okText='Delete'
cancelText='Cancel'
okHandler={ this.removeObject.bind(this) }
cancelHandler={ this.hideDeleteConfirmation.bind(this) }>
</ConfirmModal>
<Modal show={ shareObject.show }
animation={ false }
onHide={ this.hideShareObjectModal.bind(this) }
bsSize="small">
<ModalHeader>
Share Object
</ModalHeader>
<ModalBody>
<div className="input-group copy-text">
<label>
Shareable Link
</label>
<input type="text"
ref="copyTextInput"
readOnly="readOnly"
value={ window.location.protocol + '//' + shareObject.url }
onClick={ this.selectTexts.bind(this) } />
</div>
<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 className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1, shareObject.object) } />
<div className="set-expire-title">
Days
</div>
<div className="set-expire-value">
<input ref="expireDays"
type="number"
min={ 0 }
max={ 7 }
defaultValue={ 5 }
readOnly="readOnly"
/>
</div>
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1, shareObject.object) } />
</div>
<div className="set-expire-item">
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1, shareObject.object) } />
<div className="set-expire-title">
Hours
</div>
<div className="set-expire-value">
<input ref="expireHours"
type="number"
min={ 0 }
max={ 23 }
defaultValue={ 0 }
readOnly="readOnly"
/>
</div>
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1, shareObject.object) } />
</div>
<div className="set-expire-item">
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireMins', 1, shareObject.object) } />
<div className="set-expire-title">
Minutes
</div>
<div className="set-expire-value">
<input ref="expireMins"
type="number"
min={ 0 }
max={ 59 }
defaultValue={ 0 }
readOnly="readOnly"
/>
</div>
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1, shareObject.object) } />
</div>
</div>
</div>
</ModalBody>
<div className="modal-footer">
<CopyToClipboard text={ window.location.protocol + '//' + shareObject.url } onCopy={ this.showMessage.bind(this) }>
<button className="btn btn-success">
Copy Link
</button>
</CopyToClipboard>
<button className="btn btn-link" onClick={ this.hideShareObjectModal.bind(this) }>
Cancel
</button>
</div>
</Modal>
{ settingsModal }
</Dropzone>
</div>
</div>
)
}
}

@ -0,0 +1,36 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import classNames from "classnames"
import { connect } from "react-redux"
import SideBar from "./SideBar"
class Browser extends React.Component {
render() {
return (
<div
className={classNames({
"file-explorer": true
})}
>
<SideBar />
</div>
)
}
}
export default connect(state => state)(Browser)

@ -0,0 +1,43 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import classNames from "classnames"
export const Bucket = ({ bucket, isActive, selectBucket }) => {
return (
<li
className={classNames({
active: isActive
})}
onClick={e => {
e.preventDefault()
selectBucket(bucket)
}}
>
<a
href=""
className={classNames({
"fesli-loading": false
})}
>
{bucket}
</a>
</li>
)
}
export default Bucket

@ -0,0 +1,35 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { connect } from "react-redux"
import * as actionsBuckets from "../actions/buckets"
import { getCurrentBucket } from "../selectors/buckets"
import Bucket from "./Bucket"
const mapStateToProps = (state, ownProps) => {
return {
isActive: getCurrentBucket(state) === ownProps.bucket
}
}
const mapDispatchToProps = dispatch => {
return {
selectBucket: bucket => dispatch(actionsBuckets.setCurrentBucket(bucket))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Bucket)

@ -0,0 +1,59 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { connect } from "react-redux"
import { Scrollbars } from "react-custom-scrollbars"
import * as actionsBuckets from "../actions/buckets"
import { getVisibleBuckets } from "../selectors/buckets"
import BucketContainer from "./BucketContainer"
export class BucketList extends React.Component {
componentWillMount() {
const { fetchBuckets } = this.props
fetchBuckets()
}
render() {
const { visibleBuckets } = this.props
return (
<div className="fesl-inner">
<Scrollbars
renderTrackVertical={props => <div className="scrollbar-vertical" />}
>
<ul>
{visibleBuckets.map(bucket => (
<BucketContainer key={bucket} bucket={bucket} />
))}
</ul>
</Scrollbars>
</div>
)
}
}
const mapStateToProps = state => {
return {
visibleBuckets: getVisibleBuckets(state)
}
}
const mapDispatchToProps = dispatch => {
return {
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(BucketList)

@ -0,0 +1,44 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { connect } from "react-redux"
import * as actionsBuckets from "../actions/buckets"
export const BucketSearch = ({ onChange }) => (
<div
className="input-group ig-dark ig-left ig-search"
style={{ display: "block" }}
>
<input
className="ig-text"
type="text"
onChange={e => onChange(e.target.value)}
placeholder="Search Buckets..."
/>
<i className="ig-helpers" />
</div>
)
const mapDispatchToProps = dispatch => {
return {
onChange: filter => {
dispatch(actionsBuckets.setFilter(filter))
}
}
}
export default connect(undefined, mapDispatchToProps)(BucketSearch)

@ -0,0 +1,26 @@
/*
* Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
export const Host = () => (
<div className="fes-host">
<i className="fa fa-globe" />
<a href="/">{window.location.host}</a>
</div>
)
export default Host

@ -1,5 +1,5 @@
/*
* Minio Cloud Storage (C) 2016 Minio, Inc.
* Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,115 +14,118 @@
* limitations under the License.
*/
import React from 'react'
import classNames from 'classnames'
import logo from '../../img/logo.svg'
import Alert from 'react-bootstrap/lib/Alert'
import * as actions from '../actions'
import InputGroup from '../components/InputGroup'
import React from "react"
import { connect } from "react-redux"
import classNames from "classnames"
import logo from "../../img/logo.svg"
import Alert from "./Alert"
import * as actionsAlert from "../actions/alert"
import InputGroup from "../components/InputGroup"
import { minioBrowserPrefix } from "../constants"
import web from "../web"
import { Redirect } from "react-router-dom"
export default class Login extends React.Component {
export class Login extends React.Component {
handleSubmit(event) {
event.preventDefault()
const {web, dispatch, loginRedirectPath} = this.props
let message = ''
if (!document.getElementById('accessKey').value) {
message = 'Secret Key cannot be empty'
const { dispatch, history } = this.props
let message = ""
if (!document.getElementById("accessKey").value) {
message = "Secret Key cannot be empty"
}
if (!document.getElementById('secretKey').value) {
message = 'Access Key cannot be empty'
if (!document.getElementById("secretKey").value) {
message = "Access Key cannot be empty"
}
if (message) {
dispatch(actions.showAlert({
type: 'danger',
message
}))
dispatch(
actionsAlert.set({
type: "danger",
message
})
)
return
}
web.Login({
username: document.getElementById('accessKey').value,
password: document.getElementById('secretKey').value
})
.then((res) => {
this.context.router.push(loginRedirectPath)
web
.Login({
username: document.getElementById("accessKey").value,
password: document.getElementById("secretKey").value
})
.then(res => {
history.push(minioBrowserPrefix)
})
.catch(e => {
dispatch(actions.setLoginError())
dispatch(actions.showAlert({
type: 'danger',
message: e.message
}))
dispatch(
actionsAlert.set({
type: "danger",
message: e.message
})
)
})
}
componentWillMount() {
const {dispatch} = this.props
const { dispatch } = this.props
// Clear out any stale message in the alert of previous page
dispatch(actions.showAlert({
type: 'danger',
message: ''
}))
document.body.classList.add('is-guest')
dispatch(actionsAlert.clear())
document.body.classList.add("is-guest")
}
componentWillUnmount() {
document.body.classList.remove('is-guest')
document.body.classList.remove("is-guest")
}
hideAlert() {
const {dispatch} = this.props
dispatch(actions.hideAlert())
clearAlert() {
const { dispatch } = this.props
dispatch(actionsAlert.clear())
}
render() {
const {alert} = this.props
let alertBox = <Alert className={ 'alert animated ' + (alert.show ? 'fadeInDown' : 'fadeOutUp') } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }>
<div className='text-center'>
{ alert.message }
</div>
</Alert>
const { alert } = this.props
if (web.LoggedIn()) {
return <Redirect to={minioBrowserPrefix} />
}
let alertBox = <Alert {...alert} onDismiss={this.clearAlert.bind(this)} />
// Make sure you don't show a fading out alert box on the initial web-page load.
if (!alert.message)
alertBox = ''
if (!alert.message) alertBox = ""
return (
<div className="login">
{ alertBox }
{alertBox}
<div className="l-wrap">
<form onSubmit={ this.handleSubmit.bind(this) }>
<InputGroup className="ig-dark"
<form onSubmit={this.handleSubmit.bind(this)}>
<InputGroup
className="ig-dark"
label="Access Key"
id="accessKey"
name="username"
type="text"
spellCheck="false"
required="required"
autoComplete="username">
</InputGroup>
<InputGroup className="ig-dark"
autoComplete="username"
/>
<InputGroup
className="ig-dark"
label="Secret Key"
id="secretKey"
name="password"
type="password"
spellCheck="false"
required="required"
autoComplete="new-password">
</InputGroup>
autoComplete="new-password"
/>
<button className="lw-btn" type="submit">
<i className="fa fa-sign-in"></i>
<i className="fa fa-sign-in" />
</button>
</form>
</div>
<div className="l-footer">
<a className="lf-logo" href=""><img src={ logo } alt="" /></a>
<div className="lf-server">
{ window.location.host }
</div>
<a className="lf-logo" href="">
<img src={logo} alt="" />
</a>
<div className="lf-server">{window.location.host}</div>
</div>
</div>
)
}
}
Login.contextTypes = {
router: React.PropTypes.object.isRequired
}
export default connect(state => state)(Login)

@ -1,5 +1,5 @@
/*
* Minio Cloud Storage (C) 2016 Minio, Inc.
* Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,86 +14,37 @@
* limitations under the License.
*/
import React from 'react'
import classNames from 'classnames'
import ClickOutHandler from 'react-onclickout'
import Scrollbars from 'react-custom-scrollbars/lib/Scrollbars'
import connect from 'react-redux/lib/components/connect'
import React from "react"
import classNames from "classnames"
import ClickOutHandler from "react-onclickout"
import { connect } from "react-redux"
import logo from '../../img/logo.svg'
import Dropdown from 'react-bootstrap/lib/Dropdown'
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy, deleteBucket, toggleBucketDropdown, showBucketDropdown}) => {
const list = visibleBuckets.map((bucket, i) => {
return <li className={ classNames({
'active': bucket === currentBucket
}) } key={ i } onClick={ (e) => selectBucket(e, bucket) }>
<a href="" className={ classNames({
'fesli-loading': bucket === loadBucket
}) }>
{ bucket }
</a>
<Dropdown open={bucket === currentBucket && showBucketDropdown} onToggle={toggleBucketDropdown} className="bucket-dropdown" id="bucket-dropdown">
<Dropdown.Toggle noCaret>
<i className="zmdi zmdi-more-vert" />
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<li>
<a onClick={ showPolicy }>Edit policy</a>
</li>
<li>
<a onClick={ (e) => deleteBucket(e, bucket) }>Delete</a>
</li>
</Dropdown.Menu>
</Dropdown>
</li>
})
import logo from "../../img/logo.svg"
import Dropdown from "react-bootstrap/lib/Dropdown"
import BucketSearch from "./BucketSearch"
import BucketList from "./BucketList"
import Host from "./Host"
export const SideBar = () => {
return (
<ClickOutHandler onClickOut={ clickOutside }>
<div className={ classNames({
'fe-sidebar': true,
'toggled': sidebarStatus
}) }>
<ClickOutHandler>
<div
className={classNames({
"fe-sidebar": true
})}
>
<div className="fes-header clearfix hidden-sm hidden-xs">
<img src={ logo } alt="" />
<img src={logo} alt="" />
<h2>Minio Browser</h2>
</div>
<div className="fes-list">
<div className="input-group ig-dark ig-left ig-search" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
<input className="ig-text"
type="text"
onChange={ searchBuckets }
placeholder="Search Buckets..." />
<i className="ig-helpers"></i>
</div>
<div className="fesl-inner">
<Scrollbars renderScrollbarVertical={ props => <div className="scrollbar-vertical" /> }>
<ul>
{ list }
</ul>
</Scrollbars>
</div>
</div>
<div className="fes-host">
<i className="fa fa-globe"></i>
<a href="/">
{ window.location.host }
</a>
<BucketSearch />
<BucketList />
</div>
<Host />
</div>
</ClickOutHandler>
)
}
// Subscribe it to state changes that affect only the sidebar.
export default connect(state => {
return {
visibleBuckets: state.visibleBuckets,
loadBucket: state.loadBucket,
currentBucket: state.currentBucket,
showBucketDropdown: state.showBucketDropdown,
sidebarStatus: state.sidebarStatus
}
})(SideBar)
export default connect(state => state)(SideBar)

@ -0,0 +1,39 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow } from "enzyme"
import { Bucket } from "../Bucket"
describe("Bucket", () => {
it("should render without crashing", () => {
shallow(<Bucket />)
})
it("should call selectBucket when clicked", () => {
const selectBucket = jest.fn()
const wrapper = shallow(
<Bucket bucket={"test"} selectBucket={selectBucket} />
)
wrapper.find("li").simulate("click", { preventDefault: jest.fn() })
expect(selectBucket).toHaveBeenCalledWith("test")
})
it("should highlight the selected bucket", () => {
const wrapper = shallow(<Bucket bucket={"test"} isActive={true} />)
expect(wrapper.find("li").hasClass("active")).toBeTruthy()
})
})

@ -0,0 +1,29 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow } from "enzyme"
import { BucketList } from "../BucketList"
describe("Bucket", () => {
it("should call fetchBuckets before component is mounted", () => {
const fetchBuckets = jest.fn()
const wrapper = shallow(
<BucketList visibleBuckets={[]} fetchBuckets={fetchBuckets} />
)
expect(fetchBuckets).toHaveBeenCalled()
})
})

@ -0,0 +1,32 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from "react"
import { shallow } from "enzyme"
import { BucketSearch } from "../BucketSearch"
describe("BucketSearch", () => {
it("should render without crashing", () => {
shallow(<BucketSearch />)
})
it("should call onChange with search text", () => {
const onChange = jest.fn()
const wrapper = shallow(<BucketSearch onChange={onChange} />)
wrapper.find("input").simulate("change", { target: { value: "test" } })
expect(onChange).toHaveBeenCalledWith("test")
})
})

@ -1,54 +0,0 @@
/*
* Minio Cloud Storage (C) 2016 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 ReactTestUtils, {renderIntoDocument} from 'react-addons-test-utils'
import expect from 'expect'
import Login from '../Login'
describe('Login', () => {
it('it should have empty credentials', () => {
const alert = {
show: false
}
const dispatch = () => {}
let loginComponent = renderIntoDocument(<Login alert={alert} dispatch={dispatch} />)
const accessKey = document.getElementById('accessKey')
const secretKey = document.getElementById('secretKey')
// Validate default value.
expect(accessKey.value).toEqual('')
expect(secretKey.value).toEqual('')
})
it('it should set accessKey and secretKey', () => {
const alert = {
show: false
}
const dispatch = () => {}
let loginComponent = renderIntoDocument(<Login alert={alert} dispatch={dispatch} />)
let accessKey = loginComponent.refs.accessKey
let secretKey = loginComponent.refs.secretKey
accessKey.value = 'demo-username'
secretKey.value = 'demo-password'
ReactTestUtils.Simulate.change(accessKey)
ReactTestUtils.Simulate.change(secretKey)
// Validate if the change has occurred.
expect(loginComponent.refs.accessKey.value).toEqual('demo-username')
expect(loginComponent.refs.secretKey.value).toEqual('demo-password')
})
});
*/

@ -0,0 +1,21 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import "jest-enzyme"
import { configure } from "enzyme"
import Adapter from "enzyme-adapter-react-16"
configure({ adapter: new Adapter() })

@ -0,0 +1,30 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
let delay = [0, 400]
function handleLoader(i) {
if (i < 2) {
setTimeout(function() {
document.querySelector(".page-load").classList.add("pl-" + i)
handleLoader(i + 1)
}, delay[i])
}
}
const hideLoader = () => handleLoader(0)
export default hideLoader

@ -1,215 +0,0 @@
/*
* Minio Cloud Storage (C) 2016 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 actions from './actions'
import { minioBrowserPrefix } from './constants'
export default (state = {
buckets: [],
visibleBuckets: [],
objects: [],
istruncated: true,
storageInfo: {},
serverInfo: {},
currentBucket: '',
showBucketDropdown: false,
currentPath: '',
showMakeBucketModal: false,
uploads: {},
alert: {
show: false,
type: 'danger',
message: ''
},
loginError: false,
sortNameOrder: false,
sortSizeOrder: false,
sortDateOrder: false,
latestUiVersion: currentUiVersion,
sideBarActive: false,
loginRedirectPath: minioBrowserPrefix,
settings: {
accessKey: '',
secretKey: '',
secretKeyVisible: false
},
showSettings: false,
policies: [],
deleteConfirmation: {
object: '',
show: false
},
shareObject: {
show: false,
url: '',
object: ''
},
prefixWritable: false,
checkedObjects: []
}, action) => {
let newState = Object.assign({}, state)
switch (action.type) {
case actions.SET_WEB:
newState.web = action.web
break
case actions.SET_BUCKETS:
newState.buckets = action.buckets
break
case actions.ADD_BUCKET:
newState.buckets = [action.bucket, ...newState.buckets]
newState.visibleBuckets = [action.bucket, ...newState.visibleBuckets]
break
case actions.REMOVE_BUCKET:
newState.buckets = newState.buckets.filter(bucket => bucket != action.bucket)
newState.visibleBuckets = newState.visibleBuckets.filter(bucket => bucket != action.bucket)
newState.currentBucket = ""
break
case actions.SHOW_BUCKET_DROPDOWN:
newState.showBucketDropdown = action.showBucketDropdown
break
case actions.SET_VISIBLE_BUCKETS:
newState.visibleBuckets = action.visibleBuckets
break
case actions.SET_CURRENT_BUCKET:
newState.currentBucket = action.currentBucket
break
case actions.APPEND_OBJECTS:
newState.objects = [...newState.objects, ...action.objects]
newState.marker = action.marker
newState.istruncated = action.istruncated
break
case actions.SET_OBJECTS:
newState.objects = [...action.objects]
break
case actions.RESET_OBJECTS:
newState.objects = []
newState.marker = ""
newState.istruncated = false
break
case actions.SET_CURRENT_PATH:
newState.currentPath = action.currentPath
break
case actions.SET_STORAGE_INFO:
newState.storageInfo = action.storageInfo
break
case actions.SET_SERVER_INFO:
newState.serverInfo = action.serverInfo
break
case actions.SHOW_MAKEBUCKET_MODAL:
newState.showMakeBucketModal = action.showMakeBucketModal
break
case actions.UPLOAD_PROGRESS:
newState.uploads = Object.assign({}, newState.uploads)
newState.uploads[action.slug].loaded = action.loaded
break
case actions.ADD_UPLOAD:
newState.uploads = Object.assign({}, newState.uploads, {
[action.slug]: {
loaded: 0,
size: action.size,
xhr: action.xhr,
name: action.name
}
})
break
case actions.STOP_UPLOAD:
newState.uploads = Object.assign({}, newState.uploads)
delete newState.uploads[action.slug]
break
case actions.SET_ALERT:
if (newState.alert.alertTimeout) clearTimeout(newState.alert.alertTimeout)
if (!action.alert.show) {
newState.alert = Object.assign({}, newState.alert, {
show: false
})
} else {
newState.alert = action.alert
}
break
case actions.SET_LOGIN_ERROR:
newState.loginError = true
break
case actions.SET_SHOW_ABORT_MODAL:
newState.showAbortModal = action.showAbortModal
break
case actions.SHOW_ABOUT:
newState.showAbout = action.showAbout
break
case actions.SET_SORT_NAME_ORDER:
newState.sortNameOrder = action.sortNameOrder
break
case actions.SET_SORT_SIZE_ORDER:
newState.sortSizeOrder = action.sortSizeOrder
break
case actions.SET_SORT_DATE_ORDER:
newState.sortDateOrder = action.sortDateOrder
break
case actions.SET_LATEST_UI_VERSION:
newState.latestUiVersion = action.latestUiVersion
break
case actions.SET_SIDEBAR_STATUS:
newState.sidebarStatus = action.sidebarStatus
break
case actions.SET_LOGIN_REDIRECT_PATH:
newState.loginRedirectPath = action.path
case actions.SET_LOAD_BUCKET:
newState.loadBucket = action.loadBucket
break
case actions.SET_LOAD_PATH:
newState.loadPath = action.loadPath
break
case actions.SHOW_SETTINGS:
newState.showSettings = action.showSettings
break
case actions.SET_SETTINGS:
newState.settings = Object.assign({}, newState.settings, action.settings)
break
case actions.SHOW_BUCKET_POLICY:
newState.showBucketPolicy = action.showBucketPolicy
break
case actions.SET_POLICIES:
newState.policies = action.policies
break
case actions.DELETE_CONFIRMATION:
newState.deleteConfirmation = Object.assign({}, action.payload)
break
case actions.SET_SHARE_OBJECT:
newState.shareObject = Object.assign({}, action.shareObject)
break
case actions.SET_PREFIX_WRITABLE:
newState.prefixWritable = action.prefixWritable
break
case actions.REMOVE_OBJECT:
let idx = newState.objects.findIndex(object => object.name === action.object)
if (idx == -1) break
newState.objects = [...newState.objects.slice(0, idx), ...newState.objects.slice(idx + 1)]
break
case actions.CHECKED_OBJECTS_ADD:
newState.checkedObjects = [...newState.checkedObjects, action.objectName]
break
case actions.CHECKED_OBJECTS_REMOVE:
let index = newState.checkedObjects.indexOf(action.objectName)
if (index == -1) break
newState.checkedObjects = [...newState.checkedObjects.slice(0, index), ...newState.checkedObjects.slice(index + 1)]
break
case actions.CHECKED_OBJECTS_RESET:
newState.checkedObjects = []
break
}
return newState
}

@ -0,0 +1,87 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import reducer from "../alert"
import * as actionsAlert from "../../actions/alert"
describe("buckets reducer", () => {
it("should return the initial state", () => {
expect(reducer(undefined, {})).toEqual({
show: false,
type: "danger"
})
})
it("should handle SET_ALERT", () => {
expect(
reducer(undefined, {
type: actionsAlert.SET,
alert: { id: 1, type: "danger", message: "Test message" }
})
).toEqual({
show: true,
id: 1,
type: "danger",
message: "Test message"
})
})
it("should clear alert if id not passed", () => {
expect(
reducer(
{ show: true, type: "danger", message: "Test message" },
{
type: actionsAlert.CLEAR
}
)
).toEqual({
show: false,
type: "danger"
})
})
it("should clear alert if id is matching", () => {
expect(
reducer(
{ show: true, id: 1, type: "danger", message: "Test message" },
{
type: actionsAlert.CLEAR,
alert: { id: 1 }
}
)
).toEqual({
show: false,
type: "danger"
})
})
it("should not clear alert if id is not matching", () => {
expect(
reducer(
{ show: true, id: 1, type: "danger", message: "Test message" },
{
type: actionsAlert.CLEAR,
alert: { id: 2 }
}
)
).toEqual({
show: true,
id: 1,
type: "danger",
message: "Test message"
})
})
})

@ -0,0 +1,64 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import reducer from "../buckets"
import * as actions from "../../actions/buckets"
describe("buckets reducer", () => {
it("should return the initial state", () => {
expect(reducer(undefined, {})).toEqual({
list: [],
filter: "",
currentBucket: ""
})
})
it("should handle SET_BUCKETS", () => {
expect(
reducer(undefined, { type: actions.SET_LIST, buckets: ["bk1", "bk2"] })
).toEqual({
list: ["bk1", "bk2"],
filter: "",
currentBucket: ""
})
})
it("should handle SET_BUCKETS_FILTER", () => {
expect(
reducer(undefined, {
type: actions.SET_FILTER,
filter: "test"
})
).toEqual({
list: [],
filter: "test",
currentBucket: ""
})
})
it("should handle SELECT_BUCKET", () => {
expect(
reducer(undefined, {
type: actions.SET_CURRENT_BUCKET,
bucket: "test"
})
).toEqual({
list: [],
filter: "",
currentBucket: "test"
})
})
})

@ -0,0 +1,41 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as actionsAlert from "../actions/alert"
const initialState = {
show: false,
type: "danger"
}
export default (state = initialState, action) => {
switch (action.type) {
case actionsAlert.SET:
return {
show: true,
id: action.alert.id,
type: action.alert.type,
message: action.alert.message
}
case actionsAlert.CLEAR:
if (action.alert && action.alert.id != state.id) {
return state
} else {
return initialState
}
default:
return state
}
}

@ -0,0 +1,42 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as actionsBuckets from "../actions/buckets"
export default (
state = { list: [], filter: "", currentBucket: "" },
action
) => {
switch (action.type) {
case actionsBuckets.SET_LIST:
return {
...state,
list: action.buckets
}
case actionsBuckets.SET_FILTER:
return {
...state,
filter: action.filter
}
case actionsBuckets.SET_CURRENT_BUCKET:
return {
...state,
currentBucket: action.bucket
}
default:
return state
}
}

@ -0,0 +1,26 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { combineReducers } from "redux"
import alert from "./alert"
import buckets from "./buckets"
const rootReducer = combineReducers({
alert,
buckets
})
export default rootReducer

@ -0,0 +1,38 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVisibleBuckets, getCurrentBucket } from "../buckets"
describe("getVisibleBuckets", () => {
let state
beforeEach(() => {
state = {
buckets: {
list: ["test1", "test11", "test2"]
}
}
})
it("should return all buckets if no filter specified", () => {
state.buckets.filter = ""
expect(getVisibleBuckets(state)).toEqual(["test1", "test11", "test2"])
})
it("should return all matching buckets if filter is specified", () => {
state.buckets.filter = "test1"
expect(getVisibleBuckets(state)).toEqual(["test1", "test11"])
})
})

@ -0,0 +1,28 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSelector } from "reselect"
const bucketsSelector = state => state.buckets.list
const bucketsFilterSelector = state => state.buckets.filter
export const getVisibleBuckets = createSelector(
bucketsSelector,
bucketsFilterSelector,
(buckets, filter) => buckets.filter(bucket => bucket.indexOf(filter) > -1)
)
export const getCurrentBucket = state => state.buckets.currentBucket

@ -0,0 +1,26 @@
/*
* Minio Cloud Storage (C) 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createStore, applyMiddleware } from "redux"
import thunkMiddleware from "redux-thunk"
import reducers from "../reducers/index"
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore)
export default function configureStore(initialState) {
const store = createStoreWithMiddleware(reducers, initialState)
return store
}

@ -1,5 +1,5 @@
/*
* Minio Cloud Storage (C) 2016 Minio, Inc.
* Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,17 +14,14 @@
* limitations under the License.
*/
import { browserHistory } from 'react-router'
import JSONrpc from './jsonrpc'
import * as actions from './actions'
import { minioBrowserPrefix } from './constants.js'
import Moment from 'moment'
import storage from 'local-storage-fallback'
export default class Web {
constructor(endpoint, dispatch) {
class Web {
constructor(endpoint) {
const namespace = 'Web'
this.dispatch = dispatch
this.JSONrpc = new JSONrpc({
endpoint,
namespace
@ -37,7 +34,7 @@ export default class Web {
.catch(err => {
if (err.status === 401) {
storage.removeItem('token')
browserHistory.push(`${minioBrowserPrefix}/login`)
location.reload()
throw new Error('Please re-login.')
}
if (err.status)
@ -128,3 +125,7 @@ export default class Web {
return this.makeCall('ListAllBucketPolicies', args)
}
}
const web = new Web(`${window.location.protocol}//${window.location.host}${minioBrowserPrefix}/webrpc`);
export default web;

@ -3,12 +3,16 @@
"version": "0.0.1",
"description": "Minio Browser",
"scripts": {
"test": "karma start",
"test": "jest",
"dev": "NODE_ENV=dev webpack-dev-server --devtool cheap-module-eval-source-map --progress --colors --hot --content-base dev",
"build": "NODE_ENV=dev node build.js",
"release": "NODE_ENV=production MINIO_UI_BUILD=RELEASE node build.js",
"format": "esformatter -i 'app/**/*.js'"
},
"jest": {
"setupTestFrameworkScriptFile": "./app/js/jest/setup.js",
"testURL": "https://localhost:8080"
},
"repository": {
"type": "git",
"url": "https://github.com/minio/minio"
@ -23,6 +27,7 @@
"async": "^1.5.2",
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-jest": "^22.1.0",
"babel-loader": "^7.1.2",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
@ -32,20 +37,17 @@
"babel-register": "^6.14.0",
"copy-webpack-plugin": "^0.3.3",
"css-loader": "^0.23.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"esformatter": "^0.10.0",
"esformatter-jsx": "^7.4.1",
"esformatter-jsx-ignore": "^1.0.6",
"html-webpack-plugin": "^2.30.1",
"jest": "^22.1.4",
"jest-enzyme": "^4.0.2",
"json-loader": "^0.5.4",
"karma": "^0.13.22",
"karma-chrome-launcher": "^0.2.3",
"karma-cli": "^0.1.2",
"karma-firefox-launcher": "^0.1.7",
"karma-mocha": "^0.2.2",
"karma-webpack": "^2.0.9",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"mocha": "^2.5.3",
"purifycss-webpack-plugin": "^2.0.3",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
@ -56,28 +58,30 @@
"classnames": "^2.2.3",
"expect": "^1.20.2",
"font-awesome": "^4.7.0",
"history": "^1.17.0",
"history": "^4.7.2",
"humanize": "0.0.9",
"json-loader": "^0.5.4",
"local-storage-fallback": "^1.3.0",
"local-storage-fallback": "^4.0.2",
"material-design-iconic-font": "^2.2.0",
"mime-db": "^1.25.0",
"mime-types": "^2.1.13",
"moment": "^2.15.1",
"react": "^0.14.8",
"react": "^16.2.0",
"react-addons-test-utils": "^0.14.8",
"react-bootstrap": "^0.28.5",
"react-copy-to-clipboard": "^4.2.3",
"react-custom-scrollbars": "^2.2.2",
"react-dom": "^0.14.6",
"react-dropzone": "^3.5.3",
"react-bootstrap": "^0.32.0",
"react-copy-to-clipboard": "^5.0.1",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.2.0",
"react-dropzone": "^4.2.3",
"react-infinite-scroller": "^1.0.6",
"react-onclickout": "2.0.4",
"react-redux": "^4.4.5",
"react-router": "^2.8.1",
"redux": "^3.6.0",
"redux-thunk": "^1.0.3",
"superagent": "^1.8.4",
"react-redux": "^5.0.6",
"react-router-dom": "^4.2.0",
"redux": "^3.7.2",
"redux-mock-store": "^1.5.1",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",
"superagent": "^3.8.2",
"superagent-es6-promise": "^1.0.0",
"webpack": "^3.10.0"
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save