Merge branch 'master' into bug/2903-wrong-font-for-error

# Conflicts:
#	dev-helpers/index.html
This commit is contained in:
Owen Conti
2017-07-17 18:51:04 -06:00
46 changed files with 954 additions and 459 deletions

View File

@@ -0,0 +1,47 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
const propStyle = { color: "#999", fontStyle: "italic" }
export default class ArrayModel extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
name: PropTypes.string,
required: PropTypes.bool,
expandDepth: PropTypes.number,
depth: PropTypes.number
}
render(){
let { getComponent, required, schema, depth, expandDepth } = this.props
let items = schema.get("items")
let title = schema.get("title") || name
let properties = schema.filter( ( v, key) => ["type", "items", "$$ref"].indexOf(key) === -1 )
const ModelCollapse = getComponent("ModelCollapse")
const Model = getComponent("Model")
const titleEl = title &&
<span className="model-title">
<span className="model-title__text">{ title }</span>
</span>
return <span className="model">
<ModelCollapse title={titleEl} collapsed={ depth > expandDepth } collapsedContent="[...]">
[
<span><Model { ...this.props } schema={ items } required={ false }/></span>
]
{
properties.size ? <span>
{ properties.entrySeq().map( ( [ key, v ] ) => <span key={`${key}-${v}`} style={propStyle}>
<br />{ `${key}:`}{ String(v) }</span>)
}<br /></span>
: null
}
</ModelCollapse>
{ required && <span style={{ color: "red" }}>*</span>}
</span>
}
}

View File

@@ -0,0 +1,19 @@
import React from "react"
import ImPropTypes from "react-immutable-proptypes"
const EnumModel = ({ value, getComponent }) => {
let ModelCollapse = getComponent("ModelCollapse")
let collapsedContent = <span>Array [ { value.count() } ]</span>
return <span className="prop-enum">
Enum:<br />
<ModelCollapse collapsedContent={ collapsedContent }>
[ { value.join(", ") } ]
</ModelCollapse>
</span>
}
EnumModel.propTypes = {
value: ImPropTypes.iterable,
getComponent: ImPropTypes.func
}
export default EnumModel

View File

