Merge branch 'master' into bug/3072-error-response-fixes

This commit is contained in:
shockey
2017-06-29 17:20:09 -07:00
committed by GitHub
19 changed files with 411 additions and 77 deletions

View File

@@ -126,9 +126,13 @@ If you'd like to use the bundle files via npm, check out the [`swagger-ui-dist`
#### Parameters #### Parameters
Parameters with dots in their names are single strings used to organize subordinate parameters, and are not indicative of a nested structure.
Parameter Name | Description Parameter Name | Description
--- | --- --- | ---
url | The url pointing to API definition (normally `swagger.json` or `swagger.yaml`). url | The url pointing to API definition (normally `swagger.json` or `swagger.yaml`). Will be ignored if `urls` or `spec` is used.
urls | An array of API definition objects (`{url: "<url>", name: "<name>"}`) used by Topbar plugin. When used and Topbar plugin is enabled, the `url` parameter will not be parsed. Names and URLs must be unique among all items in this array, since they're used as identifiers.
urls.primaryName | When using `urls`, you can use this subparameter. If the value matches the name of a spec provided in `urls`, that spec will be displayed when Swagger-UI loads, instead of defaulting to the first spec in `urls`.
spec | A JSON object describing the OpenAPI Specification. When used, the `url` parameter will not be parsed. This is useful for testing manually-generated specifications without hosting them. spec | A JSON object describing the OpenAPI Specification. When used, the `url` parameter will not be parsed. This is useful for testing manually-generated specifications without hosting them.
validatorUrl | By default, Swagger-UI attempts to validate specs against swagger.io's online validator. You can use this parameter to set a different validator URL, for example for locally deployed validators ([Validator Badge](https://github.com/swagger-api/validator-badge)). Setting it to `null` will disable validation. validatorUrl | By default, Swagger-UI attempts to validate specs against swagger.io's online validator. You can use this parameter to set a different validator URL, for example for locally deployed validators ([Validator Badge](https://github.com/swagger-api/validator-badge)). Setting it to `null` will disable validation.
dom_id | The id of a dom element inside which SwaggerUi will put the user interface for swagger. dom_id | The id of a dom element inside which SwaggerUi will put the user interface for swagger.
@@ -139,11 +143,12 @@ parameterMacro | MUST be a function. Function to set default value to parameters
modelPropertyMacro | MUST be a function. Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable modelPropertyMacro | MUST be a function. Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable
docExpansion | Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). The default is 'list'. docExpansion | Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). The default is 'list'.
displayOperationId | Controls the display of operationId in operations list. The default is `false`. displayOperationId | Controls the display of operationId in operations list. The default is `false`.
displayRequestDuration | Controls the display of the request duration (in milliseconds) for `Try it out` requests. The default is `false`.
### Plugins ### Plugins
#### Topbar plugin #### Topbar plugin
Topbar plugin enables top bar with input for spec path and explore button. By default the plugin is enabled, and to disable it you need to remove Topbar plugin from presets in `src/standalone/index.js`: Topbar plugin enables top bar with input for spec path and explore button or a dropdown if `urls` is used. By default the plugin is enabled, and to disable it you need to remove Topbar plugin from presets in `src/standalone/index.js`:
``` ```
let preset = [ let preset = [

1
dist/index.html vendored
View File

@@ -71,6 +71,7 @@
<script src="./swagger-ui-standalone-preset.js"> </script> <script src="./swagger-ui-standalone-preset.js"> </script>
<script> <script>
window.onload = function() { window.onload = function() {
// Build a system // Build a system
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: "http://petstore.swagger.io/v2/swagger.json", url: "http://petstore.swagger.io/v2/swagger.json",

View File

@@ -8,19 +8,32 @@ const Headers = ( { headers } )=>{
<pre>{headers}</pre> <pre>{headers}</pre>
</div>) </div>)
} }
Headers.propTypes = { Headers.propTypes = {
headers: PropTypes.array.isRequired headers: PropTypes.array.isRequired
} }
const Duration = ( { duration } ) => {
return (
<div>
<h5>Request duration</h5>
<pre>{duration} ms</pre>
</div>
)
}
Duration.propTypes = {
duration: PropTypes.number.isRequired
}
export default class LiveResponse extends React.Component { export default class LiveResponse extends React.Component {
static propTypes = { static propTypes = {
response: PropTypes.object.isRequired, response: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired getComponent: PropTypes.func.isRequired,
displayRequestDuration: PropTypes.bool.isRequired
} }
render() { render() {
const { request, response, getComponent } = this.props const { request, response, getComponent, displayRequestDuration } = this.props
const status = response.get("status") const status = response.get("status")
const url = response.get("url") const url = response.get("url")
@@ -28,6 +41,7 @@ export default class LiveResponse extends React.Component {
const notDocumented = response.get("notDocumented") const notDocumented = response.get("notDocumented")
const isError = response.get("error") const isError = response.get("error")
const body = response.get("text") const body = response.get("text")
const duration = response.get("duration")
const headersKeys = Object.keys(headers) const headersKeys = Object.keys(headers)
const contentType = headers["content-type"] const contentType = headers["content-type"]
@@ -78,6 +92,9 @@ export default class LiveResponse extends React.Component {
{ {
hasHeaders ? <Headers headers={ returnObject }/> : null hasHeaders ? <Headers headers={ returnObject }/> : null
} }
{
displayRequestDuration && duration ? <Duration duration={ duration } /> : null
}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -117,12 +117,13 @@ class ObjectModel extends Component {
class Primitive extends Component { class Primitive extends Component {
static propTypes = { static propTypes = {
schema: PropTypes.object.isRequired, schema: PropTypes.object.isRequired,
name: PropTypes.string,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
required: PropTypes.bool required: PropTypes.bool
} }
render(){ render(){
let { schema, getComponent, required } = this.props let { schema, getComponent, name, required } = this.props
if(!schema || !schema.get) { if(!schema || !schema.get) {
// don't render if schema isn't correctly formed // don't render if schema isn't correctly formed
@@ -133,12 +134,18 @@ class Primitive extends Component {
let format = schema.get("format") let format = schema.get("format")
let xml = schema.get("xml") let xml = schema.get("xml")
let enumArray = schema.get("enum") let enumArray = schema.get("enum")
let title = schema.get("title") || name
let description = schema.get("description") let description = schema.get("description")
let properties = schema.filter( ( v, key) => ["enum", "type", "format", "description", "$$ref"].indexOf(key) === -1 ) let properties = schema.filter( ( v, key) => ["enum", "type", "format", "description", "$$ref"].indexOf(key) === -1 )
let style = required ? { fontWeight: "bold" } : {} let style = required ? { fontWeight: "bold" } : {}
const Markdown = getComponent("Markdown") const Markdown = getComponent("Markdown")
return <span className="prop"> return <span className="model">
{
title && <span className="model-title" style={{ marginRight: "2em" }}>
<span className="model-title__text">{ title }</span>
</span>
}
<span className="prop-type" style={ style }>{ type }</span> { required && <span style={{ color: "red" }}>*</span>} <span className="prop-type" style={ style }>{ type }</span> { required && <span style={{ color: "red" }}>*</span>}
{ format && <span className="prop-format">(${format})</span>} { format && <span className="prop-format">(${format})</span>}
{ {
@@ -176,17 +183,20 @@ class ArrayModel extends Component {
} }
render(){ render(){
let { required, schema, depth, expandDepth } = this.props let { required, schema, depth, name, expandDepth } = this.props
let items = schema.get("items") let items = schema.get("items")
let title = schema.get("title") || name
let properties = schema.filter( ( v, key) => ["type", "items", "$$ref"].indexOf(key) === -1 ) let properties = schema.filter( ( v, key) => ["type", "items", "$$ref"].indexOf(key) === -1 )
return <span className="model"> return <span className="model">
<span className="model-title"> {
<span className="model-title__text">{ schema.get("title") }</span> title && <span className="model-title">
<span className="model-title__text">{ title }</span>
</span> </span>
}
<Collapse collapsed={ depth > expandDepth } collapsedContent="[...]"> <Collapse collapsed={ depth > expandDepth } collapsedContent="[...]">
[ [
<span><Model { ...this.props } schema={ items } required={ false }/></span> <span><Model { ...this.props } name="" schema={ items } required={ false }/></span>
] ]
{ {
properties.size ? <span> properties.size ? <span>
@@ -249,13 +259,13 @@ class Model extends Component {
name={ name || modelName } name={ name || modelName }
isRef={ isRef!== undefined ? isRef : !!$$ref }/> isRef={ isRef!== undefined ? isRef : !!$$ref }/>
case "array": case "array":
return <ArrayModel className="array" { ...this.props } schema={ modelSchema } required={ required } /> return <ArrayModel className="array" { ...this.props } schema={ modelSchema } name={ name || modelName } required={ required } />
case "string": case "string":
case "number": case "number":
case "integer": case "integer":
case "boolean": case "boolean":
default: default:
return <Primitive getComponent={ getComponent } schema={ modelSchema } required={ required }/> return <Primitive { ...this.props } getComponent={ getComponent } schema={ modelSchema } name={ name || modelName } required={ required }/>
} }
} }
} }

View File

@@ -24,8 +24,8 @@ export default class Models extends Component {
return <section className={ showModels ? "models is-open" : "models"}> return <section className={ showModels ? "models is-open" : "models"}>
<h4 onClick={() => layoutActions.show("models", !showModels)}> <h4 onClick={() => layoutActions.show("models", !showModels)}>
<span>Models</span> <span>Models</span>
<svg width="20" height="20"> <svg className="arrow" width="20" height="20">
<use xlinkHref="#large-arrow" /> <use xlinkHref={showModels ? "#large-arrow-down" : "#large-arrow"} />
</svg> </svg>
</h4> </h4>
<Collapse isOpened={showModels} animated> <Collapse isOpened={showModels} animated>

View File

@@ -17,6 +17,7 @@ export default class Operation extends PureComponent {
allowTryItOut: PropTypes.bool, allowTryItOut: PropTypes.bool,
displayOperationId: PropTypes.bool, displayOperationId: PropTypes.bool,
displayRequestDuration: PropTypes.bool,
response: PropTypes.object, response: PropTypes.object,
request: PropTypes.object, request: PropTypes.object,
@@ -37,6 +38,7 @@ export default class Operation extends PureComponent {
response: null, response: null,
allowTryItOut: true, allowTryItOut: true,
displayOperationId: false, displayOperationId: false,
displayRequestDuration: false
} }
constructor(props, context) { constructor(props, context) {
@@ -107,7 +109,7 @@ export default class Operation extends PureComponent {
request, request,
allowTryItOut, allowTryItOut,
displayOperationId, displayOperationId,
displayRequestDuration,
fn, fn,
getComponent, getComponent,
specActions, specActions,
@@ -126,6 +128,7 @@ export default class Operation extends PureComponent {
let schemes = operation.get("schemes") let schemes = operation.get("schemes")
let parameters = getList(operation, ["parameters"]) let parameters = getList(operation, ["parameters"])
let operationId = operation.get("__originalOperationId") let operationId = operation.get("__originalOperationId")
let operationScheme = specSelectors.operationScheme(path, method)
const Responses = getComponent("responses") const Responses = getComponent("responses")
const Parameters = getComponent( "parameters" ) const Parameters = getComponent( "parameters" )
@@ -211,7 +214,8 @@ export default class Operation extends PureComponent {
<Schemes schemes={ schemes } <Schemes schemes={ schemes }
path={ path } path={ path }
method={ method } method={ method }
specActions={ specActions }/> specActions={ specActions }
operationScheme={ operationScheme } />
</div> : null </div> : null
} }
@@ -250,6 +254,7 @@ export default class Operation extends PureComponent {
produces={ produces } produces={ produces }
producesValue={ operation.get("produces_value") } producesValue={ operation.get("produces_value") }
pathMethod={ [path, method] } pathMethod={ [path, method] }
displayRequestDuration={ displayRequestDuration }
fn={fn} /> fn={fn} />
} }
</div> </div>

View File

@@ -32,7 +32,7 @@ export default class Operations extends React.Component {
const Collapse = getComponent("Collapse") const Collapse = getComponent("Collapse")
let showSummary = layoutSelectors.showSummary() let showSummary = layoutSelectors.showSummary()
let { docExpansion, displayOperationId } = getConfigs() let { docExpansion, displayOperationId, displayRequestDuration } = getConfigs()
return ( return (
<div> <div>
@@ -87,6 +87,7 @@ export default class Operations extends React.Component {
allowTryItOut={allowTryItOut} allowTryItOut={allowTryItOut}
displayOperationId={displayOperationId} displayOperationId={displayOperationId}
displayRequestDuration={displayRequestDuration}
specActions={ specActions } specActions={ specActions }
specSelectors={ specSelectors } specSelectors={ specSelectors }

View File

@@ -5,14 +5,16 @@ import { getSampleSchema } from "core/utils"
const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => { const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => {
if ( examples && examples.size ) { if ( examples && examples.size ) {
return examples.entrySeq().map( ([ key, example ]) => { return examples.entrySeq().map( ([ key, example ]) => {
let exampleValue let exampleValue = example
if ( example.toJS ) {
try { try {
exampleValue = example && example.toJS ? example.toJS() : example exampleValue = JSON.stringify(example.toJS(), null, 2)
exampleValue = JSON.stringify(exampleValue, null, 2)
} }
catch(e) { catch(e) {
exampleValue = String(example) exampleValue = String(example)
} }
}
return (<div key={ key }> return (<div key={ key }>
<h5>{ key }</h5> <h5>{ key }</h5>
<HighlightCode className="example" value={ exampleValue } /> <HighlightCode className="example" value={ exampleValue } />

View File

@@ -14,19 +14,21 @@ export default class Responses extends React.Component {
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
pathMethod: PropTypes.array.isRequired, pathMethod: PropTypes.array.isRequired,
displayRequestDuration: PropTypes.bool.isRequired,
fn: PropTypes.object.isRequired fn: PropTypes.object.isRequired
} }
static defaultProps = { static defaultProps = {
request: null, request: null,
tryItOutResponse: null, tryItOutResponse: null,
produces: fromJS(["application/json"]) produces: fromJS(["application/json"]),
displayRequestDuration: false
} }
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val) onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val)
render() { render() {
let { responses, request, tryItOutResponse, getComponent, specSelectors, fn, producesValue } = this.props let { responses, request, tryItOutResponse, getComponent, specSelectors, fn, producesValue, displayRequestDuration } = this.props
let defaultCode = defaultStatusCode( responses ) let defaultCode = defaultStatusCode( responses )
const ContentType = getComponent( "contentType" ) const ContentType = getComponent( "contentType" )
@@ -53,7 +55,8 @@ export default class Responses extends React.Component {
: <div> : <div>
<LiveResponse request={ request } <LiveResponse request={ request }
response={ tryItOutResponse } response={ tryItOutResponse }
getComponent={ getComponent } /> getComponent={ getComponent }
displayRequestDuration={ displayRequestDuration } />
<h4>Responses</h4> <h4>Responses</h4>
</div> </div>

View File

@@ -6,7 +6,8 @@ export default class Schemes extends React.Component {
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
schemes: PropTypes.object.isRequired, schemes: PropTypes.object.isRequired,
path: PropTypes.string, path: PropTypes.string,
method: PropTypes.string method: PropTypes.string,
operationScheme: PropTypes.string
} }
componentWillMount() { componentWillMount() {
@@ -16,11 +17,18 @@ export default class Schemes extends React.Component {
this.setScheme(schemes.first()) this.setScheme(schemes.first())
} }
componentWillReceiveProps(nextProps) {
if ( this.props.operationScheme && !nextProps.schemes.has(this.props.operationScheme) ) {
//fire 'change' event if our selected scheme is no longer an option
this.setScheme(nextProps.schemes.first())
}
}
onChange =( e ) => { onChange =( e ) => {
this.setScheme( e.target.value ) this.setScheme( e.target.value )
} }
setScheme =( value ) => { setScheme = ( value ) => {
let { path, method, specActions } = this.props let { path, method, specActions } = this.props
specActions.setScheme( value, path, method ) specActions.setScheme( value, path, method )

View File

@@ -6,9 +6,9 @@ import ApisPreset from "core/presets/apis"
import * as AllPlugins from "core/plugins/all" import * as AllPlugins from "core/plugins/all"
import { parseSeach, filterConfigs } from "core/utils" import { parseSeach, filterConfigs } from "core/utils"
const CONFIGS = [ "url", "spec", "validatorUrl", "onComplete", "onFailure", "authorizations", "docExpansion", const CONFIGS = [ "url", "urls", "urls.primaryName", "spec", "validatorUrl", "onComplete", "onFailure", "authorizations", "docExpansion",
"apisSorter", "operationsSorter", "supportedSubmitMethods", "dom_id", "defaultModelRendering", "oauth2RedirectUrl", "apisSorter", "operationsSorter", "supportedSubmitMethods", "dom_id", "defaultModelRendering", "oauth2RedirectUrl",
"showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" ] "showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" , "displayRequestDuration"]
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo
@@ -23,12 +23,14 @@ module.exports = function SwaggerUI(opts) {
dom_id: null, dom_id: null,
spec: {}, spec: {},
url: "", url: "",
urls: null,
layout: "BaseLayout", layout: "BaseLayout",
docExpansion: "list", docExpansion: "list",
validatorUrl: "https://online.swagger.io/validator", validatorUrl: "https://online.swagger.io/validator",
configs: {}, configs: {},
custom: {}, custom: {},
displayOperationId: false, displayOperationId: false,
displayRequestDuration: false,
// Initial set of plugins ( TODO rename this, or refactor - we don't need presets _and_ plugins. Its just there for performance. // Initial set of plugins ( TODO rename this, or refactor - we don't need presets _and_ plugins. Its just there for performance.
// Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest. // Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest.

View File

@@ -9,7 +9,7 @@ const primitives = {
"number": () => 0, "number": () => 0,
"number_float": () => 0.0, "number_float": () => 0.0,
"integer": () => 0, "integer": () => 0,
"boolean": () => true "boolean": (schema) => typeof schema.default === "boolean" ? schema.default : true
} }
const primitive = (schema) => { const primitive = (schema) => {

View File

@@ -207,8 +207,14 @@ export const executeRequest = (req) => ({fn, specActions, specSelectors}) => {
specActions.setRequest(req.pathName, req.method, parsedRequest) specActions.setRequest(req.pathName, req.method, parsedRequest)
// track duration of request
const startTime = Date.now()
return fn.execute(req) return fn.execute(req)
.then( res => specActions.setResponse(req.pathName, req.method, res)) .then( res => {
res.duration = Date.now() - startTime
specActions.setResponse(req.pathName, req.method, res)
} )
.catch( err => specActions.setResponse(req.pathName, req.method, { error: true, err: serializeError(err) } ) ) .catch( err => specActions.setResponse(req.pathName, req.method, { error: true, err: serializeError(err) } ) )
} }

View File

@@ -450,15 +450,15 @@ export const propChecker = (props, nextProps, objectList=[], ignoreList=[]) => {
|| objectList.some( objectPropName => !eq(props[objectPropName], nextProps[objectPropName]))) || objectList.some( objectPropName => !eq(props[objectPropName], nextProps[objectPropName])))
} }
const validateNumber = ( val ) => { export const validateNumber = ( val ) => {
if ( !/^-?\d+(.?\d+)?$/.test(val)) { if ( !/^-?\d+(\.?\d+)?$/.test(val)) {
return "Value must be a number" return "Value must be a number"
} }
} }
const validateInteger = ( val ) => { export const validateInteger = ( val ) => {
if ( !/^-?\d+$/.test(val)) { if ( !/^-?\d+$/.test(val)) {
return "Value must be integer" return "Value must be an integer"
} }
} }
@@ -469,13 +469,14 @@ export const validateParam = (param, isXml) => {
let required = param.get("required") let required = param.get("required")
let type = param.get("type") let type = param.get("type")
if ( required && (!value || (type==="array" && Array.isArray(value) && !value.length ))) { let stringCheck = type === "string" && !value
let arrayCheck = type === "array" && Array.isArray(value) && !value.length
let listCheck = type === "array" && Im.List.isList(value) && !value.count()
if ( required && (stringCheck || arrayCheck || listCheck) ) {
errors.push("Required field is not provided") errors.push("Required field is not provided")
return errors return errors
} }
if ( !value ) return errors
if ( type === "number" ) { if ( type === "number" ) {
let err = validateNumber(value) let err = validateNumber(value)
if (!err) return errors if (!err) return errors

View File

@@ -7,7 +7,7 @@ export default class Topbar extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { url: props.specSelectors.url() } this.state = { url: props.specSelectors.url(), selectedIndex: 0 }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@@ -19,14 +19,68 @@ export default class Topbar extends React.Component {
this.setState({url: value}) this.setState({url: value})
} }
downloadUrl = (e) => { loadSpec = (url) => {
this.props.specActions.updateUrl(this.state.url) this.props.specActions.updateUrl(url)
this.props.specActions.download(this.state.url) this.props.specActions.download(url)
}
onUrlSelect =(e)=> {
let url = e.target.value || e.target.href
this.loadSpec(url)
this.setSelectedUrl(url)
e.preventDefault() e.preventDefault()
} }
downloadUrl = (e) => {
this.loadSpec(this.state.url)
e.preventDefault()
}
setSelectedUrl = (selectedUrl) => {
const configs = this.props.getConfigs()
const urls = configs.urls || []
if(urls && urls.length) {
if(selectedUrl)
{
urls.forEach((spec, i) => {
if(spec.url === selectedUrl)
{
this.setState({selectedIndex: i})
}
})
}
}
}
componentWillMount() {
const configs = this.props.getConfigs()
const urls = configs.urls || []
if(urls && urls.length) {
let primaryName = configs["urls.primaryName"]
if(primaryName)
{
urls.forEach((spec, i) => {
if(spec.name === primaryName)
{
this.setState({selectedIndex: i})
}
})
}
}
}
componentDidMount() {
const urls = this.props.getConfigs().urls || []
if(urls && urls.length) {
this.loadSpec(urls[this.state.selectedIndex].url)
}
}
render() { render() {
let { getComponent, specSelectors } = this.props let { getComponent, specSelectors, getConfigs } = this.props
const Button = getComponent("Button") const Button = getComponent("Button")
const Link = getComponent("Link") const Link = getComponent("Link")
@@ -36,6 +90,31 @@ export default class Topbar extends React.Component {
let inputStyle = {} let inputStyle = {}
if(isFailed) inputStyle.color = "red" if(isFailed) inputStyle.color = "red"
if(isLoading) inputStyle.color = "#aaa" if(isLoading) inputStyle.color = "#aaa"
const { urls } = getConfigs()
let control = []
let formOnSubmit = null
if(urls) {
let rows = []
urls.forEach((link, i) => {
rows.push(<option key={i} value={link.url}>{link.name}</option>)
})
control.push(
<label className="select-label" htmlFor="select"><span>Select a spec</span>
<select id="select" disabled={isLoading} onChange={ this.onUrlSelect } value={urls[this.state.selectedIndex].url}>
{rows}
</select>
</label>
)
}
else {
formOnSubmit = this.downloadUrl
control.push(<input className="download-url-input" type="text" onChange={ this.onUrlChange } value={this.state.url} disabled={isLoading} style={inputStyle} />)
control.push(<Button className="download-url-button" onClick={ this.downloadUrl }>Explore</Button>)
}
return ( return (
<div className="topbar"> <div className="topbar">
<div className="wrapper"> <div className="wrapper">
@@ -44,14 +123,12 @@ export default class Topbar extends React.Component {
<img height="30" width="30" src={ Logo } alt="Swagger UX"/> <img height="30" width="30" src={ Logo } alt="Swagger UX"/>
<span>swagger</span> <span>swagger</span>
</Link> </Link>
<form className="download-url-wrapper" onSubmit={this.downloadUrl}> <form className="download-url-wrapper" onSubmit={formOnSubmit}>
<input className="download-url-input" type="text" onChange={ this.onUrlChange } value={this.state.url} disabled={isLoading} style={inputStyle} /> {control}
<Button className="download-url-button" onClick={ this.downloadUrl }>Explore</Button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
) )
} }
} }
@@ -59,5 +136,6 @@ export default class Topbar extends React.Component {
Topbar.propTypes = { Topbar.propTypes = {
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.isRequired
} }

View File

@@ -1,11 +1,10 @@
.swagger-ui { .swagger-ui {
.topbar { .topbar {
background-color: #89bf04; background-color: #89bf04;
} }
.topbar-wrapper { .topbar-wrapper {
padding: 0.7em padding: 0.7em;
} }
.topbar-logo__img { .topbar-logo__img {

View File

@@ -95,14 +95,7 @@ section.models
h4 h4
{ {
margin: 0 0 5px 0; margin: 0 0 5px 0;
border-bottom: 1px solid rgba(#3b4151, .3); border-bottom: 1px solid rgba(#3b4151, .3);
svg
{
transform: rotate(90deg);
}
} }
} }
h4 h4

View File

@@ -6,7 +6,6 @@
.topbar-wrapper .topbar-wrapper
{ {
display: flex; display: flex;
align-items: center; align-items: center;
} }
a a
@@ -15,13 +14,13 @@
font-weight: bold; font-weight: bold;
display: flex; display: flex;
align-items: center;
flex: 1;
max-width: 300px; max-width: 300px;
text-decoration: none; text-decoration: none;
flex: 1;
align-items: center;
@include text_headline(#fff); @include text_headline(#fff);
span span
@@ -34,8 +33,8 @@
.download-url-wrapper .download-url-wrapper
{ {
display: flex; display: flex;
flex: 3; flex: 3;
justify-content: flex-end;
input[type=text] input[type=text]
{ {
@@ -48,6 +47,38 @@
outline: none; outline: none;
} }
.select-label
{
display: flex;
align-items: center;
width: 100%;
max-width: 600px;
margin: 0;
span
{
font-size: 16px;
flex: 1;
padding: 0 10px 0 0;
text-align: right;
}
select
{
flex: 2;
width: 100%;
border: 2px solid #547f00;
outline: none;
box-shadow: none;
}
}
.download-url-button .download-url-button
{ {
font-size: 16px; font-size: 16px;

View File

@@ -1,7 +1,7 @@
/* eslint-env mocha */ /* eslint-env mocha */
import expect from "expect" import expect from "expect"
import { fromJS } from "immutable" import { fromJS } from "immutable"
import { mapToList } from "core/utils" import { mapToList, validateNumber, validateInteger, validateParam } from "core/utils"
describe("utils", function(){ describe("utils", function(){
@@ -67,9 +67,181 @@ describe("utils", function(){
// Then // Then
expect(aList.toJS()).toEqual([]) expect(aList.toJS()).toEqual([])
}) })
}) })
describe("validateNumber", function() {
let errorMessage = "Value must be a number"
it("doesn't return for whole numbers", function() {
expect(validateNumber(0)).toBeFalsy()
expect(validateNumber(1)).toBeFalsy()
expect(validateNumber(20)).toBeFalsy()
expect(validateNumber(5000000)).toBeFalsy()
expect(validateNumber("1")).toBeFalsy()
expect(validateNumber("2")).toBeFalsy()
expect(validateNumber(-1)).toBeFalsy()
expect(validateNumber(-20)).toBeFalsy()
expect(validateNumber(-5000000)).toBeFalsy()
})
it("doesn't return for negative numbers", function() {
expect(validateNumber(-1)).toBeFalsy()
expect(validateNumber(-20)).toBeFalsy()
expect(validateNumber(-5000000)).toBeFalsy()
})
it("doesn't return for decimal numbers", function() {
expect(validateNumber(1.1)).toBeFalsy()
expect(validateNumber(2.5)).toBeFalsy()
expect(validateNumber(-30.99)).toBeFalsy()
})
it("returns a message for strings", function() {
expect(validateNumber("")).toEqual(errorMessage)
expect(validateNumber(" ")).toEqual(errorMessage)
expect(validateNumber("test")).toEqual(errorMessage)
})
it("returns a message for invalid input", function() {
expect(validateNumber(undefined)).toEqual(errorMessage)
expect(validateNumber(null)).toEqual(errorMessage)
expect(validateNumber({})).toEqual(errorMessage)
expect(validateNumber([])).toEqual(errorMessage)
expect(validateNumber(true)).toEqual(errorMessage)
expect(validateNumber(false)).toEqual(errorMessage)
})
})
describe("validateInteger", function() {
let errorMessage = "Value must be an integer"
it("doesn't return for positive integers", function() {
expect(validateInteger(0)).toBeFalsy()
expect(validateInteger(1)).toBeFalsy()
expect(validateInteger(20)).toBeFalsy()
expect(validateInteger(5000000)).toBeFalsy()
expect(validateInteger("1")).toBeFalsy()
expect(validateInteger("2")).toBeFalsy()
expect(validateInteger(-1)).toBeFalsy()
expect(validateInteger(-20)).toBeFalsy()
expect(validateInteger(-5000000)).toBeFalsy()
})
it("doesn't return for negative integers", function() {
expect(validateInteger(-1)).toBeFalsy()
expect(validateInteger(-20)).toBeFalsy()
expect(validateInteger(-5000000)).toBeFalsy()
})
it("returns a message for decimal values", function() {
expect(validateInteger(1.1)).toEqual(errorMessage)
expect(validateInteger(2.5)).toEqual(errorMessage)
expect(validateInteger(-30.99)).toEqual(errorMessage)
})
it("returns a message for strings", function() {
expect(validateInteger("")).toEqual(errorMessage)
expect(validateInteger(" ")).toEqual(errorMessage)
expect(validateInteger("test")).toEqual(errorMessage)
})
it("returns a message for invalid input", function() {
expect(validateInteger(undefined)).toEqual(errorMessage)
expect(validateInteger(null)).toEqual(errorMessage)
expect(validateInteger({})).toEqual(errorMessage)
expect(validateInteger([])).toEqual(errorMessage)
expect(validateInteger(true)).toEqual(errorMessage)
expect(validateInteger(false)).toEqual(errorMessage)
})
})
describe("validateParam", function() {
let param = null
let result = null
it("validates required strings", function() {
param = fromJS({
required: true,
type: "string",
value: ""
})
result = validateParam( param, false )
expect( result ).toEqual( ["Required field is not provided"] )
})
it("validates required arrays", function() {
param = fromJS({
required: true,
type: "array",
value: []
})
result = validateParam( param, false )
expect( result ).toEqual( ["Required field is not provided"] )
param = fromJS({
required: true,
type: "array",
value: []
})
result = validateParam( param, false )
expect( result ).toEqual( ["Required field is not provided"] )
})
it("validates numbers", function() {
param = fromJS({
required: false,
type: "number",
value: "test"
})
result = validateParam( param, false )
expect( result ).toEqual( ["Value must be a number"] )
})
it("validates integers", function() {
param = fromJS({
required: false,
type: "integer",
value: "test"
})
result = validateParam( param, false )
expect( result ).toEqual( ["Value must be an integer"] )
})
it("validates arrays", function() {
// empty array
param = fromJS({
required: false,
type: "array",
value: []
})
result = validateParam( param, false )
expect( result ).toEqual( [] )
// numbers
param = fromJS({
required: false,
type: "array",
value: ["number"],
items: {
type: "number"
}
})
result = validateParam( param, false )
expect( result ).toEqual( [{index: 0, error: "Value must be a number"}] )
// integers
param = fromJS({
required: false,
type: "array",
value: ["not", "numbers"],
items: {
type: "integer"
}
})
result = validateParam( param, false )
expect( result ).toEqual( [{index: 0, error: "Value must be an integer"}, {index: 1, error: "Value must be an integer"}] )
})
})
}) })