Skip to content

Commit

Permalink
[DEV-1040] Add OpenAPI component (#353)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremygordillo authored Jan 31, 2024
1 parent bcbe0b4 commit b2f50b8
Show file tree
Hide file tree
Showing 25 changed files with 2,149 additions and 41 deletions.
6 changes: 6 additions & 0 deletions .changeset/clever-coins-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"nextjs-website": minor
"gitbook-docs": patch
---

[DEV-1040] Rendering component OpenAPI Gitbook
4 changes: 4 additions & 0 deletions apps/nextjs-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"test": "jest -i"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@aws-amplify/auth": "^5.6.6",
"@aws-amplify/ui-react": "^5.3.1",
"@aws-sdk/client-dynamodb": "^3.496.0",
Expand All @@ -37,6 +38,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-syntax-highlighter": "^15.5.0",
"swagger-ui": "^5.9.1",
"swiper": "^10.0.3"
},
"devDependencies": {
Expand All @@ -45,11 +47,13 @@
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"@types/react-syntax-highlighter": "^15.5.7",
"@types/swagger-ui": "^3.52.4",
"eslint": "8.47.0",
"eslint-config-custom": "*",
"eslint-config-next": "13.4.19",
"jest": "^29.5.0",
"jest-mock-extended": "^3.0.5",
"openapi-types": "^12.1.3",
"ts-jest": "^29.1.1",
"typescript": "5.1.6"
},
Expand Down
1 change: 1 addition & 0 deletions apps/nextjs-website/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import '@/styles/globals.css';
import '@/polyfill';
import ThemeRegistry from './ThemeRegistry';
import { getProducts } from '@/lib/api';
import SiteFooter from '@/components/atoms/SiteFooter/SiteFooter';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Tabs from './components/Tabs';
import Quote from './components/Quote';
import Embed from './components/Embed';
import CodeBlock from './components/CodeBlock';
import Swagger from './components/Swagger';
import Swagger from './components/Swagger/Swagger';
import PageLink from '@/components/organisms/GitBookContent/components/PageLink';
import Cards, { Card, CardItem } from './components/Cards';
import { ParseContentConfig } from 'gitbook-docs/parseContent';
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { KeyboardArrowRight } from '@mui/icons-material';
import {
Box,
Collapse,
List,
ListItemButton,
ListItemIcon,
Typography,
} from '@mui/material';
import { OpenAPIV3 } from 'openapi-types';
import { MouseEventHandler, PropsWithChildren, useState } from 'react';
import { useModelProps } from './hooks/useModel';

type ModelEntryProps = {
title?: string;
required?: boolean;
schemaType?: string;
};

type ModelItemProps = ModelEntryProps & {
description?: string;
onClick?: MouseEventHandler<HTMLDivElement>;
};

const ModelItem = ({
description,
title,
required,
schemaType,
onClick,
}: ModelItemProps) => {
const showIcon = typeof onClick === 'function';
return (
<ListItemButton
sx={{
display: 'block',
background: 'transparent!important',
py: 1,
}}
disableGutters
onClick={onClick}
>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
flexGrow: 1,
width: '100%',
gap: 2,
}}
>
<ListItemIcon>
{showIcon && <KeyboardArrowRight sx={{ fontSize: '1.125rem' }} />}
</ListItemIcon>
{title && (
<Typography sx={{ fontWeight: 'bold' }}>
{title}
{required && (
<Typography component='span' color='red'>
*
</Typography>
)}
</Typography>
)}
<Typography sx={{ color: (theme) => theme.palette.primary.main }}>
{schemaType}
</Typography>
</Box>
<Typography sx={{ ml: 2 }}>{description}</Typography>
</ListItemButton>
);
};

const ModelListEntry = ({
title,
required,
schemaType,
children,
}: PropsWithChildren<ModelEntryProps>) => {
const [open, setOpen] = useState(false);

return (
<List disablePadding>
<ModelItem
title={title}
required={required}
schemaType={schemaType}
onClick={() => setOpen(!open)}
/>
<Collapse
sx={{ borderLeft: 1, borderColor: 'divider', ml: 1, pl: 2 }}
in={open}
timeout='auto'
unmountOnExit
>
{children}
</Collapse>
</List>
);
};

type ModelProps = {
label?: string;
model: OpenAPIV3.SchemaObject;
requiredAttrs?: ReadonlyArray<string>;
};

