Merge branch 'master' into issue-1334
* master: (50 commits) update NoErrorsPlugin to NoEmitOnErrorsPlugin Undo change to `swagger-client` dependency trigger setScheme when a new set of schemes come in fix eslint issues pin yams version update dist revert dependencies minify bundle css migrate webpack to v2.6.1 #3110 - Fix models down arrow icon in firefox Fixes #3299 - export validateNumber and validateInteger for easy reuse and testing. Broke validateParam required check onto multiple lines. Added tests for validateNumber, validateInteger, and validateParam Improve README with more information about `urls` Rename variable: "selectedName" -> "primaryName" "name" -> "urls.primaryName" Update selectedIndex when a new URL is loaded Use select value to avoid react warning Properly added name config Now access it through getConfigs Documented it in README Add displayRequestDuration configuration option. Fix for #2947 - Display property names for non-object models #3256 - Remove unnecessary JSON.stringify call on example values that are already strings ... # Conflicts: # dist/swagger-ui-bundle.js # dist/swagger-ui-bundle.js.map # dist/swagger-ui-standalone-preset.js # dist/swagger-ui-standalone-preset.js.map # dist/swagger-ui.css # dist/swagger-ui.css.map # dist/swagger-ui.js # dist/swagger-ui.js.map # make-webpack-config.js # package.json # webpack-dist.config.js # webpack-hot-dev-server.config.js
This commit is contained in:
1
.babelrc
1
.babelrc
@@ -5,6 +5,7 @@
|
||||
"stage-0"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-runtime",
|
||||
[
|
||||
"module-alias",
|
||||
[
|
||||
|
||||
13
README.md
13
README.md
@@ -6,6 +6,8 @@
|
||||
|
||||
**This is the new version of swagger-ui, 3.x. Want to learn more? Check out our [FAQ](http://swagger.io/new-ui-faq/).**
|
||||
|
||||
**👉🏼 Want to score an easy open-source contribution?** Check out our [Good first contribution](https://github.com/swagger-api/swagger-ui/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+first+contribution%22) label.
|
||||
|
||||
As a brand new version, written from the ground up, there are some known issues and unimplemented features. Check out the [Known Issues](#known-issues) section for more details.
|
||||
|
||||
This repo publishes to two different NPM packages:
|
||||
@@ -20,7 +22,7 @@ The OpenAPI Specification has undergone 4 revisions since initial creation in 20
|
||||
|
||||
Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes | Status
|
||||
------------------ | ------------ | -------------------------- | ----- | ------
|
||||
3.0.16 | 2017-06-17 | 2.0 | [tag v3.0.16](https://github.com/swagger-api/swagger-ui/tree/v3.0.16) |
|
||||
3.0.17 | 2017-06-23 | 2.0 | [tag v3.0.17](https://github.com/swagger-api/swagger-ui/tree/v3.0.17) |
|
||||
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.0.24 | 2014-09-12 | 1.1, 1.2 | [tag v2.0.24](https://github.com/swagger-api/swagger-ui/tree/v2.0.24) |
|
||||
@@ -131,9 +133,13 @@ If you'd like to use the bundle files via npm, check out the [`swagger-ui-dist`
|
||||
|
||||
#### 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
|
||||
--- | ---
|
||||
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.
|
||||
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.
|
||||
@@ -144,11 +150,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
|
||||
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`.
|
||||
displayRequestDuration | Controls the display of the request duration (in milliseconds) for `Try it out` requests. The default is `false`.
|
||||
|
||||
### Plugins
|
||||
|
||||
#### 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 = [
|
||||
|
||||
1
dist/index.html
vendored
1
dist/index.html
vendored
@@ -71,6 +71,7 @@
|
||||
<script src="./swagger-ui-standalone-preset.js"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
|
||||
// Build a system
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "http://petstore.swagger.io/v2/swagger.json",
|
||||
|
||||
@@ -73,7 +73,7 @@ module.exports = function(rules, options) {
|
||||
})
|
||||
)
|
||||
|
||||
plugins.push( new webpack.NoErrorsPlugin())
|
||||
plugins.push( new webpack.NoEmitOnErrorsPlugin())
|
||||
|
||||
} else { // development mode
|
||||
var spec
|
||||
@@ -141,7 +141,7 @@ module.exports = function(rules, options) {
|
||||
],
|
||||
extensions: [".web.js", ".js", ".jsx", ".json", ".less"],
|
||||
alias: {
|
||||
base: "getbase/src/less/base"
|
||||
base: "getbase/src/less/base",
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "swagger-ui",
|
||||
"version": "3.0.16",
|
||||
"version": "3.0.17",
|
||||
"main": "dist/swagger-ui.js",
|
||||
"repository": "git@github.com:swagger-api/swagger-ui.git",
|
||||
"contributors": [
|
||||
@@ -38,7 +38,6 @@
|
||||
"dev:e2e": "TEST_SPEC_NAME=1.json webpack-dev-server --host 0.0.0.0 --config webpack-hot-dev-server.config.js --inline --hot --progress --content-base dev-helpers/"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"base64-js": "^1.2.0",
|
||||
"brace": "0.10.0",
|
||||
"deep-extend": "0.4.1",
|
||||
@@ -52,6 +51,7 @@
|
||||
"matcher": "^1.0.0",
|
||||
"memoizee": "0.4.5",
|
||||
"promise-worker": "^1.1.1",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-perf": "15.4.2",
|
||||
"react-addons-shallow-compare": "15.6.0",
|
||||
@@ -71,14 +71,14 @@
|
||||
"redux-logger": "*",
|
||||
"reselect": "3.0.1",
|
||||
"sanitize-html": "^1.14.1",
|
||||
"serialize-error": "2.1.0",
|
||||
"shallowequal": "1.0.1",
|
||||
"swagger-client": "~3.0.15",
|
||||
"serialize-error": "2.0.0",
|
||||
"shallowequal": "0.2.2",
|
||||
"swagger-client": "3.0.16",
|
||||
"url-parse": "^1.1.8",
|
||||
"whatwg-fetch": "2.0.3",
|
||||
"worker-loader": "^0.8.0",
|
||||
"xml": "1.0.1",
|
||||
"yaml-js": "^0.2.0"
|
||||
"yaml-js": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "7.1.1",
|
||||
@@ -86,6 +86,7 @@
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^7.1.0",
|
||||
"babel-plugin-module-alias": "^1.6.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-es2015": "^6.22.0",
|
||||
"babel-preset-es2015-ie": "^6.6.2",
|
||||
"babel-preset-react": "^6.23.0",
|
||||
@@ -139,7 +140,7 @@
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"IE 10"
|
||||
"IE 11"
|
||||
],
|
||||
"optionalDependencies": {
|
||||
"webpack-dev-server": "2.5.0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
@@ -6,7 +7,7 @@ export default class App extends React.Component {
|
||||
let { getComponent, layoutSelectors } = this.props
|
||||
const layoutName = layoutSelectors.current()
|
||||
const Component = getComponent(layoutName, true)
|
||||
return Component ? Component : ()=> <h1> No layout defined for "{layoutName}" </h1>
|
||||
return Component ? Component : ()=> <h1> No layout defined for "{layoutName}" </h1>
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class ApiKeyAuth extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class AuthorizationPopup extends React.Component {
|
||||
close =() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class AuthorizeBtn extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
export default class AuthorizeOperationBtn extends React.Component {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
export default class Auths extends React.Component {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
export default class BasicAuth extends React.Component {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class AuthError extends React.Component {
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import oauth2Authorize from "core/oauth2-authorize"
|
||||
|
||||
const IMPLICIT = "implicit"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class Clear extends Component {
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import { fromJS } from "immutable"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import curlify from "core/curlify"
|
||||
|
||||
export default class Curl extends React.Component {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Collapse from "react-collapse"
|
||||
import { presets } from "react-motion"
|
||||
import ObjectInspector from "react-object-inspector"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List } from "immutable"
|
||||
import Collapse from "react-collapse"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class Execute extends Component {
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Im from "immutable"
|
||||
|
||||
export default class Headers extends React.Component {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { highlight } from "core/utils"
|
||||
|
||||
export default class HighlightCode extends Component {
|
||||
@@ -8,17 +9,21 @@ export default class HighlightCode extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
highlight(this.refs.el)
|
||||
highlight(this.el)
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
highlight(this.refs.el)
|
||||
highlight(this.el)
|
||||
}
|
||||
|
||||
initializeComponent = (c) => {
|
||||
this.el = c
|
||||
}
|
||||
|
||||
render () {
|
||||
let { value, className } = this.props
|
||||
className = className || ""
|
||||
|
||||
return <pre ref="el" className={className + " microlight"}>{ value }</pre>
|
||||
return <pre ref={this.initializeComponent} className={className + " microlight"}>{ value }</pre>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { fromJS } from "immutable"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import OriCollapse from "react-collapse"
|
||||
|
||||
function xclass(...args) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class BaseLayout extends React.Component {
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class XPane extends React.Component {
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
|
||||
const Headers = ( { headers } )=>{
|
||||
@@ -8,28 +9,40 @@ const Headers = ( { headers } )=>{
|
||||
<pre>{headers}</pre>
|
||||
</div>)
|
||||
}
|
||||
|
||||
Headers.propTypes = {
|
||||
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 {
|
||||
static propTypes = {
|
||||
response: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
displayRequestDuration: PropTypes.bool.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
const { request, response, getComponent } = this.props
|
||||
const { request, response, getComponent, displayRequestDuration } = this.props
|
||||
|
||||
const status = response.get("status")
|
||||
const url = response.get("url")
|
||||
const headers = response.get("headers").toJS()
|
||||
const notDocumented = response.get("notDocumented")
|
||||
const isError = response.get("error")
|
||||
|
||||
const body = isError ? response.get("response").get("text") : response.get("text")
|
||||
|
||||
const body = response.get("text")
|
||||
const duration = response.get("duration")
|
||||
const headersKeys = Object.keys(headers)
|
||||
const contentType = headers["content-type"]
|
||||
|
||||
@@ -80,6 +93,9 @@ export default class LiveResponse extends React.Component {
|
||||
{
|
||||
hasHeaders ? <Headers headers={ returnObject }/> : null
|
||||
}
|
||||
{
|
||||
displayRequestDuration && duration ? <Duration duration={ duration } /> : null
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class ModelExample extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import { List } from "immutable"
|
||||
const braceOpen = "{"
|
||||
@@ -117,12 +118,13 @@ class ObjectModel extends Component {
|
||||
class Primitive extends Component {
|
||||
static propTypes = {
|
||||
schema: PropTypes.object.isRequired,
|
||||
name: PropTypes.string,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
required: PropTypes.bool
|
||||
}
|
||||
|
||||
render(){
|
||||
let { schema, getComponent, required } = this.props
|
||||
let { schema, getComponent, name, required } = this.props
|
||||
|
||||
if(!schema || !schema.get) {
|
||||
// don't render if schema isn't correctly formed
|
||||
@@ -133,12 +135,18 @@ class Primitive extends Component {
|
||||
let format = schema.get("format")
|
||||
let xml = schema.get("xml")
|
||||
let enumArray = schema.get("enum")
|
||||
let title = schema.get("title") || name
|
||||
let description = schema.get("description")
|
||||
let properties = schema.filter( ( v, key) => ["enum", "type", "format", "description", "$$ref"].indexOf(key) === -1 )
|
||||
let style = required ? { fontWeight: "bold" } : {}
|
||||
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>}
|
||||
{ format && <span className="prop-format">(${format})</span>}
|
||||
{
|
||||
@@ -176,17 +184,20 @@ class ArrayModel extends Component {
|
||||
}
|
||||
|
||||
render(){
|
||||
let { required, schema, depth, expandDepth } = this.props
|
||||
let { required, schema, depth, name, expandDepth } = this.props
|
||||
let items = schema.get("items")
|
||||
let title = schema.get("title") || name
|
||||
let properties = schema.filter( ( v, key) => ["type", "items", "$$ref"].indexOf(key) === -1 )
|
||||
|
||||
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>
|
||||
}
|
||||
<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>
|
||||
@@ -249,13 +260,13 @@ class Model extends Component {
|
||||
name={ name || modelName }
|
||||
isRef={ isRef!== undefined ? isRef : !!$$ref }/>
|
||||
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 "number":
|
||||
case "integer":
|
||||
case "boolean":
|
||||
default:
|
||||
return <Primitive getComponent={ getComponent } schema={ modelSchema } required={ required }/>
|
||||
return <Primitive { ...this.props } getComponent={ getComponent } schema={ modelSchema } name={ name || modelName } required={ required }/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class Models extends Component {
|
||||
static propTypes = {
|
||||
@@ -24,8 +24,8 @@ export default class Models extends Component {
|
||||
return <section className={ showModels ? "models is-open" : "models"}>
|
||||
<h4 onClick={() => layoutActions.show("models", !showModels)}>
|
||||
<span>Models</span>
|
||||
<svg width="20" height="20">
|
||||
<use xlinkHref="#large-arrow" />
|
||||
<svg className="arrow" width="20" height="20">
|
||||
<use xlinkHref={showModels ? "#large-arrow-down" : "#large-arrow"} />
|
||||
</svg>
|
||||
</h4>
|
||||
<Collapse isOpened={showModels} animated>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class OnlineValidatorBadge extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import shallowCompare from "react-addons-shallow-compare"
|
||||
import React, { PureComponent } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { getList } from "core/utils"
|
||||
import * as CustomPropTypes from "core/proptypes"
|
||||
|
||||
//import "less/opblock"
|
||||
|
||||
export default class Operation extends React.Component {
|
||||
export default class Operation extends PureComponent {
|
||||
static propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
method: PropTypes.string.isRequired,
|
||||
@@ -18,6 +18,7 @@ export default class Operation extends React.Component {
|
||||
allowTryItOut: PropTypes.bool,
|
||||
|
||||
displayOperationId: PropTypes.bool,
|
||||
displayRequestDuration: PropTypes.bool,
|
||||
|
||||
response: PropTypes.object,
|
||||
request: PropTypes.object,
|
||||
@@ -38,6 +39,7 @@ export default class Operation extends React.Component {
|
||||
response: null,
|
||||
allowTryItOut: true,
|
||||
displayOperationId: false,
|
||||
displayRequestDuration: false
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
@@ -70,10 +72,6 @@ export default class Operation extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(props, state) {
|
||||
return shallowCompare(this, props, state)
|
||||
}
|
||||
|
||||
toggleShown =() => {
|
||||
let { layoutActions, isShownKey } = this.props
|
||||
layoutActions.show(isShownKey, !this.isShown())
|
||||
@@ -112,7 +110,7 @@ export default class Operation extends React.Component {
|
||||
request,
|
||||
allowTryItOut,
|
||||
displayOperationId,
|
||||
|
||||
displayRequestDuration,
|
||||
fn,
|
||||
getComponent,
|
||||
specActions,
|
||||
@@ -131,6 +129,7 @@ export default class Operation extends React.Component {
|
||||
let schemes = operation.get("schemes")
|
||||
let parameters = getList(operation, ["parameters"])
|
||||
let operationId = operation.get("__originalOperationId")
|
||||
let operationScheme = specSelectors.operationScheme(path, method)
|
||||
|
||||
const Responses = getComponent("responses")
|
||||
const Parameters = getComponent( "parameters" )
|
||||
@@ -216,7 +215,8 @@ export default class Operation extends React.Component {
|
||||
<Schemes schemes={ schemes }
|
||||
path={ path }
|
||||
method={ method }
|
||||
specActions={ specActions }/>
|
||||
specActions={ specActions }
|
||||
operationScheme={ operationScheme } />
|
||||
</div> : null
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ export default class Operation extends React.Component {
|
||||
produces={ produces }
|
||||
producesValue={ operation.get("produces_value") }
|
||||
pathMethod={ [path, method] }
|
||||
displayRequestDuration={ displayRequestDuration }
|
||||
fn={fn} />
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class Operations extends React.Component {
|
||||
|
||||
@@ -32,7 +33,7 @@ export default class Operations extends React.Component {
|
||||
const Collapse = getComponent("Collapse")
|
||||
|
||||
let showSummary = layoutSelectors.showSummary()
|
||||
let { docExpansion, displayOperationId } = getConfigs()
|
||||
let { docExpansion, displayOperationId, displayRequestDuration } = getConfigs()
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -87,6 +88,7 @@ export default class Operations extends React.Component {
|
||||
allowTryItOut={allowTryItOut}
|
||||
|
||||
displayOperationId={displayOperationId}
|
||||
displayRequestDuration={displayRequestDuration}
|
||||
|
||||
specActions={ specActions }
|
||||
specSelectors={ specSelectors }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Link } from "core/components/layout-utils"
|
||||
|
||||
export default class Overview extends React.Component {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import shallowCompare from "react-addons-shallow-compare"
|
||||
import React, { PureComponent } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { fromJS, List } from "immutable"
|
||||
import { getSampleSchema } from "core/utils"
|
||||
|
||||
const NOOP = Function.prototype
|
||||
|
||||
export default class ParamBody extends Component {
|
||||
export default class ParamBody extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
param: PropTypes.object,
|
||||
@@ -41,10 +41,6 @@ export default class ParamBody extends Component {
|
||||
this.updateValues.call(this, this.props)
|
||||
}
|
||||
|
||||
shouldComponentUpdate(props, state) {
|
||||
return shallowCompare(this, props, state)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.updateValues.call(this, nextProps)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import win from "core/window"
|
||||
|
||||
|
||||
export default class ParameterRow extends Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Component, PropTypes } from "react"
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import ImPropTypes from "react-immutable-proptypes"
|
||||
import Im from "immutable"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import Remarkable from "react-remarkable"
|
||||
import sanitize from "sanitize-html"
|
||||
|
||||
@@ -11,6 +12,12 @@ const sanitizeOptions = {
|
||||
|
||||
function Markdown({ source }) {
|
||||
const sanitized = sanitize(source, sanitizeOptions)
|
||||
|
||||
// sometimes the sanitizer returns "undefined" as a string
|
||||
if(!source || !sanitized || sanitized === "undefined") {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Remarkable
|
||||
options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}}
|
||||
source={sanitized}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { formatXml } from "core/utils"
|
||||
import lowerCase from "lodash/lowerCase"
|
||||
|
||||
@@ -6,7 +7,7 @@ export default class ResponseBody extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
content: PropTypes.any.isRequired,
|
||||
contentType: PropTypes.string.isRequired,
|
||||
contentType: PropTypes.string,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
headers: PropTypes.object,
|
||||
url: PropTypes.string
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { fromJS } from "immutable"
|
||||
import { getSampleSchema } from "core/utils"
|
||||
|
||||
const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => {
|
||||
if ( examples && examples.size ) {
|
||||
return examples.entrySeq().map( ([ key, example ]) => {
|
||||
let exampleValue
|
||||
let exampleValue = example
|
||||
if ( example.toJS ) {
|
||||
try {
|
||||
exampleValue = example && example.toJS ? example.toJS() : example
|
||||
exampleValue = JSON.stringify(exampleValue, null, 2)
|
||||
exampleValue = JSON.stringify(example.toJS(), null, 2)
|
||||
}
|
||||
catch(e) {
|
||||
exampleValue = String(example)
|
||||
}
|
||||
}
|
||||
|
||||
return (<div key={ key }>
|
||||
<h5>{ key }</h5>
|
||||
<HighlightCode className="example" value={ exampleValue } />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { fromJS } from "immutable"
|
||||
import { defaultStatusCode } from "core/utils"
|
||||
|
||||
@@ -14,19 +15,21 @@ export default class Responses extends React.Component {
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
specActions: PropTypes.object.isRequired,
|
||||
pathMethod: PropTypes.array.isRequired,
|
||||
displayRequestDuration: PropTypes.bool.isRequired,
|
||||
fn: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
request: null,
|
||||
tryItOutResponse: null,
|
||||
produces: fromJS(["application/json"])
|
||||
produces: fromJS(["application/json"]),
|
||||
displayRequestDuration: false
|
||||
}
|
||||
|
||||
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val)
|
||||
|
||||
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 )
|
||||
|
||||
const ContentType = getComponent( "contentType" )
|
||||
@@ -53,7 +56,8 @@ export default class Responses extends React.Component {
|
||||
: <div>
|
||||
<LiveResponse request={ request }
|
||||
response={ tryItOutResponse }
|
||||
getComponent={ getComponent } />
|
||||
getComponent={ getComponent }
|
||||
displayRequestDuration={ displayRequestDuration } />
|
||||
<h4>Responses</h4>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class Schemes extends React.Component {
|
||||
|
||||
@@ -6,7 +7,8 @@ export default class Schemes extends React.Component {
|
||||
specActions: PropTypes.object.isRequired,
|
||||
schemes: PropTypes.object.isRequired,
|
||||
path: PropTypes.string,
|
||||
method: PropTypes.string
|
||||
method: PropTypes.string,
|
||||
operationScheme: PropTypes.string
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -16,6 +18,13 @@ export default class Schemes extends React.Component {
|
||||
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 ) => {
|
||||
this.setScheme( e.target.value )
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class TryItOutButton extends React.Component {
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import ApisPreset from "core/presets/apis"
|
||||
import * as AllPlugins from "core/plugins/all"
|
||||
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",
|
||||
"showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" ]
|
||||
"showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" , "displayRequestDuration"]
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo
|
||||
@@ -23,12 +23,14 @@ module.exports = function SwaggerUI(opts) {
|
||||
dom_id: null,
|
||||
spec: {},
|
||||
url: "",
|
||||
urls: null,
|
||||
layout: "BaseLayout",
|
||||
docExpansion: "list",
|
||||
validatorUrl: "https://online.swagger.io/validator",
|
||||
configs: {},
|
||||
custom: {},
|
||||
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.
|
||||
// Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest.
|
||||
@@ -117,6 +119,7 @@ module.exports = function SwaggerUI(opts) {
|
||||
return downloadSpec()
|
||||
}
|
||||
|
||||
return system
|
||||
}
|
||||
|
||||
// Add presets
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { PropTypes, Component } from "react"
|
||||
import shallowCompare from "react-addons-shallow-compare"
|
||||
import React, { PureComponent, Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { List, fromJS } from "immutable"
|
||||
//import "less/json-schema-form"
|
||||
|
||||
@@ -74,7 +74,7 @@ export class JsonSchema_string extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonSchema_array extends Component {
|
||||
export class JsonSchema_array extends PureComponent {
|
||||
|
||||
static propTypes = JsonSchemaPropShape
|
||||
static defaultProps = JsonSchemaDefaultProps
|
||||
@@ -89,10 +89,6 @@ export class JsonSchema_array extends Component {
|
||||
this.setState({value: props.value})
|
||||
}
|
||||
|
||||
shouldComponentUpdate(props, state) {
|
||||
return shallowCompare(this, props, state)
|
||||
}
|
||||
|
||||
onChange = () => this.props.onChange(this.state.value)
|
||||
|
||||
onItemChange = (itemVal, i) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const primitives = {
|
||||
"number": () => 0,
|
||||
"number_float": () => 0.0,
|
||||
"integer": () => 0,
|
||||
"boolean": () => true
|
||||
"boolean": (schema) => typeof schema.default === "boolean" ? schema.default : true
|
||||
}
|
||||
|
||||
const primitive = (schema) => {
|
||||
@@ -74,6 +74,10 @@ export const sampleFromSchema = (schema, config={}) => {
|
||||
return normalizeArray(schema["enum"])[0]
|
||||
}
|
||||
|
||||
if (type === "file") {
|
||||
return
|
||||
}
|
||||
|
||||
return primitive(schema)
|
||||
}
|
||||
|
||||
|
||||
@@ -207,8 +207,14 @@ export const executeRequest = (req) => ({fn, specActions, specSelectors}) => {
|
||||
|
||||
specActions.setRequest(req.pathName, req.method, parsedRequest)
|
||||
|
||||
// track duration of request
|
||||
const startTime = Date.now()
|
||||
|
||||
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) } ) )
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
[UPDATE_PARAM]: ( state, {payload} ) => {
|
||||
let { path, paramName, value, isXml } = payload
|
||||
return state.updateIn( [ "resolved", "paths", ...path, "parameters" ], fromJS([]), parameters => {
|
||||
let index = parameters.findIndex( p => p.get( "name" ) === paramName )
|
||||
const index = parameters.findIndex(p => p.get( "name" ) === paramName )
|
||||
if (!(value instanceof win.File)) {
|
||||
value = fromJSOrdered( value )
|
||||
}
|
||||
@@ -75,7 +75,12 @@ export default {
|
||||
[SET_RESPONSE]: (state, { payload: { res, path, method } } ) =>{
|
||||
let result
|
||||
if ( res.error ) {
|
||||
result = Object.assign({error: true}, res.err)
|
||||
result = Object.assign({
|
||||
error: true,
|
||||
name: res.err.name,
|
||||
message: res.err.message,
|
||||
statusCode: res.err.statusCode
|
||||
}, res.err.response)
|
||||
} else {
|
||||
result = res
|
||||
}
|
||||
@@ -86,7 +91,7 @@ export default {
|
||||
let newState = state.setIn( [ "responses", path, method ], fromJSOrdered(result) )
|
||||
|
||||
// ImmutableJS messes up Blob. Needs to reset its value.
|
||||
if (res.data instanceof win.Blob) {
|
||||
if (win.Blob && res.data instanceof win.Blob) {
|
||||
newState = newState.setIn( [ "responses", path, method, "text" ], res.data)
|
||||
}
|
||||
return newState
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import SplitPane from "react-split-pane"
|
||||
import "./split-pane-mode.less"
|
||||
|
||||
@@ -23,9 +24,13 @@ export default class SplitPaneMode extends React.Component {
|
||||
children: [],
|
||||
};
|
||||
|
||||
initializeComponent = (c) => {
|
||||
this.splitPane = c
|
||||
}
|
||||
|
||||
onDragFinished = () => {
|
||||
let { threshold, layoutActions } = this.props
|
||||
let { position, draggedSize } = this.refs.splitPane.state
|
||||
let { position, draggedSize } = this.splitPane.state
|
||||
this.draggedSize = draggedSize
|
||||
|
||||
let nearLeftEdge = position <= threshold
|
||||
@@ -62,7 +67,7 @@ export default class SplitPaneMode extends React.Component {
|
||||
return (
|
||||
<SplitPane
|
||||
disabledClass={""}
|
||||
ref={"splitPane"}
|
||||
ref={this.initializeComponent}
|
||||
split='vertical'
|
||||
defaultSize={"50%"}
|
||||
primary="second"
|
||||
|
||||
@@ -65,11 +65,11 @@ export const render = (getSystem, getStore, getComponent, getComponents, dom) =>
|
||||
}
|
||||
|
||||
// Render try/catch wrapper
|
||||
const createClass = component => React.createClass({
|
||||
const createClass = component => class extends Component {
|
||||
render() {
|
||||
return component(this.props)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const Fallback = ({ name }) => <div style={{ // eslint-disable-line react/prop-types
|
||||
padding: "1em",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PropTypes } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
// Takes a list and proptype, and returns a PropType.shape({ [item]: propType })
|
||||
const mapListToPropTypeShape = (list, propType) => PropTypes.shape(
|
||||
|
||||
@@ -240,7 +240,7 @@ export default class Store {
|
||||
action = {type: NEW_THROWN_ERR, error: true, payload: serializeError(e) }
|
||||
}
|
||||
finally{
|
||||
return action
|
||||
return action // eslint-disable-line no-unsafe-finally
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Im from "immutable"
|
||||
import shallowEqual from "shallowequal"
|
||||
|
||||
import camelCase from "lodash/camelCase"
|
||||
import upperFirst from "lodash/upperFirst"
|
||||
import _memoize from "lodash/memoize"
|
||||
import find from "lodash/find"
|
||||
import some from "lodash/some"
|
||||
import eq from "lodash/eq"
|
||||
import { memoizedSampleFromSchema, memoizedCreateXMLExample } from "core/plugins/samples/fn"
|
||||
@@ -343,7 +343,7 @@ export function highlight (el) {
|
||||
while (![
|
||||
1, // 0: whitespace
|
||||
// 1: operator or braces
|
||||
/[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr),
|
||||
/[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr), // eslint-disable-line no-useless-escape
|
||||
/[\])]/[test](chr), // 2: closing brace
|
||||
/[$\w]/[test](chr), // 3: (key)word
|
||||
chr == "/" && // 4: regex
|
||||
@@ -418,11 +418,6 @@ export function pascalCaseFilename(filename) {
|
||||
return pascalCase(filename.replace(/\.[^./]*$/, ""))
|
||||
}
|
||||
|
||||
// Only compare a set of props
|
||||
export function shallowEqualKeys(a,b, keys) {
|
||||
return !!keys.find(key => !shallowEqual(a[key], b[key]))
|
||||
}
|
||||
|
||||
// Check if ...
|
||||
// - new props
|
||||
// - If immutable, use .is()
|
||||
@@ -455,15 +450,15 @@ export const propChecker = (props, nextProps, objectList=[], ignoreList=[]) => {
|
||||
|| objectList.some( objectPropName => !eq(props[objectPropName], nextProps[objectPropName])))
|
||||
}
|
||||
|
||||
const validateNumber = ( val ) => {
|
||||
if ( !/^-?\d+(.?\d+)?$/.test(val)) {
|
||||
export const validateNumber = ( val ) => {
|
||||
if ( !/^-?\d+(\.?\d+)?$/.test(val)) {
|
||||
return "Value must be a number"
|
||||
}
|
||||
}
|
||||
|
||||
const validateInteger = ( val ) => {
|
||||
export const validateInteger = ( val ) => {
|
||||
if ( !/^-?\d+$/.test(val)) {
|
||||
return "Value must be integer"
|
||||
return "Value must be an integer"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,13 +469,14 @@ export const validateParam = (param, isXml) => {
|
||||
let required = param.get("required")
|
||||
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")
|
||||
return errors
|
||||
}
|
||||
|
||||
if ( !value ) return errors
|
||||
|
||||
if ( type === "number" ) {
|
||||
let err = validateNumber(value)
|
||||
if (!err) return errors
|
||||
@@ -593,3 +589,10 @@ export const filterConfigs = (configs, allowed) => {
|
||||
|
||||
return filteredConfigs
|
||||
}
|
||||
|
||||
// Is this really required as a helper? Perhaps. TODO: expose the system of presets.apis in docs, so we know what is supported
|
||||
export const shallowEqualKeys = (a,b, keys) => {
|
||||
return !!find(keys, (key) => {
|
||||
return eq(a[key], b[key])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
//import "./topbar.less"
|
||||
import Logo from "./logo_small.png"
|
||||
@@ -7,7 +8,7 @@ export default class Topbar extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { url: props.specSelectors.url() }
|
||||
this.state = { url: props.specSelectors.url(), selectedIndex: 0 }
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -19,14 +20,68 @@ export default class Topbar extends React.Component {
|
||||
this.setState({url: value})
|
||||
}
|
||||
|
||||
downloadUrl = (e) => {
|
||||
this.props.specActions.updateUrl(this.state.url)
|
||||
this.props.specActions.download(this.state.url)
|
||||
loadSpec = (url) => {
|
||||
this.props.specActions.updateUrl(url)
|
||||
this.props.specActions.download(url)
|
||||
}
|
||||
|
||||
onUrlSelect =(e)=> {
|
||||
let url = e.target.value || e.target.href
|
||||
this.loadSpec(url)
|
||||
this.setSelectedUrl(url)
|
||||
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() {
|
||||
let { getComponent, specSelectors } = this.props
|
||||
let { getComponent, specSelectors, getConfigs } = this.props
|
||||
const Button = getComponent("Button")
|
||||
const Link = getComponent("Link")
|
||||
|
||||
@@ -36,6 +91,31 @@ export default class Topbar extends React.Component {
|
||||
let inputStyle = {}
|
||||
if(isFailed) inputStyle.color = "red"
|
||||
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 (
|
||||
<div className="topbar">
|
||||
<div className="wrapper">
|
||||
@@ -44,14 +124,12 @@ export default class Topbar extends React.Component {
|
||||
<img height="30" width="30" src={ Logo } alt="Swagger UX"/>
|
||||
<span>swagger</span>
|
||||
</Link>
|
||||
<form className="download-url-wrapper" onSubmit={this.downloadUrl}>
|
||||
<input className="download-url-input" type="text" onChange={ this.onUrlChange } value={this.state.url} disabled={isLoading} style={inputStyle} />
|
||||
<Button className="download-url-button" onClick={ this.downloadUrl }>Explore</Button>
|
||||
<form className="download-url-wrapper" onSubmit={formOnSubmit}>
|
||||
{control}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -59,5 +137,6 @@ export default class Topbar extends React.Component {
|
||||
Topbar.propTypes = {
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
specActions: PropTypes.object.isRequired,
|
||||
getComponent: PropTypes.func.isRequired
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
getConfigs: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
.swagger-ui {
|
||||
|
||||
.topbar {
|
||||
background-color: #89bf04;
|
||||
}
|
||||
|
||||
.topbar-wrapper {
|
||||
padding: 0.7em
|
||||
padding: 0.7em;
|
||||
}
|
||||
|
||||
.topbar-logo__img {
|
||||
|
||||
2
src/polyfills.js
Normal file
2
src/polyfills.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// Promise global, Used ( at least ) by 'whatwg-fetch'. And required by IE 11
|
||||
require("core-js/fn/promise")
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from "react"
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
export default class StandaloneLayout extends React.Component {
|
||||
|
||||
|
||||
@@ -95,14 +95,7 @@ section.models
|
||||
h4
|
||||
{
|
||||
margin: 0 0 5px 0;
|
||||
|
||||
border-bottom: 1px solid rgba(#3b4151, .3);
|
||||
|
||||
|
||||
svg
|
||||
{
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
h4
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
.topbar-wrapper
|
||||
{
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
a
|
||||
@@ -15,13 +14,13 @@
|
||||
font-weight: bold;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
max-width: 300px;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
@include text_headline(#fff);
|
||||
|
||||
span
|
||||
@@ -34,8 +33,8 @@
|
||||
.download-url-wrapper
|
||||
{
|
||||
display: flex;
|
||||
|
||||
flex: 3;
|
||||
justify-content: flex-end;
|
||||
|
||||
input[type=text]
|
||||
{
|
||||
@@ -48,6 +47,38 @@
|
||||
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
|
||||
{
|
||||
font-size: 16px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @return {string} When run in NodeJS env, returns the absolute path to the current directory
|
||||
* When run outside of NodeJS, will return an error message
|
||||
*/
|
||||
const getAbsoluteFSPath = () => {
|
||||
const getAbsoluteFSPath = function () {
|
||||
// detect whether we are running in a browser or nodejs
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
return require("path").resolve(__dirname)
|
||||
|
||||
37
test/bugs/3279-empty-markdown-source.js
Normal file
37
test/bugs/3279-empty-markdown-source.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/* eslint-env mocha */
|
||||
import React from "react"
|
||||
import expect from "expect"
|
||||
import { render } from "enzyme"
|
||||
import Markdown from "components/providers/markdown"
|
||||
|
||||
describe("UI-3279: Empty Markdown inputs causing bare `undefined` in output", function(){
|
||||
it("should return no text for `null` as source input", function(){
|
||||
let props = {
|
||||
source: null
|
||||
}
|
||||
|
||||
let el = render(<Markdown {...props}/>)
|
||||
|
||||
expect(el.text()).toEqual("")
|
||||
})
|
||||
|
||||
it("should return no text for `undefined` as source input", function(){
|
||||
let props = {
|
||||
source: undefined
|
||||
}
|
||||
|
||||
let el = render(<Markdown {...props}/>)
|
||||
|
||||
expect(el.text()).toEqual("")
|
||||
})
|
||||
|
||||
it("should return no text for empty string as source input", function(){
|
||||
let props = {
|
||||
source: ""
|
||||
}
|
||||
|
||||
let el = render(<Markdown {...props}/>)
|
||||
|
||||
expect(el.text()).toEqual("")
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import expect from "expect"
|
||||
import { fromJS } from "immutable"
|
||||
import { transform } from "corePlugins/err/error-transformers/transformers/parameter-oneof"
|
||||
|
||||
@@ -69,4 +69,56 @@ describe("spec plugin - reducer", function(){
|
||||
expect(result.toJS()).toEqual(state.toJS())
|
||||
})
|
||||
})
|
||||
|
||||
describe("set response value", function() {
|
||||
it("should combine the response and error objects", () => {
|
||||
const setResponse = reducer["spec_set_response"]
|
||||
|
||||
const path = "/pet/post"
|
||||
const method = "POST"
|
||||
|
||||
const state = fromJS({})
|
||||
const result = setResponse(state, {
|
||||
payload: {
|
||||
path: path,
|
||||
method: method,
|
||||
res: {
|
||||
error: true,
|
||||
err: {
|
||||
message: "Not Found",
|
||||
name: "Error",
|
||||
response: {
|
||||
data: "response data",
|
||||
headers: {
|
||||
key: "value"
|
||||
},
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: "Not Found"
|
||||
},
|
||||
status: 404,
|
||||
statusCode: 404
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let expectedResult = {
|
||||
error: true,
|
||||
message: "Not Found",
|
||||
name: "Error",
|
||||
data: "response data",
|
||||
headers: {
|
||||
key: "value"
|
||||
},
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusCode: 404,
|
||||
statusText: "Not Found"
|
||||
}
|
||||
|
||||
const response = result.getIn(["responses", path, method]).toJS()
|
||||
expect(response).toEqual(expectedResult)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-env mocha */
|
||||
import expect from "expect"
|
||||
import { fromJS } from "immutable"
|
||||
import { mapToList } from "core/utils"
|
||||
import { mapToList, validateNumber, validateInteger, validateParam } from "core/utils"
|
||||
|
||||
describe("utils", function(){
|
||||
|
||||
@@ -67,9 +67,181 @@ describe("utils", function(){
|
||||
|
||||
// Then
|
||||
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"}] )
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ module.exports = require('./make-webpack-config.js')(rules, {
|
||||
|
||||
entry: {
|
||||
'swagger-ui-bundle': [
|
||||
'babel-polyfill',
|
||||
'./src/polyfills',
|
||||
'./src/core/index.js'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ module.exports = require('./make-webpack-config.js')(rules, {
|
||||
|
||||
entry: {
|
||||
'swagger-ui-standalone-preset': [
|
||||
'./src/polyfills',
|
||||
'./src/standalone/index.js'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var path = require('path')
|
||||
var fs = require('fs')
|
||||
var node_modules = fs.readdirSync('node_modules').filter(function(x) { return x !== '.bin' })
|
||||
const nodeModules = fs.readdirSync("node_modules").filter(function(x) { return x !== ".bin" })
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
|
||||
var rules = [
|
||||
@@ -29,7 +29,10 @@ var rules = [
|
||||
use: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: [
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: { minimize: true }
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: { sourceMap: true }
|
||||
@@ -66,9 +69,9 @@ module.exports = require('./make-webpack-config.js')(rules, {
|
||||
|
||||
entry: {
|
||||
"swagger-ui": [
|
||||
'babel-polyfill',
|
||||
'./src/style/main.scss',
|
||||
'./src/core/index.js'
|
||||
"./src/style/main.scss",
|
||||
"./src/polyfills",
|
||||
"./src/core/index.js"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -76,11 +79,11 @@ module.exports = require('./make-webpack-config.js')(rules, {
|
||||
// webpack injects some stuff into the resulting file,
|
||||
// these libs need to be pulled in to keep that working.
|
||||
var exceptionsForWebpack = ["ieee754", "base64-js"]
|
||||
if(node_modules.indexOf(request) !== -1 || exceptionsForWebpack.indexOf(request) !== -1) {
|
||||
cb(null, 'commonjs ' + request)
|
||||
return;
|
||||
if(nodeModules.indexOf(request) !== -1 || exceptionsForWebpack.indexOf(request) !== -1) {
|
||||
cb(null, "commonjs " + request)
|
||||
return
|
||||
}
|
||||
cb();
|
||||
cb()
|
||||
},
|
||||
|
||||
output: {
|
||||
|
||||
@@ -62,10 +62,11 @@ module.exports = require("./make-webpack-config")(rules, {
|
||||
devtool: "eval",
|
||||
entry: {
|
||||
'swagger-ui-bundle': [
|
||||
'babel-polyfill',
|
||||
'./src/core/index.js',
|
||||
'./src/polyfills',
|
||||
'./src/core/index.js'
|
||||
],
|
||||
'swagger-ui-standalone-preset': [
|
||||
'./src/polyfills',
|
||||
'./src/standalone/index.js',
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user