Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): make API component resolve inline and remote refs #575

Merged
merged 13 commits into from
Sep 29, 2020
3 changes: 1 addition & 2 deletions packages/elements/src/__stories__/components/Api.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { text, withKnobs } from '@storybook/addon-knobs';
import { boolean } from '@storybook/addon-knobs/react';
import { boolean, text, withKnobs } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import cn from 'classnames';
import * as React from 'react';
Expand Down
12 changes: 6 additions & 6 deletions packages/elements/src/components/API/SidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ import * as React from 'react';

import { useTocContents } from '../../hooks/useTocContents';
import { ILinkComponentProps, ITableOfContentsTree } from '../../types';
import { getNodeType, isOperation, IUriMap } from '../../utils/oas';
import { getNodeType, isOperation } from '../../utils/oas';
import { Docs } from '../Docs';
import { Row } from '../TableOfContents/Row';
import { TryIt } from '../TryIt';
import { TryItHeader } from '../TryIt/header';

type SidebarLayoutProps = {
pathname: string;
uriMap: IUriMap;
tree: ITableOfContentsTree;
bundledNodeData: unknown;
linkComponent?: React.ComponentType<ILinkComponentProps>;
apiDescriptionUrl?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the cleaned up implementation this was made redundant here, we're not using it anywhere. Let's remove it.

};

