From cead24b0f7cbeb6e8be5032b852c56a9727f9d25 Mon Sep 17 00:00:00 2001 From: Krishna Srinivas Date: Mon, 23 Jan 2017 18:07:22 -0800 Subject: [PATCH] miniobrowser: Bring Minio browser source into minio repo. (#3617) --- browser/.babelrc | 8 + browser/.editorconfig | 16 + browser/.esformatter | 23 + browser/README.md | 37 + browser/app/css/loader.css | 98 +++ browser/app/fonts/lato/lato-normal.woff | Bin 0 -> 38240 bytes browser/app/fonts/lato/lato-normal.woff2 | Bin 0 -> 30348 bytes browser/app/img/arrow.svg | 3 + browser/app/img/browsers/chrome.png | Bin 0 -> 3726 bytes browser/app/img/browsers/firefox.png | Bin 0 -> 4795 bytes browser/app/img/browsers/safari.png | Bin 0 -> 4971 bytes browser/app/img/favicon.ico | Bin 0 -> 1340 bytes browser/app/img/logo.svg | 57 ++ browser/app/img/more-h-light.svg | 3 + browser/app/img/more-h.svg | 1 + browser/app/img/select-caret.svg | 3 + browser/app/index.html | 56 ++ browser/app/index.js | 116 +++ browser/app/js/__tests__/jsonrpc-test.js | 43 + browser/app/js/actions.js | 509 ++++++++++++ browser/app/js/components/Browse.js | 734 ++++++++++++++++++ browser/app/js/components/BrowserDropdown.js | 56 ++ browser/app/js/components/BrowserUpdate.js | 42 + browser/app/js/components/ConfirmModal.js | 50 ++ browser/app/js/components/Dropzone.js | 65 ++ browser/app/js/components/InputGroup.js | 49 ++ browser/app/js/components/Login.js | 133 ++++ browser/app/js/components/ObjectsList.js | 75 ++ browser/app/js/components/Path.js | 41 + browser/app/js/components/Policy.js | 80 ++ browser/app/js/components/PolicyInput.js | 83 ++ browser/app/js/components/SettingsModal.js | 215 +++++ browser/app/js/components/SideBar.js | 85 ++ browser/app/js/components/UploadModal.js | 141 ++++ .../app/js/components/__tests__/Login-test.js | 54 ++ browser/app/js/constants.js | 23 + browser/app/js/jsonrpc.js | 91 +++ browser/app/js/mime.js | 106 +++ browser/app/js/reducers.js | 176 +++++ browser/app/js/utils.js | 85 ++ browser/app/js/web.js | 124 +++ browser/app/less/inc/alert.less | 68 ++ browser/app/less/inc/animate/animate.less | 13 + browser/app/less/inc/animate/fadeIn.less | 26 + browser/app/less/inc/animate/fadeInDown.less | 54 ++ browser/app/less/inc/animate/fadeInUp.less | 54 ++ browser/app/less/inc/animate/fadeOut.less | 26 + browser/app/less/inc/animate/fadeOutDown.less | 54 ++ browser/app/less/inc/animate/fadeOutUp.less | 51 ++ browser/app/less/inc/animate/zoomIn.less | 23 + browser/app/less/inc/base.less | 31 + browser/app/less/inc/buttons.less | 53 ++ browser/app/less/inc/dropdown.less | 26 + browser/app/less/inc/file-explorer.less | 160 ++++ browser/app/less/inc/font.less | 7 + browser/app/less/inc/form.less | 249 ++++++ browser/app/less/inc/generics.less | 83 ++ browser/app/less/inc/header.less | 242 ++++++ browser/app/less/inc/ie-warning.less | 81 ++ browser/app/less/inc/list.less | 352 +++++++++ browser/app/less/inc/login.less | 104 +++ browser/app/less/inc/misc.less | 102 +++ browser/app/less/inc/mixin.less | 52 ++ browser/app/less/inc/modal.less | 294 +++++++ browser/app/less/inc/sidebar.less | 187 +++++ browser/app/less/inc/variables.less | 94 +++ browser/app/less/main.less | 39 + browser/build.js | 126 +++ browser/karma.conf.js | 40 + browser/package.json | 82 ++ browser/tests.webpack.js | 2 + .../miniobrowser => browser}/ui-assets.go | 0 browser/webpack.config.js | 105 +++ browser/webpack.production.config.js | 88 +++ cmd/web-handlers.go | 2 +- cmd/web-router.go | 2 +- vendor/github.com/minio/miniobrowser/LICENSE | 202 ----- vendor/vendor.json | 6 - 78 files changed, 6451 insertions(+), 210 deletions(-) create mode 100644 browser/.babelrc create mode 100644 browser/.editorconfig create mode 100644 browser/.esformatter create mode 100644 browser/README.md create mode 100644 browser/app/css/loader.css create mode 100755 browser/app/fonts/lato/lato-normal.woff create mode 100755 browser/app/fonts/lato/lato-normal.woff2 create mode 100644 browser/app/img/arrow.svg create mode 100644 browser/app/img/browsers/chrome.png create mode 100644 browser/app/img/browsers/firefox.png create mode 100644 browser/app/img/browsers/safari.png create mode 100644 browser/app/img/favicon.ico create mode 100644 browser/app/img/logo.svg create mode 100644 browser/app/img/more-h-light.svg create mode 100644 browser/app/img/more-h.svg create mode 100644 browser/app/img/select-caret.svg create mode 100644 browser/app/index.html create mode 100644 browser/app/index.js create mode 100644 browser/app/js/__tests__/jsonrpc-test.js create mode 100644 browser/app/js/actions.js create mode 100644 browser/app/js/components/Browse.js create mode 100644 browser/app/js/components/BrowserDropdown.js create mode 100644 browser/app/js/components/BrowserUpdate.js create mode 100644 browser/app/js/components/ConfirmModal.js create mode 100644 browser/app/js/components/Dropzone.js create mode 100644 browser/app/js/components/InputGroup.js create mode 100644 browser/app/js/components/Login.js create mode 100644 browser/app/js/components/ObjectsList.js create mode 100644 browser/app/js/components/Path.js create mode 100644 browser/app/js/components/Policy.js create mode 100644 browser/app/js/components/PolicyInput.js create mode 100644 browser/app/js/components/SettingsModal.js create mode 100644 browser/app/js/components/SideBar.js create mode 100644 browser/app/js/components/UploadModal.js create mode 100644 browser/app/js/components/__tests__/Login-test.js create mode 100644 browser/app/js/constants.js create mode 100644 browser/app/js/jsonrpc.js create mode 100644 browser/app/js/mime.js create mode 100644 browser/app/js/reducers.js create mode 100644 browser/app/js/utils.js create mode 100644 browser/app/js/web.js create mode 100644 browser/app/less/inc/alert.less create mode 100644 browser/app/less/inc/animate/animate.less create mode 100644 browser/app/less/inc/animate/fadeIn.less create mode 100644 browser/app/less/inc/animate/fadeInDown.less create mode 100644 browser/app/less/inc/animate/fadeInUp.less create mode 100644 browser/app/less/inc/animate/fadeOut.less create mode 100644 browser/app/less/inc/animate/fadeOutDown.less create mode 100644 browser/app/less/inc/animate/fadeOutUp.less create mode 100644 browser/app/less/inc/animate/zoomIn.less create mode 100644 browser/app/less/inc/base.less create mode 100644 browser/app/less/inc/buttons.less create mode 100644 browser/app/less/inc/dropdown.less create mode 100644 browser/app/less/inc/file-explorer.less create mode 100644 browser/app/less/inc/font.less create mode 100644 browser/app/less/inc/form.less create mode 100644 browser/app/less/inc/generics.less create mode 100644 browser/app/less/inc/header.less create mode 100644 browser/app/less/inc/ie-warning.less create mode 100644 browser/app/less/inc/list.less create mode 100644 browser/app/less/inc/login.less create mode 100644 browser/app/less/inc/misc.less create mode 100644 browser/app/less/inc/mixin.less create mode 100644 browser/app/less/inc/modal.less create mode 100644 browser/app/less/inc/sidebar.less create mode 100644 browser/app/less/inc/variables.less create mode 100644 browser/app/less/main.less create mode 100644 browser/build.js create mode 100644 browser/karma.conf.js create mode 100644 browser/package.json create mode 100644 browser/tests.webpack.js rename {vendor/github.com/minio/miniobrowser => browser}/ui-assets.go (100%) create mode 100644 browser/webpack.config.js create mode 100644 browser/webpack.production.config.js delete mode 100644 vendor/github.com/minio/miniobrowser/LICENSE 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 0000000000000000000000000000000000000000..f2317755c63f47a6bd74acdf0140651b266f9c69 GIT binary patch literal 38240 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 MkN^Qz)kKW|0C6biBme*a literal 0 HcmV?d00001 diff --git a/browser/app/fonts/lato/lato-normal.woff2 b/browser/app/fonts/lato/lato-normal.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..2a119ebd595ba7e29b9ca44d78df16a35720d586 GIT binary patch literal 30348 zcmV)NK)1hlPew8T0RR910CtQ36951J0V-es0CpSz0t43o00000000000000000000 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

63G5A8(ny|T z0Y$ivi$rm--ts;U)c(T8-e~vZO8lD9fn;rfkrY3HEzz|Ya~bSEXo)2zGH5MO1d*-F zrsXa6*q4~RgNf^VRq%-8cj2&FTffd>Jl%o5n1YD*jnGk{nbVGEUPf<%53+;M3Zha6 zTa0p?opXLOFSN?9$j{GgHkOVLXsR9~)gMNyr>~zDm>x?&gGd5lxVjfiz3`aP2d1@p zq2;pEgBP8QZ!Btoo2^6>Nq>SMA>P#uU;WBY=G{P?`m}*@$!;QeK}mJlTxi6QjcL+YGRq=szo+r|;&?}M{$YK1wU=YE`;?|mbE>7) zF>5h@(UrixsqqL-I=swu*{h;?EFG5O3}GgRT63GqWbG$yZbvA8i?hwho@tiKTt0(i z1j#-&p)=VQuM@nv<)1rHQfrN@I>CSV!Wp>?O?eDMG-dr4S>KV6?XKH}irZk?QqIdP zu{M`KsjaTEv%IUCt+rZK<(r~v(2T`yJL7Aeq1E+OTCI8=XnXD4BkQ(Vyhq_YnO|Ot zci&dtPlVMjo2!Ag5{mQoE!7Uy}6NI1X5M(_~h+HhE2Dz!uU!`POUM9$J; z)9~rnDeFy*!}sb-pQ^aMd)6M?x{5$4evb=)?A?a%O`J9}e+xjZ7uIyHtt0wY0Rg)R zv-+yZqrlF%E!U*hDyO_>d$FpT`Hvo_)U!PRd~d2-RK{=moIYYv-KuCc%sc`rK9i;i z^V;8Y{L85+v7FJ;UY>>=i5pJs8Q+&3k?rN4c06&I0KtB)0x5B$WN zi*|10Pt!%mClIyfT3je*%_oY@Yj63);+B<-uTU?gOJ41}>~DOHc-gw_>-fu$9Tc1CX8kM&VmOh`0 zVlBmho`U!aBMW(CP+)I}yF@KM2tkm#Azrk}f-QN9ZP6$$6e`SWMM&5R26$ODKh-g` zdg%QUY%3XZ`eae%Y7mhuUDU+<1d?6hu^g(vMEy%C9Zh%Jl4+WgV=B_KbMs9Ayhc75S8a zE7Y`l&bEoD5kAC@dh`<|W{Qj8H;|Yc7OEkr7h(c0F+YT6b5XUSQ1fC~Pt6gJsy>jO zQ8Ief-j)h)$D=A_(x;V6K^%RAwUc@!LU@{Cw|-=16F0!zfsA7U7SK_~x1l``*|b?z zZOi5Ez?(VwDw4uLvNXlQcNE5w>i(%M%7VC=!=b~k-V~?xlAiYJGr*w6n%&R5gQJ{B zj88v&e*rNNeYfH&QZZt9^hU}s4$|f!7g<%ITb)2^U0$};NYZ9+eW5UkggE@R&(Syk z4vYT^^g+h)uCf2H!3izs>)*u>{W6BZo(IVG!wfzbyh#x&JtofUSy#mk8O>c{I!}K+ z$bxrA!@W zs8y$4gGRH>@errEL1Sdb(rbRweh+N+*t6-FGmhKtj!pIiLvZkDaIw|%J_#26&id6a zelu*;t6nx{p80lK;B^bV;x%u3!<*jnrA6NNj(3e)><7Et@PQ94@wIPlv-D2F)0Ez9 zP>Z&^%X`|POQ&wjePxAJR_d|FYS#=|XYEX@Z{9b)bDf*Vq+4zWkKhx6JrTSGgqV;J z(vXEb6rl`NsAo;BUvbq7bEi&y(H7@Ib8>4#8$KQhUCapI@C*I7w^zFw^qQFM zW-Wn0CRlwbv0NONP@1w>EP+7aVz^i?j*I6KxI`|AOBQ97k)tsLxYQVD)x&C&QCU`8 zqw)I0($f>@cC;>IHS*5@01-N+34FlCa&b2=ezgI_e_tJU0G{fk$=Y)+c + + \ 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 0000000000000000000000000000000000000000..278ef4d15ecf2d715b900a25064ecb2fe8728f1b GIT binary patch literal 3726 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 literal 0 HcmV?d00001 diff --git a/browser/app/img/browsers/firefox.png b/browser/app/img/browsers/firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..2803f10a79548091e18bdff5ccafb9b9720b6e4e GIT binary patch literal 4795 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 VpWD}dK9~Ri002ovPDHLkV1mB@DEj~a literal 0 HcmV?d00001 diff --git a/browser/app/img/browsers/safari.png b/browser/app/img/browsers/safari.png new file mode 100644 index 0000000000000000000000000000000000000000..4ed52b904f1a7caf4b7144e45c301cb6ace38005 GIT binary patch literal 4971 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 literal 0 HcmV?d00001 diff --git a/browser/app/img/favicon.ico b/browser/app/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0718efa8328532746848d3ef36b3f1befc69c061 GIT binary patch literal 1340 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7a$D;Kb?2i11Zh|kH}&m z?E%JaC$sH9f@KAc=|EZmjN5x7xLyfrRub*JWVB>yR_F@XD@r>8#G5*HY-nN?*J?8LD&q?MaQ={jUX~A+mgsEP zHF`>+9y(5I>Mw;%Ubb@9%oy?aOZg_X%v3q*%Wre9_4o6d_wVNwzvC54_*r9}QeemA zm$+;B^4*WNI;l1v|Kz)F>pivPt%q01b3QV2V*CA(OHI4=M2Od)#sAvc|K5sBfAv!% z=RoO=-rZhTBC8rU)(FQK>3x57)$jeCcSi)@bnR(Oy|HTo?^IL8(tT3v-4mnT-rUK1 za_)pio_Bj~^Gh$rqEw4E(-$2v(?s9@SGe}|9rxGo&u$1$+NbxX*(*8WoWdXF`zDtw zE$1Ee*qU_Q_nQB;*|%hmxL--~kk2Fqgv%i~CoIGthll2GbZx4L` z>=k(aS;kSNvtP90Icx9n-)napO@C8n-1oq{hj+vjo_-o^tZ(J2ytUIDPu0OW!S*~>@slK?_J5jHrOHM^)>%!&B zZ{OP58{OUNbp7bp)yJfz!uVSSu03>`Gv&^t?0^YJ&ipOAuJXU%U$nFHCH2jYv@ZsKUz zxi)3`@y|cM-TQfLo@j$*LfY0Y`H9}S&l%1!>WC!>HEZaVG#*{@kkdxDQu~iS1M^F9 z@y|N{*{@9H+Bomib!lKmimVEWC<)F_D=AMbN@ZZExb^vruaEy%U!U_n=fVsw8(-QS zU|?|7+~B&oNf3vLNwCN1GoD*7>FerU(m&^+<8}4Bi^g?Rkg^N@C;hLTKjZDW_QGk6 zlV=$j*7`}TDG}An23n#BvLqysN(R + + + + + + 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",