Skip to content

Commit

Permalink
feat: add custom temporary responses (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
dadamssg committed Apr 2, 2019
1 parent 3985505 commit d7b404e
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 12 deletions.
3 changes: 2 additions & 1 deletion api-explorer/src/Accordion.js
Expand Up @@ -42,6 +42,7 @@ export default class Accordion extends PureComponent {
data-toggle='collapse'
data-target={`#${collapseId}`}
aria-controls={collapseId}
id={`${id}-link`}
>
{this.props.title}
</button>
Expand All @@ -63,4 +64,4 @@ export default class Accordion extends PureComponent {
</div>
)
}
}
}
4 changes: 2 additions & 2 deletions api-explorer/src/ActivateStaticResponse.js
@@ -1,6 +1,6 @@
import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import axios from 'axios/index'
import axios from 'axios'
import config from './config'

export default class ActivateStaticResponse extends PureComponent {
Expand Down Expand Up @@ -61,4 +61,4 @@ export default class ActivateStaticResponse extends PureComponent {
</a>
)
}
}
}
133 changes: 133 additions & 0 deletions api-explorer/src/CustomResponseForm.js
@@ -0,0 +1,133 @@
import React from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import config from './config'

const textAreaStyle = {
fontSize: 12,
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace'
}

class CustomResponseForm extends React.Component {
static propTypes = {
route: PropTypes.object.isRequired,
onSuccess: PropTypes.func.isRequired,
response: PropTypes.object
}
state = {
loading: false,
title: '',
status: '200',
response: ''
}
constructor (props) {
super(props)
const {response = {}} = props
this.state = {
loading: false,
title: response.title || '',
status: `${response.status || '200'}`,
response: response.response ? JSON.stringify(response.response, null, 2) : ''
}
}
onSubmit = (e) => {
e.preventDefault()
const errors = []
if (this.state.status.length !== 3) {
errors.push('Invalid status.')
}
try {
JSON.parse(this.state.response)
} catch (e) {
errors.push('Invalid json.')
}
if (errors.length > 0) {
window.alert(errors.join('\n'))
} else {
this.save()
}
}
save = () => {
if (this.state.loading) return
this.setState({loading: true})
const {route} = this.props
const payload = {
response: {
...this.props.response,
title: this.state.title,
status: Number(this.state.status),
response: JSON.parse(this.state.response)
}
}
axios.put(`${config.api}/_route/${route.id}/responses`, payload).then(res => {
const state = this.props.response
? {
loading: false
}
: {
loading: false,
title: '',
status: 200,
response: ''
}
this.setState(state)
this.props.onSuccess()
if (this.props.response) {
window.alert('Saved')
}
})
.catch(() => {
this.setState({loading: false})
})
}
render () {
return (
<form onSubmit={this.onSubmit}>
<div className='form-group'>
<label>Title</label>
<input
required
className='form-control'
value={this.state.title}
onChange={(e) => {
this.setState({title: e.target.value})
}}
/>
</div>
<div className='form-group'>
<label>Status</label>
<input
required
className='form-control'
value={this.state.status}
maxLength={3}
onChange={(e) => {
const status = String(e.target.value).substring(0, 3).replace(/\D/g, '')
this.setState({status})
}}
/>
</div>
<div className='form-group'>
<label>Response</label>
<textarea
required
className='form-control'
rows={10}
value={this.state.response}
style={textAreaStyle}
onChange={(e) => this.setState({response: e.target.value})}
/>
</div>
<button
type='submit'
className='btn btn-sm btn-primary'
disabled={this.state.loading}
>
{this.state.loading ? 'Submitting' : 'Submit'}
</button>
</form>
)
}
}

export default CustomResponseForm
38 changes: 38 additions & 0 deletions api-explorer/src/DeleteCustomResponse.js
@@ -0,0 +1,38 @@
import React from 'react'
import PropTypes from 'prop-types'
import axios from 'axios'
import config from './config'

class DeleteCustomResponse extends React.Component {
static propTypes = {
route: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
onSuccess: PropTypes.func.isRequired
}
state = {
loading: false
}
onClick = () => {
if (this.state.loading) return
const {route, id} = this.props
this.setState({loading: true})
axios.delete(`${config.api}/_route/${route.id}/responses/${id}`).then(() => {
this.setState({loading: false})
this.props.onSuccess()
})
.catch(() => {
this.setState({loading: false})
})
}
render () {
return (
<a onClick={this.onClick} style={{cursor: 'pointer'}}>
<span className='ml-1 badge badge-danger'>
{this.state.loading ? '...' : <span>&times;</span>}
</span>
</a>
)
}
}

export default DeleteCustomResponse
2 changes: 2 additions & 0 deletions api-explorer/src/Route.js
Expand Up @@ -7,6 +7,8 @@ import RequestForm from './RequestForm'
import PathReferences from './PathReferences'
import Markdown from './Markdown'
import StaticResponses from './StaticResponses'
import CustomResponseForm from './CustomResponseForm'
import Accordion from './Accordion'

