diff --git a/browser/.babelrc b/browser/.babelrc
new file mode 100644
index 000000000..9cd3d553f
--- /dev/null
+++ b/browser/.babelrc
@@ -0,0 +1,8 @@
+{
+ "presets": [
+ "es2015",
+ "react"
+ ],
+
+ "plugins": ["transform-object-rest-spread"]
+}
diff --git a/browser/.editorconfig b/browser/.editorconfig
new file mode 100644
index 000000000..92926b6de
--- /dev/null
+++ b/browser/.editorconfig
@@ -0,0 +1,16 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.json]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/browser/.esformatter b/browser/.esformatter
new file mode 100644
index 000000000..1677d7c4b
--- /dev/null
+++ b/browser/.esformatter
@@ -0,0 +1,23 @@
+{
+ "plugins": [
+ "esformatter-jsx"
+ ],
+ // Copied from https://github.com/royriojas/esformatter-jsx
+ "jsx": {
+ "formatJSX": true, //Duh! that's the default
+ "attrsOnSameLineAsTag": false, // move each attribute to its own line
+ "maxAttrsOnTag": 3, // if lower or equal than 3 attributes, they will be kept on a single line
+ "firstAttributeOnSameLine": true, // keep the first attribute in the same line as the tag
+ "formatJSXExpressions": true, // default true, if false jsxExpressions won't be recursively formatted
+ "JSXExpressionsSingleLine": true, // default true, if false the JSXExpressions might span several lines
+ "alignWithFirstAttribute": false, // do not align attributes with the first tag
+ "spaceInJSXExpressionContainers": " ", // default to one space. Make it empty if you don't like spaces between JSXExpressionContainers
+ "removeSpaceBeforeClosingJSX": false, // default false. if true =>
+ "closingTagOnNewLine": false, // default false. if true attributes on multiple lines will close the tag on a new line
+ "JSXAttributeQuotes": "", // possible values "single" or "double". Leave it as empty string if you don't want to modify the attributes' quotes
+ "htmlOptions": {
+ // put here the options for js-beautify.html
+ }
+ }
+}
+
diff --git a/browser/README.md b/browser/README.md
new file mode 100644
index 000000000..74bb563f2
--- /dev/null
+++ b/browser/README.md
@@ -0,0 +1,37 @@
+# Minio File Browser
+
+``Minio Browser`` provides minimal set of UI to manage buckets and objects on ``minio`` server. ``Minio Browser`` is written in javascript and released under [Apache 2.0 License](./LICENSE).
+
+## Installation
+
+### Install yarn:
+```sh
+$ curl -o- -L https://yarnpkg.com/install.sh | bash
+$ yarn
+```
+
+### Install `go-bindata` and `go-bindata-assetfs`.
+
+If you do not have a working Golang environment, please follow [Install Golang](https://docs.minio.io/docs/how-to-install-golang)
+
+```sh
+$ go get github.com/jteeuwen/go-bindata/...
+$ go get github.com/elazarl/go-bindata-assetfs/...
+```
+
+## Generating Assets.
+
+### Generate ui-assets.go
+
+```sh
+$ yarn release
+```
+This generates ui-assets.go in the current direcotry. Now do `make` in the parent directory to build the minio binary with the newly generated ui-assets.go
+
+### Run Minio Browser with live reload.
+
+```sh
+$ yarn dev
+```
+
+Open [http://localhost:8080/minio/](http://localhost:8080/minio/) in your browser to play with the application
diff --git a/browser/app/css/loader.css b/browser/app/css/loader.css
new file mode 100644
index 000000000..d7ae07b02
--- /dev/null
+++ b/browser/app/css/loader.css
@@ -0,0 +1,98 @@
+.page-load {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background: #32393F;
+ z-index: 100;
+ transition: opacity 200ms;
+ -webkit-transition: opacity 200ms;
+}
+
+.pl-0{
+ opacity: 0;
+}
+
+.pl-1 {
+ display: none;
+}
+
+.pl-inner {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ left: 50%;
+ margin-left: -50px;
+ top: 50%;
+ margin-top: -50px;
+ text-align: center;
+ -webkit-animation: fade-in 500ms;
+ animation: fade-in 500ms;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+ animation-delay: 350ms;
+ -webkit-animation-delay: 350ms;
+ -webkit-backface-visibility: visible;
+ backface-visibility: visible;
+}
+
+.pl-inner:before {
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ display: block;
+ -webkit-animation: spin 1000ms infinite linear;
+ animation: spin 1000ms infinite linear;
+ border: 1px solid rgba(255, 255, 255, 0.2);;
+ border-left-color: #fff;
+ border-radius: 50%;
+}
+
+.pl-inner > img {
+ width: 30px;
+ margin-top: 28px;
+}
+
+@-webkit-keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@-webkit-keyframes spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
diff --git a/browser/app/fonts/lato/lato-normal.woff b/browser/app/fonts/lato/lato-normal.woff
new file mode 100755
index 000000000..f2317755c
Binary files /dev/null and b/browser/app/fonts/lato/lato-normal.woff differ
diff --git a/browser/app/fonts/lato/lato-normal.woff2 b/browser/app/fonts/lato/lato-normal.woff2
new file mode 100755
index 000000000..2a119ebd5
Binary files /dev/null and b/browser/app/fonts/lato/lato-normal.woff2 differ
diff --git a/browser/app/img/arrow.svg b/browser/app/img/arrow.svg
new file mode 100644
index 000000000..fb5574ff8
--- /dev/null
+++ b/browser/app/img/arrow.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/browser/app/img/browsers/chrome.png b/browser/app/img/browsers/chrome.png
new file mode 100644
index 000000000..278ef4d15
Binary files /dev/null and b/browser/app/img/browsers/chrome.png differ
diff --git a/browser/app/img/browsers/firefox.png b/browser/app/img/browsers/firefox.png
new file mode 100644
index 000000000..2803f10a7
Binary files /dev/null and b/browser/app/img/browsers/firefox.png differ
diff --git a/browser/app/img/browsers/safari.png b/browser/app/img/browsers/safari.png
new file mode 100644
index 000000000..4ed52b904
Binary files /dev/null and b/browser/app/img/browsers/safari.png differ
diff --git a/browser/app/img/favicon.ico b/browser/app/img/favicon.ico
new file mode 100644
index 000000000..0718efa83
Binary files /dev/null and b/browser/app/img/favicon.ico differ
diff --git a/browser/app/img/logo.svg b/browser/app/img/logo.svg
new file mode 100644
index 000000000..e4fa2b767
--- /dev/null
+++ b/browser/app/img/logo.svg
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+ Minio Logo
+
+
+
+
+
+
+
+
diff --git a/browser/app/img/more-h-light.svg b/browser/app/img/more-h-light.svg
new file mode 100644
index 000000000..0c2e2da60
--- /dev/null
+++ b/browser/app/img/more-h-light.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/browser/app/img/more-h.svg b/browser/app/img/more-h.svg
new file mode 100644
index 000000000..cf69dcf6b
--- /dev/null
+++ b/browser/app/img/more-h.svg
@@ -0,0 +1 @@
+
diff --git a/browser/app/img/select-caret.svg b/browser/app/img/select-caret.svg
new file mode 100644
index 000000000..b2b26b86b
--- /dev/null
+++ b/browser/app/img/select-caret.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/browser/app/index.html b/browser/app/index.html
new file mode 100644
index 000000000..dfc0f555b
--- /dev/null
+++ b/browser/app/index.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Minio Browser
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/app/index.js b/browser/app/index.js
new file mode 100644
index 000000000..d750577bc
--- /dev/null
+++ b/browser/app/index.js
@@ -0,0 +1,116 @@
+/*
+ * Minio Browser (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 './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 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
+ { props.children }
+
+}
+
+ReactDOM.render((
+
+
+
+
+
+
+
+
+
+
+
+
+ ), 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')
+}
diff --git a/browser/app/js/__tests__/jsonrpc-test.js b/browser/app/js/__tests__/jsonrpc-test.js
new file mode 100644
index 000000000..341d0c286
--- /dev/null
+++ b/browser/app/js/__tests__/jsonrpc-test.js
@@ -0,0 +1,43 @@
+/*
+ * Minio Browser (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 expect from 'expect';
+import JSONrpc from '../jsonrpc';
+
+describe('jsonrpc', () => {
+ it('should fail with invalid endpoint', (done) => {
+ try {
+ let jsonRPC = new JSONrpc({
+ endpoint: 'htt://localhost:9000',
+ namespace: 'Test'
+ });
+ } catch (e) {
+ done();
+ }
+ });
+ 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');
+ });
+});
+
diff --git a/browser/app/js/actions.js b/browser/app/js/actions.js
new file mode 100644
index 000000000..598124b84
--- /dev/null
+++ b/browser/app/js/actions.js
@@ -0,0 +1,509 @@
+/*
+ * Minio Browser (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 url from 'url'
+import Moment from 'moment'
+import web from './web'
+import * as utils from './utils'
+import storage from 'local-storage-fallback'
+
+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 ADD_OBJECT = 'ADD_OBJECT'
+export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS'
+export const SET_OBJECTS = 'SET_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 showDeleteConfirmation = (object) => {
+ return {
+ type: DELETE_CONFIRMATION,
+ payload: {
+ object,
+ show: true
+ }
+ }
+}
+
+export const hideDeleteConfirmation = () => {
+ return {
+ type: DELETE_CONFIRMATION,
+ payload: {
+ object: '',
+ show: false
+ }
+ }
+}
+
+export const showShareObject = url => {
+ return {
+ type: SET_SHARE_OBJECT,
+ shareObject: {
+ url: url,
+ show: true
+ }
+ }
+}
+
+export const hideShareObject = () => {
+ return {
+ type: SET_SHARE_OBJECT,
+ shareObject: {
+ url: '',
+ show: false
+ }
+ }
+}
+
+export const shareObject = (object, expiry) => (dispatch, getState) => {
+ const {currentBucket, web} = getState()
+ let host = location.host
+ let bucket = currentBucket
+
+ if (!web.LoggedIn()) {
+ dispatch(showShareObject(`${host}/${bucket}/${object}`))
+ return
+ }
+ web.PresignedGet({
+ host,
+ bucket,
+ object,
+ expiry
+ })
+ .then(obj => {
+ dispatch(showShareObject(obj.url))
+ })
+ .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 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 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
+ }
+}
+
+export const setObjects = (objects) => {
+ return {
+ type: SET_OBJECTS,
+ 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 selectPrefix = prefix => {
+ return (dispatch, getState) => {
+ const {currentBucket, web} = getState()
+ dispatch(setLoadPath(prefix))
+ web.ListObjects({
+ bucketName: currentBucket,
+ prefix
+ })
+ .then(res => {
+ let objects = res.objects
+ if (!objects)
+ objects = []
+ dispatch(setObjects(
+ utils.sortObjectsByName(objects.map(object => {
+ object.name = object.name.replace(`${prefix}`, ''); return object
+ }))
+ ))
+ 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(''))
+ })
+ }
+}
+
+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 uploadFile = (file, xhr) => {
+ return (dispatch, getState) => {
+ const {currentBucket, currentPath} = getState()
+ const objectName = `${currentPath}${file.name}`
+ const uploadUrl = `${window.location.origin}/minio/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 || xhr.status == 500) {
+ setShowAbortModal(false)
+ dispatch(stopUpload({
+ slug
+ }))
+ dispatch(showAlert({
+ type: 'danger',
+ message: 'Unauthorized request.'
+ }))
+ }
+ 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
+ }
+}
diff --git a/browser/app/js/components/Browse.js b/browser/app/js/components/Browse.js
new file mode 100644
index 000000000..671552529
--- /dev/null
+++ b/browser/app/js/components/Browse.js
@@ -0,0 +1,734 @@
+/*
+ * Minio Browser (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'
+
+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,
+ envVars: res.MinioEnvVars
+ })
+ 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 + '/') {
+ dispatch(actions.setCurrentBucket(''))
+ dispatch(actions.setCurrentPath(''))
+ dispatch(actions.setObjects([]))
+ 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)))
+ }
+
+ 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 {
+ window.location = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=${storage.getItem('token')}`
+ }
+ }
+
+ 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())
+ }
+
+ showBucketPolicy(e) {
+ e.preventDefault()
+ const {dispatch} = this.props
+ dispatch(actions.showBucketPolicy())
+ }
+
+ hideBucketPolicy(e) {
+ e.preventDefault()
+ const {dispatch} = this.props
+ dispatch(actions.hideBucketPolicy())
+ }
+
+ uploadFile(e) {
+ e.preventDefault()
+ const {dispatch, buckets} = this.props
+
+ if (buckets.length === 0) {
+ dispatch(actions.showAlert({
+ type: 'danger',
+ message: "Bucket needs to be created 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} = this.props
+ web.RemoveObject({
+ bucketName: currentBucket,
+ objectName: deleteConfirmation.object
+ })
+ .then(() => {
+ this.hideDeleteConfirmation()
+ dispatch(actions.selectPrefix(currentPath))
+ })
+ .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
+ dispatch(actions.shareObject(object))
+ }
+
+ 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`)
+ }
+
+ landingPage(e) {
+ e.preventDefault()
+ this.props.dispatch(actions.selectBucket(this.props.buckets[0]))
+ }
+
+ 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) {
+ inc === -1 ? this.refs[targetInput].stepDown(1) : this.refs[targetInput].stepUp(1)
+
+ if (this.refs.expireDays.value == 7) {
+ this.refs.expireHours.value = 0
+ this.refs.expireMins.value = 0
+ }
+ }
+
+
+ render() {
+ const {total, free} = this.props.storageInfo
+ const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy} = 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} = 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 ? :
+
+ let alertBox =
+
+ { alert.message }
+
+
+ // Make sure you don't show a fading out alert box on the initial web-page load.
+ if (!alert.message)
+ alertBox = ''
+
+ let signoutTooltip =
+ Sign out
+
+ let uploadTooltip =
+ Upload file
+
+ let makeBucketTooltip =
+ Create bucket
+
+ let loginButton = ''
+ let browserDropdownButton = ''
+ let storageUsageDetails = ''
+
+ let used = total - free
+ let usedPercent = (used / total) * 100 + '%'
+ let freePercent = free * 100 / total
+
+ if (web.LoggedIn()) {
+ browserDropdownButton =
+ } else {
+ loginButton = Login
+ }
+
+ if (web.LoggedIn()) {
+ storageUsageDetails =
+
+
+
+ Used:
+ { humanize.filesize(total - free) }
+
+
+ Free:
+ { humanize.filesize(total - used) }
+
+
+
+
+ }
+
+ let createButton = ''
+ if (web.LoggedIn()) {
+ createButton =
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } else {
+ if (prefixWritable)
+ createButton =
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+ return (
+
+
+
+
+ { alertBox }
+
+
+
+
+
+
+ { storageUsageDetails }
+
+
+ { loginButton }
+ { browserDropdownButton }
+
+
+
+
+
+ Name
+
+
+
+ Size
+
+
+
+ Last Modified
+
+
+
+
+
+
+
+
+
+ { createButton }
+
+
+ ×
+
+
+
+
+
+
+
+ ×
+
+
+
+
+
+
+
+
+
+ Version
+
+ { version }
+
+
+
+ Memory
+
+ { memory }
+
+
+
+ Platform
+
+ { platform }
+
+
+
+ Runtime
+
+ { runtime }
+
+
+
+
+
+
+
+ Bucket Policy (
+ { currentBucket })
+
+ ×
+
+
+
+
+ { policies.map((policy, i) =>
+ ) }
+
+
+
+
+
+
+ Share Object
+
+
+
+
+ Shareable Link
+
+
+
+
+
+ Expires in
+
+
+
+
+
+
+
+ Minutes
+
+
+
+
+
+
+
+
+
+
+
+
+ Copy Link
+
+
+
+ Cancel
+
+
+
+ { settingsModal }
+
+
+
+ )
+ }
+}
diff --git a/browser/app/js/components/BrowserDropdown.js b/browser/app/js/components/BrowserDropdown.js
new file mode 100644
index 000000000..1aa272551
--- /dev/null
+++ b/browser/app/js/components/BrowserDropdown.js
@@ -0,0 +1,56 @@
+/*
+ * Minio Browser (C) 2016, 2017 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/lib/components/connect'
+import Dropdown from 'react-bootstrap/lib/Dropdown'
+
+let BrowserDropdown = ({fullScreen, showAbout, showSettings, logout}) => {
+ return (
+
+
+
+ )
+}
+
+export default connect(state => state)(BrowserDropdown)
diff --git a/browser/app/js/components/BrowserUpdate.js b/browser/app/js/components/BrowserUpdate.js
new file mode 100644
index 000000000..be4c7af3a
--- /dev/null
+++ b/browser/app/js/components/BrowserUpdate.js
@@ -0,0 +1,42 @@
+/*
+ * Minio Browser (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 connect from 'react-redux/lib/components/connect'
+
+import Tooltip from 'react-bootstrap/lib/Tooltip'
+import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
+
+let BrowserUpdate = ({latestUiVersion}) => {
+ // Don't show an update if we're already updated!
+ if (latestUiVersion === currentUiVersion) return ( )
+
+ return (
+
+
+
+ New update available. Click to refresh.
+ }>
+
+
+ )
+}
+
+export default connect(state => {
+ return {
+ latestUiVersion: state.latestUiVersion
+ }
+})(BrowserUpdate)
diff --git a/browser/app/js/components/ConfirmModal.js b/browser/app/js/components/ConfirmModal.js
new file mode 100644
index 000000000..fd98fa313
--- /dev/null
+++ b/browser/app/js/components/ConfirmModal.js
@@ -0,0 +1,50 @@
+/*
+ * Minio Browser (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 Modal from 'react-bootstrap/lib/Modal'
+import ModalBody from 'react-bootstrap/lib/ModalBody'
+
+let ConfirmModal = ({baseClass, icon, text, sub, okText, cancelText, okHandler, cancelHandler, show}) => {
+ return (
+
+
+
+
+
+
+ { text }
+
+
+ { sub }
+
+
+
+
+ { okText }
+
+
+ { cancelText }
+
+
+
+ )
+}
+
+export default ConfirmModal
diff --git a/browser/app/js/components/Dropzone.js b/browser/app/js/components/Dropzone.js
new file mode 100644
index 000000000..0ddab2661
--- /dev/null
+++ b/browser/app/js/components/Dropzone.js
@@ -0,0 +1,65 @@
+/*
+ * Minio Browser (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 ReactDropzone from 'react-dropzone'
+import * as actions from '../actions'
+
+// Dropzone is a drag-and-drop element for uploading files. It will create a
+// landing zone of sorts that automatically receives the files.
+export default class Dropzone extends React.Component {
+
+ onDrop(files) {
+ // FIXME: Currently you can upload multiple files, but only one abort
+ // modal will be shown, and progress updates will only occur for one
+ // file at a time. See #171.
+ files.forEach(file => {
+ let req = new XMLHttpRequest()
+
+ // Dispatch the upload.
+ web.dispatch(actions.uploadFile(file, req))
+ })
+ }
+
+ render() {
+ // Overwrite the default styling from react-dropzone; otherwise it
+ // won't handle child elements correctly.
+ const style = {
+ height: '100%',
+ borderWidth: '2px',
+ borderStyle: 'dashed',
+ borderColor: '#fff'
+ }
+ const activeStyle = {
+ borderColor: '#777'
+ }
+ const rejectStyle = {
+ backgroundColor: '#ffdddd'
+ }
+
+ // disableClick means that it won't trigger a file upload box when
+ // the user clicks on a file.
+ return (
+
+ { this.props.children }
+
+ )
+ }
+}
diff --git a/browser/app/js/components/InputGroup.js b/browser/app/js/components/InputGroup.js
new file mode 100644
index 000000000..c2b0e2ab2
--- /dev/null
+++ b/browser/app/js/components/InputGroup.js
@@ -0,0 +1,49 @@
+/*
+ * Minio Browser (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'
+
+let InputGroup = ({label, id, name, value, onChange, type, spellCheck, required, readonly, autoComplete, align, className}) => {
+ var input =
+ if (readonly)
+ input =
+ return
+ { input }
+
+
+ { label }
+
+
+}
+
+export default InputGroup
diff --git a/browser/app/js/components/Login.js b/browser/app/js/components/Login.js
new file mode 100644
index 000000000..b5db1a872
--- /dev/null
+++ b/browser/app/js/components/Login.js
@@ -0,0 +1,133 @@
+/*
+ * Minio Browser (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 logo from '../../img/logo.svg'
+import Alert from 'react-bootstrap/lib/Alert'
+import * as actions from '../actions'
+import InputGroup from '../components/InputGroup'
+
+export default 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'
+ }
+ if (!document.getElementById('secretKey').value) {
+ message = 'Access Key cannot be empty'
+ }
+ if (message) {
+ dispatch(actions.showAlert({
+ type: 'danger',
+ message
+ }))
+ return
+ }
+ web.Login({
+ username: document.getElementById('accessKey').value,
+ password: document.getElementById('secretKey').value
+ })
+ .then((res) => {
+ this.context.router.push(loginRedirectPath)
+ })
+ .catch(e => {
+ dispatch(actions.setLoginError())
+ dispatch(actions.showAlert({
+ type: 'danger',
+ message: e.message
+ }))
+ })
+ }
+
+ componentWillMount() {
+ 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')
+ }
+
+ componentWillUnmount() {
+ document.body.classList.remove('is-guest')
+ }
+
+ hideAlert() {
+ const {dispatch} = this.props
+ dispatch(actions.hideAlert())
+ }
+
+ render() {
+ const {alert} = this.props
+ let alertBox =
+
+ { alert.message }
+
+
+ // Make sure you don't show a fading out alert box on the initial web-page load.
+ if (!alert.message)
+ alertBox = ''
+ return (
+
+ { alertBox }
+
+
+
+
+
+
+ { window.location.host }
+
+
+
+ )
+ }
+}
+
+Login.contextTypes = {
+ router: React.PropTypes.object.isRequired
+}
diff --git a/browser/app/js/components/ObjectsList.js b/browser/app/js/components/ObjectsList.js
new file mode 100644
index 000000000..623594218
--- /dev/null
+++ b/browser/app/js/components/ObjectsList.js
@@ -0,0 +1,75 @@
+/*
+ * Minio Browser (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 Moment from 'moment'
+import humanize from 'humanize'
+import connect from 'react-redux/lib/components/connect'
+import Dropdown from 'react-bootstrap/lib/Dropdown'
+
+
+let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath}) => {
+ const list = objects.map((object, i) => {
+ let size = object.name.endsWith('/') ? '-' : humanize.filesize(object.size)
+ let lastModified = object.name.endsWith('/') ? '-' : Moment(object.lastModified).format('lll')
+ let loadingClass = loadPath === `${currentPath}${object.name}` ? 'fesl-loading' : ''
+ let actionButtons = ''
+ let deleteButton = ''
+ if (web.LoggedIn())
+ deleteButton = showDeleteConfirmation(e, `${currentPath}${object.name}`) }>
+ if (!object.name.endsWith('/')) {
+ actionButtons =
+
+
+ shareObject(e, `${currentPath}${object.name}`) }>
+ { deleteButton }
+
+
+ }
+ return (
+
+
+
+ { size }
+
+
+ { lastModified }
+
+
+ { actionButtons }
+
+
+ )
+ })
+ return (
+
+ { list }
+
+ )
+}
+
+// Subscribe it to state changes.
+export default connect(state => {
+ return {
+ objects: state.objects,
+ currentPath: state.currentPath,
+ loadPath: state.loadPath
+ }
+})(ObjectsList)
diff --git a/browser/app/js/components/Path.js b/browser/app/js/components/Path.js
new file mode 100644
index 000000000..6ca85869b
--- /dev/null
+++ b/browser/app/js/components/Path.js
@@ -0,0 +1,41 @@
+/*
+ * Minio Browser (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 connect from 'react-redux/lib/components/connect'
+
+let Path = ({currentBucket, currentPath, selectPrefix}) => {
+ let dirPath = []
+ let path = ''
+ if (currentPath) {
+ path = currentPath.split('/').map((dir, i) => {
+ dirPath.push(dir)
+ let dirPath_ = dirPath.join('/') + '/'
+ return selectPrefix(e, dirPath_) }>{ dir }
+ })
+ }
+
+ return (
+
+ )
+}
+
+export default connect(state => {
+ return {
+ currentBucket: state.currentBucket,
+ currentPath: state.currentPath
+ }
+})(Path)
diff --git a/browser/app/js/components/Policy.js b/browser/app/js/components/Policy.js
new file mode 100644
index 000000000..65930ad64
--- /dev/null
+++ b/browser/app/js/components/Policy.js
@@ -0,0 +1,80 @@
+import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
+
+import React, { Component, PropTypes } from 'react'
+import connect from 'react-redux/lib/components/connect'
+import classnames from 'classnames'
+import * as actions from '../actions'
+
+class Policy extends Component {
+ constructor(props, context) {
+ super(props, context)
+ this.state = {}
+ }
+
+ handlePolicyChange(e) {
+ this.setState({
+ policy: {
+ policy: e.target.value
+ }
+ })
+ }
+
+ removePolicy(e) {
+ e.preventDefault()
+ const {dispatch, currentBucket, prefix} = this.props
+ let newPrefix = prefix.replace(currentBucket + '/', '')
+ newPrefix = newPrefix.replace('*', '')
+ web.SetBucketPolicy({
+ bucketName: currentBucket,
+ prefix: newPrefix,
+ policy: 'none'
+ })
+ .then(() => {
+ dispatch(actions.setPolicies(this.props.policies.filter(policy => policy.prefix != prefix)))
+ })
+ .catch(e => dispatch(actions.showAlert({
+ type: 'danger',
+ message: e.message,
+ })))
+ }
+
+ render() {
+ const {policy, prefix, currentBucket} = this.props
+ let newPrefix = prefix.replace(currentBucket + '/', '')
+ newPrefix = newPrefix.replace('*', '')
+
+ if (!newPrefix)
+ newPrefix = '*'
+
+ return (
+
+
+ { newPrefix }
+
+
+
+
+ Read Only
+
+
+ Write Only
+
+
+ Read and Write
+
+
+
+
+
+ Remove
+
+
+
+ )
+ }
+}
+
+export default connect(state => state)(Policy)
diff --git a/browser/app/js/components/PolicyInput.js b/browser/app/js/components/PolicyInput.js
new file mode 100644
index 000000000..75809df96
--- /dev/null
+++ b/browser/app/js/components/PolicyInput.js
@@ -0,0 +1,83 @@
+import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
+import React, { Component, PropTypes } from 'react'
+import connect from 'react-redux/lib/components/connect'
+import classnames from 'classnames'
+import * as actions from '../actions'
+
+class PolicyInput extends Component {
+ componentDidMount() {
+ const {web, dispatch} = this.props
+ web.ListAllBucketPolicies({
+ bucketName: this.props.currentBucket
+ }).then(res => {
+ let policies = res.policies
+ if (policies) dispatch(actions.setPolicies(policies))
+ }).catch(err => {
+ dispatch(actions.showAlert({
+ type: 'danger',
+ message: err.message
+ }))
+ })
+ }
+
+ componentWillUnmount() {
+ const {dispatch} = this.props
+ dispatch(actions.setPolicies([]))
+ }
+
+ handlePolicySubmit(e) {
+ e.preventDefault()
+ const {web, dispatch} = this.props
+
+ web.SetBucketPolicy({
+ bucketName: this.props.currentBucket,
+ prefix: this.prefix.value,
+ policy: this.policy.value
+ })
+ .then(() => {
+ dispatch(actions.setPolicies([{
+ policy: this.policy.value,
+ prefix: this.prefix.value + '*',
+ }, ...this.props.policies]))
+ this.prefix.value = ''
+ })
+ .catch(e => dispatch(actions.showAlert({
+ type: 'danger',
+ message: e.message,
+ })))
+ }
+
+ render() {
+ return (
+
+
+ this.prefix = prefix }
+ className="form-control"
+ placeholder="Prefix"
+ editable={ true } />
+
+
+ this.policy = policy } className="form-control">
+
+ Read Only
+
+
+ Write Only
+
+
+ Read and Write
+
+
+
+
+
+ Add
+
+
+
+ )
+ }
+}
+
+export default connect(state => state)(PolicyInput)
diff --git a/browser/app/js/components/SettingsModal.js b/browser/app/js/components/SettingsModal.js
new file mode 100644
index 000000000..51bd4333b
--- /dev/null
+++ b/browser/app/js/components/SettingsModal.js
@@ -0,0 +1,215 @@
+/*
+ * Minio Browser (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 connect from 'react-redux/lib/components/connect'
+import * as actions from '../actions'
+
+import Tooltip from 'react-bootstrap/lib/Tooltip'
+import Modal from 'react-bootstrap/lib/Modal'
+import ModalBody from 'react-bootstrap/lib/ModalBody'
+import ModalHeader from 'react-bootstrap/lib/ModalHeader'
+import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
+import InputGroup from './InputGroup'
+
+class SettingsModal extends React.Component {
+
+ // When the settings are shown, it loads the access key and secret key.
+ componentWillMount() {
+ const {web, dispatch} = this.props
+ const {serverInfo} = this.props
+
+ let accessKeyEnv = ''
+ let secretKeyEnv = ''
+ // Check environment variables first. They may or may not have been
+ // loaded already; they load in Browse#componentDidMount.
+ if (serverInfo.envVars) {
+ serverInfo.envVars.forEach(envVar => {
+ let keyVal = envVar.split('=')
+ if (keyVal[0] == 'MINIO_ACCESS_KEY') {
+ accessKeyEnv = keyVal[1]
+ } else if (keyVal[0] == 'MINIO_SECRET_KEY') {
+ secretKeyEnv = keyVal[1]
+ }
+ })
+ }
+ if (accessKeyEnv != '' || secretKeyEnv != '') {
+ dispatch(actions.setSettings({
+ accessKey: accessKeyEnv,
+ secretKey: secretKeyEnv,
+ keysReadOnly: true
+ }))
+ } else {
+ web.GetAuth()
+ .then(data => {
+ dispatch(actions.setSettings({
+ accessKey: data.accessKey,
+ secretKey: data.secretKey
+ }))
+ })
+ }
+ }
+
+ // When they are re-hidden, the keys are unloaded from memory.
+ componentWillUnmount() {
+ const {dispatch} = this.props
+
+ dispatch(actions.setSettings({
+ accessKey: '',
+ secretKey: '',
+ secretKeyVisible: false
+ }))
+ dispatch(actions.hideSettings())
+ }
+
+ // Handle field changes from inside the modal.
+ accessKeyChange(e) {
+ const {dispatch} = this.props
+ dispatch(actions.setSettings({
+ accessKey: e.target.value
+ }))
+ }
+
+ secretKeyChange(e) {
+ const {dispatch} = this.props
+ dispatch(actions.setSettings({
+ secretKey: e.target.value
+ }))
+ }
+
+ secretKeyVisible(secretKeyVisible) {
+ const {dispatch} = this.props
+ dispatch(actions.setSettings({
+ secretKeyVisible
+ }))
+ }
+
+ // Save the auth params and set them.
+ setAuth(e) {
+ e.preventDefault()
+ const {web, dispatch} = this.props
+
+ let accessKey = document.getElementById('accessKey').value
+ let secretKey = document.getElementById('secretKey').value
+ web.SetAuth({
+ accessKey,
+ secretKey
+ })
+ .then(data => {
+ dispatch(actions.setSettings({
+ accessKey: '',
+ secretKey: '',
+ secretKeyVisible: false
+ }))
+ dispatch(actions.hideSettings())
+ dispatch(actions.showAlert({
+ type: 'success',
+ message: 'Changed credentials'
+ }))
+ })
+ .catch(err => {
+ dispatch(actions.setSettings({
+ accessKey: '',
+ secretKey: '',
+ secretKeyVisible: false
+ }))
+ dispatch(actions.hideSettings())
+ dispatch(actions.showAlert({
+ type: 'danger',
+ message: err.message
+ }))
+ })
+ }
+
+ generateAuth(e) {
+ e.preventDefault()
+ const {dispatch} = this.props
+
+ web.GenerateAuth()
+ .then(data => {
+ dispatch(actions.setSettings({
+ secretKeyVisible: true
+ }))
+ dispatch(actions.setSettings({
+ accessKey: data.accessKey,
+ secretKey: data.secretKey
+ }))
+ })
+ }
+
+ hideSettings(e) {
+ e.preventDefault()
+
+ const {dispatch} = this.props
+ dispatch(actions.hideSettings())
+ }
+
+ render() {
+ let {settings} = this.props
+
+ return (
+
+
+ Change Password
+
+
+
+
+
+
+
+
+ Generate
+
+
+ Update
+
+
+ Cancel
+
+
+
+ )
+ }
+}
+
+export default connect(state => {
+ return {
+ web: state.web,
+ settings: state.settings,
+ serverInfo: state.serverInfo
+ }
+})(SettingsModal)
diff --git a/browser/app/js/components/SideBar.js b/browser/app/js/components/SideBar.js
new file mode 100644
index 000000000..ad4aee576
--- /dev/null
+++ b/browser/app/js/components/SideBar.js
@@ -0,0 +1,85 @@
+/*
+ * Minio Browser (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 ClickOutHandler from 'react-onclickout'
+import Scrollbars from 'react-custom-scrollbars/lib/Scrollbars'
+import connect from 'react-redux/lib/components/connect'
+
+import logo from '../../img/logo.svg'
+
+let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, landingPage, sidebarStatus, clickOutside, showPolicy}) => {
+
+ const list = visibleBuckets.map((bucket, i) => {
+ return selectBucket(e, bucket) }>
+
+ { bucket }
+
+
+
+ })
+
+ return (
+
+
+
+ )
+}
+
+// Subscribe it to state changes that affect only the sidebar.
+export default connect(state => {
+ return {
+ visibleBuckets: state.visibleBuckets,
+ loadBucket: state.loadBucket,
+ currentBucket: state.currentBucket,
+ sidebarStatus: state.sidebarStatus
+ }
+})(SideBar)
diff --git a/browser/app/js/components/UploadModal.js b/browser/app/js/components/UploadModal.js
new file mode 100644
index 000000000..6658ab225
--- /dev/null
+++ b/browser/app/js/components/UploadModal.js
@@ -0,0 +1,141 @@
+/*
+ * Minio Browser (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 humanize from 'humanize'
+import classNames from 'classnames'
+import connect from 'react-redux/lib/components/connect'
+
+import ProgressBar from 'react-bootstrap/lib/ProgressBar'
+import ConfirmModal from './ConfirmModal'
+
+import * as actions from '../actions'
+
+// UploadModal is a modal that handles multiple file uploads.
+// During the upload, it displays a progress bar, and can transform into an
+// abort modal if the user decides to abort the uploads.
+class UploadModal extends React.Component {
+
+ // Abort all the current uploads.
+ abortUploads(e) {
+ e.preventDefault()
+ const {dispatch, uploads} = this.props
+
+ for (var slug in uploads) {
+ let upload = uploads[slug]
+ upload.xhr.abort()
+ dispatch(actions.stopUpload({
+ slug
+ }))
+ }
+
+ this.hideAbort(e)
+ }
+
+ // Show the abort modal instead of the progress modal.
+ showAbort(e) {
+ e.preventDefault()
+ const {dispatch} = this.props
+
+ dispatch(actions.setShowAbortModal(true))
+ }
+
+ // Show the progress modal instead of the abort modal.
+ hideAbort(e) {
+ e.preventDefault()
+ const {dispatch} = this.props
+
+ dispatch(actions.setShowAbortModal(false))
+ }
+
+ render() {
+ const {uploads, showAbortModal} = this.props
+
+ // Show the abort modal.
+ if (showAbortModal) {
+ let baseClass = classNames({
+ 'abort-upload': true
+ })
+ let okIcon = classNames({
+ 'fa': true,
+ 'fa-times': true
+ })
+ let cancelIcon = classNames({
+ 'fa': true,
+ 'fa-cloud-upload': true
+ })
+
+ return (
+
+
+ )
+ }
+
+ // If we don't have any files uploading, don't show anything.
+ let numberUploading = Object.keys(uploads).length
+ if (numberUploading == 0)
+ return ( )
+
+ let totalLoaded = 0
+ let totalSize = 0
+
+ // Iterate over each upload, adding together the total size and that
+ // which has been uploaded.
+ for (var slug in uploads) {
+ let upload = uploads[slug]
+ totalLoaded += upload.loaded
+ totalSize += upload.size
+ }
+
+ let percent = (totalLoaded / totalSize) * 100
+
+ // If more than one: "Uploading files (5)..."
+ // If only one: "Uploading myfile.txt..."
+ let text = 'Uploading ' + (numberUploading == 1 ? `'${uploads[Object.keys(uploads)[0]].name}'` : `files (${numberUploading})`) + '...'
+
+ return (
+
+
+ ×
+
+
+ { text }
+
+
+
+ { humanize.filesize(totalLoaded) } ({ percent.toFixed(2) } %)
+
+
+ )
+ }
+}
+
+export default connect(state => {
+ return {
+ uploads: state.uploads,
+ showAbortModal: state.showAbortModal
+ }
+})(UploadModal)
diff --git a/browser/app/js/components/__tests__/Login-test.js b/browser/app/js/components/__tests__/Login-test.js
new file mode 100644
index 000000000..1397fb637
--- /dev/null
+++ b/browser/app/js/components/__tests__/Login-test.js
@@ -0,0 +1,54 @@
+/*
+ * Minio Browser (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( )
+ 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( )
+ 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')
+ })
+});
+*/
diff --git a/browser/app/js/constants.js b/browser/app/js/constants.js
new file mode 100644
index 000000000..35c20f418
--- /dev/null
+++ b/browser/app/js/constants.js
@@ -0,0 +1,23 @@
+/*
+ * Minio Browser (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.
+ */
+
+// File for all the browser constants.
+
+// minioBrowserPrefix absolute path.
+export const minioBrowserPrefix = '/minio'
+export const READ_ONLY = 'readonly'
+export const WRITE_ONLY = 'writeonly'
+export const READ_WRITE = 'readwrite'
diff --git a/browser/app/js/jsonrpc.js b/browser/app/js/jsonrpc.js
new file mode 100644
index 000000000..99e1d1102
--- /dev/null
+++ b/browser/app/js/jsonrpc.js
@@ -0,0 +1,91 @@
+/*
+ * Minio Browser (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 SuperAgent from 'superagent-es6-promise';
+import url from 'url'
+import Moment from 'moment'
+
+export default class JSONrpc {
+ constructor(params) {
+ this.endpoint = params.endpoint
+ this.namespace = params.namespace
+ this.version = '2.0';
+ const parsedUrl = url.parse(this.endpoint)
+ this.host = parsedUrl.hostname
+ this.path = parsedUrl.path
+ this.port = parsedUrl.port
+
+ switch (parsedUrl.protocol) {
+ case 'http:': {
+ this.scheme = 'http'
+ if (parsedUrl.port === 0) {
+ this.port = 80
+ }
+ break
+ }
+ case 'https:': {
+ this.scheme = 'https'
+ if (parsedUrl.port === 0) {
+ this.port = 443
+ }
+ break
+ }
+ default: {
+ throw new Error('Unknown protocol: ' + parsedUrl.protocol)
+ }
+ }
+ }
+ // call('Get', {id: NN, params: [...]}, function() {})
+ call(method, options, token) {
+ if (!options) {
+ options = {}
+ }
+ if (!options.id) {
+ options.id = 1;
+ }
+ if (!options.params) {
+ options.params = {};
+ }
+ const dataObj = {
+ id: options.id,
+ jsonrpc: this.version,
+ params: options.params ? options.params : {},
+ method: this.namespace ? this.namespace + '.' + method : method
+ }
+ let requestParams = {
+ host: this.host,
+ port: this.port,
+ path: this.path,
+ scheme: this.scheme,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-amz-date': Moment().utc().format('YYYYMMDDTHHmmss') + 'Z'
+ }
+ }
+
+ if (token) {
+ requestParams.headers.Authorization = 'Bearer ' + token
+ }
+
+ let req = SuperAgent.post(this.endpoint)
+ for (let key in requestParams.headers) {
+ req.set(key, requestParams.headers[key])
+ }
+ // req.set('Access-Control-Allow-Origin', 'http://localhost:8080')
+ return req.send(JSON.stringify(dataObj)).then(res => res)
+ }
+}
diff --git a/browser/app/js/mime.js b/browser/app/js/mime.js
new file mode 100644
index 000000000..9c8ed40fa
--- /dev/null
+++ b/browser/app/js/mime.js
@@ -0,0 +1,106 @@
+/*
+ * Minio Browser (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 mimedb from 'mime-types'
+
+const isFolder = (name, contentType) => {
+ if (name.endsWith('/')) return true
+ return false
+}
+
+const isPdf = (name, contentType) => {
+ if (contentType === 'application/pdf') return true
+ return false
+}
+
+const isZip = (name, contentType) => {
+ if (!contentType || !contentType.includes('/')) return false
+ if (contentType.split('/')[1].includes('zip')) return true
+ return false
+}
+
+const isCode = (name, contentType) => {
+ const codeExt = ['c', 'cpp', 'go', 'py', 'java', 'rb', 'js', 'pl', 'fs',
+ 'php', 'css', 'less', 'scss', 'coffee', 'net', 'html',
+ 'rs', 'exs', 'scala', 'hs', 'clj', 'el', 'scm', 'lisp',
+ 'asp', 'aspx']
+ const ext = name.split('.').reverse()[0]
+ for (var i in codeExt) {
+ if (ext === codeExt[i]) return true
+ }
+ return false
+}
+
+const isExcel = (name, contentType) => {
+ if (!contentType || !contentType.includes('/')) return false
+ const types = ['excel', 'spreadsheet']
+ const subType = contentType.split('/')[1]
+ for (var i in types) {
+ if (subType.includes(types[i])) return true
+ }
+ return false
+}
+
+const isDoc = (name, contentType) => {
+ if (!contentType || !contentType.includes('/')) return false
+ const types = ['word', '.document']
+ const subType = contentType.split('/')[1]
+ for (var i in types) {
+ if (subType.includes(types[i])) return true
+ }
+ return false
+}
+
+const isPresentation = (name, contentType) => {
+ if (!contentType || !contentType.includes('/')) return false
+ var types = ['powerpoint', 'presentation']
+ const subType = contentType.split('/')[1]
+ for (var i in types) {
+ if (subType.includes(types[i])) return true
+ }
+ return false
+}
+
+const typeToIcon = (type) => {
+ return (name, contentType) => {
+ if (!contentType || !contentType.includes('/')) return false
+ if (contentType.split('/')[0] === type) return true
+ return false
+ }
+}
+
+export const getDataType = (name, contentType) => {
+ if (contentType === "") {
+ contentType = mimedb.lookup(name) || 'application/octet-stream'
+ }
+ const check = [
+ ['folder', isFolder],
+ ['code', isCode],
+ ['audio', typeToIcon('audio')],
+ ['image', typeToIcon('image')],
+ ['video', typeToIcon('video')],
+ ['text', typeToIcon('text')],
+ ['pdf', isPdf],
+ ['zip', isZip],
+ ['excel', isExcel],
+ ['doc', isDoc],
+ ['presentation', isPresentation]
+ ]
+ for (var i in check) {
+ if (check[i][1](name, contentType)) return check[i][0]
+ }
+ return 'other'
+}
diff --git a/browser/app/js/reducers.js b/browser/app/js/reducers.js
new file mode 100644
index 000000000..00482e1ff
--- /dev/null
+++ b/browser/app/js/reducers.js
@@ -0,0 +1,176 @@
+/*
+ * Minio Browser (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: [],
+ storageInfo: {},
+ serverInfo: {},
+ currentBucket: '',
+ 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: '',
+ expiry: 604800
+ },
+ prefixWritable: false
+ }, 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.SET_VISIBLE_BUCKETS:
+ newState.visibleBuckets = action.visibleBuckets
+ break
+ case actions.SET_CURRENT_BUCKET:
+ newState.currentBucket = action.currentBucket
+ break
+ case actions.SET_OBJECTS:
+ newState.objects = action.objects
+ 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
+ }
+ return newState
+}
diff --git a/browser/app/js/utils.js b/browser/app/js/utils.js
new file mode 100644
index 000000000..3aee71a1b
--- /dev/null
+++ b/browser/app/js/utils.js
@@ -0,0 +1,85 @@
+/*
+ * Minio Browser (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 { minioBrowserPrefix } from './constants.js'
+
+export const sortObjectsByName = (objects, order) => {
+ let folders = objects.filter(object => object.name.endsWith('/'))
+ let files = objects.filter(object => !object.name.endsWith('/'))
+ folders = folders.sort((a, b) => {
+ if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
+ if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
+ return 0
+ })
+ files = files.sort((a, b) => {
+ if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
+ if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
+ return 0
+ })
+ if (order) {
+ folders = folders.reverse()
+ files = files.reverse()
+ }
+ return [...folders, ...files]
+}
+
+export const sortObjectsBySize = (objects, order) => {
+ let folders = objects.filter(object => object.name.endsWith('/'))
+ let files = objects.filter(object => !object.name.endsWith('/'))
+ files = files.sort((a, b) => a.size - b.size)
+ if (order)
+ files = files.reverse()
+ return [...folders, ...files]
+}
+
+export const sortObjectsByDate = (objects, order) => {
+ let folders = objects.filter(object => object.name.endsWith('/'))
+ let files = objects.filter(object => !object.name.endsWith('/'))
+ files = files.sort((a, b) => new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime())
+ if (order)
+ files = files.reverse()
+ return [...folders, ...files]
+}
+
+export const pathSlice = (path) => {
+ path = path.replace(minioBrowserPrefix, '')
+ let prefix = ''
+ let bucket = ''
+ if (!path) return {
+ bucket,
+ prefix
+ }
+ let objectIndex = path.indexOf('/', 1)
+ if (objectIndex == -1) {
+ bucket = path.slice(1)
+ return {
+ bucket,
+ prefix
+ }
+ }
+ bucket = path.slice(1, objectIndex)
+ prefix = path.slice(objectIndex + 1)
+ return {
+ bucket,
+ prefix
+ }
+}
+
+export const pathJoin = (bucket, prefix) => {
+ if (!prefix)
+ prefix = ''
+ return minioBrowserPrefix + '/' + bucket + '/' + prefix
+}
diff --git a/browser/app/js/web.js b/browser/app/js/web.js
new file mode 100644
index 000000000..a4c241137
--- /dev/null
+++ b/browser/app/js/web.js
@@ -0,0 +1,124 @@
+/*
+ * Minio Browser (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 { 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) {
+ const namespace = 'Web'
+ this.dispatch = dispatch
+ this.JSONrpc = new JSONrpc({
+ endpoint,
+ namespace
+ })
+ }
+ makeCall(method, options) {
+ return this.JSONrpc.call(method, {
+ params: options
+ }, storage.getItem('token'))
+ .catch(err => {
+ if (err.status === 401) {
+ storage.removeItem('token')
+ browserHistory.push(`${minioBrowserPrefix}/login`)
+ throw new Error('Please re-login.')
+ }
+ if (err.status)
+ throw new Error(`Server returned error [${err.status}]`)
+ throw new Error('Minio server is unreachable')
+ })
+ .then(res => {
+ let json = JSON.parse(res.text)
+ let result = json.result
+ let error = json.error
+ if (error) {
+ throw new Error(error.message)
+ }
+ if (!Moment(result.uiVersion).isValid()) {
+ throw new Error("Invalid UI version in the JSON-RPC response")
+ }
+ if (result.uiVersion !== currentUiVersion
+ && currentUiVersion !== 'MINIO_UI_VERSION') {
+ storage.setItem('newlyUpdated', true)
+ location.reload()
+ }
+ return result
+ })
+ }
+ LoggedIn() {
+ return !!storage.getItem('token')
+ }
+ Login(args) {
+ return this.makeCall('Login', args)
+ .then(res => {
+ storage.setItem('token', `${res.token}`)
+ return res
+ })
+ }
+ Logout() {
+ storage.removeItem('token')
+ }
+ ServerInfo() {
+ return this.makeCall('ServerInfo')
+ }
+ StorageInfo() {
+ return this.makeCall('StorageInfo')
+ }
+ ListBuckets() {
+ return this.makeCall('ListBuckets')
+ }
+ MakeBucket(args) {
+ return this.makeCall('MakeBucket', args)
+ }
+ ListObjects(args) {
+ return this.makeCall('ListObjects', args)
+ }
+ PresignedGet(args) {
+ return this.makeCall('PresignedGet', args)
+ }
+ PutObjectURL(args) {
+ return this.makeCall('PutObjectURL', args)
+ }
+ RemoveObject(args) {
+ return this.makeCall('RemoveObject', args)
+ }
+ GetAuth() {
+ return this.makeCall('GetAuth')
+ }
+ GenerateAuth() {
+ return this.makeCall('GenerateAuth')
+ }
+ SetAuth(args) {
+ return this.makeCall('SetAuth', args)
+ .then(res => {
+ storage.setItem('token', `${res.token}`)
+ return res
+ })
+ }
+ GetBucketPolicy(args) {
+ return this.makeCall('GetBucketPolicy', args)
+ }
+ SetBucketPolicy(args) {
+ return this.makeCall('SetBucketPolicy', args)
+ }
+ ListAllBucketPolicies(args) {
+ return this.makeCall('ListAllBucketPolicies', args)
+ }
+}
diff --git a/browser/app/less/inc/alert.less b/browser/app/less/inc/alert.less
new file mode 100644
index 000000000..4c60e5d65
--- /dev/null
+++ b/browser/app/less/inc/alert.less
@@ -0,0 +1,68 @@
+.alert {
+ border: 0;
+ position: fixed;
+ max-width: 500px;
+ margin: 0;
+ box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1);
+ color: @white;
+ width: 100%;
+ right: 20px;
+ border-radius: 3px;
+ padding: 17px 50px 17px 17px;
+ z-index: 10010;
+ .animation-duration(800ms);
+ .animation-fill-mode(both);
+
+ &:not(.progress) {
+ top: 20px;
+
+ @media(min-width: (@screen-sm-min)) {
+ left: 50%;
+ margin-left: -250px;
+ }
+ }
+
+ &.progress {
+ bottom: 20px;
+ right: 20px;
+ }
+
+ &.alert-danger {
+ background: @red;
+ }
+
+ &.alert-success {
+ background: @green;
+ }
+
+ &.alert-info {
+ background: @blue;
+ }
+
+ @media(max-width: (@screen-xs-max)) {
+ left: 20px;
+ width: ~"calc(100% - 40px)";
+ max-width: 100%;
+ }
+
+ .progress {
+ margin: 10px 10px 8px 0;
+ height: 5px;
+ box-shadow: none;
+ border-radius: 1px;
+ background-color: @blue;
+ border-radius: 2px;
+ overflow: hidden;
+ }
+
+ .progress-bar {
+ box-shadow: none;
+ background-color: @white;
+ height: 100%;
+ }
+
+ .close {
+ position: absolute;
+ top: 15px;
+ }
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/animate.less b/browser/app/less/inc/animate/animate.less
new file mode 100644
index 000000000..33c53b1ae
--- /dev/null
+++ b/browser/app/less/inc/animate/animate.less
@@ -0,0 +1,13 @@
+.animated{
+ &.infinite {
+ .animation-iteration-count(infinite);
+ }
+}
+
+@import 'fadeIn';
+@import 'fadeInDown';
+@import 'fadeInUp';
+@import 'fadeOut';
+@import 'fadeOutDown';
+@import 'fadeOutUp';
+@import 'zoomIn';
diff --git a/browser/app/less/inc/animate/fadeIn.less b/browser/app/less/inc/animate/fadeIn.less
new file mode 100644
index 000000000..50282ac98
--- /dev/null
+++ b/browser/app/less/inc/animate/fadeIn.less
@@ -0,0 +1,26 @@
+@-webkit-keyframes fadeIn {
+ 0% {opacity: 0;}
+ 100% {opacity: 1;}
+}
+
+@-moz-keyframes fadeIn {
+ 0% {opacity: 0;}
+ 100% {opacity: 1;}
+}
+
+@-o-keyframes fadeIn {
+ 0% {opacity: 0;}
+ 100% {opacity: 1;}
+}
+
+@keyframes fadeIn {
+ 0% {opacity: 0;}
+ 100% {opacity: 1;}
+}
+
+.fadeIn {
+ -webkit-animation-name: fadeIn;
+ -moz-animation-name: fadeIn;
+ -o-animation-name: fadeIn;
+ animation-name: fadeIn;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/fadeInDown.less b/browser/app/less/inc/animate/fadeInDown.less
new file mode 100644
index 000000000..2a959322e
--- /dev/null
+++ b/browser/app/less/inc/animate/fadeInDown.less
@@ -0,0 +1,54 @@
+@-webkit-keyframes fadeInDown {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translateY(-20px);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: translateY(0);
+ }
+}
+
+@-moz-keyframes fadeInDown {
+ 0% {
+ opacity: 0;
+ -moz-transform: translateY(-20px);
+ }
+
+ 100% {
+ opacity: 1;
+ -moz-transform: translateY(0);
+ }
+}
+
+@-o-keyframes fadeInDown {
+ 0% {
+ opacity: 0;
+ -ms-transform: translateY(-20px);
+ }
+
+ 100% {
+ opacity: 1;
+ -ms-transform: translateY(0);
+ }
+}
+
+@keyframes fadeInDown {
+ 0% {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fadeInDown {
+ -webkit-animation-name: fadeInDown;
+ -moz-animation-name: fadeInDown;
+ -o-animation-name: fadeInDown;
+ animation-name: fadeInDown;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/fadeInUp.less b/browser/app/less/inc/animate/fadeInUp.less
new file mode 100644
index 000000000..54b4d26ec
--- /dev/null
+++ b/browser/app/less/inc/animate/fadeInUp.less
@@ -0,0 +1,54 @@
+@-webkit-keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ -webkit-transform: translateY(20px);
+ }
+
+ 100% {
+ opacity: 1;
+ -webkit-transform: translateY(0);
+ }
+}
+
+@-moz-keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ -moz-transform: translateY(20px);
+ }
+
+ 100% {
+ opacity: 1;
+ -moz-transform: translateY(0);
+ }
+}
+
+@-o-keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ -o-transform: translateY(20px);
+ }
+
+ 100% {
+ opacity: 1;
+ -o-transform: translateY(0);
+ }
+}
+
+@keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fadeInUp {
+ -webkit-animation-name: fadeInUp;
+ -moz-animation-name: fadeInUp;
+ -o-animation-name: fadeInUp;
+ animation-name: fadeInUp;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/fadeOut.less b/browser/app/less/inc/animate/fadeOut.less
new file mode 100644
index 000000000..ba64505b9
--- /dev/null
+++ b/browser/app/less/inc/animate/fadeOut.less
@@ -0,0 +1,26 @@
+@-webkit-keyframes fadeOut {
+ 0% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+@-moz-keyframes fadeOut {
+ 0% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+@-o-keyframes fadeOut {
+ 0% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+@keyframes fadeOut {
+ 0% {opacity: 1;}
+ 100% {opacity: 0;}
+}
+
+.fadeOut {
+ -webkit-animation-name: fadeOut;
+ -moz-animation-name: fadeOut;
+ -o-animation-name: fadeOut;
+ animation-name: fadeOut;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/fadeOutDown.less b/browser/app/less/inc/animate/fadeOutDown.less
new file mode 100644
index 000000000..214e75367
--- /dev/null
+++ b/browser/app/less/inc/animate/fadeOutDown.less
@@ -0,0 +1,54 @@
+@-webkit-keyframes fadeOutDown {
+ 0% {
+ opacity: 1;
+ -webkit-transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translateY(20px);
+ }
+}
+
+@-moz-keyframes fadeOutDown {
+ 0% {
+ opacity: 1;
+ -moz-transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ -moz-transform: translateY(20px);
+ }
+}
+
+@-o-keyframes fadeOutDown {
+ 0% {
+ opacity: 1;
+ -o-transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ -o-transform: translateY(20px);
+ }
+}
+
+@keyframes fadeOutDown {
+ 0% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+}
+
+.fadeOutDown {
+ -webkit-animation-name: fadeOutDown;
+ -moz-animation-name: fadeOutDown;
+ -o-animation-name: fadeOutDown;
+ animation-name: fadeOutDown;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/fadeOutUp.less b/browser/app/less/inc/animate/fadeOutUp.less
new file mode 100644
index 000000000..cf6115ac0
--- /dev/null
+++ b/browser/app/less/inc/animate/fadeOutUp.less
@@ -0,0 +1,51 @@
+@-webkit-keyframes fadeOutUp {
+ 0% {
+ opacity: 1;
+ -webkit-transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ -webkit-transform: translateY(-20px);
+ }
+}
+@-moz-keyframes fadeOutUp {
+ 0% {
+ opacity: 1;
+ -moz-transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ -moz-transform: translateY(-20px);
+ }
+}
+@-o-keyframes fadeOutUp {
+ 0% {
+ opacity: 1;
+ -o-transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ -o-transform: translateY(-20px);
+ }
+}
+@keyframes fadeOutUp {
+ 0% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+}
+
+.fadeOutUp {
+ -webkit-animation-name: fadeOutUp;
+ -moz-animation-name: fadeOutUp;
+ -o-animation-name: fadeOutUp;
+ animation-name: fadeOutUp;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/animate/zoomIn.less b/browser/app/less/inc/animate/zoomIn.less
new file mode 100644
index 000000000..34c754fef
--- /dev/null
+++ b/browser/app/less/inc/animate/zoomIn.less
@@ -0,0 +1,23 @@
+@-webkit-keyframes zoomIn {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+@keyframes zoomIn {
+ from {
+ opacity: 0;
+ -webkit-transform: scale3d(.3, .3, .3);
+ transform: scale3d(.3, .3, .3);
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/base.less b/browser/app/less/inc/base.less
new file mode 100644
index 000000000..4f288ae82
--- /dev/null
+++ b/browser/app/less/inc/base.less
@@ -0,0 +1,31 @@
+* {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ &:focus,
+ &:active {
+ outline: 0;
+ }
+}
+
+html {
+ font-size: 10px;
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+html,
+body {
+ min-height: 100%;
+}
+
+a {
+ .transition(color);
+ .transition-duration(300ms);
+
+}
+
+button {
+ border: 0;
+}
+
+
diff --git a/browser/app/less/inc/buttons.less b/browser/app/less/inc/buttons.less
new file mode 100644
index 000000000..28131641a
--- /dev/null
+++ b/browser/app/less/inc/buttons.less
@@ -0,0 +1,53 @@
+.btn {
+ border: 0;
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 2px;
+ text-align: center;
+ .transition(all);
+ .transition-duration(300ms);
+
+ &:hover,
+ &:focus {
+ .opacity(0.9);
+ }
+}
+
+/*-----------------------------------
+ Button Variants
+------------------------------------*/
+.btn-variant(@bg-color, @color) {
+ color: @color;
+ background-color: @bg-color;
+
+ &:hover,
+ &:focus {
+ color: @color;
+ background-color: darken(@bg-color, 6%);
+ }
+
+
+}
+
+.btn-block {
+ display: block;
+ width: 100%;
+}
+
+.btn-link {
+ .btn-variant(#eee, #545454);
+}
+
+.btn-danger {
+ .btn-variant(@red, @white);
+}
+
+.btn-primary {
+ .btn-variant(@blue, @white);
+}
+
+.btn-success {
+ .btn-variant(@green, @white);
+}
+//-----------------------------------
\ No newline at end of file
diff --git a/browser/app/less/inc/dropdown.less b/browser/app/less/inc/dropdown.less
new file mode 100644
index 000000000..4d160464e
--- /dev/null
+++ b/browser/app/less/inc/dropdown.less
@@ -0,0 +1,26 @@
+.dropdown-menu {
+ padding: 15px 0;
+ top: 0;
+ margin-top: -1px;
+
+ & > li {
+ & > a {
+ padding: 8px 20px;
+ font-size: 15px;
+
+ & > i {
+ width: 20px;
+ position: relative;
+ top: 1px;
+ }
+ }
+ }
+}
+
+.dropdown-menu-right {
+ & > li {
+ & > a {
+ text-align: right;
+ }
+ }
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/file-explorer.less b/browser/app/less/inc/file-explorer.less
new file mode 100644
index 000000000..da0ddcdd7
--- /dev/null
+++ b/browser/app/less/inc/file-explorer.less
@@ -0,0 +1,160 @@
+/*------------------------------
+ Layout
+--------------------------------*/
+.file-explorer {
+ background-color: @white;
+ position: relative;
+ height: 100%;
+
+ &.toggled {
+ height: 100vh;
+ overflow: hidden;
+ }
+}
+
+.fe-body {
+ @media(min-width: @screen-md-min) {
+ padding: 0 0 40px @fe-sidebar-width;
+ }
+
+ @media(max-width: @screen-sm-max) {
+ padding: 75px 0 80px;
+ }
+
+ min-height:100vh;
+ overflow: auto;
+}
+
+
+/*------------------------------
+ Create and Upload Button
+--------------------------------*/
+.feb-actions {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+
+ .dropdown-menu {
+ min-width: 55px;
+ width: 55px;
+ text-align: center;
+ background: transparent;
+ box-shadow: none;
+ margin: 0;
+ }
+
+ &.open {
+ .feba-btn {
+ .scale(1);
+
+ &:first-child {
+ .animation-name(feba-btn-anim);
+ .animation-duration(300ms);
+ }
+
+ &:last-child {
+ .animation-name(feba-btn-anim);
+ .animation-duration(100ms);
+ }
+ }
+
+ .feba-toggle {
+ background: darken(@red, 10%);
+
+ & > span {
+ .rotate(135deg);
+ }
+ }
+ }
+}
+
+.feba-toggle {
+ width: 55px;
+ height: 55px;
+ line-height: 55px;
+ border-radius: 50%;
+ background: @red;
+ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);
+ display: inline-block;
+ text-align: center;
+ border: 0;
+ padding: 0;
+
+ span {
+ display: inline-block;
+ height: 100%;
+ width: 100%;
+ }
+
+ i {
+ color: @white;
+ font-size: 17px;
+ line-height: 58px;
+ }
+}
+
+.feba-toggle,
+.feba-toggle > span {
+ .transition(all);
+ .transition-duration(250ms);
+ .backface-visibility(hidden);
+}
+
+.feba-btn {
+ width: 40px;
+ margin-top: 10px;
+ height: 40px;
+ border-radius: 50%;
+ text-align: center;
+ display: inline-block;
+ color: @white;
+ line-height: 40px;
+ box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);
+ -webkit-transform: scale(0);
+ transform: scale(0);
+ position: relative;
+
+ &:hover,
+ &:focus {
+ color: @white;
+ }
+
+ label {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+ cursor: pointer;
+ }
+}
+
+.feba-bucket {
+ background: @orange;
+}
+
+.feba-upload {
+ background: @yellow;
+}
+
+@-webkit-keyframes feba-btn-anim {
+ from {
+ .scale(0);
+ .opacity(0);
+ }
+ to {
+ .scale(1);
+ .opacity(1);
+ }
+}
+
+@keyframes feba-btn-anim {
+ from {
+ .scale(0);
+ .opacity(0);
+ }
+ to {
+ .scale(1);
+ .opacity(1);
+ }
+}
diff --git a/browser/app/less/inc/font.less b/browser/app/less/inc/font.less
new file mode 100644
index 000000000..bdb7f98d8
--- /dev/null
+++ b/browser/app/less/inc/font.less
@@ -0,0 +1,7 @@
+@font-face {
+ font-family: Lato;
+ src: url('../../fonts/lato/lato-normal.woff2') format('woff2'),
+ url('../../fonts/lato/lato-normal.woff') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/form.less b/browser/app/less/inc/form.less
new file mode 100644
index 000000000..6fa7dbaab
--- /dev/null
+++ b/browser/app/less/inc/form.less
@@ -0,0 +1,249 @@
+.form-control {
+ border: 0;
+ border-bottom: 1px solid @input-border;
+ color: #32393F;
+ padding: 5px;
+ width: 100%;
+ font-size: 13px;
+ background-color: transparent;
+}
+
+select.form-control {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ border-radius: 0;
+ background: url(../../img/select-caret.svg) no-repeat bottom 7px right;
+
+}
+
+
+/*--------------------------
+ Input Group
+----------------------------*/
+.input-group {
+ position: relative;
+ &:not(:last-child) {
+ margin-bottom: 25px;
+ }
+
+ label:not(.ig-label) {
+ font-size: 13px;
+ display: block;
+ margin-bottom: 10px;
+ }
+}
+
+.ig-label {
+ position: absolute;
+ text-align: center;
+ bottom: 7px;
+ left: 0;
+ width: 100%;
+ .transition(all);
+ .transition-duration(250ms);
+ padding: 2px 0 3px;
+ border-radius: 2px;
+ font-weight: 400;
+}
+
+.ig-helpers {
+ z-index: 1;
+ width: 100%;
+ left: 0;
+
+ &,
+ &:before,
+ &:after {
+ position: absolute;
+ height: 2px;
+ bottom: 0;
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ width: 0;
+ .transition(all);
+ .transition-duration(250ms);
+ background-color: #03A9F4;
+ }
+
+ &:before {
+ left: 50%;
+ }
+
+ &:after {
+ right: 50%;
+ }
+}
+
+.ig-text {
+ width: 100%;
+ height: 40px;
+ border: 0;
+ background: transparent;
+ text-align: center;
+ position: relative;
+ z-index: 1;
+ border-bottom: 1px solid #eee;
+ color: #32393F;
+ font-size: 13px;
+
+
+ &:focus + .ig-helpers {
+ &:before,
+ &:after {
+ width: 50%;
+ }
+ }
+
+ &:valid,
+ &:disabled,
+ &:focus {
+ & ~ .ig-label {
+ bottom: 35px;
+ font-size: 13px;
+ z-index: 1;
+ }
+ }
+
+ &:disabled {
+ .opacity(0.5);
+ }
+}
+
+.ig-dark {
+ .ig-text {
+ color: @white;
+ border-color: rgba(255,255,255,0.1);
+ }
+
+ .ig-helpers {
+ &:before,
+ &:after {
+ background-color: #dfdfdf;
+ height: 1px;
+ }
+ }
+}
+
+.ig-left {
+ .ig-label,
+ .ig-text {
+ text-align: left;
+ }
+}
+
+.ig-error {
+ .ig-label {
+ color: #E23F3F;
+ }
+ .ig-helpers i {
+ &:first-child,
+ &:first-child:before,
+ &:first-child:after {
+ background: rgba(226, 63, 63, 0.43);
+ }
+ &:last-child,
+ &:last-child:before,
+ &:last-child:after {
+ background: #E23F3F !important;
+ }
+ }
+ &:after {
+ content: "\f05a";
+ font-family: FontAwesome;
+ position: absolute;
+ top: 17px;
+ right: 9px;
+ font-size: 20px;
+ color: #D33D3E;
+ }
+}
+
+.ig-search {
+ &:before {
+ font-family: @font-family-icon;
+ content: '\f002';
+ font-size: 15px;
+ position: absolute;
+ left: 2px;
+ top: 8px;
+ }
+
+ .ig-text {
+ padding-left: 25px;
+ }
+}
+
+
+/*--------------------------
+ Share Spinners
+----------------------------*/
+.set-expire {
+ border: 1px solid @input-border;
+ margin: 35px 0 30px;
+}
+
+.set-expire-item {
+ padding: 9px 5px 3px;
+ position: relative;
+ display: table-cell;
+ width: 1%;
+ text-align: center;
+
+ &:not(:last-child) {
+ border-right: 1px solid @input-border;
+ }
+}
+
+.set-expire-title {
+ font-size: 10px;
+ text-transform: uppercase;
+}
+
+.set-expire-value {
+ display: inline-block;
+ overflow: hidden;
+ position: relative;
+ left: -8px;
+
+ input {
+ font-size: 20px;
+ text-align: center;
+ position: relative;
+ right: -15px;
+ border: 0;
+ color: @text-strong-color;
+ padding: 0;
+ height: 25px;
+ width: 100%;
+ font-weight: normal;
+ }
+}
+
+.set-expire-decrease,
+.set-expire-increase {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ background: url(../../img/arrow.svg) no-repeat center;
+ background-size: 85%;
+ left: 50%;
+ margin-left: -10px;
+ .opacity(0.2);
+ cursor: pointer;
+
+ &:hover {
+ .opacity(0.5);
+ }
+}
+
+.set-expire-increase {
+ top: -25px;
+}
+
+.set-expire-decrease {
+ bottom: -27px;
+ .rotate(-180deg);
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/generics.less b/browser/app/less/inc/generics.less
new file mode 100644
index 000000000..1c46dfc60
--- /dev/null
+++ b/browser/app/less/inc/generics.less
@@ -0,0 +1,83 @@
+/*----------------------------
+ Text Alignment
+-----------------------------*/
+.text-center { text-align: center !important; }
+.text-left { text-align: left !important; }
+.text-right { text-align: right !important; }
+
+
+/*----------------------------
+ Float
+-----------------------------*/
+.clearfix { .clearfix(); }
+.pull-right { float: right !important; }
+.pull-left { float: left !important; }
+
+
+/*----------------------------
+ Position
+-----------------------------*/
+.p-relative { position: relative; }
+
+
+/*---------------------------------------------------------------------------
+ Generate Margin Class
+ margin, margin-top, margin-bottom, margin-left, margin-right
+----------------------------------------------------------------------------*/
+
+.margin (@label, @size: 1, @key:1) when (@size =< 30){
+ .m-@{key} {
+ margin: @size !important;
+ }
+
+ .m-t-@{key} {
+ margin-top: @size !important;
+ }
+
+ .m-b-@{key} {
+ margin-bottom: @size !important;
+ }
+
+ .m-l-@{key} {
+ margin-left: @size !important;
+ }
+
+ .m-r-@{key} {
+ margin-right: @size !important;
+ }
+
+ .margin(@label - 5; @size + 5; @key + 5);
+}
+
+.margin(25, 0px, 0);
+
+
+/*---------------------------------------------------------------------------
+ Generate Padding Class
+ padding, padding-top, padding-bottom, padding-left, padding-right
+----------------------------------------------------------------------------*/
+.padding (@label, @size: 1, @key:1) when (@size =< 30){
+ .p-@{key} {
+ padding: @size !important;
+ }
+
+ .p-t-@{key} {
+ padding-top: @size !important;
+ }
+
+ .p-b-@{key} {
+ padding-bottom: @size !important;
+ }
+
+ .p-l-@{key} {
+ padding-left: @size !important;
+ }
+
+ .p-r-@{key} {
+ padding-right: @size !important;
+ }
+
+ .padding(@label - 5; @size + 5; @key + 5);
+}
+
+.padding(25, 0px, 0);
\ No newline at end of file
diff --git a/browser/app/less/inc/header.less b/browser/app/less/inc/header.less
new file mode 100644
index 000000000..e95f05d66
--- /dev/null
+++ b/browser/app/less/inc/header.less
@@ -0,0 +1,242 @@
+/*--------------------------
+ Header
+----------------------------*/
+.fe-header {
+ padding: 45px 55px 20px;
+
+ @media(min-width: @screen-md-min) {
+ position: relative;
+ }
+
+ @media(max-width: (@screen-xs-max - 100)) {
+ padding: 25px 25px 20px;
+ }
+
+ h2 {
+ font-size: 16px;
+ font-weight: normal;
+ margin: 0;
+
+ & > span {
+ margin-bottom: 7px;
+ display: inline-block;
+
+ &:not(:first-child) {
+ &:before {
+ content: '/';
+ margin: 0 4px;
+ color: @text-color;
+ }
+ }
+ }
+ }
+
+ p {
+ margin-top: 7px;
+ }
+}
+
+
+/*--------------------------
+ Disk usage
+----------------------------*/
+.feh-usage {
+ margin-top: 12px;
+ max-width: 285px;
+
+ @media(max-width: (@screen-xs-max - 100px)) {
+ max-width: 100%;
+ font-size: 12px;
+ }
+
+ & > ul {
+ margin-top: 7px;
+ list-style: none;
+ padding: 0;
+
+ & > li {
+ padding-right: 0;
+ display: inline-block;
+ }
+ }
+}
+
+.fehu-chart {
+ height: 5px;
+ background: #eee;
+ position: relative;
+ border-radius: 2px;
+ overflow: hidden;
+
+ & > div {
+ position: absolute;
+ left: 0;
+ height: 100%;
+ background: @link-color;
+ }
+}
+
+/*--------------------------
+ Header Actions
+----------------------------*/
+.feh-actions {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ position: absolute;
+ right: 35px;
+ top: 30px;
+ z-index: 11;
+
+ @media(max-width: (@screen-sm-max)) {
+ top: 7px;
+ right: 10px;
+ position: fixed;
+ }
+
+ & > li {
+ display: inline-block;
+ text-align: right;
+ vertical-align: top;
+ line-height: 100%;
+
+ & > a,
+ & > .btn-group > button {
+ display: block;
+ height: 45px;
+ min-width: 45px;
+ text-align: center;
+ border-radius: 50%;
+ padding: 0;
+ border: 0;
+ background: none;
+
+ @media(min-width: @screen-md-min) {
+ color: #7B7B7B;
+ font-size: 21px;
+ line-height: 45px;
+ .transition(all);
+ .transition-duration(300ms);
+
+ &:hover {
+ background: rgba(0,0,0,0.09);
+ }
+ }
+
+ @media(max-width: (@screen-sm-max)) {
+ background: url(../../img/more-h-light.svg) no-repeat center;
+
+ .fa-reorder {
+ display: none;
+ }
+ }
+
+ }
+ }
+}
+
+
+/*--------------------------
+ Mobile Header
+----------------------------*/
+@media(max-width: @screen-sm-max) {
+ .fe-header-mobile {
+ background-color: @dark-gray;
+ padding: 10px 50px 9px 12px;
+ text-align: center;
+ position: fixed;
+ z-index: 10;
+ box-shadow: 0 0 10px rgba(0,0,0,0.3);
+ left: 0;
+ top: 0;
+ width: 100%;
+
+ .mh-logo {
+ height: 35px;
+ position: relative;
+ top: 4px;
+ }
+ }
+
+ .feh-trigger {
+ width: 41px;
+ height: 41px;
+ cursor: pointer;
+ float: left;
+ position: relative;
+ text-align: center;
+
+ &:before,
+ &:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+
+ }
+
+ &:after {
+ z-index: 1;
+ }
+
+ &:before {
+ background: rgba(255, 255, 255, 0.1);
+ .transition(all);
+ .transition-duration(300ms);
+ .scale(0);
+ }
+ }
+
+ .feht-toggled {
+ &:before {
+ .scale(1);
+ }
+
+ .feht-lines {
+ .rotate(180deg);
+
+ & > div {
+ &.top {
+ width: 12px;
+ transform: translateX(8px) translateY(1px) rotate(45deg);
+ -webkit-transform: translateX(8px) translateY(1px) rotate(45deg);
+ }
+
+ &.bottom {
+ width: 12px;
+ transform: translateX(8px) translateY(-1px) rotate(-45deg);
+ -webkit-transform: translateX(8px) translateY(-1px) rotate(-45deg);
+ }
+ }
+ }
+ }
+
+ .feht-lines,
+ .feht-lines > div {
+ .transition(all);
+ .transition-duration(300ms);
+ }
+
+ .feht-lines {
+ width: 18px;
+ height: 12px;
+ display: inline-block;
+ margin-top: 14px;
+
+ & > div {
+ background-color: #EAEAEA;
+ width: 18px;
+ height: 2px;
+
+ &.center {
+ margin: 3px 0;
+ }
+ }
+ }
+}
+
+
+
diff --git a/browser/app/less/inc/ie-warning.less b/browser/app/less/inc/ie-warning.less
new file mode 100644
index 000000000..c4bcc0a50
--- /dev/null
+++ b/browser/app/less/inc/ie-warning.less
@@ -0,0 +1,81 @@
+.ie-warning {
+ background-color: #ff5252;
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ left: 0;
+ top: 0;
+ text-align: center;
+
+ &:before {
+ width: 1px;
+ content: '';
+ height: 100%;
+ }
+
+ &:before,
+ .iw-inner {
+ display: inline-block;
+ vertical-align: middle;
+ }
+}
+
+.iw-inner {
+ width: 470px;
+ height: 300px;
+ background-color: @white;
+ border-radius: 5px;
+ padding: 40px;
+ position: relative;
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ width: 230px;
+ margin-left: 80px;
+ margin-top: 16px;
+
+ & > li {
+ float: left;
+
+ & > a {
+ display: block;
+ padding: 10px 15px 7px;
+ font-size: 14px;
+ margin: 0 1px;
+ border-radius: 3px;
+
+ &:hover {
+ background: #eee;
+ }
+
+ img {
+ height: 40px;
+ margin-bottom: 5px;
+ }
+ }
+ }
+ }
+}
+
+.iwi-icon {
+ color: #ff5252;
+ font-size: 40px;
+ display: block;
+ line-height: 100%;
+ margin-bottom: 15px;
+}
+
+.iwi-skip {
+ position: absolute;
+ left: 0;
+ bottom: -35px;
+ width: 100%;
+ color: rgba(255, 255, 255, 0.6);
+ cursor: pointer;
+
+ &:hover {
+ color: @white;
+ }
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/list.less b/browser/app/less/inc/list.less
new file mode 100644
index 000000000..d3ee399b1
--- /dev/null
+++ b/browser/app/less/inc/list.less
@@ -0,0 +1,352 @@
+/*--------------------------
+ Row
+----------------------------*/
+.fesl-row {
+ padding-right: 40px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ position: relative;
+
+ @media (min-width: (@screen-sm-min - 100px)) {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ }
+
+ .clearfix();
+}
+
+header.fesl-row {
+ @media (min-width:(@screen-sm-min - 100px)) {
+ margin-bottom: 20px;
+ border-bottom: 1px solid lighten(@text-muted-color, 20%);
+ padding-left: 40px;
+
+ .fesl-item,
+ .fesli-sort {
+ .transition(all);
+ .transition-duration(300ms);
+ }
+
+ .fesl-item {
+ cursor: pointer;
+ color: @text-color;
+ font-weight: 500;
+ margin-bottom: -5px;
+
+ & > .fesli-sort {
+ float: right;
+ margin: 4px 0 0;
+ .opacity(0);
+ color: @dark-gray;
+ font-size: 14px;
+ }
+
+ &:hover:not(.fi-actions) {
+ background: lighten(@text-muted-color, 22%);
+ color: @dark-gray;
+
+ & > .fesli-sort {
+ .opacity(0.5);
+ }
+ }
+ }
+ }
+
+ @media (max-width:(@screen-xs-max - 100px)) {
+ display: none;
+ }
+}
+
+div.fesl-row {
+ padding-left: 85px;
+ border-bottom: 1px solid transparent;
+ cursor: default;
+
+ @media (max-width: (@screen-xs-max - 100px)) {
+ padding-left: 70px;
+ padding-right: 45px;
+ }
+
+ &:nth-child(even) {
+ background-color: #fafafa;
+ }
+
+ &:hover {
+ background-color: #fbf7dc;
+ }
+
+ &[data-type]:before {
+ font-family: @font-family-icon;
+ width: 35px;
+ height: 35px;
+ text-align: center;
+ line-height: 35px;
+ position: absolute;
+ border-radius: 50%;
+ font-size: 16px;
+ left: 50px;
+ top: 9px;
+ color: @white;
+
+ @media (max-width: (@screen-xs-max - 100px)) {
+ left: 20px;
+ }
+ }
+
+ &[data-type="folder"] {
+ @media (max-width: (@screen-xs-max - 100px)) {
+ .fesl-item {
+ &.fi-name {
+ padding-top: 10px;
+ padding-bottom: 7px;
+ }
+
+ &.fi-size,
+ &.fi-modified {
+ display: none;
+ }
+ }
+ }
+ }
+
+ /*--------------------------
+ Icons
+ ----------------------------*/
+ &[data-type=folder]:before {
+ content: '\f114';
+ background-color: #a1d6dd;
+ }
+ &[data-type=pdf]:before {
+ content: "\f1c1";
+ background-color: #fa7775;
+ }
+ &[data-type=zip]:before {
+ content: "\f1c6";
+ background-color: #427089;
+ }
+ &[data-type=audio]:before {
+ content: "\f1c7";
+ background-color: #009688
+ }
+ &[data-type=code]:before {
+ content: "\f1c9";
+ background-color: #997867;
+ }
+ &[data-type=excel]:before {
+ content: "\f1c3";
+ background-color: #64c866;
+ }
+ &[data-type=image]:before {
+ content: "\f1c5";
+ background-color: #f06292;
+ }
+ &[data-type=video]:before {
+ content: "\f1c8";
+ background-color: #f8c363;
+ }
+ &[data-type=other]:before {
+ content: "\f016";
+ background-color: #afafaf;
+ }
+ &[data-type=text]:before {
+ content: "\f0f6";
+ background-color: #8a8a8a;
+ }
+ &[data-type=doc]:before {
+ content: "\f1c2";
+ background-color: #2196f5;
+ }
+ &[data-type=presentation]:before {
+ content: "\f1c4";
+ background-color: #896ea6;
+ }
+
+ &.fesl-loading{
+ &:before {
+ content: '';
+ }
+
+ &:after {
+ .list-loader(20px, 20px, rgba(255, 255, 255, 0.5), @white);
+ left: 57px;
+ top: 17px;
+
+ @media (max-width: (@screen-xs-max - 100px)) {
+ left: 27px;
+ }
+ }
+
+ }
+}
+
+
+/*--------------------------
+ Files and Folders
+----------------------------*/
+.fesl-item {
+ display: block;
+
+ a {
+ color: darken(@text-color, 5%);
+ }
+
+ @media(min-width: (@screen-sm-min - 100px)) {
+ &:not(.fi-actions) {
+ text-overflow: ellipsis;
+ padding: 10px 15px;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ &.fi-name {
+ flex: 3;
+ }
+
+ &.fi-size {
+ width: 140px;
+ }
+
+ &.fi-modified {
+ width: 190px;
+ }
+
+ &.fi-actions {
+ width: 40px;
+ }
+ }
+
+ @media(max-width: (@screen-xs-max - 100px)) {
+ padding: 0;
+
+ &.fi-name {
+ width: 100%;
+ margin-bottom: 3px;
+ }
+
+ &.fi-size,
+ &.fi-modified {
+ font-size: 12px;
+ color: #B5B5B5;
+ float: left;
+ }
+
+ &.fi-modified {
+ max-width: 72px;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ &.fi-size {
+ margin-right: 10px;
+ }
+
+ &.fi-actions {
+ position: absolute;
+ top: 5px;
+ right: 10px;
+ }
+ }
+}
+
+
+/*--------------------------
+ Action buttons
+----------------------------*/
+.fia-toggle {
+ height: 36px;
+ width: 36px;
+ background: transparent url(../../img/more-h.svg) no-repeat center;
+ position: relative;
+ top: 3px;
+ .opacity(0.4);
+
+ &:hover {
+ .opacity(0.7);
+ }
+}
+
+.fi-actions {
+ .dropdown-menu {
+ background-color: transparent;
+ box-shadow: none;
+ padding: 0;
+ right: 38px;
+ left: auto;
+ margin: 0;
+ height: 100%;
+ text-align: right;
+ }
+
+ .dropdown {
+ &.open {
+ .dropdown-menu {
+ .fiad-action {
+ right: 0;
+ }
+ }
+ }
+ }
+}
+
+.fiad-action {
+ height: 35px;
+ width: 35px;
+ background: @amber;
+ display: inline-block;
+ border-radius: 50%;
+ text-align: center;
+ line-height: 35px;
+ font-weight: normal;
+ position: relative;
+ top: 4px;
+ margin-left: 5px;
+ .animation-name(fiad-action-anim);
+ .transform-origin(center center);
+ .backface-visibility(none);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
+ &:nth-child(2) {
+ .animation-duration(100ms);
+ }
+
+ &:nth-child(1) {
+ .animation-duration(250ms);
+ }
+
+ & > i {
+ font-size: 14px;
+ color: @white;
+ }
+
+ &:hover {
+ background-color: darken(@amber, 3%);
+ }
+}
+
+
+@-webkit-keyframes fiad-action-anim {
+ from {
+ .scale(0);
+ .opacity(0);
+ right: -20px;
+ }
+ to {
+ .scale(1);
+ .opacity(1);
+ right: 0;
+ }
+}
+
+@keyframes fiad-action-anim {
+ from {
+ .scale(0);
+ .opacity(0);
+ right: -20px;
+ }
+ to {
+ .scale(1);
+ .opacity(1);
+ right: 0;
+ }
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/login.less b/browser/app/less/inc/login.less
new file mode 100644
index 000000000..7e86faa0d
--- /dev/null
+++ b/browser/app/less/inc/login.less
@@ -0,0 +1,104 @@
+.login {
+ height: 100vh;
+ min-height: 500px;
+ background: @dark-gray;
+
+ text-align: center;
+ &:before {
+ height: ~"calc(100% - 110px)";
+ width: 1px;
+ content: "";
+ }
+}
+
+.l-wrap,
+.login:before {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.l-wrap {
+ width: 80%;
+ max-width: 500px;
+ margin-top: -50px;
+ &.toggled {
+ display: inline-block;
+ }
+
+ .input-group:not(:last-child) {
+ margin-bottom: 40px;
+ }
+}
+
+.l-footer {
+ height: 110px;
+ padding: 0 50px;
+}
+
+.lf-logo {
+ float: right;
+ img {
+ width: 40px;
+ }
+}
+
+.lf-server {
+ float: left;
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 20px;
+ font-weight: 400;
+ padding-top: 40px;
+}
+
+@media (max-width: @screen-sm-min) {
+ .lf-logo,
+ .lf-server {
+ float: none;
+ display: block;
+ text-align: center;
+ width: 100%;
+ }
+
+ .lf-logo {
+ margin-bottom: 5px;
+ }
+
+ .lf-server {
+ font-size: 15px;
+ }
+}
+
+.lw-btn {
+ width: 50px;
+ height: 50px;
+ border: 1px solid @white;
+ display: inline-block;
+ border-radius: 50%;
+ font-size: 22px;
+ color: @white;
+ .transition(all);
+ .transition-duration(300ms);
+ opacity: 0.3;
+ background-color: transparent;
+ line-height: 45px;
+ padding: 0;
+ &:hover {
+ color: @white;
+ opacity: 0.8;
+ border-color: @white;
+ }
+
+ i {
+ display: block;
+ width: 100%;
+ padding-left: 3px;
+ }
+}
+
+/*------------------------------
+ Chrome autofill fix
+-------------------------------*/
+input:-webkit-autofill {
+ -webkit-box-shadow:0 0 0 50px @dark-gray inset !important;
+ -webkit-text-fill-color: @white !important;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/misc.less b/browser/app/less/inc/misc.less
new file mode 100644
index 000000000..dba1b43b5
--- /dev/null
+++ b/browser/app/less/inc/misc.less
@@ -0,0 +1,102 @@
+/*--------------------------
+ Close
+----------------------------*/
+.close-variant(@color, @bg-color, @color-hover, @bg-color-hover) {
+ span {
+ background-color: @bg-color;
+ color: @color;
+ }
+
+ &:hover,
+ &:focus {
+ span {
+ background-color: @bg-color-hover;
+ color: @color-hover;
+ }
+ }
+}
+
+.close {
+ right: 15px;
+ font-weight: normal;
+ opacity: 1;
+ font-size: 18px;
+ position: absolute;
+ text-align: center;
+ top: 16px;
+ z-index: 1;
+ padding: 0;
+ border: 0;
+ background-color: transparent;
+
+ span {
+ width: 25px;
+ height: 25px;
+ display: block;
+ border-radius: 50%;
+ line-height: 24px;
+ text-shadow: none;
+ }
+
+ &:not(.close-alt) {
+ .close-variant(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1), @white, rgba(255, 255, 255, 0.2));
+ }
+}
+
+.close-alt {
+ .close-variant(#989898, #efefef, #7b7b7b, #e8e8e8);
+}
+
+
+/*--------------------------
+ Hidden
+----------------------------*/
+.hidden {
+ display: none !important;
+}
+
+
+/*--------------------------
+ Copy text
+----------------------------*/
+.copy-text {
+ input {
+ width: 100%;
+ border-radius: 1px;
+ border: 1px solid @input-border;
+ padding: 7px 12px;
+ font-size: 13px;
+ line-height: 100%;
+ cursor: text;
+ .transition(border-color);
+ .transition-duration(300ms);
+
+ &:hover {
+ border-color: darken(@input-border, 5%);
+ }
+ }
+}
+
+/*--------------------------
+ Sharing
+----------------------------*/
+.share-availability {
+ margin-bottom: 40px;
+
+ &:before,
+ &:after {
+ position: absolute;
+ bottom: -30px;
+ font-size: 10px;
+ }
+
+ &:before {
+ content: '01 Sec';
+ left: 0;
+ }
+
+ &:after {
+ content: '7 days';
+ right: 0;
+ }
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/mixin.less b/browser/app/less/inc/mixin.less
new file mode 100644
index 000000000..528f2f26b
--- /dev/null
+++ b/browser/app/less/inc/mixin.less
@@ -0,0 +1,52 @@
+/*--------------------------
+ User Select
+----------------------------*/
+.user-select(@value) {
+ -webkit-user-select: @value;
+ -moz-user-select: @value;
+ -ms-user-select: @value;
+ user-select: @value;
+}
+
+
+/*----------------------------------------
+ CSS Animations based on animate.css
+-----------------------------------------*/
+.animated(@name, @duration) {
+ -webkit-animation-name: @name;
+ animation-name: @name;
+ -webkit-animation-duration: @duration;
+ animation-duration: @duration;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+}
+
+/*-------------------------------------------------
+ For loop mixin for generate custom classes
+--------------------------------------------------*/
+.for(@i, @n) {.-each(@i)}
+.for(@n) when (isnumber(@n)) {.for(1, @n)}
+.for(@i, @n) when not (@i = @n) {
+ .for((@i + (@n - @i) / abs(@n - @i)), @n);
+}
+
+.for(@array) when (default()) {.for-impl_(length(@array))}
+.for-impl_(@i) when (@i > 1) {.for-impl_((@i - 1))}
+.for-impl_(@i) when (@i > 0) {.-each(extract(@array, @i))}
+
+/*----------------------------------------
+ List Loader
+-----------------------------------------*/
+.list-loader(@width, @height, @borderColor, @borderColorBottom) {
+ content: '';
+ width: @width;
+ height: @height;
+ border-radius: 50%;
+ .animated(zoomIn, 500ms);
+ border: 2px solid @borderColor;
+ border-bottom-color: @borderColorBottom;
+ position: absolute;
+ z-index: 1;
+ -webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
+ animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
+}
\ No newline at end of file
diff --git a/browser/app/less/inc/modal.less b/browser/app/less/inc/modal.less
new file mode 100644
index 000000000..6d66ffa77
--- /dev/null
+++ b/browser/app/less/inc/modal.less
@@ -0,0 +1,294 @@
+/*--------------------------
+ Modal
+----------------------------*/
+.modal {
+ @media(min-width: @screen-sm-min) {
+ text-align: center;
+
+ &:before {
+ content: '';
+ height: 100%;
+ width: 1px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .modal-dialog {
+ text-align: left;
+ margin: 10px auto;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+}
+
+.modal-dark {
+ .modal-header {
+ color: rgba(255, 255, 255, 0.4);
+
+ small {
+ color: rgba(255, 255, 255, 0.2);
+ }
+ }
+
+ .modal-content {
+ background-color: @dark-gray;
+ }
+}
+
+.modal-backdrop {
+ .animated(fadeIn, 200ms);
+}
+
+.modal-dialog {
+ .animated(zoomIn, 200ms);
+}
+
+.modal-header {
+ color: @text-strong-color;
+ position: relative;
+
+ small {
+ display: block;
+ text-transform: none;
+ font-size: 12px;
+ margin-top: 5px;
+ color: #a8a8a8;
+ }
+}
+
+.modal-content {
+ border-radius: 3px;
+ box-shadow: none;
+}
+
+.modal-footer {
+ padding: 0 30px 30px;
+ text-align: center;
+}
+
+
+/*--------------------------
+ Dialog
+----------------------------*/
+.modal-confirm {
+ .modal-dialog {
+ text-align: center;
+ }
+}
+
+.mc-icon {
+ margin: 0 0 10px;
+
+ & > i {
+ font-size: 60px;
+ }
+}
+
+.mci-red {
+ color: #ff8f8f;
+}
+
+.mci-amber {
+ color: @amber;
+}
+
+.mci-green {
+ color: #64e096;
+}
+
+.mc-text {
+ color: @text-strong-color;
+}
+
+.mc-sub {
+ color: @text-muted-color;
+ margin-top: 5px;
+ font-size: 13px;
+}
+//--------------------------
+
+
+/*--------------------------
+ About
+----------------------------*/
+.modal-about {
+ @media (max-width: @screen-xs-max) {
+ text-align: center;
+
+ .modal-dialog {
+ max-width: 400px;
+ width: 90%;
+ margin: 20px auto 0;
+ }
+ }
+}
+
+.ma-inner {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-height: 350px;
+ position: relative;
+
+ @media (min-width: @screen-sm-min) {
+ &:before {
+ content: '';
+ width: 150px;
+ height: 100%;
+ top: 0;
+ left: 0;
+ position: absolute;
+ border-radius: 3px 0 0px 3px;
+ background-color: #23282C;
+ }
+ }
+}
+
+.mai-item {
+ &:first-child {
+ width: 150px;
+ text-align: center;
+ }
+
+ &:last-child {
+ flex: 4;
+ padding: 30px;
+ }
+}
+
+.maii-logo {
+ width: 70px;
+ position: relative;
+
+}
+
+.maii-list {
+ list-style: none;
+ padding: 0;
+
+ & > li {
+ margin-bottom: 15px;
+
+ div {
+ color: rgba(255, 255, 255, 0.8);
+ text-transform: uppercase;
+ font-size: 14px;
+ }
+
+ small {
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.4);
+ }
+ }
+}
+//--------------------------
+
+
+/*--------------------------
+ Preferences
+----------------------------*/
+.toggle-password {
+ position: absolute;
+ bottom: 30px;
+ right: 35px;
+ width: 30px;
+ height: 30px;
+ border: 1px solid #eee;
+ border-radius: 0;
+ text-align: center;
+ cursor: pointer;
+ z-index: 10;
+ background-color: @white;
+ padding-top: 5px;
+
+ &.toggled {
+ background: #eee;
+ }
+}
+//--------------------------
+
+
+/*--------------------------
+ Policy
+----------------------------*/
+.pm-body {
+ padding-bottom: 30px;
+}
+
+.pmb-header {
+ margin-bottom: 35px;
+}
+
+.pmb-list {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 35px;
+
+ &:nth-child(even) {
+ background-color: #F7F7F7;
+ }
+
+ .form-control {
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+
+header.pmb-list {
+ margin: 20px 0 10px;
+}
+
+.pmbl-item {
+ display: block;
+ font-size: 13px;
+
+ &:nth-child(1) {
+ flex: 2;
+ }
+
+ &:nth-child(2) {
+ margin: 0 25px;
+ width: 150px;
+ }
+
+ &:nth-child(3) {
+ width: 70px;
+ }
+}
+
+div.pmb-list {
+ select {
+ border: 0;
+ }
+
+ .pml-item {
+ &:not(:last-child) {
+ padding: 0 5px;
+ }
+ }
+}
+//--------------------------
+
+
+/*--------------------------
+ Create Bucket
+----------------------------*/
+.modal-create-bucket {
+ .modal-dialog {
+ position: fixed;
+ right: 25px;
+ bottom: 95px;
+ margin: 0;
+ height: 110px;
+ }
+
+ .modal-content {
+ width: 100%;
+ height: 100%;
+ }
+}
+//--------------------------
+
diff --git a/browser/app/less/inc/sidebar.less b/browser/app/less/inc/sidebar.less
new file mode 100644
index 000000000..c975472eb
--- /dev/null
+++ b/browser/app/less/inc/sidebar.less
@@ -0,0 +1,187 @@
+/*--------------------------
+ Sidebar
+----------------------------*/
+.fe-sidebar {
+ width: @fe-sidebar-width;
+ background-color: @dark-gray;
+ position: fixed;
+ height: 100%;
+ overflow: hidden;
+ padding: 35px;
+
+ @media(min-width: @screen-md-min) {
+ .translate3d(0, 0, 0);
+ }
+
+ @media(max-width: @screen-sm-max) {
+ padding-top: 85px;
+ z-index: 9;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.65);
+ .transition(all);
+ .transition-duration(300ms);
+ .translate3d((-@fe-sidebar-width - 15px), 0, 0);
+
+ &.toggled {
+ .translate3d(0, 0, 0);
+ }
+ }
+
+ a {
+ color: rgba(255, 255, 255, 0.58);
+
+ &:hover {
+ color: @white;
+ }
+ }
+}
+
+/*--------------------------
+ Header
+----------------------------*/
+.fes-header {
+ margin-bottom: 40px;
+
+ img,
+ h2 {
+ float: left;
+ }
+
+ h2 {
+ margin: 13px 0 0 10px;
+ font-weight: normal;
+ }
+
+ img {
+ width: 32px;
+ }
+}
+
+/*--------------------------
+ List
+----------------------------*/
+.fesl-inner {
+ height: ~"calc(100vh - 260px)";
+ overflow: auto;
+ padding: 0;
+ margin: 0 -35px;
+
+ & li {
+ position: relative;
+
+ & > a {
+ display: block;
+ padding: 10px 40px 12px 65px;
+ .text-overflow();
+
+ &:before {
+ font-family: FontAwesome;
+ content: '\f0a0';
+ font-size: 17px;
+ position: absolute;
+ top: 10px;
+ left: 35px;
+ .opacity(0.8);
+ }
+
+ &.fesli-loading {
+ &:before {
+ .list-loader(20px, 20px, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.5));
+ left: 32px;
+ top: 0;
+ bottom: 0;
+ margin: auto;
+ }
+ }
+ }
+
+ &.active {
+ background-color: rgba(0, 0, 0, 0.2);
+
+ & > a {
+ color: @white;
+ }
+ }
+
+ &:not(.active):hover {
+ background-color: rgba(0, 0, 0, 0.1);
+
+ & > a {
+ color: @white;
+ }
+ }
+
+ &:hover {
+ .fesli-trigger {
+ .opacity(0.6);
+
+ &:hover {
+ .opacity(1);
+ }
+ }
+ }
+ }
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ &:hover .scrollbar-vertical {
+ opacity: 1;
+ }
+}
+
+.fesli-trigger {
+ .opacity(0);
+ .transition(all);
+ .transition-duration(200ms);
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 40px;
+ height: 100%;
+ cursor: pointer;
+ background: url(../../img/more-h-light.svg) no-repeat left;
+}
+
+/* Scrollbar */
+.scrollbar-vertical {
+ position: absolute;
+ right: 5px;
+ width: 4px;
+ height: 100%;
+ opacity: 0;
+ .transition(opacity);
+ .transition-duration(300ms);
+
+ div {
+ border-radius: 1px !important;
+ background-color: #6a6a6a !important;
+ }
+}
+
+/*--------------------------
+ Host
+----------------------------*/
+.fes-host {
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ z-index: 1;
+ background: @dark-gray;
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 15px;
+ font-weight: 400;
+ width: @fe-sidebar-width;
+ padding: 20px;
+ .text-overflow();
+
+ & > i {
+ margin-right: 10px;
+ }
+}
+
+
+
+
diff --git a/browser/app/less/inc/variables.less b/browser/app/less/inc/variables.less
new file mode 100644
index 000000000..de6589994
--- /dev/null
+++ b/browser/app/less/inc/variables.less
@@ -0,0 +1,94 @@
+/*--------------------------
+ Base
+----------------------------*/
+@font-family-sans-serif : 'Lato', sans-serif;
+@font-family-icon : 'fontAwesome';
+@body-bg : #edecec;
+@text-color : #8e8e8e;
+@font-size-base : 15px;
+@link-color : #46a5e0;
+@link-hover-decoration : none;
+
+
+/*--------------------------
+ File Explorer
+----------------------------*/
+@fe-sidebar-width : 300px;
+@text-muted-color : #BDBDBD;
+@text-strong-color : #333;
+
+/*--------------------------
+ Colors
+----------------------------*/
+@cyan : #2ED2FF;
+@amber : #ffc107;
+@red : #ff726f;
+@grey : #f5f5f5;
+@dark-blue : #0084d3;
+@blue : #00a6f7;
+@white : #ffffff;
+@black : #1b1e25;
+@blue : #50b2ff;
+@light-blue : #c1d1e8;
+@green : #33d46f;
+@yellow : #FFC107;
+@orange : #ffc155;
+@purple : #9C27B0;
+@teal : #009688;
+@brown : #795548;
+@blue-gray : #374952;
+@dark-gray : #32393F;
+
+
+/*--------------------------
+ Dropdown
+----------------------------*/
+@dropdown-fallback-border : transparent;
+@dropdown-border : transparent;
+@dropdown-divider-bg : '';
+@dropdown-link-hover-bg : rgba(0,0,0,0.05);
+@dropdown-link-color : @text-color;
+@dropdown-link-hover-color : #333;
+@dropdown-link-disabled-color : #e4e4e4;
+@dropdown-divider-bg : rgba(0,0,0,0.08);
+@dropdown-link-active-color : #333;
+@dropdown-link-active-bg : rgba(0, 0, 0, 0.075);
+@dropdown-shadow : 0 2px 10px rgba(0, 0, 0, 0.2);
+
+
+/*--------------------------
+ Modal
+----------------------------*/
+@modal-content-fallback-border-color: transparent;
+@modal-content-border-color: transparent;
+@modal-backdrop-bg: rgba(0,0,0,0.1);
+@modal-header-border-color: transparent;
+@modal-title-line-height: transparent;
+@modal-footer-border-color: transparent;
+@modal-inner-padding: 30px 35px;
+@modal-title-padding: 30px 35px 0px;
+@modal-sm: 400px;
+
+
+/*-------------------------
+ Buttons
+--------------------------*/
+@btn-border-radius-large: 2px;
+@btn-border-radius-small: 2px;
+@btn-border-radius-base: 2px;
+
+
+/*-------------------------
+ Colors
+--------------------------*/
+@brand-primary: #2196F3;
+@brand-success: #4CAF50;
+@brand-info: #00BCD4;
+@brand-warning: #FF9800;
+@brand-danger: #FF5722;
+
+
+/*-------------------------
+ Form
+--------------------------*/
+@input-border: #eee;
\ No newline at end of file
diff --git a/browser/app/less/main.less b/browser/app/less/main.less
new file mode 100644
index 000000000..29aa50772
--- /dev/null
+++ b/browser/app/less/main.less
@@ -0,0 +1,39 @@
+/*----------------------------
+ Bootstrap
+-----------------------------*/
+@import "../../node_modules/bootstrap/less/scaffolding.less";
+@import "../../node_modules/bootstrap/less/variables.less";
+@import "../../node_modules/bootstrap/less/grid.less";
+@import "../../node_modules/bootstrap/less/mixins.less";
+@import "../../node_modules/bootstrap/less/normalize.less";
+@import "../../node_modules/bootstrap/less/dropdowns.less";
+@import "../../node_modules/bootstrap/less/modals.less";
+@import "../../node_modules/bootstrap/less/tooltip.less";
+@import "../../node_modules/bootstrap/less/responsive-utilities.less";
+
+
+/*----------------------------
+ App
+-----------------------------*/
+@import 'inc/mixin';
+@import 'inc/variables';
+@import 'inc/base';
+@import 'inc/animate/animate';
+@import 'inc/generics';
+@import 'inc/font';
+@import 'inc/form';
+@import 'inc/buttons';
+@import 'inc/misc';
+@import 'inc/login';
+@import 'inc/header';
+@import 'inc/sidebar';
+@import 'inc/list';
+@import 'inc/file-explorer';
+@import 'inc/ie-warning';
+
+/*----------------------------
+ Boostrap
+-----------------------------*/
+@import 'inc/dropdown';
+@import 'inc/alert';
+@import 'inc/modal';
diff --git a/browser/build.js b/browser/build.js
new file mode 100644
index 000000000..f612b7d02
--- /dev/null
+++ b/browser/build.js
@@ -0,0 +1,126 @@
+/*
+ * Minio Browser (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.
+ */
+
+var moment = require('moment')
+var async = require('async')
+var exec = require('child_process').exec
+var fs = require('fs')
+
+var isProduction = process.env.NODE_ENV == 'production' ? true : false
+var assetsFileName = ''
+var commitId = ''
+var date = moment.utc()
+var version = date.format('YYYY-MM-DDTHH:mm:ss') + 'Z'
+var releaseTag = date.format('YYYY-MM-DDTHH-mm-ss') + 'Z'
+var buildType = 'DEVELOPMENT'
+if (process.env.MINIO_UI_BUILD) buildType = process.env.MINIO_UI_BUILD
+
+rmDir = function(dirPath) {
+ try { var files = fs.readdirSync(dirPath); }
+ catch(e) { return; }
+ if (files.length > 0)
+ for (var i = 0; i < files.length; i++) {
+ var filePath = dirPath + '/' + files[i];
+ if (fs.statSync(filePath).isFile())
+ fs.unlinkSync(filePath);
+ else
+ rmDir(filePath);
+ }
+ fs.rmdirSync(dirPath);
+};
+
+async.waterfall([
+ function(cb) {
+ rmDir('production');
+ rmDir('dev');
+ var cmd = 'webpack -p --config webpack.production.config.js'
+ if (!isProduction) {
+ cmd = 'webpack';
+ }
+ console.log('Running', cmd)
+ exec(cmd, cb)
+ },
+ function(stdout, stderr, cb) {
+ if (isProduction) {
+ fs.renameSync('production/index_bundle.js',
+ 'production/index_bundle-' + releaseTag + '.js')
+ } else {
+ fs.renameSync('dev/index_bundle.js',
+ 'dev/index_bundle-' + releaseTag + '.js')
+ }
+ var cmd = 'git log --format="%H" -n1'
+ console.log('Running', cmd)
+ exec(cmd, cb)
+ },
+ function(stdout, stderr, cb) {
+ if (!stdout) throw new Error('commitId is empty')
+ commitId = stdout.replace('\n', '')
+ if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
+ assetsFileName = 'ui-assets.go';
+ var cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true production/...'
+ if (!isProduction) {
+ cmd = 'go-bindata-assetfs -pkg miniobrowser -nocompress=true dev/...'
+ }
+ console.log('Running', cmd)
+ exec(cmd, cb)
+ },
+ function(stdout, stderr, cb) {
+ var cmd = 'gofmt -s -w -l bindata_assetfs.go'
+ console.log('Running', cmd)
+ exec(cmd, cb)
+ },
+ function(stdout, stderr, cb) {
+ fs.renameSync('bindata_assetfs.go', assetsFileName)
+ fs.appendFileSync(assetsFileName, '\n')
+ fs.appendFileSync(assetsFileName, 'var UIReleaseTag = "' + buildType + '.' +
+ releaseTag + '"\n')
+ fs.appendFileSync(assetsFileName, 'var UICommitID = "' + commitId + '"\n')
+ fs.appendFileSync(assetsFileName, 'var UIVersion = "' + version + '"')
+ fs.appendFileSync(assetsFileName, '\n')
+ var contents;
+ if (isProduction) {
+ contents = fs.readFileSync(assetsFileName, 'utf8')
+ .replace(/_productionIndexHtml/g, '_productionIndexHTML')
+ .replace(/productionIndexHtmlBytes/g, 'productionIndexHTMLBytes')
+ .replace(/productionIndexHtml/g, 'productionIndexHTML')
+ .replace(/_productionIndex_bundleJs/g, '_productionIndexBundleJs')
+ .replace(/productionIndex_bundleJsBytes/g, 'productionIndexBundleJsBytes')
+ .replace(/productionIndex_bundleJs/g, 'productionIndexBundleJs')
+ .replace(/_productionJqueryUiMinJs/g, '_productionJqueryUIMinJs')
+ .replace(/productionJqueryUiMinJsBytes/g, 'productionJqueryUIMinJsBytes')
+ .replace(/productionJqueryUiMinJs/g, 'productionJqueryUIMinJs');
+ } else {
+ contents = fs.readFileSync(assetsFileName, 'utf8')
+ .replace(/_devIndexHtml/g, '_devIndexHTML')
+ .replace(/devIndexHtmlBytes/g, 'devIndexHTMLBytes')
+ .replace(/devIndexHtml/g, 'devIndexHTML')
+ .replace(/_devIndex_bundleJs/g, '_devIndexBundleJs')
+ .replace(/devIndex_bundleJsBytes/g, 'devIndexBundleJsBytes')
+ .replace(/devIndex_bundleJs/g, 'devIndexBundleJs')
+ .replace(/_devJqueryUiMinJs/g, '_devJqueryUIMinJs')
+ .replace(/devJqueryUiMinJsBytes/g, 'devJqueryUIMinJsBytes')
+ .replace(/devJqueryUiMinJs/g, 'devJqueryUIMinJs');
+ }
+ contents = contents.replace(/MINIO_UI_VERSION/g, version)
+ contents = contents.replace(/index_bundle.js/g, 'index_bundle-' + releaseTag + '.js')
+
+ fs.writeFileSync(assetsFileName, contents, 'utf8')
+ console.log('UI assets file :', assetsFileName)
+ cb()
+ }
+ ], function(err) {
+ if (err) return console.log(err)
+ })
diff --git a/browser/karma.conf.js b/browser/karma.conf.js
new file mode 100644
index 000000000..5637a12f5
--- /dev/null
+++ b/browser/karma.conf.js
@@ -0,0 +1,40 @@
+var webpack = require('webpack');
+
+module.exports = function (config) {
+ config.set({
+ browsers: [ process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome' ],
+ singleRun: true,
+ frameworks: [ 'mocha' ],
+ files: [
+ 'tests.webpack.js'
+ ],
+ preprocessors: {
+ 'tests.webpack.js': [ 'webpack' ]
+ },
+ reporters: [ 'dots' ],
+ webpack: {
+ module: {
+ loaders: [{
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ loader: 'babel',
+ query: {
+ presets: ['react', 'es2015']
+ }
+ }, {
+ test: /\.less$/,
+ loader: 'style!css!less'
+ }, {
+ test: /\.css$/,
+ loader: 'style!css'
+ }, {
+ test: /\.(eot|woff|woff2|ttf|svg|png)/,
+ loader: 'url'
+ }]
+ }
+ },
+ webpackServer: {
+ noInfo: true
+ }
+ });
+};
diff --git a/browser/package.json b/browser/package.json
new file mode 100644
index 000000000..23e8145c6
--- /dev/null
+++ b/browser/package.json
@@ -0,0 +1,82 @@
+{
+ "name": "minio-browser",
+ "version": "0.0.1",
+ "description": "Minio Browser",
+ "scripts": {
+ "test": "karma start",
+ "dev": "NODE_ENV=dev webpack-dev-server --devtool eval --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'"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/minio/miniobrowser"
+ },
+ "author": "Minio Inc",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/minio/miniobrowser/issues"
+ },
+ "homepage": "https://github.com/minio/miniobrowser",
+ "devDependencies": {
+ "async": "^1.5.2",
+ "babel-cli": "^6.14.0",
+ "babel-core": "^6.14.0",
+ "babel-loader": "^6.2.5",
+ "babel-plugin-syntax-object-rest-spread": "^6.13.0",
+ "babel-plugin-transform-object-rest-spread": "^6.8.0",
+ "babel-preset-es2015": "^6.14.0",
+ "babel-preset-react": "^6.11.1",
+ "babel-register": "^6.14.0",
+ "copy-webpack-plugin": "^0.3.3",
+ "css-loader": "^0.23.1",
+ "esformatter": "^0.10.0",
+ "esformatter-jsx-ignore": "^1.0.6",
+ "expect": "^1.20.2",
+ "history": "^1.17.0",
+ "html-webpack-plugin": "^2.22.0",
+ "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": "^1.7.0",
+ "less": "^2.7.1",
+ "less-loader": "^2.2.3",
+ "mocha": "^2.5.3",
+ "moment": "^2.15.1",
+ "purifycss-webpack-plugin": "^2.0.3",
+ "react": "^0.14.8",
+ "react-addons-test-utils": "^0.14.8",
+ "react-bootstrap": "^0.28.5",
+ "react-custom-scrollbars": "^2.3.0",
+ "react-redux": "^4.4.5",
+ "react-router": "^2.8.1",
+ "redux": "^3.6.0",
+ "redux-thunk": "^1.0.3",
+ "style-loader": "^0.13.1",
+ "superagent": "^1.8.4",
+ "superagent-es6-promise": "^1.0.0",
+ "url-loader": "^0.5.7",
+ "webpack": "^1.12.11",
+ "webpack-dev-server": "^1.14.1"
+ },
+ "dependencies": {
+ "bootstrap": "^3.3.6",
+ "classnames": "^2.2.3",
+ "font-awesome": "^4.7.0",
+ "humanize": "0.0.9",
+ "json-loader": "^0.5.4",
+ "local-storage-fallback": "^1.3.0",
+ "mime-db": "^1.25.0",
+ "mime-types": "^2.1.13",
+ "react": "^0.14.8",
+ "react-copy-to-clipboard": "^4.2.3",
+ "react-custom-scrollbars": "^2.2.2",
+ "react-dom": "^0.14.6",
+ "react-dropzone": "^3.5.3",
+ "react-onclickout": "2.0.4"
+ }
+}
diff --git a/browser/tests.webpack.js b/browser/tests.webpack.js
new file mode 100644
index 000000000..871037f23
--- /dev/null
+++ b/browser/tests.webpack.js
@@ -0,0 +1,2 @@
+var context = require.context('./app', true, /-test\.js$/);
+context.keys().forEach(context);
\ No newline at end of file
diff --git a/vendor/github.com/minio/miniobrowser/ui-assets.go b/browser/ui-assets.go
similarity index 100%
rename from vendor/github.com/minio/miniobrowser/ui-assets.go
rename to browser/ui-assets.go
diff --git a/browser/webpack.config.js b/browser/webpack.config.js
new file mode 100644
index 000000000..3ccdaba0b
--- /dev/null
+++ b/browser/webpack.config.js
@@ -0,0 +1,105 @@
+/*
+ * Minio Browser (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.
+ */
+
+var webpack = require('webpack')
+var path = require('path')
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var purify = require("purifycss-webpack-plugin")
+
+var exports = {
+ context: __dirname,
+ entry: [
+ path.resolve(__dirname, 'app/index.js')
+ ],
+ output: {
+ path: path.resolve(__dirname, 'dev'),
+ filename: 'index_bundle.js',
+ publicPath: '/minio/'
+ },
+ module: {
+ loaders: [{
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ loader: 'babel',
+ query: {
+ presets: ['react', 'es2015']
+ }
+ }, {
+ test: /\.less$/,
+ loader: 'style!css!less'
+ }, {
+ test: /\.json$/,
+ loader: 'json-loader'
+ },{
+ test: /\.css$/,
+ loader: 'style!css'
+ }, {
+ test: /\.(eot|woff|woff2|ttf|svg|png)/,
+ loader: 'url'
+ }]
+ },
+ node:{
+ fs:'empty'
+ },
+ devServer: {
+ historyApiFallback: {
+ index: '/minio/'
+ },
+ proxy: {
+ '/minio/webrpc': {
+ target: 'http://localhost:9000',
+ secure: false
+ },
+ '/minio/upload/*': {
+ target: 'http://localhost:9000',
+ secure: false
+ },
+ '/minio/download/*': {
+ target: 'http://localhost:9000',
+ secure: false
+ },
+ }
+ },
+ plugins: [
+ new CopyWebpackPlugin([
+ {from: 'app/css/loader.css'},
+ {from: 'app/img/favicon.ico'},
+ {from: 'app/img/browsers/chrome.png'},
+ {from: 'app/img/browsers/firefox.png'},
+ {from: 'app/img/browsers/safari.png'},
+ {from: 'app/img/logo.svg'},
+ {from: 'app/index.html'}
+ ]),
+ new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
+ new purify({
+ basePath: __dirname,
+ paths: [
+ "app/index.html",
+ "app/js/*.js"
+ ]
+ })
+ ]
+}
+
+if (process.env.NODE_ENV === 'dev') {
+ exports.entry = [
+ 'webpack/hot/dev-server',
+ 'webpack-dev-server/client?http://localhost:8080',
+ path.resolve(__dirname, 'app/index.js')
+ ]
+}
+
+module.exports = exports
diff --git a/browser/webpack.production.config.js b/browser/webpack.production.config.js
new file mode 100644
index 000000000..9c0604dcc
--- /dev/null
+++ b/browser/webpack.production.config.js
@@ -0,0 +1,88 @@
+/*
+ * Isomorphic Javascript library for Minio Browser JSON-RPC API, (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.
+ */
+
+var webpack = require('webpack')
+var path = require('path')
+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var purify = require("purifycss-webpack-plugin")
+
+var exports = {
+ context: __dirname,
+ entry: [
+ path.resolve(__dirname, 'app/index.js')
+ ],
+ output: {
+ path: path.resolve(__dirname, 'production'),
+ filename: 'index_bundle.js'
+ },
+ module: {
+ loaders: [{
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ loader: 'babel',
+ query: {
+ presets: ['react', 'es2015']
+ }
+ }, {
+ test: /\.less$/,
+ loader: 'style!css!less'
+ }, {
+ test: /\.json$/,
+ loader: 'json-loader'
+ }, {
+ test: /\.css$/,
+ loader: 'style!css'
+ }, {
+ test: /\.(eot|woff|woff2|ttf|svg|png)/,
+ loader: 'url'
+ }]
+ },
+ node:{
+ fs:'empty'
+ },
+ plugins: [
+ new CopyWebpackPlugin([
+ {from: 'app/css/loader.css'},
+ {from: 'app/img/favicon.ico'},
+ {from: 'app/img/browsers/chrome.png'},
+ {from: 'app/img/browsers/firefox.png'},
+ {from: 'app/img/browsers/safari.png'},
+ {from: 'app/img/logo.svg'},
+ {from: 'app/index.html'}
+ ]),
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': '"production"'
+ }),
+ new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
+ new purify({
+ basePath: __dirname,
+ paths: [
+ "app/index.html",
+ "app/js/*.js"
+ ]
+ })
+ ]
+}
+
+if (process.env.NODE_ENV === 'dev') {
+ exports.entry = [
+ 'webpack/hot/dev-server',
+ 'webpack-dev-server/client?http://localhost:8080',
+ path.resolve(__dirname, 'app/index.js')
+ ]
+}
+
+module.exports = exports
diff --git a/cmd/web-handlers.go b/cmd/web-handlers.go
index d4cb4a498..8346ef944 100644
--- a/cmd/web-handlers.go
+++ b/cmd/web-handlers.go
@@ -33,7 +33,7 @@ import (
"github.com/gorilla/mux"
"github.com/gorilla/rpc/v2/json2"
"github.com/minio/minio-go/pkg/policy"
- "github.com/minio/miniobrowser"
+ "github.com/minio/minio/browser"
)
// WebGenericArgs - empty struct for calls that don't accept arguments
diff --git a/cmd/web-router.go b/cmd/web-router.go
index b84ea27f8..f478abfb8 100644
--- a/cmd/web-router.go
+++ b/cmd/web-router.go
@@ -25,7 +25,7 @@ import (
router "github.com/gorilla/mux"
jsonrpc "github.com/gorilla/rpc/v2"
"github.com/gorilla/rpc/v2/json2"
- "github.com/minio/miniobrowser"
+ "github.com/minio/minio/browser"
)
// webAPI container for Web API.
diff --git a/vendor/github.com/minio/miniobrowser/LICENSE b/vendor/github.com/minio/miniobrowser/LICENSE
deleted file mode 100644
index 8f71f43fe..000000000
--- a/vendor/github.com/minio/miniobrowser/LICENSE
+++ /dev/null
@@ -1,202 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright {yyyy} {name of copyright owner}
-
- 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.
-
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 2411c44d9..8b55ad8eb 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -199,12 +199,6 @@
"revision": "9e734013294ab153b0bdbe182738bcddd46f1947",
"revisionTime": "2016-08-18T00:31:20Z"
},
- {
- "checksumSHA1": "lkkQ8bAbNRvg9AceSmuAfh3udFg=",
- "path": "github.com/minio/miniobrowser",
- "revision": "10e951aa618d52796584f9dd233353a52d104c8d",
- "revisionTime": "2017-01-23T04:37:46Z"
- },
{
"checksumSHA1": "GOSe2XEQI4AYwrMoLZu8vtmzkJM=",
"path": "github.com/minio/redigo/redis",