docusaurus-openapi
docusaurus-openapi copied to clipboard
feat(openapi): redesigned page header
This PR is the first of many to overhaul the design and experience of the OpenAPI docs page.
What's new
- Adds
ApiItem/Headercomponent todocusaurus-theme-openapi- This component is responsible for the new experience of the docs header
- Color coded request methods
- Combines the path and currently selected server url to display the fully qualified request url
- Adds
doc-subtitlerole to the description.
- Refactors the
renderfunction to add two new line breaks between each markdown generated section (this avoids markdown parser errors with MDX). - Refactors the redux provider to include both the
ApiItemand theApiDemoPanel- This allows us to share state between the components, such as updating the server url when the user changes between a sandbox and production environment.
- Includes custom styling in the demo for color coded request method pills.
- Documents in the demo site how developers can achieve this in their own site.

- Documents in the demo site how developers can achieve this in their own site.
Deploy Preview for docusaurus-openapi ready!
| Name | Link |
|---|---|
| Latest commit | 4f652d870a7298f1feec8f9e1f231290f4680873 |
| Latest deploy log | https://app.netlify.com/sites/docusaurus-openapi/deploys/63142f38a96b050008198154 |
| Deploy Preview | https://deploy-preview-211--docusaurus-openapi.netlify.app |
| Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify site settings.
Ideally I would like to merge this into a next branch instead of main. Technically this is "production ready", but the design will be more cohesive if we wait until the majority of adjustments are made.
amazing work here and in the additional PRs started @sean-perkins 🎉 . thank you ❤️
I actually have many similar design requests from the Auth0/Okta API docs design team and we are currently POC'ing this template alongside our MUI-based design system. I can't quite open source this, but I will surely report any feedback in the future.
hey @sean-perkins, just wanted to quickly share a few updates here on our end that I think may help this PR. One gap i've noted in our specs here is the use of oneOf and anyOf in our response schemas. These also exist in Response Body so I have sort combined Schema display into a singular component for now, and used a similar recursion logic. Our display is more "list" style than the proposed modal design though... but eventually we'd love editable fields there inline too. Just food for thought for you!
docusaurus-plugin-openapi/src/markdown/index.ts:
import { ApiPageMetadata, InfoPageMetadata } from "../types";
import { createComponentImports } from "./createComponentImports";
import { createDeprecationNotice } from "./createDeprecationNotice";
import { createDescription } from "./createDescription";
import { createHeader } from "./createHeader";
import { createParamsTable } from "./createParamsTable";
import { createRequestBodyTable } from "./createRequestBodyTable";
import { createResponseMessages } from "./createResponseMessages";
import { createResponseSchema } from "./createResponseSchema";
// import { createResponseSchemaTable } from "./createResponseSchemaTable";
import { createSecurityRequirement } from "./createSecurityRequirement";
import { createVersionBadge } from "./createVersionBadge";
import { render } from "./utils";
export function createApiPageMD({ title, api }: ApiPageMetadata) {
const {
deprecated,
"x-deprecated-description": deprecatedDescription,
parameters,
requestBody,
responses,
security,
securitySchemes,
} = api;
return render([
createComponentImports(),
createHeader(title, api),
createDeprecationNotice({ deprecated, description: deprecatedDescription }),
createSecurityRequirement(securitySchemes, security),
createParamsTable({ parameters, type: "path", title: "Path Parameters" }),
createParamsTable({ parameters, type: "query", title: "Query Parameters" }),
createParamsTable({ parameters, type: "header", title: "Headers" }),
createParamsTable({ parameters, type: "cookie", title: "Cookies" }),
createRequestBodyTable({ requestBody }),
// createResponseSchemaTable({ responses }),
createResponseSchema({ responses }),
createResponseMessages({ responses }),
]);
}
docusaurus-plugin-openapi/src/markdown/createResponseSchema.ts:
/* ============================================================================
* Copyright (c) Cloud Annotations
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* ========================================================================== */
import { MediaTypeObject, SchemaObject } from "../openapi/types";
import { ApiItem } from "../types";
import { createDescription } from "./createDescription";
import { Children, create, guard } from "./utils";
function createSchemaListItem(
properties: SchemaObject["properties"]
): string[] | undefined {
if (properties === undefined) {
return undefined;
}
return Object.keys(properties).map((prop) => {
const section = properties![prop];
return create("SchemaListItem", {
propertyName: prop,
required: section.required,
schema: section,
children: [
create("SchemaObject", {
object: section,
children: [],
}),
createDescription(section.description),
guard(section.properties || section.items, () => {
return create("div", {
children: [
guard(section.properties, () => {
return createSchemaListItem(section.properties);
}),
guard(section.items, () => {
return createSchemaListItem(section.items?.properties);
}),
],
});
}),
],
});
});
}
function createResponseSchemaItem(schema: SchemaObject): Children {
if (schema === undefined) {
return undefined;
}
if (schema.oneOf) {
// top-level oneOf
const children = schema.oneOf.map((childSchema) => {
return create("SchemaListItem", {
propertyName: "type:",
schema: childSchema,
children: createResponseSchemaItem(childSchema),
});
});
return create("SchemaListItem", {
propertyName: "oneOf:",
anyOfOneOfSchemas: schema.oneOf,
schema: schema,
children: create("", { children: children }),
});
}
if (schema.anyOf) {
// top-level anyOf
const children = schema.anyOf.map((childSchema) => {
return create("SchemaListItem", {
propertyName: "type:",
schema: childSchema,
children: createResponseSchemaItem(childSchema),
});
});
return create("SchemaListItem", {
propertyName: "anyOf:",
anyOfOneOfSchemas: schema.anyOf,
schema: schema,
children: create("", { children: children }),
});
}
if (schema.items?.properties) {
// top-level array
return createSchemaListItem(schema.items?.properties);
}
if (schema.properties) {
// top-level object
return createSchemaListItem(schema.properties);
}
// top-level primitive
return create("SchemaListItem", {
propertyName: undefined,
schema: schema,
children: [createDescription(schema.description)],
});
}
interface SchemaProps {
title: string;
body: {
content?: {
[key: string]: MediaTypeObject;
};
};
}
export function createSchemaList({ title, body, ...rest }: SchemaProps) {
if (body === undefined || body.content === undefined) {
return undefined;
}
// TODO: We just pick the first content-type. Should we diplay multiple and allow user to select?
const randomFirstKey = Object.keys(body.content)[0];
const firstBody = body.content[randomFirstKey].schema;
if (firstBody === undefined) {
return undefined;
}
// we don't show the table if there is no properties to show
if (firstBody.properties !== undefined) {
if (Object.keys(firstBody.properties).length === 0) {
return undefined;
}
}
return firstBody;
}
interface Props {
responses: ApiItem["responses"];
}
export function createResponseSchema({ responses }: Props) {
if (responses === undefined) {
return undefined;
}
const codes = Object.keys(responses);
if (codes.length === 0) {
return undefined;
}
const allSchemas = codes
.map((code) => {
const content = responses[code].content;
const schema = createSchemaList({
title: code,
body: { content: content },
});
if (schema) {
const children = createResponseSchemaItem(schema);
if (children === undefined) {
return undefined;
}
return create("ResponseSchema", {
responseCode: code,
description:
responses[code].description &&
createDescription(responses[code].description),
contentType: content && Object.keys(content)[0], // todo: only showing the first contentType for now
schemaName: undefined, // todo: find schema name / $ref
type: schema.type, // todo: what should be shown for oneOf/anyOf/allOf?
children: children,
});
}
return undefined;
})
.filter((c) => c !== undefined);
return allSchemas?.length
? create("ContentSection", {
title: "Response Schemas",
expand: true,
children: allSchemas,
})
: undefined;
}
docusaurus-plugin-openapi/src/markdown/createRequestBodyTable.ts:
/* ============================================================================
* Copyright (c) Cloud Annotations
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* ========================================================================== */
import type { RequestBodyObject, SchemaObject } from "../openapi/types";
import { createDescription } from "./createDescription";
import { create, guard } from "./utils";
interface Props {
requestBody?: RequestBodyObject;
}
function createBodyParamListItem(
properties: SchemaObject["properties"]
): string[] | undefined {
if (properties === undefined) {
return undefined;
}
return Object.keys(properties).map((prop) => {
const section = properties![prop];
return create("SchemaListItem", {
propertyName: prop,
required: section.required,
schema: section,
children: [
create("SchemaObject", {
object: section,
children: [],
}),
createDescription(section.description),
guard(section.properties || section.items, () => {
return create("div", {
children: [
guard(section.properties, () => {
return createBodyParamListItem(section.properties);
}),
guard(section.items, () => {
return createBodyParamListItem(section.items?.properties);
}),
],
});
}),
],
});
});
}
function createRequestBodyItem(
schema: SchemaObject | undefined
): string | string[] | undefined {
if (schema === undefined) {
return undefined;
}
if (schema.oneOf) {
// top-level oneOf
const children = schema.oneOf.map((childSchema) => {
return create("SchemaListItem", {
propertyName: "type:",
schema: childSchema,
children: createRequestBodyItem(childSchema),
});
});
return create("SchemaListItem", {
propertyName: "oneOf:",
anyOfOneOfSchemas: schema.oneOf,
schema: schema,
children: create("", { children: children }),
});
}
if (schema.anyOf) {
// top-level anyOf
const children = schema.anyOf.map((childSchema) => {
return create("SchemaListItem", {
propertyName: "type:",
schema: childSchema,
children: createRequestBodyItem(childSchema),
});
});
return create("SchemaListItem", {
propertyName: "anyOf:",
anyOfOneOfSchemas: schema.anyOf,
schema: schema,
children: create("", { children: children }),
});
}
if (schema.items?.properties) {
// top-level array
return createBodyParamListItem(schema.items?.properties);
}
if (schema.properties) {
// top-level object
return createBodyParamListItem(schema.properties);
}
// top-level primitive
return create("SchemaListItem", {
propertyName: undefined,
schema: schema,
children: [createDescription(schema.description)],
});
}
export function createRequestBodyTable({ requestBody }: Props) {
if (requestBody === undefined) {
return undefined;
}
return create("ContentSection", {
title: "Body Parameters",
expand: true,
children: Object.keys(requestBody.content).map((contentType) => {
const { schema } = requestBody.content[contentType];
const children = createRequestBodyItem(schema);
if (children === undefined) {
return undefined;
}
return create("RequestBody", {
contentType: contentType,
children: children,
});
}),
});
}
I've consolidated SchemaListItem to be used in both templates and make use of something similar to your ListItem though:
docusuarus-theme-openapi/src/theme/ApiItem/SchemaListItem/index.tsx:
/* ============================================================================
* Copyright (c) Cloud Annotations
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* ========================================================================== */
import React from "react";
import { getSchemaTypeColor } from "@a0/docusaurus-plugin-openapi/lib/markdown/schemaTypeStyles";
import { SchemaObject as OpenApiSchemaObject } from "@a0/docusaurus-plugin-openapi/src/openapi/types";
import { Label } from "@a0/quantum-product";
import { getSchemaQualifiedType } from "../../../utils";
import ListItem from "../ListItem";
import styles from "./styles.module.css";
interface Props {
propertyName: string;
required?: boolean;
schema: OpenApiSchemaObject;
paramType?: string;
anyOfOneOfSchemas?: OpenApiSchemaObject[];
children?: React.ReactNode;
}
function SchemaListItem({
propertyName,
required,
schema,
anyOfOneOfSchemas,
children,
}: Props): JSX.Element | null {
return (
<ListItem>
<div className={styles.propertySection}>
<div className={styles.paramHeader}>
{propertyName && <strong>{propertyName}</strong>}
{anyOfOneOfSchemas && <span>[</span>}
{anyOfOneOfSchemas &&
anyOfOneOfSchemas.map((childSchema, i) => {
return (
<>
<span
className={styles.paramType}
style={{
color: getSchemaTypeColor(
getSchemaQualifiedType(childSchema)
),
}}
>
{getSchemaQualifiedType(childSchema)}
</span>
{i !== anyOfOneOfSchemas.length - 1 && <span>{","}</span>}
</>
);
})}
{anyOfOneOfSchemas && <span>]</span>}
<span
className={styles.paramType}
style={{
color: getSchemaTypeColor(getSchemaQualifiedType(schema)),
}}
>
{getSchemaQualifiedType(schema)}
</span>
{required && (
<Label color="danger" variant="outlined">
Required
</Label>
)}
</div>
{/* Renders the description, schema object and nested properties */}
{children}
</div>
</ListItem>
);
}
export default SchemaListItem;
Here's a few screenshots of how this looks...
fwiw, i have fully swizzled Layout now and injected MUI into a lot of this, so i'd probably need to allow swizzling of these templates if we ever open source this template, but its working quite well and really appreciate all the effort here that made this possible! I will look for any other opportunities to contribute back though (potentially w/ oauth/security additions in the future). 🎈 🍰 🍨