Merge branch 'master' into update/toggleMenuItem

This commit is contained in:
Roman Hotsiy 2019-07-29 17:30:26 +03:00 committed by GitHub
commit bfd703d2da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 812 additions and 1081 deletions

View File

@ -1,7 +1,11 @@
language: node_js
node_js:
- '8'
cache: yarn
- '10'
cache:
yarn: true
directories:
# we also need to cache folder with Cypress binary
- ~/.cache
env:
global:
- GH_REF: github.com/Redocly/redoc.git
@ -14,6 +18,9 @@ env:
- secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo=
addons:
chrome: stable
apt:
packages:
- libgconf-2-4 # for cypress
before_script: npm run bundle
script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm
run e2e)

View File

@ -1,3 +1,16 @@
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
### Bug Fixes
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)

View File

@ -243,6 +243,7 @@ You can use all of the following options with standalone version on <redoc> tag
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `disableSearch` - disable search indexing and search box
* `onlyRequiredInSamples` - shows only required fields in request samples.
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
## Advanced usage of standalone version

View File

@ -156,7 +156,9 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
},
);
} else if (request.url === '/') {
respondWithGzip(pageHTML, request, response);
respondWithGzip(pageHTML, request, response, {
'Content-Type': 'text/html',
});
} else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {

View File

@ -1,25 +1,28 @@
{
"name": "redoc-cli",
"version": "0.8.4",
"version": "0.8.5",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
"repository": "https://github.com/Redocly/redoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT",
"engines": {
"node": ">= 8"
},
"dependencies": {
"chokidar": "^2.0.4",
"handlebars": "^4.0.11",
"chokidar": "^3.0.2",
"handlebars": "^4.1.2",
"isarray": "^2.0.4",
"mkdirp": "^0.5.1",
"mobx": "^4.2.0",
"node-libs-browser": "^2.2.0",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"redoc": "^2.0.0-rc.8-1",
"styled-components": "^4.1.3",
"tslib": "^1.9.3",
"yargs": "^12.0.5"
"node-libs-browser": "^2.2.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"redoc": "^2.0.0-rc.10",
"styled-components": "^4.3.2",
"tslib": "^1.10.0",
"yargs": "^13.2.4"
},
"scripts": {
"ci-publish": "ci-publish"
@ -28,7 +31,7 @@
"access": "public"
},
"devDependencies": {
"@types/chokidar": "^1.7.5",
"@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1"

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ info:
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<security-definitions />
<SecurityDefinitions />
version: 1.0.0
title: Swagger Petstore
@ -63,6 +63,14 @@ tags:
description: Access to Petstore orders
- name: user
description: Operations about user
- name: pet_model
x-displayName: The Pet Model
description: |
<ObjectDescription schemaRef="#/components/schemas/Pet" />
- name: store_model
x-displayName: The Order Model
description: |
<ObjectDescription schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
x-tagGroups:
- name: General
tags:
@ -71,6 +79,10 @@ x-tagGroups:
- name: User Management
tags:
- user
- name: Models
tags:
- pet_model
- store_model
paths:
/pet:
parameters:
@ -754,6 +766,11 @@ components:
description: Indicates whenever order was completed or not
type: boolean
default: false
readOnly: true
rqeuestId:
description: Unique Request Id
type: string
writeOnly: true
xml:
name: Order
Pet:
@ -926,3 +943,10 @@ components:
type: apiKey
name: api_key
in: header
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
complete: false

View File

@ -6,7 +6,7 @@ describe('Menu', () => {
it('should have valid items count', () => {
cy.get('.menu-content')
.find('li')
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8));
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
});
it('should sync active menu items while scroll', () => {

View File

@ -1,6 +1,6 @@
{
"name": "redoc",
"version": "2.0.0-rc.9",
"version": "2.0.0-rc.10",
"description": "ReDoc",
"repository": {
"type": "git",
@ -28,7 +28,7 @@
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run bundlesize && npm run license-check",
"test": "npm run lint && npm run unit && npm run license-check",
"unit": "jest --coverage",
"e2e": "cypress run",
"e2e-ci": "cypress run --record",

View File

@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps {
data: any;
children: (
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
}
export class CopyButtonWrapper extends React.PureComponent<

View File

@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
display: block;
height: 0;
position: absolute;
right: 0.35em;
right: 0.6em;
top: 50%;
margin-top: -0.125em;
width: 0;

View File

@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor};
word-break: break-word;
`;
export const TypeFormat = TypeName;

View File

@ -44,8 +44,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null;
const website =
(info.contact &&
info.contact.url && (
(info.contact && info.contact.url && (
<InfoSpan>
URL: <a href={info.contact.url}>{info.contact.url}</a>
</InfoSpan>
@ -53,8 +52,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
null;
const email =
(info.contact &&
info.contact.email && (
(info.contact && info.contact.email && (
<InfoSpan>
{info.contact.name || 'E-mail'}:{' '}
<a href={'mailto:' + info.contact.email}>{info.contact.email}</a>
@ -70,11 +68,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) ||
null;
const version =
(info.version && (
<span>({info.version})</span>
)) ||
null;
const version = (info.version && <span>({info.version})</span>) || null;
return (
<Section>

View File

@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils';
import { expandDefaultServerVariables, getBasePath } from '../../utils';
import {
EndpointInfo,
HttpVerb,
@ -60,21 +60,24 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
/>
</EndpointInfo>
<ServersOverlay expanded={expanded}>
{operation.servers.map(server => (
<ServerItem key={server.url}>
{operation.servers.map(server => {
const normalizedUrl = expandDefaultServerVariables(server.url, server.variables);
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} />
<SelectOnClick>
<ServerUrl>
<span>
{hideHostname || options.hideHostname
? getBasePath(server.url)
: server.url}
? getBasePath(normalizedUrl)
: normalizedUrl}
</span>
{operation.path}
</ServerUrl>
</SelectOnClick>
</ServerItem>
))}
);
})}
</ServersOverlay>
</OperationEndpointWrap>
)}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export interface EnumValuesProps {
values: string[];
@ -9,8 +10,10 @@ export interface EnumValuesProps {
}
export class EnumValues extends React.PureComponent<EnumValuesProps> {
static contextType = OptionsContext;
render() {
const { values, type } = this.props;
const { enumSkipQuotes } = this.context;
if (!values.length) {
return null;
}
@ -21,11 +24,14 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
{type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>
{values.map((value, idx) => (
{values.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}>
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '}
<ExampleValue>{exampleValue}</ExampleValue>
</React.Fragment>
))}
);
})}
</div>
);
}

View File

@ -65,8 +65,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} />
</PropertyDetailsCell>
</tr>
{field.expanded &&
withSubSchema && (
{field.expanded && withSubSchema && (
<tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap>

View File

@ -17,10 +17,7 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
return (
<div>
<FieldLabel> {this.props.label} </FieldLabel>{' '}
<ExampleValue>
{value}
</ExampleValue>
<FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
</div>
);
}

View File

@ -21,10 +21,13 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> {
static contextType = OptionsContext;
render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { enumSkipQuotes } = this.context;
const { schema, description, example, deprecated } = field;
@ -33,7 +36,8 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
if (showExamples) {
const label = l('example') + ':';
if (field.in && field.style) {
const serializedValue = serializeParameterValue(field, example);
const serializedValue =
example !== undefined ? serializeParameterValue(field, example) : undefined;
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
@ -64,7 +68,7 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<Badge type="warning"> {l('deprecated')} </Badge>
</div>
)}
<FieldDetail label={l('default') + ':'} value={schema.default} />
<FieldDetail raw={enumSkipQuotes} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}

View File

@ -5,6 +5,7 @@ import { SampleControls } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv';
import { jsonToHTML } from '../../utils/jsonToHtml';
import { OptionsContext } from '../OptionsProvider';
import { jsonStyles } from './style';
export interface JsonProps {
@ -32,12 +33,18 @@ class Json extends React.PureComponent<JsonProps> {
<span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span>
</SampleControls>
<OptionsContext.Consumer>
{options => (
<PrismDiv
className={this.props.className}
// tslint:disable-next-line
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
/>
)}
</OptionsContext.Consumer>
</JsonViewerWrap>
);

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown';
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface MediaTypeChildProps {
schema: SchemaModel;
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
export interface MediaTypesSwitchProps {
content?: MediaContentModel;
withLabel?: boolean;
renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeMime: MediaTypeModel) => JSX.Element;
}
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
};
});
const Wrapper = ({ children }) =>
this.props.withLabel ? (
<DropdownWrapper>
<DropdownLabel>Content type</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return (
<>
<Wrapper>
{this.props.renderDropdown({
value: options[activeMimeIdx],
options,
onChange: this.switchMedia,
})}
</Wrapper>
{this.props.children(content.active)}
</>
);

View File

@ -1,17 +1,33 @@
import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import styled from '../../styled-components';
import { DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example';
import { NoSampleLabel } from './styled.elements';
import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps {
mediaType: MediaTypeModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
interface MediaTypeSamplesState {
activeIdx: number;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
state = {
activeIdx: 0,
};
switchMedia = ({ value }) => {
this.setState({
activeIdx: parseInt(value, 10),
});
};
render() {
const { activeIdx } = this.state;
const examples = this.props.mediaType.examples || {};
const mimeType = this.props.mediaType.name;
@ -21,28 +37,46 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
if (examplesNames.length === 0) {
return noSample;
}
if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => {
return {
label: examples[name].summary || name,
value: idx.toString(),
};
});
const example = examples[examplesNames[activeIdx]];
const description = example.description;
return (
<SmallTabs defaultIndex={0}>
<TabList>
{examplesNames.map(name => (
<Tab key={name}> {examples[name].summary || name} </Tab>
))}
</TabList>
{examplesNames.map(name => (
<TabPanel key={name}>
<Example example={examples[name]} mimeType={mimeType} />
</TabPanel>
))}
</SmallTabs>
<SamplesWrapper>
<DropdownWrapper>
<DropdownLabel>Example</DropdownLabel>
{this.props.renderDropdown({
value: options[activeIdx],
options,
onChange: this.switchMedia,
})}
</DropdownWrapper>
<div>
{description && <Markdown source={description} />}
<Example example={example} mimeType={mimeType} />
</div>
</SamplesWrapper>
);
} else {
const name = examplesNames[0];
const example = examples[examplesNames[0]];
return (
<div>
<Example example={examples[name]} mimeType={mimeType} />
</div>
<SamplesWrapper>
{example.description && <Markdown source={example.description} />}
<Example example={example} mimeType={mimeType} />
</SamplesWrapper>
);
}
}
}
const SamplesWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -2,10 +2,9 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { MediaTypeSamples } from './MediaTypeSamples';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { MediaContentModel } from '../../services/models';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
export interface PayloadSamplesProps {
@ -21,8 +20,14 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
}
return (
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}>
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />}
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
{mediaType => (
<MediaTypeSamples
key="samples"
mediaType={mediaType}
renderDropdown={this.renderDropdown}
/>
)}
</MediaTypesSwitch>
);
}

View File

@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
prevRef.current = example;
useEffect(
() => {
useEffect(() => {
const load = async () => {
setIsLoading(true);
try {
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
};
load();
},
[example, mimeType],
);
}, [example, mimeType]);
return value.current;
}

View File

@ -1,29 +1,48 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div`
border-bottom: 1px solid rgba(255, 255, 255, 0.9);
padding: 12px;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0;
display: block;
color: rgba(255, 255, 255, 0.8);
`;
export const DropdownLabel = styled.span`
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 12px;
position: absolute;
z-index: 1;
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
position: relative;
`;
export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control,
.Dropdown-control:hover {
font-size: 1em;
border: none;
padding: 0 1.2em 0 0;
padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none;
@ -34,6 +53,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
}
.Dropdown-menu {
margin: 0;
margin-top: 2px;
}
`;

View File

@ -28,8 +28,7 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
code={code}
opened={expanded}
/>
{expanded &&
!empty && (
{expanded && !empty && (
<ResponseDetailsWrap>
<ResponseDetails response={this.props.response} />
</ResponseDetailsWrap>

View File

@ -34,9 +34,9 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
const filteredFields = needFilter
? fields.filter(item => {
return (
(this.props.skipReadOnly && !item.schema.readOnly) ||
(this.props.skipWriteOnly && !item.schema.writeOnly)
return !(
(this.props.skipReadOnly && item.schema.readOnly) ||
(this.props.skipWriteOnly && item.schema.writeOnly)
);
})
: fields;

View File

@ -0,0 +1,93 @@
import * as React from 'react';
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import styled from '../../styled-components';
import { OpenAPIMediaType } from '../../types';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
import { Schema } from '../Schema';
export interface ObjectDescriptionProps {
schemaRef: string;
exampleRef?: string;
showReadOnly?: boolean;
showWriteOnly?: boolean;
parser: OpenAPIParser;
options: RedocNormalizedOptions;
}
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
if (!schemaRef) {
return {};
}
const info: OpenAPIMediaType = {
schema: { $ref: schemaRef },
};
if (exampleRef) {
info.examples = { example: { $ref: exampleRef } };
}
return info;
}
private _mediaModel: MediaTypeModel;
private get mediaModel() {
const { parser, schemaRef, exampleRef, options } = this.props;
if (!this._mediaModel) {
this._mediaModel = new MediaTypeModel(
parser,
'json',
false,
SchemaDefinition.getMediaType(schemaRef, exampleRef),
options,
);
}
return this._mediaModel;
}
render() {
const { showReadOnly = true, showWriteOnly = false } = this.props;
return (
<Section>
<Row>
<MiddlePanel>
<Schema
skipWriteOnly={!showWriteOnly}
skipReadOnly={!showReadOnly}
schema={this.mediaModel.schema}
/>
</MiddlePanel>
<DarkRightPanel>
<MediaSamplesWrap>
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
</MediaSamplesWrap>
</DarkRightPanel>
</Row>
</Section>
);
}
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
}
const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 4}px;
margin: 0;
}
& > div > pre {
padding: 0;
}
`;

View File

@ -57,16 +57,13 @@ export class MenuItem extends React.Component<MenuItemProps> {
{item.name}
{this.props.children}
</MenuItemTitle>
{(item.depth > 0 &&
item.items.length > 0 && (
{(item.depth > 0 && item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) ||
null}
</MenuItemLabel>
)}
{!withoutChildren &&
item.items &&
item.items.length > 0 && (
{!withoutChildren && item.items && item.items.length > 0 && (
<MenuItems
expanded={item.expanded}
items={item.items}
@ -83,7 +80,7 @@ export interface OperationMenuItemContentProps {
}
@observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
render() {
const { item } = this.props;
return (

View File

@ -10,8 +10,13 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore';
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
import {
SCHEMA_DEFINITION_JSX_NAME,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
export interface StoreState {
menu: {
@ -151,5 +156,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
securitySchemes: store.spec.securitySchemes,
}),
},
[SECURITY_DEFINITIONS_JSX_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
[SCHEMA_DEFINITION_JSX_NAME]: {
component: SchemaDefinition,
propsSelector: (store: AppStore) => ({
parser: store.spec.parser,
options: store.options,
}),
},
},
};

View File

@ -1,6 +1,6 @@
import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils';
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -65,6 +65,7 @@ export class MarkdownRenderer {
container: MarkdownHeading[] = this.headings,
parentId?: string,
): MarkdownHeading {
name = unescapeHTMLChars(name);
const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name,
@ -88,7 +89,7 @@ export class MarkdownRenderer {
}
attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => {
const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
};

View File

@ -42,7 +42,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@ -59,14 +59,16 @@ export class MenuBuilder {
*/
static addMarkdownItems(
description: string,
parent: GroupModel | undefined,
initialDepth: number,
options: RedocNormalizedOptions,
): ContentItemModel[] {
const renderer = new MarkdownRenderer(options);
const headings = renderer.extractHeadings(description || '');
const mapHeadingsDeep = (parent, items, depth = 1) =>
const mapHeadingsDeep = (_parent, items, depth = 1) =>
items.map(heading => {
const group = new GroupModel('section', heading, parent);
const group = new GroupModel('section', heading, _parent);
group.depth = depth;
if (heading.items) {
group.items = mapHeadingsDeep(group, heading.items, depth + 1);
@ -82,7 +84,7 @@ export class MenuBuilder {
return group;
});
return mapHeadingsDeep(undefined, headings);
return mapHeadingsDeep(parent, headings, initialDepth);
}
/**
@ -144,15 +146,22 @@ export class MenuBuilder {
}
const item = new GroupModel('tag', tag, parent);
item.depth = GROUP_DEPTH + 1;
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
// don't put empty tag into content, instead put its operations
if (tag.name === '') {
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
const items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
];
res.push(...items);
continue;
}
item.items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
];
res.push(item);
}
return res;

View File

@ -23,12 +23,14 @@ export interface RedocRawOptions {
showExtensions?: boolean | string | string[];
hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string;
}
function argValueToBoolean(val?: string | boolean): boolean {
@ -111,6 +113,16 @@ export class RedocNormalizedOptions {
return value;
}
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
if (level === 'all') {
return +Infinity;
}
if (!isNaN(Number(level))) {
return Math.ceil(Number(level));
}
return 2;
}
theme: ResolvedThemeInterface;
scrollYOffset: () => number;
hideHostname: boolean;
@ -127,6 +139,8 @@ export class RedocNormalizedOptions {
showExtensions: boolean | string[];
hideSingleRequestSampleTab: boolean;
menuToggle: boolean;
jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
/* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean;
@ -159,6 +173,10 @@ export class RedocNormalizedOptions {
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel,
);
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);

View File

@ -40,7 +40,14 @@ export class GroupModel implements IMenuItem {
this.type = type;
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
this.level = (tagOrGroup as MarkdownHeading).level || 1;
// remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos);
}
this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;

View File

@ -13,6 +13,7 @@ import {
import { OpenAPIParser } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => {
describe('openapi getStatusCode', () => {
@ -297,11 +298,8 @@ describe('Utils', () => {
it('should expand variables', () => {
const servers = normalizeServers('', [
{
url: '{protocol}{host}{basePath}',
url: 'http://{host}{basePath}',
variables: {
protocol: {
default: 'http://',
},
host: {
default: '127.0.0.1',
},
@ -319,9 +317,15 @@ describe('Utils', () => {
},
]);
expect(servers[0].url).toEqual('http://127.0.0.1/path/to/endpoint');
expect(servers[1].url).toEqual('http://127.0.0.2:{port}');
expect(servers[2].url).toEqual('http://127.0.0.3');
expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual(
'http://127.0.0.1/path/to/endpoint',
);
expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual(
'http://127.0.0.2:{port}',
);
expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual(
'http://127.0.0.3',
);
});
});

View File

@ -194,3 +194,7 @@ function parseURL(url: string) {
return new URL(url);
}
}
export function unescapeHTMLChars(str: string): string {
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
}

View File

@ -1,11 +1,10 @@
let level = 1;
const COLLAPSE_LEVEL = 2;
export function jsonToHTML(json) {
export function jsonToHTML(json, maxExpandLevel) {
level = 1;
let output = '';
output += '<div class="redoc-json">';
output += valueToHTML(json);
output += valueToHTML(json, maxExpandLevel);
output += '</div>';
return output;
}
@ -33,20 +32,20 @@ function punctuation(val) {
return '<span class="token punctuation">' + val + '</span>';
}
function valueToHTML(value) {
function valueToHTML(value, maxExpandLevel: number) {
const valueType = typeof value;
let output = '';
if (value === undefined || value === null) {
output += decorateWithSpan('null', 'token keyword');
} else if (value && value.constructor === Array) {
level++;
output += arrayToHTML(value);
output += arrayToHTML(value, maxExpandLevel);
level--;
} else if (value && value.constructor === Date) {
output += decorateWithSpan('"' + value.toISOString() + '"', 'token string');
} else if (valueType === 'object') {
level++;
output += objectToHTML(value);
output += objectToHTML(value, maxExpandLevel);
level--;
} else if (valueType === 'number') {
output += decorateWithSpan(value, 'token number');
@ -70,8 +69,8 @@ function valueToHTML(value) {
return output;
}
function arrayToHTML(json) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : '';
function arrayToHTML(json, maxExpandLevel: number) {
const collapsed = level > maxExpandLevel ? 'collapsed' : '';
let output = `<div class="collapser"></div>${punctuation(
'[',
)}<span class="ellipsis"></span><ul class="array collapsible">`;
@ -80,7 +79,7 @@ function arrayToHTML(json) {
for (let i = 0; i < length; i++) {
hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">';
output += valueToHTML(json[i]);
output += valueToHTML(json[i], maxExpandLevel);
if (i < length - 1) {
output += ',';
}
@ -93,8 +92,8 @@ function arrayToHTML(json) {
return output;
}
function objectToHTML(json) {
const collapsed = level > COLLAPSE_LEVEL ? 'collapsed' : '';
function objectToHTML(json, maxExpandLevel: number) {
const collapsed = level > maxExpandLevel ? 'collapsed' : '';
const keys = Object.keys(json);
const length = keys.length;
let output = `<div class="collapser"></div>${punctuation(
@ -106,7 +105,7 @@ function objectToHTML(json) {
hasContents = true;
output += '<li><div class="hoverable ' + collapsed + '">';
output += '<span class="property token string">"' + htmlEncode(key) + '"</span>: ';
output += valueToHTML(json[key]);
output += valueToHTML(json[key], maxExpandLevel);
if (i < length - 1) {
output += punctuation(',');
}

View File

@ -453,7 +453,7 @@ export function mergeSimilarMediaTypes(types: Dict<OpenAPIMediaType>): Dict<Open
return mergedTypes;
}
function expandVariables(url: string, variables: object = {}) {
export function expandDefaultServerVariables(url: string, variables: object = {}) {
return url.replace(
/(?:{)(\w+)(?:})/g,
(match, name) => (variables[name] && variables[name].default) || match,
@ -482,21 +482,23 @@ export function normalizeServers(
];
}
function normalizeUrl(url: string, variables: object | undefined): string {
url = expandVariables(url, variables);
function normalizeUrl(url: string): string {
return resolveUrl(baseUrl, url);
}
return servers.map(server => {
return {
...server,
url: normalizeUrl(server.url, server.variables),
url: normalizeUrl(server.url),
description: server.description || '',
};
});
}
export const SECURITY_DEFINITIONS_COMPONENT_NAME = 'security-definitions';
export const SECURITY_DEFINITIONS_JSX_NAME = 'SecurityDefinitions';
export const SCHEMA_DEFINITION_JSX_NAME = 'ObjectDescription';
export let SECURITY_SCHEMES_SECTION_PREFIX = 'section/Authentication/';
export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix;