docs: oas (#197)

Adds OpenAPI specification of Storefront and Admin APIs.
Updates docs project.
This commit is contained in:
Sebastian Rindom
2021-03-10 11:51:54 +01:00
committed by GitHub
parent 975de99ee7
commit 8edb32c742
240 changed files with 23592 additions and 419 deletions
+88
View File
@@ -0,0 +1,88 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
import styled from "@emotion/styled"
import Markdown from "react-markdown"
import RouteSection from "./route-section"
import JsonBox from "./json-box"
import EndpointOverview from "./endpoint-overview"
import Parameters from "./parameters"
const convertToKebabCase = string => {
return string.replace(/\s+/g, "-").toLowerCase()
}
const EndpointContainer = styled(Flex)`
min-height: 90vh;
position: relative;
border-top: hairline;
code {
background-color: #e3e8ee;
border-radius: 5px;
padding: 4px;
}
`
const DocsReader = ({ tags, spec }) => {
return (
<Flex flexDirection="column" width="100%">
{Object.entries(tags).map(([tagName, endpoints]) => (
<EndpointContainer id={convertToKebabCase(tagName)} p={4}>
<Flex flexDirection="row" width="100%">
<Flex py={5} flexDirection="column" p={4} width="100%">
<EndpointOverview
title={tagName}
description={""}
routes={endpoints}
spec={spec}
/>
<Flex
flexDirection="row"
width={"100%"}
sx={{
position: "relative",
borderTop: "hairline",
}}
>
<Flex width={"100%"} flexDirection="column">
{endpoints.map((endpoint, i) => (
<Flex
id={convertToKebabCase(endpoint.summary)}
py={4}
flexDirection="row"
width="100%"
>
<Flex
pr={5}
width={"55%"}
flexDirection="column"
sx={{ lineHeight: "26px" }}
>
<Text mb={3} fontSize={3}>
{endpoint.summary}
</Text>
<RouteSection
basePath={""}
method={endpoint.method}
path={endpoint.path}
/>
<Markdown>{endpoint.description}</Markdown>
<Parameters endpoint={endpoint} />
</Flex>
<Flex py={5} width="45%" flex="1">
<JsonBox name={tagName} endpoint={endpoint} />
</Flex>
</Flex>
))}
</Flex>
</Flex>
</Flex>
</Flex>
</EndpointContainer>
))}
</Flex>
)
}
export default DocsReader
+65
View File
@@ -0,0 +1,65 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
import RoutesOverview from "./routes-overview"
import ParamSection from "./param-section"
import JsonBox from "./json-box"
const EndpointOverview = ({ title, description, routes, spec }) => {
let schema = {}
let attrs = []
let resourceId
if (spec) {
const tag = spec.tags.find(t => t.name === title)
if (tag && tag["x-resourceId"]) {
resourceId = tag["x-resourceId"]
schema = spec.components.schemas[tag["x-resourceId"]]
for (const [name, details] of Object.entries(schema.properties)) {
if (!attrs.find(a => a.name === name)) {
attrs.push({
name,
...details,
})
}
}
}
}
return (
<Flex flexDirection="row" pb={4}>
<Flex
flexDirection="column"
width="55%"
pr={5}
sx={{ lineHeight: "26px" }}
>
<Text mb={3} fontSize={4}>
{title}
</Text>
<Text mb={4}>{description || schema.description}</Text>
{attrs.length > 0 && (
<Box
sx={{
borderBottom: "hairline",
}}
my="2"
>
<Text my={2}>Attributes</Text>
{attrs.map(p => (
<ParamSection param={p} />
))}
</Box>
)}
</Flex>
<Flex width={"45%"} flexDirection="column">
{routes && <RoutesOverview content={routes} />}
{resourceId && <JsonBox text={"OBJECT"} resourceId={resourceId} />}
</Flex>
</Flex>
)
}
export default EndpointOverview
+88
View File
@@ -0,0 +1,88 @@
import React, { useState, useEffect } from "react"
import { graphql } from "gatsby"
import { Flex, Box, Text, Image } from "rebass"
import styled from "@emotion/styled"
import { AnchorLink } from "gatsby-plugin-anchor-links"
import Markdown from "react-markdown"
import Highlight from "react-highlight.js"
import "highlight.js/styles/a11y-light.css"
import fixtures from "../../../docs/api/fixtures.json"
export const ResponseContainer = styled(Flex)`
border: 1px solid #e3e8ee;
border-radius: 5px;
margin-left: auto;
margin-right: auto;
width: 100%;
max-height: calc(90vh - 20px);
overflow-y: scroll;
align-self: flex-start;
font-size: 1;
position: sticky;
top: 20px;
code {
background: #f7fafc !important;
}
`
const JsonBox = ({ text, resourceId, endpoint }) => {
const [json, setJson] = useState({})
useEffect(() => {
const toSet = {}
if (endpoint) {
const props =
endpoint?.responses?.["200"]?.content?.["application/json"]?.schema
?.properties
if (props) {
for (const [name, details] of Object.entries(props)) {
if (
details["x-resourceId"] &&
details["x-resourceId"] in fixtures.resources
) {
toSet[name] = fixtures.resources[details["x-resourceId"]]
} else {
toSet[name] = details
}
}
}
}
if (resourceId) {
setJson(fixtures.resources[resourceId])
} else {
setJson(toSet)
}
}, [])
return (
<ResponseContainer flexDirection="column" as="pre">
<Text
fontSize={0}
fontFamily="body"
py={2}
px={3}
color="#4f566b"
backgroundColor="#e3e8ee"
>
{text || "RESPONSE"}
</Text>
<Box
w={1}
flex="1"
sx={{ overflowY: "scroll" }}
backgroundColor="#f7fafc"
>
<Highlight language="json">
{JSON.stringify(json, undefined, 2)}
</Highlight>
</Box>
</ResponseContainer>
)
}
export default JsonBox
+145
View File
@@ -0,0 +1,145 @@
import React from "react"
import styled from "@emotion/styled"
import { Flex, Box, Text } from "rebass"
import Markdown from "react-markdown"
import Collapsible from "react-collapsible"
const ExpandContainer = styled.div`
.child-attrs {
cursor: pointer;
font-size: 12px;
box-sizing: border-box;
padding-left: 10px;
padding-right: 10px;
width: max-content;
border-radius: 5px;
border: 1px solid #e3e8ee;
color: #afafaf;
&:hover {
color: #212121;
}
}
.child-attrs.is-open {
width: 100%;
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
`
const Expand = ({ schema }) => {
const properties = schema.properties
let aggregated = []
for (const [name, details] of Object.entries(properties)) {
if (!aggregated.find(a => a.name === name)) {
aggregated.push({
name,
...details,
})
}
}
return (
<ExpandContainer>
<Collapsible
transitionTime={50}
triggerClassName={"child-attrs"}
triggerOpenedClassName={"child-attrs"}
triggerTagName="div"
trigger={"Show nested attributes"}
triggerWhenOpen={"Hide"}
>
<Box
sx={{
padding: "10px",
borderRadius: "5px",
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
border: "hairline",
}}
mb="2"
>
<Text my={2}>{schema.title}</Text>
{aggregated.map(param => {
let type = param.type
if (!type && param.schema) {
type = param.schema.type
}
return (
<Box
py={2}
sx={{
borderTop: "hairline",
}}
>
<Flex
fontSize="1"
alignItems="baseline"
pb={1}
fontFamily="monospace"
>
<Box mr={2} fontSize={"12px"}>
{param.name}
</Box>
<Text color={"gray"} fontSize={"10px"}>
{type}
</Text>
{param.required && (
<Text ml={1} fontSize={"10px"} variant="labels.required">
required
</Text>
)}
</Flex>
<Text fontSize={0}>
<Markdown>{param.description}</Markdown>
</Text>
</Box>
)
})}
</Box>
</Collapsible>
</ExpandContainer>
)
}
const ParamSection = ({ routeParam, param }) => {
let type = param.type
if (!type && param.schema) {
type = param.schema.type
}
return (
<Box
py={2}
sx={{
borderTop: "hairline",
}}
>
<Flex fontSize="1" alignItems="baseline" pb={1} fontFamily="monospace">
<Box mr={2} fontSize={"12px"}>
{param.name}
</Box>
<Text color={"gray"} fontSize={"10px"}>
{type}
</Text>
{(param.required || routeParam) && (
<Text ml={1} fontSize={"10px"} variant="labels.required">
required
</Text>
)}
</Flex>
<Text fontSize={0}>
<Markdown>{param.description}</Markdown>
</Text>
{param.anyOf && param.anyOf.map(schema => <Expand schema={schema} />)}
{param.oneOf && param.oneOf.map(schema => <Expand schema={schema} />)}
{param.type === "array" && param.items?.properties && (
<Expand schema={param.items} />
)}
</Box>
)
}
export default ParamSection
+41
View File
@@ -0,0 +1,41 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
import ParamSection from "./param-section"
const Parameters = ({ endpoint }) => {
const aggregated = endpoint.parameters || []
const reqBody = endpoint.requestBody || {}
const props = reqBody.content?.["application/json"]?.schema?.properties
if (props) {
for (const [name, details] of Object.entries(props)) {
if (!aggregated.find(a => a.name === name)) {
aggregated.push({
name,
...details,
})
}
}
}
if (!aggregated.length) {
return null
}
return (
<Box
sx={{
borderBottom: "hairline",
}}
my="2"
>
<Text my={2}>Parameters</Text>
{aggregated.map(p => (
<ParamSection param={p} />
))}
</Box>
)
}
export default Parameters
+19
View File
@@ -0,0 +1,19 @@
import React from "react"
import { Flex, Box, Text } from "rebass"
const RouteSection = ({ basePath, path, method }) => {
path = path.replaceAll(/{(.*?)}/g, ":$1")
return (
<Box py={2}>
<Flex fontFamily="monospace">
<Text mr={2} variant={`labels.${method}`}>
{method}
</Text>
<Text>{`${basePath}${path === "/" ? "" : path}`}</Text>
</Flex>
</Box>
)
}
export default RouteSection
+64
View File
@@ -0,0 +1,64 @@
import React, { useState, useEffect } from "react"
import styled from "@emotion/styled"
import { Flex, Box, Text, Image } from "rebass"
const StyledRoutesOverview = styled(Flex)`
border: 1px solid #e3e8ee;
border-radius: 5px;
margin-left: auto;
margin-right: auto;
width: 100%;
max-height: calc(90vh - 20px);
overflow-y: scroll;
align-self: flex-start;
font-size: 1;
top: 20px;
bottom: 20px;
`
const RoutesOverview = ({ content }) => {
if (!content) return null
return (
<StyledRoutesOverview mb={3} flexDirection="column">
<Text
fontSize={0}
fontFamily="body"
py={2}
px={3}
color="#4f566b"
backgroundColor="#e3e8ee"
>
ENDPOINTS
</Text>
<Box
w={1}
px={4}
py={2}
flex="1"
sx={{ overflowY: "scroll" }}
backgroundColor="#f7fafc"
>
<Flex fontSize={1} flexDirection="column">
{content.map(route => (
<Flex mb={2} width="100%">
<Text
width="55px"
mr={2}
variant={`labels.${route.method}`}
textAlign="right"
>
{route.method}
</Text>
<Text color="#4f566b">
{route.path.replaceAll(/{(.*?)}/g, ":$1")}
</Text>
</Flex>
))}
</Flex>
</Box>
</StyledRoutesOverview>
)
}
export default RoutesOverview
+107
View File
@@ -0,0 +1,107 @@
import React from "react"
import { Flex } from "rebass"
import { Label, Select as RebassSelect } from "@rebass/forms"
import styled from "@emotion/styled"
import Typography from "./typography"
const StyledSelect = styled(RebassSelect)`
${Typography.Base}
padding-right: 28px;
${props =>
props.isCurrencyInput &&
`
box-shadow: none;
border: none;
&:hover {
box-shadow: none;
}
`}
`
const StyledLabel = styled.div`
${Typography.Base}
${props =>
props.inline
? `
text-align: right;
padding-right: 15px;
`
: `
padding-bottom: 10px;
`}
${props =>
props.required &&
`
&:after {
color: rgba(255, 0, 0, 0.5);
content: " *";
}
`}
`
const Select = React.forwardRef(
(
{
name = "",
label = "",
defaultValue = "",
options = [],
placeholder = "",
value,
onChange,
inline,
required,
selectHeight,
isCurrencyInput,
...props
},
ref
) => {
return (
<Flex
alignItems={inline && "center"}
flexDirection={inline ? "row" : "column"}
{...props}
>
{label && (
<Label
flex={"30% 0 0"}
maxWidth={"200px"}
htmlFor={name}
display={props.start ? "flex" : inline && "inline !important"}
>
<StyledLabel required={required} inline={inline}>
{label}
</StyledLabel>
</Label>
)}
<StyledSelect
isCurrencyInput={isCurrencyInput}
flex="50% 0 0"
variant="buttons.primary"
name={name}
height={selectHeight || "inherit"}
minWidth={"unset"}
width={"unset"}
ref={ref}
value={value}
defaultValue={defaultValue}
onChange={onChange}
>
{placeholder && <option>{placeholder}</option>}
{options.map((option, index) => (
<option key={index} value={option.value}>
{option.label || option.value}
</option>
))}
</StyledSelect>
</Flex>
)
}
)
export default Select
+136
View File
@@ -0,0 +1,136 @@
import React, { useState, useEffect } from "react"
import { Link, navigate } from "gatsby"
import { Flex, Box, Text } from "rebass"
import { AnchorLink } from "gatsby-plugin-anchor-links"
import styled from "@emotion/styled"
import Collapsible from "react-collapsible"
import Select from "./select"
const convertToKebabCase = string => {
return string.replace(/\s+/g, "-").toLowerCase()
}
const StyledNavItem = styled(Flex)`
padding-left: 16px;
padding-right: 10px;
align-items: center;
border-radius: 5pt;
cursor: pointer;
margin-bottom: 5px;
height: 25px;
&:hover {
background-color: #e0e0e059;
}
`
const StyledAnchorLink = styled(AnchorLink)`
display: flex;
margin-left: 10px;
padding-left: 10px;
padding-right: 10px;
align-items: center;
border-radius: 5pt;
cursor: pointer;
margin-bottom: 5px;
text-decoration: none;
align-items: center;
color: black;
height: 25px;
[fill*="red"] {
fill: #454545;
}
&:hover {
${props =>
!props.active &&
`
background-color: #e0e0e059;
`}
}
&.active {
background-color: #e0e0e0;
}
`
const SideBarContainer = styled(Flex)`
position: sticky;
top: 0;
height: 100vh;
overflow-y: scroll;
background-color: #f0f0f0;
min-width: 250px;
flex-direction: column;
`
const StyledLink = styled(Link)`
color: #212121;
font-decoration: none;
`
const SideBar = ({ tags }) => {
const [api, setApi] = useState("store")
useEffect(() => {
const pathname = window.location.pathname
const matches = pathname.match(/api\/(store|admin)/)
if (pathname.length > 1) {
setApi(matches[1])
}
}, [])
return (
<SideBarContainer>
<Flex
width="100%"
alignContent="center"
justifyContent={"center"}
sx={{ borderBottom: "hairline" }}
px={4}
py={3}
mb={3}
justifyContent="center"
flexDirection="column"
>
<Flex width={"100%"} alignContent="center">
<Text
fontFamily="Medusa"
fontSize="26px"
color="#454b54"
fontWeight={300}
>
medusa
</Text>
</Flex>
<Flex pt={3} justifyContent="space-between">
<Select
value={api}
onChange={e => navigate(`/api/${e.target.value}`)}
options={[
{ value: "admin", label: "Admin" },
{ value: "store", label: "Storefront" },
]}
/>
</Flex>
</Flex>
{Object.entries(tags).map(([tag, details]) => {
return (
<Box pt={1} px={3}>
<Collapsible
transitionTime={50}
trigger={<StyledNavItem fontSize={1}>{tag}</StyledNavItem>}
>
{details.map(e => (
<StyledAnchorLink to={`#${convertToKebabCase(e.summary)}`}>
<Text fontSize={0}>{e.summary}</Text>
</StyledAnchorLink>
))}
</Collapsible>
</Box>
)
})}
</SideBarContainer>
)
}
export default SideBar
+54
View File
@@ -0,0 +1,54 @@
import { css } from "@emotion/core"
const Largest = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 22px;
font-weight: 300;
line-height: 1.5;
letter-spacing: normal;
`
const Large = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 18px;
font-weight: 300;
line-height: 1.22;
letter-spacing: -0.5px;
`
const Medium = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 16px;
font-weight: 300;
line-height: 1.22;
letter-spacing: normal;
`
const Base = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 14px;
font-weight: 300;
line-height: 1.22;
letter-spacing: -0.25px;
`
const Small = props => css`
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Ubuntu, sans-serif;
font-size: 12px;
font-weight: 300;
line-height: 12px;
letter-spacing: 0px;
`
export default {
Largest,
Large,
Medium,
Base,
Small,
}