diff --git a/.github/issue_template.md b/.github/issue_template.md index 0c45b92b..3582a980 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,6 +1,9 @@ + +### Description + + + + +### Motivation and Context + + + + + + + +### How Has This Been Tested? + + + + + + +### Screenshots (if appropriate): + + + +### Types of changes + +- [ ] No code changes (changes to documentation, CI, metadata, etc) +- [ ] Dependency changes (any modification to dependencies in `package.json`) +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +### Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. diff --git a/.gitignore b/.gitignore index 8c713860..2114a60c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,11 @@ node_modules .idea .deps_check .DS_Store +.nyc_output npm-debug.log* .eslintcache package-lock.json *.iml selenium-debug.log test/e2e/db.json +docs/_book diff --git a/.npmignore b/.npmignore index a5d40957..70897c3a 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,4 @@ !dist/swagger-ui.js.map !dist/swagger-ui.css !dist/swagger-ui.css.map +!dist/oauth2-redirect.html diff --git a/.travis.yml b/.travis.yml index da8c805b..9149557c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ language: node_js node_js: - '6.9' +cache: + directories: + - node_modules services: - docker branches: only: - master - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ +install: "npm i && npm update" before_deploy: - npm run build env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3acb80b0..3b6d0c15 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ We love contributions from our community of users! This document explains our gu #### Environment setup -0. Install Node.js (4 or newer) and npm (3 or newer). +0. Install Node.js (6 or newer) and npm (3 or newer). 1. Make a fork of Swagger-UI on GitHub, then clone your fork to your machine. 2. Run `npm install` in your Swagger-UI directory. 3. Run `npm run dev`. `localhost:3200` should open automatically. diff --git a/Dockerfile b/Dockerfile index ee177b4e..fd831f04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ ENV OAUTH_APP_NAME "**None**" ENV OAUTH_ADDITIONAL_PARAMS "**None**" ENV SWAGGER_JSON "/app/swagger.json" ENV PORT 80 +ENV BASE_URL "" RUN apk add --update nginx RUN mkdir -p /run/nginx diff --git a/README.md b/README.md index aee2b2e8..bc824717 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The OpenAPI Specification has undergone 5 revisions since initial creation in 20 Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes ------------------ | ------------ | -------------------------- | ----- -3.4.0 | 2017-10-20 | 2.0, 3.0 | [tag v3.4.0](https://github.com/swagger-api/swagger-ui/tree/v3.4.0) +3.6.1 | 2017-12-01 | 2.0, 3.0 | [tag v3.6.1](https://github.com/swagger-api/swagger-ui/tree/v3.6.1) 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) @@ -30,35 +30,25 @@ Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes 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) +## Documentation -### How to run +#### Usage +- [Installation](docs/usage/installation.md) +- [Configuration](docs/usage/configuration.md) +- [CORS](docs/usage/cors.md) +- [OAuth2](docs/usage/oauth2.md) +- [Deep Linking](docs/usage/deep-linking.md) +- [Limitations](docs/usage/limitations.md) +- [Version detection](docs/usage/version-detection.md) -##### Easy start! Docker -You can pull a pre-built docker image of the swagger-ui directly from Dockerhub: - -``` -docker pull swaggerapi/swagger-ui -docker run -p 80:8080 swaggerapi/swagger-ui -``` - -Will start nginx with swagger-ui on port 80. - -Or you can provide your own swagger.json on your host - -``` -docker run -p 80:8080 -e SWAGGER_JSON=/foo/swagger.json -v /bar:/foo swaggerapi/swagger-ui -``` - -##### Prerequisites -- Node 6.x -- NPM 3.x - -If you just want to see your specs, open `dist/index.html` in your browser directly from your filesystem. - -If you'd like to make modifications to the codebase, run the dev server with: `npm run dev`. A development server will open on `3200`. - -If you'd like to rebuild the `/dist` folder with your codebase changes, run `npm run build`. +#### Customization +- [Overview](docs/customization/overview.md) +- [Plugin API](docs/customization/plugin-api.md) +- [Custom layout](docs/customization/custom-layout.md) +#### Development +- [Setting up](docs/development/setting-up.md) +- [Scripts](docs/development/scripts.md) ##### Integration Tests @@ -67,193 +57,19 @@ http://nightwatchjs.org/gettingstarted#selenium-server-setup Integration tests can be run locally with `npm run e2e` - be sure you aren't running a dev server when testing! - -##### Browser support +### Browser support Swagger UI works in the latest versions of Chrome, Safari, Firefox, Edge and IE11. ### Known Issues To help with the migration, here are the currently known issues with 3.X. This list will update regularly, and will not include features that were not implemented in previous versions. -- Only part of the [parameters](#parameters) previously supported are available. +- Only part of the parameters previously supported are available. - The JSON Form Editor is not implemented. - Support for `collectionFormat` is partial. - l10n (translations) is not implemented. - Relative path support for external files is not implemented. -### Direct use of JS and CSS assets -To include the JS, CSS and image assets directly into a webpage, use the [swagger-ui-dist](https://www.npmjs.com/package/swagger-ui-dist) package. -The root directory of this package contains the contents of the _dist/_ directory of this repo. -As a node module, `swagger-ui-dist` also exports the `swagger-ui-bundle` and `swagger-ui-standalone-preset` objects. - -### SwaggerUIBundle -To use swagger-ui's bundles, you should take a look at the [source of swagger-ui html page](https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html) and customize it. This basically requires you to instantiate a SwaggerUi object as below: - -```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" - }) -``` - -#### 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`, -`useBasicAuthenticationWithAccessCodeGrant`. - -Config Name | Description ---- | --- -client_id | Default clientId. MUST be a string -client_secret | Default clientSecret. MUST be a string -realm | realm query parameter (for oauth1) added to `authorizationUrl` and `tokenUrl` . MUST be a string -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` - -```javascript -const ui = SwaggerUIBundle({...}) - -// Method can be called in any place after calling constructor SwaggerUIBundle -ui.initOAuth({ - clientId: "your-client-id", - clientSecret: "your-client-secret-if-required", - realm: "your-realms", - appName: "your-app-name", - scopeSeparator: " ", - additionalQueryStringParams: {test: "hello"} - }) -``` - -If you'd like to use the bundle files via npm, check out the [`swagger-ui-dist` package](https://www.npmjs.com/package/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`). Will be ignored if `urls` or `spec` is used. -urls | An array of API definition objects (`{url: "", 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. -domNode | The HTML DOM element inside which SwaggerUi will put the user interface for swagger. Overrides `dom_id`. -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. -defaultModelRendering | Controls how models are shown when the API is first rendered. (The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links.) It can be set to 'model' or 'example', and the default is 'example'. -defaultModelExpandDepth | The default expansion depth for models. The default value is 1. -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`. -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) -requestInterceptor | MUST be a function. Function to intercept try-it-out requests. Accepts one argument requestInterceptor(request) and must return the potentially modified request. -responseInterceptor | MUST be a function. Function to intercept try-it-out responses. Accepts one argument responseInterceptor(response) and must return the potentially modified response. -showMutatedRequest | If set to `true` (the default), uses the mutated request returned from a rquestInterceptor to produce the curl command in the UI, otherwise the request before the requestInterceptor was applied is used. - -### Plugins - -#### Topbar plugin -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`: - -```javascript -let preset = [ - // TopbarPlugin, - ConfigsPlugin, - () => { - return { - components: { StandaloneLayout } - } - } -] -``` - -#### Configs plugin -Configs plugin allows to fetch external configs instead of passing them to `SwaggerUIBundle`. Fetched configs support two formats: JSON or yaml. The plugin is enabled by default. -There are three options of passing config: -- add a query parameter `config` with URL to a server where the configs are hosted. For ex. http://petstore.swagger.io/?config=http://localhost:3001/config.yaml -- add a config `configUrl` with URL to SwaggerUIBundle -- change default configs in `swagger-config.yaml` *Note: after changing, the project must be re-built* - -These options can be used altogether, the order of inheritance is following (from the lowest priority to the highest): -`swagger-config.yaml` -> config passed to `SwaggerUIBundle` -> config fetched from `configUrl` passed to `SwaggerUIBundle` -> config fetched from URL passed as a query parameter `config` - -## CORS Support - -CORS is a technique to prevent websites from doing bad things with your personal data. Most browsers + JavaScript toolkits not only support CORS but enforce it, which has implications for your API server which supports Swagger. - -You can read about CORS here: http://www.w3.org/TR/cors. - -There are two cases where no action is needed for CORS support: - -1. swagger-ui is hosted on the same server as the application itself (same host *and* port). -2. The application is located behind a proxy that enables the required CORS headers. This may already be covered within your organization. - -Otherwise, CORS support needs to be enabled for: - -1. Your Swagger docs. For Swagger 2.0 it's the `swagger.json`/`swagger.yaml` and any externally `$ref`ed docs. -2. For the `Try it now` button to work, CORS needs to be enabled on your API endpoints as well. - -### Testing CORS Support - -You can verify CORS support with one of three techniques: - -- Curl your API and inspect the headers. For instance: - -```bash -$ curl -I "http://petstore.swagger.io/v2/swagger.json" -HTTP/1.1 200 OK -Date: Sat, 31 Jan 2015 23:05:44 GMT -Access-Control-Allow-Origin: * -Access-Control-Allow-Methods: GET, POST, DELETE, PUT, PATCH, OPTIONS -Access-Control-Allow-Headers: Content-Type, api_key, Authorization -Content-Type: application/json -Content-Length: 0 -``` - -This tells us that the petstore resource listing supports OPTIONS, and the following headers: `Content-Type`, `api_key`, `Authorization`. - -- Try swagger-ui from your file system and look at the debug console. If CORS is not enabled, you'll see something like this: - -``` -XMLHttpRequest cannot load http://sad.server.com/v2/api-docs. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. -``` - -Swagger-UI cannot easily show this error state. - -- Using the http://www.test-cors.org website. Keep in mind this will show a successful result even if `Access-Control-Allow-Headers` is not available, which is still required for Swagger-UI to function properly. - -### Enabling CORS - -The method of enabling CORS depends on the server and/or framework you use to host your application. http://enable-cors.org provides information on how to enable CORS in some common web servers. - -Other servers/frameworks may provide you information on how to enable it specifically in their use case. - -### CORS and Header Parameters - -Swagger lets you easily send headers as parameters to requests. The name of these headers *MUST* be supported in your CORS configuration as well. From our example above: - -``` -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. diff --git a/docker-run.sh b/docker-run.sh index db1216bd..39e25f20 100644 --- a/docker-run.sh +++ b/docker-run.sh @@ -21,6 +21,16 @@ replace_or_delete_in_index () { fi } +if [ "${BASE_URL}" ]; then + NGINX_WITH_BASE_URL="${NGINX_ROOT}${BASE_URL}" + + mkdir -p ${NGINX_WITH_BASE_URL} + mv ${NGINX_ROOT}/*.* ${NGINX_WITH_BASE_URL}/ + + INDEX_FILE=$NGINX_WITH_BASE_URL/index.html + NGINX_ROOT=$NGINX_WITH_BASE_URL +fi + replace_in_index myApiKeyXXXX123456789 $API_KEY replace_or_delete_in_index your-client-id $OAUTH_CLIENT_ID replace_or_delete_in_index your-client-secret-if-required $OAUTH_CLIENT_SECRET diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..13d8028f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,5 @@ +# Swagger-UI + +Welcome to the Swagger-UI documentation! + +A table of contents can be found at `SUMMARY.md`. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000..9ab19e1e --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,17 @@ +#### Usage +- [Installation](docs/usage/installation.md) +- [Configuration](docs/usage/configuration.md) +- [CORS](docs/usage/cors.md) +- [OAuth2](docs/usage/oauth2.md) +- [Deep Linking](docs/usage/deep-linking.md) +- [Limitations](docs/usage/limitations.md) +- [Version detection](docs/usage/version-detection.md) + +#### Customization +- [Overview](docs/customization/overview.md) +- [Plugin API](docs/customization/plugin-api.md) +- [Custom layout](docs/customization/custom-layout.md) + +#### Development +- [Setting up](docs/development/setting-up.md) +- [Scripts](docs/development/scripts.md) diff --git a/docs/book.json b/docs/book.json new file mode 100644 index 00000000..f5a5c8fe --- /dev/null +++ b/docs/book.json @@ -0,0 +1,3 @@ +{ + "title": "Swagger-UI" +} diff --git a/docs/customization/custom-layout.md b/docs/customization/custom-layout.md new file mode 100644 index 00000000..e3ab906b --- /dev/null +++ b/docs/customization/custom-layout.md @@ -0,0 +1,92 @@ +# Creating a custom layout + +**Layouts** are a special type of component that Swagger-UI uses as the root component for the entire application. You can define custom layouts in order to have high-level control over what ends up on the page. + +By default, Swagger-UI uses `BaseLayout`, which is built into the application. You can specify a different layout to be used by passing the layout's name as the `layout` parameter to Swagger-UI. Be sure to provide your custom layout as a component to Swagger-UI. + +
+ +For example, if you wanted to create a custom layout that only displayed operations, you could define an `OperationsLayout`: + +```js +import React from "react" + +// Create the layout component +class OperationsLayout extends React.Component { + render() { + const { + getComponent + } = this.props + + const Operations = getComponent("Operations", true) + + return { +
+ +
+ } + } +} + +// Create the plugin that provides our layout component +const OperationsLayoutPlugin = function() { + return { + components: { + OperationsLayout: OperationsLayout + } + } +} + +// Provide the plugin to Swagger-UI, and select OperationsLayout +// as the layout for Swagger-UI +SwaggerUI({ + url: "http://petstore.swagger.io/v2/swagger.json", + plugins: [ OperationsLayoutPlugin ], + layout: "OperationsLayout" +}) +``` + +### Augmenting the default layout + +If you'd like to build around the `BaseLayout` instead of replacing it, you can pull the `BaseLayout` into your custom layout and use it: + +```js +import React from "react" + +// Create the layout component +class AugmentingLayout extends React.Component { + render() { + const { + getComponent + } = this.props + + const BaseLayout = getComponent("BaseLayout", true) + + return { +
+
+

