@@ -0,0 +1,38 @@ | |||
{ | |||
"presets": [ | |||
"es2015", | |||
"react", | |||
"stage-0" | |||
], | |||
"plugins": [ | |||
[ | |||
"module-alias", | |||
[ | |||
{ | |||
"expose": "components", | |||
"src": "src/core/components" | |||
}, | |||
{ | |||
"expose": "core", | |||
"src": "src/core" | |||
}, | |||
{ | |||
"expose": "img", | |||
"src": "src/img" | |||
}, | |||
{ | |||
"expose": "corePlugins", | |||
"src": "src/core/plugins" | |||
}, | |||
{ | |||
"expose": "less", | |||
"src": "src/less" | |||
}, | |||
{ | |||
"expose": "base", | |||
"src": "npm:getbase/src/less/base" | |||
} | |||
] | |||
] | |||
] | |||
} |
@@ -0,0 +1,5 @@ | |||
node_modules | |||
.idea | |||
.deps_check | |||
.DS_Store | |||
npm-debug.log |
@@ -0,0 +1,11 @@ | |||
Copyright 2017 SmartBear Software | |||
Licensed under the Apache License, Version 2.0 (the "License"); | |||
you may not use this file except in compliance with the License. | |||
You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. |
@@ -0,0 +1,111 @@ | |||
# Swagger UI | |||
[![NPM version](https://badge.fury.io/js/swagger-ui.svg)](http://badge.fury.io/js/swagger-ui) | |||
## New! | |||
This is the new version of swagger-ui, 3.x. | |||
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: | |||
Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes | Status | |||
------------------ | ------------ | -------------------------- | ----- | ------ | |||
3.0.0 | 2017-03-17 | 2.0 | [tag v3.0.0](https://github.com/swagger-api/swagger-ui/tree/v3.0.0) | | |||
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 | |||
##### Prerequisites | |||
- Node 6.x | |||
- NPM 3.x | |||
If you just want to see your specs, open `public/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`. | |||
##### Browser support | |||
Swagger UI works in the latest versions of Chrome, Safari, Firefox, Edge and IE11. | |||
## 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. | |||
## License | |||
Copyright 2017 SmartBear Software | |||
Licensed under the Apache License, Version 2.0 (the "License"); | |||
you may not use this file except in compliance with the License. | |||
You may obtain a copy of the License at [apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. |
@@ -0,0 +1,28 @@ | |||
function extsToRegExp(exts) { | |||
return new RegExp("\\.(" + exts.map(function(ext) { | |||
return ext.replace(/\./g, "\\."); | |||
}).join("|") + ")(\\?.*)?$"); | |||
} | |||
module.exports = function loadersByExtension(obj) { | |||
var loaders = []; | |||
Object.keys(obj).forEach(function(key) { | |||
var exts = key.split("|"); | |||
var value = obj[key]; | |||
var entry = { | |||
extensions: exts, | |||
test: extsToRegExp(exts) | |||
}; | |||
if(Array.isArray(value)) { | |||
entry.loaders = value; | |||
} else if(typeof value === "string") { | |||
entry.loader = value; | |||
} else { | |||
Object.keys(value).forEach(function(valueKey) { | |||
entry[valueKey] = value[valueKey]; | |||
}); | |||
} | |||
loaders.push(entry); | |||
}); | |||
return loaders; | |||
}; |
@@ -0,0 +1,92 @@ | |||
<!DOCTYPE html> | |||
<!-- this is for the dev server --> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Swagger UI</title> | |||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet"> | |||
<style> | |||
html | |||
{ | |||
box-sizing: border-box; | |||
overflow: -moz-scrollbars-vertical; | |||
overflow-y: scroll; | |||
} | |||
*, | |||
*:before, | |||
*:after | |||
{ | |||
box-sizing: inherit; | |||
} | |||
body { | |||
margin:0; | |||
background: #fafafa; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0"> | |||
<defs> | |||
<symbol viewBox="0 0 20 20" id="unlocked"> | |||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="locked"> | |||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="close"> | |||
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="large-arrow"> | |||
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="large-arrow-down"> | |||
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" id="jump-to"> | |||
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" id="expand"> | |||
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/> | |||
</symbol> | |||
</defs> | |||
</svg> | |||
<div id="swagger-ui"></div> | |||
<!-- don't be alarmed, these don't match what's in dist, because webpack-dev-server serves them in memory. --> | |||
<script src="/dist/SwaggerUIBundle.js"> </script> | |||
<script src="/dist/SwaggerUIStandalonePreset.js"> </script> | |||
<script> | |||
window.onload = function() { | |||
// Build a system | |||
const ui = SwaggerUIBundle({ | |||
url: "http://petstore.swagger.io/v2/swagger.json", | |||
dom_id: '#swagger-ui', | |||
presets: [ | |||
SwaggerUIBundle.presets.apis, | |||
// yay ES6 modules ↘ | |||
Array.isArray(SwaggerUIStandalonePreset) ? SwaggerUIStandalonePreset : SwaggerUIStandalonePreset.default | |||
], | |||
plugins: [ | |||
SwaggerUIBundle.plugins.DownloadUrl | |||
], | |||
layout: "StandaloneLayout" | |||
}) | |||
window.ui = ui | |||
} | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1 @@ | |||
{"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AA6OA;;;;;;AAoIA;AAm7FA;AAwtCA;AAg0IA;;;;;AAkxBA;AAo8IA;AA41GA;AA23FA;AAmqFA;AA0nFA;AA49CA;AAwhDA;AAkrCA;AAumFA;AAmnHA;;;;;;;;;;;;;;AAqjHA;AAyoIA;AAkuJA;AAilHA;AA4kGA;AAwkEA;AAs3DA;AAovDA;AAotBA;AAoqGA;;;;;;AAueA;AAimGA;AA44EA;;;;;AAoGA;AA2qFA;AAo2CA;AAgvDA;AA8tCA;AAoiEA;AA69FA;;;;;;;;;AA20BA;AA2zIA;AAm4DA","sourceRoot":""} |
@@ -0,0 +1 @@ | |||
{"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AAwSA;AAyiGA;AAqwFA;;;;;;AA4eA;AAkvFA;AAu+CA;AAo+CA;AAgrCA;AAgyEA","sourceRoot":""} |
@@ -0,0 +1 @@ | |||
{"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""} |
@@ -0,0 +1 @@ | |||
{"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;AAooGA;AAw0HA;AAijGA;AA6lCA;AA29BA;AAmwCA;AAw4BA","sourceRoot":""} |
@@ -0,0 +1,145 @@ | |||
var path = require('path') | |||
var webpack = require('webpack') | |||
var ExtractTextPlugin = require('extract-text-webpack-plugin') | |||
var deepExtend = require('deep-extend') | |||
var autoprefixer = require('autoprefixer') | |||
var loadersByExtension = require('./build-tools/loadersByExtension') | |||
module.exports = function(options) { | |||
// Special options, that have logic in this file | |||
// ...with defaults | |||
var specialOptions = deepExtend({}, { | |||
hot: false, | |||
separateStylesheets: true, | |||
minimize: false, | |||
longTermCaching: false, | |||
sourcemaps: false, | |||
}, options._special) | |||
var loadersMap = { | |||
'js(x)?': { | |||
loader: 'babel?retainLines=true', | |||
include: [ path.join(__dirname, 'src') ], | |||
}, | |||
'json': 'json-loader', | |||
'txt|yaml': 'raw-loader', | |||
'png|jpg|jpeg|gif|svg': specialOptions.disableAssets ? 'null-loader' : 'url-loader?limit=10000', | |||
'woff|woff2': specialOptions.disableAssets ? 'null-loader' : 'url-loader?limit=100000', | |||
'ttf|eot': specialOptions.disableAssets ? 'null-loader' : 'file-loader', | |||
"worker.js": ["worker-loader?inline=true", "babel"] | |||
} | |||
var plugins = [] | |||
if( specialOptions.separateStylesheets ) { | |||
plugins.push(new ExtractTextPlugin('[name].css' + (specialOptions.longTermCaching ? '?[contenthash]' : ''), { | |||
allChunks: true | |||
})) | |||
} | |||
if( specialOptions.minimize ) { | |||
plugins.push( | |||
new webpack.optimize.UglifyJsPlugin({ | |||
compressor: { | |||
warnings: false | |||
} | |||
}), | |||
new webpack.optimize.DedupePlugin() | |||
) | |||
plugins.push( new webpack.NoErrorsPlugin()) | |||
} | |||
plugins.push( | |||
new webpack.DefinePlugin({ | |||
'process.env': { | |||
NODE_ENV: specialOptions.minimize ? JSON.stringify('production') : null, | |||
WEBPACK_INLINE_STYLES: !Boolean(specialOptions.separateStylesheets) | |||
}, | |||
})) | |||
var cssLoader = 'css-loader!postcss-loader' | |||
var completeStylesheetLoaders = deepExtend({ | |||
'css': cssLoader, | |||
'scss': cssLoader + '!' + 'sass-loader?outputStyle=expanded&sourceMap=true&sourceMapContents=true', | |||
'less': cssLoader + '!' + 'less-loader', | |||
}, specialOptions.stylesheetLoaders) | |||
if(specialOptions.cssModules) { | |||
cssLoader = cssLoader + '?module' + (specialOptions.minimize ? '' : '&localIdentName=[path][name]---[local]---[hash:base64:5]') | |||
} | |||
Object.keys(completeStylesheetLoaders).forEach(function(ext) { | |||
var ori = completeStylesheetLoaders[ext] | |||
if(specialOptions.separateStylesheets) { | |||
completeStylesheetLoaders[ext] = ExtractTextPlugin.extract('style-loader', ori) | |||
} else { | |||
completeStylesheetLoaders[ext] = 'style-loader!' + ori | |||
} | |||
}) | |||
var loaders = loadersByExtension(deepExtend({}, loadersMap, specialOptions.loaders, completeStylesheetLoaders)) | |||
var extraLoaders = (options.module || {} ).loaders | |||
if(Array.isArray(extraLoaders)) { | |||
loaders = loaders.concat(extraLoaders) | |||
delete options.module.loaders | |||
} | |||
var completeConfig = deepExtend({ | |||
entry: {}, | |||
output: { | |||
path: path.join(__dirname, 'dist'), | |||
publicPath: '/', | |||
filename: '[name].js', | |||
chunkFilename: '[name].js' | |||
}, | |||
target: 'web', | |||
// yaml-js has a reference to `fs`, this is a workaround | |||
node: { | |||
fs: 'empty' | |||
}, | |||
module: { | |||
loaders: loaders, | |||
}, | |||
resolveLoader: { | |||
root: path.join(__dirname, 'node_modules'), | |||
}, | |||
externals: { | |||
'buffertools': true // json-react-schema/deeper depends on buffertools, which fails. | |||
}, | |||
resolve: { | |||
root: path.join(__dirname, './src'), | |||
modulesDirectories: ['node_modules'], | |||
extensions: ["", ".web.js", ".js", ".jsx", ".json", ".less"], | |||
packageAlias: 'browser', | |||
alias: { | |||
base: "getbase/src/less/base" | |||
} | |||
}, | |||
postcss: function() { | |||
return [autoprefixer] | |||
}, | |||
devtool: specialOptions.sourcemaps ? 'cheap-module-source-map' : null, | |||
plugins, | |||
}, options) | |||
return completeConfig | |||
} |
@@ -0,0 +1,124 @@ | |||
{ | |||
"name": "swagger-ui", | |||
"version": "3.0.0", | |||
"main": "dist/swagger-ui.js", | |||
"repository": "git@github.com:swagger-api/swagger-ui.git", | |||
"contributors": [ | |||
"(in alphabetical order)", | |||
"Anna Bodnia <anna.bodnia@gmail.com>", | |||
"Buu Nguyen <buunguyen@gmail.com>", | |||
"Josh Ponelat <jponelat@gmail.com>", | |||
"Kyle Shockey <kyleshockey1@gmail.com>", | |||
"Robert Barnwell <robert@robertismy.name>", | |||
"Sahar Jafari <shr.jafari@gmail.com>" | |||
], | |||
"license": "Apache-2.0", | |||
"scripts": { | |||
"start": "http-server -i -a 0.0.0.0 -p 3001", | |||
"build": "npm run build-core && npm run build-bundle && npm run build-standalone", | |||
"build-bundle": "webpack --config webpack-dist-bundle.config.js --colors", | |||
"build-core": "webpack --config webpack-dist.config.js --colors", | |||
"build-standalone": "webpack --config webpack-dist-standalone.config.js --colors", | |||
"predev": "npm install", | |||
"dev": "npm-run-all --parallel hot-server watch open-localhost", | |||
"watch": "webpack --config webpack-watch.config.js --watch --progress", | |||
"open-localhost": "node -e 'require(\"open\")(\"http://localhost:3200\")'", | |||
"hot-server": "webpack-dev-server --host 0.0.0.0 --config webpack-hot-dev-server.config.js --inline --hot --progress --content-base dist/", | |||
"deps-license": "license-checker --production --csv --out $npm_package_config_deps_check_dir/licenses.csv && license-checker --development --csv --out $npm_package_config_deps_check_dir/licenses-dev.csv", | |||
"deps-size": "webpack -p --config webpack.check.js --json | webpack-bundle-size-analyzer >| $npm_package_config_deps_check_dir/sizes.txt", | |||
"deps-check": "npm run deps-license && npm run deps-size", | |||
"just-test-in-node": "mocha --recursive --compilers js:babel-core/register test/core" | |||
}, | |||
"dependencies": { | |||
"brace": "0.7.0", | |||
"btoa": "^1.1.2", | |||
"debounce": "1.0.0", | |||
"deep-extend": "0.4.1", | |||
"expect": "1.20.2", | |||
"getbase": "^2.8.2", | |||
"immutable": "^3.x.x", | |||
"js-yaml": "^3.5.5", | |||
"jsonschema": "^1.1.0", | |||
"less": "2.7.1", | |||
"lodash": "4.17.2", | |||
"matcher": "^0.1.2", | |||
"memoizee": "0.4.1", | |||
"promise-worker": "^1.1.1", | |||
"react": "^15.4.0", | |||
"react-addons-perf": "0.14.8", | |||
"react-addons-shallow-compare": "0.14.8", | |||
"react-addons-test-utils": "0.14.8", | |||
"react-collapse": "2.3.1", | |||
"react-dom": "^15.4.0", | |||
"react-height": "^2.0.0", | |||
"react-hot-loader": "1.3.1", | |||
"react-immutable-proptypes": "2.1.0", | |||
"react-motion": "0.4.4", | |||
"react-object-inspector": "0.2.1", | |||
"react-redux": "^4.x.x", | |||
"react-remarkable": "1.1.1", | |||
"react-split-pane": "0.1.57", | |||
"redux": "^3.x.x", | |||
"redux-immutable": "3.0.8", | |||
"redux-logger": "*", | |||
"reselect": "2.5.3", | |||
"serialize-error": "2.0.0", | |||
"swagger-client": "^3.0.1", | |||
"whatwg-fetch": "0.11.1", | |||
"worker-loader": "^0.7.1", | |||
"xml": "1.0.1", | |||
"yaml-js": "^0.1.3", | |||
"yaml-worker": "^2.1.0" | |||
}, | |||
"devDependencies": { | |||
"autoprefixer": "6.6.1", | |||
"babel-core": "^6.23.1", | |||
"babel-eslint": "^7.1.1", | |||
"babel-loader": "^6.3.2", | |||
"babel-plugin-module-alias": "^1.6.0", | |||
"babel-preset-es2015": "^6.22.0", | |||
"babel-preset-es2015-ie": "^6.6.2", | |||
"babel-preset-react": "^6.23.0", | |||
"babel-preset-stage-0": "^6.22.0", | |||
"babel-runtime": "^6.23.0", | |||
"css-loader": "0.22.0", | |||
"deep-extend": "^0.4.1", | |||
"deepmerge": "^1.3.2", | |||
"extract-text-webpack-plugin": "0.8.2", | |||
"file-loader": "0.8.4", | |||
"html-webpack-plugin": "^2.28.0", | |||
"imports-loader": "0.6.5", | |||
"json-loader": "0.5.3", | |||
"less": "2.5.3", | |||
"less-loader": "2.2.1", | |||
"license-checker": "^8.0.4", | |||
"mocha": "^2.5.3", | |||
"node-sass": "^4.5.0", | |||
"npm-run-all": "3.1.1", | |||
"null-loader": "0.1.1", | |||
"open": "0.0.5", | |||
"postcss-loader": "0.7.0", | |||
"raw-loader": "0.5.1", | |||
"react-hot-loader": "^1.3.1", | |||
"rimraf": "^2.6.0", | |||
"sass-loader": "^6.0.2", | |||
"shallowequal": "0.2.2", | |||
"standard": "^8.6.0", | |||
"standard-loader": "^5.0.0", | |||
"style-loader": "0.13.0", | |||
"url-loader": "0.5.6", | |||
"webpack": "^1.14.0", | |||
"webpack-bundle-size-analyzer": "^2.5.0" | |||
}, | |||
"config": { | |||
"deps_check_dir": ".deps_check" | |||
}, | |||
"browserslist": [ | |||
"> 1%", | |||
"last 2 versions", | |||
"IE 10" | |||
], | |||
"optionalDependencies": { | |||
"webpack-dev-server": "1.14.0" | |||
} | |||
} |
@@ -0,0 +1,93 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Swagger UI</title> | |||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet"> | |||
<link rel="stylesheet" type="text/css" href="../dist/swagger-ui.css" > | |||
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" /> | |||
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" /> | |||
<style> | |||
html | |||
{ | |||
box-sizing: border-box; | |||
overflow: -moz-scrollbars-vertical; | |||
overflow-y: scroll; | |||
} | |||
*, | |||
*:before, | |||
*:after | |||
{ | |||
box-sizing: inherit; | |||
} | |||
body { | |||
margin:0; | |||
background: #fafafa; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0"> | |||
<defs> | |||
<symbol viewBox="0 0 20 20" id="unlocked"> | |||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="locked"> | |||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="close"> | |||
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="large-arrow"> | |||
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" id="large-arrow-down"> | |||
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" id="jump-to"> | |||
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" id="expand"> | |||
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/> | |||
</symbol> | |||
</defs> | |||
</svg> | |||
<div id="swagger-ui"></div> | |||
<script src="../dist/swagger-ui-bundle.js"> </script> | |||
<script src="../dist/swagger-ui-standalone-preset.js"> </script> | |||
<script> | |||
window.onload = function() { | |||
// Build a system | |||
const ui = SwaggerUIBundle({ | |||
url: "http://petstore.swagger.io/v2/swagger.json", | |||
dom_id: '#swagger-ui', | |||
presets: [ | |||
SwaggerUIBundle.presets.apis, | |||
// yay ES6 modules ↘ | |||
Array.isArray(SwaggerUIStandalonePreset) ? SwaggerUIStandalonePreset : SwaggerUIStandalonePreset.default | |||
], | |||
plugins: [ | |||
SwaggerUIBundle.plugins.DownloadUrl | |||
], | |||
layout: "StandaloneLayout" | |||
}) | |||
window.ui = ui | |||
} | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,83 @@ | |||
<!doctype html> | |||
<html lang="en-US"> | |||
<body onload="run()"> | |||
</body> | |||
</html> | |||
<script> | |||
'use strict'; | |||
function run () { | |||
var oauth2 = window.opener.swaggerUIRedirectOauth2; | |||
var sentState = oauth2.state; | |||
var isValid, qp; | |||
qp = (window.location.hash || location.search).substring(1); | |||
qp = qp ? JSON.parse('{"' + qp.replace(/&/g, '","').replace(/=/g, '":"') + '"}', | |||
function (key, value) { | |||
return key === "" ? value : decodeURIComponent(value) | |||
} | |||
) : {} | |||
isValid = qp.state === sentState | |||
if (oauth2.auth.schema.get("flow") === "accessCode" && !oauth2.auth.code) { | |||
if (!isValid) { | |||
oauth2.errCb({ | |||
authId: oauth2.auth.name, | |||
source: "auth", | |||
level: "warning", | |||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server" | |||
}); | |||
} | |||
if (qp.code) { | |||
delete oauth2.state; | |||
oauth2.auth.code = qp.code; | |||
createForm(oauth2.auth, qp).submit(); | |||
} else { | |||
oauth2.errCb({ | |||
authId: oauth2.auth.name, | |||
source: "auth", | |||
level: "error", | |||
message: "Authorization failed: no accessCode came from the server" | |||
}); | |||
window.close(); | |||
} | |||
} else { | |||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid}); | |||
window.close(); | |||
} | |||
} | |||
function createForm(auth, qp) { | |||
var form = document.createElement("form"); | |||
var schema = auth.schema; | |||
var action = schema.get("tokenUrl"); | |||
var name, input; | |||
var fields = { | |||
code: qp.code, | |||
"redirect_uri": location.protocol + "//" + location.host + location.pathname, | |||
"grant_type": "authorization_code", | |||
"client_secret": auth.clientSecret, | |||
"client_id": auth.clientId | |||
} | |||
for ( name in fields ) { | |||
input = document.createElement("input"); | |||
input.name = name; | |||
input.value = fields[name]; | |||
input.type = "hidden"; | |||
form.appendChild(input); | |||
} | |||
form.method = "POST"; | |||
form.action = action; | |||
document.body.appendChild(form); | |||
return form; | |||
} | |||
</script> |
@@ -0,0 +1,6 @@ | |||
/* global ace */ | |||
ace.define("ace/snippets/yaml", | |||
["require","exports","module"], function(e,t,n){ // eslint-disable-line no-unused-vars | |||
t.snippetText=undefined | |||
t.scope="yaml" | |||
}) |
@@ -0,0 +1,27 @@ | |||
import React, { PropTypes } from "react" | |||
export default class App extends React.Component { | |||
getLayout() { | |||
let { getComponent, layoutSelectors } = this.props | |||
const layoutName = layoutSelectors.current() | |||
const Component = getComponent(layoutName, true) | |||
return Component ? Component : ()=> <h1> No layout defined for "{layoutName}" </h1> | |||
} | |||
render() { | |||
const Layout = this.getLayout() | |||
return ( | |||
<Layout/> | |||
) | |||
} | |||
} | |||
App.propTypes = { | |||
getComponent: PropTypes.func.isRequired, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
} | |||
App.defaultProps = { | |||
} |
@@ -0,0 +1,82 @@ | |||
import React, { PropTypes } from "react" | |||
export default class ApiKeyAuth extends React.Component { | |||
static propTypes = { | |||
authorized: PropTypes.object, | |||
getComponent: PropTypes.func.isRequired, | |||
errSelectors: PropTypes.object.isRequired, | |||
schema: PropTypes.object.isRequired, | |||
name: PropTypes.string.isRequired, | |||
onChange: PropTypes.func | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
let { name, schema } = this.props | |||
let value = this.getValue() | |||
this.state = { | |||
name: name, | |||
schema: schema, | |||
value: value | |||
} | |||
} | |||
getValue () { | |||
let { name, authorized } = this.props | |||
return authorized && authorized.getIn([name, "value"]) | |||
} | |||
onChange =(e) => { | |||
let { onChange } = this.props | |||
let value = e.target.value | |||
let newState = Object.assign({}, this.state, { value: value }) | |||
this.setState(newState) | |||
onChange(newState) | |||
} | |||
render() { | |||
let { schema, getComponent, errSelectors, name } = this.props | |||
const Input = getComponent("Input") | |||
const Row = getComponent("Row") | |||
const Col = getComponent("Col") | |||
const AuthError = getComponent("authError") | |||
const Markdown = getComponent( "Markdown" ) | |||
const JumpToPath = getComponent("JumpToPath", true) | |||
let value = this.getValue() | |||
let errors = errSelectors.allErrors().filter( err => err.get("authId") === name) | |||
return ( | |||
<div> | |||
<h4>Api key authorization<JumpToPath path={[ "securityDefinitions", name ]} /></h4> | |||
{ value && <h6>Authorized</h6>} | |||
<Row> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} | |||
source={ schema.get("description") } /> | |||
</Row> | |||
<Row> | |||
<p>Name: <code>{ schema.get("name") }</code></p> | |||
</Row> | |||
<Row> | |||
<p>In: <code>{ schema.get("in") }</code></p> | |||
</Row> | |||
<Row> | |||
<label>Value:</label> | |||
<Col> | |||
{ | |||
value || <Input type="text" onChange={ this.onChange }/> | |||
} | |||
</Col> | |||
</Row> | |||
{ | |||
errors.valueSeq().map( (error, key) => { | |||
return <AuthError error={ error } | |||
key={ key }/> | |||
} ) | |||
} | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
import React, { PropTypes } from "react" | |||
export default class AuthorizationPopup extends React.Component { | |||
close =() => { | |||
let { authActions } = this.props | |||
authActions.showDefinitions(false) | |||
} | |||
render() { | |||
let { authSelectors, authActions, getComponent, errSelectors, specSelectors, fn: { AST } } = this.props | |||
let definitions = authSelectors.shownDefinitions() | |||
const Auths = getComponent("auths") | |||
return ( | |||
<div className="dialog-ux"> | |||
<div className="backdrop-ux"></div> | |||
<div className="modal-ux"> | |||
<div className="modal-dialog-ux"> | |||
<div className="modal-ux-inner"> | |||
<div className="modal-ux-header"> | |||
<h3>Available authorizations</h3> | |||
<button type="button" className="close-modal" onClick={ this.close }> | |||
<svg width="20" height="20"> | |||
<use xlinkHref="#close" /> | |||
</svg> | |||
</button> | |||
</div> | |||
<div className="modal-ux-content"> | |||
{ | |||
definitions.valueSeq().map(( definition, key ) => { | |||
return <Auths key={ key } | |||
AST={AST} | |||
definitions={ definition } | |||
getComponent={ getComponent } | |||
errSelectors={ errSelectors } | |||
authSelectors={ authSelectors } | |||
authActions={ authActions } | |||
specSelectors={ specSelectors }/> | |||
}) | |||
} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
) | |||
} | |||
static propTypes = { | |||
fn: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
authSelectors: PropTypes.object.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
errSelectors: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
import React, { PropTypes } from "react" | |||
export default class AuthorizeBtn extends React.Component { | |||
static propTypes = { | |||
className: PropTypes.string | |||
} | |||
onClick =() => { | |||
let { authActions, authSelectors, errActions} = this.props | |||
let definitions = authSelectors.definitionsToAuthorize() | |||
authActions.showDefinitions(definitions) | |||
} | |||
render() { | |||
let { authSelectors, getComponent } = this.props | |||
//must be moved out of button component | |||
const AuthorizationPopup = getComponent("authorizationPopup", true) | |||
let showPopup = !!authSelectors.shownDefinitions() | |||
let isAuthorized = !!authSelectors.authorized().size | |||
return ( | |||
<div className="auth-wrapper"> | |||
<button className={isAuthorized ? "btn authorize locked" : "btn authorize unlocked"} onClick={ this.onClick }> | |||
<span>Authorize</span> | |||
<svg width="20" height="20"> | |||
<use xlinkHref={ isAuthorized ? "#locked" : "#unlocked" } /> | |||
</svg> | |||
</button> | |||
{ showPopup && <AuthorizationPopup /> } | |||
</div> | |||
) | |||
} | |||
static propTypes = { | |||
getComponent: PropTypes.func.isRequired, | |||
authSelectors: PropTypes.object.isRequired, | |||
errActions: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
import React, { PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
export default class AuthorizeOperationBtn extends React.Component { | |||
onClick =(e) => { | |||
e.stopPropagation() | |||
let { security, authActions, authSelectors } = this.props | |||
let definitions = authSelectors.getDefinitionsByNames(security) | |||
authActions.showDefinitions(definitions) | |||
} | |||
render() { | |||
let { security, authSelectors } = this.props | |||
let isAuthorized = authSelectors.isAuthorized(security) | |||
return ( | |||
<button className={isAuthorized ? "authorization__btn locked" : "authorization__btn unlocked"} onClick={ this.onClick }> | |||
<svg width="20" height="20"> | |||
<use xlinkHref={ isAuthorized ? "#locked" : "#unlocked" } /> | |||
</svg> | |||
</button> | |||
) | |||
} | |||
static propTypes = { | |||
authSelectors: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
security: ImPropTypes.iterable.isRequired | |||
} | |||
} |
@@ -0,0 +1,138 @@ | |||
import React, { PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
export default class Auths extends React.Component { | |||
static propTypes = { | |||
definitions: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
authSelectors: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
specSelectors: PropTypes.object.isRequired | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
this.state = {} | |||
} | |||
onAuthChange =(auth) => { | |||
let { name } = auth | |||
this.setState({ [name]: auth }) | |||
} | |||
submitAuth =(e) => { | |||
e.preventDefault() | |||
let { authActions } = this.props | |||
authActions.authorize(this.state) | |||
} | |||
logoutClick =(e) => { | |||
e.preventDefault() | |||
let { authActions, definitions } = this.props | |||
let auths = definitions.map( (val, key) => { | |||
return key | |||
}).toArray() | |||
authActions.logout(auths) | |||
} | |||
render() { | |||
let { definitions, getComponent, authSelectors, errSelectors, specSelectors } = this.props | |||
const ApiKeyAuth = getComponent("apiKeyAuth") | |||
const BasicAuth = getComponent("basicAuth") | |||
const Oauth2 = getComponent("oauth2", true) | |||
const Button = getComponent("Button") | |||
const JumpToPath = getComponent("JumpToPath", true) | |||
let specStr = specSelectors.specStr() | |||
let authorized = authSelectors.authorized() | |||
let authorizedAuth = definitions.filter( (definition, key) => { | |||
return !!authorized.get(key) | |||
}) | |||
let nonOauthDefinitions = definitions.filter( schema => schema.get("type") !== "oauth2") | |||
let oauthDefinitions = definitions.filter( schema => schema.get("type") === "oauth2") | |||
return ( | |||
<div className="auth-container"> | |||
{ | |||
!!nonOauthDefinitions.size && <form onSubmit={ this.submitAuth }> | |||
{ | |||
nonOauthDefinitions.map( (schema, name) => { | |||
let type = schema.get("type") | |||
let authEl | |||
switch(type) { | |||
case "apiKey": authEl = <ApiKeyAuth key={ name } | |||
schema={ schema } | |||
name={ name } | |||
errSelectors={ errSelectors } | |||
authorized={ authorized } | |||
getComponent={ getComponent } | |||
onChange={ this.onAuthChange } /> | |||
break | |||
case "basic": authEl = <BasicAuth key={ name } | |||
schema={ schema } | |||
name={ name } | |||
errSelectors={ errSelectors } | |||
authorized={ authorized } | |||
getComponent={ getComponent } | |||
onChange={ this.onAuthChange } /> | |||
break | |||
default: authEl = <div key={ name }>Unknown security definition type { type }</div> | |||
} | |||
return (<div key={`${name}-jump`}> | |||
{ authEl } | |||
</div>) | |||
}).toArray() | |||
} | |||
<div className="auth-btn-wrapper"> | |||
{ | |||
nonOauthDefinitions.size === authorizedAuth.size ? <Button className="btn modal-btn auth" onClick={ this.logoutClick }>Logout</Button> | |||
: <Button type="submit" className="btn modal-btn auth authorize">Authorize</Button> | |||
} | |||
</div> | |||
</form> | |||
} | |||
{ | |||
oauthDefinitions && oauthDefinitions.size ? <div> | |||
<div className="scope-def"> | |||
<p>Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.</p> | |||
<p>API requires the following scopes. Select which ones you want to grant to Swagger UI.</p> | |||
</div> | |||
{ | |||
definitions.filter( schema => schema.get("type") === "oauth2") | |||
.map( (schema, name) =>{ | |||
return (<div key={ name }> | |||
<Oauth2 authorized={ authorized } | |||
schema={ schema } | |||
name={ name } /> | |||
</div>) | |||
} | |||
).toArray() | |||
} | |||
</div> : null | |||
} | |||
</div> | |||
) | |||
} | |||
static propTypes = { | |||
errSelectors: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
authSelectors: PropTypes.object.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
definitions: ImPropTypes.iterable.isRequired | |||
} | |||
} |
@@ -0,0 +1,97 @@ | |||
import React, { PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
export default class BasicAuth extends React.Component { | |||
static propTypes = { | |||
authorized: PropTypes.object, | |||
getComponent: PropTypes.func.isRequired, | |||
schema: PropTypes.object.isRequired, | |||
onChange: PropTypes.func.isRequired | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
let { schema, name } = this.props | |||
let value = this.getValue() | |||
let username = value.username | |||
this.state = { | |||
name: name, | |||
schema: schema, | |||
value: !username ? {} : { | |||
username: username | |||
} | |||
} | |||
} | |||
getValue () { | |||
let { authorized, name } = this.props | |||
return authorized && authorized.getIn([name, "value"]) || {} | |||
} | |||
onChange =(e) => { | |||
let { onChange } = this.props | |||
let { value, name } = e.target | |||
let newValue = this.state.value | |||
newValue[name] = value | |||
this.setState({ value: newValue }) | |||
onChange(this.state) | |||
} | |||
render() { | |||
let { schema, getComponent, name, errSelectors } = this.props | |||
const Input = getComponent("Input") | |||
const Row = getComponent("Row") | |||
const Col = getComponent("Col") | |||
const AuthError = getComponent("authError") | |||
const JumpToPath = getComponent("JumpToPath", true) | |||
const Markdown = getComponent( "Markdown" ) | |||
let username = this.getValue().username | |||
let errors = errSelectors.allErrors().filter( err => err.get("authId") === name) | |||
return ( | |||
<div> | |||
<h4>Basic authorization<JumpToPath path={[ "securityDefinitions", name ]} /></h4> | |||
{ username && <h6>Authorized</h6> } | |||
<Row> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} | |||
source={ schema.get("description") } /> | |||
</Row> | |||
<Row> | |||
<Col tablet={2} desktop={2}>username:</Col> | |||
<Col tablet={10} desktop={10}> | |||
{ | |||
username || <Input type="text" required="required" name="username" onChange={ this.onChange }/> | |||
} | |||
</Col> | |||
</Row> | |||
{ | |||
!username && <Row> | |||
<Col tablet={2} desktop={2}>password:</Col> | |||
<Col tablet={10} desktop={10}><Input required="required" autoComplete="new-password" name="password" type="password" onChange={ this.onChange }/></Col> | |||
</Row> | |||
} | |||
{ | |||
errors.valueSeq().map( (error, key) => { | |||
return <AuthError error={ error } | |||
key={ key }/> | |||
} ) | |||
} | |||
</div> | |||
) | |||
} | |||
static propTypes = { | |||
name: PropTypes.string.isRequired, | |||
errSelectors: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
onChange: PropTypes.func, | |||
schema: ImPropTypes.map, | |||
authorized: ImPropTypes.map | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
import React, { PropTypes } from "react" | |||
export default class AuthError extends React.Component { | |||
static propTypes = { | |||
error: PropTypes.object.isRequired | |||
} | |||
render() { | |||
let { error } = this.props | |||
let level = error.get("level") | |||
let message = error.get("message") | |||
let source = error.get("source") | |||
return ( | |||
<div className="errors" style={{ backgroundColor: "#ffeeee", color: "red", margin: "1em" }}> | |||
<b style={{ textTransform: "capitalize", marginRight: "1em"}} >{ source } { level }</b> | |||
<span>{ message }</span> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,218 @@ | |||
import React, { PropTypes } from "react" | |||
import oauth2Authorize from "core/oauth2-authorize" | |||
const IMPLICIT = "implicit" | |||
const ACCESS_CODE = "accessCode" | |||
const PASSWORD = "password" | |||
const APPLICATION = "application" | |||
export default class Oauth2 extends React.Component { | |||
static propTypes = { | |||
name: PropTypes.string, | |||
authorized: PropTypes.object, | |||
configs: PropTypes.object, | |||
getComponent: PropTypes.func.isRequired, | |||
schema: PropTypes.object.isRequired, | |||
authSelectors: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
errSelectors: PropTypes.object.isRequired, | |||
errActions: PropTypes.object.isRequired | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
let { name, schema, authorized } = this.props | |||
let auth = authorized && authorized.get(name) | |||
let username = auth && auth.get("username") || "" | |||
let clientId = auth && auth.get("clientId") || "" | |||
let clientSecret = auth && auth.get("clientSecret") || "" | |||
let passwordType = auth && auth.get("passwordType") || "none" | |||
this.state = { | |||
name: name, | |||
schema: schema, | |||
scopes: [], | |||
clientId: clientId, | |||
clientSecret: clientSecret, | |||
username: username, | |||
password: "", | |||
passwordType: passwordType | |||
} | |||
} | |||
authorize =() => { | |||
let { authActions, errActions, getConfigs } = this.props | |||
let configs = getConfigs() | |||
errActions.clear({authId: name,type: "auth", source: "auth"}) | |||
oauth2Authorize(this.state, authActions, errActions, configs) | |||
} | |||
onScopeChange =(e) => { | |||
let { target } = e | |||
let { checked } = target | |||
let scope = target.dataset.value | |||
if ( checked && this.state.scopes.indexOf(scope) === -1 ) { | |||
let newScopes = this.state.scopes.concat([scope]) | |||
this.setState({ scopes: newScopes }) | |||
} else if ( !checked && this.state.scopes.indexOf(scope) > -1) { | |||
this.setState({ scopes: this.state.scopes.filter((val) => val !== scope) }) | |||
} | |||
} | |||
onInputChange =(e) => { | |||
let { target : { dataset : { name }, value } } = e | |||
let state = { | |||
[name]: value | |||
} | |||
this.setState(state) | |||
} | |||
logout =(e) => { | |||
e.preventDefault() | |||
let { authActions, errActions, name } = this.props | |||
errActions.clear({authId: name, type: "auth", source: "auth"}) | |||
authActions.logout([ name ]) | |||
} | |||
render() { | |||
let { schema, getComponent, authSelectors, errSelectors, name } = this.props | |||
const Input = getComponent("Input") | |||
const Row = getComponent("Row") | |||
const Col = getComponent("Col") | |||
const Button = getComponent("Button") | |||
const AuthError = getComponent("authError") | |||
const JumpToPath = getComponent("JumpToPath", true) | |||
const Markdown = getComponent( "Markdown" ) | |||
let flow = schema.get("flow") | |||
let scopes = schema.get("allowedScopes") || schema.get("scopes") | |||
let authorizedAuth = authSelectors.authorized().get(name) | |||
let isAuthorized = !!authorizedAuth | |||
let errors = errSelectors.allErrors().filter( err => err.get("authId") === name) | |||
let isValid = !errors.filter( err => err.get("source") === "validation").size | |||
return ( | |||
<div> | |||
<h4>OAuth2.0 <JumpToPath path={[ "securityDefinitions", name ]} /></h4> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} | |||
source={ schema.get("description") } /> | |||
{ isAuthorized && <h6>Authorized</h6> } | |||
{ ( flow === IMPLICIT || flow === ACCESS_CODE ) && <p>Authorization URL: <code>{ schema.get("authorizationUrl") }</code></p> } | |||
{ ( flow === PASSWORD || flow === ACCESS_CODE || flow === APPLICATION ) && <p>Token URL:<code> { schema.get("tokenUrl") }</code></p> } | |||
<p className="flow">Flow: <code>{ schema.get("flow") }</code></p> | |||
{ | |||
flow === PASSWORD && ( !isAuthorized || isAuthorized && this.state.username) && <Row> | |||
<Col tablet={2} desktop={2}>username:</Col> | |||
<Col tablet={10} desktop={10}> | |||
{ | |||
isAuthorized ? <span>{ this.state.username }</span> | |||
: <input type="text" data-name="username" onChange={ this.onInputChange }/> | |||
} | |||
</Col> | |||
</Row> | |||
} | |||
{ | |||
flow === PASSWORD && !isAuthorized && <Row> | |||
<Col tablet={2} desktop={2}>password:</Col> | |||
<Col tablet={10} desktop={10}> | |||
<input type="password" data-name="password" onChange={ this.onInputChange }/> | |||
</Col> | |||
</Row> | |||
} | |||
{ | |||
flow === PASSWORD && <Row> | |||
<Col tablet={2} desktop={2}>type:</Col> | |||
<Col tablet={10} desktop={10}> | |||
{ | |||
isAuthorized ? <span>{ this.state.passwordType }</span> | |||
: <select data-name="passwordType" onChange={ this.onInputChange }> | |||
<option value="none">None or other</option> | |||
<option value="basic">Basic auth</option> | |||
<option value="request">Request body</option> | |||
</select> | |||
} | |||
</Col> | |||
</Row> | |||
} | |||
{ | |||
( flow === IMPLICIT || flow === ACCESS_CODE || ( flow === PASSWORD && this.state.passwordType!== "none") ) && | |||
( !isAuthorized || isAuthorized && this.state.clientId) && <Row> | |||
<label htmlFor="client_id">client_id:</label> | |||
<Col tablet={10} desktop={10}> | |||
{ | |||
isAuthorized ? <span>{ this.state.clientId }</span> | |||
: <input id="client_id" type="text" required={ flow === PASSWORD } data-name="clientId" | |||
onChange={ this.onInputChange }/> | |||
} | |||
</Col> | |||
</Row> | |||
} | |||
{ | |||
( flow === ACCESS_CODE || ( flow === PASSWORD && this.state.passwordType!== "none") ) && <Row> | |||
<label htmlFor="client_secret">client_secret:</label> | |||
<Col tablet={10} desktop={10}> | |||
{ | |||
isAuthorized ? <span>{ this.state.clientSecret }</span> | |||
: <input id="client_secret" type="text" data-name="clientSecret" | |||
onChange={ this.onInputChange }/> | |||
} | |||
</Col> | |||
</Row> | |||
} | |||
{ | |||
!isAuthorized && flow !== PASSWORD && scopes && scopes.size ? <div className="scopes"> | |||
<h2>Scopes:</h2> | |||
{ scopes.map((description, name) => { | |||
return ( | |||
<Row key={ name }> | |||
<div className="checkbox"> | |||
<Input data-value={ name } | |||
id={`${name}-checkbox`} | |||
disabled={ isAuthorized } | |||
type="checkbox" | |||
onChange={ this.onScopeChange }/> | |||
<label htmlFor={`${name}-checkbox`}> | |||
<span className="item"></span> | |||
<div className="text"> | |||
<p className="name">{name}</p> | |||
<p className="description">{description}</p> | |||
</div> | |||
</label> | |||
</div> | |||
</Row> | |||
) | |||
}).toArray() | |||
} | |||
</div> : null | |||
} | |||
{ | |||
errors.valueSeq().map( (error, key) => { | |||
return <AuthError error={ error } | |||
key={ key }/> | |||
} ) | |||
} | |||
<div className="auth-btn-wrapper"> | |||
{ isValid && flow !== APPLICATION && | |||
( isAuthorized ? <Button className="btn modal-btn auth authorize" onClick={ this.logout }>Logout</Button> | |||
: <Button className="btn modal-btn auth authorize" onClick={ this.authorize }>Authorize</Button> | |||
) | |||
} | |||
</div> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
import React, { Component, PropTypes } from "react" | |||
export default class Clear extends Component { | |||
onClick =() => { | |||
let { specActions, path, method } = this.props | |||
specActions.clearResponse( path, method ) | |||
specActions.clearRequest( path, method ) | |||
} | |||
render(){ | |||
return ( | |||
<button className="btn btn-clear opblock-control__btn" onClick={ this.onClick }> | |||
Clear | |||
</button> | |||
) | |||
} | |||
static propTypes = { | |||
specActions: PropTypes.object.isRequired, | |||
path: PropTypes.string.isRequired, | |||
method: PropTypes.string.isRequired, | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
import React, { PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
import { fromJS } from 'immutable' | |||
const noop = ()=>{} | |||
export default class ContentType extends React.Component { | |||
static propTypes = { | |||
contentTypes: PropTypes.oneOfType([ImPropTypes.list, ImPropTypes.set]), | |||
value: PropTypes.string, | |||
onChange: PropTypes.func, | |||
className: PropTypes.string | |||
} | |||
static defaultProps = { | |||
onChange: noop, | |||
value: null, | |||
contentTypes: fromJS(["application/json"]), | |||
} | |||
componentDidMount() { | |||
// Needed to populate the form, initially | |||
this.props.onChange(this.props.contentTypes.first()) | |||
} | |||
onChangeWrapper = e => this.props.onChange(e.target.value) | |||
render() { | |||
let { contentTypes, className, value } = this.props | |||
if ( !contentTypes || !contentTypes.size ) | |||
return null | |||
return ( | |||
<div className={ "content-type-wrapper " + ( className || "" ) }> | |||
<select className="content-type" value={value} onChange={this.onChangeWrapper} > | |||
{ contentTypes.map( (val) => { | |||
return <option key={ val } value={ val }>{ val }</option> | |||
}).toArray()} | |||
</select> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
import React, { PropTypes } from "react" | |||
import curlify from "core/curlify" | |||
export default class Curl extends React.Component { | |||
static propTypes = { | |||
request: PropTypes.object.isRequired | |||
} | |||
handleFocus(e) { | |||
e.target.select() | |||
document.execCommand("copy") | |||
} | |||
render() { | |||
let { request } = this.props | |||
let curl = curlify(request) | |||
return ( | |||
<div> | |||
<h4>Curl</h4> | |||
<div className="copy-paste"> | |||
<textarea onFocus={this.handleFocus} className="curl" style={{ whiteSpace: "normal" }} defaultValue={curl}></textarea> | |||
</div> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,51 @@ | |||
import React, { PropTypes } from "react" | |||
import Collapse from "react-collapse" | |||
import { presets } from "react-motion" | |||
import ObjectInspector from "react-object-inspector" | |||
import Perf from "react-addons-perf" | |||
export default class Debug extends React.Component { | |||
constructor() { | |||
super() | |||
this.state = { | |||
jsonDumpOpen: false | |||
} | |||
this.toggleJsonDump = (e) => { | |||
e.preventDefault() | |||
this.setState({jsonDumpOpen: !this.state.jsonDumpOpen}) | |||
} | |||
window.Perf = Perf | |||
} | |||
plusOrMinus(bool) { | |||
return bool ? "-" : "+" | |||
} | |||
render() { | |||
let { getState } = this.props | |||
window.props = this.props | |||
return ( | |||
<div className="info"> | |||
<h3><a onClick={this.toggleJsonDump}> {this.plusOrMinus(this.state.jsonDumpOpen)} App </a></h3> | |||
<Collapse isOpened={this.state.jsonDumpOpen} springConfig={presets.noWobble}> | |||
<ObjectInspector data={getState().toJS() || {}} name="state" initialExpandedPaths={["state"]}/> | |||
</Collapse> | |||
</div> | |||
) | |||
} | |||
} | |||
Debug.propTypes = { | |||
getState: PropTypes.func.isRequired | |||
} | |||
@@ -0,0 +1,113 @@ | |||
import React, { PropTypes } from "react" | |||
import Im, { List } from "immutable" | |||
import Collapse from "react-collapse" | |||
import sortBy from "lodash/sortBy" | |||
export default class Errors extends React.Component { | |||
static propTypes = { | |||
jumpToLine: PropTypes.func, | |||
errSelectors: PropTypes.object.isRequired, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
layoutActions: PropTypes.object.isRequired | |||
} | |||
render() { | |||
let { jumpToLine, errSelectors, layoutSelectors, layoutActions } = this.props | |||
let errors = errSelectors.allErrors() | |||
// all thrown errors, plus error-level everything else | |||
let allErrorsToDisplay = errors.filter(err => err.get("type") === "thrown" ? true :err.get("level") === "error") | |||
if(!allErrorsToDisplay || allErrorsToDisplay.count() < 1) { | |||
return null | |||
} | |||
let isVisible = layoutSelectors.isShown(["errorPane"], true) | |||
let toggleVisibility = () => layoutActions.show(["errorPane"], !isVisible) | |||
let sortedJSErrors = allErrorsToDisplay.sortBy(err => err.get("line")) | |||
return ( | |||
<pre className="errors-wrapper"> | |||
<hgroup className="error"> | |||
<h4 className="errors__title">Errors</h4> | |||
<button className="btn errors__clear-btn" onClick={ toggleVisibility }>{ isVisible ? "Hide" : "Show" }</button> | |||
</hgroup> | |||
<Collapse isOpened={ isVisible } animated > | |||
<div className="errors"> | |||
{ sortedJSErrors.map((err, i) => { | |||
if(err.get("type") === "thrown") { | |||
return <ThrownErrorItem key={ i } error={ err.get("error") || err } jumpToLine={jumpToLine} /> | |||
} | |||
if(err.get("type") === "spec") { | |||
return <SpecErrorItem key={ i } error={ err } jumpToLine={jumpToLine} /> | |||
} | |||
}) } | |||
</div> | |||
</Collapse> | |||
</pre> | |||
) | |||
} | |||
} | |||
const ThrownErrorItem = ( { error, jumpToLine } ) => { | |||
if(!error) { | |||
return null | |||
} | |||
let errorLine = error.get("line") | |||
return ( | |||
<div className="error-wrapper"> | |||
{ !error ? null : | |||
<div> | |||
<h4>{ (error.get("source") && error.get("level")) ? | |||
toTitleCase(error.get("source")) + " " + error.get("level") : "" } | |||
{ error.get("path") ? <small> at {error.get("path")}</small>: null }</h4> | |||
<span style={{ whiteSpace: "pre-line", "maxWidth": "100%" }}> | |||
{ error.get("message") } | |||
</span> | |||
<div> | |||
{ errorLine ? <a onClick={jumpToLine.bind(null, errorLine)}>Jump to line { errorLine }</a> : null } | |||
</div> | |||
</div> | |||
} | |||
</div> | |||
) | |||
} | |||
const SpecErrorItem = ( { error, jumpToLine } ) => { | |||
return ( | |||
<div className="error-wrapper"> | |||
{ !error ? null : | |||
<div> | |||
<h4>{ toTitleCase(error.get("source")) + " " + error.get("level") }{ error.get("path") ? <small> at {List.isList(error.get("path")) ? error.get("path").join(".") : error.get("path")}</small>: null }</h4> | |||
<span style={{ whiteSpace: "pre-line"}}>{ error.get("message") }</span> | |||
<div> | |||
{ jumpToLine ? ( | |||
<a onClick={jumpToLine.bind(null, error.get("line"))}>Jump to line { error.get("line") }</a> | |||
) : null } | |||
</div> | |||
</div> | |||
} | |||
</div> | |||
) | |||
} | |||
function toTitleCase(str) { | |||
return str | |||
.split(" ") | |||
.map(substr => substr[0].toUpperCase() + substr.slice(1)) | |||
.join(" ") | |||
} | |||
ThrownErrorItem.propTypes = { | |||
error: PropTypes.object.isRequired, | |||
jumpToLine: PropTypes.func | |||
} | |||
SpecErrorItem.propTypes = { | |||
error: PropTypes.object.isRequired, | |||
jumpToLine: PropTypes.func | |||
} |
@@ -0,0 +1,41 @@ | |||
import React, { Component, PropTypes } from "react" | |||
import { fromJS } from "immutable" | |||
export default class Execute extends Component { | |||
static propTypes = { | |||
specSelectors: PropTypes.object.isRequired, | |||
specActions: PropTypes.object.isRequired, | |||
operation: PropTypes.object.isRequired, | |||
path: PropTypes.string.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
method: PropTypes.string.isRequired, | |||
onExecute: PropTypes.func | |||
} | |||
onClick=()=>{ | |||
let { specSelectors, specActions, operation, path, method } = this.props | |||
specActions.validateParams( [path, method] ) | |||
if ( specSelectors.validateBeforeExecute([path, method]) ) { | |||
if(this.props.onExecute) { | |||
this.props.onExecute() | |||
} | |||
specActions.execute( { operation, path, method } ) | |||
} | |||
} | |||
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val) | |||
render(){ | |||
let { getComponent, operation, specActions, path, method } = this.props | |||
const ContentType = getComponent( "contentType" ) | |||
return ( | |||
<button className="btn execute opblock-control__btn" onClick={ this.onClick }> | |||
Execute | |||
</button> | |||
) | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import React from "react" | |||
export default class Footer extends React.Component { | |||
render() { | |||
return ( | |||
<div className="footer"></div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,46 @@ | |||
import React, { PropTypes } from "react" | |||
import Im from "immutable" | |||
export default class Headers extends React.Component { | |||
static propTypes = { | |||
headers: PropTypes.object.isRequired | |||
}; | |||
render() { | |||
let { headers } = this.props | |||
if ( !headers || !headers.size ) | |||
return null | |||
return ( | |||
<div className="headers-wrapper"> | |||
<h4 className="headers__title">Headers:</h4> | |||
<table className="headers"> | |||
<thead> | |||
<tr className="header-row"> | |||
<th className="header-col">Name</th> | |||
<th className="header-col">Description</th> | |||
<th className="header-col">Type</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{ | |||
headers.entrySeq().map( ([ key, header ]) => { | |||
if(!Im.Map.isMap(header)) { | |||
return null | |||
} | |||
return (<tr key={ key }> | |||
<td className="header-col">{ key }</td> | |||
<td className="header-col">{ header.get( "description" ) }</td> | |||
<td className="header-col">{ header.get( "type" ) }</td> | |||
</tr>) | |||
}).toArray() | |||
} | |||
</tbody> | |||
</table> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
import React, { Component, PropTypes } from "react" | |||
import { highlight } from "core/utils" | |||
export default class HighlightCode extends Component { | |||
static propTypes = { | |||
value: PropTypes.string.isRequired, | |||
className: PropTypes.string | |||
} | |||
componentDidMount() { | |||
highlight(this.refs.el) | |||
} | |||
componentDidUpdate() { | |||
highlight(this.refs.el) | |||
} | |||
render () { | |||
let { value, className } = this.props | |||
className = className || "" | |||
return <pre ref="el" className={className + " microlight"}>{ value }</pre> | |||
} | |||
} |
@@ -0,0 +1,128 @@ | |||
import React, { PropTypes } from "react" | |||
import { fromJS } from "immutable" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
class Path extends React.Component { | |||
static propTypes = { | |||
host: PropTypes.string, | |||
basePath: PropTypes.string | |||
} | |||
render() { | |||
let { host, basePath } = this.props | |||
return ( | |||
<pre className="base-url"> | |||
[ Base url: {host}{basePath}] | |||
</pre> | |||
) | |||
} | |||
} | |||
class Contact extends React.Component { | |||
static propTypes = { | |||
data: PropTypes.object | |||
} | |||
render(){ | |||
let { data } = this.props | |||
let name = data.get("name") || "the developer" | |||
let url = data.get("url") | |||
let email = data.get("email") | |||
return ( | |||
<div> | |||
{ url && <div><a href={ url } target="_blank">{ name } - Website</a></div> } | |||
{ email && | |||
<a href={`mailto:${email}`}> | |||
{ url ? `Send email to ${name}` : `Contact ${name}`} | |||
</a> | |||
} | |||
</div> | |||
) | |||
} | |||
} | |||
class License extends React.Component { | |||
static propTypes = { | |||
license: PropTypes.object | |||
} | |||
render(){ | |||
let { license } = this.props | |||
let name = license.get("name") || "License" | |||
let url = license.get("url") | |||
return ( | |||
<div> | |||
{ | |||
url ? <a href={ url }>{ name }</a> | |||
: <span>{ name }</span> | |||
} | |||
</div> | |||
) | |||
} | |||
} | |||
export default class Info extends React.Component { | |||
static propTypes = { | |||
info: PropTypes.object, | |||
url: PropTypes.string, | |||
host: PropTypes.string, | |||
basePath: PropTypes.string, | |||
externalDocs: ImPropTypes.map, | |||
getComponent: PropTypes.func.isRequired, | |||
} | |||
render() { | |||
let { info, url, host, basePath, getComponent, externalDocs } = this.props | |||
let version = info.get("version") | |||
let description = info.get("description") | |||
let title = info.get("title") | |||
let termsOfService = info.get("termsOfService") | |||
let contact = info.get("contact") | |||
let license = info.get("license") | |||
const { url:externalDocsUrl, description:externalDocsDescription } = (externalDocs || fromJS({})).toJS() | |||
const Markdown = getComponent("Markdown") | |||
return ( | |||
<div className="info"> | |||
<hgroup className="main"> | |||
<h2 className="title" >{ title } | |||
{ version && <small><pre className="version"> { version } </pre></small> } | |||
</h2> | |||
{ host || basePath ? <Path host={ host } basePath={ basePath } /> : null } | |||
{ url && <a href={ url }><span className="url"> { url } </span></a> } | |||
</hgroup> | |||
<div className="description"> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} source={ description } /> | |||
</div> | |||
{ | |||
termsOfService && <div> | |||
<a href={ termsOfService }>Terms of service</a> | |||
</div> | |||
} | |||
{ contact && contact.size ? <Contact data={ contact } /> : null } | |||
{ license && license.size ? <License license={ license } /> : null } | |||
{ externalDocsUrl ? | |||
<a target="_blank" href={externalDocsUrl}>{externalDocsDescription || externalDocsUrl}</a> | |||
: null } | |||
</div> | |||
) | |||
} | |||
} | |||
Info.propTypes = { | |||
title: PropTypes.any, | |||
description: PropTypes.any, | |||
version: PropTypes.any, | |||
url: PropTypes.string | |||
} |
@@ -0,0 +1,252 @@ | |||
import React, { PropTypes } from "react" | |||
import OriCollapse from "react-collapse" | |||
import _Markdown from "react-remarkable" | |||
const noop = () => {} | |||
function xclass(...args) { | |||
return args.filter(a => !!a).join(" ").trim() | |||
} | |||
export const Markdown = _Markdown | |||
export class Container extends React.Component { | |||
render() { | |||
let { fullscreen, full, ...rest } = this.props | |||
// Normal element | |||
if(fullscreen) | |||
return <section {...rest}/> | |||
let containerClass = "container" + (full ? "-full" : "") | |||
return ( | |||
<section {...rest} className={xclass(rest.className, containerClass)}/> | |||
) | |||
} | |||
} | |||
Container.propTypes = { | |||
fullscreen: PropTypes.bool, | |||
full: PropTypes.bool, | |||
className: PropTypes.string | |||
} | |||
const DEVICES = { | |||
"mobile": "", | |||
"tablet": "-tablet", | |||
"desktop": "-desktop", | |||
"large": "-hd" | |||
} | |||
export class Col extends React.Component { | |||
render() { | |||
const { | |||
hide, | |||
keepContents, | |||
mobile, /* we don't want these in the final component, since React now complains. So we extract them */ | |||
tablet, | |||
desktop, | |||
large, | |||
...rest | |||
} = this.props | |||
if(hide && !keepContents) | |||
return <span/> | |||
let classesAr = [] | |||
for (let device in DEVICES) { | |||
let deviceClass = DEVICES[device] | |||
if(device in this.props) { | |||
let val = this.props[device] | |||
if(val < 1) { | |||
classesAr.push("none" + deviceClass) | |||
continue | |||
} | |||
classesAr.push("block" + deviceClass) | |||
classesAr.push("col-" + val + deviceClass) | |||
} | |||
} | |||
let classes = xclass(rest.className, "clear", ...classesAr) | |||
return ( | |||
<section {...rest} style={{display: hide ? "none": null}} className={classes}/> | |||
) | |||
} | |||
} | |||
Col.propTypes = { | |||
hide: PropTypes.bool, | |||
keepContents: PropTypes.bool, | |||
mobile: PropTypes.number, | |||
tablet: PropTypes.number, | |||
desktop: PropTypes.number, | |||
large: PropTypes.number, | |||
className: PropTypes.string | |||
} | |||
export class Row extends React.Component { | |||
render() { | |||
return <div {...this.props} className={xclass(this.props.className, "wrapper")} /> | |||
} | |||
} | |||
Row.propTypes = { | |||
className: PropTypes.string | |||
} | |||
export class Button extends React.Component { | |||
static propTypes = { | |||
className: PropTypes.string | |||
} | |||
static defaultProps = { | |||
className: "" | |||
} | |||
render() { | |||
return <button {...this.props} className={xclass(this.props.className, "button")} /> | |||
} | |||
} | |||
export const TextArea = (props) => <textarea {...props} /> | |||
export const Input = (props) => <input {...props} /> | |||
export class Select extends React.Component { | |||
static propTypes = { | |||
allowedValues: PropTypes.object, | |||
value: PropTypes.any, | |||
onChange: PropTypes.func, | |||
multiple: PropTypes.bool, | |||
allowEmptyValue: PropTypes.bool | |||
} | |||
static defaultProps = { | |||
multiple: false, | |||
allowEmptyValue: true | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
let value | |||
if (props.value !== undefined) { | |||
value = props.value | |||
} else { | |||
value = props.multiple ? [""] : "" | |||
} | |||
this.state = { value: value } | |||
} | |||
onChange = (e) => { | |||
let { onChange, multiple } = this.props | |||
let options = [].slice.call(e.target.options) | |||
let value | |||
if (multiple) { | |||
value = options.filter(function (option) { | |||
return option.selected | |||
}) | |||
.map(function (option){ | |||
return option.value | |||
}) | |||
} else { | |||
value = e.target.value | |||
} | |||
this.setState({value: value}) | |||
onChange && onChange(value) | |||
} | |||
render(){ | |||
let { allowedValues, multiple, allowEmptyValue } = this.props | |||
let value = this.state.value.toJS ? this.state.value.toJS() : this.state.value | |||
return ( | |||
<select multiple={ multiple } value={ value } onChange={ this.onChange } > | |||
{ allowEmptyValue ? <option value="">--</option> : null } | |||
{ | |||
allowedValues.map(function (item, key) { | |||
return <option key={ key } value={ String(item) }>{ item }</option> | |||
}) | |||
} | |||
</select> | |||
) | |||
} | |||
} | |||
export class Link extends React.Component { | |||
render() { | |||
return <a {...this.props} className={xclass(this.props.className, "link")}/> | |||
} | |||
} | |||
Link.propTypes = { | |||
className: PropTypes.string | |||
} | |||
const NoMargin = ({children}) => <div style={{height: "auto", border: "none", margin: 0, padding: 0}}> {children} </div> | |||
NoMargin.propTypes = { | |||
children: PropTypes.node | |||
} | |||
export class Collapse extends React.Component { | |||
static propTypes = { | |||
isOpened: PropTypes.bool, | |||
children: PropTypes.node.isRequired, | |||
animated: PropTypes.bool | |||
} | |||
static defaultProps = { | |||
isOpened: false, | |||
animated: false | |||
} | |||
renderNotAnimated() { | |||
if(!this.props.isOpened) | |||
return <noscript/> | |||
return ( | |||
<NoMargin> | |||
{this.props.children} | |||
</NoMargin> | |||
) | |||
} | |||
render() { | |||
let { animated, isOpened, children } = this.props | |||
if(!animated) | |||
return this.renderNotAnimated() | |||
children = isOpened ? children : null | |||
return ( | |||
<OriCollapse isOpened={isOpened}> | |||
<NoMargin> | |||
{children} | |||
</NoMargin> | |||
</OriCollapse> | |||
) | |||
} | |||
} |
@@ -0,0 +1,72 @@ | |||
import React, { PropTypes } from "react" | |||
export default class XPane extends React.Component { | |||
render() { | |||
let { getComponent, specSelectors, specActions, layoutSelectors, layoutActions } = this.props | |||
let info = specSelectors.info() | |||
let url = specSelectors.url() | |||
let showEditor = layoutSelectors.isShown("editor") | |||
let Info = getComponent("info") | |||
let Operations = getComponent("operations", true) | |||
let Overview = getComponent("overview", true) | |||
let Editor = getComponent("editor", true) | |||
let Footer = getComponent("footer", true) | |||
let Header = getComponent("header", true) | |||
let Container = getComponent("Container") | |||
let Row = getComponent("Row") | |||
let Col = getComponent("Col") | |||
let Button = getComponent("Button") | |||
let showEditorAction = ()=> layoutActions.show("editor", !showEditor) | |||
return ( | |||
<Container fullscreen> | |||
<Header/> | |||
{ | |||
info && info.size ? <Info version={info.get("version")} | |||
description={info.get("description")} | |||
title={info.get("title")} | |||
url={url}/> | |||
: null | |||
} | |||
<Button onClick={showEditorAction}>{showEditor ? "Hide" : "Show"} Editor</Button> | |||
<Button onClick={specActions.formatIntoYaml}>Format contents</Button> | |||
<Row> | |||
<Col desktop={3} > | |||
<Overview/> | |||
</Col> | |||
<Col hide={!showEditor} keepContents={true} desktop={5} > | |||
<Editor/> | |||
</Col> | |||
<Col desktop={showEditor ? 4 : 9} > | |||
<Operations/> | |||
</Col> | |||
</Row> | |||
<Footer></Footer> | |||
</Container> | |||
) | |||
} | |||
} | |||
XPane.propTypes = { | |||
getComponent: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
specActions: PropTypes.object.isRequired, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
layoutActions: PropTypes.object.isRequired | |||
} | |||
@@ -0,0 +1,91 @@ | |||
import React, { PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
const Headers = ( { headers } )=>{ | |||
return ( | |||
<div> | |||
<h5>Response headers</h5> | |||
<pre>{headers}</pre> | |||
</div>) | |||
} | |||
Headers.propTypes = { | |||
headers: PropTypes.array.isRequired | |||
} | |||
export default class LiveResponse extends React.Component { | |||
static propTypes = { | |||
response: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired | |||
} | |||
render() { | |||
let { request, response, getComponent } = this.props | |||
const Curl = getComponent("curl") | |||
let body = response.get("text") | |||
let status = response.get("status") | |||
let url = response.get("url") | |||
let originalHeaders = response.get("headers") | |||
let headers = originalHeaders && originalHeaders.toJS() | |||
let headersKeys = Object.keys(headers) | |||
let returnObject = headersKeys.map(key => { | |||
return <span className="headerline" key={key}> {key}: {headers[key]} </span> | |||
}) | |||
let notDocumented = response.get("notDocumented") | |||
let ResponseBody = getComponent("responseBody") | |||
let contentType = headers && headers["content-type"] | |||
let isError = response.get("error") | |||
return ( | |||
<div> | |||
{ request && <Curl request={ request }/> } | |||
<h4>Server response</h4> | |||
<table className="responses-table"> | |||
<thead> | |||
<tr className="responses-header"> | |||
<td className="col col_header response-col_status">Code</td> | |||
<td className="col col_header response-col_description">Details</td> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr className="response"> | |||
<td className="col response-col_status"> | |||
{ status } | |||
{ | |||
!notDocumented ? null : | |||
<div className="response-undocumented"> | |||
<i> Undocumented </i> | |||
</div> | |||
} | |||
</td> | |||
<td className="col response-col_description"> | |||
{ | |||
!isError ? null : <span> | |||
{`${response.get("name")}: ${response.get("message")}`} | |||
</span> | |||
} | |||
{ | |||
!body || isError ? null | |||
: <ResponseBody content={ body } | |||
contentType={ contentType } | |||
url={ url } | |||
headers={ headers } | |||
getComponent={ getComponent }/> | |||
} | |||
{ | |||
!headers ? null : <Headers headers={ returnObject }/> | |||
} | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
) | |||
} | |||
static propTypes = { | |||
getComponent: PropTypes.func.isRequired, | |||
request: ImPropTypes.map, | |||
response: ImPropTypes.map | |||
} | |||
} |
@@ -0,0 +1,58 @@ | |||
import React, { PropTypes } from "react" | |||
export default class ModelExample extends React.Component { | |||
static propTypes = { | |||
getComponent: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
schema: PropTypes.object.isRequired, | |||
example: PropTypes.any.isRequired, | |||
isExecute: PropTypes.bool | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
this.state = { | |||
activeTab: "example" | |||
} | |||
} | |||
activeTab =( e ) => { | |||
let { target : { dataset : { name } } } = e | |||
this.setState({ | |||
activeTab: name | |||
}) | |||
} | |||
render() { | |||
let { getComponent, specSelectors, schema, example, isExecute } = this.props | |||
const Model = getComponent("model") | |||
return <div> | |||
<ul className="tab"> | |||
<li className={ "tabitem" + ( isExecute || this.state.activeTab === "example" ? " active" : "") }> | |||
<a className="tablinks" data-name="example" onClick={ this.activeTab }>Example Value</a> | |||
</li> | |||
<li className={ "tabitem" + ( !isExecute && this.state.activeTab === "model" ? " active" : "") }> | |||
<a className={ "tablinks" + ( isExecute ? " inactive" : "" )} data-name="model" onClick={ this.activeTab }>Model</a> | |||
</li> | |||
</ul> | |||
<div> | |||
{ | |||
(isExecute || this.state.activeTab === "example") && example | |||
} | |||
{ | |||
!isExecute && this.state.activeTab === "model" && <Model schema={ schema } | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors } | |||
expandDepth={ 1 } /> | |||
} | |||
</div> | |||
</div> | |||
} | |||
} |
@@ -0,0 +1,300 @@ | |||
import React, { Component, PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
import isObject from "lodash/isObject" | |||
import { List } from "immutable" | |||
const braceOpen = "{" | |||
const braceClose = "}" | |||
const EnumModel = ({ value }) => { | |||
let collapsedContent = <span>Array [ { value.count() } ]</span> | |||
return <span className="prop-enum"> | |||
Enum:<br /> | |||
<Collapse collapsedContent={ collapsedContent }> | |||
[ { value.join(", ") } ] | |||
</Collapse> | |||
</span> | |||
} | |||
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 JumpToPathSection = ({ name }) => <span className="model-jump-to-path"><JumpToPath path={`definitions.${name}`} /></span> | |||
let collapsedContent = (<span> | |||
<span>{ braceOpen }</span>...<span>{ braceClose }</span> | |||
{ | |||
isRef ? <JumpToPathSection name={ name }/> : "" | |||
} | |||
</span>) | |||
return <span className="model"> | |||
{ | |||
title && <span className="model-title"> | |||
{ isRef && schema.get("$$ref") && <span className="model-hint">{ schema.get("$$ref") }</span> } | |||
<span className="model-title__text">{ title }</span> | |||
</span> | |||
} | |||
<Collapse collapsed={ depth > expandDepth } collapsedContent={ collapsedContent }> | |||
<span className="brace-open object">{ braceOpen }</span> | |||
{ | |||
!isRef ? null : <JumpToPathSection name={ name }/> | |||
} | |||
<span className="inner-object"> | |||
{ | |||
<table className="model" style={{ marginLeft: "2em" }}><tbody> | |||
{ | |||
!description ? null : <tr style={{ color: "#999", fontStyle: "italic" }}> | |||
<td>description:</td> | |||
<td>{ description }</td> | |||
</tr> | |||
} | |||
{ | |||
!(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 (<tr key={key}> | |||
<td style={ propertyStyle }>{ key }:</td> | |||
<td style={{ verticalAlign: "top" }}> | |||
<Model key={ `object-${name}-${key}_${value}` } { ...props } | |||
required={ isRequired } | |||
getComponent={ getComponent } | |||
schema={ value } | |||
depth={ depth + 1 } /> | |||
</td> | |||
</tr>) | |||
}).toArray() | |||
} | |||
{ | |||
!additionalProperties || !additionalProperties.size ? null | |||
: <tr> | |||
<td>{ "< * >:" }</td> | |||
<td> | |||
<Model { ...props } required={ false } | |||
getComponent={ getComponent } | |||
schema={ additionalProperties } | |||
depth={ depth + 1 } /> | |||
</td> | |||
</tr> | |||
} | |||
</tbody></table> | |||
} | |||
</span> | |||
<span className="brace-close">{ braceClose }</span> | |||
</Collapse> | |||
</span> | |||
} | |||
} | |||
class Primitive extends Component { | |||
static propTypes = { | |||
schema: PropTypes.object.isRequired, | |||
required: PropTypes.bool | |||
} | |||
render(){ | |||
let { schema, required } = this.props | |||
if(!schema || !schema.get) { | |||
// don't render if schema isn't correctly formed | |||
return <div></div> | |||
} | |||
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", "$$ref"].indexOf(key) === -1 ) | |||
let style = required ? { fontWeight: "bold" } : {} | |||
let propStyle = { color: "#999", fontStyle: "italic" } | |||
return <span className="prop"> | |||
<span className="prop-type" style={ style }>{ type }</span> { required && <span style={{ color: "red" }}>*</span>} | |||
{ format && <span className="prop-format">(${format})</span>} | |||
{ | |||
properties.size ? properties.entrySeq().map( ( [ key, v ] ) => <span key={`${key}-${v}`} style={ propStyle }> | |||
<br />{ key !== "description" && key + ": " }{ String(v) }</span>) | |||
: null | |||
} | |||
{ | |||
xml && xml.size ? (<span><br /><span style={ propStyle }>xml:</span> | |||
{ | |||
xml.entrySeq().map( ( [ key, v ] ) => <span key={`${key}-${v}`} style={ propStyle }><br/> {key}: { String(v) }</span>).toArray() | |||
} | |||
</span>): null | |||
} | |||
{ | |||
enumArray && <EnumModel value={ enumArray } /> | |||
} | |||
</span> | |||
} | |||
} | |||
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, expandDepth } = this.props | |||
let items = schema.get("items") | |||
return <span> | |||
<Collapse collapsed={ depth > expandDepth } collapsedContent="[...]"> | |||
[ | |||
<span><Model { ...this.props } schema={ items } required={ false }/></span> | |||
] | |||
</Collapse> | |||
{ required && <span style={{ color: "red" }}>*</span>} | |||
</span> | |||
} | |||
} | |||
class Model extends Component { | |||
static propTypes = { | |||
schema: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
name: PropTypes.string, | |||
isRef: PropTypes.bool, | |||
required: PropTypes.bool, | |||
expandDepth: PropTypes.number, | |||
depth: PropTypes.number | |||
} | |||
getModelName =( ref )=> { | |||
if ( ref.indexOf("#/definitions/") !== -1 ) { | |||
return ref.replace(/^.*#\/definitions\//, "") | |||
} | |||
} | |||
getRefSchema =( model )=> { | |||
let { specSelectors } = this.props | |||
return specSelectors.findDefinition(model) | |||
} | |||
render () { | |||
let { schema, required, name, isRef } = this.props | |||
let $$ref = schema && schema.get("$$ref") | |||
let modelName = $$ref && this.getModelName( $$ref ) | |||
let modelSchema, type | |||
if ( schema && (schema.get("type") || schema.get("properties")) ) { | |||
modelSchema = schema | |||
} else if ( $$ref ) { | |||
modelSchema = this.getRefSchema( modelName ) | |||
} | |||
type = modelSchema && modelSchema.get("type") | |||
if ( !type && modelSchema && modelSchema.get("properties") ) { | |||
type = "object" | |||
} | |||
switch(type) { | |||
case "object": | |||
return <ObjectModel className="object" { ...this.props } schema={ modelSchema } | |||
name={ modelName || name } | |||
isRef={ isRef!== undefined ? isRef : !!$$ref }/> | |||
case "array": | |||
return <ArrayModel className="array" { ...this.props } schema={ modelSchema } required={ required } /> | |||
case "string": | |||
case "number": | |||
case "integer": | |||
case "boolean": | |||
default: | |||
return <Primitive schema={ modelSchema } required={ required }/> | |||
} | |||
} | |||
} | |||
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 { name, schema } = this.props | |||
let title = schema.get("title") || name | |||
return <div className="model-box"> | |||
<Model { ...this.props } depth={ 1 } expandDepth={ this.props.expandDepth || 0 }/> | |||
</div> | |||
} | |||
} | |||
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 (<span> | |||
<span onClick={ this.toggleCollapsed } style={{ "cursor": "pointer" }}> | |||
<span className={ "model-toggle" + ( this.state.collapsed ? " collapsed" : "" ) }></span> | |||
</span> | |||
{ this.state.collapsed ? this.state.collapsedContent : this.props.children } | |||
</span>) | |||
} | |||
} |
@@ -0,0 +1,42 @@ | |||
import React, { Component, PropTypes } from "react" | |||
export default class Models extends Component { | |||
static propTypes = { | |||
getComponent: PropTypes.func, | |||
specSelectors: PropTypes.object | |||
} | |||
render(){ | |||
let { specSelectors, getComponent, layoutSelectors, layoutActions } = this.props | |||
let definitions = specSelectors.definitions() | |||
let showModels = layoutSelectors.isShown('models', true) | |||
const Model = getComponent("model") | |||
const Collapse = getComponent("Collapse") | |||
if (!definitions.size) return null | |||
return <section className={ showModels ? "models is-open" : "models"}> | |||
<h4 onClick={() => layoutActions.show('models', !showModels)}> | |||
<span>Models</span> | |||
<svg width="20" height="20"> | |||
<use xlinkHref="#large-arrow" /> | |||
</svg> | |||
</h4> | |||
<Collapse isOpened={showModels} animated> | |||
{ | |||
definitions.entrySeq().map( ( [ name, model ])=>{ | |||
return <div className="model-container" key={ `models-section-${name}` }> | |||
<Model name={ name } | |||
schema={ model } | |||
isRef={ true } | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors }/> | |||
</div> | |||
}).toArray() | |||
} | |||
</Collapse> | |||
</section> | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
import React from "react" | |||
export default class OnlineValidatorBadge extends React.Component { | |||
constructor(props, context) { | |||
super(props, context) | |||
let { specSelectors, getConfigs } = props | |||
let { validatorUrl } = getConfigs() | |||
this.state = { | |||
url: specSelectors.url(), | |||
validatorUrl: validatorUrl | |||
} | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
let { specSelectors, getConfigs } = nextProps | |||
let { validatorUrl } = getConfigs() | |||
this.setState({ | |||
url: specSelectors.url(), | |||
validatorUrl: validatorUrl | |||
}) | |||
} | |||
render() { | |||
let { getConfigs } = this.props | |||
let { spec } = getConfigs() | |||
if ( typeof spec === "object" && Object.keys(spec).length) return null | |||
if (!this.state.url) { | |||
return null | |||
} | |||
return (<span style={{ float: "right"}}> | |||
<a target="_blank" href={`${ this.state.validatorUrl }/debug?url=${ this.state.url }`}> | |||
<img alt="Online validator badge" src={`${ this.state.validatorUrl }?url=${ this.state.url }`} /> | |||
</a> | |||
</span>) | |||
} | |||
} |
@@ -0,0 +1,258 @@ | |||
import React, { PropTypes } from "react" | |||
import { Map, fromJS } from "immutable" | |||
import shallowCompare from "react-addons-shallow-compare" | |||
import { getList } from "core/utils" | |||
import * as CustomPropTypes from "core/proptypes" | |||
//import "less/opblock" | |||
export default class Operation extends React.Component { | |||
static propTypes = { | |||
path: PropTypes.string.isRequired, | |||
method: PropTypes.string.isRequired, | |||
operation: PropTypes.object.isRequired, | |||
showSummary: PropTypes.bool, | |||
isShownKey: CustomPropTypes.arrayOrString.isRequired, | |||
jumpToKey: CustomPropTypes.arrayOrString.isRequired, | |||
allowTryItOut: PropTypes.bool, | |||
response: PropTypes.object, | |||
request: PropTypes.object, | |||
getComponent: PropTypes.func.isRequired, | |||
authActions: PropTypes.object, | |||
authSelectors: PropTypes.object, | |||
specActions: PropTypes.object.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
layoutActions: PropTypes.object.isRequired, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
fn: PropTypes.object.isRequired | |||
} | |||
static defaultProps = { | |||
showSummary: true, | |||
response: null, | |||
allowTryItOut: true, | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
this.state = { | |||
tryItOutEnabled: false | |||
} | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
const defaultContentType = "application/json" | |||
let { specActions, path, method, operation } = nextProps | |||
let producesValue = operation.get("produces_value") | |||
let produces = operation.get("produces") | |||
let consumes = operation.get("consumes") | |||
let consumesValue = operation.get("consumes_value") | |||
if(nextProps.response !== this.props.response) { | |||
this.setState({ executeInProgress: false }) | |||
} | |||
if (producesValue === undefined) { | |||
producesValue = produces && produces.size ? produces.first() : defaultContentType | |||
specActions.changeProducesValue([path, method], producesValue) | |||
} | |||
if (consumesValue === undefined) { | |||
consumesValue = consumes && consumes.size ? consumes.first() : defaultContentType | |||
specActions.changeConsumesValue([path, method], consumesValue) | |||
} | |||
} | |||
shouldComponentUpdate(props, state) { | |||
return shallowCompare(this, props, state) | |||
} | |||
toggleShown =() => { | |||
let { layoutActions, isShownKey } = this.props | |||
layoutActions.show(isShownKey, !this.isShown()) | |||
} | |||
isShown =() => { | |||
let { layoutSelectors, isShownKey } = this.props | |||
return layoutSelectors.isShown(isShownKey, false ) // Here is where we set the default | |||
} | |||
onTryoutClick =() => { | |||
this.setState({tryItOutEnabled: !this.state.tryItOutEnabled}) | |||
} | |||
onCancelClick =() => { | |||
let { specActions, path, method } = this.props | |||
this.setState({tryItOutEnabled: !this.state.tryItOutEnabled}) | |||
specActions.clearValidateParams([path, method]) | |||
} | |||
onExecute = () => { | |||
this.setState({ executeInProgress: true }) | |||
} | |||
render() { | |||
let { | |||
isShownKey, | |||
jumpToKey, | |||
path, | |||
method, | |||
operation, | |||
showSummary, | |||
response, | |||
request, | |||
allowTryItOut, | |||
fn, | |||
getComponent, | |||
specActions, | |||
specSelectors, | |||
authActions, | |||
authSelectors, | |||
layoutSelectors, | |||
layoutActions, | |||
} = this.props | |||
let summary = operation.get("summary") | |||
let description = operation.get("description") | |||
let deprecated = operation.get("deprecated") | |||
let externalDocs = operation.get("externalDocs") | |||
let responses = operation.get("responses") | |||
let security = operation.get("security") || specSelectors.security() | |||
let produces = operation.get("produces") | |||
let schemes = operation.get("schemes") | |||
let parameters = getList(operation, ["parameters"]) | |||
const Responses = getComponent("responses") | |||
const Parameters = getComponent( "parameters" ) | |||
const Execute = getComponent( "execute" ) | |||
const Clear = getComponent( "clear" ) | |||
const AuthorizeOperationBtn = getComponent( "authorizeOperationBtn" ) | |||
const JumpToPath = getComponent("JumpToPath", true) | |||
const Collapse = getComponent( "Collapse" ) | |||
const Markdown = getComponent( "Markdown" ) | |||
const Schemes = getComponent( "schemes" ) | |||
// Merge in Live Response | |||
if(response && response.size > 0) { | |||
let notDocumented = !responses.get(String(response.get("status"))) | |||
response = response.set("notDocumented", notDocumented) | |||
} | |||
let { tryItOutEnabled } = this.state | |||
let shown = this.isShown() | |||
let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method ) | |||
return ( | |||
<div className={deprecated ? "opblock opblock-deprecated" : shown ? `opblock opblock-${method} is-open` : `opblock opblock-${method}`} id={isShownKey} > | |||
<div className={`opblock-summary opblock-summary-${method}`} onClick={this.toggleShown} > | |||
<span className="opblock-summary-method">{method.toUpperCase()}</span> | |||
<span className={ deprecated ? "opblock-summary-path__deprecated" : "opblock-summary-path" } > | |||
<span>{path}</span> | |||
<JumpToPath path={jumpToKey} /> | |||
</span> | |||
{ !showSummary ? null : | |||
<div className="opblock-summary-description"> | |||
{ summary } | |||
</div> | |||
} | |||
{ | |||
(!security || !security.count()) ? null : | |||
<AuthorizeOperationBtn authActions={ authActions } | |||
security={ security } | |||
authSelectors={ authSelectors }/> | |||
} | |||
</div> | |||
<Collapse isOpened={shown} animated> | |||
<div className="opblock-body"> | |||
{ deprecated && <h4 className="opblock-title_normal"> Warning: Deprecated</h4>} | |||
{ description && | |||
<div className="opblock-description-wrapper"> | |||
<div className="opblock-description"> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} source={ description } /> | |||
</div> | |||
</div> | |||
} | |||
{ | |||
externalDocs && externalDocs.get("url") ? | |||
<div className="opblock-external-docs-wrapper"> | |||
<h4 className="opblock-title_normal">Find more details</h4> | |||
<div className="opblock-external-docs"> | |||
<span className="opblock-external-docs__description">{ externalDocs.get("description") }</span> | |||
<a className="opblock-external-docs__link" href={ externalDocs.get("url") }>{ externalDocs.get("url") }</a> | |||
</div> | |||
</div> : null | |||
} | |||
<Parameters | |||
parameters={parameters} | |||
onChangeKey={onChangeKey} | |||
onTryoutClick = { this.onTryoutClick } | |||
onCancelClick = { this.onCancelClick } | |||
tryItOutEnabled = { tryItOutEnabled } | |||
allowTryItOut={allowTryItOut} | |||
fn={fn} | |||
getComponent={ getComponent } | |||
specActions={ specActions } | |||
specSelectors={ specSelectors } | |||
pathMethod={ [path, method] } | |||
/> | |||
{!tryItOutEnabled || !allowTryItOut ? null : schemes && schemes.size ? <Schemes schemes={ schemes } | |||
path={ path } | |||
method={ method } | |||
specActions={ specActions }/> | |||
: null | |||
} | |||
<div className={(!tryItOutEnabled || !response || !allowTryItOut) ? "execute-wrapper" : "btn-group"}> | |||
{ !tryItOutEnabled || !allowTryItOut ? null : | |||
<Execute | |||
getComponent={getComponent} | |||
operation={ operation } | |||
specActions={ specActions } | |||
specSelectors={ specSelectors } | |||
path={ path } | |||
method={ method } | |||
onExecute={ this.onExecute } /> | |||
} | |||
{ (!tryItOutEnabled || !response || !allowTryItOut) ? null : | |||
<Clear | |||
onClick={ this.onClearClick } | |||
specActions={ specActions } | |||
path={ path } | |||
method={ method }/> | |||
} | |||
</div> | |||
{this.state.executeInProgress ? <div className="loading-container"><div className="loading"></div></div> : null} | |||
{ !responses ? null : | |||
<Responses | |||
responses={ responses } | |||
request={ request } | |||
tryItOutResponse={ response } | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors } | |||
specActions={ specActions } | |||
produces={ produces } | |||
producesValue={ operation.get("produces_value") } | |||
pathMethod={ [path, method] } | |||
fn={fn} /> | |||
} | |||
</div> | |||
</Collapse> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,133 @@ | |||
import React, { PropTypes } from "react" | |||
import {presets} from "react-motion" | |||
export default class Operations extends React.Component { | |||
static propTypes = { | |||
specSelectors: PropTypes.object.isRequired, | |||
specActions: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
layoutActions: PropTypes.object.isRequired, | |||
authActions: PropTypes.object.isRequired, | |||
authSelectors: PropTypes.object.isRequired, | |||
}; | |||
static defaultProps = { | |||
}; | |||
render() { | |||
let { | |||
specSelectors, | |||
specActions, | |||
getComponent, | |||
layoutSelectors, | |||
layoutActions, | |||
authActions, | |||
authSelectors, | |||
fn | |||
} = this.props | |||
let taggedOps = specSelectors.taggedOperations() | |||
const Operation = getComponent("operation") | |||
const Collapse = getComponent("Collapse") | |||
const Schemes = getComponent("schemes") | |||
let showSummary = layoutSelectors.showSummary() | |||
return ( | |||
<div> | |||
{ | |||
taggedOps.map( (tagObj, tag) => { | |||
let operations = tagObj.get("operations") | |||
let tagDescription = tagObj.getIn(["tagDetails", "description"], null) | |||
let isShownKey = ["operations-tag", tag] | |||
let showTag = layoutSelectors.isShown(isShownKey, true) | |||
return ( | |||
<div className={showTag ? "opblock-tag-section is-open" : "opblock-tag-section"} key={"operation-" + tag}> | |||
<h4 className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }> | |||
<span onClick={() => layoutActions.show(isShownKey, !showTag)}>{tag}</span> | |||
{ !tagDescription ? null : | |||
<small onClick={() => layoutActions.show(isShownKey, !showTag)} > | |||
{ tagDescription } | |||
</small> | |||
} | |||
<button className="expand-methods" title="Expand all methods"> | |||
<svg className="expand" width="20" height="20"> | |||
<use xlinkHref="#expand" /> | |||
</svg> | |||
</button> | |||
<button className="expand-operation" title="Expand operation" onClick={() => layoutActions.show(isShownKey, !showTag)}> | |||
<svg className="arrow" width="20" height="20"> | |||
<use xlinkHref={showTag ? "#large-arrow-down" : "#large-arrow"} /> | |||
</svg> | |||
</button> | |||
</h4> | |||
<Collapse isOpened={showTag}> | |||
{ | |||
operations.map( op => { | |||
const isShownKey = ["operations", op.get("id"), tag] | |||
const path = op.get("path", "") | |||
const method = op.get("method", "") | |||
const jumpToKey = `paths.${path}.${method}` | |||
const allowTryItOut = specSelectors.allowTryItOutFor(op.get("path"), op.get("method")) | |||
const response = specSelectors.responseFor(op.get("path"), op.get("method")) | |||
const request = specSelectors.requestFor(op.get("path"), op.get("method")) | |||
return <Operation | |||
{...op.toObject()} | |||
isShownKey={isShownKey} | |||
jumpToKey={jumpToKey} | |||
showSummary={showSummary} | |||
key={isShownKey} | |||
response={ response } | |||
request={ request } | |||
allowTryItOut={allowTryItOut} | |||
specActions={ specActions } | |||
specSelectors={ specSelectors } | |||
layoutActions={ layoutActions } | |||
layoutSelectors={ layoutSelectors } | |||
authActions={ authActions } | |||
authSelectors={ authSelectors } | |||
getComponent={ getComponent } | |||
fn={fn} | |||
/> | |||
}).toArray() | |||
} | |||
</Collapse> | |||
</div> | |||
) | |||
}).toArray() | |||
} | |||
{ taggedOps.size < 1 ? <h3> No operations defined in spec! </h3> : null } | |||
</div> | |||
) | |||
} | |||
} | |||
Operations.propTypes = { | |||
layoutActions: PropTypes.object.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
specActions: PropTypes.object.isRequired, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
fn: PropTypes.object.isRequired | |||
} |
@@ -0,0 +1,119 @@ | |||
import React, { PropTypes } from "react" | |||
import { Link } from "core/components/layout-utils" | |||
export default class Overview extends React.Component { | |||
constructor(...args) { | |||
super(...args) | |||
this.setTagShown = this._setTagShown.bind(this) | |||
} | |||
_setTagShown(showTagId, shown) { | |||
this.props.layoutActions.show(showTagId, shown) | |||
} | |||
showOp(key, shown) { | |||
let { layoutActions } = this.props | |||
layoutActions.show(key, shown) | |||
} | |||
render() { | |||
let { specSelectors, layoutSelectors, layoutActions, getComponent } = this.props | |||
let taggedOps = specSelectors.taggedOperations() | |||
const Collapse = getComponent("Collapse") | |||
return ( | |||
<div> | |||
<h4 className="overview-title">Overview</h4> | |||
{ | |||
taggedOps.map( (tagObj, tag) => { | |||
let operations = tagObj.get("operations") | |||
let tagDetails = tagObj.get("tagDetails") | |||
let showTagId = ["overview-tags", tag] | |||
let showTag = layoutSelectors.isShown(showTagId, true) | |||
let toggleShow = ()=> layoutActions.show(showTagId, !showTag) | |||
return ( | |||
<div key={"overview-"+tag}> | |||
<h4 onClick={toggleShow} className="link overview-tag"> {showTag ? "-" : "+"}{tag}</h4> | |||
<Collapse isOpened={showTag} animated> | |||
{ | |||
operations.map( op => { | |||
let { path, method, operation, id } = op.toObject() // toObject is shallow | |||
let showOpIdPrefix = "operations" | |||
let showOpId = id | |||
let shown = layoutSelectors.isShown([showOpIdPrefix, showOpId]) | |||
return <OperationLink key={id} | |||
path={path} | |||
method={method} | |||
id={path + "-" + method} | |||
shown={shown} | |||
showOpId={showOpId} | |||
showOpIdPrefix={showOpIdPrefix} | |||
href={`#operation-${showOpId}`} | |||
onClick={layoutActions.show} /> | |||
}).toArray() | |||
} | |||
</Collapse> | |||
</div> | |||
) | |||
}).toArray() | |||
} | |||
{ taggedOps.size < 1 && <h3> No operations defined in spec! </h3> } | |||
</div> | |||
) | |||
} | |||
} | |||
Overview.propTypes = { | |||
layoutSelectors: PropTypes.object.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
layoutActions: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired | |||
} | |||
export class OperationLink extends React.Component { | |||
constructor(props) { | |||
super(props) | |||
this.onClick = this._onClick.bind(this) | |||
} | |||
_onClick() { | |||
let { showOpId, showOpIdPrefix, onClick, shown } = this.props | |||
onClick([showOpIdPrefix, showOpId], !shown) | |||
} | |||
render() { | |||
let { id, method, shown, href } = this.props | |||
return ( | |||
<Link href={ href } style={{fontWeight: shown ? "bold" : "normal"}} onClick={this.onClick} className="block opblock-link"> | |||
<div> | |||
<small className={`bold-label-${method}`}>{method.toUpperCase()}</small> | |||
<span className="bold-label" >{id}</span> | |||
</div> | |||
</Link> | |||
) | |||
} | |||
} | |||
OperationLink.propTypes = { | |||
href: PropTypes.string, | |||
onClick: PropTypes.func, | |||
id: PropTypes.string.isRequired, | |||
method: PropTypes.string.isRequired, | |||
shown: PropTypes.bool.isRequired, | |||
showOpId: PropTypes.string.isRequired, | |||
showOpIdPrefix: PropTypes.string.isRequired | |||
} |
@@ -0,0 +1,141 @@ | |||
import React, { Component, PropTypes } from "react" | |||
import shallowCompare from "react-addons-shallow-compare" | |||
import { Set, fromJS, List } from "immutable" | |||
import { getSampleSchema } from "core/utils" | |||
const NOOP = Function.prototype | |||
export default class ParamBody extends Component { | |||
static propTypes = { | |||
param: PropTypes.object, | |||
onChange: PropTypes.func, | |||
onChangeConsumes: PropTypes.func, | |||
consumes: PropTypes.object, | |||
consumesValue: PropTypes.string, | |||
fn: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
isExecute: PropTypes.bool, | |||
specSelectors: PropTypes.object.isRequired, | |||
pathMethod: PropTypes.array.isRequired | |||
}; | |||
static defaultProp = { | |||
consumes: fromJS(["application/json"]), | |||
param: fromJS({}), | |||
onChange: NOOP, | |||
onChangeConsumes: NOOP, | |||
}; | |||
constructor(props, context) { | |||
super(props, context) | |||
this.state = { | |||
isEditBox: false, | |||
value: "" | |||
} | |||
} | |||
componentDidMount() { | |||
this.updateValues.call(this, this.props) | |||
} | |||
shouldComponentUpdate(props, state) { | |||
return shallowCompare(this, props, state) | |||
} | |||
componentWillReceiveProps(nextProps) { | |||
this.updateValues.call(this, nextProps) | |||
} | |||
updateValues = (props) => { | |||
let { specSelectors, pathMethod, param, isExecute, consumesValue="", onChangeConsumes } = props | |||
let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name")) : {} | |||
let isXml = /xml/i.test(consumesValue) | |||
let paramValue = isXml ? parameter.get("value_xml") : parameter.get("value") | |||
if ( paramValue ) { | |||
this.setState({ value: paramValue }) | |||
this.onChange(paramValue, {isXml: isXml, isEditBox: isExecute}) | |||
} else { | |||
if (isXml) { | |||
this.onChange(this.sample("xml"), {isXml: isXml, isEditBox: isExecute}) | |||
} else { | |||
this.onChange(this.sample(), {isEditBox: isExecute}) | |||
} | |||
} | |||
} | |||
sample = (xml) => { | |||
let { param, fn:{inferSchema} } = this.props | |||
let schema = inferSchema(param.toJS()) | |||
return getSampleSchema(schema, xml) | |||
} | |||
onChange = (value, { isEditBox, isXml }) => { | |||
this.setState({value, isEditBox}) | |||
this._onChange(value, isXml) | |||
} | |||
_onChange = (val, isXml) => { (this.props.onChange || NOOP)(this.props.param, val, isXml) } | |||
handleOnChange = e => { | |||
let {consumesValue} = this.props | |||
this.onChange(e.target.value.trim(), {isXml: /xml/i.test(consumesValue)}) | |||
} | |||
toggleIsEditBox = () => this.setState( state => ({isEditBox: !state.isEditBox})) | |||
render() { | |||
let { | |||
onChangeConsumes, | |||
param, | |||
isExecute, | |||
specSelectors, | |||
pathMethod, | |||
getComponent, | |||
} = this.props | |||
const Button = getComponent("Button") | |||
const TextArea = getComponent("TextArea") | |||
const HighlightCode = getComponent("highlightCode") | |||
const ContentType = getComponent("contentType") | |||
// for domains where specSelectors not passed | |||
let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name")) : param | |||
let errors = parameter.get("errors", List()) | |||
let consumesValue = specSelectors.contentTypeValues(pathMethod).get("requestContentType") | |||
let consumes = this.props.consumes && this.props.consumes.size ? this.props.consumes : ParamBody.defaultProp.consumes | |||
let { value, isEditBox } = this.state | |||
return ( | |||
<div className="body-param"> | |||
{ | |||
isEditBox && isExecute | |||
? <TextArea className={ "body-param__text" + ( errors.count() ? " invalid" : "")} value={value} onChange={ this.handleOnChange }/> | |||
: (value && <HighlightCode className="body-param__example" | |||
value={ value }/>) | |||
} | |||
<div className="body-param-options"> | |||
{ | |||
!isExecute ? null | |||
: <div className="body-param-edit"> | |||
<Button className={isEditBox ? "btn cancel body-param__example-edit" : "btn edit body-param__example-edit"} | |||
onClick={this.toggleIsEditBox}>{ isEditBox ? "Cancel" : "Edit"} | |||
</Button> | |||
</div> | |||
} | |||
<label htmlFor=""> | |||
<span>Parameter content type</span> | |||
<ContentType value={ consumesValue } contentTypes={ consumes } onChange={onChangeConsumes} className="body-param-content-type" /> | |||
</label> | |||
</div> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,119 @@ | |||
import React, { Component, PropTypes } from "react" | |||
import win from "core/window" | |||
export default class ParameterRow extends Component { | |||
static propTypes = { | |||
onChange: PropTypes.func.isRequired, | |||
param: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
fn: PropTypes.object.isRequired, | |||
isExecute: PropTypes.bool, | |||
onChangeConsumes: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
pathMethod: PropTypes.array.isRequired | |||
} | |||
constructor(props, context) { | |||
super(props, context) | |||
let { specSelectors, pathMethod, param } = props | |||
let defaultValue = param.get("default") | |||
let parameter = specSelectors.getParameter(pathMethod, param.get("name")) | |||
let value = parameter ? parameter.get("value") : "" | |||
if ( defaultValue !== undefined && value === undefined ) { | |||
this.onChangeWrapper(defaultValue) | |||
} | |||
} | |||
componentWillReceiveProps(props) { | |||
let { specSelectors, pathMethod, param } = props | |||
let defaultValue = param.get("default") | |||
let parameter = specSelectors.getParameter(pathMethod, param.get("name")) | |||
let value = parameter ? parameter.get("value") : "" | |||
if ( defaultValue !== undefined && value === undefined ) { | |||
this.onChangeWrapper(defaultValue) | |||
} | |||
} | |||
onChangeWrapper = (value) => { | |||
let { onChange, param } = this.props | |||
return onChange(param, value) | |||
} | |||
render() { | |||
let {param, onChange, getComponent, isExecute, fn, onChangeConsumes, specSelectors, pathMethod} = this.props | |||
// const onChangeWrapper = (value) => onChange(param, value) | |||
const JsonSchemaForm = getComponent("JsonSchemaForm") | |||
const ParamBody = getComponent("ParamBody") | |||
let inType = param.get("in") | |||
let bodyParam = inType !== "body" ? null | |||
: <ParamBody getComponent={getComponent} | |||
fn={fn} | |||
param={param} | |||
consumes={ specSelectors.operationConsumes(pathMethod) } | |||
consumesValue={ specSelectors.contentTypeValues(pathMethod).get("requestContentType") } | |||
onChange={onChange} | |||
onChangeConsumes={onChangeConsumes} | |||
isExecute={ isExecute } | |||
specSelectors={ specSelectors } | |||
pathMethod={ pathMethod } | |||
/> | |||
const ModelExample = getComponent("modelExample") | |||
const Markdown = getComponent("Markdown") | |||
let schema = param.get("schema") | |||
let isFormData = inType === "formData" | |||
let isFormDataSupported = "FormData" in win | |||
let required = param.get("required") | |||
let itemType = param.getIn(["items", "type"]) | |||
let parameter = specSelectors.getParameter(pathMethod, param.get("name")) | |||
let value = parameter ? parameter.get("value") : "" | |||
return ( | |||
<tr> | |||
<td className="col parameters-col_name"> | |||
<div className={required ? "parameter__name required" : "parameter__name"}> | |||
{ param.get("name") } | |||
{ !required ? null : <span style={{color: "red"}}> *</span> } | |||
</div> | |||
<div className="parаmeter__type">{ param.get("type") } { itemType && `[${itemType}]` }</div> | |||
<div className="parameter__in">({ param.get("in") })</div> | |||
</td> | |||
<td className="col parameters-col_description"> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} | |||
source={ param.get("description") }/> | |||
{(isFormData && !isFormDataSupported) && <div>Error: your browser does not support FormData</div>} | |||
{ bodyParam || !isExecute ? null | |||
: <JsonSchemaForm fn={fn} | |||
getComponent={getComponent} | |||
value={ value } | |||
required={ required } | |||
description={param.get("description") ? `${param.get("name")} - ${param.get("description")}` : `${param.get("name")}`} | |||
onChange={ this.onChangeWrapper } | |||
schema={ param }/> | |||
} | |||
{ | |||
bodyParam && schema ? <ModelExample getComponent={ getComponent } | |||
isExecute={ isExecute } | |||
specSelectors={ specSelectors } | |||
schema={ schema } | |||
example={ bodyParam }/> | |||
: null | |||
} | |||
</td> | |||
</tr> | |||
) | |||
} | |||
} |
@@ -0,0 +1,109 @@ | |||
import React, { Component, PropTypes } from "react" | |||
import ImPropTypes from "react-immutable-proptypes" | |||
import Im, { fromJS } from "immutable" | |||
// More readable, just iterate over maps, only | |||
const eachMap = (iterable, fn) => iterable.valueSeq().filter(Im.Map.isMap).map(fn) | |||
export default class Parameters extends Component { | |||
static propTypes = { | |||
parameters: ImPropTypes.list.isRequired, | |||
specActions: PropTypes.object.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
fn: PropTypes.object.isRequired, | |||
tryItOutEnabled: PropTypes.bool, | |||
allowTryItOut: PropTypes.bool, | |||
onTryoutClick: PropTypes.func, | |||
onCancelClick: PropTypes.func, | |||
onChangeKey: PropTypes.array, | |||
pathMethod: PropTypes.array.isRequired | |||
} | |||
static defaultProps = { | |||
onTryoutClick: Function.prototype, | |||
onCancelClick: Function.prototype, | |||
tryItOutEnabled: false, | |||
allowTryItOut: true, | |||
onChangeKey: [], | |||
} | |||
onChange = ( param, value, isXml ) => { | |||
let { | |||
specActions: { changeParam }, | |||
onChangeKey, | |||
} = this.props | |||
changeParam( onChangeKey, param.get("name"), value, isXml) | |||
} | |||
onChangeConsumesWrapper = ( val ) => { | |||
let { | |||
specActions: { changeConsumesValue }, | |||
onChangeKey | |||
} = this.props | |||
changeConsumesValue(onChangeKey, val) | |||
} | |||
render(){ | |||
let { | |||
onTryoutClick, | |||
onCancelClick, | |||
parameters, | |||
allowTryItOut, | |||
tryItOutEnabled, | |||
fn, | |||
getComponent, | |||
specSelectors, | |||
pathMethod | |||
} = this.props | |||
const ParameterRow = getComponent("parameterRow") | |||
const TryItOutButton = getComponent("TryItOutButton") | |||
const isExecute = tryItOutEnabled && allowTryItOut | |||
return ( | |||
<div className="opblock-section"> | |||
<div className="opblock-section-header"> | |||
<h4 className="opblock-title">Parameters</h4> | |||
{ allowTryItOut ? ( | |||
<TryItOutButton enabled={ tryItOutEnabled } onCancelClick={ onCancelClick } onTryoutClick={ onTryoutClick } /> | |||
) : null } | |||
</div> | |||
{ !parameters.count() ? <div className="opblock-description-wrapper"><p>No parameters</p></div> : | |||
<div className="table-container"> | |||
<table className="parameters"> | |||
<thead> | |||
<tr> | |||
<th className="col col_header parameters-col_name">Name</th> | |||
<th className="col col_header parameters-col_description">Description</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{ | |||
eachMap(parameters, (parameter, k) => ( | |||
<ParameterRow fn={ fn } | |||
getComponent={ getComponent } | |||
param={ parameter } | |||
key={ parameter.get( "name" ) } | |||
onChange={ this.onChange } | |||
onChangeConsumes={this.onChangeConsumesWrapper} | |||
specSelectors={ specSelectors } | |||
pathMethod={ pathMethod } | |||
isExecute={ isExecute }/> | |||
)).toArray() | |||
} | |||
</tbody> | |||
</table> | |||
</div> | |||
} | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,95 @@ | |||
import React, { PropTypes } from "react" | |||
import { formatXml } from "core/utils" | |||
import lowerCase from "lodash/lowerCase" | |||
export default class ResponseBody extends React.Component { | |||
static propTypes = { | |||
content: PropTypes.any.isRequired, | |||
contentType: PropTypes.string.isRequired, | |||
getComponent: PropTypes.func.isRequired, | |||
headers: PropTypes.object, | |||
url: PropTypes.string | |||
} | |||
render() { | |||
let { content, contentType, url, headers={}, getComponent } = this.props | |||
const HighlightCode = getComponent("highlightCode") | |||
let body, bodyEl | |||
url = url || "" | |||
// JSON | |||
if (/json/i.test(contentType)) { | |||
try { | |||
body = JSON.stringify(JSON.parse(content), null, " ") | |||
} catch (error) { | |||
body = "can't parse JSON. Raw result:\n\n" + content | |||
} | |||
bodyEl = <HighlightCode value={ body } /> | |||
// XML | |||
} else if (/xml/i.test(contentType)) { | |||
body = formatXml(content) | |||
bodyEl = <HighlightCode value={ body } /> | |||
// HTML or Plain Text | |||
} else if (lowerCase(contentType) === "text/html" || /text\/plain/.test(contentType)) { | |||
bodyEl = <HighlightCode value={ content } /> | |||
// Image | |||
} else if (/^image\//i.test(contentType)) { | |||
bodyEl = <img src={ url } /> | |||
// Audio | |||
} else if (/^audio\//i.test(contentType)) { | |||
bodyEl = <pre><audio controls><source src={ url } type={ contentType } /></audio></pre> | |||
// Download | |||
} else if ( | |||
/^application\/octet-stream/i.test(contentType) || | |||
headers["Content-Disposition"] && (/attachment/i).test(headers["Content-Disposition"]) || | |||
headers["content-disposition"] && (/attachment/i).test(headers["content-disposition"]) || | |||
headers["Content-Description"] && (/File Transfer/i).test(headers["Content-Description"]) || | |||
headers["content-description"] && (/File Transfer/i).test(headers["content-description"])) { | |||
let contentLength = headers["content-length"] || headers["Content-Length"] | |||
if ( !(+contentLength) ) return null | |||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) | |||
if (!isSafari && "Blob" in window) { | |||
let type = contentType || "text/html" | |||
let blob = (content instanceof Blob) ? content : new Blob([content], {type: type}) | |||
let href = window.URL.createObjectURL(blob) | |||
let fileName = url.substr(url.lastIndexOf("/") + 1) | |||
let download = [type, fileName, href].join(":") | |||
// Use filename from response header | |||
let disposition = headers["content-disposition"] || headers["Content-Disposition"] | |||
if (typeof disposition !== "undefined") { | |||
let responseFilename = /filename=([^;]*);?/i.exec(disposition) | |||
if (responseFilename !== null && responseFilename.length > 1) { | |||
download = responseFilename[1] | |||
} | |||
} | |||
bodyEl = <div><a href={ href } download={ download }>{ "Download file" }</a></div> | |||
} else { | |||
bodyEl = <pre>Download headers detected but your browser does not support downloading binary via XHR (Blob).</pre> | |||
} | |||
// Anything else (CORS) | |||
} else if (typeof content === "string") { | |||
bodyEl = <HighlightCode value={ content } /> | |||
} else { | |||
bodyEl = <div>Unknown response type</div> | |||
} | |||
return ( !bodyEl ? null : <div> | |||
<h5>Response body</h5> | |||
{ bodyEl } | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,91 @@ | |||
import React, { PropTypes } from "react" | |||
import { fromJS } from 'immutable' | |||
import { getSampleSchema } from "core/utils" | |||
const getExampleComponent = ( sampleResponse, examples, HighlightCode ) => { | |||
if ( examples && examples.size ) { | |||
return examples.entrySeq().map( ([ key, example ]) => { | |||
return (<div key={ key }> | |||
<h5>{ key }</h5> | |||
<HighlightCode className="example" value={ example } /> | |||
</div>) | |||
}).toArray() | |||
} | |||
if ( sampleResponse ) { return <div> | |||
<HighlightCode className="example" value={ sampleResponse } /> | |||
</div> | |||
} | |||
return null | |||
} | |||
export default class Response extends React.Component { | |||
static propTypes = { | |||
code: PropTypes.string.isRequired, | |||
response: PropTypes.object, | |||
className: PropTypes.string, | |||
getComponent: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
fn: PropTypes.object.isRequired, | |||
contentType: PropTypes.string | |||
} | |||
static defaultProps = { | |||
response: fromJS({}), | |||
}; | |||
render() { | |||
let { | |||
code, | |||
response, | |||
className, | |||
fn, | |||
getComponent, | |||
specSelectors, | |||
contentType | |||
} = this.props | |||
let { inferSchema } = fn | |||
let schema = inferSchema(response.toJS()) | |||
let headers = response.get("headers") | |||
let examples = response.get("examples") | |||
const Headers = getComponent("headers") | |||
const HighlightCode = getComponent("highlightCode") | |||
const ModelExample = getComponent("modelExample") | |||
const Markdown = getComponent( "Markdown" ) | |||
let sampleResponse = schema ? getSampleSchema(schema, contentType, { includeReadOnly: true }) : null | |||
let example = getExampleComponent( sampleResponse, examples, HighlightCode ) | |||
return ( | |||
<tr className={ "response " + ( className || "") }> | |||
<td className="col response-col_status"> | |||
{ code } | |||
</td> | |||
<td className="col response-col_description"> | |||
<div className="response-col_description__inner"> | |||
<Markdown options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}} source={ response.get( "description" ) } /> | |||
</div> | |||
{ example ? ( | |||
<ModelExample | |||
getComponent={ getComponent } | |||
specSelectors={ specSelectors } | |||
schema={ fromJS(schema) } | |||
example={ example }/> | |||
) : null} | |||
{ headers ? ( | |||
<Headers headers={ headers }/> | |||
) : null} | |||
</td> | |||
</tr> | |||
) | |||
} | |||
} |
@@ -0,0 +1,93 @@ | |||
import React, { PropTypes } from "react" | |||
import { fromJS } from "immutable" | |||
import { defaultStatusCode } from "core/utils" | |||
export default class Responses extends React.Component { | |||
static propTypes = { | |||
request: PropTypes.object, | |||
tryItOutResponse: PropTypes.object, | |||
responses: PropTypes.object.isRequired, | |||
produces: PropTypes.object, | |||
producesValue: PropTypes.any, | |||
getComponent: PropTypes.func.isRequired, | |||
specSelectors: PropTypes.object.isRequired, | |||
specActions: PropTypes.object.isRequired, | |||
pathMethod: PropTypes.array.isRequired, | |||
fn: PropTypes.object.isRequired | |||
} | |||
static defaultProps = { | |||
request: null, | |||
tryItOutResponse: null, | |||
produces: fromJS(["application/json"]) | |||
} | |||
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue(this.props.pathMethod, val) | |||
render() { | |||
let { responses, request, tryItOutResponse, getComponent, specSelectors, fn, producesValue } = this.props | |||
let defaultCode = defaultStatusCode( responses ) | |||
const ContentType = getComponent( "contentType" ) | |||
const LiveResponse = getComponent( "liveResponse" ) | |||
const Response = getComponent( "response" ) | |||
let produces = this.props.produces && this.props.produces.size ? this.props.produces : Responses.defaultProps.produces | |||
return ( | |||
<div className="responses-wrapper"> | |||
<div className="opblock-section-header"> | |||
<h4>Responses</h4> | |||
<label> | |||
<span>Response content type</span> | |||
<ContentType value={producesValue} | |||
onChange={this.onChangeProducesWrapper} | |||
contentTypes={produces} | |||
className="execute-content-type"/> | |||
</label> | |||
</div> | |||
<div className="responses-inner"> | |||
{ | |||
!tryItOutResponse ? null | |||
: <div> | |||
<LiveResponse request={ request } | |||
response={ tryItOutResponse } | |||
getComponent={ getComponent } /> | |||
<h4>Responses</h4> | |||
</div> | |||
} | |||
<table className="responses-table"> | |||
<thead> | |||
<tr className="responses-header"> | |||
<td className="col col_header response-col_status">Code</td> | |||
<td className="col col_header response-col_description">Description</td> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{ | |||
responses.entrySeq().map( ([code, response]) => { | |||
let className = tryItOutResponse && tryItOutResponse.get("status") == code ? "response_current" : "" | |||
return ( | |||
<Response key={ code } | |||
isDefault={defaultCode === code} | |||
fn={fn} | |||
className={ className } | |||
code={ code } | |||
response={ response } | |||
specSelectors={ specSelectors } | |||
contentType={ producesValue } | |||
getComponent={ getComponent }/> | |||
) | |||
}).toArray() | |||
} | |||
</tbody> | |||
</table> | |||
</div> | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
import React, { PropTypes } from "react" | |||
export default class Schemes extends React.Component { | |||
static propTypes = { | |||
specActions: PropTypes.object.isRequired, | |||
schemes: PropTypes.object.isRequired, | |||
path: PropTypes.string, | |||
method: PropTypes.string | |||
} | |||
componentWillMount() { | |||
let { schemes } = this.props | |||
//fire 'change' event to set default 'value' of select | |||
this.setScheme(schemes.first()) | |||
} | |||
onChange =( e ) => { | |||
let { path, method, specActions } = this.props | |||
this.setScheme( e.target.value ) | |||
} | |||
setScheme =( value ) => { | |||
let { path, method, specActions } = this.props | |||
specActions.setScheme( value, path, method ) | |||
} | |||
render() { | |||
let { schemes } = this.props | |||
return ( | |||
<label htmlFor="schemes"> | |||
<span>Schemes</span> | |||
<select onChange={ this.onChange }> | |||
{ schemes.valueSeq().map( | |||
( scheme ) => <option value={ scheme } key={ scheme }>{ scheme }</option> | |||
).toArray()} | |||
</select> | |||
</label> | |||
) | |||
} | |||
} |
@@ -0,0 +1,27 @@ | |||
import React, { PropTypes } from "react" | |||
export default class TryItOutButton extends React.Component { | |||
static propTypes = { | |||
onTryoutClick: PropTypes.func, | |||
enabled: PropTypes.bool, // Try it out is enabled, ie: the user has access to the form | |||
}; | |||
static defaultProps = { | |||
onTryoutClick: Function.prototype, | |||
enabled: false, | |||
}; | |||
render() { | |||
const { onTryoutClick, onCancelClick, enabled } = this.props | |||
return ( | |||
<div className="try-out"> | |||
{ | |||
enabled ? <button className="btn try-out__btn cancel" onClick={ onTryoutClick }>Cancel</button> | |||
: <button className="btn try-out__btn" onClick={ onCancelClick }>Try it out </button> | |||
} | |||
</div> | |||
) | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
export default function curl( request ){ | |||
let curlified = [] | |||
let type = "" | |||
let headers = request.get("headers") | |||
curlified.push( "curl" ) | |||
curlified.push( "-X", request.get("method") ) | |||
curlified.push( request.get("url") ) | |||
if ( headers && headers.size ) { | |||
for( let p of request.get("headers").entries() ){ | |||
let [ h,v ] = p | |||
type = v | |||
curlified.push( "-H " ) | |||
curlified.push( `"${h}: ${v}"` ) | |||
} | |||
} | |||
if ( request.get("body") ){ | |||
if(type === "multipart/form-data" && request.get("method") === "POST") { | |||
let formDataBody = request.get("body").split("&") | |||
for(var data in formDataBody) { | |||
curlified.push( "-F" ) | |||
curlified.push(formDataBody[data]) | |||
} | |||
} else { | |||
curlified.push( "-d" ) | |||
curlified.push( JSON.stringify( request.get("body") ).replace(/\\n/g, "") ) | |||
} | |||
} | |||
return curlified.join( " " ) | |||
} |
@@ -0,0 +1,119 @@ | |||
import deepExtend from "deep-extend" | |||
import System from "core/system" | |||
import ApisPreset from "core/presets/apis" | |||
import * as AllPlugins from "core/plugins/all" | |||
import { filterConfigs } from "plugins/configs" | |||
module.exports = function SwaggerUI(opts) { | |||
const defaults = { | |||
// Some general settings, that we floated to the top | |||
dom_id: null, | |||
spec: {}, | |||
url: "", | |||
layout: "Layout", | |||
configs: { | |||
validatorUrl: "https://online.swagger.io/validator" | |||
}, | |||
// 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: [ | |||
], | |||
// Plugins; ( loaded after presets ) | |||
plugins: [ | |||
], | |||
// Inline Plugin | |||
fn: { }, | |||
components: { }, | |||
state: { }, | |||
// Override some core configs... at your own risk | |||
store: { }, | |||
} | |||
const config = deepExtend({}, defaults, opts) | |||
const storeConfigs = deepExtend({}, config.store, { | |||
system: { | |||
configs: config.configs | |||
}, | |||
plugins: config.presets, | |||
state: { | |||
layout: { | |||
layout: config.layout | |||
}, | |||
spec: { | |||
spec: "", | |||
url: config.url | |||
} | |||
} | |||
}) | |||
let inlinePlugin = ()=> { | |||
return { | |||
fn: config.fn, | |||
components: config.components, | |||
state: config.state, | |||
} | |||
} | |||
var store = new System(storeConfigs) | |||
store.register([config.plugins, inlinePlugin]) | |||
var system = store.getSystem() | |||
const downloadSpec = (configs) => { | |||
if(typeof config !== "object") { | |||
return system | |||
} | |||
let localConfig = system.specSelectors.getLocalConfig ? system.specSelectors.getLocalConfig() : {} | |||
let mergedConfig = deepExtend({}, config, configs, localConfig) | |||
store.setConfigs(filterConfigs(mergedConfig)) | |||
if(typeof mergedConfig.spec === "object" && Object.keys(mergedConfig.spec).length) { | |||
system.specActions.updateUrl("") | |||
system.specActions.updateLoadingStatus("success"); | |||
system.specActions.updateSpec(JSON.stringify(mergedConfig.spec)) | |||
} else if(mergedConfig.url) { | |||
system.specActions.updateUrl(mergedConfig.url) | |||
system.specActions.download(mergedConfig.url) | |||
} | |||
if(mergedConfig.dom_id) | |||
system.render(mergedConfig.dom_id, "App") | |||
return system | |||
} | |||
if (system.specActions.getConfigByUrl && !system.specActions.getConfigByUrl(downloadSpec)) { | |||
return downloadSpec(config) | |||
} | |||
if (system.specActions.download && config.url) { | |||
system.specActions.download(config.url) | |||
} | |||
if(config.spec && typeof config.spec === "string") | |||
system.specActions.updateSpec(config.spec) | |||
if(config.dom_id) { | |||
system.render(config.dom_id, "App") | |||
} else { | |||
console.error("Skipped rendering: no `dom_id` was specified") | |||
} | |||
return system | |||
} | |||
// Add presets | |||
module.exports.presets = { | |||
apis: ApisPreset, | |||
} | |||
// All Plugins | |||
module.exports.plugins = AllPlugins |
@@ -0,0 +1,187 @@ | |||
import React, { PropTypes, Component } from "react" | |||
import { arrayify } from "core/utils" | |||
import shallowCompare from "react-addons-shallow-compare" | |||
import { List, fromJS } from "immutable" | |||
import assign from "object-assign" | |||
//import "less/json-schema-form" | |||
const noop = ()=> {} | |||
const JsonSchemaPropShape = { | |||
getComponent: PropTypes.func.isRequired, | |||
value: PropTypes.any, | |||
onChange: PropTypes.func, | |||
keyName: PropTypes.any, | |||
fn: PropTypes.object.isRequired, | |||
schema: PropTypes.object, | |||
required: PropTypes.bool, | |||
description: PropTypes.any | |||
} | |||
const JsonSchemaDefaultProps = { | |||
value: "", | |||
onChange: noop, | |||
schema: {}, | |||
keyName: "", | |||
required: false | |||
} | |||
export class JsonSchemaForm extends Component { | |||
static propTypes = JsonSchemaPropShape | |||
static defaultProps = JsonSchemaDefaultProps | |||
render() { | |||
let { schema, value, onChange, getComponent, fn } = this.props | |||
if(schema.toJS) | |||
schema = schema.toJS() | |||
let { type, format="" } = schema | |||
let Comp = getComponent(`JsonSchema_${type}_${format}`) || getComponent(`JsonSchema_${type}`) || getComponent("JsonSchema_string") | |||
return <Comp { ...this.props } fn={fn} getComponent={getComponent} value={value} onChange={onChange} schema={schema}/> | |||
} | |||
} | |||
export class JsonSchema_string extends Component { | |||
static propTypes = JsonSchemaPropShape | |||
static defaultProps = JsonSchemaDefaultProps | |||
onChange = (e) => { | |||
const value = this.props.schema["type"] === "file" ? e.target.files[0] : e.target.value | |||
this.props.onChange(value, this.props.keyName) | |||
} | |||
onEnumChange = (val) => this.props.onChange(val) | |||
render() { | |||
let { getComponent, value, schema, fn, required, description } = this.props | |||
let enumValue = schema["enum"] | |||
let errors = schema.errors || [] | |||
if ( enumValue ) { | |||
const Select = getComponent("Select") | |||
return (<Select allowedValues={ enumValue } | |||
value={ value } | |||
allowEmptyValue={ !required } | |||
onChange={ this.onEnumChange }/>) | |||
} | |||
const isDisabled = schema["in"] === "formData" && !("FormData" in window) | |||
const Input = getComponent("Input") | |||
if (schema["type"] === "file") { | |||
return <Input type="file" className={ errors.length ? "invalid" : ""} onChange={ this.onChange } disabled={isDisabled}/> | |||
} | |||
else { | |||
return <Input type="text" className={ errors.length ? "invalid" : ""} value={value} placeholder={description} onChange={ this.onChange } disabled={isDisabled}/> | |||
} | |||
} | |||
} | |||
export class JsonSchema_array extends Component { | |||
static propTypes = JsonSchemaPropShape | |||
static defaultProps = JsonSchemaDefaultProps | |||
constructor(props, context) { | |||
super(props, context) | |||
this.state = {value: props.value} | |||
} | |||
componentWillReceiveProps(props) { | |||
if(props.value !== this.state.value) | |||
this.setState({value: props.value}) | |||
} | |||
shouldComponentUpdate(props, state) { | |||
return shallowCompare(this, props, state) | |||
} | |||
onChange = () => this.props.onChange(this.state.value) | |||
onItemChange = (itemVal, i) => { | |||
this.setState(state => ({ | |||
value: state.value.set(i, itemVal) | |||
}), this.onChange) | |||
} | |||
removeItem = (i) => { | |||
this.setState(state => ({ | |||
value: state.value.remove(i) | |||
}), this.onChange) | |||
} | |||
addItem = () => { | |||
this.setState(state => { | |||
state.value = state.value || List() | |||
return { | |||
value: state.value.push("") | |||
} | |||
}, this.onChange) | |||
} | |||
onEnumChange = (value) => { | |||
this.setState(state => ({ | |||
value: value | |||
}), this.onChange) | |||
} | |||
render() { | |||
let { getComponent, onChange, required, schema, fn } = this.props | |||
let itemSchema = fn.inferSchema(schema.items) | |||
const JsonSchemaForm = getComponent("JsonSchemaForm") | |||
const Button = getComponent("Button") | |||
let enumValue = itemSchema["enum"] | |||
let value = this.state.value | |||
if ( enumValue ) { | |||
const Select = getComponent("Select") | |||
return (<Select multiple={ true } | |||
value={ value } | |||
allowedValues={ enumValue } | |||
allowEmptyValue={ !required } | |||
onChange={ this.onEnumChange }/>) | |||
} | |||
let errors = schema.errors || [] | |||
return ( | |||
<div> | |||
{ !value || value.count() < 1 ? | |||
(errors.length ? <span style={{ color: "red", fortWeight: "bold" }}>{ errors[0] }</span> : null) : | |||
value.map( (item,i) => { | |||
let schema = Object.assign({}, itemSchema) | |||
let err = errors.filter((err) => err.index === i) | |||
if ( err.length ) { | |||
schema.errors = [ err[0].error + i ] | |||
} | |||
return ( | |||
<div key={i} className="json-schema-form-item"> | |||
<JsonSchemaForm fn={fn} getComponent={getComponent} value={item} onChange={(val) => this.onItemChange(val, i)} schema={schema} /> | |||
<Button className="json-schema-form-item-remove" onClick={()=> this.removeItem(i)} > - </Button> | |||
</div> | |||
) | |||
}).toArray() | |||
} | |||
<Button className="json-schema-form-item-add" onClick={this.addItem}> Add item </Button> | |||
</div> | |||
) | |||
} | |||
} | |||
export class JsonSchema_boolean extends Component { | |||
static propTypes = JsonSchemaPropShape | |||
static defaultProps = JsonSchemaDefaultProps | |||
onEnumChange = (val) => this.props.onChange(val) | |||
render() { | |||
let { getComponent, required, value } = this.props | |||
const Select = getComponent("Select") | |||
return (<Select value={ String(value) } | |||
allowedValues={ fromJS(["true", "false"]) } | |||
allowEmptyValue={ !required } | |||
onChange={ this.onEnumChange }/>) | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
import win from "core/window" | |||
export default function authorize ( auth, authActions, errActions, configs ) { | |||
let { schema, scopes, name, clientId } = auth | |||
let redirectUrl = configs.oauth2RedirectUrl | |||
let scopeSeparator = " " | |||
let state = name | |||
let flow = schema.get("flow") | |||
let url | |||
if (flow === "password") { | |||
authActions.authorizePassword(auth) | |||
return | |||
} | |||
// todo move to parser | |||
if ( !redirectUrl ) { | |||
errActions.newAuthErr( { | |||
authId: name, | |||
source: "validation", | |||
level: "error", | |||
message: "oauth2RedirectUri configuration is not passed. Oauth2 authorization cannot be performed." | |||
}) | |||
return | |||
} | |||
if (flow === "implicit" || flow === "accessCode") { | |||
url = schema.get("authorizationUrl") + "?response_type=" + (flow === "implicit" ? "token" : "code") | |||
} | |||
url += "&redirect_uri=" + encodeURIComponent(redirectUrl) | |||
+ "&scope=" + encodeURIComponent(scopes.join(scopeSeparator)) | |||
+ "&state=" + encodeURIComponent(state) | |||
+ "&client_id=" + encodeURIComponent(clientId) | |||
// pass action authorizeOauth2 and authentication data through window | |||
// to authorize with oauth2 | |||
win.swaggerUIRedirectOauth2 = { | |||
auth: auth, | |||
state: state, | |||
callback: authActions.preAuthorizeOauth2, | |||
errCb: errActions.newAuthErr | |||
} | |||
win.open(url) | |||
} |
@@ -0,0 +1,67 @@ | |||
import get from "lodash/get" | |||
export function transformPathToArray(property, jsSpec) { | |||
if(property.slice(0,9) === "instance.") { | |||
var str = property.slice(9) | |||
} else { // eslint-disable-next-line no-redeclare | |||
var str = property | |||
} | |||
var pathArr = [] | |||
str | |||
.split(".") | |||
.map(item => { | |||
// "key[0]" becomes ["key", "0"] | |||
if(item.includes("[")) { | |||
let index = parseInt(item.match(/\[(.*)\]/)[1]) | |||
let keyName = item.slice(0, item.indexOf("[")) | |||
return [keyName, index.toString()] | |||
} else { | |||
return item | |||
} | |||
}) | |||
.reduce(function(a, b) { | |||
// flatten! | |||
return a.concat(b) | |||
}, []) | |||
.concat([""]) // add an empty item into the array, so we don't get stuck with something in our buffer below | |||
.reduce((buffer, curr, i, arr) => { | |||
let obj = pathArr.length ? get(jsSpec, pathArr) : jsSpec | |||
if(get(obj, makeAccessArray(buffer, curr))) { | |||
if(buffer.length) { | |||
pathArr.push(buffer) | |||
} | |||
if(curr.length) { | |||
pathArr.push(curr) | |||
} | |||
return "" | |||
} else { | |||
// attach key to buffer | |||
return `${buffer}${buffer.length ? "." : ""}${curr}` | |||
} | |||
}, "") | |||
if(typeof get(jsSpec, pathArr) !== "undefined") { | |||
return pathArr | |||
} else { | |||
// if our path is not correct (there is no value at the path), | |||
// return null | |||
return null | |||
} | |||
} | |||
function makeAccessArray(buffer, curr) { | |||
let arr = [] | |||
if(buffer.length) { | |||
arr.push(buffer) | |||
} | |||
if(curr.length) { | |||
arr.push(curr) | |||
} | |||
return arr | |||
} |
@@ -0,0 +1,17 @@ | |||
import { pascalCaseFilename } from "core/utils" | |||
const request = require.context(".", true, /\.jsx?$/) | |||
request.keys().forEach( function( key ){ | |||
if( key === "./index.js" ) { | |||
return | |||
} | |||
// if( key.slice(2).indexOf("/") > -1) { | |||
// // skip files in subdirs | |||
// return | |||
// } | |||
let mod = request(key) | |||
module.exports[pascalCaseFilename(key)] = mod.default ? mod.default : mod | |||
}) |
@@ -0,0 +1,36 @@ | |||
import React, { PropTypes } from "react" | |||
export default function (system) { | |||
return { | |||
components: { | |||
NoHostWarning, | |||
}, | |||
statePlugins: { | |||
spec: { | |||
selectors: { | |||
allowTryItOutFor, | |||
} | |||
} | |||
} | |||
} | |||
} | |||
// This is a quick style. How do we improve this? | |||
const style = { | |||
backgroundColor: "#e7f0f7", | |||
padding: "1rem", | |||
borderRadius: "3px", | |||
} | |||
function NoHostWarning() { | |||
return ( | |||
<div style={style}>Note: The interactive forms are disabled, as no `host` property was found in the specification. Please see: <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object" target="_blank">OAI 2.0/#swagger-object</a></div> | |||
) | |||
} | |||
// Only allow if, there is a host field | |||
function allowTryItOutFor(state) { | |||
return ({specSelectors}) => { | |||
return specSelectors.hasHost(state) | |||
} | |||
} |
@@ -0,0 +1,284 @@ | |||
import YAML from "yaml-js" | |||
import isArray from "lodash/isArray" | |||
import lodashFind from "lodash/find" | |||
import { memoize } from "core/utils" | |||
let cachedCompose = memoize(YAML.compose) // TODO: build a custom cache based on content | |||
var MAP_TAG = "tag:yaml.org,2002:map" | |||
var SEQ_TAG = "tag:yaml.org,2002:seq" | |||
export function getLineNumberForPath(yaml, path) { | |||
// Type check | |||
if (typeof yaml !== "string") { | |||
throw new TypeError("yaml should be a string") | |||
} | |||
if (!isArray(path)) { | |||
throw new TypeError("path should be an array of strings") | |||
} | |||
var i = 0 | |||
let ast = cachedCompose(yaml) | |||
// simply walks the tree using current path recursively to the point that | |||
// path is empty | |||
return find(ast, path) | |||
function find(current, path, last) { | |||
if(!current) { | |||
// something has gone quite wrong | |||
// return the last start_mark as a best-effort | |||
if(last && last.start_mark) | |||
return last.start_mark.line | |||
return 0 | |||
} | |||
if (path.length && current.tag === MAP_TAG) { | |||
for (i = 0; i < current.value.length; i++) { | |||
var pair = current.value[i] | |||
var key = pair[0] | |||
var value = pair[1] | |||
if (key.value === path[0]) { | |||
return find(value, path.slice(1), current) | |||
} | |||
if (key.value === path[0].replace(/\[.*/, "")) { | |||
// access the array at the index in the path (example: grab the 2 in "tags[2]") | |||
var index = parseInt(path[0].match(/\[(.*)\]/)[1]) | |||
if(value.value.length === 1 && index !== 0 && !!index) { | |||
var nextVal = lodashFind(value.value[0], { value: index.toString() }) | |||
} else { // eslint-disable-next-line no-redeclare | |||
var nextVal = value.value[index] | |||
} | |||
return find(nextVal, path.slice(1), value.value) | |||
} | |||
} | |||
} | |||
if (path.length && current.tag === SEQ_TAG) { | |||
var item = current.value[path[0]] | |||
if (item && item.tag) { | |||
return find(item, path.slice(1), current.value) | |||
} | |||
} | |||
if (current.tag === MAP_TAG && !Array.isArray(last)) { | |||
return current.start_mark.line | |||
} else { | |||
return current.start_mark.line + 1 | |||
} | |||
} | |||
} | |||
/** | |||
* Get a position object with given | |||
* @param {string} yaml | |||
* YAML or JSON string | |||
* @param {array} path | |||
* an array of stings that constructs a | |||
* JSON Path similiar to JSON Pointers(RFC 6901). The difference is, each | |||
* component of path is an item of the array intead of beinf seperated with | |||
* slash(/) in a string | |||
*/ | |||
export function positionRangeForPath(yaml, path) { | |||
// Type check | |||
if (typeof yaml !== "string") { | |||
throw new TypeError("yaml should be a string") | |||
} | |||
if (!isArray(path)) { | |||
throw new TypeError("path should be an array of strings") | |||
} | |||
var invalidRange = { | |||
start: {line: -1, column: -1}, | |||
end: {line: -1, column: -1} | |||
} | |||
var i = 0 | |||
let ast = cachedCompose(yaml) | |||
// simply walks the tree using current path recursively to the point that | |||
// path is empty. | |||
return find(ast) | |||
function find(current) { | |||
if (current.tag === MAP_TAG) { | |||
for (i = 0; i < current.value.length; i++) { | |||
var pair = current.value[i] | |||
var key = pair[0] | |||
var value = pair[1] | |||
if (key.value === path[0]) { | |||
path.shift() | |||
return find(value) | |||
} | |||
} | |||
} | |||
if (current.tag === SEQ_TAG) { | |||
var item = current.value[path[0]] | |||
if (item && item.tag) { | |||
path.shift() | |||
return find(item) | |||
} | |||
} | |||
// if path is still not empty we were not able to find the node | |||
if (path.length) { | |||
return invalidRange | |||
} | |||
return { | |||
/* jshint camelcase: false */ | |||
start: { | |||
line: current.start_mark.line, | |||
column: current.start_mark.column | |||
}, | |||
end: { | |||
line: current.end_mark.line, | |||
column: current.end_mark.column | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Get a JSON Path for position object in the spec | |||
* @param {string} yaml | |||
* YAML or JSON string | |||
* @param {object} position | |||
* position in the YAML or JSON string with `line` and `column` properties. | |||
* `line` and `column` number are zero indexed | |||
*/ | |||
export function pathForPosition(yaml, position) { | |||
// Type check | |||
if (typeof yaml !== "string") { | |||
throw new TypeError("yaml should be a string") | |||
} | |||
if (typeof position !== "object" || typeof position.line !== "number" || | |||
typeof position.column !== "number") { | |||
throw new TypeError("position should be an object with line and column" + | |||
" properties") | |||
} | |||
try { | |||
var ast = cachedCompose(yaml) | |||
} catch (e) { | |||
console.error("Error composing AST", e) | |||
console.error(`Problem area:\n`, yaml.split("\n").slice(position.line - 5, position.line + 5).join("\n")) | |||
return null | |||
} | |||
var path = [] | |||
return find(ast) | |||
/** | |||
* recursive find function that finds the node matching the position | |||
* @param {object} current - AST object to serach into | |||
*/ | |||
function find(current) { | |||
// algorythm: | |||
// is current a promitive? | |||
// // finish recursion without modifying the path | |||
// is current a hash? | |||
// // find a key or value that position is in their range | |||
// // if key is in range, terminate recursion with exisiting path | |||
// // if a value is in range push the corresponding key to the path | |||
// // andcontinue recursion | |||
// is current an array | |||
// // find the item that position is in it"s range and push the index | |||
// // of the item to the path and continue recursion with that item. | |||
var i = 0 | |||
if (!current || [MAP_TAG, SEQ_TAG].indexOf(current.tag) === -1) { | |||
return path | |||
} | |||
if (current.tag === MAP_TAG) { | |||
for (i = 0; i < current.value.length; i++) { | |||
var pair = current.value[i] | |||
var key = pair[0] | |||
var value = pair[1] | |||
if (isInRange(key)) { | |||
return path | |||
} else if (isInRange(value)) { | |||
path.push(key.value) | |||
return find(value) | |||
} | |||
} | |||
} | |||
if (current.tag === SEQ_TAG) { | |||
for (i = 0; i < current.value.length; i++) { | |||
var item = current.value[i] | |||
if (isInRange(item)) { | |||
path.push(i.toString()) | |||
return find(item) | |||
} | |||
} | |||
} | |||
return path | |||
/** | |||
* Determines if position is in node"s range | |||
* @param {object} node - AST node | |||
* @return {Boolean} true if position is in node"s range | |||
*/ | |||
function isInRange(node) { | |||
/* jshint camelcase: false */ | |||
// if node is in a single line | |||
if (node.start_mark.line === node.end_mark.line) { | |||
return (position.line === node.start_mark.line) && | |||
(node.start_mark.column <= position.column) && | |||
(node.end_mark.column >= position.column) | |||
} | |||
// if position is in the same line as start_mark | |||
if (position.line === node.start_mark.line) { | |||
return position.column >= node.start_mark.column | |||
} | |||
// if position is in the same line as end_mark | |||
if (position.line === node.end_mark.line) { | |||
return position.column <= node.end_mark.column | |||
} | |||
// if position is between start and end lines return true, otherwise | |||
// return false. | |||
return (node.start_mark.line < position.line) && | |||
(node.end_mark.line > position.line) | |||
} | |||
} | |||
} | |||
// utility fns | |||
export let pathForPositionAsync = promisifySyncFn(pathForPosition) | |||
export let positionRangeForPathAsync = promisifySyncFn(positionRangeForPath) | |||
export let getLineNumberForPathAsync = promisifySyncFn(getLineNumberForPath) | |||
function promisifySyncFn(fn) { | |||
return function(...args) { | |||
return new Promise(function(resolve, reject) { | |||
resolve(fn(...args)) | |||
}) | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import * as AST from "./ast" | |||
import JumpToPath from "./jump-to-path" | |||
export default function() { | |||
return { | |||
fn: { AST }, | |||
components: { JumpToPath } | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import React from "react" | |||
// Nothing by default- component can be overriden by another plugin. | |||
export default class JumpToPath extends React.Component { | |||
render() { | |||
return null | |||
} | |||
} |
@@ -0,0 +1,118 @@ | |||
import win from "core/window" | |||
import btoa from "btoa" | |||
export const SHOW_AUTH_POPUP = "show_popup" | |||
export const AUTHORIZE = "authorize" | |||
export const LOGOUT = "logout" | |||
export const PRE_AUTHORIZE_OAUTH2 = "pre_authorize_oauth2" | |||
export const AUTHORIZE_OAUTH2 = "authorize_oauth2" | |||
export const VALIDATE = "validate" | |||
export function showDefinitions(payload) { | |||
return { | |||
type: SHOW_AUTH_POPUP, | |||
payload: payload | |||
} | |||
} | |||
export function authorize(payload) { | |||
return { | |||
type: AUTHORIZE, | |||
payload: payload | |||
} | |||
} | |||
export function logout(payload) { | |||
return { | |||
type: LOGOUT, | |||
payload: payload | |||
} | |||
} | |||
export const preAuthorizeOauth2 = (payload) => ( { authActions, errActions } ) => { | |||
let { auth , token, isValid } = payload | |||
let { schema, name } = auth | |||
let flow = schema.get("flow") | |||
// remove oauth2 property from window after redirect from authentication | |||
delete win.swaggerUIRedirectOauth2 | |||
if ( flow !== "accessCode" && !isValid ) { | |||
errActions.newAuthErr( { | |||
authId: name, | |||
source: "auth", | |||
level: "warning", | |||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server" | |||
}) | |||
} | |||
if ( token.error ) { | |||
errActions.newAuthErr({ | |||
authId: name, | |||
source: "auth", | |||
level: "error", | |||
message: JSON.stringify(token) | |||
}) | |||
return | |||
} | |||
authActions.authorizeOauth2({ auth, token }) | |||
} | |||
export function authorizeOauth2(payload) { | |||
return { | |||
type: AUTHORIZE_OAUTH2, | |||
payload: payload | |||
} | |||
} | |||
export const authorizePassword = ( auth ) => ( { fn, authActions, errActions } ) => { | |||
let { schema, name, username, password, passwordType, clientId, clientSecret } = auth | |||
let req = { | |||
url: schema.get("tokenUrl"), | |||
method: "post", | |||
headers: { | |||
"content-type": "application/x-www-form-urlencoded" | |||
}, | |||
query: { | |||
grant_type: "password", | |||
username, | |||
password | |||
} | |||
} | |||
if ( passwordType === "basic") { | |||
req.headers.authorization = "Basic " + btoa(clientId + ":" + clientSecret) | |||
} else if ( passwordType === "request") { | |||
req.query = Object.assign(req.query, { client_id: clientId, client_secret: clientSecret }) | |||
} | |||
return fn.fetch(req) | |||
.then(( response ) => { | |||
let token = JSON.parse(response.data) | |||
let error = token && ( token.error || "" ) | |||
let parseError = token && ( token.parseError || "" ) | |||
if ( !response.ok ) { | |||
errActions.newAuthErr( { | |||
authId: name, | |||
level: "error", | |||
source: "auth", | |||
message: response.statusText | |||
} ) | |||
return | |||
} | |||
if ( error || parseError ) { | |||
errActions.newAuthErr({ | |||
authId: name, | |||
level: "error", | |||
source: "auth", | |||
message: JSON.stringify(token) | |||
}) | |||
return | |||
} | |||
authActions.authorizeOauth2({ auth, token }) | |||
}) | |||
.catch(err => { errActions.newAuthErr( err ) }) | |||
} |
@@ -0,0 +1,19 @@ | |||
import reducers from "./reducers" | |||
import * as actions from "./actions" | |||
import * as selectors from "./selectors" | |||
import * as specWrapActionReplacements from "./spec-wrap-actions" | |||
export default function() { | |||
return { | |||
statePlugins: { | |||
auth: { | |||
reducers, | |||
actions, | |||
selectors | |||
}, | |||
spec: { | |||
wrapActions: specWrapActionReplacements | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,63 @@ | |||
import { fromJS, Map } from "immutable" | |||
import btoa from "btoa" | |||
import { | |||
SHOW_AUTH_POPUP, | |||
AUTHORIZE, | |||
PRE_AUTHORIZE_OAUTH2, | |||
AUTHORIZE_OAUTH2, | |||
LOGOUT | |||
} from "./actions" | |||
export default { | |||
[SHOW_AUTH_POPUP]: (state, { payload } ) =>{ | |||
return state.set( "showDefinitions", payload ) | |||
}, | |||
[AUTHORIZE]: (state, { payload } ) =>{ | |||
let securities = fromJS(payload) | |||
let map = state.get("authorized") || Map() | |||
// refactor withMutations | |||
securities.entrySeq().forEach( ([ key, security ]) => { | |||
let type = security.getIn(["schema", "type"]) | |||
let name = security.get("name") | |||
if ( type === "apiKey" ) { | |||
map = map.set(key, security) | |||
} else if ( type === "basic" ) { | |||
let username = security.getIn(["value", "username"]) | |||
let password = security.getIn(["value", "password"]) | |||
map = map.setIn([key, "value"], { | |||
username: username, | |||
header: "Basic " + btoa(username + ":" + password) | |||
}) | |||
map = map.setIn([key, "schema"], security.get("schema")) | |||
} | |||
}) | |||
return state.set( "authorized", map ) | |||
}, | |||
[AUTHORIZE_OAUTH2]: (state, { payload } ) =>{ | |||
let { auth, token } = payload | |||
let parsedAuth | |||
auth.token = token | |||
parsedAuth = fromJS(auth) | |||
return state.setIn( [ "authorized", parsedAuth.get("name") ], parsedAuth ) | |||
}, | |||
[LOGOUT]: (state, { payload } ) =>{ | |||
let result = state.get("authorized").withMutations((authorized) => { | |||
payload.forEach((auth) => { | |||
authorized.delete(auth) | |||
}) | |||
}) | |||
return state.set("authorized", result) | |||
} | |||
} |
@@ -0,0 +1,78 @@ | |||
import { createSelector } from "reselect" | |||
import { List, Map } from "immutable" | |||
const state = state => state | |||
export const shownDefinitions = createSelector( | |||
state, | |||
auth => auth.get( "showDefinitions" ) | |||
) | |||
export const definitionsToAuthorize = createSelector( | |||
state, | |||
auth =>( { specSelectors } ) => { | |||
let definitions = specSelectors.securityDefinitions() | |||
let list = List() | |||
//todo refactor | |||
definitions.entrySeq().forEach( ([ key, val ]) => { | |||
let map = Map() | |||
map = map.set(key, val) | |||
list = list.push(map) | |||
}) | |||
return list | |||
} | |||
) | |||
export const getDefinitionsByNames = ( state, securities ) =>( { specSelectors } ) => { | |||
let securityDefinitions = specSelectors.securityDefinitions() | |||
let result = List() | |||
securities.valueSeq().forEach( (names) => { | |||
let map = Map() | |||
names.entrySeq().forEach( ([name, scopes]) => { | |||
let definition = securityDefinitions.get(name) | |||
let allowedScopes | |||
if ( definition.get("type") === "oauth2" && scopes.size ) { | |||
allowedScopes = definition.get("scopes") | |||
allowedScopes.keySeq().forEach( (key) => { | |||
if ( !scopes.contains(key) ) { | |||
allowedScopes = allowedScopes.delete(key) | |||
} | |||
}) | |||
definition = definition.set("allowedScopes", allowedScopes) | |||
} | |||
map = map.set(name, definition) | |||
}) | |||
result = result.push(map) | |||
}) | |||
return result | |||
} | |||
export const authorized = createSelector( | |||
state, | |||
auth => auth.get("authorized") || Map() | |||
) | |||
export const isAuthorized = ( state, securities ) =>( { authSelectors } ) => { | |||
let authorized = authSelectors.authorized() | |||
let isAuth = false | |||
return !!securities.toJS().filter( ( security ) => { | |||
let isAuthorized = true | |||
return Object.keys(security).map((key) => { | |||
return !isAuthorized || !!authorized.get(key) | |||
}).indexOf(false) === -1 | |||
}).length | |||
} |
@@ -0,0 +1,13 @@ | |||
import { Map } from "immutable" | |||
// Add security to the final `execute` call ( via `extras` ) | |||
export const execute = ( oriAction, { authSelectors, specSelectors }) => ({ path, method, operation, extras }) => { | |||
let securities = { | |||
authorized: authSelectors.authorized() && authSelectors.authorized().toJS(), | |||
definitions: specSelectors.securityDefinitions() && specSelectors.securityDefinitions().toJS(), | |||
specSecurity: specSelectors.security() && specSelectors.security().toJS() | |||
} | |||
return oriAction({ path, method, operation, securities, ...extras }) | |||
} | |||
@@ -0,0 +1,67 @@ | |||
/* global Promise */ | |||
import { createSelector } from "reselect" | |||
import { Map } from "immutable" | |||
export default function downloadUrlPlugin (toolbox) { | |||
let { fn, Im } = toolbox | |||
const actions = { | |||
download: (url)=> ({ errActions, specSelectors, specActions }) => { | |||
let { fetch } = fn | |||
url = url || specSelectors.url() | |||
specActions.updateLoadingStatus("loading") | |||
fetch(url, { | |||
headers: { | |||
"Accept": "application/json" | |||
} | |||
}).then(next,next) | |||
function next(res) { | |||
if(res instanceof Error || res.status >= 400) { | |||
specActions.updateLoadingStatus("failed") | |||
return errActions.newThrownErr( new Error(res.statusText + " " + url) ) | |||
} | |||
specActions.updateLoadingStatus("success") | |||
specActions.updateSpec(res.text) | |||
specActions.updateUrl(url) | |||
} | |||
}, | |||
updateLoadingStatus: (status) => { | |||
let enums = [null, "loading", "failed", "success"] | |||
if(enums.indexOf(status) === -1) { | |||
console.error(`Error: ${status} is not one of ${JSON.stringify(enums)}`) | |||
} | |||
return { | |||
type: "spec_update_loading_status", | |||
payload: status | |||
} | |||
} | |||
} | |||
let reducers = { | |||
"spec_update_loading_status": (state, action) => { | |||
return (typeof action.payload === "string") | |||
? state.set("loadingStatus", action.payload) | |||
: state | |||
} | |||
} | |||
let selectors = { | |||
loadingStatus: createSelector( | |||
state => { | |||
return state || Map() | |||
}, | |||
spec => spec.get("loadingStatus") || null | |||
) | |||
} | |||
return { | |||
statePlugins: { | |||
spec: { actions, reducers, selectors } | |||
} | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
import serializeError from "serialize-error" | |||
export const NEW_THROWN_ERR = "err_new_thrown_err" | |||
export const NEW_THROWN_ERR_BATCH = "err_new_thrown_err_batch" | |||
export const NEW_SPEC_ERR = "err_new_spec_err" | |||
export const NEW_AUTH_ERR = "err_new_auth_err" | |||
export const CLEAR = "err_clear" | |||
export function newThrownErr(err, action) { | |||
return { | |||
type: NEW_THROWN_ERR, | |||
payload: { action, error: serializeError(err) } | |||
} | |||
} | |||
export function newThrownErrBatch(errors) { | |||
return { | |||
type: NEW_THROWN_ERR_BATCH, | |||
payload: errors | |||
} | |||
} | |||
export function newSpecErr(err, action) { | |||
return { | |||
type: NEW_SPEC_ERR, | |||
payload: err | |||
} | |||
} | |||
export function newAuthErr(err, action) { | |||
return { | |||
type: NEW_AUTH_ERR, | |||
payload: err | |||
} | |||
} | |||
export function clear(filter = {}) { | |||
// filter looks like: {type: 'spec'}, {source: 'parser'} | |||
return { | |||
type: CLEAR, | |||
payload: filter | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
# Error transformers | |||
Error transformers provide a standard interface for making generated error messages more useful to end users. | |||
### Inputs & outputs | |||
Each transformer's `transform` function is given an Immutable List of Immutable Maps as its first argument. | |||
It is expected that each `transform` function returns a List of similarly-formed Maps. | |||
These errors originate from the Redux error actions that add errors to state. Errors are transformed before being passed into the reducer. | |||
It's important that all the keys present in each error (specifically, `line`, `level`, `message`, `source`, and `type`) are present when the transformer is finished. | |||
##### Deleting an error | |||
If you want to delete an error completely, you can overwrite it with `null`. The null value will be filtered out of the transformed error array before the errors are returned. | |||
å | |||
### Example transformer | |||
This transformer will increase all your line numbers by 10. | |||
``` | |||
export function transform(errors) { | |||
return errors.map(err => { | |||
err.line += 10 | |||
return err | |||
}) | |||
} | |||
``` |
@@ -0,0 +1,57 @@ | |||
import concat from "lodash/concat" | |||
import reduce from "lodash/reduce" | |||
let request = require.context("./transformers/", true, /\.js$/) | |||
let errorTransformers = [] | |||
request.keys().forEach( function( key ){ | |||
if( key === "./hook.js" ) { | |||
return | |||
} | |||
if( !key.match(/js$/) ) { | |||
return | |||
} | |||
if( key.slice(2).indexOf("/") > -1) { | |||
// skip files in subdirs | |||
return | |||
} | |||
errorTransformers.push({ | |||
name: toTitleCase(key).replace(".js", "").replace("./", ""), | |||
transform: request(key).transform | |||
}) | |||
}) | |||
export default function transformErrors (errors, system) { | |||
let inputs = { | |||
jsSpec: system.specSelectors.specJson().toJS() | |||
} | |||
let transformedErrors = reduce(errorTransformers, (result, transformer) => { | |||
try { | |||
let newlyTransformedErrors = transformer.transform(result, inputs) | |||
return newlyTransformedErrors.filter(err => !!err) // filter removed errors | |||
} catch(e) { | |||
console.error("Transformer error:", e) | |||
return result | |||
} | |||
}, errors) | |||
return transformedErrors | |||
.filter(err => !!err) // filter removed errors | |||
.map(err => { | |||
if(!err.get("line") && err.get("path")) { | |||
// TODO: re-resolve line number if we've transformed it away | |||
} | |||
return err | |||
}) | |||
} | |||
function toTitleCase(str) { | |||
return str | |||
.split("-") | |||
.map(substr => substr[0].toUpperCase() + substr.slice(1)) | |||
.join("") | |||
} |
@@ -0,0 +1,29 @@ | |||
export function transform(errors) { | |||
// JSONSchema refers to the current object being validated | |||
// as 'instance'. This isn't helpful to users, so we remove it. | |||
return errors | |||
.map(err => { | |||
let seekStr = "is not of a type(s)" | |||
let i = err.get("message").indexOf(seekStr) | |||
if(i > -1) { | |||
let types = err.get("message").slice(i + seekStr.length).split(",") | |||
return err.set("message", err.get("message").slice(0, i) + makeNewMessage(types)) | |||
} else { | |||
return err | |||
} | |||
}) | |||
} | |||
function makeNewMessage(types) { | |||
return types.reduce((p, c, i, arr) => { | |||
if(i === arr.length - 1 && arr.length > 1) { | |||
return p + "or " + c | |||
} else if(arr[i+1] && arr.length > 2) { | |||
return p + c + ", " | |||
} else if(arr[i+1]) { | |||
return p + c + " " | |||
} else { | |||
return p + c | |||
} | |||
}, "should be a") | |||
} |
@@ -0,0 +1,60 @@ | |||
import get from "lodash/get" | |||
import last from "lodash/get" | |||
import { fromJS, List } from "immutable" | |||
export function transform(errors, { jsSpec }) { | |||
// LOOK HERE THIS TRANSFORMER IS CURRENTLY DISABLED 😃 | |||
// TODO: finish implementing, fix flattening problem | |||
/* eslint-disable no-unreachable */ | |||
return errors | |||
// JSONSchema gives us very little to go on | |||
let searchStr = "is not exactly one from <#/definitions/parameter>,<#/definitions/jsonReference>" | |||
return errors | |||
.map(err => { | |||
let message = err.get("message") | |||
let isParameterOneOfError = message.indexOf(searchStr) > -1 | |||
if(isParameterOneOfError) { | |||
// try to find what's wrong | |||
return createTailoredParameterError(err, jsSpec) | |||
} else { | |||
return err | |||
} | |||
}) | |||
.flatten(true) // shallow Immutable flatten | |||
} | |||
const VALID_IN_VALUES = ["path", "query", "header", "body", "formData"] | |||
const VALID_COLLECTIONFORMAT_VALUES = ["csv", "ssv", "tsv", "pipes", "multi"] | |||
function createTailoredParameterError(err, jsSpec) { | |||
let newErrs = [] | |||
let parameter = get(jsSpec, err.get("path")) | |||
// find addressable cases | |||
if(parameter.in && VALID_IN_VALUES.indexOf(parameter.in) === -1) { | |||
let message = `Wrong value for the "in" keyword. Expected one of: ${VALID_IN_VALUES.join(", ")}.` | |||
newErrs.push({ | |||
message, | |||
path: err.get("path") + ".in", | |||
type: "spec", | |||
source: "schema", | |||
level: "error" | |||
}) | |||
} | |||
if(parameter.collectionFormat && VALID_COLLECTIONFORMAT_VALUES.indexOf(parameter.collectionFormat) === -1) { | |||
let message = `Wrong value for the "collectionFormat" keyword. Expected one of: ${VALID_COLLECTIONFORMAT_VALUES.join(", ")}.` | |||
newErrs.push({ | |||
message, | |||
path: err.get("path") + ".collectionFormat", | |||
type: "spec", | |||
source: "schema", | |||
level: "error" | |||
}) | |||
} | |||
return newErrs.length ? fromJS(newErrs) : err // fall back to making no changes | |||
} |
@@ -0,0 +1,10 @@ | |||
export function transform(errors) { | |||
return errors | |||
.map(err => { | |||
return err.set("message", removeSubstring(err.get("message"), "instance.")) | |||
}) | |||
} | |||
function removeSubstring(str, substr) { | |||
return str.replace(new RegExp(substr, "g"), "") | |||
} |
@@ -0,0 +1,15 @@ | |||
import makeReducers from "./reducers" | |||
import * as actions from "./actions" | |||
import * as selectors from "./selectors" | |||
export default function(system) { | |||
return { | |||
statePlugins: { | |||
err: { | |||
reducers: makeReducers(system), | |||
actions, | |||
selectors | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
import { | |||
NEW_THROWN_ERR, | |||
NEW_THROWN_ERR_BATCH, | |||
NEW_SPEC_ERR, | |||
NEW_AUTH_ERR, | |||
CLEAR | |||
} from "./actions" | |||
import reject from "lodash/reject" | |||
import Im, { fromJS, List } from "immutable" | |||
import transformErrors from "./error-transformers/hook" | |||
let DEFAULT_ERROR_STRUCTURE = { | |||
// defaults | |||
line: 0, | |||
level: "error", | |||
message: "Unknown error" | |||
} | |||
export default function(system) { | |||
return { | |||
[NEW_THROWN_ERR]: (state, { payload }) => { | |||
let error = Object.assign(DEFAULT_ERROR_STRUCTURE, payload, {type: "thrown"}) | |||
return state | |||
.update("errors", errors => (errors || List()).push( fromJS( error )) ) | |||
.update("errors", errors => transformErrors(errors, system.getSystem())) | |||
}, | |||
[NEW_THROWN_ERR_BATCH]: (state, { payload }) => { | |||
payload = payload.map(err => { | |||
return fromJS(Object.assign(DEFAULT_ERROR_STRUCTURE, err, { type: "thrown" })) | |||
}) | |||
return state | |||
.update("errors", errors => (errors || List()).concat( fromJS( payload )) ) | |||
.update("errors", errors => transformErrors(errors, system.getSystem())) | |||
}, | |||
[NEW_SPEC_ERR]: (state, { payload }) => { | |||
let error = fromJS(payload) | |||
error = error.set("type", "spec") | |||
return state | |||
.update("errors", errors => (errors || List()).push( fromJS(error)).sortBy(err => err.get("line")) ) | |||
.update("errors", errors => transformErrors(errors, system.getSystem())) | |||
}, | |||
[NEW_AUTH_ERR]: (state, { payload }) => { | |||
let error = fromJS(Object.assign({}, payload)) | |||
error = error.set("type", "auth") | |||
return state | |||
.update("errors", errors => (errors || List()).push( fromJS(error)) ) | |||
.update("errors", errors => transformErrors(errors, system.getSystem())) | |||
}, | |||
[CLEAR]: (state, { payload }) => { | |||
if(!payload) { | |||
return | |||
} | |||
// TODO: Rework, to use immutable only, no need for lodash | |||
let newErrors = Im.fromJS(reject((state.get("errors") || List()).toJS(), payload)) | |||
return state.merge({ | |||
errors: newErrors | |||
}) | |||
} | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
import { List } from "immutable" | |||
import { createSelector } from "reselect" | |||
const state = state => state | |||
export const allErrors = createSelector( | |||
state, | |||
err => err.get("errors", List()) | |||
) | |||
export const lastError = createSelector( | |||
allErrors, | |||
all => all.last() | |||
) | |||
@@ -0,0 +1,45 @@ | |||
import { normalizeArray } from "core/utils" | |||
export const UPDATE_LAYOUT = "layout_update_layout" | |||
export const UPDATE_MODE = "layout_update_mode" | |||
export const SHOW = "layout_show" | |||
// export const ONLY_SHOW = "layout_only_show" | |||
export function updateLayout(layout) { | |||
return { | |||
type: UPDATE_LAYOUT, | |||
payload: layout | |||
} | |||
} | |||
export function show(thing, shown=true) { | |||
thing = normalizeArray(thing) | |||
return { | |||
type: SHOW, | |||
payload: {thing, shown} | |||
} | |||
} | |||
// Simple string key-store, used for | |||
export function changeMode(thing, mode="") { | |||
thing = normalizeArray(thing) | |||
return { | |||
type: UPDATE_MODE, | |||
payload: {thing, mode} | |||
} | |||
} | |||
// export function onlyShow(thing, shown=true) { | |||
// thing = normalizeArray(thing) | |||
// if(thing.length < 2) | |||
// throw new Error("layoutActions.onlyShow only works, when `thing` is an array with length > 1") | |||
// return { | |||
// type: ONLY_SHOW, | |||
// payload: {thing, shown} | |||
// } | |||
// } | |||
@@ -0,0 +1,15 @@ | |||
import reducers from "./reducers" | |||
import * as actions from "./actions" | |||
import * as selectors from "./selectors" | |||
export default function() { | |||
return { | |||
statePlugins: { | |||
layout: { | |||
reducers, | |||
actions, | |||
selectors | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
import { | |||
UPDATE_LAYOUT, | |||
UPDATE_MODE, | |||
SHOW | |||
} from "./actions" | |||
export default { | |||
[UPDATE_LAYOUT]: (state, action) => state.set("layout", action.payload), | |||
[SHOW]: (state, action) => { | |||
let thing = action.payload.thing | |||
let shown = action.payload.shown | |||
return state.setIn(["shown"].concat(thing), shown) | |||
}, | |||
[UPDATE_MODE]: (state, action) => { | |||
let thing = action.payload.thing | |||
let mode = action.payload.mode | |||
return state.setIn(["modes"].concat(thing), (mode || "") + "") | |||
} | |||
} | |||
@@ -0,0 +1,22 @@ | |||
import { createSelector } from "reselect" | |||
import { normalizeArray } from "core/utils" | |||
const state = state => state | |||
export const current = state => state.get("layout") | |||
export const isShown = (state, thing, def) => { | |||
thing = normalizeArray(thing) | |||
return Boolean(state.getIn(["shown", ...thing], def)) | |||
} | |||
export const whatMode = (state, thing, def="") => { | |||
thing = normalizeArray(thing) | |||
return state.getIn(["modes", ...thing], def) | |||
} | |||
export const showSummary = createSelector( | |||
state, | |||
state => !isShown(state, "editor") | |||
) | |||
@@ -0,0 +1,28 @@ | |||
export default function ({configs}) { | |||
const levels = { | |||
"debug": 0, | |||
"info": 1, | |||
"log": 2, | |||
"warn": 3, | |||
"error": 4 | |||
} | |||
const getLevel = (level) => levels[level] || -1 | |||
let { logLevel } = configs | |||
let logLevelInt = getLevel(logLevel) | |||
function log(level, ...args) { | |||
if(getLevel(level) >= logLevelInt) | |||
// eslint-disable-next-line no-console | |||
console[level](...args) | |||
} | |||
log.warn = log.bind(null, "warn") | |||
log.error = log.bind(null, "error") | |||
log.info = log.bind(null, "info") | |||
log.debug = log.bind(null, "debug") | |||
return { rootInjects: { log } } | |||
} |
@@ -0,0 +1,223 @@ | |||
import { objectify, isFunc, normalizeArray } from "core/utils" | |||
import XML from "xml" | |||
import memoizee from "memoizee" | |||
const primitives = { | |||
"string": () => "string", | |||
"string_email": () => "user@example.com", | |||
"string_date-time": () => new Date().toISOString(), | |||
"number": () => 0, | |||
"number_float": () => 0.0, | |||
"integer": () => 0, | |||
"boolean": () => true | |||
} | |||
const primitive = (schema) => { | |||
schema = objectify(schema) | |||
let { type, format } = schema | |||
let fn = primitives[`${type}_${format}`] || primitives[type] | |||
if(isFunc(fn)) | |||
return fn(schema) | |||
return "Unknown Type: " + schema.type | |||
} | |||
export const sampleFromSchema = (schema, config={}) => { | |||
let { type, example, properties, additionalProperties, items } = objectify(schema) | |||
let { includeReadOnly } = config | |||
if(example !== undefined) | |||
return example | |||
if(!type) { | |||
if(properties) { | |||
type = "object" | |||
} else if(items) { | |||
type = "array" | |||
} else { | |||
return | |||
} | |||
} | |||
if(type === "object") { | |||
let props = objectify(properties) | |||
let obj = {} | |||
for (var name in props) { | |||
if ( !props[name].readOnly || includeReadOnly ) { | |||
obj[name] = sampleFromSchema(props[name]) | |||
} | |||
} | |||
if ( additionalProperties === true ) { | |||
obj.additionalProp1 = {} | |||
} else if ( additionalProperties ) { | |||
let additionalProps = objectify(additionalProperties) | |||
let additionalPropVal = sampleFromSchema(additionalProps) | |||
for (let i = 1; i < 4; i++) { | |||
obj["additionalProp" + i] = additionalPropVal | |||
} | |||
} | |||
return obj | |||
} | |||
if(type === "array") { | |||
return [ sampleFromSchema(items) ] | |||
} | |||
if(schema["enum"]) { | |||
if(schema["default"]) | |||
return schema["default"] | |||
return normalizeArray(schema["enum"])[0] | |||
} | |||
return primitive(schema) | |||
} | |||
export const inferSchema = (thing) => { | |||
if(thing.schema) | |||
thing = thing.schema | |||
if(thing.properties) { | |||
thing.type = "object" | |||
} | |||
return thing // Hopefully this will have something schema like in it... `type` for example | |||
} | |||
export const sampleXmlFromSchema = (schema, config={}) => { | |||
let objectifySchema = objectify(schema) | |||
let { type, properties, additionalProperties, items, example } = objectifySchema | |||
let { includeReadOnly } = config | |||
let defaultValue = objectifySchema.default | |||
let res = {} | |||
let _attr = {} | |||
let { xml } = schema | |||
let { name, prefix, namespace } = xml | |||
let enumValue = objectifySchema.enum | |||
let displayName, value | |||
if(!type) { | |||
if(properties || additionalProperties) { | |||
type = "object" | |||
} else if(items) { | |||
type = "array" | |||
} else { | |||
return | |||
} | |||
} | |||
name = name || "notagname" | |||
// add prefix to name if exists | |||
displayName = (prefix ? prefix + ":" : "") + name | |||
if ( namespace ) { | |||
//add prefix to namespace if exists | |||
let namespacePrefix = prefix ? ( "xmlns:" + prefix ) : "xmlns" | |||
_attr[namespacePrefix] = namespace | |||
} | |||
if (type === "array") { | |||
if (items) { | |||
items.xml = items.xml || xml || {} | |||
items.xml.name = items.xml.name || xml.name | |||
if (xml.wrapped) { | |||
res[displayName] = [] | |||
if (Array.isArray(defaultValue)) { | |||
defaultValue.forEach((v)=>{ | |||
items.default = v | |||
res[displayName].push(sampleXmlFromSchema(items, config)) | |||
}) | |||
} else { | |||
res[displayName] = [sampleXmlFromSchema(items, config)] | |||
} | |||
if (_attr) { | |||
res[displayName].push({_attr: _attr}) | |||
} | |||
return res | |||
} | |||
let _res = [] | |||
if (Array.isArray(defaultValue)) { | |||
defaultValue.forEach((v)=>{ | |||
items.default = v | |||
_res.push(sampleXmlFromSchema(items, config)) | |||
}) | |||
return _res | |||
} | |||
return sampleXmlFromSchema(items, config) | |||
} | |||
} | |||
if (type === "object") { | |||
let props = objectify(properties) | |||
res[displayName] = [] | |||
example = example || {} | |||
for (let propName in props) { | |||
if ( !props[propName].readOnly || includeReadOnly ) { | |||
props[propName].xml = props[propName].xml || {} | |||
if (props[propName].xml.attribute) { | |||
let enumAttrVal = Array.isArray(props[propName].enum) && props[propName].enum[0] | |||
let attrExample = props[propName].example | |||
let attrDefault = props[propName].default | |||
_attr[props[propName].xml.name || propName] = attrExample!== undefined && attrExample | |||
|| example[propName] !== undefined && example[propName] || attrDefault !== undefined && attrDefault | |||
|| enumAttrVal || primitive(props[propName]) | |||
} else { | |||
props[propName].xml.name = props[propName].xml.name || propName | |||
props[propName].example = props[propName].example !== undefined ? props[propName].example : example[propName] | |||
res[displayName].push(sampleXmlFromSchema(props[propName])) | |||
} | |||
} | |||
} | |||
if (additionalProperties === true) { | |||
res[displayName].push({additionalProp: "Anything can be here"}) | |||
} else if (additionalProperties) { | |||
res[displayName].push({additionalProp: primitive(additionalProperties)}) | |||
} | |||
if (_attr) { | |||
res[displayName].push({_attr: _attr}) | |||
} | |||
return res | |||
} | |||
if (example !== undefined) { | |||
value = example | |||
} else if (defaultValue !== undefined) { | |||
//display example if exists | |||
value = defaultValue | |||
} else if (Array.isArray(enumValue)) { | |||
//display enum first value | |||
value = enumValue[0] | |||
} else { | |||
//set default value | |||
value = primitive(schema) | |||
} | |||
res[displayName] = _attr ? [{_attr: _attr}, value] : value | |||
return res | |||
} | |||
export function createXMLExample(schema, config) { | |||
let json = sampleXmlFromSchema(schema, config) | |||
if (!json) { return } | |||
return XML(json, { declaration: true, indent: "\t" }) | |||
} | |||
export const memoizedCreateXMLExample = memoizee(createXMLExample) | |||
export const memoizedSampleFromSchema = memoizee(sampleFromSchema) |
@@ -0,0 +1,5 @@ | |||
import * as fn from "./fn" | |||
export default function () { | |||
return { fn } | |||
} |
@@ -0,0 +1,234 @@ | |||
import YAML from "js-yaml" | |||
import serializeError from "serialize-error" | |||
// Actions conform to FSA (flux-standard-actions) | |||
// {type: string,payload: Any|Error, meta: obj, error: bool} | |||
export const UPDATE_SPEC = "spec_update_spec" | |||
export const UPDATE_URL = "spec_update_url" | |||
export const UPDATE_JSON = "spec_update_json" | |||
export const UPDATE_PARAM = "spec_update_param" | |||
export const VALIDATE_PARAMS = "spec_validate_param" | |||
export const SET_RESPONSE = "spec_set_response" | |||
export const SET_REQUEST = "spec_set_request" | |||
export const LOG_REQUEST = "spec_log_request" | |||
export const CLEAR_RESPONSE = "spec_clear_response" | |||
export const CLEAR_REQUEST = "spec_clear_request" | |||
export const ClEAR_VALIDATE_PARAMS = "spec_clear_validate_param" | |||
export const UPDATE_OPERATION_VALUE = "spec_update_operation_value" | |||
export const UPDATE_RESOLVED = "spec_update_resolved" | |||
export const SET_SCHEME = "set_scheme" | |||
export function updateSpec(spec) { | |||
if(spec instanceof Error) { | |||
return {type: UPDATE_SPEC, error: true, payload: spec} | |||
} | |||
if(typeof spec === "string") { | |||
return { | |||
type: UPDATE_SPEC, | |||
payload: spec.replace(/\t/g, " ") || "" | |||
} | |||
} | |||
return { | |||
type: UPDATE_SPEC, | |||
payload: "" | |||
} | |||
} | |||
export function updateResolved(spec) { | |||
return { | |||
type: UPDATE_RESOLVED, | |||
payload: spec | |||
} | |||
} | |||
export function updateUrl(url) { | |||
return {type: UPDATE_URL, payload: url} | |||
} | |||
export function updateJsonSpec(json) { | |||
if(!json || typeof json !== "object") { | |||
throw new Error("updateJson must only accept a simple JSON object") | |||
} | |||
return {type: UPDATE_JSON, payload: json} | |||
} | |||
export const parseToJson = (str) => ({specActions, specSelectors, errActions}) => { | |||
let { specStr } = specSelectors | |||
let json = null | |||
try { | |||
str = str || specStr() | |||
errActions.clear({ source: "parser" }) | |||
json = YAML.safeLoad(str) | |||
} catch(e) { | |||
// TODO: push error to state | |||
console.error(e) | |||
return errActions.newSpecErr({ | |||
source: "parser", | |||
level: "error", | |||
message: e.reason, | |||
line: e.mark && e.mark.line ? e.mark.line + 1 : undefined | |||
}) | |||
} | |||
return specActions.updateJsonSpec(json) | |||
} | |||
export const resolveSpec = (json, url) => ({specActions, specSelectors, errActions, fn: { fetch, resolve, AST }}) => { | |||
if(typeof(json) === "undefined") { | |||
json = specSelectors.specJson() | |||
} | |||
if(typeof(url) === "undefined") { | |||
url = specSelectors.url() | |||
} | |||
let { getLineNumberForPath } = AST | |||
let specStr = specSelectors.specStr() | |||
return resolve({fetch, spec: json, baseDoc: url}) | |||
.then( ({spec, errors}) => { | |||
errActions.clear({ | |||
type: "thrown" | |||
}) | |||
if(errors.length > 0) { | |||
let preparedErrors = errors | |||
.map(err => { | |||
console.error(err) | |||
err.line = err.fullPath ? getLineNumberForPath(specStr, err.fullPath) : null | |||
err.path = err.fullPath ? err.fullPath.join(".") : null | |||
err.level = "error" | |||
err.type = "thrown" | |||
err.source = "resolver" | |||
Object.defineProperty(err, "message", { enumerable: true, value: err.message }) | |||
return err | |||
}) | |||
errActions.newThrownErrBatch(preparedErrors) | |||
} | |||
return specActions.updateResolved(spec) | |||
}) | |||
} | |||
export const formatIntoYaml = () => ({specActions, specSelectors}) => { | |||
let { specStr } = specSelectors | |||
let { updateSpec } = specActions | |||
try { | |||
let yaml = YAML.safeDump(YAML.safeLoad(specStr()), {indent: 2}) | |||
updateSpec(yaml) | |||
} catch(e) { | |||
updateSpec(e) | |||
} | |||
} | |||
export function changeParam( path, paramName, value, isXml ){ | |||
return { | |||
type: UPDATE_PARAM, | |||
payload:{ path, value, paramName, isXml } | |||
} | |||
} | |||
export function validateParams( payload ){ | |||
return { | |||
type: VALIDATE_PARAMS, | |||
payload:{ pathMethod: payload } | |||
} | |||
} | |||
export function clearValidateParams( payload ){ | |||
return { | |||
type: ClEAR_VALIDATE_PARAMS, | |||
payload:{ pathMethod: payload } | |||
} | |||
} | |||
export function changeConsumesValue(path, value) { | |||
return { | |||
type: UPDATE_OPERATION_VALUE, | |||
payload:{ path, value, key: "consumes_value" } | |||
} | |||
} | |||
export function changeProducesValue(path, value) { | |||
return { | |||
type: UPDATE_OPERATION_VALUE, | |||
payload:{ path, value, key: "produces_value" } | |||
} | |||
} | |||
export const setResponse = ( path, method, res ) => { | |||
return { | |||
payload: { path, method, res }, | |||
type: SET_RESPONSE | |||
} | |||
} | |||
export const setRequest = ( path, method, req ) => { | |||
return { | |||
payload: { path, method, req }, | |||
type: SET_REQUEST | |||
} | |||
} | |||
// This is for debugging, remove this comment if you depend on this action | |||
export const logRequest = (req) => { | |||
return { | |||
payload: req, | |||
type: LOG_REQUEST | |||
} | |||
} | |||
// Actually fire the request via fn.execute | |||
// (For debugging) and ease of testing | |||
export const executeRequest = (req) => ({fn, specActions, errActions}) => { | |||
let { pathName, method } = req | |||
let parsedRequest = Object.assign({}, req) | |||
if ( pathName && method ) { | |||
parsedRequest.operationId = method.toLowerCase() + "-" + pathName | |||
} | |||
parsedRequest = fn.buildRequest(parsedRequest) | |||
specActions.setRequest(req.pathName, req.method, parsedRequest) | |||
return fn.execute(req) | |||
.then( fn.serializeRes ) | |||
.then( res => specActions.setResponse(req.pathName, req.method, res)) | |||
.catch( err => specActions.setResponse(req.pathName, req.method, { error: true, err: serializeError(err) } ) ) | |||
} | |||
// I'm using extras as a way to inject properties into the final, `execute` method - It's not great. Anyone have a better idea? @ponelat | |||
export const execute = ( { path, method, ...extras }={} ) => (system) => { | |||
let { fn:{fetch}, specSelectors, specActions } = system | |||
let spec = specSelectors.spec().toJS() | |||
let scheme = specSelectors.operationScheme(path, method) | |||
let { requestContentType, responseContentType } = specSelectors.contentTypeValues([path, method]).toJS() | |||
let isXml = /xml/i.test(requestContentType) | |||
let parameters = specSelectors.parameterValues([path, method], isXml).toJS() | |||
return specActions.executeRequest({fetch, spec, pathName: path, method, parameters, requestContentType, scheme, responseContentType, ...extras }) | |||
} | |||
export function clearResponse (path, method) { | |||
return { | |||
type: CLEAR_RESPONSE, | |||
payload:{ path, method } | |||
} | |||
} | |||
export function clearRequest (path, method) { | |||
return { | |||
type: CLEAR_REQUEST, | |||
payload:{ path, method } | |||
} | |||
} | |||
export function setScheme (scheme, path, method) { | |||
return { | |||
type: SET_SCHEME, | |||
payload: { scheme, path, method } | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
import reducers from "./reducers" | |||
import * as actions from "./actions" | |||
import * as selectors from "./selectors" | |||
import * as wrapActions from "./wrap-actions" | |||
export default function() { | |||
return { | |||
statePlugins: { | |||
spec: { | |||
wrapActions, | |||
reducers, | |||
actions, | |||
selectors | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,123 @@ | |||
import { fromJS } from "immutable" | |||
import { fromJSOrdered, validateParam } from "core/utils" | |||
import win from "../../window" | |||
import { | |||
UPDATE_SPEC, | |||
UPDATE_URL, | |||
UPDATE_JSON, | |||
UPDATE_PARAM, | |||
VALIDATE_PARAMS, | |||
SET_RESPONSE, | |||
SET_REQUEST, | |||
UPDATE_RESOLVED, | |||
UPDATE_OPERATION_VALUE, | |||
CLEAR_RESPONSE, | |||
CLEAR_REQUEST, | |||
ClEAR_VALIDATE_PARAMS, | |||
SET_SCHEME | |||
} from "./actions" | |||
export default { | |||
[UPDATE_SPEC]: (state, action) => { | |||
return (typeof action.payload === "string") | |||
? state.set("spec", action.payload) | |||
: state | |||
}, | |||
[UPDATE_URL]: (state, action) => { | |||
return state.set("url", action.payload+"") | |||
}, | |||
[UPDATE_JSON]: (state, action) => { | |||
return state.set("json", fromJSOrdered(action.payload)) | |||
}, | |||
[UPDATE_RESOLVED]: (state, action) => { | |||
return state.setIn(["resolved"], fromJSOrdered(action.payload)) | |||
}, | |||
[UPDATE_PARAM]: ( state, {payload} ) => { | |||
let { path, paramName, value, isXml } = payload | |||
return state.updateIn( [ "resolved", "paths", ...path, "parameters" ], fromJS([]), parameters => { | |||
let index = parameters.findIndex( p => p.get( "name" ) === paramName ) | |||
if (!(value instanceof win.File)) { | |||
value = fromJSOrdered( value ) | |||
} | |||
return parameters.setIn( [ index, isXml ? "value_xml" : "value" ], value) | |||
}) | |||
}, | |||
[VALIDATE_PARAMS]: ( state, { payload: { pathMethod } } ) => { | |||
let operation = state.getIn( [ "resolved", "paths", ...pathMethod ] ) | |||
let parameters = operation.get("parameters") | |||
let isXml = /xml/i.test(operation.get("consumes_value")) | |||
return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => { | |||
return parameters.withMutations( parameters => { | |||
for ( let i = 0, len = parameters.count(); i < len; i++ ) { | |||
let errors = validateParam(parameters.get(i), isXml) | |||
parameters.setIn([i, "errors"], fromJS(errors)) | |||
} | |||
}) | |||
}) | |||
}, | |||
[ClEAR_VALIDATE_PARAMS]: ( state, { payload: { pathMethod } } ) => { | |||
let operation = state.getIn( [ "resolved", "paths", ...pathMethod ] ) | |||
let parameters = operation.get("parameters") | |||
return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => { | |||
return parameters.withMutations( parameters => { | |||
for ( let i = 0, len = parameters.count(); i < len; i++ ) { | |||
parameters.setIn([i, "errors"], fromJS({})) | |||
} | |||
}) | |||
}) | |||
}, | |||
[SET_RESPONSE]: (state, { payload: { res, path, method } } ) =>{ | |||
let result | |||
if ( res.error ) { | |||
result = Object.assign({error: true}, res.err) | |||
} else { | |||
result = res | |||
} | |||
let newState = state.setIn( [ "responses", path, method ], fromJSOrdered(result) ) | |||
// ImmutableJS messes up Blob. Needs to reset its value. | |||
if (res.data instanceof win.Blob) { | |||
newState = newState.setIn( [ "responses", path, method, "text" ], res.data) | |||
} | |||
return newState | |||
}, | |||
[SET_REQUEST]: (state, { payload: { req, path, method } } ) =>{ | |||
return state.setIn( [ "requests", path, method ], fromJSOrdered(req)) | |||
}, | |||
[UPDATE_OPERATION_VALUE]: (state, { payload: { path, value, key } }) => { | |||
return state.setIn(["resolved", "paths", ...path, key], fromJS(value)) | |||
}, | |||
[CLEAR_RESPONSE]: (state, { payload: { path, method } } ) =>{ | |||
return state.deleteIn( [ "responses", path, method ]) | |||
}, | |||
[CLEAR_REQUEST]: (state, { payload: { path, method } } ) =>{ | |||
return state.deleteIn( [ "requests", path, method ]) | |||
}, | |||
[SET_SCHEME]: (state, { payload: { scheme, path, method } } ) =>{ | |||
if ( path && method ) { | |||
return state.setIn( [ "scheme", path, method ], scheme) | |||
} | |||
if (!path && !method) { | |||
return state.setIn( [ "scheme", "_defaultScheme" ], scheme) | |||
} | |||
} | |||
} |
@@ -0,0 +1,318 @@ | |||
import { createSelector } from "reselect" | |||
import { fromJS, Set, Map, List } from "immutable" | |||
const DEFAULT_TAG = "default" | |||
const OPERATION_METHODS = ["get", "put", "post", "delete", "options", "head", "patch"] | |||
const state = state => { | |||
return state || Map() | |||
} | |||
export const lastError = createSelector( | |||
state, | |||
spec => spec.get("lastError") | |||
) | |||
export const url = createSelector( | |||
state, | |||
spec => spec.get("url") | |||
) | |||
export const specStr = createSelector( | |||
state, | |||
spec => spec.get("spec") || "" | |||
) | |||
export const specSource = createSelector( | |||
state, | |||
spec => spec.get("specSource") || "not-editor" | |||
) | |||
export const specJson = createSelector( | |||
state, | |||
spec => spec.get("json", Map()) | |||
) | |||
export const specResolved = createSelector( | |||
state, | |||
spec => spec.get("resolved", Map()) | |||
) | |||
// Default Spec ( as an object ) | |||
export const spec = state => { | |||
let res = specResolved(state) | |||
if(res.count() < 1) | |||
res = specJson(state) | |||
return res | |||
} | |||
export const info = createSelector( | |||
spec, | |||
spec => returnSelfOrNewMap(spec && spec.get("info")) | |||
) | |||
export const externalDocs = createSelector( | |||
spec, | |||
spec => returnSelfOrNewMap(spec && spec.get("externalDocs")) | |||
) | |||
export const version = createSelector( | |||
info, | |||
info => info && info.get("version") | |||
) | |||
export const semver = createSelector( | |||
version, | |||
version => /v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(version).slice(1) | |||
) | |||
export const paths = createSelector( | |||
spec, | |||
spec => spec.get("paths") | |||
) | |||
export const operations = createSelector( | |||
paths, | |||
paths => { | |||
if(!paths || paths.size < 1) | |||
return List() | |||
let list = List() | |||
if(!paths || !paths.forEach) { | |||
return List() | |||
} | |||
paths.forEach((path, pathName) => { | |||
if(!path || !path.forEach) { | |||
return {} | |||
} | |||
path.forEach((operation, method) => { | |||
if(OPERATION_METHODS.indexOf(method) === -1) { | |||
return | |||
} | |||
list = list.push(fromJS({ | |||
path: pathName, | |||
method, | |||
operation, | |||
id: `${method}-${pathName}` | |||
})) | |||
}) | |||
}) | |||
return list | |||
} | |||
) | |||
export const consumes = createSelector( | |||
spec, | |||
spec => Set(spec.get("consumes")) | |||
) | |||
export const produces = createSelector( | |||
spec, | |||
spec => Set(spec.get("produces")) | |||
) | |||
export const security = createSelector( | |||
spec, | |||
spec => spec.get("security", List()) | |||
) | |||
export const securityDefinitions = createSelector( | |||
spec, | |||
spec => spec.get("securityDefinitions") | |||
) | |||
export const findDefinition = ( state, name ) => { | |||
return specResolved(state).getIn(["definitions", name], null) | |||
} | |||
export const definitions = createSelector( | |||
spec, | |||
spec => spec.get("definitions") || Map() | |||
) | |||
export const basePath = createSelector( | |||
spec, | |||
spec => spec.get("basePath") | |||
) | |||
export const host = createSelector( | |||
spec, | |||
spec => spec.get("host") | |||
) | |||
export const schemes = createSelector( | |||
spec, | |||
spec => spec.get("schemes", Map()) | |||
) | |||
export const operationsWithRootInherited = createSelector( | |||
operations, | |||
consumes, | |||
produces, | |||
(operations, consumes, produces) => { | |||
return operations.map( ops => ops.update("operation", op => { | |||
if(op) { | |||
if(!Map.isMap(op)) { return } | |||
return op.withMutations( op => { | |||
if ( !op.get("consumes") ) { | |||
op.update("consumes", a => Set(a).merge(consumes)) | |||
} | |||
if ( !op.get("produces") ) { | |||
op.update("produces", a => Set(a).merge(produces)) | |||
} | |||
return op | |||
}) | |||
} else { | |||
// return something with Immutable methods | |||
return Map() | |||
} | |||
})) | |||
} | |||
) | |||
export const tags = createSelector( | |||
spec, | |||
json => json.get("tags", List()) | |||
) | |||
export const tagDetails = (state, tag) => { | |||
let currentTags = tags(state) || List() | |||
return currentTags.filter(Map.isMap).find(t => t.get("name") === tag, Map()) | |||
} | |||
export const operationsWithTags = createSelector( | |||
operationsWithRootInherited, | |||
operations => { | |||
return operations.reduce( (taggedMap, op) => { | |||
let tags = Set(op.getIn(["operation","tags"])) | |||
if(tags.count() < 1) | |||
return taggedMap.update(DEFAULT_TAG, List(), ar => ar.push(op)) | |||
return tags.reduce( (res, tag) => res.update(tag, List(), (ar) => ar.push(op)), taggedMap ) | |||
}, Map()) | |||
} | |||
) | |||
export const taggedOperations = createSelector( | |||
state, | |||
operationsWithTags, | |||
(state, tagMap) => { | |||
return tagMap.map((ops, tag) => Map({tagDetails: tagDetails(state, tag), operations: ops})) | |||
} | |||
) | |||
export const responses = createSelector( | |||
state, | |||
state => state.get( "responses", Map() ) | |||
) | |||
export const requests = createSelector( | |||
state, | |||
state => state.get( "requests", Map() ) | |||
) | |||
export const responseFor = (state, path, method) => { | |||
return responses(state).getIn([path, method], null) | |||
} | |||
export const requestFor = (state, path, method) => { | |||
return requests(state).getIn([path, method], null) | |||
} | |||
export const allowTryItOutFor = (state, path, method ) => { | |||
// This is just a hook for now. | |||
return true | |||
} | |||
// Get the parameter value by parameter name | |||
export function getParameter(state, pathMethod, name) { | |||
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([])) | |||
return params.filter( (p) => { | |||
return Map.isMap(p) && p.get("name") === name | |||
}).first() | |||
} | |||
export const hasHost = createSelector( | |||
spec, | |||
spec => { | |||
const host = spec.get("host") | |||
return typeof host === "string" && host.length > 0 && host[0] !== "/" | |||
} | |||
) | |||
// Get the parameter values, that the user filled out | |||
export function parameterValues(state, pathMethod, isXml) { | |||
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([])) | |||
return params.reduce( (hash, p) => { | |||
let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value") | |||
return hash.set(p.get("name"), value) | |||
}, fromJS({})) | |||
} | |||
// True if any parameter includes `in: ?` | |||
export function parametersIncludeIn(parameters, inValue="") { | |||
if(List.isList(parameters)) { | |||
return parameters.some( p => Map.isMap(p) && p.get("in") === inValue ) | |||
} | |||
} | |||
// True if any parameter includes `type: ?` | |||
export function parametersIncludeType(parameters, typeValue="") { | |||
if(List.isList(parameters)) { | |||
return parameters.some( p => Map.isMap(p) && p.get("type") === typeValue ) | |||
} | |||
} | |||
// Get the consumes/produces value that the user selected | |||
export function contentTypeValues(state, pathMethod) { | |||
let op = spec(state).getIn(["paths", ...pathMethod], fromJS({})) | |||
const parameters = op.get("parameters") || new List() | |||
const requestContentType = ( | |||
parametersIncludeType(parameters, "file") ? "multipart/form-data" | |||
: parametersIncludeIn(parameters, "formData") ? "application/x-www-form-urlencoded" | |||
: op.get("consumes_value") | |||
) | |||
return fromJS({ | |||
requestContentType, | |||
responseContentType: op.get("produces_value") | |||
}) | |||
} | |||
// Get the consumes/produces by path | |||
export function operationConsumes(state, pathMethod) { | |||
return spec(state).getIn(["paths", ...pathMethod, "consumes"], fromJS({})) | |||
} | |||
export const operationScheme = ( state, path, method ) => { | |||
return state.getIn(["scheme", path, method]) || state.getIn(["scheme", "_defaultScheme"]) || "http" | |||
} | |||
export const canExecuteScheme = ( state, path, method ) => { | |||
return ["http", "https"].indexOf(operationScheme(state, path, method)) > -1 | |||
} | |||
export const validateBeforeExecute = ( state, pathMethod ) => { | |||
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([])) | |||
let isValid = true | |||
params.forEach( (p) => { | |||
let errors = p.get("errors") | |||
if ( errors && errors.count() ) { | |||
isValid = false | |||
} | |||
}) | |||
return isValid | |||
} | |||
function returnSelfOrNewMap(obj) { | |||
// returns obj if obj is an Immutable map, else returns a new Map | |||
return Map.isMap(obj) ? obj : new Map() | |||
} |
@@ -0,0 +1,15 @@ | |||
export const updateSpec = (ori, {specActions}) => (...args) => { | |||
ori(...args) | |||
specActions.parseToJson(...args) | |||
} | |||
export const updateJsonSpec = (ori, {specActions}) => (...args) => { | |||
ori(...args) | |||
specActions.resolveSpec(...args) | |||
} | |||
// Log the request ( just for debugging, shouldn't affect prod ) | |||
export const executeRequest = (ori, { specActions }) => (req) => { | |||
specActions.logRequest(req) | |||
return ori(req) | |||
} |
@@ -0,0 +1,17 @@ | |||
import { pascalCaseFilename } from "core/utils" | |||
const request = require.context(".", true, /\.jsx?$/) | |||
request.keys().forEach( function( key ){ | |||
if( key === "./index.js" ) { | |||
return | |||
} | |||
// if( key.slice(2).indexOf("/") > -1) { | |||
// // skip files in subdirs | |||
// return | |||
// } | |||
let mod = request(key) | |||
module.exports[pascalCaseFilename(key)] = mod.default ? mod.default : mod | |||
}) |
@@ -0,0 +1,81 @@ | |||
import React, { PropTypes } from "react" | |||
import SplitPane from "react-split-pane" | |||
import "./split-pane-mode.less" | |||
const MODE_KEY = ["split-pane-mode"] | |||
const MODE_LEFT = "left" | |||
const MODE_RIGHT = "right" | |||
const MODE_BOTH = "both" // or anything other than left/right | |||
export default class SplitPaneMode extends React.Component { | |||
static propTypes = { | |||
threshold: PropTypes.number, | |||
children: PropTypes.array, | |||
layoutSelectors: PropTypes.object.isRequired, | |||
layoutActions: PropTypes.object.isRequired, | |||
}; | |||
static defaultProps = { | |||
threshold: 100, // in pixels | |||
children: [], | |||
}; | |||
onDragFinished = () => { | |||
let { threshold, layoutActions } = this.props | |||
let { position, draggedSize } = this.refs.splitPane.state | |||
this.draggedSize = draggedSize | |||
let nearLeftEdge = position <= threshold | |||
let nearRightEdge = draggedSize <= threshold | |||
layoutActions | |||
.changeMode(MODE_KEY, ( | |||
nearLeftEdge | |||
? MODE_RIGHT : nearRightEdge | |||
? MODE_LEFT : MODE_BOTH | |||
)) | |||
} | |||
sizeFromMode = (mode, defaultSize) => { | |||
if(mode === MODE_LEFT) { | |||
this.draggedSize = null | |||
return "0px" | |||
} else if (mode === MODE_RIGHT) { | |||
this.draggedSize = null | |||
return "100%" | |||
} | |||
// mode === "both" | |||
return this.draggedSize || defaultSize | |||
} | |||
render() { | |||
let { children, layoutSelectors } = this.props | |||
const mode = layoutSelectors.whatMode(MODE_KEY) | |||
const left = mode === MODE_RIGHT ? <noscript/> : children[0] | |||
const right = mode === MODE_LEFT ? <noscript/> : children[1] | |||
const size = this.sizeFromMode(mode, '50%') | |||
return ( | |||
<SplitPane | |||
disabledClass={''} | |||
ref={'splitPane'} | |||
split='vertical' | |||
defaultSize={'50%'} | |||
primary="second" | |||
minSize={0} | |||
size={size} | |||
onDragFinished={this.onDragFinished} | |||
allowResize={mode !== MODE_LEFT && mode !== MODE_RIGHT } | |||
resizerStyle={{"flex": "0 0 auto", "position": "relative"}} | |||
> | |||
{ left } | |||
{ right } | |||
</SplitPane> | |||
) | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
.swagger-ui { | |||
.Resizer.vertical.disabled { | |||
display: none; | |||
} | |||
} |