mirror of
https://github.com/Redocly/redoc.git
synced 2025-01-31 01:54:08 +03:00
Make ReDoc render sync + separate standalone componenet
- refactored usage of store so now <Redoc> is sync - other minor refactors
This commit is contained in:
parent
0061a87496
commit
109d135959
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": false
|
||||
"editor.formatOnSave": true
|
||||
}
|
|
@ -71,14 +71,12 @@
|
|||
"preact": "^8.2.5",
|
||||
"preact-compat": "^3.17.0",
|
||||
"prismjs": "^1.8.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-dropdown": "^1.3.0",
|
||||
"react-markdown": "^2.5.0",
|
||||
"react-perfect-scrollbar": "^0.2.2",
|
||||
"react-tabs": "^2.0.0",
|
||||
"recompose": "^0.25.1",
|
||||
"remarkable": "^1.7.1",
|
||||
"slugify": "^1.2.1",
|
||||
"styled-components": "^2.2.1"
|
||||
|
|
|
@ -11,3 +11,12 @@ export const RightPanel = styled.div`
|
|||
bckground-color: ${props => props.theme.rightPanel.backgroundColor};
|
||||
padding: ${props => props.theme.spacingUnit * 2}px;
|
||||
`;
|
||||
|
||||
export const DarkRightPanel = styled(RightPanel)`
|
||||
background-color: ${props => props.theme.rightPanel.backgroundColor};
|
||||
`;
|
||||
|
||||
export const Row = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`;
|
||||
|
|
|
@ -7,8 +7,9 @@ import { OpenAPIExternalDocumentation } from '../../types';
|
|||
import { ApiInfoModel } from '../../services/models';
|
||||
import { SecurityDefs } from '../SecurityDefs/SecurityDefs';
|
||||
|
||||
import { MiddlePanel, DarkRightPanel, Row } from '../../common-elements/';
|
||||
|
||||
import {
|
||||
ApiInfoWrap,
|
||||
ApiHeader,
|
||||
DownloadButton,
|
||||
InfoSpan,
|
||||
|
@ -18,7 +19,7 @@ import {
|
|||
|
||||
interface ApiInfoProps {
|
||||
info: ApiInfoModel;
|
||||
externalDocs: OpenAPIExternalDocumentation;
|
||||
externalDocs?: OpenAPIExternalDocumentation;
|
||||
}
|
||||
|
||||
@observer
|
||||
|
@ -65,7 +66,8 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
null;
|
||||
|
||||
return (
|
||||
<ApiInfoWrap className="api-info">
|
||||
<Row>
|
||||
<MiddlePanel className="api-info">
|
||||
<ApiHeader>
|
||||
{info.title} <span>({info.version})</span>
|
||||
</ApiHeader>
|
||||
|
@ -101,7 +103,9 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
components={{ 'security-definitions': SecurityDefs }}
|
||||
/>
|
||||
</div>
|
||||
</ApiInfoWrap>
|
||||
</MiddlePanel>
|
||||
<DarkRightPanel />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { getContext } from 'recompose';
|
||||
|
||||
import { BaseContainerProps } from '../../types/components';
|
||||
import { ApiInfo } from './ApiInfo';
|
||||
|
||||
@observer
|
||||
export class ApiInfoContainer extends React.Component<BaseContainerProps> {
|
||||
render() {
|
||||
const { info, externalDocs } = this.props.store.spec;
|
||||
return <ApiInfo info={info!} externalDocs={externalDocs!} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default getContext<BaseContainerProps>({
|
||||
store: PropTypes.object,
|
||||
})(ApiInfoContainer);
|
|
@ -1,18 +1,14 @@
|
|||
import { OpenAPIInfo } from '../../types';
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { getContext } from 'recompose';
|
||||
|
||||
import { BaseContainerProps } from '../../types/components';
|
||||
import { LogoImgEl } from './styled.elements';
|
||||
|
||||
const LinkWrap = url => Component => <a href={url}>{Component}</a>;
|
||||
|
||||
@observer
|
||||
class ApiLogo extends React.Component<BaseContainerProps> {
|
||||
export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
|
||||
render() {
|
||||
const { spec } = this.props.store;
|
||||
const info = spec.info!;
|
||||
const { info } = this.props;
|
||||
const logoInfo = info['x-logo'];
|
||||
if (!logoInfo || !logoInfo.url) return null;
|
||||
|
||||
|
@ -22,7 +18,3 @@ class ApiLogo extends React.Component<BaseContainerProps> {
|
|||
return info.contact && info.contact.url ? LinkWrap(info.contact.url)(logo) : logo;
|
||||
}
|
||||
}
|
||||
|
||||
export default getContext<BaseContainerProps>({
|
||||
store: PropTypes.object,
|
||||
})(ApiLogo);
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { getContext } from 'recompose';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { BaseContainerProps } from '../../types/components';
|
||||
import { ContentItems } from './ContentItems';
|
||||
|
||||
@observer
|
||||
export class ContentContainer extends React.Component<BaseContainerProps> {
|
||||
render() {
|
||||
const items = this.props.store.menu.items;
|
||||
return <ContentItems items={items as any} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default getContext<BaseContainerProps>({
|
||||
store: PropTypes.object,
|
||||
})(ContentContainer);
|
|
@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
|
|||
import { SECTION_ATTR } from '../../services/MenuStore';
|
||||
import { Markdown } from '../Markdown/Markdown';
|
||||
|
||||
import { H1, MiddlePanel, ShareLink } from '../../common-elements';
|
||||
import { DarkRightPanel, H1, MiddlePanel, ShareLink, Row } from '../../common-elements';
|
||||
import { Operation } from '../Operation/Operation';
|
||||
import { ContentItemModel } from '../../services/MenuBuilder';
|
||||
import { OperationModel } from '../../services/models';
|
||||
|
@ -60,13 +60,16 @@ export class TagItem extends React.Component<ContentItemProps> {
|
|||
render() {
|
||||
const { name, description } = this.props.item;
|
||||
return (
|
||||
<MiddlePanel>
|
||||
<Row>
|
||||
<MiddlePanel key="middle">
|
||||
<H1>
|
||||
<ShareLink href={'#' + this.props.item.getHash()} />
|
||||
{name}
|
||||
</H1>
|
||||
{description !== undefined && <Markdown source={description} />}
|
||||
</MiddlePanel>
|
||||
<DarkRightPanel key="right" />
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import ApiInfoContainer from '../ApiInfo/ApiInfoContainer';
|
||||
import { RedocWrap, MenuContent, ApiContent, Background } from './elements';
|
||||
import ApiLogo from '../ApiLogo/ApiLogo';
|
||||
import SideMenu from '../SideMenu/SideMenu';
|
||||
import ContentContainer from '../ContentItems/ContentContainer';
|
||||
|
||||
export class ContentRoot extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<RedocWrap className="redoc-wrap">
|
||||
<MenuContent className="menu-content">
|
||||
<ApiLogo />
|
||||
<SideMenu />
|
||||
</MenuContent>
|
||||
<Background className="background-wrap">
|
||||
<div className="redoc-background" />
|
||||
</Background>
|
||||
<ApiContent className="api-content">
|
||||
<ApiInfoContainer />
|
||||
<ContentContainer />
|
||||
</ApiContent>
|
||||
</RedocWrap>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
import * as PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { Children } from 'react';
|
||||
import { getContext } from 'recompose';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from '../../styled-components';
|
||||
|
||||
import { AppStore } from '../../services';
|
||||
import { Spinner } from './Spinner.svg';
|
||||
|
||||
const LoadingMessage = styled.div`
|
||||
|
@ -17,10 +13,9 @@ const LoadingMessage = styled.div`
|
|||
color: ${props => props.theme.colors.main};
|
||||
`;
|
||||
|
||||
@observer
|
||||
class LoadingWrap extends React.Component<{ store: AppStore }> {
|
||||
export class LoadingWrap extends React.Component<{ loading: boolean }> {
|
||||
render() {
|
||||
if (this.props.store.spec.loaded) {
|
||||
if (this.props.loading) {
|
||||
return Children.only(this.props.children);
|
||||
}
|
||||
return (
|
||||
|
@ -31,7 +26,3 @@ class LoadingWrap extends React.Component<{ store: AppStore }> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default getContext<{ store: AppStore }>({
|
||||
store: PropTypes.object,
|
||||
})(LoadingWrap);
|
||||
|
|
|
@ -3,7 +3,7 @@ import styled from '../../styled-components';
|
|||
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { H2, MiddlePanel, RightPanel, Badge } from '../../common-elements';
|
||||
import { H2, MiddlePanel, DarkRightPanel, Badge, Row } from '../../common-elements';
|
||||
|
||||
import { Markdown } from '../Markdown/Markdown';
|
||||
import { Parameters } from '../Parameters/Parameters';
|
||||
|
@ -15,14 +15,19 @@ import { Endpoint } from '../Endpoint/Endpoint';
|
|||
|
||||
import { OperationModel as OperationType } from '../../services/models';
|
||||
|
||||
const OperationRow = styled.div`
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 30px;
|
||||
const OperationRow = styled(Row)`
|
||||
transform: translateZ(0);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
positioin: relative;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: block;
|
||||
content: '';
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
interface OperationProps {
|
||||
|
@ -47,11 +52,11 @@ export class Operation extends React.Component<OperationProps> {
|
|||
<Parameters parameters={operation.parameters} body={operation.requestBody} />
|
||||
<ResponsesList responses={operation.responses} />
|
||||
</MiddlePanel>
|
||||
<RightPanel>
|
||||
<DarkRightPanel>
|
||||
<Endpoint operation={operation} />
|
||||
<RequestSamples operation={operation} />
|
||||
<ResponseSamples operation={operation} />
|
||||
</RightPanel>
|
||||
</DarkRightPanel>
|
||||
</OperationRow>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { ContentRoot } from './ContentRoot/ContentRoot';
|
||||
|
||||
import { ThemeProvider } from '../styled-components';
|
||||
import defaultTheme from '../theme';
|
||||
|
||||
import LoadingWrap from './LoadingWrap/LoadingWrap';
|
||||
import { StoreProvider } from './StoreProvider';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
export interface RedocProps {
|
||||
specUrl?: string;
|
||||
spec?: object;
|
||||
theme?: any;
|
||||
store?: any;
|
||||
}
|
||||
|
||||
export class Redoc extends React.Component<RedocProps> {
|
||||
render() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<StoreProvider {...this.props}>
|
||||
<ThemeProvider theme={this.props.theme || defaultTheme}>
|
||||
<LoadingWrap>
|
||||
<ContentRoot />
|
||||
</LoadingWrap>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
44
src/components/Redoc/Redoc.tsx
Normal file
44
src/components/Redoc/Redoc.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { ThemeInterface } from '../../theme';
|
||||
import * as React from 'react';
|
||||
import { ThemeProvider } from '../../styled-components';
|
||||
|
||||
import { ApiInfo } from '../ApiInfo/ApiInfo';
|
||||
import { RedocWrap, MenuContent, ApiContent } from './elements';
|
||||
import { ApiLogo } from '../ApiLogo/ApiLogo';
|
||||
import { SideMenu } from '../SideMenu/SideMenu';
|
||||
import { ContentItems } from '../ContentItems/ContentItems';
|
||||
import { AppStore } from '../../services';
|
||||
|
||||
import defaultTheme from '../../theme';
|
||||
|
||||
interface RedocProps {
|
||||
store: AppStore;
|
||||
options?: {
|
||||
theme?: ThemeInterface;
|
||||
};
|
||||
}
|
||||
|
||||
export class Redoc extends React.Component<RedocProps> {
|
||||
componentDidMount() {
|
||||
this.props.store.menu.updateOnHash();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { store: { spec, menu }, options = {} } = this.props;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={options.theme || defaultTheme}>
|
||||
<RedocWrap className="redoc-wrap">
|
||||
<MenuContent className="menu-content">
|
||||
<ApiLogo info={spec.info} />
|
||||
<SideMenu menu={menu} />
|
||||
</MenuContent>
|
||||
<ApiContent className="api-content">
|
||||
<ApiInfo info={spec.info} externalDocs={spec.externalDocs} />
|
||||
<ContentItems items={menu.items as any} />;
|
||||
</ApiContent>
|
||||
</RedocWrap>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -49,21 +49,3 @@ export const ApiContent = styled.div`
|
|||
z-index: 10;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const Background = styled.div`
|
||||
position: absolute;
|
||||
left: ${props => props.theme.menu.width};
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
|
||||
.redoc-background {
|
||||
background-color: ${props => props.theme.rightPanel.backgroundColor};
|
||||
left: ${props => 100 - props.theme.rightPanel.width}%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
`;
|
41
src/components/RedocStandalone.tsx
Normal file
41
src/components/RedocStandalone.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { ThemeInterface } from '../theme';
|
||||
|
||||
import { LoadingWrap } from './LoadingWrap/LoadingWrap';
|
||||
import { StoreProvider } from './StoreProvider';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { Redoc } from './Redoc/Redoc';
|
||||
|
||||
export interface RedocProps {
|
||||
specOrSpecUrl: string | object;
|
||||
options?: {
|
||||
theme?: ThemeInterface;
|
||||
};
|
||||
}
|
||||
|
||||
export class RedocStandalone extends React.Component<RedocProps> {
|
||||
render() {
|
||||
const { specOrSpecUrl, options } = this.props;
|
||||
let specUrl;
|
||||
let spec;
|
||||
|
||||
if (typeof specOrSpecUrl === 'string') {
|
||||
specUrl = specOrSpecUrl;
|
||||
} else if (typeof specOrSpecUrl === 'object') {
|
||||
spec = specOrSpecUrl;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<StoreProvider spec={spec} specUrl={specUrl}>
|
||||
{({ loading, store }) => (
|
||||
<LoadingWrap loading={loading}>
|
||||
<Redoc store={store} options={options} />
|
||||
</LoadingWrap>
|
||||
)}
|
||||
</StoreProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,15 @@
|
|||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { getContext } from 'recompose';
|
||||
|
||||
import { BaseContainerProps } from '../../types/components';
|
||||
import { IMenuItem } from '../../services/MenuStore';
|
||||
|
||||
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
|
||||
import { MenuStore, IMenuItem } from '../../services/MenuStore';
|
||||
import { MenuItems } from './MenuItems';
|
||||
|
||||
import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar';
|
||||
|
||||
@observer
|
||||
class SideMenu extends React.Component<BaseContainerProps> {
|
||||
export class SideMenu extends React.Component<{ menu: MenuStore }> {
|
||||
render() {
|
||||
const store = this.props.store.menu;
|
||||
const store = this.props.menu;
|
||||
return (
|
||||
<PerfectScrollbar>
|
||||
<MenuItems items={store.items} onActivate={this.activate} />
|
||||
|
@ -21,10 +18,6 @@ class SideMenu extends React.Component<BaseContainerProps> {
|
|||
}
|
||||
|
||||
activate = (item: IMenuItem) => {
|
||||
this.props.store.menu.activateAndScroll(item, true);
|
||||
this.props.menu.activateAndScroll(item, true);
|
||||
};
|
||||
}
|
||||
|
||||
export default getContext<BaseContainerProps>({
|
||||
store: PropTypes.object,
|
||||
})(SideMenu);
|
||||
|
|
|
@ -1,34 +1,51 @@
|
|||
import { Component, Children } from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { AppStore, HistoryService } from '../services/';
|
||||
import { Component } from 'react';
|
||||
import { AppStore } from '../services/';
|
||||
import { loadSpec } from '../utils';
|
||||
|
||||
interface SpecProps {
|
||||
specUrl?: string;
|
||||
spec?: object;
|
||||
store?: AppStore;
|
||||
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export class StoreProvider extends Component<SpecProps, { error?: Error }> {
|
||||
store: AppStore;
|
||||
interface SpecState {
|
||||
error?: Error;
|
||||
loading: boolean;
|
||||
store?: AppStore;
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
};
|
||||
export class StoreProvider extends Component<SpecProps, SpecState> {
|
||||
store: AppStore;
|
||||
|
||||
constructor(props: SpecProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
||||
this.store = props.store || new AppStore();
|
||||
this.state = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
if (!this.store.spec.loaded) {
|
||||
this.store.spec
|
||||
.load(props.spec! || props.specUrl)
|
||||
.then(() => {
|
||||
HistoryService.emit();
|
||||
this.setError();
|
||||
})
|
||||
.catch(e => this.setError(e));
|
||||
this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
let { specUrl, spec } = this.props;
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const resolvedSpec = await loadSpec(spec || specUrl!);
|
||||
this.setState({
|
||||
loading: false,
|
||||
store: new AppStore(resolvedSpec, specUrl),
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,12 +55,8 @@ export class StoreProvider extends Component<SpecProps, { error?: Error }> {
|
|||
});
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return { store: this.props.store || this.store };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) throw this.state.error;
|
||||
return Children.only(this.props.children);
|
||||
return this.props.children(this.state);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
export * from './Redoc';
|
||||
export * from './RedocStandalone';
|
||||
export * from './Redoc/Redoc';
|
||||
export * from './Redoc/elements';
|
||||
export * from './Schema/';
|
||||
export * from './Operation/Operation';
|
||||
// re-export the rest of components
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
// import DevTools from 'mobx-react-devtools';
|
||||
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
import { Redoc, RedocProps } from './components/Redoc';
|
||||
import { Redoc } from './components/Redoc/Redoc';
|
||||
import { AppStore } from './services/AppStore';
|
||||
import { loadSpec } from './utils/loadSpec';
|
||||
|
||||
const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
|
||||
const renderRoot = (Component: typeof Redoc, props: { store: AppStore }) =>
|
||||
render(
|
||||
<div>
|
||||
<AppContainer>
|
||||
|
@ -17,26 +18,32 @@ const renderRoot = (Component: typeof Redoc, props: RedocProps) =>
|
|||
);
|
||||
|
||||
const big = window.location.search.indexOf('big') > -1;
|
||||
const props = {
|
||||
specUrl: big ? 'big-swagger.json' : 'swagger.yaml',
|
||||
store: new AppStore(),
|
||||
};
|
||||
|
||||
renderRoot(Redoc, props);
|
||||
const specUrl = big ? 'big-swagger.json' : 'swagger.yaml';
|
||||
|
||||
let store;
|
||||
|
||||
async function init() {
|
||||
const spec = await loadSpec(specUrl);
|
||||
store = new AppStore(spec, specUrl);
|
||||
renderRoot(Redoc, { store: store });
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
if (module.hot) {
|
||||
const reload = (reloadStore = false) => () => {
|
||||
if (reloadStore) {
|
||||
// create a new Store
|
||||
props.store.dispose();
|
||||
store.dispose();
|
||||
|
||||
const state = props.store.toJS();
|
||||
props.store = AppStore.fromJS(state);
|
||||
const state = store.toJS();
|
||||
store = AppStore.fromJS(state);
|
||||
}
|
||||
|
||||
renderRoot(Redoc, props);
|
||||
renderRoot(Redoc, { store: store });
|
||||
};
|
||||
|
||||
module.hot.accept(['./components/Redoc'], reload());
|
||||
module.hot.accept(['./components/Redoc/Redoc'], reload());
|
||||
module.hot.accept(['./services/AppStore'], reload(true));
|
||||
}
|
||||
|
|
18
src/index.ts
18
src/index.ts
|
@ -1,2 +1,20 @@
|
|||
import { render } from 'react-dom';
|
||||
import * as React from 'react';
|
||||
|
||||
import { RedocStandalone } from './components/Redoc';
|
||||
|
||||
export * from './components';
|
||||
export * from './services';
|
||||
|
||||
export function init(specOrSpecUrl: string | any, options?: any, element?: Element) {
|
||||
render(
|
||||
React.createElement(
|
||||
RedocStandalone,
|
||||
{
|
||||
specOrSpecUrl,
|
||||
},
|
||||
[],
|
||||
),
|
||||
element || document.querySelector('redoc'),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import { OpenAPISpec } from '../types';
|
||||
import { SpecStore } from './models';
|
||||
import { MenuStore } from './MenuStore';
|
||||
import { ScrollService } from './ScrollService';
|
||||
|
||||
type StoreData = {
|
||||
menu: {
|
||||
activeItemIdx: number;
|
||||
};
|
||||
spec: {
|
||||
url: string;
|
||||
data: any;
|
||||
};
|
||||
};
|
||||
|
||||
export class AppStore {
|
||||
menu: MenuStore;
|
||||
scroll: ScrollService;
|
||||
spec: SpecStore;
|
||||
static i = 25;
|
||||
|
||||
// TODO: store serialization ???
|
||||
private scroll: ScrollService;
|
||||
|
||||
constructor() {
|
||||
constructor(spec: OpenAPISpec, specUrl?: string) {
|
||||
this.scroll = new ScrollService();
|
||||
this.spec = new SpecStore();
|
||||
this.spec = new SpecStore(spec, specUrl);
|
||||
this.menu = new MenuStore(this.spec, this.scroll);
|
||||
}
|
||||
|
||||
|
@ -26,16 +35,14 @@ export class AppStore {
|
|||
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
|
||||
*/
|
||||
// TODO:
|
||||
toJS() {
|
||||
toJS(): StoreData {
|
||||
return {
|
||||
menu: {
|
||||
activeMenuIdx: this.menu.activeItemIdx,
|
||||
activeItemIdx: this.menu.activeItemIdx,
|
||||
},
|
||||
spec: {
|
||||
parser: {
|
||||
specUrl: this.spec.parser.specUrl,
|
||||
spec: this.spec.parser.spec,
|
||||
},
|
||||
url: this.spec.parser.specUrl,
|
||||
data: this.spec.parser.spec,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -44,10 +51,8 @@ export class AppStore {
|
|||
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
|
||||
*/
|
||||
// TODO:
|
||||
static fromJS(state): AppStore {
|
||||
const inst = new AppStore();
|
||||
inst.spec.parser.specUrl = state.spec.parser.specUrl;
|
||||
inst.spec.parser.spec = state.spec.parser.spec;
|
||||
static fromJS(state: StoreData): AppStore {
|
||||
const inst = new AppStore(state.spec.data, state.spec.url);
|
||||
inst.menu.activeItemIdx = state.menu.activeItemIdx || 0;
|
||||
inst.menu.activate(inst.menu.flatItems[inst.menu.activeItemIdx]);
|
||||
return inst;
|
||||
|
|
|
@ -30,7 +30,7 @@ export class MenuBuilder {
|
|||
* Builds page content structure based on tags
|
||||
*/
|
||||
static buildStructure(parser: OpenAPIParser): ContentItemModel[] {
|
||||
const spec = parser.spec!;
|
||||
const spec = parser.spec;
|
||||
|
||||
const items: ContentItemModel[] = [];
|
||||
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
|
||||
|
|
|
@ -102,7 +102,7 @@ export class MenuStore {
|
|||
* @param hash current hash
|
||||
*/
|
||||
@action.bound
|
||||
updateOnHash(hash: string): boolean {
|
||||
updateOnHash(hash: string = HistoryService.hash): boolean {
|
||||
if (!hash) return false;
|
||||
let item: IMenuItem | undefined;
|
||||
hash = hash.substr(1);
|
||||
|
@ -155,9 +155,8 @@ export class MenuStore {
|
|||
get flatItems(): IMenuItem[] {
|
||||
if (!this._flatItems) {
|
||||
this._flatItems = flattenByProp(this.items, 'items');
|
||||
}
|
||||
|
||||
this._flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
|
||||
}
|
||||
|
||||
return this._flatItems;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { action, computed, observable } from 'mobx';
|
||||
import * as JsonSchemaRefParser from 'json-schema-ref-parser';
|
||||
import { observable } from 'mobx';
|
||||
import { resolve as urlResolve } from 'url';
|
||||
|
||||
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
|
||||
|
@ -38,44 +37,21 @@ class RefCounter {
|
|||
*/
|
||||
export class OpenAPIParser {
|
||||
@observable specUrl: string;
|
||||
@observable.ref spec?: OpenAPISpec;
|
||||
|
||||
private _parser: JsonSchemaRefParser;
|
||||
private _refCounter: RefCounter = new RefCounter();
|
||||
|
||||
@computed
|
||||
get loaded(): boolean {
|
||||
return this.spec !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* loads and bundles the spec via url to spec or by providing spec itself.
|
||||
* Async as bundling is async as spec may contain extrenal refs.
|
||||
* @param urlOrObject url to the spec or the spec itself
|
||||
*/
|
||||
@action
|
||||
async load(urlOrObject: string | object): Promise<OpenAPISpec> {
|
||||
if (this.loaded) {
|
||||
return this.spec!;
|
||||
}
|
||||
|
||||
this._parser = new JsonSchemaRefParser();
|
||||
if (typeof urlOrObject === 'string') {
|
||||
this.specUrl = urlResolve(window.location.href, urlOrObject);
|
||||
} else {
|
||||
this.specUrl = window.location.href;
|
||||
}
|
||||
|
||||
const spec = await this._parser.bundle(urlOrObject, {
|
||||
resolve: { http: { withCredentials: false } },
|
||||
} as object);
|
||||
@observable.ref spec: OpenAPISpec;
|
||||
|
||||
constructor(spec: OpenAPISpec, specUrl?: string) {
|
||||
this.validate(spec);
|
||||
|
||||
this.spec = spec;
|
||||
|
||||
return this.spec!;
|
||||
if (typeof specUrl === 'string') {
|
||||
this.specUrl = urlResolve(window.location.href, specUrl);
|
||||
} else {
|
||||
this.specUrl = window.location.href;
|
||||
}
|
||||
}
|
||||
|
||||
private _refCounter: RefCounter = new RefCounter();
|
||||
|
||||
validate(spec: any) {
|
||||
// TODO: validate
|
||||
|
@ -90,17 +66,12 @@ export class OpenAPIParser {
|
|||
byRef = <T extends any = any>(ref: string): T | undefined => {
|
||||
let res;
|
||||
if (this.spec === undefined) return;
|
||||
if (ref.charAt(0) !== '#') ref = '#' + ref;
|
||||
try {
|
||||
res = JsonPointer.get(this.spec, decodeURIComponent(ref));
|
||||
} catch (e) {
|
||||
// if resolved from outer files simple jsonpointer.get fails to get correct schema
|
||||
if (ref.charAt(0) !== '#') ref = '#' + ref;
|
||||
try {
|
||||
res = this._parser.$refs.get(decodeURIComponent(ref));
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
|
@ -227,7 +198,7 @@ export class OpenAPIParser {
|
|||
*/
|
||||
findDerived($refs: string[]): Dict<string> {
|
||||
const res: Dict<string> = {};
|
||||
const schemas = (this.spec!.components && this.spec!.components!.schemas) || {};
|
||||
const schemas = (this.spec.components && this.spec.components.schemas) || {};
|
||||
for (let defName in schemas) {
|
||||
const def = this.deref(schemas[defName]);
|
||||
if (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { OpenAPISpec } from '../types';
|
||||
import { observable, computed } from 'mobx';
|
||||
|
||||
// import { OpenAPIExternalDocumentation, OpenAPIInfo } from '../types';
|
||||
|
@ -12,34 +13,22 @@ import { ApiInfoModel } from './models/ApiInfo';
|
|||
export class SpecStore {
|
||||
@observable.ref parser: OpenAPIParser;
|
||||
|
||||
constructor() {
|
||||
this.parser = new OpenAPIParser();
|
||||
}
|
||||
|
||||
load(specOrUrl: string | object) {
|
||||
return this.parser.load(specOrUrl);
|
||||
constructor(spec: OpenAPISpec, specUrl?: string) {
|
||||
this.parser = new OpenAPIParser(spec, specUrl);
|
||||
}
|
||||
|
||||
@computed
|
||||
get loaded() {
|
||||
return this.parser.loaded;
|
||||
}
|
||||
|
||||
@computed
|
||||
get info() {
|
||||
if (!this.parser.loaded) return;
|
||||
get info(): ApiInfoModel {
|
||||
return new ApiInfoModel(this.parser);
|
||||
}
|
||||
|
||||
@computed
|
||||
get externalDocs() {
|
||||
if (this.parser.loaded) return;
|
||||
return this.parser.spec!.externalDocs;
|
||||
return this.parser.spec.externalDocs;
|
||||
}
|
||||
|
||||
@computed
|
||||
get operationGroups() {
|
||||
if (!this.parser.loaded) return [];
|
||||
return MenuBuilder.buildStructure(this.parser);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export class ApiInfoModel implements OpenAPIInfo {
|
|||
license?: OpenAPILicense;
|
||||
|
||||
constructor(public parser: OpenAPIParser) {
|
||||
Object.assign(this, parser.spec!.info);
|
||||
Object.assign(this, parser.spec.info);
|
||||
}
|
||||
|
||||
get downloadLink() {
|
||||
|
|
|
@ -79,13 +79,15 @@ export class OperationModel implements IMenuItem {
|
|||
if (parseInt(code) >= 100 && parseInt(code) <= 399) {
|
||||
hasSuccessResponses = true;
|
||||
}
|
||||
return isNumeric(code) || code === 'default'
|
||||
return isNumeric(code) || code === 'default';
|
||||
}) // filter out other props (e.g. x-props)
|
||||
.map(code => new ResponseModel(parser, code, hasSuccessResponses, operationSpec.responses[code]));
|
||||
.map(
|
||||
code => new ResponseModel(parser, code, hasSuccessResponses, operationSpec.responses[code]),
|
||||
);
|
||||
|
||||
this.servers = normalizeServers(
|
||||
parser.specUrl,
|
||||
operationSpec.servers || parser.spec!.servers || [],
|
||||
operationSpec.servers || parser.spec.servers || [],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from './styled';
|
|||
export * from './openapi';
|
||||
export * from './helpers';
|
||||
export * from './highlight';
|
||||
export * from './loadSpec';
|
||||
|
|
10
src/utils/loadSpec.ts
Normal file
10
src/utils/loadSpec.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { OpenAPISpec } from '../types';
|
||||
|
||||
import * as JsonSchemaRefParser from 'json-schema-ref-parser';
|
||||
|
||||
export async function loadSpec(specUrlOrObject: object | string): Promise<OpenAPISpec> {
|
||||
const _parser = new JsonSchemaRefParser();
|
||||
return await _parser.bundle(specUrlOrObject, {
|
||||
resolve: { http: { withCredentials: false } },
|
||||
} as object);
|
||||
}
|
19
yarn.lock
19
yarn.lock
|
@ -1036,10 +1036,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^4.0.0"
|
||||
|
||||
change-emitter@^0.1.2:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
|
||||
|
||||
cheerio@^1.0.0-rc.2:
|
||||
version "1.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
|
||||
|
@ -2431,7 +2427,7 @@ fb-watchman@^2.0.0:
|
|||
dependencies:
|
||||
bser "^2.0.0"
|
||||
|
||||
fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9:
|
||||
fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9:
|
||||
version "0.8.16"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
|
||||
dependencies:
|
||||
|
@ -5927,15 +5923,6 @@ readdirp@^2.0.0:
|
|||
readable-stream "^2.0.2"
|
||||
set-immediate-shim "^1.0.1"
|
||||
|
||||
recompose@^0.25.1:
|
||||
version "0.25.1"
|
||||
resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.25.1.tgz#5eb9d6cf6e25a9ffad73cbbae5658b5b55d6e728"
|
||||
dependencies:
|
||||
change-emitter "^0.1.2"
|
||||
fbjs "^0.8.1"
|
||||
hoist-non-react-statics "^2.3.1"
|
||||
symbol-observable "^1.0.4"
|
||||
|
||||
recursive-readdir@2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"
|
||||
|
@ -6835,10 +6822,6 @@ svgo@^0.7.0:
|
|||
sax "~1.2.1"
|
||||
whet.extend "~0.9.9"
|
||||
|
||||
symbol-observable@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
|
||||
|
||||
symbol-tree@^3.2.1:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
|
||||
|
|
Loading…
Reference in New Issue
Block a user