I have a custom header above Swagger-UI!

+
+ +
+ } + } +} + +// Create the plugin that provides our layout component +const AugmentingLayoutPlugin = function() { + return { + components: { + AugmentingLayout: AugmentingLayout + } + } +} + +// Provide the plugin to Swagger-UI, and select AugmentingLayout +// as the layout for Swagger-UI +SwaggerUI({ + url: "http://petstore.swagger.io/v2/swagger.json", + plugins: [ AugmentingLayoutPlugin ], + layout: "AugmentingLayout" +}) +``` diff --git a/docs/customization/overview.md b/docs/customization/overview.md new file mode 100644 index 00000000..c64f0c0c --- /dev/null +++ b/docs/customization/overview.md @@ -0,0 +1,71 @@ +# Plugin system overview + +### Prior art + +Swagger-UI leans heavily on concepts and patterns found in React and Redux. + +If you aren't already familiar, here's some suggested reading: + +- [React: Quick Start (reactjs.org)](https://reactjs.org/docs/hello-world.html) +- [Redux README (redux.js.org)](http://redux.js.org/) + +In the following documentation, we won't take the time to define the fundamentals covered in the resources above. + +> **Note**: Some of the examples in this section contain JSX, which is a syntax extension to JavaScript that is useful for writing React components. +> +> If you don't want to set up a build pipeline capable of translating JSX to JavaScript, take a look at [React without JSX (reactjs.org)](https://reactjs.org/docs/react-without-jsx.html). You can use our `system.React` reference to leverage React without needing to pull a copy into your project. + +### The System + +The _system_ is the heart of the Swagger-UI application. At runtime, it's a JavaScript object that holds many things: + +- React components +- Bound Redux actions and reducers +- Bound Reselect state selectors +- System-wide collection of available components +- Built-in helpers like `getComponent`, `makeMappedContainer`, and `getStore` +- References to the React and Immutable.js libraries (`system.React`, `system.Im`) +- User-defined helper functions + +The system is built up when Swagger-UI is called by iterating through ("compiling") each plugin that Swagger-UI has been given, through the `presets` and `plugins` configuration options. + +### Presets + +Presets are arrays of plugins, which are provided to Swagger-UI through the `presets` configuration option. All plugins within presets are compiled before any plugins provided via the `plugins` configuration option. Consider the following example: + +```javascript +const MyPreset = [FirstPlugin, SecondPlugin, ThirdPlugin] + +SwaggerUI({ + presets: [ + MyPreset + ] +}) +``` + +By default, Swagger-UI includes the internal `ApisPreset`, which contains a set of plugins that provide baseline functionality for Swagger-UI. If you specify your own `presets` option, you need to add the ApisPreset manually, like so: + +```javascript +SwaggerUI({ + presets: [ + SwaggerUI.presets.apis, + MyAmazingCustomPreset + ] +}) +``` + +The need to provide the `apis` preset when adding other presets is an artifact of Swagger-UI's original design, and will likely be removed in the next major version. + +### getComponent + +`getComponent` is a helper function injected into every container component, which is used to get references to components provided by the plugin system. + +All components should be loaded through `getComponent`, since it allows other plugins to modify the component. It is preferred over a conventional `import` statement. + +Container components in Swagger-UI can be loaded by passing `true` as the second argument to `getComponent`, like so: + +```javascript +getComponent("ContainerComponentName", true) +``` + +This will map the current system as props to the component. diff --git a/docs/customization/plugin-api.md b/docs/customization/plugin-api.md new file mode 100644 index 00000000..245b5a14 --- /dev/null +++ b/docs/customization/plugin-api.md @@ -0,0 +1,382 @@ +# Plugins + +A plugin is a function that returns an object - more specifically, the object may contain functions and components that augment and modify Swagger-UI's functionality. + +### Format + +A plugin return value may contain any of these keys, where `myStateKey` is a name for a piece of state: + +```javascript +{ + statePlugins: { + myStateKey: { + actions, + reducers, + selectors, + wrapActions, + wrapSelectors + } + }, + components: {}, + wrapComponents: {}, + fn: {} +} +``` + +### System is provided to plugins + +Let's assume we have a plugin, `NormalPlugin`, that exposes a `doStuff` action under the `normal` state namespace. + +```javascript +const ExtendingPlugin = function(system) { + return { + statePlugins: { + extending: { + actions: { + doExtendedThings: function(...args) { + // you can do other things in here if you want + return system.normalActions.doStuff(...args) + } + } + } + } + } +} +``` + +As you can see, each plugin is passed a reference to the `system` being built up. As long as `NormalPlugin` is compiled before `ExtendingPlugin`, this will work without any issues. + +There is no dependency management built into the plugin system, so if you create a plugin that relies on another, it is your responsibility to make sure that the dependent plugin is loaded _after_ the plugin being depended on. + +### Interfaces + +##### Actions + +```javascript +const MyActionPlugin = () => { + return { + statePlugins: { + example: { + actions: { + updateFavoriteColor: (str) => { + return { + type: "EXAMPLE_SET_FAV_COLOR", + payload: str + } + } + } + } + } + } +} +``` + +Once an action has been defined, you can use it anywhere that you can get a system reference: + +```javascript +// elsewhere +system.exampleActions.updateFavoriteColor("blue") +``` + +The Action interface enables the creation of new Redux action creators within a piece of state in the Swagger-UI system. + +This action creator function will be exposed to container components as `exampleActions.updateFavoriteColor`. When this action creator is called, the return value (which should be a [Flux Standard Action](https://github.com/acdlite/flux-standard-action)) will be passed to the `example` reducer, which we'll define in the next section. + +For more information about the concept of actions in Redux, see the [Redux Actions documentation](http://redux.js.org/docs/basics/Actions.html). + +##### Reducers + +Reducers take a state (which is an [Immutable.js map](https://facebook.github.io/immutable-js/docs/#/Map)) and an action, and return a new state. + +Reducers must be provided to the system under the name of the action type that they handle, in this case, `EXAMPLE_SET_FAV_COLOR`. + +```javascript +const MyReducerPlugin = function(system) { + return { + statePlugins: { + example: { + reducers: { + "EXAMPLE_SET_FAV_COLOR": (state, action) => { + // you're only working with the state under the namespace, in this case "example". + // So you can do what you want, without worrying about /other/ namespaces + return state.set("favColor", action.payload) + } + } + } + } + } +} +``` + +##### Selectors + +Selectors reach into their namespace's state to retrieve or derive data from the state. + +They're an easy way to keep logic in one place, and is preferred over passing state data directly into components. + + +```javascript +const MySelectorPlugin = function(system) { + return { + statePlugins: { + example: { + selectors: { + myFavoriteColor: (state) => state.get("favColor") + } + } + } + } +} +``` + +You can also use the Reselect library to memoize your selectors, which is recommended for any selectors that will see heavy use, since Reselect automatically memoizes selector calls for you: + +```javascript +import { createSelector } from "reselect" + +const MySelectorPlugin = function(system) { + return { + statePlugins: { + example: { + selectors: { + // this selector will be memoized after it is run once for a + // value of `state` + myFavoriteColor: createSelector((state) => state.get("favColor")) + } + } + } + } +} +``` + +Once a selector has been defined, you can use it anywhere that you can get a system reference: +```javascript +system.exampleSelectors.myFavoriteColor() // gets `favColor` in state for you +``` + +##### Components + +You can provide a map of components to be integrated into the system. + +Be mindful of the key names for the components you provide, as you'll need to use those names to refer to the components elsewhere. + +```javascript +class HelloWorldClass extends React.Component { + render() { + return

Hello World!

+ } +} + +const MyComponentPlugin = function(system) { + return { + components: { + HelloWorldClass: HelloWorldClass + // components can just be functions, these are called "stateless components" + HelloWorldStateless: () =>

Hello World!

, + } + } +} +``` + +```javascript +// elsewhere +const HelloWorldStateless = system.getComponent("HelloWorldStateless") +const HelloWorldClass = system.getComponent("HelloWorldClass") +``` + +You can also "cancel out" any components that you don't want by creating a stateless component that always returns `null`: + +```javascript +const NeverShowInfoPlugin = function(system) { + return { + components: { + info: () => null + } + } +} +``` + +##### Wrap-Actions + +Wrap Actions allow you to override the behavior of an action in the system. + +They are function factories with the signature `(oriAction, system) => (...args) => result`. + +A Wrap Action's first argument is `oriAction`, which is the action being wrapped. It is your responsibility to call the `oriAction` - if you don't, the original action will not fire! + +This mechanism is useful for conditionally overriding built-in behaviors, or listening to actions. + +```javascript +// FYI: in an actual Swagger-UI, `updateSpec` is already defined in the core code +// it's just here for clarity on what's behind the scenes +const MySpecPlugin = function(system) { + return { + statePlugins: { + spec: { + actions: { + updateSpec: (str) => { + return { + type: "SPEC_UPDATE_SPEC", + payload: str + } + } + } + } + } + } +} + +// this plugin allows you to watch changes to the spec that is in memory +const MyWrapActionPlugin = function(system) { + return { + statePlugins: { + spec: { + wrapActions: { + updateSpec: (oriAction, system) => (str) => { + // here, you can hand the value to some function that exists outside of Swagger-UI + console.log("Here is my API definition", str) + return oriAction(str) // don't forget! otherwise, Swagger-UI won't update + } + } + } + } + } +} +``` + +##### Wrap-Selectors + +Wrap Selectors allow you to override the behavior of a selector in the system. + +They are function factories with the signature `(oriSelector, system) => (state, ...args) => result`. + +This interface is useful for controlling what data flows into components. We use this in the core code to disable selectors based on the API definition's version. + +```javascript +import { createSelector } from 'reselect' + +// FYI: in an actual Swagger-UI, the `url` spec selector is already defined +// it's just here for clarity on what's behind the scenes +const MySpecPlugin = function(system) { + return { + statePlugins: { + spec: { + selectors: { + url: createSelector( + state => state.get("url") + ) + } + } + } + } +} + +const MyWrapSelectorsPlugin = function(system) { + return { + statePlugins: { + spec: { + wrapSelectors: { + url: (oriSelector, system) => (state, ...args) => { + console.log('someone asked for the spec url!!! it is', state.get('url')) + // you can return other values here... + // but let's just enable the default behavior + return oriSelector(state, ...args) + } + } + } + } + } +} +``` + +##### Wrap-Components + +Wrap Components allow you to override a component registered within the system. + +Wrap Components are function factories with the signature `(OriginalComponent, system) => props => ReactElement`. If you'd prefer to provide a React component class, `(OriginalComponent, system) => ReactClass` works as well. + +```javascript +const MyWrapBuiltinComponentPlugin = function(system) { + return { + wrapComponents: { + info: (Original, system) => (props) => { + return
+

