allow non-loggedin users to access public bucket (#5570)

* conditionally render main action buttons

- Make bucket action will be available only for loggedIn users
- File upload button will be avaialble for loggedIn users
  and non-loggedIn users if the prefix is writable

* select the bucket and prefix from the url

When the url contains bucket and prefix, it will be selected
by default instead of the first bucket from the list.

* show BucketSearch only for LoggedIn users

* allow non-LoggedIn users to access public bucket

* removed unused Router imports

* fix test case failures in BucketList.test.js

* remove dupicate minioBrowserPrefix from url

since history is already initialized with minioBrowserPrefix,
no need to use it in push or replace

* remove unused match from App component

* remove unused minioBrowserPrefix imports
This commit is contained in:
Kanagaraj M 2018-02-24 08:59:30 +05:30 committed by Harshavardhana
parent bb0adea494
commit 416841869a
16 changed files with 239 additions and 68 deletions

View File

@ -21,10 +21,10 @@ import "material-design-iconic-font/dist/css/material-design-iconic-font.min.css
import React from "react"
import ReactDOM from "react-dom"
import { BrowserRouter, Route } from "react-router-dom"
import { Router, Route } from "react-router-dom"
import { Provider } from "react-redux"
import { minioBrowserPrefix } from "./js/constants"
import history from "./js/history"
import configureStore from "./js/store/configure-store"
import hideLoader from "./js/loader"
import App from "./js/App"
@ -33,9 +33,9 @@ const store = configureStore()
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Route path={minioBrowserPrefix} component={App} />
</BrowserRouter>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById("root")
)

View File

@ -34,13 +34,15 @@ const AuthorizedRoute = ({ component: Component, ...rest }) => (
/>
)
export const App = ({ match }) => (
<Switch>
<AuthorizedRoute exact path={match.url} component={Browser} />
<Route path={`${match.url}/login`} component={Login} />
<AuthorizedRoute path={`${match.url}/:bucket/*`} component={Browser} />
<AuthorizedRoute path={`${match.url}/:bucket`} component={Browser} />
</Switch>
)
export const App = () => {
return (
<Switch>
<AuthorizedRoute exact path={"/"} component={Browser} />
<Route path={"/login"} component={Login} />
<Route path={"/:bucket/*"} component={Browser} />
<Route path={"/:bucket"} component={Browser} />
</Switch>
)
}
export default App

View File

