diff --git a/api/get-fetch.js b/api/get-fetch.js
new file mode 100644
index 0000000..00d09ad
--- /dev/null
+++ b/api/get-fetch.js
@@ -0,0 +1,45 @@
+/* global btoa */
+const isomorphicFetch = require('isomorphic-fetch')
+
+function getFetch ({url, username, password, fetch = isomorphicFetch}) {
+ if (!url) {
+ throw new Error(`getFetch URL required`)
+ }
+
+ async function fetcher (endpoint, params) {
+ const config = {
+ credentials: 'include',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ ...params
+ }
+ if (username && password) {
+ config.headers['Authorization'] = `Basic ${btoa(`${username}:${password}`)}`
+ delete config.credentials
+ }
+
+ const urlWithSlash = url.endsWith('/')
+ ? url
+ : `${url}/`
+
+ const response = await fetch(`${urlWithSlash}${endpoint}`, config)
+ let body
+ try {
+ body = await response.json()
+ } catch (e) {
+ console.warn('error caught in fetch parsing body')
+ }
+ if (!response.ok) {
+ const error = new Error()
+ Object.assign(error, {status: response.status, statusText: response.statusText}, body)
+ throw error
+ }
+ return body
+ }
+
+ return fetcher
+}
+
+module.exports = {getFetch}
diff --git a/api/index.js b/api/index.js
new file mode 100644
index 0000000..3071c07
--- /dev/null
+++ b/api/index.js
@@ -0,0 +1,3 @@
+const {RemoteCouchApi} = require('./remote-couch-api')
+
+module.exports = {RemoteCouchApi}
diff --git a/api/pouchdb.js b/api/pouchdb.js
new file mode 100644
index 0000000..a45ed6d
--- /dev/null
+++ b/api/pouchdb.js
@@ -0,0 +1,25 @@
+// see note in webpack config about this goofy movie.
+const PouchDB = pickModule(require('pouchdb-core'))
+const idbAdapter = pickModule(require('pouchdb-adapter-idb'))
+const memoryAdapter = pickModule(require('pouchdb-adapter-memory'))
+const httpAdapter = pickModule(require('pouchdb-adapter-http'))
+const replication = pickModule(require('pouchdb-replication'))
+const find = pickModule(require('pouchdb-find'))
+const mapReduce = pickModule(require('pouchdb-mapreduce'))
+
+PouchDB
+ .plugin(idbAdapter)
+ .plugin(httpAdapter)
+ .plugin(replication)
+ .plugin(mapReduce)
+ .plugin(memoryAdapter)
+ .plugin(find)
+
+// pouch exports modules in a way that you cannot simply "require" these in
+// node as well as in webpack, which is why we need this workaround:
+// https://github.com/pouchdb-community/pouchdb-authentication/issues/164#issuecomment-357697828
+function pickModule (mod) {
+ return 'default' in mod ? mod.default : mod
+}
+
+module.exports = {PouchDB}
diff --git a/api/remote-couch-api.js b/api/remote-couch-api.js
new file mode 100644
index 0000000..eb50ed6
--- /dev/null
+++ b/api/remote-couch-api.js
@@ -0,0 +1,115 @@
+const {getFetch} = require('./get-fetch')
+const {PouchDB} = require('./pouchdb')
+
+class RemoteCouchApi {
+ constructor (url) {
+ url = url.endsWith('/') ? url : `${url}/`
+
+ this.url = url
+ this.fetcher = getFetch({url})
+ // this will probably change to support node
+ // will have to deal with setDatabase & username/passwords
+ this.PouchDBConstructor = PouchDB.defaults({prefix: url})
+ this.GenericPouchDB = PouchDB
+ this.databases = {}
+ }
+
+ async logout () {
+ await this.fetcher('_session', {method: 'DELETE'})
+ this.user = null
+ }
+
+ async getCurrentUser () {
+ const session = await this.getCurrentSession()
+ if (!session) return null
+
+ return this.getUserFromSession(session)
+ }
+
+ async getCurrentSession () {
+ const {userCtx} = await this.fetcher('_session')
+ // this is couch telling us there's no session
+ if (!userCtx.name) return null
+ return userCtx
+ }
+
+ async getUserFromSession (session) {
+ // couchdb's way of saying admin user, usually does not have a user doc
+ if (session.roles && session.roles.includes('_admin')) {
+ delete session.ok
+ this.user = session
+ return session
+ }
+
+ const user = await this.fetcher(`_users/org.couchdb.user:${session.name}`)
+ this.user = user
+ return user
+ }
+
+ async login (username = '', password = '') {
+ const session = await this.fetcher(
+ '_session', {method: 'POST', body: JSON.stringify({username, password})}
+ )
+ return this.getUserFromSession(session)
+ }
+
+ getConfigUrl () {
+ const isLocal = this.url.includes('://localhost:') || this.url.includes('://127.0.0.1:')
+ return isLocal
+ ? `_node/couchdb@localhost/_config`
+ : `_node/nonode@nohost/_config`
+ }
+
+ async getConfig () {
+ return this.fetcher(`${this.getConfigUrl()}`)
+ }
+
+ async getAdminConfig () {
+ return this.fetcher(`${this.getConfigUrl()}/admins`)
+ }
+
+ async updateAdmin (username, password) {
+ const url = `${this.getConfigUrl()}/admins/${username}`
+ return this.fetcher(url, {method: 'PUT', body: `"${password}"`})
+ }
+
+ getPouchInstance (databaseName) {
+ if (!this.databases[databaseName]) {
+ this.databases[databaseName] = new this.PouchDBConstructor(databaseName)
+ }
+
+ return this.databases[databaseName]
+ }
+
+ async listDatabases () {
+ return this.fetcher('_all_dbs')
+ }
+
+ async listInfos (keys) {
+ try {
+ const response = await this.fetcher('_dbs_info', {method: 'POST', body: JSON.stringify({keys})})
+ return response
+ } catch (error) {
+ if (error.status !== 404) throw error
+
+ const promises = keys.map(dbName => this.getDatabase(dbName))
+ const response = await Promise.all(promises)
+ // make same shape as _dbs_info
+ return response.map((info, index) => ({key: keys[index], info}))
+ }
+ }
+
+ async createDatabase (databaseName) {
+ return this.fetcher(databaseName, {method: 'PUT'})
+ }
+
+ async getDatabase (databaseName) {
+ return this.fetcher(databaseName)
+ }
+
+ async destroyDatabase (databaseName) {
+ return this.fetcher(databaseName, {method: 'DELETE'})
+ }
+}
+
+module.exports = {RemoteCouchApi}
diff --git a/app/app.js b/app/app.js
index 526ca9b..c92d0a5 100644
--- a/app/app.js
+++ b/app/app.js
@@ -10,76 +10,195 @@ import ConfigContainer from './containers/ConfigContainer'
import AdminContainer from './containers/AdminContainer'
import EditDocContainer from './containers/EditDocContainer'
import QueryContainer from './containers/QueryContainer'
-import Nav from './components/Nav'
+import Footer from './components/Footer'
import Login from './components/Login'
import Loading from './components/Loading'
-import withParams from './containers/withParams'
-import { parseUrl } from './utils/utils'
-import fetcher from 'utils/fetcher'
+import { parseUrl, getParams } from './utils/utils'
+import {RemoteCouchApi} from '../api/'
import 'app-classes.css'
import 'app-tags.css'
-const Databases = withParams(DatabasesContainer)
-const LIMIT = 100
+// 1. App = if no couch in the URL, return SetupCouchContainer
+// 2. CouchRoutes = routes for "we have couch URL but not a specific database."
+// 3. OnDatabaseRoutes = we have a couch url and a specific datbase
+
+class OnDatabaseRoutes extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ loading: true
+ }
+ }
+
+ async componentDidMount () {
+ this.setupDatabase(this.props.match.params.dbName)
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.match.params.dbName !== this.props.match.params.dbName) {
+ this.setupDatabase(this.props.match.params.dbName)
+ }
+ }
+
+ setupDatabase = async (dbName) => {
+ this.pouchDB = this.props.api.getPouchInstance(dbName)
+ window.pouchDB = this.pouchDB
+ console.log(`${dbName} available in console as window.pouchDB`)
+ this.setState({loading: false})
+ }
+
+ render () {
+ const { dbName } = this.props.match.params
+ const {couchUrl} = this.props
+
+ const commonProps = {
+ dbUrl: `${couchUrl}${dbName}/`,
+ dbName,
+ pouchDB: this.pouchDB,
+ ...this.props
+ }
+
+ if (!commonProps.pouchDB) return null
+
+ return (
+
+
+
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+
+
+
+ )
+ }
+}
class CouchRoutes extends Component {
constructor (props) {
super(props)
this.state = {
- userCtx: null,
- loading: true,
- couchUrl: parseUrl(props.match.params.couch),
- dbs: null
+ user: null,
+ loading: true
}
}
async componentDidMount () {
- const { couchUrl } = this.state
- const { userCtx } = await fetcher.checkSession(couchUrl)
- const authenticated = userCtx.name || userCtx.roles.includes('_admin')
+ this.setupCouch(this.props.match.params.couch)
+ }
- if (!authenticated) {
- this.setState({loading: false})
- return
+ componentDidUpdate (prevProps) {
+ if (prevProps.match.params.couch !== this.props.match.params.couch) {
+ this.setupCouch(this.props.match.params.couch)
}
+ }
- this.onUser(userCtx)
+ login = (username, password) => {
+ return this.api.login(username, password).then(user => {
+ this.setState({user})
+ })
}
- // TODO: this cache object was never used much
- onUser = async (userCtx) => {
- const { couchUrl } = this.state
- const dbs = await fetcher.get(`${couchUrl}_all_dbs`, { limit: LIMIT })
- this.setState({ loading: false, dbs, userCtx })
+ setupCouch = async (couch) => {
+ const couchUrl = parseUrl(couch)
+ this.api = new RemoteCouchApi(couchUrl)
+ window.api = this.api
+ console.log(`
+ remote couch api on ${couchUrl} available in console as window.api,
+ api.PouchDBConstructor is pouch constructor with couchUrl prefix
+ api.GenericPouchDB is Pouch constructor without prefix
+ `)
+
+ const user = await this.api.getCurrentUser()
+ this.setState({loading: false, user})
}
render () {
- const { userCtx, loading, couchUrl, dbs } = this.state
+ const { couch } = this.props.match.params
+ const couchUrl = parseUrl(couch)
+ const searchParams = getParams(this.props.location.search)
+
+ const { user, loading } = this.state
if (loading) return
- if (!userCtx) return
+ if (!user) {
+ return (
+
+ )
+ }
+
+ const commonProps = {
+ couch,
+ couchUrl,
+ searchParams,
+ user,
+ api: this.api
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
- } />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
+ ()}
+ />
-
()} />
+ ()}
+ />
)
}
diff --git a/app/containers/DeleteDatabaseContainer.js b/app/components/DeleteDatabaseModal.js
similarity index 56%
rename from app/containers/DeleteDatabaseContainer.js
rename to app/components/DeleteDatabaseModal.js
index 3389585..b65cc5d 100644
--- a/app/containers/DeleteDatabaseContainer.js
+++ b/app/components/DeleteDatabaseModal.js
@@ -1,46 +1,29 @@
import React from 'react'
import Modal from 'components/Modal'
-import fetcher from 'utils/fetcher'
-export default class DeleteDatabaseContainer extends React.Component {
- state = { dbNameConfirmed: false, loading: false, inputText: '', error: null }
+export default class DeleteDatabaseModal extends React.Component {
+ state = { dbNameConfirmed: false, inputText: '' }
onInputChange = (e) => {
const { value } = e.target
const { dbName } = this.props
- this.setState({ dbNameConfirmed: (value === dbName), inputText: value, error: null })
- }
-
- maybeEscape = (e) => {
- if (e.keyCode === 27) {
- this.setState({ inputText: '' })
- this.props.onClose()
- }
+ this.setState({ dbNameConfirmed: (value === dbName), inputText: value })
}
onSubmit = async (e) => {
e.preventDefault()
- const { inputText, dbNameConfirmed } = this.state
- const { couchUrl, history, couch } = this.props
- const id = inputText
- if (dbNameConfirmed) {
- this.setState({ loading: true })
- try {
- await fetcher.destroy(couchUrl + id)
- history.push(`/${couch}/`)
- } catch (error) {
- this.setState({ error, loading: false })
- }
+ if (this.state.dbNameConfirmed) {
+ this.props.onConfirm()
}
}
render () {
const { show, onClose, couchUrl, dbName } = this.props
- const { dbNameConfirmed, loading, error, inputText } = this.state
+ const { dbNameConfirmed, inputText } = this.state
return (
diff --git a/app/components/Login.js b/app/components/Login.js
index 6b9c818..042eacb 100644
--- a/app/components/Login.js
+++ b/app/components/Login.js
@@ -1,5 +1,4 @@
import React from 'react'
-import fetcher from 'utils/fetcher'
import {Link} from 'react-router-dom'
export default class LoginContainer extends React.Component {
@@ -7,11 +6,9 @@ export default class LoginContainer extends React.Component {
tryLogin = async event => {
event.preventDefault()
- const {couchUrl} = this.props
this.setState({ loading: true })
try {
- const response = await fetcher.login(couchUrl, this.refs.username.value, this.refs.password.value)
- this.props.onUser(response)
+ await this.props.login(this.refs.username.value, this.refs.password.value)
} catch (error) {
this.setState({ error, loading: false })
}
diff --git a/app/containers/NewDatabaseContainer.js b/app/components/NewDatabaseModal.js
similarity index 67%
rename from app/containers/NewDatabaseContainer.js
rename to app/components/NewDatabaseModal.js
index 2d4af8d..c317b30 100644
--- a/app/containers/NewDatabaseContainer.js
+++ b/app/components/NewDatabaseModal.js
@@ -1,13 +1,12 @@
import React from 'react'
import Modal from 'components/Modal'
-import fetcher from 'utils/fetcher'
-export default class NewDatabaseContainer extends React.Component {
- state = { validDBName: false, loading: false, inputText: '', error: null }
+export default class NewDatabaseModal extends React.Component {
+ state = { validDBName: false, inputText: '' }
onInputChange = (e) => {
const { value } = e.target
- this.setState({ validDBName: isValidDBName(value), inputText: value, error: null })
+ this.setState({ validDBName: isValidDBName(value), inputText: value })
}
maybeEscape = (e) => {
@@ -20,23 +19,14 @@ export default class NewDatabaseContainer extends React.Component {
onSubmit = async (e) => {
e.preventDefault()
const { inputText, validDBName } = this.state
- const { couchUrl, history } = this.props
- const id = inputText
if (validDBName) {
- this.setState({ loading: true })
- const data = { id, name: inputText }
- try {
- await fetcher.put(couchUrl + id, data)
- history.push(id)
- } catch (error) {
- this.setState({ error, loading: false })
- }
+ this.props.onCreateDatabase(inputText)
}
}
render () {
const { show, onClose } = this.props
- const { validDBName, loading, error, inputText } = this.state
+ const { validDBName, inputText } = this.state
return (
- {error && ({error}
)}