diff --git a/.gitignore b/.gitignore index fc6ab699..2e9dcf9c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ node_modules npm-debug.log* .eslintcache package-lock.json -selenium-debug.log \ No newline at end of file +*.iml diff --git a/.travis.yml b/.travis.yml index c9d09c8d..da8c805b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ deploy: email: apiteam@swagger.io skip_cleanup: true api_key: - secure: "IJkLaACa+rfERf1O5nwlqOyuo9sbul3FBhBt4Un9P+DvEet3AoDPV9NQVLd8SkmQYKGbGQWF4BIdjrO5nqFD6Te+JTeUX5Uo/DFS/fu9qw1xv0dQpvbJFuoYnnFlbzGTEs4CFa8lbu3ZromFHQGOQxRobjsG1Kf0dWFSSzmND3g=" + secure: "YKk5L1BL4oAixvLjWp+i85fNFXK85HKOlUt6QypkZkt23My5aywuYsv5VCLjjOtuWc72zbmOzP82DTBsuRswCRViXWCiNYhl42QTdvadHu0uIlM/FL6aNlvPpzXIws4bMvz1aYOTzFTnSnNuvCTzF1daW0+2ClOo3r0nLEdDfFg=" on: tags: true repo: swagger-api/swagger-ui diff --git a/README.md b/README.md index 3ad5e488..e54b6da0 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,17 @@ This repo publishes to two different NPM packages: For the older version of swagger-ui, refer to the [*2.x branch*](https://github.com/swagger-api/swagger-ui/tree/2.x). ## Compatibility -The OpenAPI Specification has undergone 4 revisions since initial creation in 2010. Compatibility between swagger-ui and the OpenAPI Specification is as follows: +The OpenAPI Specification has undergone 5 revisions since initial creation in 2010. Compatibility between swagger-ui and the OpenAPI Specification is as follows: -Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes | Status ------------------- | ------------ | -------------------------- | ----- | ------ -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) | -1.0.13 | 2013-03-08 | 1.1, 1.2 | [tag v1.0.13](https://github.com/swagger-api/swagger-ui/tree/v1.0.13) | -1.0.1 | 2011-10-11 | 1.0, 1.1 | [tag v1.0.1](https://github.com/swagger-api/swagger-ui/tree/v1.0.1) | +Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes +------------------ | ------------ | -------------------------- | ----- +3.1.2 | 2017-07-31 | 2.0, 3.0 | [tag v3.1.2](https://github.com/swagger-api/swagger-ui/tree/v3.1.2) +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.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) +1.0.13 | 2013-03-08 | 1.1, 1.2 | [tag v1.0.13](https://github.com/swagger-api/swagger-ui/tree/v1.0.13) +1.0.1 | 2011-10-11 | 1.0, 1.1 | [tag v1.0.1](https://github.com/swagger-api/swagger-ui/tree/v1.0.1) ### How to run @@ -74,7 +75,6 @@ To help with the migration, here are the currently known issues with 3.X. This l - Only part of the [parameters](#parameters) previously supported are available. - The JSON Form Editor is not implemented. -- Shebang URL support for operations is missing. - Support for `collectionFormat` is partial. - l10n (translations) is not implemented. - Relative path support for external files is not implemented. @@ -89,22 +89,23 @@ To use swagger-ui's bundles, you should take a look at the [source of swagger-ui ```javascript const ui = SwaggerUIBundle({ - url: "http://petstore.swagger.io/v2/swagger.json", - dom_id: '#swagger-ui', - presets: [ - SwaggerUIBundle.presets.apis, - SwaggerUIStandalonePreset - ], - plugins: [ - SwaggerUIBundle.plugins.DownloadUrl - ], - layout: "StandaloneLayout" - }) + url: "http://petstore.swagger.io/v2/swagger.json", + dom_id: '#swagger-ui', + presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIStandalonePreset + ], + plugins: [ + SwaggerUIBundle.plugins.DownloadUrl + ], + layout: "StandaloneLayout" + }) ``` #### OAuth2 configuration You can configure OAuth2 authorization by calling `initOAuth` method with passed configs under the instance of `SwaggerUIBundle` -default `client_id` and `client_secret`, `realm`, an application name `appName`, `scopeSeparator`, `additionalQueryStringParams`. +default `client_id` and `client_secret`, `realm`, an application name `appName`, `scopeSeparator`, `additionalQueryStringParams`, +`useBasicAuthenticationWithAccessCodeGrant`. Config Name | Description --- | --- @@ -114,6 +115,7 @@ realm | realm query parameter (for oauth1) added to `authorizationUrl` and `toke appName | application name, displayed in authorization popup. MUST be a string scopeSeparator | scope separator for passing scopes, encoded before calling, default value is a space (encoded value `%20`). MUST be a string additionalQueryStringParams | Additional query parameters added to `authorizationUrl` and `tokenUrl`. MUST be an object +useBasicAuthenticationWithAccessCodeGrant | Only activated for the `accessCode` flow. During the `authorization_code` request to the `tokenUrl`, pass the [Client Password](https://tools.ietf.org/html/rfc6749#section-2.3.1) using the HTTP Basic Authentication scheme (`Authorization` header with `Basic base64encoded[client_id:client_secret]`). The default is `false` ``` const ui = SwaggerUIBundle({...}) @@ -144,13 +146,17 @@ spec | A JSON object describing the OpenAPI Specification. When used, the `url` 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. oauth2RedirectUrl | OAuth redirect URL +tagsSorter | Apply a sort to the tag list of each API. It can be 'alpha' (sort by paths alphanumerically) or a function (see [Array.prototype.sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) to learn how to write a sort function). Two tag name strings are passed to the sorter for each pass. Default is the order determined by Swagger-UI. operationsSorter | Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically), 'method' (sort by HTTP method) or a function (see Array.prototype.sort() to know how sort function works). Default is the order returned by the server unchanged. configUrl | Configs URL parameterMacro | MUST be a function. Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter). Operation and parameter are objects passed for context, both remain immutable modelPropertyMacro | MUST be a function. Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable docExpansion | Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). The default is 'list'. 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`. +displayRequestDuration | Controls the display of the request duration (in milliseconds) for `Try it out` requests. The default is `false`. +maxDisplayedTags | If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations. +filter | If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. Can be true/false to enable or disable, or an explicit filter string in which case filtering will be enabled using that string as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag. +deepLinking | If set to `true`, enables dynamic deep linking for tags and operations. [Docs](https://github.com/swagger-api/swagger-ui/blob/master/docs/deep-linking.md) ### Plugins @@ -240,6 +246,10 @@ Access-Control-Allow-Headers: Content-Type, api_key, Authorization Only headers with these names will be allowed to be sent by Swagger-UI. +## Security contact + +Please disclose any security-related issues or vulnerabilities by emailing [security@swagger.io](mailto:security@swagger.io), instead of using the public issue tracker. + ## License Copyright 2017 SmartBear Software diff --git a/docs/deep-linking.md b/docs/deep-linking.md new file mode 100644 index 00000000..8fd45497 --- /dev/null +++ b/docs/deep-linking.md @@ -0,0 +1,36 @@ +# Deep linking + +Swagger-UI allows you to deeply link into tags and operations within a spec. When Swagger-UI is provided a URL fragment at runtime, it will automatically expand and scroll to a specified tag or operation. + +## Usage + +👉🏼 Add `deepLinking: true` to your Swagger-UI configuration to enable this functionality. + +When you expand a tag or operation, Swagger-UI will automatically update its URL fragment with a deep link to the item. +Conversely, when you collapse a tag or operation, Swagger-UI will clear the URL fragment. + +You can also right-click a tag name or operation path in order to copy a link to that tag or operation. + +#### Fragment format + +The fragment is formatted in one of two ways: + +- `#/{tagName}`, to trigger the focus of a specific tag +- `#/{tagName}/{operationId}`, to trigger the focus of a specific operation within a tag + +`operationId` is the explicit operationId provided in the spec, if one exists. +Otherwise, Swagger-UI generates an implicit operationId by combining the operation's path and method, and escaping non-alphanumeric characters. + +## FAQ + +> I'm using Swagger-UI in an application that needs control of the URL fragment. How do I disable deep-linking? + +This functionality is disabled by default, but you can pass `deepLinking: false` into Swagger-UI as a configuration item to be sure. + +> Can I link to multiple tags or operations? + +No, this is not supported. + +> Can I collapse everything except the operation or tag I'm linking to? + +Sure - use `docExpansion: none` to collapse all tags and operations. Your deep link will take precedence over the setting, so only the tag or operation you've specified will be expanded. diff --git a/make-webpack-config.js b/make-webpack-config.js index d10ff150..c2cc2268 100644 --- a/make-webpack-config.js +++ b/make-webpack-config.js @@ -1,12 +1,13 @@ -var path = require('path') +var path = require("path") var webpack = require('webpack') var ExtractTextPlugin = require('extract-text-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') var deepExtend = require('deep-extend') const {gitDescribeSync} = require('git-describe') +const os = require("os") -var pkg = require('./package.json') +var pkg = require("./package.json") let gitInfo @@ -14,7 +15,7 @@ try { gitInfo = gitDescribeSync(__dirname) } catch(e) { gitInfo = { - hash: 'noGit', + hash: "noGit", dirty: false } } @@ -22,21 +23,21 @@ try { var commonRules = [ { test: /\.(js(x)?)(\?.*)?$/, use: [{ - loader: 'babel-loader', + loader: "babel-loader", options: { retainLines: true } }], - include: [ path.join(__dirname, 'src') ] + include: [ path.join(__dirname, "src") ] }, { test: /\.(txt|yaml)(\?.*)?$/, - loader: 'raw-loader' }, + loader: "raw-loader" }, { test: /\.(png|jpg|jpeg|gif|svg)(\?.*)?$/, - loader: 'url-loader?limit=10000' }, + loader: "url-loader?limit=10000" }, { test: /\.(woff|woff2)(\?.*)?$/, - loader: 'url-loader?limit=100000' }, + loader: "url-loader?limit=100000" }, { test: /\.(ttf|eot)(\?.*)?$/, - loader: 'file-loader' } + loader: "file-loader" } ] module.exports = function(rules, options) { @@ -55,7 +56,7 @@ module.exports = function(rules, options) { if( specialOptions.separateStylesheets ) { plugins.push(new ExtractTextPlugin({ - filename: '[name].css' + (specialOptions.longTermCaching ? '?[contenthash]' : ''), + filename: "[name].css" + (specialOptions.longTermCaching ? "?[contenthash]" : ""), allChunks: true })) } @@ -81,35 +82,36 @@ module.exports = function(rules, options) { plugins.push( new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: specialOptions.minimize ? JSON.stringify('production') : null, - WEBPACK_INLINE_STYLES: !Boolean(specialOptions.separateStylesheets) - + "process.env": { + NODE_ENV: specialOptions.minimize ? JSON.stringify("production") : null, + WEBPACK_INLINE_STYLES: !specialOptions.separateStylesheets }, - 'buildInfo': JSON.stringify({ + "buildInfo": JSON.stringify({ PACKAGE_VERSION: (pkg.version), GIT_COMMIT: gitInfo.hash, - GIT_DIRTY: gitInfo.dirty + GIT_DIRTY: gitInfo.dirty, + HOSTNAME: os.hostname(), + BUILD_TIME: new Date().toUTCString() }) })) delete options._special - var completeConfig = deepExtend({ + var completeConfig = deepExtend({ entry: {}, output: { - path: path.join(__dirname, 'dist'), - publicPath: '/', - filename: '[name].js', - chunkFilename: '[name].js' + path: path.join(__dirname, "dist"), + publicPath: "/", + filename: "[name].js", + chunkFilename: "[name].js" }, - target: 'web', + target: "web", // yaml-js has a reference to `fs`, this is a workaround node: { - fs: 'empty' + fs: "empty" }, module: { @@ -117,17 +119,17 @@ module.exports = function(rules, options) { }, resolveLoader: { - modules: [path.join(__dirname, 'node_modules')], + modules: [path.join(__dirname, "node_modules")], }, externals: { - 'buffertools': true // json-react-schema/deeper depends on buffertools, which fails. + "buffertools": true // json-react-schema/deeper depends on buffertools, which fails. }, resolve: { modules: [ - path.join(__dirname, './src'), - 'node_modules' + path.join(__dirname, "./src"), + "node_modules" ], extensions: [".web.js", ".js", ".jsx", ".json", ".less"], alias: { @@ -135,7 +137,7 @@ module.exports = function(rules, options) { } }, - devtool: specialOptions.sourcemaps ? 'cheap-module-source-map' : null, + devtool: specialOptions.sourcemaps ? "cheap-module-source-map" : null, plugins, diff --git a/package.json b/package.json index 4c0640f2..cb43b865 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,6 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "2.0.3", "less": "2.7.2", - "less-loader": "4.0.4", "license-checker": "^11.0.0", "mocha": "^3.4.2", "nightwatch": "^0.9.16", diff --git a/postcss.config.js b/postcss.config.js index f053ebf7..0ff35571 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1 +1,5 @@ -module.exports = {}; +module.exports = { + plugins: [ + require("autoprefixer") + ] +} \ No newline at end of file diff --git a/src/core/components/array-model.jsx b/src/core/components/array-model.jsx new file mode 100644 index 00000000..3442d4c9 --- /dev/null +++ b/src/core/components/array-model.jsx @@ -0,0 +1,47 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" + +const propStyle = { color: "#999", fontStyle: "italic" } + +export default class ArrayModel extends Component { + static propTypes = { + schema: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + specSelectors: PropTypes.object.isRequired, + name: PropTypes.string, + required: PropTypes.bool, + expandDepth: PropTypes.number, + depth: PropTypes.number + } + + render(){ + let { getComponent, required, schema, depth, expandDepth, name } = 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 ) + + const ModelCollapse = getComponent("ModelCollapse") + const Model = getComponent("Model") + + const titleEl = title && + + { title } + + + return + expandDepth } collapsedContent="[...]"> + [ + + ] + { + properties.size ? + { properties.entrySeq().map( ( [ key, v ] ) => +
{ `${key}:`}{ String(v) }
) + }
+ : null + } +
+ { required && *} +
+ } +} diff --git a/src/core/components/content-type.jsx b/src/core/components/content-type.jsx index 4cc22f8b..4a5e7349 100644 --- a/src/core/components/content-type.jsx +++ b/src/core/components/content-type.jsx @@ -8,7 +8,7 @@ const noop = ()=>{} export default class ContentType extends React.Component { static propTypes = { - contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set]), + contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set, ImPropTypes.seq]), value: PropTypes.string, onChange: PropTypes.func, className: PropTypes.string @@ -22,7 +22,9 @@ export default class ContentType extends React.Component { componentDidMount() { // Needed to populate the form, initially - this.props.onChange(this.props.contentTypes.first()) + if(this.props.contentTypes) { + this.props.onChange(this.props.contentTypes.first()) + } } onChangeWrapper = e => this.props.onChange(e.target.value) diff --git a/src/core/components/enum-model.jsx b/src/core/components/enum-model.jsx new file mode 100644 index 00000000..78af10da --- /dev/null +++ b/src/core/components/enum-model.jsx @@ -0,0 +1,19 @@ +import React from "react" +import ImPropTypes from "react-immutable-proptypes" + +const EnumModel = ({ value, getComponent }) => { + let ModelCollapse = getComponent("ModelCollapse") + let collapsedContent = Array [ { value.count() } ] + return + Enum:
+ + [ { value.join(", ") } ] + +
+} +EnumModel.propTypes = { + value: ImPropTypes.iterable, + getComponent: ImPropTypes.func +} + +export default EnumModel \ No newline at end of file diff --git a/src/core/components/info.jsx b/src/core/components/info.jsx index 6ea1ed48..7036de3e 100644 --- a/src/core/components/info.jsx +++ b/src/core/components/info.jsx @@ -15,7 +15,7 @@ class Path extends React.Component { return (
-        [ Base url: {host}{basePath}]
+        [ Base URL: {host}{basePath} ]
       
) } @@ -88,12 +88,13 @@ export default class Info extends React.Component { const { url:externalDocsUrl, description:externalDocsDescription } = (externalDocs || fromJS({})).toJS() const Markdown = getComponent("Markdown") + const VersionStamp = getComponent("VersionStamp") return (

{ title } - { version &&
 { version } 
} + { version && }

{ host || basePath ? : null } { url && { url } } diff --git a/src/core/components/layout-utils.jsx b/src/core/components/layout-utils.jsx index 00a4a09d..f80e0c90 100644 --- a/src/core/components/layout-utils.jsx +++ b/src/core/components/layout-utils.jsx @@ -129,7 +129,8 @@ export class Select extends React.Component { value: PropTypes.any, onChange: PropTypes.func, multiple: PropTypes.bool, - allowEmptyValue: PropTypes.bool + allowEmptyValue: PropTypes.bool, + className: PropTypes.string } static defaultProps = { @@ -142,7 +143,7 @@ export class Select extends React.Component { let value - if (props.value !== undefined) { + if (props.value) { value = props.value } else { value = props.multiple ? [""] : "" @@ -178,7 +179,7 @@ export class Select extends React.Component { let value = this.state.value.toJS ? this.state.value.toJS() : this.state.value return ( - { allowEmptyValue ? : null } { allowedValues.map(function (item, key) { diff --git a/src/core/components/layouts/base.jsx b/src/core/components/layouts/base.jsx index 9da4b347..f502b88f 100644 --- a/src/core/components/layouts/base.jsx +++ b/src/core/components/layouts/base.jsx @@ -13,8 +13,13 @@ export default class BaseLayout extends React.Component { getComponent: PropTypes.func.isRequired } + onFilterChange =(e) => { + let {target: {value}} = e + this.props.layoutActions.updateFilter(value) + } + render() { - let { specSelectors, specActions, getComponent } = this.props + let { specSelectors, specActions, getComponent, layoutSelectors } = this.props let info = specSelectors.info() let url = specSelectors.url() @@ -26,11 +31,20 @@ export default class BaseLayout extends React.Component { let Info = getComponent("info") let Operations = getComponent("operations", true) - let Models = getComponent("models", true) + let Models = getComponent("Models", true) let AuthorizeBtn = getComponent("authorizeBtn", true) let Row = getComponent("Row") let Col = getComponent("Col") let Errors = getComponent("errors", true) + + let isLoading = specSelectors.loadingStatus() === "loading" + let isFailed = specSelectors.loadingStatus() === "failed" + let filter = layoutSelectors.currentFilter() + + let inputStyle = {} + if(isFailed) inputStyle.color = "red" + if(isLoading) inputStyle.color = "#aaa" + const Schemes = getComponent("schemes") const isSpecEmpty = !specSelectors.specStr() @@ -57,6 +71,7 @@ export default class BaseLayout extends React.Component { { schemes && schemes.size ? ( ) : null } + { securityDefinitions ? ( ) : null } @@ -64,6 +79,15 @@ export default class BaseLayout extends React.Component {
) : null } + { + filter === null || filter === false ? null : +
+ + + +
+ } + diff --git a/src/core/components/model-collapse.jsx b/src/core/components/model-collapse.jsx new file mode 100644 index 00000000..b71096f2 --- /dev/null +++ b/src/core/components/model-collapse.jsx @@ -0,0 +1,47 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" + +export default class ModelCollapse extends Component { + static propTypes = { + collapsedContent: PropTypes.any, + collapsed: PropTypes.bool, + children: PropTypes.any, + title: PropTypes.element + } + + static defaultProps = { + collapsedContent: "{...}", + collapsed: true, + title: null + } + + constructor(props, context) { + super(props, context) + + let { collapsed, collapsedContent } = this.props + + this.state = { + collapsed: collapsed !== undefined ? collapsed : ModelCollapse.defaultProps.collapsed, + collapsedContent: collapsedContent || ModelCollapse.defaultProps.collapsedContent + } + } + + toggleCollapsed=()=>{ + this.setState({ + collapsed: !this.state.collapsed + }) + } + + render () { + const {title} = this.props + return ( + + { title && {title} } + + + + { this.state.collapsed ? this.state.collapsedContent : this.props.children } + + ) + } +} \ No newline at end of file diff --git a/src/core/components/model-example.jsx b/src/core/components/model-example.jsx index 1b4273ce..768ee04b 100644 --- a/src/core/components/model-example.jsx +++ b/src/core/components/model-example.jsx @@ -28,23 +28,23 @@ export default class ModelExample extends React.Component { render() { let { getComponent, specSelectors, schema, example, isExecute } = this.props - const Model = getComponent("model") + const ModelWrapper = getComponent("ModelWrapper") return
{ (isExecute || this.state.activeTab === "example") && example } { - !isExecute && this.state.activeTab === "model" && diff --git a/src/core/components/model-wrapper.jsx b/src/core/components/model-wrapper.jsx new file mode 100644 index 00000000..280a727f --- /dev/null +++ b/src/core/components/model-wrapper.jsx @@ -0,0 +1,23 @@ +import React, { Component, } from "react" +import PropTypes from "prop-types" + +export default class ModelComponent extends Component { + static propTypes = { + schema: PropTypes.object.isRequired, + name: PropTypes.string, + getComponent: PropTypes.func.isRequired, + specSelectors: PropTypes.object.isRequired, + expandDepth: PropTypes.number + } + + render(){ + let { getComponent } = this.props + const Model = getComponent("Model") + + return
+ +
+ } +} + + diff --git a/src/core/components/model.jsx b/src/core/components/model.jsx index de714403..5e9a3fcf 100644 --- a/src/core/components/model.jsx +++ b/src/core/components/model.jsx @@ -1,219 +1,7 @@ import React, { Component } from "react" import PropTypes from "prop-types" -import ImPropTypes from "react-immutable-proptypes" -import { List } from "immutable" -const braceOpen = "{" -const braceClose = "}" -const propStyle = { color: "#999", fontStyle: "italic" } - -const EnumModel = ({ value }) => { - let collapsedContent = Array [ { value.count() } ] - return - Enum:
- - [ { value.join(", ") } ] - -
-} - -EnumModel.propTypes = { - value: ImPropTypes.iterable -} - -class ObjectModel extends Component { - static propTypes = { - schema: PropTypes.object.isRequired, - getComponent: PropTypes.func.isRequired, - specSelectors: PropTypes.object.isRequired, - name: PropTypes.string, - isRef: PropTypes.bool, - expandDepth: PropTypes.number, - depth: PropTypes.number - } - - render(){ - let { schema, name, isRef, getComponent, depth, ...props } = this.props - let { expandDepth } = this.props - const JumpToPath = getComponent("JumpToPath", true) - let description = schema.get("description") - let properties = schema.get("properties") - let additionalProperties = schema.get("additionalProperties") - let title = schema.get("title") || name - let required = schema.get("required") - const Markdown = getComponent("Markdown") - const JumpToPathSection = ({ name }) => - let collapsedContent = ( - { braceOpen }...{ braceClose } - { - isRef ? : "" - } - ) - - return - { - title && - { isRef && schema.get("$$ref") && { schema.get("$$ref") } } - { title } - - } - expandDepth } collapsedContent={ collapsedContent }> - { braceOpen } - { - !isRef ? null : - } - - { - - { - !description ? null : - - - - } - { - !(properties && properties.size) ? null : properties.entrySeq().map( - ([key, value]) => { - let isRequired = List.isList(required) && required.contains(key) - let propertyStyle = { verticalAlign: "top", paddingRight: "0.2em" } - if ( isRequired ) { - propertyStyle.fontWeight = "bold" - } - - return ( - - - ) - }).toArray() - } - { - !additionalProperties || !additionalProperties.size ? null - : - - - - } -
description: - -
{ key }: - -
{ "< * >:" } - -
- } -
- { braceClose } -
-
- } -} - -class Primitive extends Component { - static propTypes = { - schema: PropTypes.object.isRequired, - name: PropTypes.string, - getComponent: PropTypes.func.isRequired, - required: PropTypes.bool - } - - render(){ - let { schema, getComponent, name, required } = this.props - - if(!schema || !schema.get) { - // don't render if schema isn't correctly formed - return
- } - - let type = schema.get("type") - 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 - { - title && - { title } - - } - { type } { required && *} - { format && (${format})} - { - properties.size ? properties.entrySeq().map( ( [ key, v ] ) => -
{ key }: { String(v) }
) - : null - } - { - !description ? null : - - } - { - xml && xml.size ? (
xml: - { - xml.entrySeq().map( ( [ key, v ] ) =>
   {key}: { String(v) }
).toArray() - } -
): null - } - { - enumArray && - } -
- } -} - -class ArrayModel extends Component { - static propTypes = { - schema: PropTypes.object.isRequired, - getComponent: PropTypes.func.isRequired, - specSelectors: PropTypes.object.isRequired, - name: PropTypes.string, - required: PropTypes.bool, - expandDepth: PropTypes.number, - depth: PropTypes.number - } - - render(){ - 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 - { - title && - { title } - - } - expandDepth } collapsedContent="[...]"> - [ - - ] - { - properties.size ? - { properties.entrySeq().map( ( [ key, v ] ) => -
{ `${key}:`}{ String(v) }
) - }
- : null - } -
- { required && *} -
- } -} - - -class Model extends Component { +export default class Model extends Component { static propTypes = { schema: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, @@ -229,6 +17,9 @@ class Model extends Component { if ( ref.indexOf("#/definitions/") !== -1 ) { return ref.replace(/^.*#\/definitions\//, "") } + if ( ref.indexOf("#/components/schemas/") !== -1 ) { + return ref.replace("#/components/schemas/", "") + } } getRefSchema =( model )=> { @@ -238,11 +29,16 @@ class Model extends Component { } render () { - let { schema, getComponent, required, name, isRef } = this.props + let { getComponent, specSelectors, schema, required, name, isRef } = this.props + let ObjectModel = getComponent("ObjectModel") + let ArrayModel = getComponent("ArrayModel") + let PrimitiveModel = getComponent("PrimitiveModel") let $$ref = schema && schema.get("$$ref") let modelName = $$ref && this.getModelName( $$ref ) let modelSchema, type + const deprecated = specSelectors.isOAS3() && schema.get("deprecated") + if ( schema && (schema.get("type") || schema.get("properties")) ) { modelSchema = schema } else if ( $$ref ) { @@ -256,73 +52,30 @@ class Model extends Component { switch(type) { case "object": - return + return case "array": - return + return case "string": case "number": case "integer": case "boolean": default: - return - } - } -} - - -export default class ModelComponent extends Component { - static propTypes = { - schema: PropTypes.object.isRequired, - name: PropTypes.string, - getComponent: PropTypes.func.isRequired, - specSelectors: PropTypes.object.isRequired, - expandDepth: PropTypes.number - } - - render(){ - return
- -
- } -} - -class Collapse extends Component { - static propTypes = { - collapsedContent: PropTypes.any, - collapsed: PropTypes.bool, - children: PropTypes.any - } - - static defaultProps = { - collapsedContent: "{...}", - collapsed: true, - } - - constructor(props, context) { - super(props, context) - - let { collapsed, collapsedContent } = this.props - - this.state = { - collapsed: collapsed !== undefined ? collapsed : Collapse.defaultProps.collapsed, - collapsedContent: collapsedContent || Collapse.defaultProps.collapsedContent - } - } - - toggleCollapsed=()=>{ - this.setState({ - collapsed: !this.state.collapsed - }) - } - - render () { - return ( - - - - { this.state.collapsed ? this.state.collapsedContent : this.props.children } - ) + return } } } diff --git a/src/core/components/models.jsx b/src/core/components/models.jsx index d5c65b70..86c3256d 100644 --- a/src/core/components/models.jsx +++ b/src/core/components/models.jsx @@ -16,7 +16,7 @@ export default class Models extends Component { let { docExpansion } = getConfigs() let showModels = layoutSelectors.isShown("models", docExpansion === "full" || docExpansion === "list" ) - const Model = getComponent("model") + const ModelWrapper = getComponent("ModelWrapper") const Collapse = getComponent("Collapse") if (!definitions.size) return null @@ -28,11 +28,11 @@ export default class Models extends Component { - + { definitions.entrySeq().map( ( [ name, model ])=>{ return
- + const collapsedContent = ( + { braceOpen }...{ braceClose } + { + isRef ? : "" + } + ) + + const titleEl = title && + { isRef && schema.get("$$ref") && { schema.get("$$ref") } } + { title } + + + return + expandDepth } collapsedContent={ collapsedContent }> + { braceOpen } + { + !isRef ? null : + } + + { + + { + !description ? null : + + + + } + { + !(properties && properties.size) ? null : properties.entrySeq().map( + ([key, value]) => { + let isRequired = List.isList(requiredProperties) && requiredProperties.contains(key) + let propertyStyle = { verticalAlign: "top", paddingRight: "0.2em" } + if ( isRequired ) { + propertyStyle.fontWeight = "bold" + } + + return ( + + + ) + }).toArray() + } + { + !additionalProperties || !additionalProperties.size ? null + : + + + + } +
description: + +
+ { key }{ isRequired && * } + + +
{ "< * >:" } + +
+ } +
+ { braceClose } +
+
+ } +} \ No newline at end of file diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index 52807722..26743fbc 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -116,7 +116,8 @@ export default class Operation extends PureComponent { specActions, specSelectors, authActions, - authSelectors + authSelectors, + getConfigs } = this.props let summary = operation.get("summary") @@ -141,6 +142,10 @@ export default class Operation extends PureComponent { const Markdown = getComponent( "Markdown" ) const Schemes = getComponent( "schemes" ) + const { deepLinking } = getConfigs() + + const isDeepLinkingEnabled = deepLinking && deepLinking !== "false" + // Merge in Live Response if(response && response.size > 0) { let notDocumented = !responses.get(String(response.get("status"))) @@ -152,13 +157,18 @@ export default class Operation extends PureComponent { let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method ) return ( -
+
- {method.toUpperCase()} - - {path} - - + {method.toUpperCase()} + + e.preventDefault()} + href={ isDeepLinkingEnabled ? `#/${isShownKey[1]}/${isShownKey[2]}` : ""} > + {path} + + + { !showSummary ? null :
@@ -191,13 +201,16 @@ export default class Operation extends PureComponent {

Find more details

- { externalDocs.get("description") } + + + { externalDocs.get("url") }
: null } { + return tag.indexOf(filter) !== -1 + }) + } + } + + if (maxDisplayedTags && !isNaN(maxDisplayedTags) && maxDisplayedTags >= 0) { + taggedOps = taggedOps.slice(0, maxDisplayedTags) + } return (
@@ -41,6 +66,8 @@ export default class Operations extends React.Component { taggedOps.map( (tagObj, tag) => { let operations = tagObj.get("operations") let tagDescription = tagObj.getIn(["tagDetails", "description"], null) + let tagExternalDocsDescription = tagObj.getIn(["tagDetails", "externalDocs", "description"]) + let tagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"]) let isShownKey = ["operations-tag", tag] let showTag = layoutSelectors.isShown(isShownKey, docExpansion === "full" || docExpansion === "list") @@ -48,14 +75,38 @@ export default class Operations extends React.Component { return (
-

layoutActions.show(isShownKey, !showTag)} className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }> - {tag} +

layoutActions.show(isShownKey, !showTag)} + className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" } + id={isShownKey.join("-")}> + e.preventDefault()} + href={ isDeepLinkingEnabled ? `#/${tag}` : ""}> + {tag} + { !tagDescription ? null : { tagDescription } } +
+ { !tagExternalDocsDescription ? null : + + { tagExternalDocsDescription } + { tagExternalDocsUrl ? ": " : null } + { tagExternalDocsUrl ? + e.stopPropagation()} + target={"_blank"} + >{tagExternalDocsUrl} : null + } + + } +
+

-
{ param.get("type") } { itemType && `[${itemType}]` }
+
{ param.get("type") } { itemType && `[${itemType}]` }
({ param.get("in") })
diff --git a/src/core/components/parameters.jsx b/src/core/components/parameters.jsx index b85cc8ab..a6b981e6 100644 --- a/src/core/components/parameters.jsx +++ b/src/core/components/parameters.jsx @@ -72,7 +72,9 @@ export default class Parameters extends Component { return (
-

Parameters

+
+

Parameters

+
{ allowTryItOut ? ( ) : null } diff --git a/src/core/components/primitive-model.jsx b/src/core/components/primitive-model.jsx new file mode 100644 index 00000000..96ec3a9d --- /dev/null +++ b/src/core/components/primitive-model.jsx @@ -0,0 +1,55 @@ +import React, { Component } from "react" +import PropTypes from "prop-types" + +const propStyle = { color: "#999", fontStyle: "italic" } + +export default class Primitive extends Component { + static propTypes = { + schema: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + required: PropTypes.bool + } + + render(){ + let { schema, getComponent, required } = this.props + + if(!schema || !schema.get) { + // don't render if schema isn't correctly formed + return
+ } + + let type = schema.get("type") + let format = schema.get("format") + let xml = schema.get("xml") + let enumArray = schema.get("enum") + 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") + const EnumModel = getComponent("EnumModel") + + return + { type } { required && *} + { format && (${format})} + { + properties.size ? properties.entrySeq().map( ( [ key, v ] ) => +
{ key }: { String(v) }
) + : null + } + { + !description ? null : + + } + { + xml && xml.size ? (
xml: + { + xml.entrySeq().map( ( [ key, v ] ) =>
   {key}: { String(v) }
).toArray() + } +
): null + } + { + enumArray && + } +
+ } +} \ No newline at end of file diff --git a/src/core/components/providers/markdown.jsx b/src/core/components/providers/markdown.jsx index ff29f021..8f303335 100644 --- a/src/core/components/providers/markdown.jsx +++ b/src/core/components/providers/markdown.jsx @@ -3,6 +3,28 @@ import PropTypes from "prop-types" import Remarkable from "react-remarkable" import sanitize from "sanitize-html" +function Markdown({ source }) { + const sanitized = sanitizer(source) + + // sometimes the sanitizer returns "undefined" as a string + if(!source || !sanitized || sanitized === "undefined") { + return null + } + + return
+ +
+} + +Markdown.propTypes = { + source: PropTypes.string.isRequired +} + +export default Markdown + const sanitizeOptions = { textFilter: function(text) { return text @@ -10,22 +32,6 @@ 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 +export function sanitizer(str) { + return sanitize(str, sanitizeOptions) } - -Markdown.propTypes = { - source: PropTypes.string.isRequired -} - -export default Markdown diff --git a/src/core/components/response-body.jsx b/src/core/components/response-body.jsx index df3bd061..0829512e 100644 --- a/src/core/components/response-body.jsx +++ b/src/core/components/response-body.jsx @@ -40,7 +40,7 @@ export default class ResponseBody extends React.Component { // Image } else if (/^image\//i.test(contentType)) { - bodyEl = + bodyEl = // Audio } else if (/^audio\//i.test(contentType)) { diff --git a/src/core/components/response.jsx b/src/core/components/response.jsx index b63c8d7b..95602c64 100644 --- a/src/core/components/response.jsx +++ b/src/core/components/response.jsx @@ -1,6 +1,6 @@ import React from "react" import PropTypes from "prop-types" -import { fromJS } from "immutable" +import { fromJS, Seq } from "immutable" import { getSampleSchema } from "core/utils" const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => { @@ -31,6 +31,13 @@ const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => { } export default class Response extends React.Component { + constructor(props, context) { + super(props, context) + + this.state = { + responseContentType: "" + } + } static propTypes = { code: PropTypes.string.isRequired, @@ -59,16 +66,29 @@ export default class Response extends React.Component { } = this.props let { inferSchema } = fn + let { isOAS3 } = specSelectors - let schema = inferSchema(response.toJS()) let headers = response.get("headers") let examples = response.get("examples") + let links = response.get("links") const Headers = getComponent("headers") const HighlightCode = getComponent("highlightCode") const ModelExample = getComponent("modelExample") const Markdown = getComponent( "Markdown" ) + const OperationLink = getComponent("operationLink") + const ContentType = getComponent("contentType") - let sampleResponse = schema ? getSampleSchema(schema, contentType, { includeReadOnly: true }) : null + var sampleResponse + var schema + + if(isOAS3()) { + let oas3SchemaForContentType = response.getIn(["content", this.state.responseContentType, "schema"]) + sampleResponse = oas3SchemaForContentType ? getSampleSchema(oas3SchemaForContentType.toJS(), this.state.responseContentType, { includeReadOnly: true }) : null + schema = oas3SchemaForContentType ? inferSchema(oas3SchemaForContentType.toJS()) : null + } else { + schema = inferSchema(response.toJS()) + sampleResponse = schema ? getSampleSchema(schema, contentType, { includeReadOnly: true }) : null + } let example = getExampleComponent( sampleResponse, examples, HighlightCode ) return ( @@ -82,6 +102,12 @@ export default class Response extends React.Component {
+ { isOAS3 ? this.setState({ responseContentType: val })} + className="response-content-type" /> : null } + { example ? ( ) : null} - + + {specSelectors.isOAS3() ? + { links ? + links.toSeq().map((link, key) => { + return + }) + : No links} + : null} ) } diff --git a/src/core/components/responses.jsx b/src/core/components/responses.jsx index b0dfc4f7..1c00ff1a 100644 --- a/src/core/components/responses.jsx +++ b/src/core/components/responses.jsx @@ -42,13 +42,13 @@ export default class Responses extends React.Component {

Responses

- }
{ @@ -68,6 +68,7 @@ export default class Responses extends React.Component { Code Description + { specSelectors.isOAS3() ? Links : null } diff --git a/src/core/components/schemes.jsx b/src/core/components/schemes.jsx index 8be4180a..f9fe8f81 100644 --- a/src/core/components/schemes.jsx +++ b/src/core/components/schemes.jsx @@ -19,8 +19,9 @@ export default class Schemes extends React.Component { } componentWillReceiveProps(nextProps) { - if ( this.props.operationScheme && !nextProps.schemes.has(this.props.operationScheme) ) { - //fire 'change' event if our selected scheme is no longer an option + if ( !this.props.operationScheme || !nextProps.schemes.has(this.props.operationScheme) ) { + // if we don't have a selected operationScheme or if our selected scheme is no longer an option, + // then fire 'change' event and select the first scheme in the list of options this.setScheme(nextProps.schemes.first()) } } diff --git a/src/core/components/version-stamp.jsx b/src/core/components/version-stamp.jsx new file mode 100644 index 00000000..262cc023 --- /dev/null +++ b/src/core/components/version-stamp.jsx @@ -0,0 +1,12 @@ +import React from "react" +import PropTypes from "prop-types" + +const VersionStamp = ({ version }) => { + return
 { version } 
+} + +VersionStamp.propTypes = { + version: PropTypes.string.isRequired +} + +export default VersionStamp diff --git a/src/core/index.js b/src/core/index.js index 80335da5..126cbc54 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -4,19 +4,21 @@ import System from "core/system" import win from "core/window" import ApisPreset from "core/presets/apis" import * as AllPlugins from "core/plugins/all" -import { parseSeach, filterConfigs } from "core/utils" - -const CONFIGS = [ "url", "urls", "urls.primaryName", "spec", "validatorUrl", "onComplete", "onFailure", "authorizations", "docExpansion", - "apisSorter", "operationsSorter", "supportedSubmitMethods", "dom_id", "defaultModelRendering", "oauth2RedirectUrl", - "showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" , "displayRequestDuration"] +import { parseSearch } from "core/utils" // eslint-disable-next-line no-undef -const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo +const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION, HOSTNAME, BUILD_TIME } = buildInfo module.exports = function SwaggerUI(opts) { win.versions = win.versions || {} - win.versions.swaggerUi = `${PACKAGE_VERSION}/${GIT_COMMIT || "unknown"}${GIT_DIRTY ? "-dirty" : ""}` + win.versions.swaggerUi = { + version: PACKAGE_VERSION, + gitRevision: GIT_COMMIT, + gitDirty: GIT_DIRTY, + buildTimestamp: BUILD_TIME, + machine: HOSTNAME + } const defaults = { // Some general settings, that we floated to the top @@ -26,15 +28,19 @@ module.exports = function SwaggerUI(opts) { urls: null, layout: "BaseLayout", docExpansion: "list", + maxDisplayedTags: null, + filter: null, validatorUrl: "https://online.swagger.io/validator", configs: {}, custom: {}, displayOperationId: false, displayRequestDuration: false, + deepLinking: 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. presets: [ + ApisPreset ], // Plugins; ( loaded after presets ) @@ -50,7 +56,9 @@ module.exports = function SwaggerUI(opts) { store: { }, } - const constructorConfig = deepExtend({}, defaults, opts) + let queryConfig = parseSearch() + + const constructorConfig = deepExtend({}, defaults, opts, queryConfig) const storeConfigs = deepExtend({}, constructorConfig.store, { system: { @@ -59,7 +67,8 @@ module.exports = function SwaggerUI(opts) { plugins: constructorConfig.presets, state: { layout: { - layout: constructorConfig.layout + layout: constructorConfig.layout, + filter: constructorConfig.filter }, spec: { spec: "", @@ -80,7 +89,6 @@ module.exports = function SwaggerUI(opts) { store.register([constructorConfig.plugins, inlinePlugin]) var system = store.getSystem() - let queryConfig = parseSeach() system.initOAuth = system.authActions.configureAuth @@ -91,7 +99,7 @@ module.exports = function SwaggerUI(opts) { let localConfig = system.specSelectors.getLocalConfig ? system.specSelectors.getLocalConfig() : {} let mergedConfig = deepExtend({}, localConfig, constructorConfig, fetchedConfig || {}, queryConfig) - store.setConfigs(filterConfigs(mergedConfig, CONFIGS)) + store.setConfigs(mergedConfig) if (fetchedConfig !== null) { if (!queryConfig.url && typeof mergedConfig.spec === "object" && Object.keys(mergedConfig.spec).length) { diff --git a/src/core/json-schema-components.js b/src/core/json-schema-components.js index bf4ae514..a8d92023 100644 --- a/src/core/json-schema-components.js +++ b/src/core/json-schema-components.js @@ -57,7 +57,8 @@ export class JsonSchema_string extends Component { if ( enumValue ) { const Select = getComponent("Select") - return () } - let errors = schema.errors || [] - return (
- { !value || value.count() < 1 ? - (errors.length ? { errors[0] } : null) : + { !value || value.count() < 1 ? null : value.map( (item,i) => { let schema = Object.assign({}, itemSchema) if ( errors.length ) { @@ -153,12 +153,12 @@ export class JsonSchema_array extends PureComponent { return (
this.onItemChange(val, i)} schema={schema} /> - +
) }).toArray() } - +
) } @@ -170,12 +170,14 @@ export class JsonSchema_boolean extends Component { onEnumChange = (val) => this.props.onChange(val) render() { - let { getComponent, required, value } = this.props + let { getComponent, value, schema } = this.props + let errors = schema.errors || [] const Select = getComponent("Select") - return (