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 + + +
+ + +
+
+ +
+
+ +
+ Days +
+
+ +
+ +
+
+ +
+ Hours +
+
+ +
+ +
+
+ +
+ Minutes +
+
+ +
+ +
+
+
+
+
+ + + + +
+
+ { 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 ( +
  • + + + + + +
  • + Github +
  • +
  • + Fullscreen +
  • +
  • + Documentation +
  • +
  • + Ask for help +
  • +
  • + About +
  • +
  • + Settings +
  • +
  • + Sign Out +
  • + + + + ) +} + +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 } +
    +
    +
    + + +
    +
    + ) +} + +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 } + + +
    +} + +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 ( +

    selectPrefix(e, '') } href="">{ currentBucket }{ path }

    + ) +} + +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 } +
    +
    + +
    +
    + +
    +
    + ) + } +} + +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 } /> +
    +
    + +
    +
    + +
    +
    + ) + } +} + +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 + + + + + + +
    + + + +
    +
    + ) + } +} + +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 ( + +
    + +
    +
    + + +
    +
    +
    }> +
      + { list } +
    + +
    +
    + +
    + + ) +} + +// 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",