export const Model = (props: ModelProps) => {
const {
description,
items,
properties,
required,
requiredAttrs,
schemaType,
title,
} = useModelProps(props);

if (schemaType === 'object') {
return (
<ModelListEntry title={title} required={required} schemaType={schemaType}>
{Object.entries(properties).map(([key, property]) => (
<Model
key={key}
label={key}
model={property}
requiredAttrs={requiredAttrs}
/>
))}
</ModelListEntry>
);
} else if (schemaType === 'array') {
return (
<ModelListEntry title={title} required={required} schemaType={schemaType}>
<Model model={items} requiredAttrs={requiredAttrs} />
</ModelListEntry>
);
} else {
return (
<ModelItem
title={title}
required={required}
schemaType={schemaType}
description={description}
/>
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Box,
Chip,
ChipProps,
Typography,
chipClasses,
styled,
} from '@mui/material';
import { OpenAPIV3 } from 'openapi-types';

import Expandable, {
ExpandableDetails,
ExpandableSummary,
} from '../Expandable';
import { Parameters } from './Parameters';
import { RequestBody } from './RequestBody';
import { Responses } from './Responses';

const StyledChip = styled(Chip)(() => ({
[`& .${chipClasses.label}`]: {
textTransform: 'uppercase',
fontWeight: 'bold',
color: 'white!important',
},
}));

export const API_METHODS_COLORS: Record<
OpenAPIV3.HttpMethods,
ChipProps['color']
> = {
get: 'primary',
post: 'success',
put: 'warning',
delete: 'error',
options: 'default',
head: 'default',
patch: 'default',
trace: 'default',
};

type OperationProps = OpenAPIV3.OperationObject<{
method: OpenAPIV3.HttpMethods;
path: string;
}>;

export const Operation = ({
method,
path,
summary,
description,
parameters,
responses,
requestBody,
servers = [],
}: OperationProps) => {
const chipColor = API_METHODS_COLORS[method] || 'default';
const baseUrl = servers[0]?.url || '';

return (
<Expandable>
<ExpandableSummary>
<Box display='inline-flex' alignItems='center' flexWrap='wrap'>
<StyledChip
sx={{ mr: 2 }}
label={method}
color={chipColor}
size='small'
/>
<Typography
variant='caption'
sx={{ color: (theme) => theme.palette.text.secondary }}
>
{baseUrl}
</Typography>
<Typography variant='caption-semibold'>{path}</Typography>
</Box>
<Typography sx={{ fontWeight: 'bold', mt: 2 }} variant='body1'>
{summary}
</Typography>
</ExpandableSummary>
<ExpandableDetails>
<Typography variant='body2'>{description}</Typography>
<Parameters parameters={parameters as OpenAPIV3.ParameterObject[]} />
{requestBody && (
<RequestBody {...(requestBody as OpenAPIV3.RequestBodyObject)} />
)}
{responses && <Responses responses={responses} />}
</ExpandableDetails>
</Expandable>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useTranslations } from 'next-intl';
import { OpenAPIV3 } from 'openapi-types';

import { Operation } from './Operation';
import { Card, Typography } from '@mui/material';

const methods = [
'get',
'post',
'put',
'delete',
'options',
'head',
'patch',
'trace',
];

const getOperations = (
pathItemObj: Omit<OpenAPIV3.PathItemObject, 'parameters'>
) =>
Object.keys(pathItemObj)
.filter((key) => methods.includes(key))
.reduce<[string, OpenAPIV3.OperationObject][]>((acc, key) => {
const method = key as OpenAPIV3.HttpMethods;
const operation = pathItemObj[method] as OpenAPIV3.OperationObject;
return [...acc, [method, operation]];
}, []);

type OperationsProps = {
spec: OpenAPIV3.Document;
validOperations?: Record<string, OpenAPIV3.HttpMethods[]>;
};

export const Operations = ({ spec, validOperations }: OperationsProps) => {
const t = useTranslations('swagger');

if (!spec.paths || !validOperations) {
const noOpHeader = t('emptyOperations.header');
const noOpMessage = t('emptyOperations.message');
return (
<Card sx={{ borderRadius: 1, p: 2 }} variant='outlined'>
<Typography sx={{ fontWeight: 'bold' }} variant='body1'>
{noOpHeader}
</Typography>
<Typography
variant='body2'
sx={{ color: (theme) => theme.palette.text.secondary }}
>
{noOpMessage}
</Typography>
</Card>
);
}

const paths = Object.entries(spec.paths);
const specServers = spec.servers || [];

const renderOperationTag = ([path, pathItemObj = {}]: [
string,
OpenAPIV3.PathItemObject | undefined
]) => {
const operations = getOperations(pathItemObj);
const headerParameters = pathItemObj?.parameters || [];
const validOperationMethods = validOperations[path] || [];

return (
<div key={`operation-${path}`}>
{operations.map(([method, operation]) => {
const httpMethod = method as OpenAPIV3.HttpMethods;
const renderOp = validOperationMethods.includes(httpMethod);

if (!renderOp) return null;

const { parameters: pathParameters = [], servers, ...op } = operation;
const parameters = pathParameters.concat(headerParameters);
const operationServers = servers || specServers;
return (
<Operation
key={`${path}-${method}`}
method={httpMethod}
path={path}
parameters={parameters}
servers={operationServers}
{...op}
/>
);
})}
</div>
);
};

return <div>{paths.map(renderOperationTag)}</div>;
};
Loading

0 comments on commit b2f50b8

Please sign in to comment.