export const SidebarLayout: React.FC<SidebarLayoutProps> = ({
pathname,
tree,
uriMap,
bundledNodeData,
linkComponent: LinkComponent,
}) => {
const contents = useTocContents(tree).map(item => ({
Expand All @@ -29,7 +30,6 @@ export const SidebarLayout: React.FC<SidebarLayoutProps> = ({
}));

const nodeType = getNodeType(pathname);
const nodeData = uriMap[pathname] || uriMap['/'];
const showTryIt = isOperation(pathname);

return (
Expand All @@ -41,12 +41,12 @@ export const SidebarLayout: React.FC<SidebarLayoutProps> = ({
/>
<div className="flex-grow p-5">
<div className="flex">
<Docs className="px-10" nodeData={nodeData} nodeType={nodeType} />
<Docs className="px-10" nodeData={bundledNodeData} nodeType={nodeType} />
{showTryIt && (
<div className="w-2/5 border-l relative">
<div className="absolute inset-0 overflow-auto px-10">
<TryItHeader />
<TryIt nodeType={nodeType} nodeData={nodeData} />
<TryIt nodeType={nodeType} nodeData={bundledNodeData} />
</div>
</div>
)}
Expand Down
19 changes: 15 additions & 4 deletions packages/elements/src/components/API/StackedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TryIt } from '../TryIt';
type StackedLayoutProps = {
uriMap: IUriMap;
tree: ITableOfContents;
bundledNodeData: unknown;
};

type ItemRowProps = {
Expand All @@ -26,7 +27,7 @@ const itemMatchesHash = (hash: string, item: Pick<ItemRowProps, 'title' | 'type'
return hash.substr(1) === `${item.title}-${item.type}`;
};

export const StackedLayout: React.FC<StackedLayoutProps> = ({ uriMap, tree }) => {
export const StackedLayout: React.FC<StackedLayoutProps> = ({ uriMap, tree, bundledNodeData }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cheating 😛
WebStorm immediately gave me an unused variable warning. And it is right, trying your external ref file: https://raw.githubusercontent.com/mmiask/anothertesting/master/reference/test.v1.yaml with the stacked layout crashes immediately upon opening the Endpoints group.

const groups = tree.items.filter(isGroup);

return (
Expand Down Expand Up @@ -163,11 +164,21 @@ const ItemRow: React.FC<ItemRowProps> = ({ data, nodeType, type, title }) => {
onChange={(tabId: PanelTabId) => setTabId(tabId)}
renderActiveTabPanelOnly
>
<Tab id="docs" title="Docs" className="p-4" panel={<Docs nodeType={nodeType} nodeData={data} headless />} />
<Tab id="tryit" title="Try It" className="p-4" panel={<TryIt nodeType={nodeType} nodeData={data} />} />
<Tab
id="docs"
title="Docs"
className="p-4"
panel={<Docs nodeType={nodeType} nodeData={bundledNodeData} headless />}
/>
<Tab
id="tryit"
title="Try It"
className="p-4"
panel={<TryIt nodeType={nodeType} nodeData={bundledNodeData} />}
/>
</Tabs>
) : (
<Docs className="mx-auto p-4" nodeType={nodeType} nodeData={data} headless />
<Docs className="mx-auto p-4" nodeType={nodeType} nodeData={bundledNodeData} headless />
)}
</Collapse>
</div>
Expand Down
27 changes: 19 additions & 8 deletions packages/elements/src/containers/API.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { SidebarLayout } from '../components/API/SidebarLayout';
import { StackedLayout } from '../components/API/StackedLayout';
import { DocsSkeleton } from '../components/Docs/Skeleton';
import { withRouter } from '../hoc/withRouter';
import { useBundledData } from '../hooks/useBundledData';
import { useParsedValue } from '../hooks/useParsedValue';
import { withStyles } from '../styled';
import { LinkComponentType, RoutingProps } from '../types';
import { computeNodeData, isOas2, isOas3, IUriMap } from '../utils/oas';
import { computeNodeData, getNodeType, isOas2, isOas3, IUriMap } from '../utils/oas';
import { computeOas2UriMap } from '../utils/oas/oas2';
import { computeOas3UriMap } from '../utils/oas/oas3';
import { InlineRefResolverProvider } from './Provider';

const fetcher = (url: string) => axios.get(url).then(res => res.data);

Expand Down Expand Up @@ -54,6 +56,8 @@ const APIImpl = withRouter<APIProps>(function API({ apiDescriptionUrl, linkCompo
const nodes = computeNodeData(uriMap);
const tree = generateToC(nodes);
const nodeData = uriMap[pathname] || uriMap['/'];
const nodeType = getNodeType(pathname);
const bundledNodeData = useBundledData(nodeType, nodeData, { baseUrl: apiDescriptionUrl });

if (error) {
return (
Expand All @@ -72,13 +76,20 @@ const APIImpl = withRouter<APIProps>(function API({ apiDescriptionUrl, linkCompo
}

return (
<div className="APIComponent flex flex-row">
{layout === 'stacked' ? (
<StackedLayout uriMap={uriMap} tree={tree} />
) : (
<SidebarLayout pathname={pathname} uriMap={uriMap} tree={tree} linkComponent={linkComponent} />
)}
</div>
<InlineRefResolverProvider document={document}>
<div className="APIComponent flex flex-row">
{layout === 'stacked' ? (
<StackedLayout uriMap={uriMap} tree={tree} bundledNodeData={bundledNodeData} />
) : (
<SidebarLayout
pathname={pathname}
tree={tree}
bundledNodeData={bundledNodeData}
linkComponent={linkComponent}
/>
)}
</div>
</InlineRefResolverProvider>
);
});

Expand Down
14 changes: 3 additions & 11 deletions packages/elements/src/containers/Docs.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { pointerToPath } from '@stoplight/json';
import { SchemaTreeRefDereferenceFn } from '@stoplight/json-schema-viewer';
import { NodeType } from '@stoplight/types';
import { FAIcon, NonIdealState } from '@stoplight/ui-kit';
import { get, isObject } from 'lodash';
import * as React from 'react';
import { useQuery } from 'urql';

import { DocsSkeleton, ParsedDocs } from '../components/Docs';
import { bundledBranchNode } from '../graphql/BranchNodeBySlug';
import { useParsedData } from '../hooks/useParsedData';
import { ActiveInfoContext, InlineRefResolverContext, IProvider, Provider } from './Provider';
import { ActiveInfoContext, InlineRefResolverProvider, IProvider, Provider } from './Provider';

export interface IDocsProps {
className?: string;
Expand All @@ -23,15 +20,10 @@ interface IDocsProvider extends IProvider {
const DocsPopup = React.memo<{ nodeType: NodeType; nodeData: unknown; className?: string }>(
({ nodeType, nodeData, className }) => {
const document = useParsedData(nodeType, nodeData);
const inlineRefResolver = React.useCallback<SchemaTreeRefDereferenceFn>(
({ pointer }, _, schema) =>
pointer === null ? null : get(isObject(document) ? document : schema, pointerToPath(pointer)),
[document],
);
return (
<InlineRefResolverContext.Provider value={inlineRefResolver}>
<InlineRefResolverProvider document={document}>
<ParsedDocs className={className} nodeType={nodeType} nodeData={document} />
</InlineRefResolverContext.Provider>
</InlineRefResolverProvider>
);
},
);
Expand Down
18 changes: 18 additions & 0 deletions packages/elements/src/containers/Provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { pointerToPath } from '@stoplight/json';
import { SchemaTreeRefDereferenceFn } from '@stoplight/json-schema-viewer';
import { IComponentMapping } from '@stoplight/markdown-viewer';
import { get, isObject } from 'lodash';
import * as React from 'react';
import { Client, Provider as UrqlProvider } from 'urql';

Expand All @@ -23,6 +25,22 @@ export const ComponentsContext = createNamedContext<IComponentMapping | undefine

export const InlineRefResolverContext = React.createContext<SchemaTreeRefDereferenceFn | undefined>(void 0);

interface InlineRefResolverProviderTypes {
document: unknown;
}

/**
* Populates `InlineRefResolverContext` with a standard inline ref resolver based on `document`.
*/
export const InlineRefResolverProvider: React.FC<InlineRefResolverProviderTypes> = ({ document, children }) => {
const inlineRefResolver = React.useCallback<SchemaTreeRefDereferenceFn>(
({ pointer }, _, schema) =>
pointer === null ? null : get(isObject(document) ? document : schema, pointerToPath(pointer)),
[document],
);
return <InlineRefResolverContext.Provider value={inlineRefResolver}>{children}</InlineRefResolverContext.Provider>;
};

const defaultIcons: NodeIconMapping = {};
export const IconsContext = createNamedContext<NodeIconMapping>('IconsContext', defaultIcons);

Expand Down
47 changes: 47 additions & 0 deletions packages/elements/src/hooks/useBundledData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import $RefParser from '@stoplight/json-schema-ref-parser';
import { NodeType } from '@stoplight/types';
import { isObject } from 'lodash';
Copy link
Contributor

@P0lip P0lip Sep 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { isObject } from 'lodash';
import { isObjectLike } from 'lodash';

isObject returns true for functions.

(or isPlainObject)

We can leave it as well too, as I doubt function will ever be provided.

import * as React from 'react';

import { useParsedData } from './useParsedData';

/**
* @param type branch node snapshot type
* @param data branch node snapshot data
*/

interface Options {
baseUrl?: string;
}

export function useBundledData(type: NodeType, data: unknown, options?: Options) {
const parsedData = useParsedData(type, data);

const [bundledData, setBundledData] = React.useState(parsedData);

React.useEffect(() => {
if (!isObject(parsedData) || type !== NodeType.HttpOperation) {
setBundledData(parsedData);
return;
}

doBundle(parsedData, options?.baseUrl)
.then(res => setBundledData(res))
.catch(reason => {
console.error(`Could not bundle: ${reason.message}`);
console.error(reason);
marcelltoth marked this conversation as resolved.
Show resolved Hide resolved
setBundledData(parsedData);
});
}, [parsedData, type, options?.baseUrl]);

return bundledData;
}

const commonBundleOptions = { continueOnError: true };
const doBundle = (data: object, baseUrl?: string) => {
if (!baseUrl) {
return $RefParser.bundle(data, commonBundleOptions);
} else {
return $RefParser.bundle(baseUrl, data, commonBundleOptions);
}
};