class Route extends Component {
static propTypes = {
Expand Down
39 changes: 34 additions & 5 deletions api-explorer/src/StaticResponses.js
Expand Up @@ -7,6 +7,8 @@ import SyntaxHighlighter from './SyntaxHighlighter'
import {StatusBadge} from './Response'
import ActivateStaticResponse from './ActivateStaticResponse'
import Markdown from './Markdown'
import CustomResponseForm from './CustomResponseForm'
import DeleteCustomResponse from './DeleteCustomResponse'

export default class StaticResponses extends PureComponent {
static propTypes = {
Expand Down Expand Up @@ -61,6 +63,13 @@ export default class StaticResponses extends PureComponent {
respId={i}
onActivate={respId => this.setState({respId})}
/>
{response.id && (
<DeleteCustomResponse
route={route}
id={response.id}
onSuccess={this.fetchData}
/>
)}
</span>
)}
>
Expand All @@ -69,13 +78,33 @@ export default class StaticResponses extends PureComponent {
{response.description}
</Markdown>
)}
<div className={response.description ? 'border p-4' : undefined}>
<SyntaxHighlighter language='json'>
{JSON.stringify(response.response, null, 4)}
</SyntaxHighlighter>
</div>
{response.id ? (
<CustomResponseForm
route={route}
response={response}
onSuccess={() => {
window.$('#custom-link').click()
this.fetchData()
}}
/>
) : (
<div className={response.description ? 'border p-4' : undefined}>
<SyntaxHighlighter language='json'>
{JSON.stringify(response.response, null, 4)}
</SyntaxHighlighter>
</div>
)}
</Accordion>
))}
<Accordion id='custom' title='Add Custom'>
<CustomResponseForm
route={route}
onSuccess={() => {
window.$('#custom-link').click()
this.fetchData()
}}
/>
</Accordion>
</div>
</div>
)
Expand Down
65 changes: 61 additions & 4 deletions src/api-route-provider.js
Expand Up @@ -7,6 +7,14 @@ const {execSync} = childProcess

export default function (app, options = {}) {
const responseStore = createActivationStore()
const customResponseStore = createCustomResponseStore()

function getAllRouteResponses (routeId) {
const fileRoute = getFileRoutes(options.routes).find(r => r.id === routeId)
const fileResponses = getRouteResponses(options, fileRoute)
const customResponses = customResponseStore.getResponses(routeId)
return fileResponses.concat(customResponses)
}

// create routes from files
getFileRoutes(options.routes).forEach(route => {
Expand All @@ -15,7 +23,7 @@ export default function (app, options = {}) {
// add methods to route
route.methods.forEach(method => {
appRoute[method]((req, res) => {
const responses = getRouteResponses(options, route)
const responses = getAllRouteResponses(route.id)
const respId = responseStore.getActivatedResponseId(route.id)
if (respId !== undefined) {
let resp = responses[respId]
Expand Down Expand Up @@ -209,18 +217,18 @@ export default function (app, options = {}) {
app.get('/_route/:routeId/responses', (req, res) => {
const routeId = req.params.routeId
const fileRoute = getFileRoutes(options.routes).find(r => r.id === routeId) || {}
const responses = getRouteResponses(options, fileRoute)
const responses = getAllRouteResponses(routeId)
const responseId = responseStore.getActivatedResponseId(fileRoute.id)
return res.json({
respId: responseId || fileRoute.response ? null : 0,
respId: responseId || (fileRoute.response ? null : 0),
responses
})
})

app.get('/_route/:routeId/responses/:respId/activate', (req, res) => {
const {routeId, respId} = req.params
const fileRoute = getFileRoutes(options.routes).find(r => r.id === routeId)
const responses = getRouteResponses(options, fileRoute)
const responses = getAllRouteResponses(routeId)
if (fileRoute && responses[respId]) {
responseStore.setActiveResponse(routeId, respId)
}
Expand All @@ -240,6 +248,27 @@ export default function (app, options = {}) {
})
})

app.put('/_route/:routeId/responses', (req, res) => {
const routeId = req.params.routeId
const {id} = customResponseStore.save(routeId, req.body.response)
const responses = getAllRouteResponses(routeId)
const index = responses.findIndex((r) => r.id === id)
responseStore.setActiveResponse(routeId, `${index > -1 ? index : ''}`)
return res.json({message: 'Saved'})
})

app.delete('/_route/:routeId/responses/:id', (req, res) => {
const routeId = req.params.routeId
const responses = getAllRouteResponses(routeId)
const activeIndex = responseStore.getActivatedResponseId(routeId)
const deleteIndex = responses.findIndex((r) => r.id === req.params.id)
if (Number(activeIndex) === Number(deleteIndex)) {
responseStore.setActiveResponse(routeId, null)
}
customResponseStore.delete(routeId, req.params.id)
return res.json({message: 'Deleted'})
})

// serve the api explorer
app.use('/_docs', express.static(path.resolve(__dirname, '..', 'api-explorer-dist')))

Expand Down Expand Up @@ -286,6 +315,34 @@ function createActivationStore () {
}
}

function createCustomResponseStore () {
const store = {} //
return {
getResponses (routeId) {
return Array.isArray(store[routeId]) ? store[routeId] : []
},
save (routeId, response) {
response = {
...response,
id: response.id || Buffer.from(String((new Date().getTime()))).toString('base64')
}
const routeResponses = this.getResponses(routeId)
const index = routeResponses.findIndex((r) => r.id === response.id)
const newResponses = [...routeResponses]
if (index > -1) {
newResponses[index] = response
} else {
newResponses.push(response)
}
store[routeId] = newResponses
return response
},
delete (routeId, id) {
store[routeId] = this.getResponses(routeId).filter((r) => r.id !== id)
}
}
}

function getRouteResponses (options, route) {
const routeResponses = Array.isArray(route.responses) ? route.responses : []
const globalResponses = Array.isArray(options.responses) ? options.responses : []
Expand Down

0 comments on commit d7b404e

Please sign in to comment.