Merge ft/performance (#3670)

* Updated docs for correct usage of SWAGGER_JSON

* Removed href attribute from anchor tag if deeplinking is disabled

* If deeplinking is disabled the anchor tag has no href attribute as a result the mouse pointer is not a pointer as it is no longer a hyperlink, setting the cursor explicitly to pointer.

* Refactor: use ternary operators at attribute level instead of element level

* Only polyfill Promise if it doesn't exist at all

* v3.1.7

* Typo fix

* fix #3624

* Squash commit: OAS3 Try-It-Out changes

* Parse JSON requestBodies so Client can consume them correctly

* Use Client branch

* Fix typo in swagger-client dependency

* Fix property names being displayed in array models

* Working on refactoring of model.jsx

* Fit linter and tests

* Add comment to array-model for to clarify change. Rework logic in `Model.render()` to fix bug with overriding name and schema from `$ref` definition.

* v3.2.0

* fromJS does not maintain order of object properties. Use a reviver function with fromJS inside the response.jsx component for the passed down schema prop.

* OAS3 Accept header control: Component-side

* OAS3 Accept header control: State-side

* Update response.jsx to use already existing, fromJSOrdered function

* Added test for response.jsx to make sure properties are passed to `ModelExample` component in the correct order

* Remove `it.only` from new test

* Fixes #3596

Wrap `isShownKey` values in a function that replaces spaces with underscores. When parsing the hash on route change, replace the spaces in the values with underscores again.

* Replace spaces with underscores when setting the hash value and inserting the ID into the DOM. Escape the deep link path when querying for the DOM element on hash change.

* Handle null value in createDeepLinkPath

* Add extra check for String types in `createDeepLinkPath`. Add `trim()` call on passed-in value in `createDeepLinkPath`. Added unit tests for new deep link util functions.

* LINTING!

* Roll back win import removal

Lost in merge conflict....

* More merge oversights...
This commit is contained in:
Owen Conti
2017-09-15 22:09:35 -06:00
committed by GitHub
parent 32c96e348b
commit 91a4794ab5
43 changed files with 1088 additions and 154 deletions

View File

@@ -25,7 +25,7 @@ or anything that violates the specifications.
| Which Swagger/OpenAPI version? | | Which Swagger/OpenAPI version? |
| Which Swagger-UI version? | | Which Swagger-UI version? |
| How did you install Swagger-UI? | | How did you install Swagger-UI? |
| Which broswer & version? | | Which browser & version? |
| Which operating system? | | Which operating system? |

View File

@@ -22,7 +22,7 @@ The OpenAPI Specification has undergone 5 revisions since initial creation in 20
Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes
------------------ | ------------ | -------------------------- | ----- ------------------ | ------------ | -------------------------- | -----
3.1.6 | 2017-08-18 | 2.0, 3.0 | [tag v3.1.6](https://github.com/swagger-api/swagger-ui/tree/v3.1.6) 3.2.0 | 2017-09-08 | 2.0, 3.0 | [tag v3.2.0](https://github.com/swagger-api/swagger-ui/tree/v3.2.0)
3.0.21 | 2017-07-26 | 2.0 | [tag v3.0.21](https://github.com/swagger-api/swagger-ui/tree/v3.0.21) 3.0.21 | 2017-07-26 | 2.0 | [tag v3.0.21](https://github.com/swagger-api/swagger-ui/tree/v3.0.21)
2.2.10 | 2017-01-04 | 1.1, 1.2, 2.0 | [tag v2.2.10](https://github.com/swagger-api/swagger-ui/tree/v2.2.10) 2.2.10 | 2017-01-04 | 1.1, 1.2, 2.0 | [tag v2.2.10](https://github.com/swagger-api/swagger-ui/tree/v2.2.10)
2.1.5 | 2016-07-20 | 1.1, 1.2, 2.0 | [tag v2.1.5](https://github.com/swagger-api/swagger-ui/tree/v2.1.5) 2.1.5 | 2016-07-20 | 1.1, 1.2, 2.0 | [tag v2.1.5](https://github.com/swagger-api/swagger-ui/tree/v2.1.5)
@@ -46,7 +46,7 @@ Will start nginx with swagger-ui on port 80.
Or you can provide your own swagger.json on your host Or you can provide your own swagger.json on your host
``` ```
docker run -p 80:8080 -e "SWAGGER_JSON=/foo/swagger.json" -v /bar:/foo swaggerapi/swagger-ui docker run -p 80:8080 -e SWAGGER_JSON=/foo/swagger.json -v /bar:/foo swaggerapi/swagger-ui
``` ```
##### Prerequisites ##### Prerequisites

View File

@@ -11,7 +11,11 @@
var redirectUrl = oauth2.redirectUrl; var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr; var isValid, qp, arr;
qp = (window.location.hash || location.search).substring(1); if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&") arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';}) arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})

View File

@@ -11,7 +11,11 @@
var redirectUrl = oauth2.redirectUrl; var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr; var isValid, qp, arr;
qp = (window.location.hash || location.search).substring(1); if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&") arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';}) arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;;;;;AAoyKA;;;;;;AAm/EA;;;;;;;;;;;;;;;;;;;;;;;;;;AAy7TA;;;;;;;;;;;;;;AAs8JA;;;;;;;;;AAwhpBA;;;;;AA41QA;AAm4DA;;;;;;AAo4YA;;;;;;AA8jaA;AAumvBA","sourceRoot":""} {"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;;;;;AAu7LA;;;;;;AA65DA;;;;;;;;;;;;;;;;;;;;;;;;;;AA68TA;;;;;;;;;;;;;;AAs8JA;;;;;;;;;AA69pBA;;;;;AA81QA;AAm4DA;;;;;;AAo4YA;;;;;;AA0iaA;AA4lvBA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AA21CA;;;;;;AAspFA","sourceRoot":""} {"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AA80CA;;;;;;AAqpFA","sourceRoot":""}

2
dist/swagger-ui.css vendored

File diff suppressed because one or more lines are too long

4
dist/swagger-ui.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AA2idA","sourceRoot":""} {"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAokeA","sourceRoot":""}

View File

@@ -1,6 +1,6 @@
{ {
"name": "swagger-ui", "name": "swagger-ui",
"version": "3.1.6", "version": "3.2.0",
"main": "dist/swagger-ui.js", "main": "dist/swagger-ui.js",
"repository": "git@github.com:swagger-api/swagger-ui.git", "repository": "git@github.com:swagger-api/swagger-ui.git",
"contributors": [ "contributors": [
@@ -41,6 +41,8 @@
"dependencies": { "dependencies": {
"base64-js": "^1.2.0", "base64-js": "^1.2.0",
"brace": "0.7.0", "brace": "0.7.0",
"classnames": "^2.2.5",
"css.escape": "1.5.1",
"deep-extend": "0.4.1", "deep-extend": "0.4.1",
"expect": "1.20.2", "expect": "1.20.2",
"getbase": "^2.8.2", "getbase": "^2.8.2",
@@ -75,7 +77,7 @@
"scroll-to-element": "^2.0.0", "scroll-to-element": "^2.0.0",
"serialize-error": "2.0.0", "serialize-error": "2.0.0",
"shallowequal": "0.2.2", "shallowequal": "0.2.2",
"swagger-client": "3.0.20", "swagger-client": "3.1.0",
"url-parse": "^1.1.8", "url-parse": "^1.1.8",
"whatwg-fetch": "0.11.1", "whatwg-fetch": "0.11.1",
"worker-loader": "^0.7.1", "worker-loader": "^0.7.1",

View File

@@ -28,10 +28,15 @@ export default class ArrayModel extends Component {
<span className="model-title__text">{ title }</span> <span className="model-title__text">{ title }</span>
</span> </span>
/*
Note: we set `name={null}` in <Model> below because we don't want
the name of the current Model passed (and displayed) as the name of the array element Model
*/
return <span className="model"> return <span className="model">
<ModelCollapse title={titleEl} collapsed={ depth > expandDepth } collapsedContent="[...]"> <ModelCollapse title={titleEl} collapsed={ depth > expandDepth } collapsedContent="[...]">
[ [
<span><Model { ...this.props } schema={ items } required={ false } depth={ depth + 1 } /></span> <span><Model { ...this.props } name={null} schema={ items } required={ false } depth={ depth + 1 } /></span>
] ]
{ {
properties.size ? <span> properties.size ? <span>

View File

@@ -8,6 +8,8 @@ export default class BaseLayout extends React.Component {
errActions: PropTypes.object.isRequired, errActions: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
oas3Selectors: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
layoutSelectors: PropTypes.object.isRequired, layoutSelectors: PropTypes.object.isRequired,
layoutActions: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired getComponent: PropTypes.func.isRequired
@@ -19,7 +21,14 @@ export default class BaseLayout extends React.Component {
} }
render() { render() {
let { specSelectors, specActions, getComponent, layoutSelectors } = this.props let {
specSelectors,
specActions,
getComponent,
layoutSelectors,
oas3Selectors,
oas3Actions
} = this.props
let info = specSelectors.info() let info = specSelectors.info()
let url = specSelectors.url() let url = specSelectors.url()
@@ -28,6 +37,7 @@ export default class BaseLayout extends React.Component {
let securityDefinitions = specSelectors.securityDefinitions() let securityDefinitions = specSelectors.securityDefinitions()
let externalDocs = specSelectors.externalDocs() let externalDocs = specSelectors.externalDocs()
let schemes = specSelectors.schemes() let schemes = specSelectors.schemes()
let servers = specSelectors.servers()
let Info = getComponent("info") let Info = getComponent("info")
let Operations = getComponent("operations", true) let Operations = getComponent("operations", true)
@@ -35,6 +45,7 @@ export default class BaseLayout extends React.Component {
let AuthorizeBtn = getComponent("authorizeBtn", true) let AuthorizeBtn = getComponent("authorizeBtn", true)
let Row = getComponent("Row") let Row = getComponent("Row")
let Col = getComponent("Col") let Col = getComponent("Col")
let Servers = getComponent("Servers")
let Errors = getComponent("errors", true) let Errors = getComponent("errors", true)
let isLoading = specSelectors.loadingStatus() === "loading" let isLoading = specSelectors.loadingStatus() === "loading"
@@ -82,6 +93,22 @@ export default class BaseLayout extends React.Component {
</div> </div>
) : null } ) : null }
{ servers && servers.size ? (
<div className="server-container">
<Col className="servers wrapper" mobile={12}>
<Servers
servers={servers}
currentServer={oas3Selectors.selectedServer()}
setSelectedServer={oas3Actions.setSelectedServer}
setServerVariableValue={oas3Actions.setServerVariableValue}
getServerVariable={oas3Selectors.serverVariableValue}
getEffectiveServerValue={oas3Selectors.serverEffectiveValue}
/>
</Col>
</div>
) : null}
{ {
filter === null || filter === false ? null : filter === null || filter === false ? null :
<div className="filter-container"> <div className="filter-container">

View File

@@ -30,39 +30,38 @@ export default class Model extends Component {
render () { render () {
let { getComponent, specSelectors, schema, required, name, isRef } = this.props let { getComponent, specSelectors, schema, required, name, isRef } = this.props
let ObjectModel = getComponent("ObjectModel") const ObjectModel = getComponent("ObjectModel")
let ArrayModel = getComponent("ArrayModel") const ArrayModel = getComponent("ArrayModel")
let PrimitiveModel = getComponent("PrimitiveModel") const PrimitiveModel = getComponent("PrimitiveModel")
let type = "object"
let $$ref = schema && schema.get("$$ref") let $$ref = schema && schema.get("$$ref")
let modelName = $$ref && this.getModelName( $$ref )
let modelSchema, type // If we weren't passed a `name` but have a ref, grab the name from the ref
if ( !name && $$ref ) {
name = this.getModelName( $$ref )
}
// If we weren't passed a `schema` but have a ref, grab the schema from the ref
if ( !schema && $$ref ) {
schema = this.getRefSchema( name )
}
const deprecated = specSelectors.isOAS3() && schema.get("deprecated") const deprecated = specSelectors.isOAS3() && schema.get("deprecated")
isRef = isRef !== undefined ? isRef : !!$$ref
if ( schema && (schema.get("type") || schema.get("properties")) ) { type = schema && schema.get("type") || type
modelSchema = schema
} else if ( $$ref ) {
modelSchema = this.getRefSchema( modelName )
}
type = modelSchema && modelSchema.get("type")
if ( !type && modelSchema && modelSchema.get("properties") ) {
type = "object"
}
switch(type) { switch(type) {
case "object": case "object":
return <ObjectModel return <ObjectModel
className="object" { ...this.props } className="object" { ...this.props }
schema={ modelSchema } schema={ schema }
name={ modelName || name } name={ name }
deprecated={deprecated} deprecated={deprecated}
isRef={ isRef!== undefined ? isRef : !!$$ref } /> isRef={ isRef } />
case "array": case "array":
return <ArrayModel return <ArrayModel
className="array" { ...this.props } className="array" { ...this.props }
schema={ modelSchema } schema={ schema }
name={ modelName || name } name={ name }
deprecated={deprecated} deprecated={deprecated}
required={ required } /> required={ required } />
case "string": case "string":
@@ -73,9 +72,10 @@ export default class Model extends Component {
return <PrimitiveModel return <PrimitiveModel
{ ...this.props } { ...this.props }
getComponent={ getComponent } getComponent={ getComponent }
schema={ modelSchema } schema={ schema }
name={ modelName || name } name={ name }
deprecated={deprecated} deprecated={deprecated}
required={ required }/> } required={ required }/>
}
} }
} }

View File

@@ -34,7 +34,6 @@ export default class Models extends Component {
return <div className="model-container" key={ `models-section-${name}` }> return <div className="model-container" key={ `models-section-${name}` }>
<ModelWrapper name={ name } <ModelWrapper name={ name }
schema={ model } schema={ model }
isRef={ true }
getComponent={ getComponent } getComponent={ getComponent }
specSelectors={ specSelectors }/> specSelectors={ specSelectors }/>
</div> </div>

View File

@@ -28,6 +28,7 @@ export default class Operation extends PureComponent {
authSelectors: PropTypes.object, authSelectors: PropTypes.object,
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
layoutActions: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired,
layoutSelectors: PropTypes.object.isRequired, layoutSelectors: PropTypes.object.isRequired,
fn: PropTypes.object.isRequired, fn: PropTypes.object.isRequired,
@@ -117,7 +118,8 @@ export default class Operation extends PureComponent {
specSelectors, specSelectors,
authActions, authActions,
authSelectors, authSelectors,
getConfigs getConfigs,
oas3Actions
} = this.props } = this.props
let summary = operation.get("summary") let summary = operation.get("summary")
@@ -163,8 +165,8 @@ export default class Operation extends PureComponent {
<span className={ deprecated ? "opblock-summary-path__deprecated" : "opblock-summary-path" } > <span className={ deprecated ? "opblock-summary-path__deprecated" : "opblock-summary-path" } >
<a <a
className="nostyle" className="nostyle"
onClick={(e) => e.preventDefault()} onClick={isDeepLinkingEnabled ? (e) => e.preventDefault() : null}
href={ isDeepLinkingEnabled ? `#/${isShownKey[1]}/${isShownKey[2]}` : ""} > href={isDeepLinkingEnabled ? `#/${isShownKey[1]}/${isShownKey[2]}` : null}>
<span>{path}</span> <span>{path}</span>
</a> </a>
<JumpToPath path={jumpToKey} /> <JumpToPath path={jumpToKey} />
@@ -265,6 +267,7 @@ export default class Operation extends PureComponent {
getComponent={ getComponent } getComponent={ getComponent }
getConfigs={ getConfigs } getConfigs={ getConfigs }
specSelectors={ specSelectors } specSelectors={ specSelectors }
oas3Actions={oas3Actions}
specActions={ specActions } specActions={ specActions }
produces={ produces } produces={ produces }
producesValue={ operation.get("produces_value") } producesValue={ operation.get("produces_value") }

View File

@@ -1,7 +1,7 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { helpers } from "swagger-client" import { helpers } from "swagger-client"
import { createDeepLinkPath } from "core/utils"
const { opId } = helpers const { opId } = helpers
export default class Operations extends React.Component { export default class Operations extends React.Component {
@@ -9,6 +9,7 @@ export default class Operations extends React.Component {
static propTypes = { static propTypes = {
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
layoutSelectors: PropTypes.object.isRequired, layoutSelectors: PropTypes.object.isRequired,
layoutActions: PropTypes.object.isRequired, layoutActions: PropTypes.object.isRequired,
@@ -21,6 +22,7 @@ export default class Operations extends React.Component {
let { let {
specSelectors, specSelectors,
specActions, specActions,
oas3Actions,
getComponent, getComponent,
layoutSelectors, layoutSelectors,
layoutActions, layoutActions,
@@ -69,7 +71,7 @@ export default class Operations extends React.Component {
let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"]) let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"])
let tagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"]) let tagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
let isShownKey = ["operations-tag", tag] let isShownKey = ["operations-tag", createDeepLinkPath(tag)]
let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list") let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list")
return ( return (
@@ -81,8 +83,8 @@ export default class Operations extends React.Component {
id={isShownKey.join("-")}> id={isShownKey.join("-")}>
<a <a
className="nostyle" className="nostyle"
onClick={(e) => e.preventDefault()} onClick={isDeepLinkingEnabled ? (e) => e.preventDefault() : null}
href={ isDeepLinkingEnabled ? `#/${tag}` : ""}> href= {isDeepLinkingEnabled ? `#/${tag}` : null}>
<span>{tag}</span> <span>{tag}</span>
</a> </a>
{ !tagDescription ? null : { !tagDescription ? null :
@@ -124,7 +126,7 @@ export default class Operations extends React.Component {
const operationId = const operationId =
op.getIn(["operation", "operationId"]) || op.getIn(["operation", "__originalOperationId"]) || opId(op.get("operation"), path, method) || op.get("id") op.getIn(["operation", "operationId"]) || op.getIn(["operation", "__originalOperationId"]) || opId(op.get("operation"), path, method) || op.get("id")
const isShownKey = ["operations", tag, operationId] const isShownKey = ["operations", createDeepLinkPath(tag), createDeepLinkPath(operationId)]
const allowTryItOut = specSelectors.allowTryItOutFor(op.get("path"), op.get("method")) const allowTryItOut = specSelectors.allowTryItOutFor(op.get("path"), op.get("method"))
const response = specSelectors.responseFor(op.get("path"), op.get("method")) const response = specSelectors.responseFor(op.get("path"), op.get("method"))
@@ -147,6 +149,8 @@ export default class Operations extends React.Component {
specActions={ specActions } specActions={ specActions }
specSelectors={ specSelectors } specSelectors={ specSelectors }
oas3Actions={oas3Actions}
layoutActions={ layoutActions } layoutActions={ layoutActions }
layoutSelectors={ layoutSelectors } layoutSelectors={ layoutSelectors }

View File

@@ -81,7 +81,6 @@ export default class ParameterRow extends Component {
const Markdown = getComponent("Markdown") const Markdown = getComponent("Markdown")
let schema = param.get("schema") let schema = param.get("schema")
let type = isOAS3 && isOAS3() ? param.getIn(["schema", "type"]) : param.get("type") let type = isOAS3 && isOAS3() ? param.getIn(["schema", "type"]) : param.get("type")
let isFormData = inType === "formData" let isFormData = inType === "formData"
let isFormDataSupported = "FormData" in win let isFormDataSupported = "FormData" in win

View File

@@ -1,7 +1,8 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import cx from "classnames"
import { fromJS, Seq } from "immutable" import { fromJS, Seq } from "immutable"
import { getSampleSchema } from "core/utils" import { getSampleSchema, fromJSOrdered } from "core/utils"
const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => { const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => {
if ( examples && examples.size ) { if ( examples && examples.size ) {
@@ -46,23 +47,35 @@ export default class Response extends React.Component {
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
fn: PropTypes.object.isRequired, fn: PropTypes.object.isRequired,
contentType: PropTypes.string contentType: PropTypes.string,
controlsAcceptHeader: PropTypes.bool,
onContentTypeChange: PropTypes.func
} }
static defaultProps = { static defaultProps = {
response: fromJS({}), response: fromJS({}),
onContentTypeChange: () => {}
}; };
_onContentTypeChange = (value) => {
const { onContentTypeChange, controlsAcceptHeader } = this.props
this.setState({ responseContentType: value })
onContentTypeChange({
value: value,
controlsAcceptHeader
})
}
render() { render() {
let { let {
code, code,
response, response,
className, className,
fn, fn,
getComponent, getComponent,
specSelectors, specSelectors,
contentType contentType,
controlsAcceptHeader
} = this.props } = this.props
let { inferSchema } = fn let { inferSchema } = fn
@@ -107,17 +120,24 @@ export default class Response extends React.Component {
<Markdown source={ response.get( "description" ) } /> <Markdown source={ response.get( "description" ) } />
</div> </div>
{ isOAS3 ? <ContentType { isOAS3 ?
<div className={cx("response-content-type", {
"controls-accept-header": controlsAcceptHeader
})}>
<ContentType
value={this.state.responseContentType} value={this.state.responseContentType}
contentTypes={ response.get("content") ? response.get("content").keySeq() : Seq() } contentTypes={ response.get("content") ? response.get("content").keySeq() : Seq() }
onChange={(val) => this.setState({ responseContentType: val })} onChange={this._onContentTypeChange}
className="response-content-type" /> : null } />
{ controlsAcceptHeader ? <small>Controls <code>Accept</code> header.</small> : null }
</div>
: null }
{ example ? ( { example ? (
<ModelExample <ModelExample
getComponent={ getComponent } getComponent={ getComponent }
specSelectors={ specSelectors } specSelectors={ specSelectors }
schema={ fromJS(schema) } schema={ fromJSOrdered(schema) }
example={ example }/> example={ example }/>
) : null} ) : null}

View File

@@ -1,7 +1,7 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { fromJS } from "immutable" import { fromJS } from "immutable"
import { defaultStatusCode } from "core/utils" import { defaultStatusCode, getAcceptControllingResponse } from "core/utils"
export default class Responses extends React.Component { export default class Responses extends React.Component {
@@ -14,6 +14,7 @@ export default class Responses extends React.Component {
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
specActions: PropTypes.object.isRequired, specActions: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
pathMethod: PropTypes.array.isRequired, pathMethod: PropTypes.array.isRequired,
displayRequestDuration: PropTypes.bool.isRequired, displayRequestDuration: PropTypes.bool.isRequired,
fn: PropTypes.object.isRequired, fn: PropTypes.object.isRequired,
@@ -29,8 +30,28 @@ export default class Responses extends React.Component {
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val) onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val)
onResponseContentTypeChange = ({ controlsAcceptHeader, value }) => {
const { oas3Actions, pathMethod } = this.props
if(controlsAcceptHeader) {
oas3Actions.setResponseContentType({
value,
pathMethod
})
}
}
render() { render() {
let { responses, request, tryItOutResponse, getComponent, getConfigs, specSelectors, fn, producesValue, displayRequestDuration } = this.props let {
responses,
request,
tryItOutResponse,
getComponent,
getConfigs,
specSelectors,
fn,
producesValue,
displayRequestDuration
} = this.props
let defaultCode = defaultStatusCode( responses ) let defaultCode = defaultStatusCode( responses )
const ContentType = getComponent( "contentType" ) const ContentType = getComponent( "contentType" )
@@ -39,6 +60,11 @@ export default class Responses extends React.Component {
let produces = this.props.produces && this.props.produces.size ? this.props.produces : Responses.defaultProps.produces let produces = this.props.produces && this.props.produces.size ? this.props.produces : Responses.defaultProps.produces
const isSpecOAS3 = specSelectors.isOAS3()
const acceptControllingResponse = isSpecOAS3 ?
getAcceptControllingResponse(responses) : null
return ( return (
<div className="responses-wrapper"> <div className="responses-wrapper">
<div className="opblock-section-header"> <div className="opblock-section-header">
@@ -78,7 +104,6 @@ export default class Responses extends React.Component {
<tbody> <tbody>
{ {
responses.entrySeq().map( ([code, response]) => { responses.entrySeq().map( ([code, response]) => {
let className = tryItOutResponse && tryItOutResponse.get("status") == code ? "response_current" : "" let className = tryItOutResponse && tryItOutResponse.get("status") == code ? "response_current" : ""
return ( return (
<Response key={ code } <Response key={ code }
@@ -88,6 +113,8 @@ export default class Responses extends React.Component {
code={ code } code={ code }
response={ response } response={ response }
specSelectors={ specSelectors } specSelectors={ specSelectors }
controlsAcceptHeader={response === acceptControllingResponse}
onContentTypeChange={this.onResponseContentTypeChange}
contentType={ producesValue } contentType={ producesValue }
getComponent={ getComponent }/> getComponent={ getComponent }/>
) )

View File

@@ -1,4 +1,5 @@
import { setHash } from "./helpers" import { setHash } from "./helpers"
import { createDeepLinkPath } from "core/utils"
export const show = (ori, { getConfigs }) => (...args) => { export const show = (ori, { getConfigs }) => (...args) => {
ori(...args) ori(...args)
@@ -19,12 +20,12 @@ export const show = (ori, { getConfigs }) => (...args) => {
if(type === "operations") { if(type === "operations") {
let [, tag, operationId] = thing let [, tag, operationId] = thing
setHash(`/${tag}/${operationId}`) setHash(`/${createDeepLinkPath(tag)}/${createDeepLinkPath(operationId)}`)
} }
if(type === "operations-tag") { if(type === "operations-tag") {
let [, tag] = thing let [, tag] = thing
setHash(`/${tag}`) setHash(`/${createDeepLinkPath(tag)}`)
} }
} }

View File

@@ -1,4 +1,5 @@
import scrollTo from "scroll-to-element" import scrollTo from "scroll-to-element"
import { escapeDeepLinkPath } from "core/utils"
const SCROLL_OFFSET = -5 const SCROLL_OFFSET = -5
let hasHashBeenParsed = false let hasHashBeenParsed = false
@@ -34,14 +35,14 @@ export const updateResolved = (ori, { layoutActions, getConfigs }) => (...args)
layoutActions.show(["operations-tag", tag], true) layoutActions.show(["operations-tag", tag], true)
layoutActions.show(["operations", tag, operationId], true) layoutActions.show(["operations", tag, operationId], true)
scrollTo(`#operations-${tag}-${operationId}`, { scrollTo(`#operations-${escapeDeepLinkPath(tag)}-${escapeDeepLinkPath(operationId)}`, {
offset: SCROLL_OFFSET offset: SCROLL_OFFSET
}) })
} else if(tag) { } else if(tag) {
// Pre-expand and scroll to the tag // Pre-expand and scroll to the tag
layoutActions.show(["operations-tag", tag], true) layoutActions.show(["operations-tag", tag], true)
scrollTo(`#operations-tag-${tag}`, { scrollTo(`#operations-tag-${escapeDeepLinkPath(tag)}`, {
offset: SCROLL_OFFSET offset: SCROLL_OFFSET
}) })
} }

View File

@@ -0,0 +1,43 @@
// Actions conform to FSA (flux-standard-actions)
// {type: string,payload: Any|Error, meta: obj, error: bool}
export const UPDATE_SELECTED_SERVER = "oas3_set_servers"
export const UPDATE_REQUEST_BODY_VALUE = "oas3_set_request_body_value"
export const UPDATE_REQUEST_CONTENT_TYPE = "oas3_set_request_content_type"
export const UPDATE_RESPONSE_CONTENT_TYPE = "oas3_set_response_content_type"
export const UPDATE_SERVER_VARIABLE_VALUE = "oas3_set_server_variable_value"
export function setSelectedServer (selectedServerUrl) {
return {
type: UPDATE_SELECTED_SERVER,
payload: selectedServerUrl
}
}
export function setRequestBodyValue ({ value, pathMethod }) {
return {
type: UPDATE_REQUEST_BODY_VALUE,
payload: { value, pathMethod }
}
}
export function setRequestContentType ({ value, pathMethod }) {
return {
type: UPDATE_REQUEST_CONTENT_TYPE,
payload: { value, pathMethod }
}
}
export function setResponseContentType ({ value, pathMethod }) {
return {
type: UPDATE_RESPONSE_CONTENT_TYPE,
payload: { value, pathMethod }
}
}
export function setServerVariableValue ({ server, key, val }) {
return {
type: UPDATE_SERVER_VARIABLE_VALUE,
payload: { server, key, val }
}
}

View File

@@ -1,9 +1,13 @@
import Callbacks from "./callbacks" import Callbacks from "./callbacks"
import RequestBody from "./request-body" import RequestBody from "./request-body"
import OperationLink from "./operation-link.jsx" import OperationLink from "./operation-link.jsx"
import Servers from "./servers"
import RequestBodyEditor from "./request-body-editor"
export default { export default {
Callbacks, Callbacks,
RequestBody, RequestBody,
Servers,
RequestBodyEditor,
operationLink: OperationLink operationLink: OperationLink
} }

View File

@@ -0,0 +1,114 @@
import React, { PureComponent } from "react"
import PropTypes from "prop-types"
import { fromJS } from "immutable"
import { getSampleSchema } from "core/utils"
const NOOP = Function.prototype
export default class RequestBodyEditor extends PureComponent {
static propTypes = {
requestBody: PropTypes.object.isRequired,
mediaType: PropTypes.string.isRequired,
onChange: PropTypes.func,
getComponent: PropTypes.func.isRequired,
isExecute: PropTypes.bool,
specSelectors: PropTypes.object.isRequired,
};
static defaultProps = {
mediaType: "application/json",
requestBody: fromJS({}),
onChange: NOOP,
};
constructor(props, context) {
super(props, context)
this.state = {
isEditBox: false,
value: ""
}
}
componentDidMount() {
this.setValueToSample.call(this)
}
componentWillReceiveProps(nextProps) {
if(this.props.mediaType !== nextProps.mediaType) {
// media type was changed
this.setValueToSample(nextProps.mediaType)
}
if(!this.props.isExecute && nextProps.isExecute) {
// we just entered execute mode,
// so enable editing for convenience
this.setState({ isEditBox: true })
}
}
setValueToSample = (explicitMediaType) => {
this.onChange(this.sample(explicitMediaType))
}
sample = (explicitMediaType) => {
let { requestBody, mediaType } = this.props
let schema = requestBody.getIn(["content", explicitMediaType || mediaType, "schema"]).toJS()
return getSampleSchema(schema, explicitMediaType || mediaType, {
includeWriteOnly: true
})
}
onChange = (value) => {
this.setState({value})
this.props.onChange(value)
}
handleOnChange = e => {
const { mediaType } = this.props
const isJson = /json/i.test(mediaType)
const inputValue = isJson ? e.target.value.trim() : e.target.value
this.onChange(inputValue)
}
toggleIsEditBox = () => this.setState( state => ({isEditBox: !state.isEditBox}))
render() {
let {
isExecute,
getComponent,
} = this.props
const Button = getComponent("Button")
const TextArea = getComponent("TextArea")
const HighlightCode = getComponent("highlightCode")
let { value, isEditBox } = this.state
return (
<div className="body-param">
{
isEditBox && isExecute
? <TextArea className={"body-param__text"} value={value} onChange={ this.handleOnChange }/>
: (value && <HighlightCode className="body-param__example"
value={ value }/>)
}
<div className="body-param-options">
{
!isExecute ? null
: <div className="body-param-edit">
<Button className={isEditBox ? "btn cancel body-param__example-edit" : "btn edit body-param__example-edit"}
onClick={this.toggleIsEditBox}>{ isEditBox ? "Cancel" : "Edit"}
</Button>
</div>
}
</div>
</div>
)
}
}

View File

@@ -2,13 +2,18 @@ import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes" import ImPropTypes from "react-immutable-proptypes"
import { OrderedMap } from "immutable" import { OrderedMap } from "immutable"
import { getSampleSchema } from "core/utils"
const RequestBody = ({
const RequestBody = ({ requestBody, getComponent, specSelectors, contentType }) => { requestBody,
getComponent,
specSelectors,
contentType,
isExecute,
onChange
}) => {
const Markdown = getComponent("Markdown") const Markdown = getComponent("Markdown")
const ModelExample = getComponent("modelExample") const ModelExample = getComponent("modelExample")
const HighlightCode = getComponent("highlightCode") const RequestBodyEditor = getComponent("RequestBodyEditor")
const requestBodyDescription = (requestBody && requestBody.get("description")) || null const requestBodyDescription = (requestBody && requestBody.get("description")) || null
const requestBodyContent = (requestBody && requestBody.get("content")) || new OrderedMap() const requestBodyContent = (requestBody && requestBody.get("content")) || new OrderedMap()
@@ -16,10 +21,6 @@ const RequestBody = ({ requestBody, getComponent, specSelectors, contentType })
const mediaTypeValue = requestBodyContent.get(contentType) const mediaTypeValue = requestBodyContent.get(contentType)
const sampleSchema = getSampleSchema(mediaTypeValue.get("schema").toJS(), contentType, {
includeWriteOnly: true
})
return <div> return <div>
{ requestBodyDescription && { requestBodyDescription &&
<Markdown source={requestBodyDescription} /> <Markdown source={requestBodyDescription} />
@@ -28,8 +29,16 @@ const RequestBody = ({ requestBody, getComponent, specSelectors, contentType })
getComponent={ getComponent } getComponent={ getComponent }
specSelectors={ specSelectors } specSelectors={ specSelectors }
expandDepth={1} expandDepth={1}
isExecute={isExecute}
schema={mediaTypeValue.get("schema")} schema={mediaTypeValue.get("schema")}
example={<HighlightCode value={sampleSchema} />} example={<RequestBodyEditor
requestBody={requestBody}
onChange={onChange}
mediaType={contentType}
getComponent={getComponent}
isExecute={isExecute}
specSelectors={specSelectors}
/>}
/> />
</div> </div>
} }
@@ -38,7 +47,9 @@ RequestBody.propTypes = {
requestBody: ImPropTypes.orderedMap.isRequired, requestBody: ImPropTypes.orderedMap.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
contentType: PropTypes.string.isRequired contentType: PropTypes.string.isRequired,
isExecute: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
} }
export default RequestBody export default RequestBody

View File

@@ -0,0 +1,155 @@
import React from "react"
import { OrderedMap } from "immutable"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
export default class Servers extends React.Component {
static propTypes = {
servers: ImPropTypes.list.isRequired,
currentServer: PropTypes.string.isRequired,
setSelectedServer: PropTypes.func.isRequired,
setServerVariableValue: PropTypes.func.isRequired,
getServerVariable: PropTypes.func.isRequired,
getEffectiveServerValue: PropTypes.func.isRequired
}
componentDidMount() {
let { servers } = this.props
//fire 'change' event to set default 'value' of select
this.setServer(servers.first().get("url"))
}
componentWillReceiveProps(nextProps) {
let {
servers,
setServerVariableValue,
getServerVariable
} = this.props
if(this.props.currentServer !== nextProps.currentServer) {
// Server has changed, we may need to set default values
let currentServerDefinition = servers
.find(v => v.get("url") === nextProps.currentServer) || OrderedMap()
let currentServerVariableDefs = currentServerDefinition.get("variables") || OrderedMap()
currentServerVariableDefs.map((val, key) => {
let currentValue = getServerVariable(nextProps.currentServer, key)
// only set the default value if the user hasn't set one yet
if(!currentValue) {
setServerVariableValue({
server: nextProps.currentServer,
key,
val: val.get("default") || ""
})
}
})
}
}
onServerChange =( e ) => {
this.setServer( e.target.value )
// set default variable values
}
onServerVariableValueChange = ( e ) => {
let {
setServerVariableValue,
currentServer
} = this.props
let variableName = e.target.getAttribute("data-variable")
let newVariableValue = e.target.value
if(typeof setServerVariableValue === "function") {
setServerVariableValue({
server: currentServer,
key: variableName,
val: newVariableValue
})
}
}
setServer = ( value ) => {
let { setSelectedServer } = this.props
setSelectedServer(value)
}
render() {
let { servers,
currentServer,
getServerVariable,
getEffectiveServerValue
} = this.props
let currentServerDefinition = servers.find(v => v.get("url") === currentServer) || OrderedMap()
let currentServerVariableDefs = currentServerDefinition.get("variables") || OrderedMap()
let shouldShowVariableUI = currentServerVariableDefs.size !== 0
return (
<div>
<label htmlFor="servers">
<span className="servers-title">Servers</span>
<select onChange={ this.onServerChange }>
{ servers.valueSeq().map(
( server ) =>
<option
value={ server.get("url") }
key={ server.get("url") }>
{ server.get("url") }
</option>
).toArray()}
</select>
</label>
{ shouldShowVariableUI ?
<div>
<h4>Server variables</h4>
<div className={"computed-url"}>
Computed URL:
<code>
{getEffectiveServerValue(currentServer)}
</code>
</div>
<table>
<tbody>
{
currentServerVariableDefs.map((val, name) => {
return <tr key={name}>
<td>{name}</td>
<td>
{ val.get("enum") ?
<select data-variable={name} onChange={this.onServerVariableValueChange}>
{val.get("enum").map(enumValue => {
return <option
selected={enumValue === getServerVariable(currentServer, name)}
key={enumValue}
value={enumValue}>
{enumValue}
</option>
})}
</select> :
<input
type={"text"}
value={getServerVariable(currentServer, name) || ""}
onChange={this.onServerVariableValueChange}
data-variable={name}
></input>
}
</td>
</tr>
})
}
</tbody>
</table>
</div>: null
}
</div>
)
}
}

View File

@@ -1,8 +1,12 @@
// import reducers from "./reducers" // import reducers from "./reducers"
// import * as actions from "./actions" // import * as actions from "./actions"
import * as wrapSelectors from "./wrap-selectors" import * as specWrapSelectors from "./spec-extensions/wrap-selectors"
import * as specSelectors from "./spec-extensions/selectors"
import components from "./components" import components from "./components"
import wrapComponents from "./wrap-components" import wrapComponents from "./wrap-components"
import * as oas3Actions from "./actions"
import * as oas3Selectors from "./selectors"
import oas3Reducers from "./reducers"
export default function() { export default function() {
return { return {
@@ -10,7 +14,13 @@ export default function() {
wrapComponents, wrapComponents,
statePlugins: { statePlugins: {
spec: { spec: {
wrapSelectors wrapSelectors: specWrapSelectors,
selectors: specSelectors
},
oas3: {
actions: oas3Actions,
reducers: oas3Reducers,
selectors: oas3Selectors,
} }
} }
} }

View File

@@ -0,0 +1,28 @@
import {
UPDATE_SELECTED_SERVER,
UPDATE_REQUEST_BODY_VALUE,
UPDATE_REQUEST_CONTENT_TYPE,
UPDATE_SERVER_VARIABLE_VALUE,
UPDATE_RESPONSE_CONTENT_TYPE
} from "./actions"
export default {
[UPDATE_SELECTED_SERVER]: (state, { payload: selectedServerUrl } ) =>{
return state.setIn( [ "selectedServer" ], selectedServerUrl)
},
[UPDATE_REQUEST_BODY_VALUE]: (state, { payload: { value, pathMethod } } ) =>{
let [path, method] = pathMethod
return state.setIn( [ "requestData", path, method, "bodyValue" ], value)
},
[UPDATE_REQUEST_CONTENT_TYPE]: (state, { payload: { value, pathMethod } } ) =>{
let [path, method] = pathMethod
return state.setIn( [ "requestData", path, method, "requestContentType" ], value)
},
[UPDATE_RESPONSE_CONTENT_TYPE]: (state, { payload: { value, pathMethod } } ) =>{
let [path, method] = pathMethod
return state.setIn( [ "requestData", path, method, "responseContentType" ], value)
},
[UPDATE_SERVER_VARIABLE_VALUE]: (state, { payload: { server, key, val } } ) =>{
return state.setIn( [ "serverVariableValues", server, key ], val)
},
}

View File

@@ -0,0 +1,58 @@
import { OrderedMap } from "immutable"
import { isOAS3 as isOAS3Helper } from "./helpers"
// Helpers
function onlyOAS3(selector) {
return (...args) => (system) => {
const spec = system.getSystem().specSelectors.specJson()
if(isOAS3Helper(spec)) {
return selector(...args)
} else {
return null
}
}
}
export const selectedServer = onlyOAS3(state => {
return state.getIn(["selectedServer"]) || ""
}
)
export const requestBodyValue = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "bodyValue"]) || null
}
)
export const requestContentType = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "requestContentType"]) || null
}
)
export const responseContentType = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "responseContentType"]) || null
}
)
export const serverVariableValue = onlyOAS3((state, server, key) => {
return state.getIn(["serverVariableValues", server, key]) || null
}
)
export const serverVariables = onlyOAS3((state, server) => {
return state.getIn(["serverVariableValues", server]) || OrderedMap()
}
)
export const serverEffectiveValue = onlyOAS3((state, server) => {
let varValues = state.getIn(["serverVariableValues", server]) || OrderedMap()
let str = server
varValues.map((val, key) => {
str = str.replace(new RegExp(`{${key}}`, "g"), val)
})
return str
}
)

View File

@@ -0,0 +1,50 @@
import { createSelector } from "reselect"
import { Map } from "immutable"
import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
// Helpers
function onlyOAS3(selector) {
return () => (system, ...args) => {
const spec = system.getSystem().specSelectors.specJson()
if(isOAS3Helper(spec)) {
return selector(...args)
} else {
return null
}
}
}
const state = state => {
return state || Map()
}
const specJson = createSelector(
state,
spec => spec.get("json", Map())
)
const specResolved = createSelector(
state,
spec => spec.get("resolved", Map())
)
const spec = state => {
let res = specResolved(state)
if(res.count() < 1)
res = specJson(state)
return res
}
// New selectors
export const servers = onlyOAS3(createSelector(
spec,
spec => spec.getIn(["servers"]) || Map()
))
export const isSwagger2 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson()
return isSwagger2Helper(spec)
}

View File

@@ -1,6 +1,6 @@
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { Map } from "immutable" import { Map } from "immutable"
import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "./helpers" import { isOAS3 as isOAS3Helper, isSwagger2 as isSwagger2Helper } from "../helpers"
// Helpers // Helpers
@@ -56,6 +56,11 @@ export const schemes = OAS3NullSelector
// New selectors // New selectors
export const servers = onlyOAS3(createSelector(
spec,
spec => spec.getIn(["servers"]) || Map()
))
export const isOAS3 = (ori, system) => () => { export const isOAS3 = (ori, system) => () => {
const spec = system.getSystem().specSelectors.specJson() const spec = system.getSystem().specSelectors.specJson()
return isOAS3Helper(spec) return isOAS3Helper(spec)

View File

@@ -3,7 +3,6 @@ import parameters from "./parameters"
import VersionStamp from "./version-stamp" import VersionStamp from "./version-stamp"
import OnlineValidatorBadge from "./online-validator-badge" import OnlineValidatorBadge from "./online-validator-badge"
import Model from "./model" import Model from "./model"
import TryItOutButton from "./try-it-out-button"
export default { export default {
Markdown, Markdown,
@@ -11,5 +10,4 @@ export default {
VersionStamp, VersionStamp,
model: Model, model: Model,
onlineValidatorBadge: OnlineValidatorBadge, onlineValidatorBadge: OnlineValidatorBadge,
TryItOutButton
} }

View File

@@ -13,8 +13,7 @@ class Parameters extends Component {
super(props) super(props)
this.state = { this.state = {
callbackVisible: false, callbackVisible: false,
parametersVisible: true, parametersVisible: true
requestBodyContentType: ""
} }
} }
@@ -24,6 +23,8 @@ class Parameters extends Component {
operation: PropTypes.object.isRequired, operation: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
oas3Actions: PropTypes.object.isRequired,
oas3Selectors: PropTypes.object.isRequired,
fn: PropTypes.object.isRequired, fn: PropTypes.object.isRequired,
tryItOutEnabled: PropTypes.bool, tryItOutEnabled: PropTypes.bool,
allowTryItOut: PropTypes.bool, allowTryItOut: PropTypes.bool,
@@ -86,6 +87,8 @@ class Parameters extends Component {
fn, fn,
getComponent, getComponent,
specSelectors, specSelectors,
oas3Actions,
oas3Selectors,
pathMethod, pathMethod,
operation operation
} = this.props } = this.props
@@ -159,16 +162,22 @@ class Parameters extends Component {
<h4 className={`opblock-title parameter__name ${requestBody.get("required") && "required"}`}>Request body</h4> <h4 className={`opblock-title parameter__name ${requestBody.get("required") && "required"}`}>Request body</h4>
<label> <label>
<ContentType <ContentType
value={this.state.requestBodyContentType} value={oas3Selectors.requestContentType(...pathMethod)}
contentTypes={ requestBody.get("content").keySeq() } contentTypes={ requestBody.get("content").keySeq() }
onChange={(val) => this.setState({ requestBodyContentType: val })} onChange={(value) => {
oas3Actions.setRequestContentType({ value, pathMethod })
}}
className="body-param-content-type" /> className="body-param-content-type" />
</label> </label>
</div> </div>
<div className="opblock-description-wrapper"> <div className="opblock-description-wrapper">
<RequestBody <RequestBody
requestBody={requestBody} requestBody={requestBody}
contentType={this.state.requestBodyContentType}/> isExecute={isExecute}
onChange={(value) => {
oas3Actions.setRequestBodyValue({ value, pathMethod })
}}
contentType={oas3Selectors.requestContentType(...pathMethod)}/>
</div> </div>
</div> </div>
} }

View File

@@ -1,5 +0,0 @@
import { OAS3ComponentWrapFactory } from "../helpers"
export default OAS3ComponentWrapFactory(() => {
return null
})

View File

@@ -1,6 +1,7 @@
import YAML from "js-yaml" import YAML from "js-yaml"
import parseUrl from "url-parse" import parseUrl from "url-parse"
import serializeError from "serialize-error" import serializeError from "serialize-error"
import { isJSONObject } from "core/utils"
// Actions conform to FSA (flux-standard-actions) // Actions conform to FSA (flux-standard-actions)
// {type: string,payload: Any|Error, meta: obj, error: bool} // {type: string,payload: Any|Error, meta: obj, error: bool}
@@ -195,7 +196,8 @@ export const logRequest = (req) => {
// Actually fire the request via fn.execute // Actually fire the request via fn.execute
// (For debugging) and ease of testing // (For debugging) and ease of testing
export const executeRequest = (req) => ({fn, specActions, specSelectors, getConfigs}) => { export const executeRequest = (req) =>
({fn, specActions, specSelectors, getConfigs, oas3Selectors}) => {
let { pathName, method, operation } = req let { pathName, method, operation } = req
let { requestInterceptor, responseInterceptor } = getConfigs() let { requestInterceptor, responseInterceptor } = getConfigs()
@@ -211,6 +213,21 @@ export const executeRequest = (req) => ({fn, specActions, specSelectors, getConf
req.operationId = fn.opId(op, pathName, method) req.operationId = fn.opId(op, pathName, method)
} }
if(specSelectors.isOAS3()) {
// OAS3 request feature support
req.server = oas3Selectors.selectedServer()
req.serverVariables = oas3Selectors.serverVariables(req.server).toJS()
req.requestContentType = oas3Selectors.requestContentType(pathName, method)
req.responseContentType = oas3Selectors.responseContentType(pathName, method) || "*/*"
const requestBody = oas3Selectors.requestBodyValue(pathName, method)
if(isJSONObject(requestBody)) {
req.requestBody = JSON.parse(requestBody)
} else {
req.requestBody = requestBody
}
}
let parsedRequest = Object.assign({}, req) let parsedRequest = Object.assign({}, req)
parsedRequest = fn.buildRequest(parsedRequest) parsedRequest = fn.buildRequest(parsedRequest)
@@ -234,7 +251,11 @@ export const executeRequest = (req) => ({fn, specActions, specSelectors, getConf
res.duration = Date.now() - startTime res.duration = Date.now() - startTime
specActions.setResponse(req.pathName, req.method, res) 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

@@ -8,11 +8,31 @@ import some from "lodash/some"
import eq from "lodash/eq" import eq from "lodash/eq"
import { memoizedSampleFromSchema, memoizedCreateXMLExample } from "core/plugins/samples/fn" import { memoizedSampleFromSchema, memoizedCreateXMLExample } from "core/plugins/samples/fn"
import win from "./window" import win from "./window"
import cssEscape from "css.escape"
const DEFAULT_REPONSE_KEY = "default" const DEFAULT_REPONSE_KEY = "default"
export const isImmutable = (maybe) => Im.Iterable.isIterable(maybe) export const isImmutable = (maybe) => Im.Iterable.isIterable(maybe)
export function isJSONObject (str) {
try {
var o = JSON.parse(str)
// Handle non-exception-throwing cases:
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
// but... JSON.parse(null) returns null, and typeof null === "object",
// so we must check for that, too. Thankfully, null is falsey, so this suffices:
if (o && typeof o === "object") {
return o
}
}
catch (e) {
// do nothing
}
return false
}
export function objectify (thing) { export function objectify (thing) {
if(!isObject(thing)) if(!isObject(thing))
return {} return {}
@@ -631,3 +651,29 @@ export const shallowEqualKeys = (a,b, keys) => {
return eq(a[key], b[key]) return eq(a[key], b[key])
}) })
} }
export function getAcceptControllingResponse(responses) {
if(!Im.OrderedMap.isOrderedMap(responses)) {
// wrong type!
return null
}
if(!responses.size) {
// responses is empty
return null
}
const suitable2xxResponse = responses.find((res, k) => {
return k.startsWith("2") && Object.keys(res.get("content") || {}).length > 0
})
// try to find a suitable `default` responses
const defaultResponse = responses.get("default") || Im.OrderedMap()
const defaultResponseMediaTypes = (defaultResponse.get("content") || Im.OrderedMap()).keySeq().toJS()
const suitableDefaultResponse = defaultResponseMediaTypes.length ? defaultResponse : null
return suitable2xxResponse || suitableDefaultResponse
}
export const createDeepLinkPath = (str) => typeof str == "string" || str instanceof String ? str.trim().replace(/\s/g, "_") : ""
export const escapeDeepLinkPath = (str) => cssEscape( createDeepLinkPath(str) )

View File

@@ -1,2 +1,5 @@
// Promise global, Used ( at least ) by 'whatwg-fetch'. And required by IE 11 // Promise global, Used ( at least ) by 'whatwg-fetch'. And required by IE 11
if(!window.Promise) {
require("core-js/fn/promise") require("core-js/fn/promise")
}

View File

@@ -636,6 +636,77 @@
} }
} }
.server-container
{
margin: 0 0 20px 0;
padding: 30px 0;
background: #fff;
box-shadow: 0 1px 2px 0 rgba(0,0,0,.15);
.computed-url {
margin: 2em 0;
code {
color: grey;
display: inline-block;
padding: 4px;
font-size: 16px;
margin: 0 1em;
font-style: italic;
}
}
.servers
{
display: flex;
align-items: center;
.servers-title {
margin-right: 1em;
}
> label
{
font-size: 12px;
display: flex;
flex-direction: column;
margin: -20px 15px 0 0;
@include text_headline();
select
{
min-width: 130px;
}
}
table {
tr {
width: 30em;
}
td {
display: inline-block;
max-width: 15em;
vertical-align: middle;
padding-top: 10px;
padding-bottom: 10px;
&:first-of-type {
padding-right: 2em;
}
input {
width: 100%;
height: 100%;
}
}
}
}
}
.loading-container .loading-container
{ {
@@ -704,6 +775,17 @@
.response-content-type { .response-content-type {
padding-top: 1em; padding-top: 1em;
&.controls-accept-header {
select {
border-color: green;
}
small {
color: green;
font-size: .7em;
}
}
} }
@keyframes blinker @keyframes blinker
@@ -726,12 +808,12 @@ section
a.nostyle { a.nostyle {
text-decoration: inherit; text-decoration: inherit;
color: inherit; color: inherit;
cursor: auto; cursor: pointer;
display: inline; display: inline;
&:visited { &:visited {
text-decoration: inherit; text-decoration: inherit;
color: inherit; color: inherit;
cursor: auto; cursor: pointer;
} }
} }

View File

@@ -0,0 +1,58 @@
import React from "react"
import expect from "expect"
import { shallow } from "enzyme"
import { fromJS } from "immutable"
import Response from "components/response"
import ModelExample from "components/model-example"
import { inferSchema } from "corePlugins/samples/fn"
describe("<Response />", function() {
const dummyComponent = () => null
const components = {
headers: dummyComponent,
highlightCode: dummyComponent,
modelExample: ModelExample,
Markdown: dummyComponent,
operationLink: dummyComponent,
contentType: dummyComponent
}
const props = {
getComponent: c => components[c],
specSelectors: {
isOAS3() {
return false
}
},
fn: {
inferSchema
},
contentType: "application/json",
className: "for-test",
response: fromJS({
type: "object",
properties: {
// Note reverse order: c, b, a
"c": {
type: "integer"
},
"b": {
type: "boolean"
},
"a": {
type: "string"
}
}
}),
code: "200"
}
it("renders the model-example schema properties in order", function() {
const wrapper = shallow(<Response {...props}/>)
const renderedModelExample = wrapper.find(ModelExample)
expect(renderedModelExample.length).toEqual(1)
// Assert the schema's properties have maintained their order
const modelExampleSchemaProperties = renderedModelExample.props().schema.toJS().properties
expect( Object.keys(modelExampleSchemaProperties) ).toEqual(["c", "b", "a"])
})
})

View File

@@ -113,7 +113,8 @@ describe("spec plugin - actions", function(){
spec: () => fromJS({}), spec: () => fromJS({}),
parameterValues: () => fromJS({}), parameterValues: () => fromJS({}),
contentTypeValues: () => fromJS({}), contentTypeValues: () => fromJS({}),
url: () => fromJS({}) url: () => fromJS({}),
isOAS3: () => false
}, },
getConfigs: () => configs getConfigs: () => configs
} }

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, OrderedMap } from "immutable"
import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered } from "core/utils" import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered, getAcceptControllingResponse, createDeepLinkPath, escapeDeepLinkPath } from "core/utils"
import win from "core/window" import win from "core/window"
describe("utils", function() { describe("utils", function() {
@@ -582,4 +582,151 @@ describe("utils", function() {
expect( result ).toEqual( [1, 1, 2, 3, 5, 8] ) expect( result ).toEqual( [1, 1, 2, 3, 5, 8] )
}) })
}) })
describe.only("getAcceptControllingResponse", () => {
it("should return the first 2xx response with a media type", () => {
const responses = fromJSOrdered({
"200": {
content: {
"application/json": {
schema: {
type: "object"
}
}
}
},
"201": {
content: {
"application/json": {
schema: {
type: "object"
}
}
}
}
})
expect(getAcceptControllingResponse(responses)).toEqual(responses.get("200"))
})
it("should skip 2xx responses without defined media types", () => {
const responses = fromJSOrdered({
"200": {
content: {
"application/json": {
schema: {
type: "object"
}
}
}
},
"201": {
content: {
"application/json": {
schema: {
type: "object"
}
}
}
}
})
expect(getAcceptControllingResponse(responses)).toEqual(responses.get("201"))
})
it("should default to the `default` response if it has defined media types", () => {
const responses = fromJSOrdered({
"200": {
description: "quite empty"
},
"201": {
description: "quite empty"
},
default: {
content: {
"application/json": {
schema: {
type: "object"
}
}
}
}
})
expect(getAcceptControllingResponse(responses)).toEqual(responses.get("default"))
})
it("should return null if there are no suitable controlling responses", () => {
const responses = fromJSOrdered({
"200": {
description: "quite empty"
},
"201": {
description: "quite empty"
},
"default": {
description: "also empty.."
}
})
expect(getAcceptControllingResponse(responses)).toBe(null)
})
it("should return null if an empty OrderedMap is passed", () => {
const responses = fromJSOrdered()
expect(getAcceptControllingResponse(responses)).toBe(null)
})
it("should return null if anything except an OrderedMap is passed", () => {
const responses = {}
expect(getAcceptControllingResponse(responses)).toBe(null)
})
})
describe("createDeepLinkPath", function() {
it("creates a deep link path replacing spaces with underscores", function() {
const result = createDeepLinkPath("tag id with spaces")
expect(result).toEqual("tag_id_with_spaces")
})
it("trims input when creating a deep link path", function() {
let result = createDeepLinkPath(" spaces before and after ")
expect(result).toEqual("spaces_before_and_after")
result = createDeepLinkPath(" ")
expect(result).toEqual("")
})
it("creates a deep link path with special characters", function() {
const result = createDeepLinkPath("!@#$%^&*(){}[]")
expect(result).toEqual("!@#$%^&*(){}[]")
})
it("returns an empty string for invalid input", function() {
expect( createDeepLinkPath(null) ).toEqual("")
expect( createDeepLinkPath(undefined) ).toEqual("")
expect( createDeepLinkPath(1) ).toEqual("")
expect( createDeepLinkPath([]) ).toEqual("")
expect( createDeepLinkPath({}) ).toEqual("")
})
})
describe("escapeDeepLinkPath", function() {
it("creates and escapes a deep link path", function() {
const result = escapeDeepLinkPath("tag id with spaces?")
expect(result).toEqual("tag_id_with_spaces\\?")
})
it("escapes a deep link path that starts with a number", function() {
const result = escapeDeepLinkPath("123")
expect(result).toEqual("\\31 23")
})
it("escapes a deep link path with a class selector", function() {
const result = escapeDeepLinkPath("hello.world")
expect(result).toEqual("hello\\.world")
})
it("escapes a deep link path with an id selector", function() {
const result = escapeDeepLinkPath("hello#world")
expect(result).toEqual("hello\\#world")
})
})
}) })