@ -21,7 +21,6 @@ import logo from "../../img/logo.svg"
import Alert from "../alert/Alert"
import * as actionsAlert from "../alert/actions"
import InputGroup from "./InputGroup"
import { minioBrowserPrefix } from "../constants"
import web from "../web"
import { Redirect } from "react-router-dom"
@ -46,7 +45,7 @@ export class Login extends React.Component {
password: document.getElementById("secretKey").value
})
.then(res => {
history.push(minioBrowserPrefix)
history.push("/")
})
.catch(e => {
showAlert("danger", e.message)
@ -67,7 +66,7 @@ export class Login extends React.Component {
render() {
const { clearAlert, alert } = this.props
if (web.LoggedIn()) {
return <Redirect to={minioBrowserPrefix} />
return <Redirect to={"/"} />
}
let alertBox = <Alert {...alert} onDismiss={clearAlert} />
// Make sure you don't show a fading out alert box on the initial web-page load.

View File

@ -17,10 +17,16 @@
import React from "react"
import { connect } from "react-redux"
import { Dropdown, OverlayTrigger, Tooltip } from "react-bootstrap"
import web from "../web"
import * as actionsBuckets from "../buckets/actions"
import * as uploadsActions from "../uploads/actions"
import { getPrefixWritable } from "../objects/selectors"
export const MainActions = ({ uploadFile, showMakeBucketModal }) => {
export const MainActions = ({
prefixWritable,
uploadFile,
showMakeBucketModal
}) => {
const uploadTooltip = <Tooltip id="tt-upload-file">Upload file</Tooltip>
const makeBucketTooltip = (
<Tooltip id="tt-create-bucket">Create bucket</Tooltip>
@ -31,47 +37,59 @@ export const MainActions = ({ uploadFile, showMakeBucketModal }) => {
e.target.value = null
}
return (
<Dropdown dropup className="feb-actions" id="fe-action-toggle">
<Dropdown.Toggle noCaret className="feba-toggle">
<span>
<i className="fa fa-plus" />
</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<OverlayTrigger placement="left" overlay={uploadTooltip}>
<a href="#" className="feba-btn feba-upload">
<input
type="file"
onChange={onFileUpload}
style={{ display: "none" }}
id="file-input"
/>
<label htmlFor="file-input">
{" "}
<i className="fa fa-cloud-upload" />{" "}
</label>
</a>
</OverlayTrigger>
<OverlayTrigger placement="left" overlay={makeBucketTooltip}>
<a
href="#"
id="show-make-bucket"
className="feba-btn feba-bucket"
onClick={e => {
e.preventDefault()
showMakeBucketModal()
}}
>
<i className="fa fa-hdd-o" />
</a>
</OverlayTrigger>
</Dropdown.Menu>
</Dropdown>
)
const loggedIn = web.LoggedIn()
if (loggedIn || prefixWritable) {
return (
<Dropdown dropup className="feb-actions" id="fe-action-toggle">
<Dropdown.Toggle noCaret className="feba-toggle">
<span>
<i className="fa fa-plus" />
</span>
</Dropdown.Toggle>
<Dropdown.Menu>
<OverlayTrigger placement="left" overlay={uploadTooltip}>
<a href="#" className="feba-btn feba-upload">
<input
type="file"
onChange={onFileUpload}
style={{ display: "none" }}
id="file-input"
/>
<label htmlFor="file-input">
{" "}
<i className="fa fa-cloud-upload" />{" "}
</label>
</a>
</OverlayTrigger>
{loggedIn && (
<OverlayTrigger placement="left" overlay={makeBucketTooltip}>
<a
href="#"
id="show-make-bucket"
className="feba-btn feba-bucket"
onClick={e => {
e.preventDefault()
showMakeBucketModal()
}}
>
<i className="fa fa-hdd-o" />
</a>
</OverlayTrigger>
)}
</Dropdown.Menu>
</Dropdown>
)
} else {
return <noscript />
}
}
const mapStateToProps = state => state
const mapStateToProps = state => {
return {
prefixWritable: getPrefixWritable(state)
}
}
const mapDispatchToProps = dispatch => {
return {

View File

@ -25,6 +25,7 @@ import BucketSearch from "../buckets/BucketSearch"
import BucketList from "../buckets/BucketList"
import Host from "./Host"
import * as actionsCommon from "./actions"
import web from "../web"
export const SideBar = ({ sidebarOpen, clickOutside }) => {
return (
@ -40,7 +41,7 @@ export const SideBar = ({ sidebarOpen, clickOutside }) => {
<h2>Minio Browser</h2>
</div>
<div className="fes-list">
<BucketSearch />
{web.LoggedIn() && <BucketSearch />}
<BucketList />
</div>
<Host />

View File

@ -18,11 +18,37 @@ import React from "react"
import { shallow, mount } from "enzyme"
import { MainActions } from "../MainActions"
jest.mock("../../web", () => ({
LoggedIn: jest
.fn(() => true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(false)
}))
describe("MainActions", () => {
it("should render without crashing", () => {
shallow(<MainActions />)
})
it("should not show any actions when user has not LoggedIn and prefixWritable is false", () => {
const wrapper = shallow(<MainActions />)
expect(wrapper.find("#show-make-bucket").length).toBe(0)
expect(wrapper.find("#file-input").length).toBe(0)
})
it("should show only file upload action when user has not LoggedIn and prefixWritable is true", () => {
const wrapper = shallow(<MainActions prefixWritable={true} />)
expect(wrapper.find("#show-make-bucket").length).toBe(0)
expect(wrapper.find("#file-input").length).toBe(1)
})
it("should show make bucket upload file actions when user has LoggedIn", () => {
const wrapper = shallow(<MainActions />)
expect(wrapper.find("#show-make-bucket").length).toBe(1)
expect(wrapper.find("#file-input").length).toBe(1)
})
it("should call showMakeBucketModal when create bucket icon is clicked", () => {
const showMakeBucketModal = jest.fn()
const wrapper = shallow(

View File

@ -18,11 +18,20 @@ import React from "react"
import { shallow } from "enzyme"
import { SideBar } from "../SideBar"
jest.mock("../../web", () => ({
LoggedIn: jest.fn(() => false).mockReturnValueOnce(true)
}))
describe("SideBar", () => {
it("should render without crashing", () => {
shallow(<SideBar />)
})
it("should not render BucketSearch for non LoggedIn users", () => {
const wrapper = shallow(<SideBar />)
expect(wrapper.find("Connect(BucketSearch)").length).toBe(0)
})
it("should call clickOutside when the user clicks outside the sidebar", () => {
const clickOutside = jest.fn()
const wrapper = shallow(<SideBar clickOutside={clickOutside} />)

View File

@ -20,11 +20,22 @@ import { Scrollbars } from "react-custom-scrollbars"
import * as actionsBuckets from "./actions"
import { getVisibleBuckets } from "./selectors"
import BucketContainer from "./BucketContainer"
import web from "../web"
import history from "../history"
import { pathSlice } from "../utils"
export class BucketList extends React.Component {
componentWillMount() {
const { fetchBuckets } = this.props
fetchBuckets()
const { fetchBuckets, setBucketList, selectBucket } = this.props
if (web.LoggedIn()) {
fetchBuckets()
} else {
const { bucket, prefix } = pathSlice(history.location.pathname)
if (bucket) {
setBucketList([bucket])
selectBucket(bucket, prefix)
}
}
}
render() {
const { visibleBuckets } = this.props
@ -52,7 +63,9 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => {
return {
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets())
fetchBuckets: () => dispatch(actionsBuckets.fetchBuckets()),
setBucketList: buckets => dispatch(actionsBuckets.setList(buckets)),
selectBucket: bucket => dispatch(actionsBuckets.selectBucket(bucket))
}
}

View File

@ -16,8 +16,16 @@
import React from "react"
import { shallow } from "enzyme"
import history from "../../history"
import { BucketList } from "../BucketList"
jest.mock("../../web", () => ({
LoggedIn: jest
.fn(() => false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(true)
}))
describe("BucketList", () => {
it("should render without crashing", () => {
const fetchBuckets = jest.fn()
@ -31,4 +39,19 @@ describe("BucketList", () => {
)
expect(fetchBuckets).toHaveBeenCalled()
})
it("should call setBucketList and selectBucket before component is mounted when the user has not loggedIn", () => {
const setBucketList = jest.fn()
const selectBucket = jest.fn()
history.push("/bk1/pre1")
const wrapper = shallow(
<BucketList
visibleBuckets={[]}
setBucketList={setBucketList}
selectBucket={selectBucket}
/>
)
expect(setBucketList).toHaveBeenCalledWith(["bk1"])
expect(selectBucket).toHaveBeenCalledWith("bk1", "pre1")
})
})

View File

@ -18,6 +18,7 @@ import configureStore from "redux-mock-store"
import thunk from "redux-thunk"
import * as actionsBuckets from "../actions"
import * as objectActions from "../../objects/actions"
import history from "../../history"
jest.mock("../../web", () => ({
ListBuckets: jest.fn(() => {
@ -36,7 +37,7 @@ const middlewares = [thunk]
const mockStore = configureStore(middlewares)
describe("Buckets actions", () => {
it("creates buckets/SET_LIST and buckets/SET_CURRENT_BUCKET after fetching the buckets", () => {
it("creates buckets/SET_LIST and buckets/SET_CURRENT_BUCKET with first bucket after fetching the buckets", () => {
const store = mockStore()
const expectedActions = [
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] },
@ -48,7 +49,35 @@ describe("Buckets actions", () => {
})
})
it("should update browser url and creates buckets/SET_CURRENT_BUCKET action when selectBucket is called", () => {
it("creates buckets/SET_CURRENT_BUCKET with bucket name in the url after fetching buckets", () => {
history.push("/test2")
const store = mockStore()
const expectedActions = [
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] },
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test2" }
]
window.location
return store.dispatch(actionsBuckets.fetchBuckets()).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
it("creates buckets/SET_CURRENT_BUCKET with first bucket when the bucket in url is not exists after fetching buckets", () => {
history.push("/test3")
const store = mockStore()
const expectedActions = [
{ type: "buckets/SET_LIST", buckets: ["test1", "test2"] },
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" }
]
window.location
return store.dispatch(actionsBuckets.fetchBuckets()).then(() => {
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
})
it("creates buckets/SET_CURRENT_BUCKET action when selectBucket is called", () => {
const store = mockStore()
const expectedActions = [
{ type: "buckets/SET_CURRENT_BUCKET", bucket: "test1" }
@ -56,7 +85,6 @@ describe("Buckets actions", () => {
store.dispatch(actionsBuckets.selectBucket("test1"))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
expect(window.location.pathname).toBe("/test1")
})
it("creates buckets/SHOW_MAKE_BUCKET_MODAL for showMakeBucketModal", () => {

View File

@ -18,6 +18,7 @@ import web from "../web"
import history from "../history"
import * as alertActions from "../alert/actions"
import * as objectsActions from "../objects/actions"
import { pathSlice } from "../utils"
export const SET_LIST = "buckets/SET_LIST"
export const ADD = "buckets/ADD"
@ -31,7 +32,12 @@ export const fetchBuckets = () => {
const buckets = res.buckets ? res.buckets.map(bucket => bucket.name) : []
dispatch(setList(buckets))
if (buckets.length > 0) {
dispatch(selectBucket(buckets[0]))
const { bucket, prefix } = pathSlice(history.location.pathname)
if (bucket && buckets.indexOf(bucket) > -1) {
dispatch(selectBucket(bucket, prefix))
} else {
dispatch(selectBucket(buckets[0]))
}
}
})
}
@ -51,11 +57,10 @@ export const setFilter = filter => {
}
}
export const selectBucket = bucket => {
export const selectBucket = (bucket, prefix) => {
return function(dispatch) {
dispatch(setCurrentBucket(bucket))
dispatch(objectsActions.selectPrefix(""))
history.push(`/${bucket}`)
dispatch(objectsActions.selectPrefix(prefix || ""))
}
}

View File

@ -26,7 +26,8 @@ jest.mock("../../web", () => ({
return Promise.resolve({
objects: [{ name: "test1" }, { name: "test2" }],
istruncated: false,
nextmarker: "test2"
nextmarker: "test2",
writable: false
})
}),
RemoveObject: jest.fn(({ bucketName, objects }) => {
@ -124,6 +125,10 @@ describe("Objects actions", () => {
{
type: "objects/SET_SORT_ORDER",
sortOrder: false
},
{
type: "objects/SET_PREFIX_WRITABLE",
prefixWritable: false
}
]
return store.dispatch(actionsObjects.fetchObjects()).then(() => {
@ -143,6 +148,10 @@ describe("Objects actions", () => {
objects: [{ name: "test1" }, { name: "test2" }],
marker: "test2",
isTruncated: false
},
{
type: "objects/SET_PREFIX_WRITABLE",
prefixWritable: false
}
]
return store.dispatch(actionsObjects.fetchObjects(true)).then(() => {
@ -197,6 +206,16 @@ describe("Objects actions", () => {
expect(window.location.pathname.endsWith("/test/abc/")).toBeTruthy()
})
it("create objects/SET_PREFIX_WRITABLE action", () => {
const store = mockStore()
const expectedActions = [
{ type: "objects/SET_PREFIX_WRITABLE", prefixWritable: true }
]
store.dispatch(actionsObjects.setPrefixWritable(true))
const actions = store.getActions()
expect(actions).toEqual(expectedActions)
})
it("creates objects/REMOVE action", () => {
const store = mockStore()
const expectedActions = [{ type: "objects/REMOVE", object: "obj1" }]

View File

@ -27,6 +27,7 @@ describe("objects reducer", () => {
currentPrefix: "",
marker: "",
isTruncated: false,
prefixWritable: false,
shareObject: {
show: false,
object: "",
@ -123,6 +124,14 @@ describe("objects reducer", () => {
expect(newState.isTruncated).toBeFalsy()
})
it("should handle SET_PREFIX_WRITABLE", () => {
const newState = reducer(undefined, {
type: actions.SET_PREFIX_WRITABLE,
prefixWritable: true
})
expect(newState.prefixWritable).toBeTruthy()
})
it("should handle SET_SHARE_OBJECT", () => {
const newState = reducer(undefined, {
type: actions.SET_SHARE_OBJECT,

View File

@ -32,6 +32,7 @@ export const REMOVE = "objects/REMOVE"
export const SET_SORT_BY = "objects/SET_SORT_BY"
export const SET_SORT_ORDER = "objects/SET_SORT_ORDER"
export const SET_CURRENT_PREFIX = "objects/SET_CURRENT_PREFIX"
export const SET_PREFIX_WRITABLE = "objects/SET_PREFIX_WRITABLE"
export const SET_SHARE_OBJECT = "objects/SET_SHARE_OBJECT"
export const CHECKED_LIST_ADD = "objects/CHECKED_LIST_ADD"
export const CHECKED_LIST_REMOVE = "objects/CHECKED_LIST_REMOVE"
@ -80,6 +81,11 @@ export const fetchObjects = append => {
dispatch(setSortBy(""))
dispatch(setSortOrder(false))
}
dispatch(setPrefixWritable(res.writable))
})
.catch(err => {
dispatch(alertActions.set({ type: "danger", message: err.message }))
history.push("/login")
})
}
}
@ -136,6 +142,11 @@ export const setCurrentPrefix = prefix => {
}
}
export const setPrefixWritable = prefixWritable => ({
type: SET_PREFIX_WRITABLE,
prefixWritable
})
export const deleteObject = object => {
return function(dispatch, getState) {
const currentBucket = getCurrentBucket(getState())

View File

@ -32,6 +32,7 @@ export default (
currentPrefix: "",
marker: "",
isTruncated: false,
prefixWritable: false,
shareObject: {
show: false,
object: "",
@ -78,6 +79,11 @@ export default (
marker: "",
isTruncated: false
}
case actionsObjects.SET_PREFIX_WRITABLE:
return {
...state,
prefixWritable: action.prefixWritable
}
case actionsObjects.SET_SHARE_OBJECT:
return {
...state,

View File

@ -19,3 +19,5 @@ import { createSelector } from "reselect"
export const getCurrentPrefix = state => state.objects.currentPrefix
export const getCheckedList = state => state.objects.checkedList
export const getPrefixWritable = state => state.objects.prefixWritable