diff --git a/browser/app/js/browser/AboutModal.js b/browser/app/js/browser/AboutModal.js
new file mode 100644
index 000000000..054526097
--- /dev/null
+++ b/browser/app/js/browser/AboutModal.js
@@ -0,0 +1,64 @@
+/*
+ * Minio Cloud Storage (C) 2018 Minio, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+import { Modal } from "react-bootstrap"
+import logo from "../../img/logo.svg"
+
+export const AboutModal = ({ serverInfo, hideAbout }) => {
+ const { version, memory, platform, runtime } = serverInfo
+ return (
+
+
+
+
+ )
+}
export default Header
diff --git a/browser/app/js/browser/InputGroup.js b/browser/app/js/browser/InputGroup.js
new file mode 100644
index 000000000..dd49482d9
--- /dev/null
+++ b/browser/app/js/browser/InputGroup.js
@@ -0,0 +1,70 @@
+/*
+ * Minio Cloud Storage (C) 2016, 2018 Minio, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+
+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/browser/Login.js b/browser/app/js/browser/Login.js
index 063e9cabb..f2c003fea 100644
--- a/browser/app/js/browser/Login.js
+++ b/browser/app/js/browser/Login.js
@@ -20,7 +20,7 @@ import classNames from "classnames"
import logo from "../../img/logo.svg"
import Alert from "../alert/Alert"
import * as actionsAlert from "../alert/actions"
-import InputGroup from "../components/InputGroup"
+import InputGroup from "./InputGroup"
import { minioBrowserPrefix } from "../constants"
import web from "../web"
import { Redirect } from "react-router-dom"
diff --git a/browser/app/js/browser/__tests__/AboutModal.test.js b/browser/app/js/browser/__tests__/AboutModal.test.js
new file mode 100644
index 000000000..dd5690987
--- /dev/null
+++ b/browser/app/js/browser/__tests__/AboutModal.test.js
@@ -0,0 +1,41 @@
+/*
+ * Minio Cloud Storage (C) 2018 Minio, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+import { shallow } from "enzyme"
+import { AboutModal } from "../AboutModal"
+
+describe("AboutModal", () => {
+ const serverInfo = {
+ version: "test",
+ memory: "test",
+ platform: "test",
+ runtime: "test"
+ }
+
+ it("should render without crashing", () => {
+ shallow()
+ })
+
+ it("should call hideAbout when close button is clicked", () => {
+ const hideAbout = jest.fn()
+ const wrapper = shallow(
+
+ )
+ wrapper.find("button").simulate("click")
+ expect(hideAbout).toHaveBeenCalled()
+ })
+})
diff --git a/browser/app/js/browser/__tests__/BrowserDropdown.test.js b/browser/app/js/browser/__tests__/BrowserDropdown.test.js
new file mode 100644
index 000000000..c388bab5f
--- /dev/null
+++ b/browser/app/js/browser/__tests__/BrowserDropdown.test.js
@@ -0,0 +1,63 @@
+/*
+ * Minio Cloud Storage (C) 2018 Minio, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+import { shallow } from "enzyme"
+import { BrowserDropdown } from "../BrowserDropdown"
+
+describe("BrowserDropdown", () => {
+ const serverInfo = {
+ version: "test",
+ memory: "test",
+ platform: "test",
+ runtime: "test"
+ }
+
+ it("should render without crashing", () => {
+ shallow(
+
+ )
+ })
+
+ it("should call fetchServerInfo after its mounted", () => {
+ const fetchServerInfo = jest.fn()
+ const wrapper = shallow(
+
+ )
+ expect(fetchServerInfo).toHaveBeenCalled()
+ })
+
+ it("should show AboutModal when About link is clicked", () => {
+ const wrapper = shallow(
+
+ )
+ wrapper.find("#show-about").simulate("click", { preventDefault: jest.fn() })
+ wrapper.update()
+ expect(wrapper.state("showAboutModal")).toBeTruthy()
+ expect(wrapper.find("AboutModal").length).toBe(1)
+ })
+
+ it("should logout and redirect to /login when logout is clicked", () => {
+ const wrapper = shallow(
+
+ )
+ wrapper.find("#logout").simulate("click", { preventDefault: jest.fn() })
+ expect(window.location.pathname.endsWith("/login")).toBeTruthy()
+ })
+})
diff --git a/browser/app/js/browser/__tests__/ChangePasswordModal.test.js b/browser/app/js/browser/__tests__/ChangePasswordModal.test.js
new file mode 100644
index 000000000..411cc938b
--- /dev/null
+++ b/browser/app/js/browser/__tests__/ChangePasswordModal.test.js
@@ -0,0 +1,109 @@
+/*
+ * Minio Cloud Storage (C) 2018 Minio, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react"
+import { shallow, mount } from "enzyme"
+import { ChangePasswordModal } from "../ChangePasswordModal"
+
+jest.mock("../../web", () => ({
+ GetAuth: jest.fn(() => {
+ return Promise.resolve({ accessKey: "test1", secretKey: "test2" })
+ }),
+ GenerateAuth: jest.fn(() => {
+ return Promise.resolve({ accessKey: "gen1", secretKey: "gen2" })
+ }),
+ SetAuth: jest.fn(({ accessKey, secretKey }) => {
+ if (accessKey == "test3" && secretKey == "test4") {
+ return Promise.resolve({})
+ } else {
+ return Promise.reject({ message: "Error" })
+ }
+ })
+}))
+
+describe("ChangePasswordModal", () => {
+ const serverInfo = {
+ version: "test",
+ memory: "test",
+ platform: "test",
+ runtime: "test",
+ info: { isEnvCreds: false }
+ }
+
+ it("should render without crashing", () => {
+ shallow()
+ })
+
+ it("should get the keys when its rendered", () => {
+ const wrapper = shallow()
+ setImmediate(() => {
+ expect(wrapper.state("accessKey")).toBe("test1")
+ expect(wrapper.state("secretKey")).toBe("test2")
+ })
+ })
+
+ it("should show readonly keys when isEnvCreds is true", () => {
+ const newServerInfo = { ...serverInfo, info: { isEnvCreds: true } }
+ const wrapper = shallow()
+ expect(wrapper.state("accessKey")).toBe("xxxxxxxxx")
+ expect(wrapper.state("secretKey")).toBe("xxxxxxxxx")
+ expect(wrapper.find("#accessKey").prop("readonly")).toBeTruthy()
+ expect(wrapper.find("#secretKey").prop("readonly")).toBeTruthy()
+ expect(wrapper.find("#generate-keys").hasClass("hidden")).toBeTruthy()
+ expect(wrapper.find("#update-keys").hasClass("hidden")).toBeTruthy()
+ })
+
+ it("should generate accessKey and secretKey when Generate buttons is clicked", () => {
+ const wrapper = shallow()
+ wrapper.find("#generate-keys").simulate("click")
+ setImmediate(() => {
+ expect(wrapper.state("accessKey")).toBe("gen1")
+ expect(wrapper.state("secretKey")).toBe("gen2")
+ })
+ })
+
+ it("should update accessKey and secretKey when Update button is clicked", () => {
+ const showAlert = jest.fn()
+ const wrapper = shallow(
+
+ )
+ wrapper
+ .find("#accessKey")
+ .simulate("change", { target: { value: "test3" } })
+ wrapper
+ .find("#secretKey")
+ .simulate("change", { target: { value: "test4" } })
+ wrapper.find("#update-keys").simulate("click")
+ setImmediate(() => {
+ expect(showAlert).toHaveBeenCalledWith({
+ type: "success",
+ message: "Changed credentials"
+ })
+ })
+ })
+
+ it("should call hideChangePassword when Cancel button is clicked", () => {
+ const hideChangePassword = jest.fn()
+ const wrapper = shallow(
+
+ )
+ wrapper.find("#cancel-change-password").simulate("click")
+ expect(hideChangePassword).toHaveBeenCalled()
+ })
+})
diff --git a/browser/app/js/browser/__tests__/Header.test.js b/browser/app/js/browser/__tests__/Header.test.js
index a48357361..7f9042e3a 100644
--- a/browser/app/js/browser/__tests__/Header.test.js
+++ b/browser/app/js/browser/__tests__/Header.test.js
@@ -18,8 +18,25 @@ import React from "react"
import { shallow } from "enzyme"
import Header from "../Header"
+jest.mock("../../web", () => ({
+ LoggedIn: jest
+ .fn(() => true)
+ .mockReturnValueOnce(true)
+ .mockReturnValueOnce(false)
+}))
describe("Header", () => {
it("should render without crashing", () => {
shallow()
})
+
+ it("should render Login button when the user has not LoggedIn", () => {
+ const wrapper = shallow()
+ expect(wrapper.find("a").text()).toBe("Login")
+ })
+
+ it("should render StorageInfo and BrowserDropdown when the user has LoggedIn", () => {
+ const wrapper = shallow()
+ expect(wrapper.find("Connect(BrowserDropdown)").length).toBe(1)
+ expect(wrapper.find("Connect(StorageInfo)").length).toBe(1)
+ })
})
diff --git a/browser/app/js/browser/__tests__/actions.test.js b/browser/app/js/browser/__tests__/actions.test.js
index d89ee5873..012d7031f 100644
--- a/browser/app/js/browser/__tests__/actions.test.js
+++ b/browser/app/js/browser/__tests__/actions.test.js
@@ -21,6 +21,15 @@ import * as actionsCommon from "../actions"
jest.mock("../../web", () => ({
StorageInfo: jest.fn(() => {
return Promise.resolve({ storageInfo: { Total: 100, Free: 60 } })
+ }),
+ ServerInfo: jest.fn(() => {
+ return Promise.resolve({
+ MinioVersion: "test",
+ MinioMemory: "test",
+ MinioPlatform: "test",
+ MinioRuntime: "test",
+ MinioGlobalInfo: "test"
+ })
})
}))
@@ -38,4 +47,24 @@ describe("Common actions", () => {
expect(actions).toEqual(expectedActions)
})
})
+
+ it("creates common/SET_SERVER_INFO after fetching the server details", () => {
+ const store = mockStore()
+ const expectedActions = [
+ {
+ type: "common/SET_SERVER_INFO",
+ serverInfo: {
+ version: "test",
+ memory: "test",
+ platform: "test",
+ runtime: "test",
+ info: "test"
+ }
+ }
+ ]
+ return store.dispatch(actionsCommon.fetchServerInfo()).then(() => {
+ const actions = store.getActions()
+ expect(actions).toEqual(expectedActions)
+ })
+ })
})
diff --git a/browser/app/js/browser/__tests__/reducer.test.js b/browser/app/js/browser/__tests__/reducer.test.js
index d7dee346f..968f60e8e 100644
--- a/browser/app/js/browser/__tests__/reducer.test.js
+++ b/browser/app/js/browser/__tests__/reducer.test.js
@@ -24,7 +24,8 @@ describe("common reducer", () => {
storageInfo: {
total: 0,
free: 0
- }
+ },
+ serverInfo: {}
})
})
@@ -67,4 +68,25 @@ describe("common reducer", () => {
storageInfo: { total: 100, free: 40 }
})
})
+
+ it("should handle SET_SERVER_INFO", () => {
+ expect(
+ reducer(undefined, {
+ type: actionsCommon.SET_SERVER_INFO,
+ serverInfo: {
+ version: "test",
+ memory: "test",
+ platform: "test",
+ runtime: "test",
+ info: "test"
+ }
+ }).serverInfo
+ ).toEqual({
+ version: "test",
+ memory: "test",
+ platform: "test",
+ runtime: "test",
+ info: "test"
+ })
+ })
})
diff --git a/browser/app/js/browser/actions.js b/browser/app/js/browser/actions.js
index 0164f7573..f82d03b4f 100644
--- a/browser/app/js/browser/actions.js
+++ b/browser/app/js/browser/actions.js
@@ -19,6 +19,7 @@ import web from "../web"
export const TOGGLE_SIDEBAR = "common/TOGGLE_SIDEBAR"
export const CLOSE_SIDEBAR = "common/CLOSE_SIDEBAR"
export const SET_STORAGE_INFO = "common/SET_STORAGE_INFO"
+export const SET_SERVER_INFO = "common/SET_SERVER_INFO"
export const toggleSidebar = () => ({
type: TOGGLE_SIDEBAR
@@ -44,3 +45,23 @@ export const setStorageInfo = storageInfo => ({
type: SET_STORAGE_INFO,
storageInfo
})
+
+export const fetchServerInfo = () => {
+ return function(dispatch) {
+ return web.ServerInfo().then(res => {
+ const serverInfo = {
+ version: res.MinioVersion,
+ memory: res.MinioMemory,
+ platform: res.MinioPlatform,
+ runtime: res.MinioRuntime,
+ info: res.MinioGlobalInfo
+ }
+ dispatch(setServerInfo(serverInfo))
+ })
+ }
+}
+
+export const setServerInfo = serverInfo => ({
+ type: SET_SERVER_INFO,
+ serverInfo
+})
diff --git a/browser/app/js/browser/reducer.js b/browser/app/js/browser/reducer.js
index 3ee036534..1e1789aef 100644
--- a/browser/app/js/browser/reducer.js
+++ b/browser/app/js/browser/reducer.js
@@ -17,7 +17,11 @@
import * as actionsCommon from "./actions"
export default (
- state = { sidebarOpen: false, storageInfo: { total: 0, free: 0 } },
+ state = {
+ sidebarOpen: false,
+ storageInfo: { total: 0, free: 0 },
+ serverInfo: {}
+ },
action
) => {
switch (action.type) {
@@ -33,6 +37,8 @@ export default (
return Object.assign({}, state, {
storageInfo: action.storageInfo
})
+ case actionsCommon.SET_SERVER_INFO:
+ return { ...state, serverInfo: action.serverInfo }
default:
return state
}
diff --git a/browser/app/js/components/BrowserDropdown.js b/browser/app/js/components/BrowserDropdown.js
deleted file mode 100644
index b242c3dd5..000000000
--- a/browser/app/js/components/BrowserDropdown.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Minio Cloud Storage (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 = ({fullScreenFunc, aboutFunc, settingsFunc, logoutFunc}) => {
- return (
-
- )
-}
-
-export default connect(state => state)(BrowserDropdown)
diff --git a/browser/app/js/components/InputGroup.js b/browser/app/js/components/InputGroup.js
deleted file mode 100644
index 9aee63f4c..000000000
--- a/browser/app/js/components/InputGroup.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Minio Cloud Storage (C) 2016 Minio, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react'
-
-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/SettingsModal.js b/browser/app/js/components/SettingsModal.js
deleted file mode 100644
index 189a2b9f8..000000000
--- a/browser/app/js/components/SettingsModal.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Minio Cloud Storage (C) 2016 Minio, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react'
-import 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.
- if (serverInfo.info.isEnvCreds) {
- dispatch(actions.setSettings({
- accessKey: 'xxxxxxxxx',
- secretKey: 'xxxxxxxxx',
- 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
-
-
-
-
-
-
-