Hello world! I am above the Info component.

+ +
+ } + } + } +} +``` + +Here's another example that includes a code sample of a component that will be wrapped: + +```javascript +///// Overriding a component from a plugin + +// Here's our normal, unmodified component. +const MyNumberDisplayPlugin = function(system) { + return { + components: { + NumberDisplay: ({ number }) => {number} + } + } +} + +// Here's a component wrapper defined as a function. +const MyWrapComponentPlugin = function(system) { + return { + wrapComponents: { + NumberDisplay: (Original, system) => (props) => { + if(props.number > 10) { + return
+

Warning! Big number ahead.

+ +
+ } else { + return + } + } + } + } +} + +// Alternatively, here's the same component wrapper defined as a class. +const MyWrapComponentPlugin = function(system) { + return { + wrapComponents: { + NumberDisplay: (Original, system) => class WrappedNumberDisplay extends React.component { + render() { + if(props.number > 10) { + return
+

Warning! Big number ahead.

+ +
+ } else { + return + } + } + } + } + } +} +``` + + + +##### fn + +The fn interface allows you to add helper functions to the system for use elsewhere. + +```javascript +import leftPad from "left-pad" + +const MyFnPlugin = function(system) { + return { + fn: { + leftPad: leftPad + } + } +} +``` diff --git a/docs/development/scripts.md b/docs/development/scripts.md new file mode 100644 index 00000000..3398d932 --- /dev/null +++ b/docs/development/scripts.md @@ -0,0 +1,30 @@ +# Helpful scripts + +Any of the scripts below can be run by typing `npm run + +``` + +See [unpkg's main page](https://unpkg.com/) for more information on how to use unpkg. diff --git a/docs/usage/limitations.md b/docs/usage/limitations.md new file mode 100644 index 00000000..f618deeb --- /dev/null +++ b/docs/usage/limitations.md @@ -0,0 +1,38 @@ +# Limitations + +### Forbidden header names + +Some header names cannot be controlled by web applications, due to security +features built into web browsers. + +Forbidden headers include: + +> - Accept-Charset +> - Accept-Encoding +> - Access-Control-Request-Headers +> - Access-Control-Request-Method +> - Connection +> - Content-Length +> - Cookie +> - Cookie2 +> - Date +> - DNT +> - Expect +> - Host +> - Keep-Alive +> - Origin +> - Proxy-* +> - Sec-* +> - Referer +> - TE +> - Trailer +> - Transfer-Encoding +> - Upgrade +> - Via +> +> _[Forbidden header names (developer.mozilla.org)](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name)_ + +The biggest impact of this is that OpenAPI 3.0 Cookie parameters cannot be +controlled when running Swagger-UI in a browser. + +For more context, see [#3956](https://github.com/swagger-api/swagger-ui/issues/3956). diff --git a/docs/usage/oauth2.md b/docs/usage/oauth2.md new file mode 100644 index 00000000..b47c097c --- /dev/null +++ b/docs/usage/oauth2.md @@ -0,0 +1,28 @@ +# 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`, +`useBasicAuthenticationWithAccessCodeGrant`. + +Config Name | Description +--- | --- +client_id | Default clientId. MUST be a string +client_secret | Default clientSecret. MUST be a string +realm | realm query parameter (for oauth1) added to `authorizationUrl` and `tokenUrl` . MUST be a string +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` + +```javascript +const ui = SwaggerUI({...}) + +// Method can be called in any place after calling constructor SwaggerUIBundle +ui.initOAuth({ + clientId: "your-client-id", + clientSecret: "your-client-secret-if-required", + realm: "your-realms", + appName: "your-app-name", + scopeSeparator: " ", + additionalQueryStringParams: {test: "hello"} + }) +``` diff --git a/docs/version-detection.md b/docs/usage/version-detection.md similarity index 87% rename from docs/version-detection.md rename to docs/usage/version-detection.md index 01bd4422..2ef2d471 100644 --- a/docs/version-detection.md +++ b/docs/usage/version-detection.md @@ -1,6 +1,6 @@ # Detecting your Swagger-UI version -At times, you're going to need to know which version of Swagger-UI you use. +At times, you're going to need to know which version of Swagger-UI you use. The first step would be to detect which major version you currently use, as the method of detecting the version has changed. If your Swagger-UI has been heavily modified and you cannot detect from the look and feel which major version you use, you'd have to try both methods to get the exact version. @@ -9,7 +9,7 @@ To help you visually detect which version you're using, we've included supportin # Swagger-UI 3.X -![Swagger-UI 3](images/swagger-ui3.png) +![Swagger-UI 3](/docs/images/swagger-ui3.png) Some distinct identifiers to Swagger-UI 3.X: - The API version appears as a badge next to its title. @@ -20,8 +20,8 @@ Some distinct identifiers to Swagger-UI 3.X: If you've determined this is the version you have, to find the exact version: - Open your browser's web console (changes between browsers) -- Type `versions` in the console and execute the call. -- You might need to expand the result, until you get a string similar to `swaggerUi : Object { version: "3.1.6", gitRevision: "g786cd47", gitDirty: true, … }`. +- Type `JSON.stringify(versions)` in the console and execute the call. +- The result should look similar to `swaggerUi : Object { version: "3.1.6", gitRevision: "g786cd47", gitDirty: true, … }`. - The version taken from that example would be `3.1.6`. Note: This functionality was added in 3.0.8. If you're unable to execute it, you're likely to use an older version, and in that case the first step would be to upgrade. @@ -29,7 +29,7 @@ Note: This functionality was added in 3.0.8. If you're unable to execute it, you # Swagger-UI 2.X and under -![Swagger-UI 2](images/swagger-ui2.png) +![Swagger-UI 2](/docs/images/swagger-ui2.png) Some distinct identifiers to Swagger-UI 3.X: - The API version appears at the bottom of the page. @@ -51,4 +51,4 @@ If you've determined this is the version you have, to find the exact version: * @link http://swagger.io * @license Apache-2.0 */ - ``` \ No newline at end of file + ``` diff --git a/make-webpack-config.js b/make-webpack-config.js index 2e81510a..0ab2f156 100644 --- a/make-webpack-config.js +++ b/make-webpack-config.js @@ -137,7 +137,7 @@ module.exports = function(rules, options) { } }, - devtool: specialOptions.sourcemaps ? "nosource-source-map" : null, + devtool: specialOptions.sourcemaps ? "nosource-source-map" : false, plugins, diff --git a/package.json b/package.json index dbaa1c80..f095ffe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swagger-ui", - "version": "3.4.0", + "version": "3.6.1", "main": "dist/swagger-ui.js", "repository": "git@github.com:swagger-api/swagger-ui.git", "contributors": [ @@ -33,16 +33,19 @@ "test-in-node": "npm run lint-errors && npm run just-test-in-node", "just-test": "karma start --config karma.conf.js", "just-test-in-node": "mocha --recursive --compilers js:babel-core/register test/core test/components test/bugs test/swagger-ui-dist-package test/xss", + "just-check-coverage": "nyc npm run just-test-in-node", "test-e2e": "sleep 3 && nightwatch test/e2e/scenarios/ --config test/e2e/nightwatch.json", "e2e-initial-render": "nightwatch test/e2e/scenarios/ --config test/e2e/nightwatch.json --group initial-render", "mock-api": "json-server --watch test/e2e/db.json --port 3204", "e2e": "npm-run-all --parallel -r hot-server mock-api test-e2e" }, "dependencies": { + "@braintree/sanitize-url": "^2.0.2", "base64-js": "^1.2.0", "brace": "0.7.0", "classnames": "^2.2.5", "commonmark": "^0.28.1", + "core-js": "^2.5.1", "css.escape": "1.5.1", "deep-extend": "0.4.1", "expect": "1.20.2", @@ -55,20 +58,20 @@ "memoizee": "0.4.1", "promise-worker": "^1.1.1", "prop-types": "^15.5.10", - "react": "^15.4.0", + "react": "^15.6.2", "react-addons-perf": "^15.4.0", "react-addons-shallow-compare": "0.14.8", - "react-addons-test-utils": "^15.4.0", - "react-collapse": "2.3.1", - "react-dom": "^15.4.0", + "react-addons-test-utils": "^15.6.2", + "react-collapse": "^4.0.3", + "react-dom": "^15.6.2", "react-height": "^2.0.0", "react-hot-loader": "1.3.1", "react-immutable-proptypes": "2.1.0", "react-markdown": "^2.5.0", - "react-motion": "0.4.4", + "react-motion": "^0.5.2", "react-object-inspector": "0.2.1", "react-redux": "^4.x.x", - "react-split-pane": "0.1.57", + "react-split-pane": "0.1.70", "redux": "^3.x.x", "redux-immutable": "3.0.8", "redux-logger": "*", @@ -78,12 +81,14 @@ "scroll-to-element": "^2.0.0", "serialize-error": "2.0.0", "shallowequal": "0.2.2", - "swagger-client": "^3.3.0", + "swagger-client": "^3.4.1", "url-parse": "^1.1.8", "whatwg-fetch": "0.11.1", "worker-loader": "^0.7.1", "xml": "1.0.1", - "yaml-js": "0.2.0" + "xml-but-prettier": "^1.0.1", + "yaml-js": "0.2.0", + "zenscroll": "4.0.0" }, "devDependencies": { "autoprefixer": "7.1.1", @@ -125,6 +130,7 @@ "node-sass": "^4.5.0", "npm-run-all": "4.0.2", "null-loader": "0.1.1", + "nyc": "^11.3.0", "open": "0.0.5", "postcss-loader": "2.0.6", "raw-loader": "0.5.1", @@ -150,5 +156,11 @@ ], "optionalDependencies": { "webpack-dev-server": "2.5.0" + }, + "nyc": { + "all": true, + "include": [ + "**/src/core/plugins/**.js" + ] } } diff --git a/src/core/components/array-model.jsx b/src/core/components/array-model.jsx index dae6645e..0eec9b01 100644 --- a/src/core/components/array-model.jsx +++ b/src/core/components/array-model.jsx @@ -7,15 +7,17 @@ export default class ArrayModel extends Component { static propTypes = { schema: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired, + getConfigs: PropTypes.func.isRequired, specSelectors: PropTypes.object.isRequired, name: PropTypes.string, required: PropTypes.bool, expandDepth: PropTypes.number, + specPath: PropTypes.array.isRequired, depth: PropTypes.number } render(){ - let { getComponent, schema, depth, expandDepth, name } = this.props + let { getComponent, getConfigs, schema, depth, expandDepth, name, specPath } = this.props let description = schema.get("description") let items = schema.get("items") let title = schema.get("title") || name @@ -24,30 +26,29 @@ export default class ArrayModel extends Component { const Markdown = getComponent("Markdown") const ModelCollapse = getComponent("ModelCollapse") const Model = getComponent("Model") + const Property = getComponent("Property") const titleEl = title && { title } - /* + /* Note: we set `name={null}` in below because we don't want the name of the current Model passed (and displayed) as the name of the array element Model - */ + */ return - expandDepth } collapsedContent="[...]"> + [ { - properties.size ? properties.entrySeq().map( ( [ key, v ] ) => -
{ key }: { String(v) }
) - : null + properties.size ? properties.entrySeq().map( ( [ key, v ] ) => ) : null } { !description ? null : } - + ]
diff --git a/src/core/components/auth/api-key-auth.jsx b/src/core/components/auth/api-key-auth.jsx index 2d8a5f52..e9c926b5 100644 --- a/src/core/components/auth/api-key-auth.jsx +++ b/src/core/components/auth/api-key-auth.jsx @@ -60,6 +60,9 @@ export default class ApiKeyAuth extends React.Component { + +

Name: { schema.get("name") }

+

In: { schema.get("in") }

diff --git a/src/core/components/auth/authorize-operation-btn.jsx b/src/core/components/auth/authorize-operation-btn.jsx index 7c2f2448..760bac2a 100644 --- a/src/core/components/auth/authorize-operation-btn.jsx +++ b/src/core/components/auth/authorize-operation-btn.jsx @@ -1,25 +1,23 @@ import React from "react" import PropTypes from "prop-types" -import ImPropTypes from "react-immutable-proptypes" export default class AuthorizeOperationBtn extends React.Component { + static propTypes = { + isAuthorized: PropTypes.bool.isRequired, + onClick: PropTypes.func + } + onClick =(e) => { e.stopPropagation() + let { onClick } = this.props - let { security, authActions, authSelectors } = this.props - let definitions = authSelectors.getDefinitionsByNames(security) - - authActions.showDefinitions(definitions) + if(onClick) { + onClick() + } } render() { - let { security, authSelectors } = this.props - - let isAuthorized = authSelectors.isAuthorized(security) - - if(isAuthorized === null) { - return null - } + let { isAuthorized } = this.props return ( { nonOauthDefinitions.size === authorizedAuth.size ? : diff --git a/src/core/components/auth/oauth2.jsx b/src/core/components/auth/oauth2.jsx index e6dbe6a5..fa980bac 100644 --- a/src/core/components/auth/oauth2.jsx +++ b/src/core/components/auth/oauth2.jsx @@ -200,11 +200,11 @@ export default class Oauth2 extends React.Component {
-