Compare commits

...
9 Commits
Author SHA1 Message Date
kyle e41067b4c1 v3.11.0 (#4254)
* v3.11.0

* Rebuild dist
2018-02-23 23:44:37 -08:00
Helder Sepulveda 558d054986 fix: package.json to reduce vulnerabilities (#4224)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/npm:lodash:20180130

Latest report for heldersepu/swagger-ui:
https://snyk.io/test/github/heldersepu/swagger-ui
2018-02-23 22:34:29 -08:00
Helder Sepulveda d1779dc47b fix: allow any guid (#4252)
* Allow any guid

* Add a few more GUID tests
2018-02-23 22:31:08 -08:00
kyle ba1603a7e2 improve: lazy resolver (#4253)
* default to empty `ImmutableMap` when grabbing op metadata
* pass `errors` into JsonSchema components
* Account for Immutable data structure in JavaScriptonSchema...
...and create empty Lists instead of Maps by default.
* Pass ImmutableList through to JsonSchema child components
* Add lazy resolving spec state extensions
* TEMPORARY: disable conventional resolved spec
* WIP
* Use resolveSubtree in Operation display
* Freebie: short-circuit Markdown component if it is given plaintext
* NEW DEFAULT BEHAVIOR: `defaultModelsExpandDepth: 1` does not expand individual models
* Render faked Model expander to trigger resolution
* Baseline support for Editor lifecycles
* Display operation summaries before the operation is resolved
* Test migrations
* WIP
* Swagger2 TIO Body params
* a bit of cleanup
* Debounce string param inputs
* Reach into unresolved operation for deprecated flag, if available
* Fire subtree request outside of render
* Remove debugging flags
* Fix logical errors in spec statePlugins
* TODOs become TODONEs!
* Migrate deeplinking feature to non-resolved spec action
* ESLint fixes
* Reduce action dispatch volume; run resolver on next tick
* Batch resolver requests; use batch progressive results in later iterations
* Add loading states to Model and Operation views
* Preserve object order; remove dupl. import; add warning for action
* LINTER!
* Use require to load SVG
       Works around Webpack weirdness
2018-02-23 22:25:28 -08:00
kyle ecf688171f feat: lazy resolver (#4249)
* default to empty `ImmutableMap` when grabbing op metadata
* pass `errors` into JsonSchema components
* Account for Immutable data structure in JavaScriptonSchema...
    ...and create empty Lists instead of Maps by default.
* Pass ImmutableList through to JsonSchema child components
* Add lazy resolving spec state extensions
* TEMPORARY: disable conventional resolved spec
* WIP
* Use resolveSubtree in Operation display
* Freebie: short-circuit Markdown component if it is given plaintext
* NEW DEFAULT BEHAVIOR: `defaultModelsExpandDepth: 1` does not expand individual models
* Render faked Model expander to trigger resolution
* Baseline support for Editor lifecycles
* Display operation summaries before the operation is resolved
* Test migrations
* WIP
* Swagger2 TIO Body params
* a bit of cleanup
* Debounce string param inputs
* Reach into unresolved operation for deprecated flag, if available
* Fire subtree request outside of render
* Remove debugging flags
* Fix logical errors in spec statePlugins
* TODOs become TODONEs!
* Migrate deeplinking feature to non-resolved spec action
* ESLint fixes
2018-02-23 01:12:53 -08:00
Ron 54ed39d69f Merge pull request #4219 from SwaroopH/patch-1
Fix Summary links
2018-02-19 21:32:40 -07:00
Ron 48a65983c1 Merge branch 'master' into patch-1 2018-02-19 21:28:52 -07:00
wayglem 1f565b4847 show oauth error callback message when auth fails (#4058)
This change aims to display the oauth error to the user when the autnetication process fails.

Close #4048
2018-02-13 15:42:37 -08:00
Swaroop Hegde ba59db2449 Fix Summary links 2018-02-13 18:08:11 +05:30
40 changed files with 497 additions and 155 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ The OpenAPI Specification has undergone 5 revisions since initial creation in 20
Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes
------------------ | ------------ | -------------------------- | -----
3.10.0 | 2018-02-09 | 2.0, 3.0 | [tag v3.10.0](https://github.com/swagger-api/swagger-ui/tree/v3.10.0)
3.11.0 | 2018-02-09 | 2.0, 3.0 | [tag v3.11.0](https://github.com/swagger-api/swagger-ui/tree/v3.11.0)
3.0.21 | 2017-07-26 | 2.0 | [tag v3.0.21](https://github.com/swagger-api/swagger-ui/tree/v3.0.21)
2.2.10 | 2017-01-04 | 1.1, 1.2, 2.0 | [tag v2.2.10](https://github.com/swagger-api/swagger-ui/tree/v2.2.10)
2.1.5 | 2016-07-20 | 1.1, 1.2, 2.0 | [tag v2.1.5](https://github.com/swagger-api/swagger-ui/tree/v2.1.5)
+8 -1
View File
@@ -45,11 +45,18 @@
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: "Authorization failed: no accessCode received from the server"
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
+8 -1
View File
@@ -45,11 +45,18 @@
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: "Authorization failed: no accessCode received from the server"
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
+15 -15
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+12 -12
View File
@@ -1,17 +1,17 @@
#### Usage
- [Installation](docs/usage/installation.md)
- [Configuration](docs/usage/configuration.md)
- [CORS](docs/usage/cors.md)
- [OAuth2](docs/usage/oauth2.md)
- [Deep Linking](docs/usage/deep-linking.md)
- [Limitations](docs/usage/limitations.md)
- [Version detection](docs/usage/version-detection.md)
- [Installation](usage/installation.md)
- [Configuration](usage/configuration.md)
- [CORS](usage/cors.md)
- [OAuth2](usage/oauth2.md)
- [Deep Linking](usage/deep-linking.md)
- [Limitations](usage/limitations.md)
- [Version detection](usage/version-detection.md)
#### Customization
- [Overview](docs/customization/overview.md)
- [Plugin API](docs/customization/plugin-api.md)
- [Custom layout](docs/customization/custom-layout.md)
- [Overview](customization/overview.md)
- [Plugin API](customization/plugin-api.md)
- [Custom layout](customization/custom-layout.md)
#### Development
- [Setting up](docs/development/setting-up.md)
- [Scripts](docs/development/scripts.md)
- [Setting up](development/setting-up.md)
- [Scripts](development/scripts.md)
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "swagger-ui",
"version": "3.10.0",
"version": "3.11.0",
"main": "dist/swagger-ui.js",
"repository": "git@github.com:swagger-api/swagger-ui.git",
"contributors": [
@@ -53,7 +53,7 @@
"ieee754": "^1.1.8",
"immutable": "^3.x.x",
"js-yaml": "^3.5.5",
"lodash": "4.17.2",
"lodash": "4.17.5",
"matcher": "^0.1.2",
"memoizee": "0.4.1",
"promise-worker": "^1.1.1",
@@ -63,6 +63,7 @@
"react-addons-shallow-compare": "0.14.8",
"react-addons-test-utils": "^15.6.2",
"react-collapse": "^4.0.3",
"react-debounce-input": "^3.2.0",
"react-dom": "^15.6.2",
"react-height": "^2.0.0",
"react-hot-loader": "1.3.1",
@@ -82,7 +83,7 @@
"scroll-to-element": "^2.0.0",
"serialize-error": "2.0.0",
"shallowequal": "0.2.2",
"swagger-client": "^3.4.11",
"swagger-client": "^3.5.0",
"url-parse": "^1.1.8",
"whatwg-fetch": "0.11.1",
"worker-loader": "^0.7.1",
+27 -9
View File
@@ -8,14 +8,17 @@ export default class ModelCollapse extends Component {
children: PropTypes.any,
title: PropTypes.element,
modelName: PropTypes.string,
onToggle: PropTypes.func
classes: PropTypes.string,
onToggle: PropTypes.func,
hideSelfOnExpand: PropTypes.bool,
}
static defaultProps = {
collapsedContent: "{...}",
expanded: false,
title: null,
onToggle: () => {}
onToggle: () => {},
hideSelfOnExpand: false
}
constructor(props, context) {
@@ -29,17 +32,23 @@ export default class ModelCollapse extends Component {
}
}
componentWillReceiveProps(nextProps){
componentDidMount() {
const { hideSelfOnExpand, expanded, modelName } = this.props
if(hideSelfOnExpand && expanded) {
// We just mounted pre-expanded, and we won't be going back..
// So let's give our parent an `onToggle` call..
// Since otherwise it will never be called.
this.props.onToggle(modelName, expanded)
}
}
if(this.props.expanded!= nextProps.expanded){
componentWillReceiveProps(nextProps){
if(this.props.expanded !== nextProps.expanded){
this.setState({expanded: nextProps.expanded})
}
}
toggleCollapsed=()=>{
if(this.props.onToggle){
this.props.onToggle(this.props.modelName,!this.state.expanded)
}
@@ -50,9 +59,18 @@ export default class ModelCollapse extends Component {
}
render () {
const {title} = this.props
const { title, classes } = this.props
if(this.state.expanded ) {
if(this.props.hideSelfOnExpand) {
return <span className={classes || ""}>
{this.props.children}
</span>
}
}
return (
<span>
<span className={classes || ""}>
{ title && <span onClick={this.toggleCollapsed} style={{ "cursor": "pointer" }}>{title}</span> }
<span onClick={ this.toggleCollapsed } style={{ "cursor": "pointer" }}>
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span>
+11
View File
@@ -49,6 +49,17 @@ export default class Model extends ImmutablePureComponent {
schema = this.getRefSchema( name )
}
if(!schema) {
return <span className="model model-title">
<span className="model-title__text">{ name }</span>
<img src={require("core/../img/rolling-load.svg")} height={"20px"} width={"20px"} style={{
marginLeft: "1em",
position: "relative",
bottom: "0px"
}} />
</span>
}
const deprecated = specSelectors.isOAS3() && schema.get("deprecated")
isRef = isRef !== undefined ? isRef : !!$$ref
type = schema && schema.get("type") || type
+52 -11
View File
@@ -6,11 +6,29 @@ export default class Models extends Component {
static propTypes = {
getComponent: PropTypes.func,
specSelectors: PropTypes.object,
specActions: PropTypes.object.isRequired,
layoutSelectors: PropTypes.object,
layoutActions: PropTypes.object,
getConfigs: PropTypes.func.isRequired
}
getSchemaBasePath = () => {
const isOAS3 = this.props.specSelectors.isOAS3()
return isOAS3 ? ["components", "schemas"] : ["definitions"]
}
getCollapsedContent = () => {
return " "
}
handleToggle = (name, isExpanded) => {
const { layoutActions } = this.props
layoutActions.show(["models", name], isExpanded)
if(isExpanded) {
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name])
}
}
render(){
let { specSelectors, getComponent, layoutSelectors, layoutActions, getConfigs } = this.props
let definitions = specSelectors.definitions()
@@ -18,10 +36,11 @@ export default class Models extends Component {
if (!definitions.size || defaultModelsExpandDepth < 0) return null
let showModels = layoutSelectors.isShown("models", defaultModelsExpandDepth > 0 && docExpansion !== "none")
const specPathBase = specSelectors.isOAS3() ? ["components", "schemas"] : ["definitions"]
const specPathBase = this.getSchemaBasePath()
const ModelWrapper = getComponent("ModelWrapper")
const Collapse = getComponent("Collapse")
const ModelCollapse = getComponent("ModelCollapse")
return <section className={ showModels ? "models is-open" : "models"}>
<h4 onClick={() => layoutActions.show("models", !showModels)}>
@@ -32,18 +51,40 @@ export default class Models extends Component {
</h4>
<Collapse isOpened={showModels}>
{
definitions.entrySeq().map( ( [ name, model ])=>{
definitions.entrySeq().map( ( [ name ])=>{
const schema = specSelectors.specResolvedSubtree([...specPathBase, name])
if(layoutSelectors.isShown(["models", name], false) && schema === undefined) {
// Firing an action in a container render is not great,
// but it works for now.
this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), name])
}
const content = <ModelWrapper name={ name }
expandDepth={ defaultModelsExpandDepth }
schema={ schema }
specPath={Im.List([...specPathBase, name])}
getComponent={ getComponent }
specSelectors={ specSelectors }
getConfigs = {getConfigs}
layoutSelectors = {layoutSelectors}
layoutActions = {layoutActions}/>
const title = <span className="model-box">
<span className="model model-title">{name}</span>
</span>
return <div id={ `model-${name}` } className="model-container" key={ `models-section-${name}` }>
<ModelWrapper name={ name }
expandDepth={ defaultModelsExpandDepth }
schema={ model }
specPath={Im.List([...specPathBase, name])}
getComponent={ getComponent }
specSelectors={ specSelectors }
getConfigs = {getConfigs}
layoutSelectors = {layoutSelectors}
layoutActions = {layoutActions}/>
<ModelCollapse
classes="model-box"
collapsedContent={this.getCollapsedContent(name)}
onToggle={this.handleToggle}
title={title}
modelName={name}
hideSelfOnExpand={true}
expanded={defaultModelsExpandDepth > 1}
>{content}</ModelCollapse>
</div>
}).toArray()
}
+31 -22
View File
@@ -5,10 +5,12 @@ import { getExtensions, sanitizeUrl } from "core/utils"
import { Iterable, List } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
export default class Operation extends PureComponent {
static propTypes = {
specPath: ImPropTypes.list.isRequired,
operation: PropTypes.instanceOf(Iterable).isRequired,
summary: PropTypes.string,
response: PropTypes.instanceOf(Iterable),
request: PropTypes.instanceOf(Iterable),
@@ -34,7 +36,8 @@ export default class Operation extends PureComponent {
operation: null,
response: null,
request: null,
specPath: List()
specPath: List(),
summary: ""
}
render() {
@@ -59,6 +62,8 @@ export default class Operation extends PureComponent {
let operationProps = this.props.operation
let {
summary,
deprecated,
isShown,
isAuthorized,
path,
@@ -76,14 +81,13 @@ export default class Operation extends PureComponent {
} = operationProps.toJS()
let {
summary,
summary: resolvedSummary,
description,
deprecated,
externalDocs,
schemes
} = op.operation
} = op
let operation = operationProps.getIn(["op", "operation"])
let operation = operationProps.getIn(["op"])
let security = operationProps.get("security")
let responses = operation.get("responses")
let produces = operation.get("produces")
@@ -132,7 +136,7 @@ export default class Operation extends PureComponent {
{ !showSummary ? null :
<div className="opblock-summary-description">
{ summary }
{ resolvedSummary || summary }
</div>
}
@@ -152,6 +156,9 @@ export default class Operation extends PureComponent {
<Collapse isOpened={isShown}>
<div className="opblock-body">
{ operation && operation.size ? null :
<img height={"32px"} width={"32px"} src={require("core/../img/rolling-load.svg")} className="opblock-loading-animation" />
}
{ deprecated && <h4 className="opblock-title_normal"> Warning: Deprecated</h4>}
{ description &&
<div className="opblock-description-wrapper">
@@ -173,23 +180,25 @@ export default class Operation extends PureComponent {
</div> : null
}
<Parameters
parameters={parameters}
specPath={specPath.push("parameters")}
operation={operation}
onChangeKey={onChangeKey}
onTryoutClick = { onTryoutClick }
onCancelClick = { onCancelClick }
tryItOutEnabled = { tryItOutEnabled }
allowTryItOut={allowTryItOut}
{ !operation || !operation.size ? null :
<Parameters
parameters={parameters}
specPath={specPath.push("parameters")}
operation={operation}
onChangeKey={onChangeKey}
onTryoutClick = { onTryoutClick }
onCancelClick = { onCancelClick }
tryItOutEnabled = { tryItOutEnabled }
allowTryItOut={allowTryItOut}
fn={fn}
getComponent={ getComponent }
specActions={ specActions }
specSelectors={ specSelectors }
pathMethod={ [path, method] }
getConfigs={ getConfigs }
/>
fn={fn}
getComponent={ getComponent }
specActions={ specActions }
specSelectors={ specSelectors }
pathMethod={ [path, method] }
getConfigs={ getConfigs }
/>
}
{ !tryItOutEnabled ? null :
<OperationServers
+2 -2
View File
@@ -47,7 +47,7 @@ export default class ParamBody extends PureComponent {
updateValues = (props) => {
let { specSelectors, pathMethod, param, isExecute, consumesValue="" } = props
let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) : fromJS({})
let parameter = specSelectors ? specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) : fromJS({})
let isXml = /xml/i.test(consumesValue)
let isJson = /json/i.test(consumesValue)
let paramValue = isXml ? parameter.get("value_xml") : parameter.get("value")
@@ -107,7 +107,7 @@ export default class ParamBody extends PureComponent {
const HighlightCode = getComponent("highlightCode")
const ContentType = getComponent("contentType")
// for domains where specSelectors not passed
let parameter = specSelectors ? specSelectors.getParameter(pathMethod, param.get("name"), param.get("in")) : param
let parameter = specSelectors ? specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in")) : 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
+3 -4
View File
@@ -24,7 +24,7 @@ export default class ParameterRow extends Component {
let { specSelectors, pathMethod, param } = props
let defaultValue = param.get("default")
let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in"))
let parameter = specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in"))
let value = parameter ? parameter.get("value") : ""
if ( defaultValue !== undefined && value === undefined ) {
this.onChangeWrapper(defaultValue)
@@ -37,7 +37,7 @@ export default class ParameterRow extends Component {
let example = param.get("example")
let defaultValue = param.get("default")
let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in"))
let parameter = specSelectors.parameterWithMeta(pathMethod, param.get("name"), param.get("in"))
let enumValue
if(isOAS3()) {
@@ -104,8 +104,7 @@ export default class ParameterRow extends Component {
let isFormDataSupported = "FormData" in win
let required = param.get("required")
let itemType = param.getIn(isOAS3 && isOAS3() ? ["schema", "items", "type"] : ["items", "type"])
let parameter = specSelectors.getParameter(pathMethod, param.get("name"), param.get("in"))
let value = parameter ? parameter.get("value") : ""
let value = param ? param.get("value") : ""
let extensions = getExtensions(param)
+1 -1
View File
@@ -101,7 +101,7 @@ export default class Parameters extends Component {
specPath={specPath.push(i.toString())}
getComponent={ getComponent }
getConfigs={ getConfigs }
param={ parameter }
param={ specSelectors.parameterWithMeta(pathMethod, parameter.get("name"), parameter.get("in")) }
key={ `${parameter.get( "in" )}.${parameter.get("name")}` }
onChange={ this.onChange }
onChangeConsumes={this.onChangeConsumesWrapper}
@@ -3,7 +3,17 @@ import PropTypes from "prop-types"
import Remarkable from "remarkable"
import sanitize from "sanitize-html"
// eslint-disable-next-line no-useless-escape
const isPlainText = (str) => /^[A-Z\s0-9!?\.]+$/gi.test(str)
function Markdown({ source }) {
if(isPlainText(source)) {
// If the source text is not Markdown,
// let's save some time and just render it.
return <div className="markdown">
{source}
</div>
}
const html = new Remarkable({
html: true,
typographer: true,
+20 -5
View File
@@ -2,7 +2,7 @@ import React, { PureComponent } from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import { helpers } from "swagger-client"
import { Iterable, fromJS } from "immutable"
import { Iterable, fromJS, Map } from "immutable"
const { opId } = helpers
@@ -82,13 +82,24 @@ export default class OperationContainer extends PureComponent {
}
componentWillReceiveProps(nextProps) {
if(nextProps.response !== this.props.response) {
const { path, method, specActions, specSelectors, response, isShown } = nextProps
const resolvedSubtree = specSelectors.specResolvedSubtree(["paths", path, method])
if(response !== this.props.response) {
this.setState({ executeInProgress: false })
}
if(isShown && resolvedSubtree === undefined) {
specActions.requestResolvedSubtree(["paths", path, method])
}
}
toggleShown =() => {
let { layoutActions, tag, operationId, isShown } = this.props
let { layoutActions, specActions, tag, operationId, path, method, isShown } = this.props
if(!isShown) {
// transitioning from collapsed to expanded
specActions.requestResolvedSubtree(["paths", path, method])
}
layoutActions.show(["operations", tag, operationId], !isShown)
}
@@ -108,7 +119,7 @@ export default class OperationContainer extends PureComponent {
render() {
let {
op,
op: unresolvedOp,
tag,
path,
method,
@@ -140,10 +151,14 @@ export default class OperationContainer extends PureComponent {
const Operation = getComponent( "operation" )
const resolvedSubtree = specSelectors.specResolvedSubtree(["paths", path, method]) || Map()
const operationProps = fromJS({
op,
op: resolvedSubtree || Map(),
tag,
path,
summary: unresolvedOp.getIn(["operation", "summary"]) || "",
deprecated: resolvedSubtree.get("deprecated") || unresolvedOp.getIn(["operation", "deprecated"]) || false,
method,
security,
isAuthorized,
+5 -1
View File
@@ -2,6 +2,7 @@ import React, { PureComponent, Component } from "react"
import PropTypes from "prop-types"
import { List, fromJS } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
import DebounceInput from "react-debounce-input"
//import "less/json-schema-form"
const noop = ()=> {}
@@ -79,10 +80,13 @@ export class JsonSchema_string extends Component {
disabled={isDisabled}/>)
}
else {
return (<Input type={ schema.format === "password" ? "password" : "text" }
return (<DebounceInput
type={ schema.format === "password" ? "password" : "text" }
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
value={value}
minLength={0}
debounceTimeout={350}
placeholder={description}
onChange={ this.onChange }
disabled={isDisabled}/>)
@@ -4,7 +4,7 @@ import { escapeDeepLinkPath } from "core/utils"
let hasHashBeenParsed = false //TODO this forces code to only run once which may prevent scrolling if page not refreshed
export const updateResolved = (ori, { layoutActions, getConfigs }) => (...args) => {
export const updateJsonSpec = (ori, { layoutActions, getConfigs }) => (...args) => {
ori(...args)
const isDeepLinkingEnabled = getConfigs().deepLinking
+3 -1
View File
@@ -30,7 +30,9 @@ export default function downloadUrlPlugin (toolbox) {
}
specActions.updateLoadingStatus("success")
specActions.updateSpec(res.text)
specActions.updateUrl(url)
if(specSelectors.url() !== url) {
specActions.updateUrl(url)
}
}
},
+9
View File
@@ -6,6 +6,7 @@ export const NEW_SPEC_ERR = "err_new_spec_err"
export const NEW_SPEC_ERR_BATCH = "err_new_spec_err_batch"
export const NEW_AUTH_ERR = "err_new_auth_err"
export const CLEAR = "err_clear"
export const CLEAR_BY = "err_clear_by"
export function newThrownErr(err) {
return {
@@ -49,3 +50,11 @@ export function clear(filter = {}) {
payload: filter
}
}
export function clearBy(filter = () => true) {
// filter is a function
return {
type: CLEAR_BY,
payload: filter
}
}
+30 -8
View File
@@ -4,12 +4,11 @@ import {
NEW_SPEC_ERR,
NEW_SPEC_ERR_BATCH,
NEW_AUTH_ERR,
CLEAR
CLEAR,
CLEAR_BY,
} from "./actions"
import reject from "lodash/reject"
import Im, { fromJS, List } from "immutable"
import { fromJS, List } from "immutable"
import transformErrors from "./error-transformers/hook"
@@ -65,11 +64,34 @@ export default function(system) {
},
[CLEAR]: (state, { payload }) => {
if(!payload) {
return
if(!payload || !state.get("errors")) {
return state
}
// TODO: Rework, to use immutable only, no need for lodash
let newErrors = Im.fromJS(reject((state.get("errors") || List()).toJS(), payload))
let newErrors = state.get("errors")
.filter(err => {
return err.keySeq().every(k => {
const errValue = err.get(k)
const filterValue = payload[k]
if(!filterValue) return true
return errValue !== filterValue
})
})
return state.merge({
errors: newErrors
})
},
[CLEAR_BY]: (state, { payload }) => {
if(!payload || typeof payload !== "function") {
return state
}
let newErrors = state.get("errors")
.filter(err => {
return payload(err)
})
return state.merge({
errors: newErrors
})
@@ -20,7 +20,7 @@ const state = state => {
return state || Map()
}
const nullSelector = createSelector(() => null)
const nullSelector = createSelector(() => null)
const OAS3NullSelector = onlyOAS3(nullSelector)
+120 -2
View File
@@ -1,7 +1,10 @@
import YAML from "js-yaml"
import { Map } from "immutable"
import parseUrl from "url-parse"
import serializeError from "serialize-error"
import isString from "lodash/isString"
import debounce from "lodash/debounce"
import set from "lodash/set"
import { isJSONObject } from "core/utils"
// Actions conform to FSA (flux-standard-actions)
@@ -21,6 +24,7 @@ export const CLEAR_REQUEST = "spec_clear_request"
export const CLEAR_VALIDATE_PARAMS = "spec_clear_validate_param"
export const UPDATE_OPERATION_META_VALUE = "spec_update_operation_meta_value"
export const UPDATE_RESOLVED = "spec_update_resolved"
export const UPDATE_RESOLVED_SUBTREE = "spec_update_resolved_subtree"
export const SET_SCHEME = "set_scheme"
const toStr = (str) => isString(str) ? str : ""
@@ -74,7 +78,14 @@ export const parseToJson = (str) => ({specActions, specSelectors, errActions}) =
return {}
}
let hasWarnedAboutResolveSpecDeprecation = false
export const resolveSpec = (json, url) => ({specActions, specSelectors, errActions, fn: { fetch, resolve, AST }, getConfigs}) => {
if(!hasWarnedAboutResolveSpecDeprecation) {
console.warn(`specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use resolveIn instead!`)
hasWarnedAboutResolveSpecDeprecation = true
}
const {
modelPropertyMacro,
parameterMacro,
@@ -124,6 +135,86 @@ export const resolveSpec = (json, url) => ({specActions, specSelectors, errActio
})
}
let requestBatch = []
const debResolveSubtrees = debounce(async () => {
const system = requestBatch.system // Just a reference to the "latest" system
if(!system) {
console.error("debResolveSubtrees: don't have a system to operate on, aborting.")
return
}
const {
errActions,
errSelectors,
fn: {
resolveSubtree,
AST: { getLineNumberForPath }
},
specSelectors,
specActions,
} = system
if(!resolveSubtree) {
console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.")
return
}
const specStr = specSelectors.specStr()
try {
var batchResult = await requestBatch.reduce(async (prev, path) => {
const { resultMap, specWithCurrentSubtrees } = await prev
const { errors, spec } = await resolveSubtree(specWithCurrentSubtrees, path)
if(errSelectors.allErrors().size) {
errActions.clear({
type: "thrown"
})
}
if(Array.isArray(errors) && errors.length > 0) {
let preparedErrors = errors
.map(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)
}
set(resultMap, path, spec)
set(specWithCurrentSubtrees, path, spec)
return {
resultMap,
specWithCurrentSubtrees
}
}, Promise.resolve({
resultMap: (specSelectors.specResolvedSubtree([]) || Map()).toJS(),
specWithCurrentSubtrees: specSelectors.specJson().toJS()
}))
delete requestBatch.system
requestBatch = [] // Clear stack
} catch(e) {
console.error(e)
}
specActions.updateResolvedSubtree([], batchResult.resultMap)
}, 35)
export const requestResolvedSubtree = path => system => {
requestBatch.push(path)
requestBatch.system = system
debResolveSubtrees()
}
export function changeParam( path, paramName, paramIn, value, isXml ){
return {
type: UPDATE_PARAM,
@@ -131,6 +222,23 @@ export function changeParam( path, paramName, paramIn, value, isXml ){
}
}
export const updateResolvedSubtree = (path, value) => {
return {
type: UPDATE_RESOLVED_SUBTREE,
payload: { path, value }
}
}
export const invalidateResolvedSubtreeCache = () => {
return {
type: UPDATE_RESOLVED_SUBTREE,
payload: {
path: [],
value: Map()
}
}
}
export const validateParams = ( payload, isOAS3 ) =>{
return {
type: VALIDATE_PARAMS,
@@ -251,6 +359,7 @@ export const executeRequest = (req) =>
// track duration of request
const startTime = Date.now()
return fn.execute(req)
.then( res => {
res.duration = Date.now() - startTime
@@ -267,13 +376,22 @@ export const executeRequest = (req) =>
// 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 spec = specSelectors.specJsonWithResolvedSubtrees().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 })
return specActions.executeRequest({
...extras,
fetch,
spec,
pathName: path,
method, parameters,
requestContentType,
scheme,
responseContentType
})
}
export function clearResponse (path, method) {
+31 -25
View File
@@ -1,7 +1,12 @@
import { fromJS } from "immutable"
import { fromJS, List } from "immutable"
import { fromJSOrdered, validateParam } from "core/utils"
import win from "../../window"
// selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper
import {
operationWithMeta
} from "./selectors"
import {
UPDATE_SPEC,
UPDATE_URL,
@@ -12,6 +17,7 @@ import {
SET_REQUEST,
SET_MUTATED_REQUEST,
UPDATE_RESOLVED,
UPDATE_RESOLVED_SUBTREE,
UPDATE_OPERATION_META_VALUE,
CLEAR_RESPONSE,
CLEAR_REQUEST,
@@ -39,38 +45,38 @@ export default {
return state.setIn(["resolved"], fromJSOrdered(action.payload))
},
[UPDATE_PARAM]: ( state, {payload} ) => {
let { path, paramName, paramIn, value, isXml } = payload
[UPDATE_RESOLVED_SUBTREE]: (state, action) => {
const { value, path } = action.payload
return state.setIn(["resolvedSubtrees", ...path], fromJSOrdered(value))
},
return state.updateIn( [ "resolved", "paths", ...path, "parameters" ], fromJS([]), parameters => {
const index = parameters.findIndex(p => p.get( "name" ) === paramName && p.get("in") === paramIn )
if (!(value instanceof win.File)) {
value = fromJSOrdered( value )
}
return parameters.setIn( [ index, isXml ? "value_xml" : "value" ], value)
})
[UPDATE_PARAM]: ( state, {payload} ) => {
let { path: pathMethod, paramName, paramIn, value, isXml } = payload
const valueKey = isXml ? "value_xml" : "value"
return state.setIn(
["meta", "paths", ...pathMethod, "parameters", `${paramName}.${paramIn}`, valueKey],
value
)
},
[VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => {
let meta = state.getIn( [ "meta", "paths", ...pathMethod ], fromJS({}) )
let isXml = /xml/i.test(meta.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, isOAS3)
parameters.setIn([i, "errors"], fromJS(errors))
}
})
const op = operationWithMeta(state, ...pathMethod)
return state.updateIn(["meta", "paths", ...pathMethod, "parameters"], fromJS({}), paramMeta => {
return op.get("parameters", List()).reduce((res, param) => {
const errors = validateParam(param, isXml, isOAS3)
return res.setIn([`${param.get("name")}.${param.get("in")}`, "errors"], fromJS(errors))
}, paramMeta)
})
},
[CLEAR_VALIDATE_PARAMS]: ( state, { payload: { pathMethod } } ) => {
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([]))
}
})
return state.updateIn( [ "meta", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => {
return parameters.map(param => param.set("errors", fromJS([])))
})
},
@@ -109,10 +115,10 @@ export default {
[UPDATE_OPERATION_META_VALUE]: (state, { payload: { path, value, key } }) => {
// path is a pathMethod tuple... can't change the name now.
let operationPath = ["resolved", "paths", ...path]
let operationPath = ["paths", ...path]
let metaPath = ["meta", "paths", ...path]
if(!state.getIn(operationPath)) {
if(!state.getIn(["json", ...operationPath]) && !state.getIn(["resolved", ...operationPath])) {
// do nothing if the operation does not exist
return state
}
+52 -10
View File
@@ -42,9 +42,18 @@ export const specResolved = createSelector(
spec => spec.get("resolved", Map())
)
export const specResolvedSubtree = (state, path) => {
return state.getIn(["resolvedSubtrees", ...path], undefined)
}
export const specJsonWithResolvedSubtrees = createSelector(
state,
spec => Map().merge(spec.get("json"), spec.get("resolvedSubtrees"))
)
// Default Spec ( as an object )
export const spec = state => {
let res = specResolved(state)
let res = specJson(state)
return res
}
@@ -137,7 +146,9 @@ export const securityDefinitions = createSelector(
export const findDefinition = ( state, name ) => {
return specResolved(state).getIn(["definitions", name], null)
const resolvedRes = state.getIn(["resolvedSubtrees", "definitions", name], null)
const unresolvedRes = state.getIn(["json", "definitions", name], null)
return resolvedRes || unresolvedRes || null
}
export const definitions = createSelector(
@@ -261,10 +272,40 @@ export const allowTryItOutFor = () => {
return true
}
export const operationWithMeta = (state, path, method) => {
const op = specJsonWithResolvedSubtrees(state).getIn(["paths", path, method], Map())
const meta = state.getIn(["meta", "paths", path, method], Map())
const mergedParams = op.get("parameters", List()).map((param) => {
return Map().merge(
param,
meta.getIn(["parameters", `${param.get("name")}.${param.get("in")}`])
)
})
return Map()
.merge(op, meta)
.set("parameters", mergedParams)
}
export const parameterWithMeta = (state, pathMethod, paramName, paramIn) => {
const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], Map())
const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], Map())
const mergedParams = opParams.map((param) => {
return Map().merge(
param,
metaParams.get(`${param.get("name")}.${param.get("in")}`)
)
})
return mergedParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, Map())
}
// Get the parameter value by parameter name
export function getParameter(state, pathMethod, name, inType) {
pathMethod = pathMethod || []
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([]))
let params = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
return params.find( (p) => {
return Map.isMap(p) && p.get("name") === name && p.get("in") === inType
}) || Map() // Always return a map
@@ -281,8 +322,9 @@ export const hasHost = createSelector(
// Get the parameter values, that the user filled out
export function parameterValues(state, pathMethod, isXml) {
pathMethod = pathMethod || []
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([]))
return params.reduce( (hash, p) => {
// let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List())
return paramValues.reduce( (hash, p) => {
let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value")
return hash.set(`${p.get("in")}.${p.get("name")}`, value)
}, fromJS({}))
@@ -305,7 +347,7 @@ export function parametersIncludeType(parameters, typeValue="") {
// Get the consumes/produces value that the user selected
export function contentTypeValues(state, pathMethod) {
pathMethod = pathMethod || []
let op = spec(state).getIn(["paths", ...pathMethod], fromJS({}))
let op = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod], fromJS({}))
let meta = state.getIn(["meta", "paths", ...pathMethod], fromJS({}))
let producesValue = currentProducesFor(state, pathMethod)
@@ -327,14 +369,14 @@ export function contentTypeValues(state, pathMethod) {
// Get the consumes/produces by path
export function operationConsumes(state, pathMethod) {
pathMethod = pathMethod || []
return spec(state).getIn(["paths", ...pathMethod, "consumes"], fromJS({}))
return state.getIn(["meta", ...pathMethod, "consumes"], fromJS({}))
}
// Get the currently selected produces value for an operation
export function currentProducesFor(state, pathMethod) {
pathMethod = pathMethod || []
const operation = spec(state).getIn(["paths", ...pathMethod], null)
const operation = specJsonWithResolvedSubtrees(state).getIn([ "paths", ...pathMethod], null)
if(operation === null) {
// return nothing if the operation does not exist
@@ -362,10 +404,10 @@ export const canExecuteScheme = ( state, path, method ) => {
export const validateBeforeExecute = ( state, pathMethod ) => {
pathMethod = pathMethod || []
let params = spec(state).getIn(["paths", ...pathMethod, "parameters"], fromJS([]))
let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
let isValid = true
params.forEach( (p) => {
paramValues.forEach( (p) => {
let errors = p.get("errors")
if ( errors && errors.count() ) {
isValid = false
+2 -2
View File
@@ -5,7 +5,7 @@ export const updateSpec = (ori, {specActions}) => (...args) => {
export const updateJsonSpec = (ori, {specActions}) => (...args) => {
ori(...args)
specActions.resolveSpec(...args)
specActions.invalidateResolvedSubtreeCache()
}
// Log the request ( just for debugging, shouldn't affect prod )
@@ -16,4 +16,4 @@ export const executeRequest = (ori, { specActions }) => (req) => {
export const validateParams = (ori, { specSelectors }) => (req) => {
return ori(req, specSelectors.isOAS3())
}
}
+1
View File
@@ -7,6 +7,7 @@ module.exports = function({ configs }) {
buildRequest: Swagger.buildRequest,
execute: Swagger.execute,
resolve: Swagger.resolve,
resolveSubtree: Swagger.resolveSubtree,
serializeRes: Swagger.serializeRes,
opId: Swagger.helpers.opId
}
+1 -1
View File
@@ -445,7 +445,7 @@ export const validateDateTime = (val) => {
export const validateGuid = (val) => {
val = val.toString().toLowerCase()
if (!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}[)}]?$/.test(val)) {
if (!/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test(val)) {
return "Value must be a Guid"
}
}
+1
View File
@@ -0,0 +1 @@
<svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-rolling" style="background-image: none; background-position: initial initial; background-repeat: initial initial;"><circle cx="50" cy="50" fill="none" ng-attr-stroke="{{config.color}}" ng-attr-stroke-width="{{config.width}}" ng-attr-r="{{config.radius}}" ng-attr-stroke-dasharray="{{config.dasharray}}" stroke="#555555" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138"><animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform></circle></svg>

After

Width:  |  Height:  |  Size: 734 B

+10
View File
@@ -586,6 +586,16 @@
}
}
.opblock-body
{
.opblock-loading-animation
{
display: block;
margin: 3em;
margin-left: auto;
margin-right: auto;
}
}
.opblock-body pre
{
+1
View File
@@ -198,6 +198,7 @@ section.models
.model-box
{
padding: 10px;
display: inline-block;
border-radius: 4px;
background: rgba($section-models-model-box-background-color,.1);
+2 -1
View File
@@ -24,7 +24,8 @@ describe("<Models/>", function(){
def1: {},
def2: {}
})
}
},
specResolvedSubtree: () => {}
},
layoutSelectors: {
isShown: createSpy()
+4
View File
@@ -177,6 +177,10 @@ describe("spec plugin - actions", function(){
})
})
describe("requestResolvedSubtree", () => {
it("should return a promise ")
})
it.skip("should call errActions.newErr, if the fn.execute rejects", function(){
})
+8 -8
View File
@@ -24,7 +24,7 @@ describe("spec plugin - selectors", function(){
// Given
const spec = fromJS({
resolved: {
json: {
paths: {
"/one": {
get: {
@@ -55,7 +55,7 @@ describe("spec plugin - selectors", function(){
it("should return { requestContentType, responseContentType } from an operation", function(){
// Given
let state = fromJS({
resolved: {
json: {
paths: {
"/one": {
get: {}
@@ -86,7 +86,7 @@ describe("spec plugin - selectors", function(){
it("should default to the first `produces` array value if current is not set", function(){
// Given
let state = fromJS({
resolved: {
json: {
paths: {
"/one": {
get: {
@@ -121,7 +121,7 @@ describe("spec plugin - selectors", function(){
it("should default to `application/json` if a default produces value is not available", function(){
// Given
let state = fromJS({
resolved: {
json: {
paths: {
"/one": {
get: {}
@@ -151,7 +151,7 @@ describe("spec plugin - selectors", function(){
it("should prioritize consumes value first from an operation", function(){
// Given
let state = fromJS({
resolved: {
json: {
paths: {
"/one": {
get: {
@@ -182,7 +182,7 @@ describe("spec plugin - selectors", function(){
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: {
json: {
paths: {
"/one": {
get: {
@@ -204,7 +204,7 @@ describe("spec plugin - selectors", function(){
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: {
json: {
paths: {
"/one": {
get: {
@@ -244,7 +244,7 @@ describe("spec plugin - selectors", function(){
// Given
let state = fromJS({
url: "https://generator.swagger.io/api/swagger.json",
resolved: {
json: {
paths: {
"/one": {
get: {
+3
View File
@@ -260,6 +260,9 @@ describe("utils", function() {
expect(validateGuid("8ce4811e-cec5-4a29-891a-15d1917153c1")).toBeFalsy()
expect(validateGuid("{8ce4811e-cec5-4a29-891a-15d1917153c1}")).toBeFalsy()
expect(validateGuid("8CE4811E-CEC5-4A29-891A-15D1917153C1")).toBeFalsy()
expect(validateGuid("6ffefd8e-a018-e811-bbf9-60f67727d806")).toBeFalsy()
expect(validateGuid("6FFEFD8E-A018-E811-BBF9-60F67727D806")).toBeFalsy()
expect(validateGuid("00000000-0000-0000-0000-000000000000")).toBeFalsy()
})
it("returns a message for invalid input'", function() {