@@ -13,8 +13,13 @@ export default class BaseLayout extends React.Component {
getComponent: PropTypes.func.isRequired
}
onFilterChange =(e) => {
let {target: {value}} = e
this.props.layoutActions.updateFilter(value)
}
render() {
let { specSelectors, specActions, getComponent } = this.props
let { specSelectors, specActions, getComponent, layoutSelectors } = this.props
let info = specSelectors.info()
let url = specSelectors.url()
@@ -26,11 +31,20 @@ export default class BaseLayout extends React.Component {
let Info = getComponent("info")
let Operations = getComponent("operations", true)
let Models = getComponent("models", true)
let Models = getComponent("Models", true)
let AuthorizeBtn = getComponent("authorizeBtn", true)
let Row = getComponent("Row")
let Col = getComponent("Col")
let Errors = getComponent("errors", true)
let isLoading = specSelectors.loadingStatus() === "loading"
let isFailed = specSelectors.loadingStatus() === "failed"
let filter = layoutSelectors.currentFilter()
let inputStyle = {}
if(isFailed) inputStyle.color = "red"
if(isLoading) inputStyle.color = "#aaa"
const Schemes = getComponent("schemes")
const isSpecEmpty = !specSelectors.specStr()
@@ -57,6 +71,7 @@ export default class BaseLayout extends React.Component {
{ schemes && schemes.size ? (
<Schemes schemes={ schemes } specActions={ specActions } />
) : null }
{ securityDefinitions ? (
<AuthorizeBtn />
) : null }
@@ -64,6 +79,15 @@ export default class BaseLayout extends React.Component {
</div>
) : null }
{
filter === null || filter === false ? null :
<div className="filter-container">
<Col className="filter wrapper" mobile={12}>
<input className="operation-filter-input" placeholder="Filter by tag" type="text" onChange={this.onFilterChange} value={filter === true || filter === "true" ? "" : filter} disabled={isLoading} style={inputStyle} />
</Col>
</div>
}
<Row>
<Col mobile={12} desktop={12} >
<Operations/>

View File

@@ -0,0 +1,47 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
export default class ModelCollapse extends Component {
static propTypes = {
collapsedContent: PropTypes.any,
collapsed: PropTypes.bool,
children: PropTypes.any,
title: PropTypes.element
}
static defaultProps = {
collapsedContent: "{...}",
collapsed: true,
title: null
}
constructor(props, context) {
super(props, context)
let { collapsed, collapsedContent } = this.props
this.state = {
collapsed: collapsed !== undefined ? collapsed : ModelCollapse.defaultProps.collapsed,
collapsedContent: collapsedContent || ModelCollapse.defaultProps.collapsedContent
}
}
toggleCollapsed=()=>{
this.setState({
collapsed: !this.state.collapsed
})
}
render () {
const {title} = this.props
return (
<span>
{ title && <span onClick={this.toggleCollapsed} style={{ "cursor": "pointer" }}>{title}</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>
)
}
}

View File

@@ -28,7 +28,7 @@ export default class ModelExample extends React.Component {
render() {
let { getComponent, specSelectors, schema, example, isExecute } = this.props
const Model = getComponent("model")
const ModelWrapper = getComponent("ModelWrapper")
return <div>
<ul className="tab">
@@ -44,7 +44,7 @@ export default class ModelExample extends React.Component {
(isExecute || this.state.activeTab === "example") && example
}
{
!isExecute && this.state.activeTab === "model" && <Model schema={ schema }
!isExecute && this.state.activeTab === "model" && <ModelWrapper schema={ schema }
getComponent={ getComponent }
specSelectors={ specSelectors }
expandDepth={ 1 } />

View File

@@ -0,0 +1,23 @@
import React, { Component, } from "react"
import PropTypes from "prop-types"
export default class ModelComponent extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
name: PropTypes.string,
getComponent: PropTypes.func.isRequired,
specSelectors: PropTypes.object.isRequired,
expandDepth: PropTypes.number
}
render(){
let { getComponent } = this.props
const Model = getComponent("Model")
return <div className="model-box">
<Model { ...this.props } depth={ 1 } expandDepth={ this.props.expandDepth || 0 }/>
</div>
}
}

View File

@@ -1,219 +1,7 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import { List } from "immutable"
const braceOpen = "{"
const braceClose = "}"
const propStyle = { color: "#999", fontStyle: "italic" }
const EnumModel = ({ value }) => {
let collapsedContent = <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 Markdown = getComponent("Markdown")
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>
<Markdown source={ 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,
name: PropTypes.string,
getComponent: PropTypes.func.isRequired,
required: PropTypes.bool
}
render(){
let { schema, getComponent, name, required } = this.props
if(!schema || !schema.get) {
// don't render if schema isn't correctly formed
return <div></div>
}
let type = schema.get("type")
let format = schema.get("format")
let xml = schema.get("xml")
let enumArray = schema.get("enum")
let title = schema.get("title") || name
let description = schema.get("description")
let properties = schema.filter( ( v, key) => ["enum", "type", "format", "description", "$$ref"].indexOf(key) === -1 )
let style = required ? { fontWeight: "bold" } : {}
const Markdown = getComponent("Markdown")
return <span className="model">
{
title && <span className="model-title" style={{ marginRight: "2em" }}>
<span className="model-title__text">{ title }</span>
</span>
}
<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 }: { String(v) }</span>)
: null
}
{
!description ? null :
<Markdown source={ description } />
}
{
xml && xml.size ? (<span><br /><span style={ propStyle }>xml:</span>
{
xml.entrySeq().map( ( [ key, v ] ) => <span key={`${key}-${v}`} style={ propStyle }><br/>&nbsp;&nbsp;&nbsp;{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, name, expandDepth } = this.props
let items = schema.get("items")
let title = schema.get("title") || name
let properties = schema.filter( ( v, key) => ["type", "items", "$$ref"].indexOf(key) === -1 )
return <span className="model">
{
title && <span className="model-title">
<span className="model-title__text">{ title }</span>
</span>
}
<Collapse collapsed={ depth > expandDepth } collapsedContent="[...]">
[
<span><Model { ...this.props } name="" schema={ items } required={ false }/></span>
]
{
properties.size ? <span>
{ properties.entrySeq().map( ( [ key, v ] ) => <span key={`${key}-${v}`} style={propStyle}>
<br />{ `${key}:`}{ String(v) }</span>)
}<br /></span>
: null
}
</Collapse>
{ required && <span style={{ color: "red" }}>*</span>}
</span>
}
}
class Model extends Component {
export default class Model extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
@@ -239,6 +27,9 @@ class Model extends Component {
render () {
let { schema, getComponent, required, name, isRef } = this.props
let ObjectModel = getComponent("ObjectModel")
let ArrayModel = getComponent("ArrayModel")
let PrimitiveModel = getComponent("PrimitiveModel")
let $$ref = schema && schema.get("$$ref")
let modelName = $$ref && this.getModelName( $$ref )
let modelSchema, type
@@ -260,69 +51,13 @@ class Model extends Component {
name={ name || modelName }
isRef={ isRef!== undefined ? isRef : !!$$ref }/>
case "array":
return <ArrayModel className="array" { ...this.props } schema={ modelSchema } name={ name || modelName } required={ required } />
return <ArrayModel className="array" { ...this.props } schema={ modelSchema } required={ required } />
case "string":
case "number":
case "integer":
case "boolean":
default:
return <Primitive { ...this.props } getComponent={ getComponent } schema={ modelSchema } name={ name || modelName } required={ required }/>
return <PrimitiveModel getComponent={ getComponent } 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(){
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>)
}
}
}

View File

@@ -16,7 +16,7 @@ export default class Models extends Component {
let { docExpansion } = getConfigs()
let showModels = layoutSelectors.isShown("models", docExpansion === "full" || docExpansion === "list" )
const Model = getComponent("model")
const ModelWrapper = getComponent("ModelWrapper")
const Collapse = getComponent("Collapse")
if (!definitions.size) return null
@@ -24,15 +24,15 @@ export default class Models extends Component {
return <section className={ showModels ? "models is-open" : "models"}>
<h4 onClick={() => layoutActions.show("models", !showModels)}>
<span>Models</span>
<svg className="arrow" width="20" height="20">
<use xlinkHref={showModels ? "#large-arrow-down" : "#large-arrow"} />
<svg width="20" height="20">
<use xlinkHref="#large-arrow" />
</svg>
</h4>
<Collapse isOpened={showModels} animated>
<Collapse isOpened={showModels}>
{
definitions.entrySeq().map( ( [ name, model ])=>{
return <div className="model-container" key={ `models-section-${name}` }>
<Model name={ name }
<ModelWrapper name={ name }
schema={ model }
isRef={ true }
getComponent={ getComponent }

View File

@@ -0,0 +1,103 @@
import React, { Component, } from "react"
import PropTypes from "prop-types"
import { List } from "immutable"
const braceOpen = "{"
const braceClose = "}"
export default 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
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 JumpToPath = getComponent("JumpToPath", true)
const Markdown = getComponent("Markdown")
const Model = getComponent("Model")
const ModelCollapse = getComponent("ModelCollapse")
const JumpToPathSection = ({ name }) => <span className="model-jump-to-path"><JumpToPath path={`definitions.${name}`} /></span>
const collapsedContent = (<span>
<span>{ braceOpen }</span>...<span>{ braceClose }</span>
{
isRef ? <JumpToPathSection name={ name }/> : ""
}
</span>)
const titleEl = 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>
return <span className="model">
<ModelCollapse title={titleEl} 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>
<Markdown source={ 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>
</ModelCollapse>
</span>
}
}

View File

@@ -116,7 +116,8 @@ export default class Operation extends PureComponent {
specActions,
specSelectors,
authActions,
authSelectors
authSelectors,
getConfigs
} = this.props
let summary = operation.get("summary")
@@ -141,6 +142,10 @@ export default class Operation extends PureComponent {
const Markdown = getComponent( "Markdown" )
const Schemes = getComponent( "schemes" )
const { deepLinking } = getConfigs()
const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"
// Merge in Live Response
if(response && response.size > 0) {
let notDocumented = !responses.get(String(response.get("status")))
@@ -152,13 +157,18 @@ export default class Operation extends PureComponent {
let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method )
return (
<div className={deprecated ? "opblock opblock-deprecated" : shown ? `opblock opblock-${method} is-open` : `opblock opblock-${method}`} id={isShownKey} >
<div className={deprecated ? "opblock opblock-deprecated" : shown ? `opblock opblock-${method} is-open` : `opblock opblock-${method}`} id={isShownKey.join("-")} >
<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>
<span className="opblock-summary-method">{method.toUpperCase()}</span>
<span className={ deprecated ? "opblock-summary-path__deprecated" : "opblock-summary-path" } >
<a
className="nostyle"
onClick={(e) => e.preventDefault()}
href={ isDeepLinkingEnabled ? `#/${isShownKey[1]}/${isShownKey[2]}` : ""} >
<span>{path}</span>
</a>
<JumpToPath path={jumpToKey} />
</span>
{ !showSummary ? null :
<div className="opblock-summary-description">
@@ -191,7 +201,9 @@ export default class Operation extends PureComponent {
<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>
<span className="opblock-external-docs__description">
<Markdown source={ externalDocs.get("description") } />
</span>
<a className="opblock-external-docs__link" href={ externalDocs.get("url") }>{ externalDocs.get("url") }</a>
</div>
</div> : null

View File

@@ -1,5 +1,8 @@
import React from "react"
import PropTypes from "prop-types"
import { helpers } from "swagger-client"
const { opId } = helpers
export default class Operations extends React.Component {
@@ -33,7 +36,29 @@ 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,
deepLinking
} = getConfigs()
const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"
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>
@@ -48,8 +73,16 @@ export default class Operations extends React.Component {
return (
<div className={showTag ? "opblock-tag-section is-open" : "opblock-tag-section"} key={"operation-" + tag}>
<h4 onClick={() => layoutActions.show(isShownKey, !showTag)} className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }>
<span>{tag}</span>
<h4
onClick={() => layoutActions.show(isShownKey, !showTag)}
className={!tagDescription ? "opblock-tag no-desc" : "opblock-tag" }
id={isShownKey.join("-")}>
<a
className="nostyle"
onClick={(e) => e.preventDefault()}
href={ isDeepLinkingEnabled ? `#/${tag}` : ""}>
<span>{tag}</span>
</a>
{ !tagDescription ? null :
<small>
{ tagDescription }
@@ -67,11 +100,14 @@ export default class Operations extends React.Component {
{
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 operationId =
op.getIn(["operation", "operationId"]) || op.getIn(["operation", "__originalOperationId"]) || opId(op.get("operation"), path, method) || op.get("id")
const isShownKey = ["operations", tag, operationId]
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"))

View File

@@ -0,0 +1,55 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
const propStyle = { color: "#999", fontStyle: "italic" }
export default class Primitive extends Component {
static propTypes = {
schema: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired,
required: PropTypes.bool
}
render(){
let { schema, getComponent, required } = this.props
if(!schema || !schema.get) {
// don't render if schema isn't correctly formed
return <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", "description", "$$ref"].indexOf(key) === -1 )
let style = required ? { fontWeight: "bold" } : {}
const Markdown = getComponent("Markdown")
const EnumModel = getComponent("EnumModel")
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 }: { String(v) }</span>)
: null
}
{
!description ? null :
<Markdown source={ description } />
}
{
xml && xml.size ? (<span><br /><span style={ propStyle }>xml:</span>
{
xml.entrySeq().map( ( [ key, v ] ) => <span key={`${key}-${v}`} style={ propStyle }><br/>&nbsp;&nbsp;&nbsp;{key}: { String(v) }</span>).toArray()
}
</span>): null
}
{
enumArray && <EnumModel value={ enumArray } getComponent={ getComponent } />
}
</span>
}
}

View File

@@ -18,10 +18,12 @@ function Markdown({ source }) {
return null
}
return <Remarkable
options={{html: true, typographer: true, linkify: true, linkTarget: "_blank"}}
source={sanitized}
></Remarkable>
return <div className="markdown">
<Remarkable
options={{html: true, typographer: true, breaks: true, linkify: true, linkTarget: "_blank"}}
source={sanitized}
></Remarkable>
</div>
}
Markdown.propTypes = {

View File

@@ -4,19 +4,48 @@ import System from "core/system"
import win from "core/window"
import ApisPreset from "core/presets/apis"
import * as AllPlugins from "core/plugins/all"
import { parseSeach, filterConfigs } from "core/utils"
import { parseSearch, filterConfigs } from "core/utils"
const CONFIGS = [ "url", "urls", "urls.primaryName", "spec", "validatorUrl", "onComplete", "onFailure", "authorizations", "docExpansion",
"apisSorter", "operationsSorter", "supportedSubmitMethods", "dom_id", "defaultModelRendering", "oauth2RedirectUrl",
"showRequestHeaders", "custom", "modelPropertyMacro", "parameterMacro", "displayOperationId" , "displayRequestDuration"]
const CONFIGS = [
"url",
"urls",
"urls.primaryName",
"spec",
"validatorUrl",
"onComplete",
"onFailure",
"authorizations",
"docExpansion",
"tagsSorter",
"maxDisplayedTags",
"filter",
"operationsSorter",
"supportedSubmitMethods",
"dom_id",
"defaultModelRendering",
"oauth2RedirectUrl",
"showRequestHeaders",
"custom",
"modelPropertyMacro",
"parameterMacro",
"displayOperationId",
"displayRequestDuration",
"deepLinking",
]
// eslint-disable-next-line no-undef
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION } = buildInfo
const { GIT_DIRTY, GIT_COMMIT, PACKAGE_VERSION, HOSTNAME, BUILD_TIME } = buildInfo
module.exports = function SwaggerUI(opts) {
win.versions = win.versions || {}
win.versions.swaggerUi = `${PACKAGE_VERSION}/${GIT_COMMIT || "unknown"}${GIT_DIRTY ? "-dirty" : ""}`
win.versions.swaggerUi = {
version: PACKAGE_VERSION,
gitRevision: GIT_COMMIT,
gitDirty: GIT_DIRTY,
buildTimestamp: BUILD_TIME,
machine: HOSTNAME
}
const defaults = {
// Some general settings, that we floated to the top
@@ -26,15 +55,19 @@ module.exports = function SwaggerUI(opts) {
urls: null,
layout: "BaseLayout",
docExpansion: "list",
maxDisplayedTags: null,
filter: null,
validatorUrl: "https://online.swagger.io/validator",
configs: {},
custom: {},
displayOperationId: false,
displayRequestDuration: false,
deepLinking: false,
// Initial set of plugins ( TODO rename this, or refactor - we don't need presets _and_ plugins. Its just there for performance.
// Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest.
presets: [
ApisPreset
],
// Plugins; ( loaded after presets )
@@ -50,7 +83,9 @@ module.exports = function SwaggerUI(opts) {
store: { },
}
const constructorConfig = deepExtend({}, defaults, opts)
let queryConfig = parseSearch()
const constructorConfig = deepExtend({}, defaults, opts, queryConfig)
const storeConfigs = deepExtend({}, constructorConfig.store, {
system: {
@@ -59,7 +94,8 @@ module.exports = function SwaggerUI(opts) {
plugins: constructorConfig.presets,
state: {
layout: {
layout: constructorConfig.layout
layout: constructorConfig.layout,
filter: constructorConfig.filter
},
spec: {
spec: "",
@@ -80,7 +116,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

@@ -0,0 +1 @@
See `docs/deep-linking.md`.

View File

@@ -0,0 +1,7 @@
export const setHash = (value) => {
if(value) {
return history.pushState(null, null, `#${value}`)
} else {
return window.location.hash = ""
}
}

View File

@@ -0,0 +1,18 @@
// import reducers from "./reducers"
// import * as actions from "./actions"
// import * as selectors from "./selectors"
import * as specWrapActions from "./spec-wrap-actions"
import * as layoutWrapActions from "./layout-wrap-actions"
export default function() {
return {
statePlugins: {
spec: {
wrapActions: specWrapActions
},
layout: {
wrapActions: layoutWrapActions
}
}
}
}

View File

@@ -0,0 +1,36 @@
import { setHash } from "./helpers"
export const show = (ori, { getConfigs }) => (...args) => {
ori(...args)
const isDeepLinkingEnabled = getConfigs().deepLinking
if(!isDeepLinkingEnabled || isDeepLinkingEnabled === "false") {
return
}
try {
let [thing, shown] = args
let [type] = thing
if(type === "operations-tag" || type === "operations") {
if(!shown) {
return setHash("/")
}
if(type === "operations") {
let [, tag, operationId] = thing
setHash(`/${tag}/${operationId}`)
}
if(type === "operations-tag") {
let [, tag] = thing
setHash(`/${tag}`)
}
}
} catch(e) {
// This functionality is not mission critical, so if something goes wrong
// we'll just move on
console.error(e)
}
}

View File

@@ -0,0 +1,51 @@
import scrollTo from "scroll-to-element"
const SCROLL_OFFSET = -5
let hasHashBeenParsed = false
export const updateResolved = (ori, { layoutActions, getConfigs }) => (...args) => {
ori(...args)
const isDeepLinkingEnabled = getConfigs().deepLinking
if(!isDeepLinkingEnabled || isDeepLinkingEnabled === "false") {
return
}
if(window.location.hash && !hasHashBeenParsed ) {
let hash = window.location.hash.slice(1) // # is first character
if(hash[0] === "!") {
// Parse UI 2.x shebangs
hash = hash.slice(1)
}
if(hash[0] === "/") {
// "/pet/addPet" => "pet/addPet"
// makes the split result cleaner
// also handles forgotten leading slash
hash = hash.slice(1)
}
let [tag, operationId] = hash.split("/")
if(tag && operationId) {
// Pre-expand and scroll to the operation
layoutActions.show(["operations-tag", tag], true)
layoutActions.show(["operations", tag, operationId], true)
scrollTo(`#operations-${tag}-${operationId}`, {
offset: SCROLL_OFFSET
})
} else if(tag) {
// Pre-expand and scroll to the tag
layoutActions.show(["operations-tag", tag], true)
scrollTo(`#operations-tag-${tag}`, {
offset: SCROLL_OFFSET
})
}
}
hasHashBeenParsed = true
}

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

@@ -200,15 +200,22 @@ export const operationsWithTags = createSelector(
}
)
export const taggedOperations = ( state ) =>( { getConfigs } ) => {
let { operationsSorter }= getConfigs()
export const taggedOperations = (state) => ({ getConfigs }) => {
let { tagsSorter, operationsSorter } = getConfigs()
return operationsWithTags(state)
.sortBy(
(val, key) => key, // get the name of the tag to be passed to the sorter
(tagA, tagB) => {
let sortFn = (typeof tagsSorter === "function" ? tagsSorter : sorters.tagsSorter[ tagsSorter ])
return (!sortFn ? null : sortFn(tagA, tagB))
}
)
.map((ops, tag) => {
let sortFn = (typeof operationsSorter === "function" ? operationsSorter : sorters.operationsSorter[ operationsSorter ])
let operations = (!sortFn ? ops : ops.sort(sortFn))
return operationsWithTags(state).map((ops, tag) => {
let sortFn = typeof operationsSorter === "function" ? operationsSorter
: sorters.operationsSorter[operationsSorter]
let operations = !sortFn ? ops : ops.sort(sortFn)
return Map({tagDetails: tagDetails(state, tag), operations: operations})})
return Map({ tagDetails: tagDetails(state, tag), operations: operations })
})
}
export const responses = createSelector(
@@ -277,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

@@ -10,6 +10,7 @@ import auth from "core/plugins/auth"
import util from "core/plugins/util"
import SplitPaneModePlugin from "core/plugins/split-pane-mode"
import downloadUrlPlugin from "core/plugins/download-url"
import deepLinkingPlugin from "core/plugins/deep-linking"
import App from "core/components/app"
import AuthorizationPopup from "core/components/auth/authorization-popup"
@@ -41,9 +42,15 @@ import Footer from "core/components/footer"
import ParamBody from "core/components/param-body"
import Curl from "core/components/curl"
import Schemes from "core/components/schemes"
import ModelCollapse from "core/components/model-collapse"
import ModelExample from "core/components/model-example"
import ModelWrapper from "core/components/model-wrapper"
import Model from "core/components/model"
import Models from "core/components/models"
import EnumModel from "core/components/enum-model"
import ObjectModel from "core/components/object-model"
import ArrayModel from "core/components/array-model"
import PrimitiveModel from "core/components/primitive-model"
import TryItOutButton from "core/components/try-it-out-button"
import Markdown from "core/components/providers/markdown"
@@ -88,8 +95,14 @@ export default function() {
curl: Curl,
schemes: Schemes,
modelExample: ModelExample,
model: Model,
models: Models,
ModelWrapper,
ModelCollapse,
Model,
Models,
EnumModel,
ObjectModel,
ArrayModel,
PrimitiveModel,
TryItOutButton,
Markdown,
BaseLayout
@@ -119,6 +132,7 @@ export default function() {
auth,
ast,
SplitPaneModePlugin,
downloadUrlPlugin
downloadUrlPlugin,
deepLinkingPlugin
]
}

View File

@@ -228,13 +228,13 @@ export function highlight (el) {
var reset = function(el) {
var text = el.textContent,
pos = 0, // current position
pos = 0, // current position
next1 = text[0], // next character
chr = 1, // current character
prev1, // previous character
prev2, // the one before the previous
token = // current token content
el.innerHTML = "", // (and cleaning the node)
chr = 1, // current character
prev1, // previous character
prev2, // the one before the previous
token = // current token content
el.innerHTML = "", // (and cleaning the node)
// current token type:
// 0: anything else (whitespaces / newlines)
@@ -274,11 +274,11 @@ export function highlight (el) {
(tokenType > 8 && chr == "\n") ||
[ // finalize conditions for other token types
// 0: whitespaces
/\S/[test](chr), // merged together
/\S/[test](chr), // merged together
// 1: operators
1, // consist of a single character
1, // consist of a single character
// 2: braces
1, // consist of a single character
1, // consist of a single character
// 3: (key)word
!/[$\w]/[test](chr),
// 4: regex
@@ -341,12 +341,12 @@ export function highlight (el) {
// condition)
tokenType = 11
while (![
1, // 0: whitespace
1, // 0: whitespace
// 1: operator or braces
/[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr), // eslint-disable-line no-useless-escape
/[\])]/[test](chr), // 2: closing brace
/[$\w]/[test](chr), // 3: (key)word
chr == "/" && // 4: regex
/[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr), // eslint-disable-line no-useless-escape
/[\])]/[test](chr), // 2: closing brace
/[$\w]/[test](chr), // 3: (key)word
chr == "/" && // 4: regex
// previous token was an
// opening brace or an
// operator (otherwise
@@ -355,13 +355,13 @@ export function highlight (el) {
// workaround for xml
// closing tags
prev1 != "<",
chr == "\"", // 5: string with "
chr == "'", // 6: string with '
chr == "\"", // 5: string with "
chr == "'", // 6: string with '
// 7: xml comment
chr+next1+text[pos+1]+text[pos+2] == "<!--",
chr+next1 == "/*", // 8: multiline comment
chr+next1 == "//", // 9: single-line comment
chr == "#" // 10: hash-style comment
chr+next1 == "/*", // 8: multiline comment
chr+next1 == "//", // 9: single-line comment
chr == "#" // 10: hash-style comment
][--tokenType]);
}
@@ -451,13 +451,13 @@ export const propChecker = (props, nextProps, objectList=[], ignoreList=[]) => {
}
export const validateNumber = ( val ) => {
if ( !/^-?\d+(\.?\d+)?$/.test(val)) {
if (!/^-?\d+(\.?\d+)?$/.test(val)) {
return "Value must be a number"
}
}
export const validateInteger = ( val ) => {
if ( !/^-?\d+$/.test(val)) {
if (!/^-?\d+$/.test(val)) {
return "Value must be an integer"
}
}
@@ -485,6 +485,10 @@ export const validateParam = (param, isXml) => {
return errors
}
if ( value === null || value === undefined ) {
return errors
}
if ( type === "number" ) {
let err = validateNumber(value)
if (!err) return errors
@@ -542,7 +546,7 @@ export const getSampleSchema = (schema, contentType="", config={}) => {
return JSON.stringify(memoizedSampleFromSchema(schema, config), null, 2)
}
export const parseSeach = () => {
export const parseSearch = () => {
let map = {}
let search = window.location.search
@@ -574,6 +578,9 @@ export const sorters = {
operationsSorter: {
alpha: (a, b) => a.get("path").localeCompare(b.get("path")),
method: (a, b) => a.get("method").localeCompare(b.get("method"))
},
tagsSorter: {
alpha: (a, b) => a.localeCompare(b)
}
}

View File

@@ -6,6 +6,10 @@ import Logo from "./logo_small.png"
export default class Topbar extends React.Component {
static propTypes = {
layoutActions: PropTypes.object.isRequired
}
constructor(props, context) {
super(props, context)
this.state = { url: props.specSelectors.url(), selectedIndex: 0 }
@@ -80,6 +84,11 @@ 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
const Button = getComponent("Button")

View File

@@ -29,7 +29,7 @@ export default class StandaloneLayout extends React.Component {
return (
<Container className='swagger-ui'>
{ Topbar ? <Topbar/> : null }
{ Topbar ? <Topbar /> : null }
{ loadingStatus === "loading" &&
<div className="info">
<h4 className="title">Loading...</h4>
@@ -45,7 +45,7 @@ export default class StandaloneLayout extends React.Component {
<h4 className="title">Failed to load config.</h4>
</div>
}
{ !loadingStatus || loadingStatus === "success" && <BaseLayout/> }
{ !loadingStatus || loadingStatus === "success" && <BaseLayout /> }
<Row>
<Col>
<OnlineValidatorBadge />

View File

@@ -27,6 +27,7 @@
.opblock-tag
{
display: flex;
align-items: center;
padding: 10px 20px 10px 10px;
@@ -35,8 +36,6 @@
border-bottom: 1px solid rgba(#3b4151, .3);
align-items: center;
&:hover
{
background: rgba(#000,.02);
@@ -88,9 +87,10 @@
font-size: 14px;
font-weight: normal;
flex: 1;
padding: 0 10px;
flex: 1;
@include text_body();
}
}
@@ -116,6 +116,8 @@
transition: all .5s;
}
.opblock
{
margin: 0 0 15px 0;
@@ -136,24 +138,23 @@
.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
@@ -166,9 +167,10 @@
{
font-size: 14px;
flex: 1;
margin: 0;
flex: 1;
@include text_headline();
}
}
@@ -197,11 +199,11 @@
font-size: 16px;
display: flex;
align-items: center;
padding: 0 10px;
@include text_code();
align-items: center;
.view-line-link
{
@@ -240,18 +242,18 @@
font-size: 13px;
flex: 1;
@include text_body();
}
.opblock-summary
{
display: flex;
align-items: center;
padding: 5px;
cursor: pointer;
align-items: center;
}
&.opblock-post
@@ -298,12 +300,24 @@
.opblock-schemes
{
padding: 8px 20px;
padding: 8px 20px;
.schemes-title
{
padding: 0 10px 0 0;
}
.schemes-title
{
padding: 0 10px 0 0;
}
}
}
.filter
{
.operation-filter-input
{
width: 100%;
margin: 20px 0;
padding: 10px 10px;
border: 2px solid #d8dde7;
}
}
@@ -358,6 +372,7 @@
}
.opblock-description-wrapper,
.opblock-external-docs-wrapper,
.opblock-title_normal
{
font-size: 12px;
@@ -386,6 +401,12 @@
}
}
.opblock-external-docs-wrapper {
h4 {
padding-left: 0px;
}
}
.execute-wrapper
{
padding: 20px;
@@ -480,13 +501,11 @@
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;
@@ -515,10 +534,9 @@
.schemes
{
display: flex;
align-items: center;
> label
> label
{
font-size: 12px;
font-weight: bold;
@@ -606,3 +624,25 @@
opacity: 0;
}
}
section
{
h3
{
@include text_headline();
}
}
a.nostyle {
text-decoration: inherit;
color: inherit;
cursor: auto;
display: inline;
&:visited {
text-decoration: inherit;
color: inherit;
cursor: auto;
}
}

View File

@@ -79,6 +79,10 @@
border-radius: 4px;
background: rgba(#000,.7);
}
p {
margin: 0 0 1em 0;
}
}

View File

@@ -43,7 +43,6 @@
margin: 0;
border: 2px solid #547f00;
border-radius: 4px 0 0 4px;
outline: none;
}