Merge branch 'master' into feature/apisSorter

This commit is contained in:
shockey
2017-07-13 21:08:47 -07:00
committed by GitHub
13 changed files with 231 additions and 71 deletions

View File

@@ -145,6 +145,8 @@ modelPropertyMacro | MUST be a function. Function to set default values to each
docExpansion | Controls the default expansion setting for the operations and tags. It can be 'list' (expands only the tags), 'full' (expands the tags and operations) or 'none' (expands nothing). The default is 'list'.
displayOperationId | Controls the display of operationId in operations list. The default is `false`.
displayRequestDuration | Controls the display of the request duration (in milliseconds) for `Try it out` requests. The default is `false`.
maxDisplayedTags | If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations.
filter | If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. Can be true/false to enable or disable, or an explicit filter string in which case filtering will be enabled using that string as the filter expression. Filtering is case sensitive matching the filter expression anywhere inside the tag.
### Plugins

View File

@@ -1,11 +1,12 @@
var path = require('path')
var path = require("path")
var webpack = require('webpack')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var deepExtend = require('deep-extend')
const {gitDescribeSync} = require('git-describe');
var webpack = require("webpack")
var ExtractTextPlugin = require("extract-text-webpack-plugin")
var deepExtend = require("deep-extend")
const {gitDescribeSync} = require("git-describe")
const os = require("os")
var pkg = require('./package.json')
var pkg = require("./package.json")
let gitInfo
@@ -13,7 +14,7 @@ try {
gitInfo = gitDescribeSync(__dirname)
} catch(e) {
gitInfo = {
hash: 'noGit',
hash: "noGit",
dirty: false
}
}
@@ -21,21 +22,21 @@ try {
var commonRules = [
{ test: /\.(js(x)?)(\?.*)?$/,
use: [{
loader: 'babel-loader',
loader: "babel-loader",
options: {
retainLines: true
}
}],
include: [ path.join(__dirname, 'src') ]
include: [ path.join(__dirname, "src") ]
},
{ test: /\.(txt|yaml)(\?.*)?$/,
loader: 'raw-loader' },
loader: "raw-loader" },
{ test: /\.(png|jpg|jpeg|gif|svg)(\?.*)?$/,
loader: 'url-loader?limit=10000' },
loader: "url-loader?limit=10000" },
{ test: /\.(woff|woff2)(\?.*)?$/,
loader: 'url-loader?limit=100000' },
loader: "url-loader?limit=100000" },
{ test: /\.(ttf|eot)(\?.*)?$/,
loader: 'file-loader' }
loader: "file-loader" }
]
module.exports = function(rules, options) {
@@ -54,7 +55,7 @@ module.exports = function(rules, options) {
if( specialOptions.separateStylesheets ) {
plugins.push(new ExtractTextPlugin({
filename: '[name].css' + (specialOptions.longTermCaching ? '?[contenthash]' : ''),
filename: "[name].css" + (specialOptions.longTermCaching ? "?[contenthash]" : ""),
allChunks: true
}))
}
@@ -78,15 +79,16 @@ module.exports = function(rules, options) {
plugins.push(
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: specialOptions.minimize ? JSON.stringify('production') : null,
WEBPACK_INLINE_STYLES: !Boolean(specialOptions.separateStylesheets)
"process.env": {
NODE_ENV: specialOptions.minimize ? JSON.stringify("production") : null,
WEBPACK_INLINE_STYLES: !specialOptions.separateStylesheets
},
'buildInfo': JSON.stringify({
"buildInfo": JSON.stringify({
PACKAGE_VERSION: (pkg.version),
GIT_COMMIT: gitInfo.hash,
GIT_DIRTY: gitInfo.dirty
GIT_DIRTY: gitInfo.dirty,
HOSTNAME: os.hostname(),
BUILD_TIME: new Date().toUTCString()
})
}))
@@ -96,17 +98,17 @@ module.exports = function(rules, options) {
entry: {},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js',
chunkFilename: '[name].js'
path: path.join(__dirname, "dist"),
publicPath: "/",
filename: "[name].js",
chunkFilename: "[name].js"
},
target: 'web',
target: "web",
// yaml-js has a reference to `fs`, this is a workaround
node: {
fs: 'empty'
fs: "empty"
},
module: {
@@ -114,17 +116,17 @@ module.exports = function(rules, options) {
},
resolveLoader: {
modules: [path.join(__dirname, 'node_modules')],
modules: [path.join(__dirname, "node_modules")],
},
externals: {
'buffertools': true // json-react-schema/deeper depends on buffertools, which fails.
"buffertools": true // json-react-schema/deeper depends on buffertools, which fails.
},
resolve: {
modules: [
path.join(__dirname, './src'),
'node_modules'
path.join(__dirname, "./src"),
"node_modules"
],
extensions: [".web.js", ".js", ".jsx", ".json", ".less"],
alias: {
@@ -132,7 +134,7 @@ module.exports = function(rules, options) {
}
},
devtool: specialOptions.sourcemaps ? 'cheap-module-source-map' : null,
devtool: specialOptions.sourcemaps ? "cheap-module-source-map" : null,
plugins,

View File

@@ -33,7 +33,21 @@ export default class Operations extends React.Component {
const Collapse = getComponent("Collapse")
let showSummary = layoutSelectors.showSummary()
let { docExpansion, displayOperationId, displayRequestDuration } = getConfigs()
let { docExpansion, displayOperationId, displayRequestDuration, maxDisplayedTags } = getConfigs()
let filter = layoutSelectors.currentFilter()
if (filter) {
if (filter !== true) {
taggedOps = taggedOps.filter((tagObj, tag) => {
return tag.indexOf(filter) !== -1
})
}
}
if (maxDisplayedTags && !isNaN(maxDisplayedTags) && maxDisplayedTags >= 0) {
taggedOps = taggedOps.slice(0, maxDisplayedTags)
}
return (
<div>

View File

@@ -6,17 +6,46 @@ import ApisPreset from "core/presets/apis"
import * as AllPlugins from "core/plugins/all"
import { parseSeach, filterConfigs } from "core/utils"
const CONFIGS = [ "url", "urls", "urls.primaryName", "spec", "validatorUrl", "onComplete", "onFailure", "authorizations", "docExpansion",
"tagsSorter", "operationsSorter", "supportedSubmitMethods", "dom_id", "defaultModelRendering", "oauth2RedirectUrl",
"showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" , "displayRequestDuration"]
const CONFIGS = [
"url",
"urls",
"urls.primaryName",
"spec",
"validatorUrl",
"onComplete",
"onFailure",
"authorizations",
"docExpansion",
"tagsSorter",
"apisSorter",
"maxDisplayedTags",
"filter",
"operationsSorter",
"supportedSubmitMethods",
"dom_id",
"defaultModelRendering",
"oauth2RedirectUrl",
"showRequestHeaders",
"custom",
"modelPropertyMacro",
"parameterMacro",
"displayOperationId",
"displayRequestDuration",
]
// eslint-disable-next-line no-undef
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION, HOSTNAME, BUILD_TIME } = buildInfo
module.exports = function SwaggerUI(opts) {
win.versions = win.versions || {}
win.versions.swaggerUi = `${PACKAGE_VERSION}/${GIT_COMMIT || "unknown"}${GIT_DIRTY ? "-dirty" : ""}`
win.versions.swaggerUi = {
version: PACKAGE_VERSION,
gitRevision: GIT_COMMIT,
gitDirty: GIT_DIRTY,
buildTimestamp: BUILD_TIME,
machine: HOSTNAME
}
const defaults = {
// Some general settings, that we floated to the top
@@ -26,6 +55,8 @@ module.exports = function SwaggerUI(opts) {
urls: null,
layout: "BaseLayout",
docExpansion: "list",
maxDisplayedTags: null,
filter: null,
validatorUrl: "https://online.swagger.io/validator",
configs: {},
custom: {},
@@ -50,7 +81,9 @@ module.exports = function SwaggerUI(opts) {
store: { },
}
const constructorConfig = deepExtend({}, defaults, opts)
let queryConfig = parseSeach()
const constructorConfig = deepExtend({}, defaults, opts, queryConfig)
const storeConfigs = deepExtend({}, constructorConfig.store, {
system: {
@@ -59,7 +92,8 @@ module.exports = function SwaggerUI(opts) {
plugins: constructorConfig.presets,
state: {
layout: {
layout: constructorConfig.layout
layout: constructorConfig.layout,
filter: constructorConfig.filter
},
spec: {
spec: "",
@@ -80,7 +114,6 @@ module.exports = function SwaggerUI(opts) {
store.register([constructorConfig.plugins, inlinePlugin])
var system = store.getSystem()
let queryConfig = parseSeach()
system.initOAuth = system.authActions.configureAuth

View File

@@ -1,6 +1,7 @@
import { normalizeArray } from "core/utils"
export const UPDATE_LAYOUT = "layout_update_layout"
export const UPDATE_FILTER = "layout_update_filter"
export const UPDATE_MODE = "layout_update_mode"
export const SHOW = "layout_show"
@@ -13,6 +14,13 @@ export function updateLayout(layout) {
}
}
export function updateFilter(filter) {
return {
type: UPDATE_FILTER,
payload: filter
}
}
export function show(thing, shown=true) {
thing = normalizeArray(thing)
return {

View File

@@ -1,5 +1,6 @@
import {
UPDATE_LAYOUT,
UPDATE_FILTER,
UPDATE_MODE,
SHOW
} from "./actions"
@@ -8,6 +9,8 @@ export default {
[UPDATE_LAYOUT]: (state, action) => state.set("layout", action.payload),
[UPDATE_FILTER]: (state, action) => state.set("filter", action.payload),
[SHOW]: (state, action) => {
let thing = action.payload.thing
let shown = action.payload.shown

View File

@@ -5,6 +5,8 @@ const state = state => state
export const current = state => state.get("layout")
export const currentFilter = state => state.get("filter")
export const isShown = (state, thing, def) => {
thing = normalizeArray(thing)
return Boolean(state.getIn(["shown", ...thing], def))

View File

@@ -284,12 +284,13 @@ export function parametersIncludeType(parameters, typeValue="") {
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")
)
const requestContentType = (
op.get("consumes_value") ? op.get("consumes_value")
: parametersIncludeType(parameters, "file") ? "multipart/form-data"
: parametersIncludeType(parameters, "formData") ? "application/x-www-form-urlencoded"
: undefined
)
return fromJS({
requestContentType,

View File

@@ -6,6 +6,11 @@ import Logo from "./logo_small.png"
export default class Topbar extends React.Component {
static propTypes = {
layoutSelectors: PropTypes.object.isRequired,
layoutActions: PropTypes.object.isRequired
}
constructor(props, context) {
super(props, context)
this.state = { url: props.specSelectors.url(), selectedIndex: 0 }
@@ -80,13 +85,19 @@ export default class Topbar extends React.Component {
}
}
onFilterChange =(e) => {
let {target: {value}} = e
this.props.layoutActions.updateFilter(value)
}
render() {
let { getComponent, specSelectors, getConfigs } = this.props
let { getComponent, specSelectors, getConfigs, layoutSelectors } = this.props
const Button = getComponent("Button")
const Link = getComponent("Link")
let isLoading = specSelectors.loadingStatus() === "loading"
let isFailed = specSelectors.loadingStatus() === "failed"
let filter = layoutSelectors.currentFilter()
let inputStyle = {}
if(isFailed) inputStyle.color = "red"
@@ -124,6 +135,10 @@ export default class Topbar extends React.Component {
<img height="30" width="30" src={ Logo } alt="Swagger UX"/>
<span>swagger</span>
</Link>
{
filter === null || filter === false ? null :
<input className="operation-filter-input" placeholder="filter..." type="text" onChange={this.onFilterChange} value={filter === true ? "" : filter} disabled={isLoading} style={inputStyle} />
}
<form className="download-url-wrapper" onSubmit={formOnSubmit}>
{control}
</form>

View File

@@ -45,6 +45,7 @@ body
.opblock-tag
{
display: flex;
align-items: center;
padding: 10px 20px 10px 10px;
@@ -53,8 +54,6 @@ body
border-bottom: 1px solid rgba(#3b4151, .3);
align-items: center;
&:hover
{
background: rgba(#000,.02);
@@ -106,9 +105,10 @@ body
font-size: 14px;
font-weight: normal;
flex: 1;
padding: 0 10px;
flex: 1;
@include text_body();
}
}
@@ -134,6 +134,8 @@ body
transition: all .5s;
}
.opblock
{
margin: 0 0 15px 0;
@@ -154,24 +156,23 @@ body
.opblock-section-header
{
display: flex;
align-items: center;
padding: 8px 20px;
background: rgba(#fff,.8);
box-shadow: 0 1px 2px rgba(#000,.1);
align-items: center;
label
{
font-size: 12px;
font-weight: bold;
display: flex;
align-items: center;
margin: 0;
align-items: center;
@include text_headline();
span
@@ -184,9 +185,10 @@ body
{
font-size: 14px;
flex: 1;
margin: 0;
flex: 1;
@include text_headline();
}
}
@@ -215,11 +217,11 @@ body
font-size: 16px;
display: flex;
align-items: center;
padding: 0 10px;
@include text_code();
align-items: center;
.view-line-link
{
@@ -258,18 +260,18 @@ body
font-size: 13px;
flex: 1;
@include text_body();
}
.opblock-summary
{
display: flex;
align-items: center;
padding: 5px;
cursor: pointer;
align-items: center;
}
&.opblock-post
@@ -498,13 +500,11 @@ body
margin: 0;
padding: 10px;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
word-break: break-word;
hyphens: auto;
white-space: pre-wrap;
border-radius: 4px;
background: #41444e;
@@ -533,7 +533,6 @@ body
.schemes
{
display: flex;
align-items: center;
> label
@@ -624,3 +623,12 @@ body
opacity: 0;
}
}
section
{
h3
{
@include text_headline();
}
}

View File

@@ -29,6 +29,12 @@
padding: 0 10px;
}
}
.operation-filter-input
{
border: 2px solid #547f00;
border-right: none;
border-radius: 4px 0 0 4px;
}
.download-url-wrapper
{
@@ -43,7 +49,7 @@
margin: 0;
border: 2px solid #547f00;
border-radius: 4px 0 0 4px;
border-radius: 0 0 0 0;
outline: none;
}

View File

@@ -52,7 +52,6 @@ describe("spec plugin - selectors", function(){
})
describe("contentTypeValues", function(){
it("should return { requestContentType, responseContentType } from an operation", function(){
// Given
let state = fromJS({
@@ -77,6 +76,73 @@ describe("spec plugin - selectors", function(){
})
})
it("should prioritize consumes value first from an operation", function(){
// Given
let state = fromJS({
resolved: {
paths: {
"/one": {
get: {
"consumes_value": "one",
"parameters": [{
"type": "file"
}],
}
}
}
}
})
// When
let contentTypes = contentTypeValues(state, [ "/one", "get" ])
// Then
expect(contentTypes.toJS().requestContentType).toEqual("one")
})
it("should fallback to multipart/form-data if there is no consumes value but there is a file parameter", function(){
// Given
let state = fromJS({
resolved: {
paths: {
"/one": {
get: {
"parameters": [{
"type": "file"
}],
}
}
}
}
})
// When
let contentTypes = contentTypeValues(state, [ "/one", "get" ])
// Then
expect(contentTypes.toJS().requestContentType).toEqual("multipart/form-data")
})
it("should fallback to application/x-www-form-urlencoded if there is no consumes value, no file parameter, but there is a formData parameter", function(){
// Given
let state = fromJS({
resolved: {
paths: {
"/one": {
get: {
"parameters": [{
"type": "formData"
}],
}
}
}
}
})
// When
let contentTypes = contentTypeValues(state, [ "/one", "get" ])
// Then
expect(contentTypes.toJS().requestContentType).toEqual("application/x-www-form-urlencoded")
})
it("should be ok, if no operation found", function(){
// Given
let state = fromJS({ })