Add generated API docs to website (#537)
* Add dependencies for openapi-page component * Add openapi-page component * Use openapi-page component for /api-docs page * add /api link to nav (#570) * website: remove superflous netlify config file * website: misc clean up website/readme - remove netlify badge - run through prettier markdown fmt * website: add /api docs to nav - adds /api-docs => /api rewrite - updates open-api component base api path to be /api - adds .html rewrite for good measure * Fix currentPath highlight issue in subnav * Clean up inline styles in operation-object * Remove React imports * Remove api-docs rewrite Co-authored-by: Kyle MacDonald <kylemac@users.noreply.github.com>
This commit is contained in:
1 parent
c701b79122
commit
1cc8737540
25 files changed
+1250
-31
No files matched your search
+21
-11
@@ -1,8 +1,6 @@
|
||||
# Boundary Documentation Website
|
||||
|
||||
[](https://app.netlify.com/sites/<NETLIFY-SLUG>/deploys)
|
||||
|
||||
This subdirectory contains the entire source for the [Boundary Website](https://boundaryproject.io). This is a [NextJS](https://nextjs.org/) project, which builds a static site from these source files.
|
||||
This subdirectory contains the entire source for the [Boundary Website](https://boundaryproject.io/). This is a [NextJS](https://nextjs.org/) project, which builds a static site from these source files.
|
||||
|
||||
<!--
|
||||
This readme file contains several blocks of generated text, to make it easier to share common information
|
||||
@@ -15,14 +13,26 @@ This subdirectory contains the entire source for the [Boundary Website](https://
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Contributions](#contributions-welcome)
|
||||
- [Running the Site Locally](#running-the-site-locally)
|
||||
- [Editing Markdown Content](#editing-markdown-content)
|
||||
- [Editing Navigation Sidebars](#editing-navigation-sidebars)
|
||||
- [Changing the Release Version](#changing-the-release-version)
|
||||
- [Redirects](#redirects)
|
||||
- [Browser Support](#browser-support)
|
||||
- [Deployment](#deployment)
|
||||
- [Boundary Documentation Website](#boundary-documentation-website)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Contributions Welcome!](#contributions-welcome)
|
||||
- [Running the Site Locally](#running-the-site-locally)
|
||||
- [Editing Markdown Content](#editing-markdown-content)
|
||||
- [Creating New Pages](#creating-new-pages)
|
||||
- [Markdown Enhancements](#markdown-enhancements)
|
||||
- [Custom Components](#custom-components)
|
||||
- [Tabs](#tabs)
|
||||
- [Enterprise Alert](#enterprise-alert)
|
||||
- [Other Components](#other-components)
|
||||
- [Syntax Highlighting](#syntax-highlighting)
|
||||
- [Editing Navigation Sidebars](#editing-navigation-sidebars)
|
||||
- [Index-less Categories](#index-less-categories)
|
||||
- [Custom or External Links](#custom-or-external-links)
|
||||
- [Changing the Release Version](#changing-the-release-version)
|
||||
- [Displaying a Prerelease](#displaying-a-prerelease)
|
||||
- [Redirects](#redirects)
|
||||
- [Browser Support](#browser-support)
|
||||
- [Deployment](#deployment)
|
||||
|
||||
<!-- BEGIN: contributions -->
|
||||
<!-- Generated text, do not edit directly -->
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import OperationObject from './partials/operation-object'
|
||||
import DocsPage from '@hashicorp/react-docs-page'
|
||||
import { getPathsFromSchema, getPropsForPage } from './utils/routing-utils'
|
||||
import styles from './openapi-page.module.css'
|
||||
import useOnClickOutside from 'lib/hooks/use-on-click-outside'
|
||||
|
||||
function OpenApiPage({
|
||||
info,
|
||||
operationCategory,
|
||||
sidenavOrder,
|
||||
productName,
|
||||
productSlug,
|
||||
pathFromRoot,
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const operationsRef = useRef(null)
|
||||
const [expandedOperations, setExpandedOperations] = useState([])
|
||||
useOnClickOutside(operationsRef, () => setExpandedOperations([]))
|
||||
|
||||
function setOperationState(slug, isExpanded) {
|
||||
const newStates = expandedOperations.filter((s) => s !== slug)
|
||||
if (isExpanded) newStates.push(slug)
|
||||
setExpandedOperations(newStates)
|
||||
}
|
||||
|
||||
const pageTitle = operationCategory ? operationCategory.name : info.title
|
||||
|
||||
return (
|
||||
<div className={styles.root} data-theme={productSlug}>
|
||||
<DocsPage
|
||||
head={{
|
||||
is: Head,
|
||||
title: `${pageTitle} | ${productName} by HashiCorp`,
|
||||
description: info.description,
|
||||
siteName: `${productName} by HashiCorp`,
|
||||
}}
|
||||
product="vault"
|
||||
resourceURL={`https://github.com/hashicorp/${productSlug}`}
|
||||
sidenav={{
|
||||
Link,
|
||||
currentPage: router.asPath,
|
||||
category: pathFromRoot,
|
||||
disableFilter: true,
|
||||
order: sidenavOrder,
|
||||
data: [],
|
||||
}}
|
||||
>
|
||||
{operationCategory ? (
|
||||
<div>
|
||||
<p className={`${styles.pageHeading} g-type-display-2`}>
|
||||
{info.title}
|
||||
</p>
|
||||
<h1 className={`${styles.categoryHeading} g-type-display-4`}>
|
||||
{operationCategory.name}
|
||||
</h1>
|
||||
<div ref={operationsRef}>
|
||||
{operationCategory.operations.map((op) => {
|
||||
const isExpanded =
|
||||
expandedOperations.indexOf(op.operationId) !== -1
|
||||
|
||||
return (
|
||||
<OperationObject
|
||||
key={op.__type + op.__path}
|
||||
path={op.__path}
|
||||
type={op.__type}
|
||||
data={op}
|
||||
isCollapsed={!isExpanded}
|
||||
setIsCollapsed={(isCollapsed) =>
|
||||
setOperationState(op.operationId, !isCollapsed)
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h1 className={`${styles.pageHeading} g-type-display-2`}>
|
||||
{info.title}
|
||||
</h1>
|
||||
<p className={`${styles.landingPlaceholder} g-type-body-long`}>
|
||||
Select a service from the sidebar.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</DocsPage>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { getPathsFromSchema, getPropsForPage }
|
||||
export default OpenApiPage
|
||||
@@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import OpenAPIPage from '.'
|
||||
|
||||
function OpenAPIPageProps(props) {
|
||||
return <OpenAPIPage {...props} />
|
||||
}
|
||||
|
||||
OpenAPIPageProps.propTypes = {
|
||||
/** Information about the API, pulled from the schema's [Info Object](https://swagger.io/specification/v2/#info-object) */
|
||||
info: PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
version: PropTypes.string,
|
||||
}),
|
||||
/** The name of the associated HashiCopr product, used in page metadata */
|
||||
productName: PropTypes.string,
|
||||
/** The slug of the associated HashiCorp product, used for theming. Only supports "boundary" or a default theme at present.*/
|
||||
productSlug: PropTypes.oneOf(['boundary']),
|
||||
/** An object that determines which operations will be shown on the page */
|
||||
operationCategory: PropTypes.shape({
|
||||
/** The name of the operation "category", used as the page title */
|
||||
name: PropTypes.string,
|
||||
/** The slug-ified name, used as the page's location */
|
||||
slug: PropTypes.string,
|
||||
/** An array of [Operation Objects](https://swagger.io/specification/v2/#operation-object) to be displayed on the page */
|
||||
operations: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
/** ["order"](https://github.com/hashicorp/react-components/blob/11d7bda7d518bc675ab0033067d43057e21f3b77/packages/docs-sidenav/index.js#L9) data generated to be passed directly to our DocsSidenav component */
|
||||
sidenavOrderData: PropTypes.object,
|
||||
}
|
||||
|
||||
export default OpenAPIPageProps
|
||||
@@ -0,0 +1,18 @@
|
||||
.root {
|
||||
/* currently only supports "boundary", may want to extend to support other product themes */
|
||||
&[data-theme='boundary'] {
|
||||
--brand: var(--boundary);
|
||||
}
|
||||
|
||||
& .pageHeading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& .categoryHeading {
|
||||
margin: 32px 0 16px 0;
|
||||
}
|
||||
|
||||
& .landingPlaceholder {
|
||||
color: var(--gray-2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
function ChevronDownBlack({ title, ...props }) {
|
||||
return (
|
||||
<svg
|
||||
width={props.width ? props.width : props.height ? 1 * props.height : 24}
|
||||
height={props.height ? props.height : props.width ? props.width / 1 : 24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<path
|
||||
d="M6 9l6 6 6-6"
|
||||
stroke={props.color || '#000'}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChevronDownBlack
|
||||
@@ -0,0 +1,18 @@
|
||||
.outer {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
&[data-useanimation='true'] {
|
||||
transition: height 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
opacity: 1;
|
||||
|
||||
&[data-iscollapsed='true'] {
|
||||
opacity: 0;
|
||||
}
|
||||
&[data-useanimation='true'] {
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component } from 'react'
|
||||
import styles from './collapsible.module.css'
|
||||
|
||||
/*
|
||||
Collapsible content block, receives children and a 'isCollapsed' prop,
|
||||
smoothly animates from collapsed (height 0) to expanded (height auto)
|
||||
*/
|
||||
export default class Collapsible extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.animateHeight = this.animateHeight.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.animateHeight()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Only update if isCollapsed prop has changed
|
||||
if (this.props.isCollapsed === prevProps.isCollapsed) return
|
||||
this.animateHeight()
|
||||
}
|
||||
|
||||
animateHeight() {
|
||||
function handleTransitionEnd(event) {
|
||||
const elem = event.target
|
||||
const elemStyle = getComputedStyle(elem)
|
||||
const isCollapsed = elem.getAttribute('data-iscollapsed')
|
||||
if (event.propertyName !== 'height') return
|
||||
const innerElem = event.target.firstChild
|
||||
const innerElemStyle = getComputedStyle(innerElem)
|
||||
if (isCollapsed === 'false') {
|
||||
elem.style.height = 'auto'
|
||||
innerElem.style.transitionDelay = '0s'
|
||||
} else if (isCollapsed === 'true') {
|
||||
const heightDurn = parseFloat(elemStyle['transition-duration'])
|
||||
const opactiyDurn = parseFloat(innerElemStyle['transition-duration'])
|
||||
innerElem.style.transitionDelay = `${heightDurn - opactiyDurn}s`
|
||||
}
|
||||
elem.removeEventListener('transitionend', handleTransitionEnd, false)
|
||||
}
|
||||
const { isCollapsed } = this.props
|
||||
// Do not attempt update if container elem is invalid
|
||||
const elem = this.containerRef
|
||||
if (!elem) return
|
||||
const elemStyle = getComputedStyle(elem)
|
||||
if (isCollapsed) {
|
||||
// Transition from auto to 0
|
||||
elem.style.height = elemStyle.height
|
||||
elem.offsetHeight // force repaint
|
||||
elem.style.height = '0px'
|
||||
elem.setAttribute('data-iscollapsed', 'true')
|
||||
elem.addEventListener('transitionend', handleTransitionEnd, false)
|
||||
} else {
|
||||
// Transition from 0 to auto
|
||||
const prevHeight = elem.style.height
|
||||
elem.style.height = 'auto'
|
||||
const endHeight = elemStyle.height
|
||||
elem.style.height = prevHeight
|
||||
elem.offsetHeight // force repaint
|
||||
elem.style.height = endHeight
|
||||
elem.setAttribute('data-iscollapsed', 'false')
|
||||
elem.addEventListener('transitionend', handleTransitionEnd, false)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, noAnimation } = this.props
|
||||
return (
|
||||
<div
|
||||
className={styles.outer}
|
||||
ref={(ref) => (this.containerRef = ref)}
|
||||
data-useanimation={Boolean(!noAnimation)}
|
||||
>
|
||||
<div className={styles.inner} data-useanimation={Boolean(!noAnimation)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import Collapsible from '../collapsible'
|
||||
import ResponseObject from '../response-object'
|
||||
import PropertyObject from '../property-object'
|
||||
import { capitalCase } from 'change-case'
|
||||
import useHover from '../../../../lib/hooks/use-hover'
|
||||
import SvgrChevronDown from '../chevron-down/black'
|
||||
|
||||
import styles from './operation-object.module.css'
|
||||
|
||||
function OperationObject({
|
||||
data,
|
||||
path,
|
||||
type,
|
||||
isHighlighted,
|
||||
isCollapsed,
|
||||
setIsCollapsed,
|
||||
}) {
|
||||
const [headerRef, isHeaderHovered] = useHover()
|
||||
|
||||
// const [isCollapsed, setIsCollapsed] = useState(true)
|
||||
const { operationId, parameters, responses, summary } = data
|
||||
const successResponse = responses['200']
|
||||
const title = capitalCase(operationId.split('_').slice(1).join())
|
||||
|
||||
// Group parameter properties by type
|
||||
const pathParams = parameters.filter((p) => p.in === 'path')
|
||||
const queryParams = parameters.filter((p) => p.in === 'query')
|
||||
const bodyParam = parameters.filter((p) => p.in === 'body')[0] // Note: we only accept a single "in=body" param
|
||||
const bodyProps = bodyParam ? getBodyParamProps(bodyParam) : []
|
||||
|
||||
return (
|
||||
<div className={styles.root} data-is-hovered={isHeaderHovered}>
|
||||
<div
|
||||
className={styles.header}
|
||||
ref={headerRef}
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
<div className={styles.meta}>
|
||||
<div
|
||||
className={styles.title}
|
||||
data-is-highlighted={isHighlighted}
|
||||
data-is-hovered={isHeaderHovered}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.endpoint}>
|
||||
<span className={styles.method} data-is-hovered={isHeaderHovered}>
|
||||
{type.toUpperCase()}{' '}
|
||||
</span>
|
||||
<span className={styles.path}>{path}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.toggleButton}>
|
||||
<span className={styles.toggleText}>
|
||||
{isCollapsed ? 'Expand' : 'Collapse'}
|
||||
</span>
|
||||
<span className={styles.toggleIcon} data-is-collapsed={isCollapsed}>
|
||||
<SvgrChevronDown width={16} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible isCollapsed={isCollapsed}>
|
||||
<div className={styles.details}>
|
||||
<p className={styles.summary}>{summary}</p>
|
||||
<TwoColumnLayout
|
||||
columnOne={
|
||||
<div>
|
||||
<p className={styles.columnHeading}>Request</p>
|
||||
{pathParams.length > 0 ? (
|
||||
<Parameters title="Path Parameters" params={pathParams} />
|
||||
) : null}
|
||||
{queryParams.length > 0 ? (
|
||||
<Parameters title="Query Parameters" params={queryParams} />
|
||||
) : null}
|
||||
{bodyProps.length > 0 ? (
|
||||
<Parameters title="Body Parameters" params={bodyProps} />
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
columnTwo={
|
||||
<div>
|
||||
<p className={styles.columnHeading}>Response</p>
|
||||
{!!successResponse ? (
|
||||
<div>
|
||||
<p
|
||||
className={`${styles.columnSectionHeading} g-type-label-strong`}
|
||||
>
|
||||
Successful Response
|
||||
</p>
|
||||
<ResponseObject data={successResponse} />
|
||||
</div>
|
||||
) : (
|
||||
<p>No response has been defined.</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getBodyParamProps(bodyParam) {
|
||||
// We always expect the bodyParam to be an object,
|
||||
// with a schema which defines the body properties.
|
||||
if (!bodyParam.schema || !bodyParam.schema.properties) return []
|
||||
// We flatten these properties to avoid showing a
|
||||
// "collapsed object" UI under the "Body Parameters" section,
|
||||
// which would be a bit redundant and annoying to have to expand
|
||||
const bodyPropsObj = bodyParam.schema.properties
|
||||
const bodyProps = Object.keys(bodyPropsObj).reduce((acc, key) => {
|
||||
const data = Object.assign({}, bodyPropsObj[key])
|
||||
// We need the property name. This is usually be handled by "key" in an object,
|
||||
// but we're flattening the object so we need to make sure it's there
|
||||
data.name = key
|
||||
if (!data.readOnly) acc.push(data)
|
||||
return acc
|
||||
}, [])
|
||||
return bodyProps
|
||||
}
|
||||
|
||||
function TwoColumnLayout({ columnOne, columnTwo }) {
|
||||
return (
|
||||
<div className={styles.twoColumnLayout}>
|
||||
<div>{columnOne}</div>
|
||||
<div></div>
|
||||
<div>{columnTwo}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Parameters({ title, params }) {
|
||||
return (
|
||||
<div>
|
||||
<p className={`${styles.columnSectionHeading} g-type-label-strong`}>
|
||||
{title}
|
||||
</p>
|
||||
{params.map((parameter, idx) => {
|
||||
return (
|
||||
<PropertyObject
|
||||
key={parameter.name}
|
||||
name={parameter.name}
|
||||
data={parameter}
|
||||
isFirstItem={idx === 0}
|
||||
isLastItem={idx === params.length - 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OperationObject
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import OperationObject from '.'
|
||||
|
||||
function OperationObjectProps(props) {
|
||||
return <OperationObject {...props} />
|
||||
}
|
||||
|
||||
/** Displays [Operation Object](https://swagger.io/specification/v2/#operation-object) data to the user. */
|
||||
OperationObjectProps.propTypes = {
|
||||
/** The path to the endpoint where this operation is executed */
|
||||
path: PropTypes.string.isRequired,
|
||||
/** The type of operation */
|
||||
type: PropTypes.oneOf([
|
||||
'get',
|
||||
'put',
|
||||
'post',
|
||||
'delete',
|
||||
'options',
|
||||
'head',
|
||||
'patch',
|
||||
]).isRequired,
|
||||
/** A subset of [Operation Object](https://swagger.io/specification/v2/#operation-object) relevant to the UI */
|
||||
data: PropTypes.shape({
|
||||
/** Flag whether this operation is deprecated */
|
||||
deprecated: PropTypes.bool,
|
||||
/** A unique string used to identify the operation.*/
|
||||
operationId: PropTypes.string,
|
||||
/** Array of parameter objects. We group them by their "in" property, and pass them to the <PropertyObject /> component for rendering. */
|
||||
parameters: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
/** The name of the parameter. Parameter names are case sensitive. */
|
||||
name: PropTypes.string.isRequired,
|
||||
/** A brief, plain text description of the parameter. Note that [GitHub-flavor markdown](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) is not currently supported. */
|
||||
description: PropTypes.string,
|
||||
/** Indicates whether this parameter is mandatory. */
|
||||
required: PropTypes.boolean,
|
||||
/** The location of the parameter. Note: `formData` and `header` are not currently supported in the UI. */
|
||||
in: PropTypes.oneOf(['query', 'path', 'body']).isRequired,
|
||||
/** Schema is used for "body" parameters. */
|
||||
schema: PropTypes.object,
|
||||
/** Type defines the parameter type, for non-"body" parameters. */
|
||||
type: PropTypes.oneOf([
|
||||
'array',
|
||||
'boolean',
|
||||
'integer',
|
||||
'number',
|
||||
'object',
|
||||
'string',
|
||||
]),
|
||||
})
|
||||
),
|
||||
/** Responses object, keys are HTTP response codes */
|
||||
responses: PropTypes.shape({
|
||||
/** We only support "200" response codes, for now. This object is passed to the <ResponseObject /> component. */
|
||||
200: PropTypes.object,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export default OperationObjectProps
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
.root {
|
||||
border-bottom: 1px solid var(--gray-6);
|
||||
margin: 0;
|
||||
margin-top: -1px;
|
||||
position: relative;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&[data-is-hovered='true'] {
|
||||
border-bottom-color: var(--brand);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
composes: g-type-body-x-strong from global;
|
||||
color: var(--gray-2);
|
||||
margin: 0 0 4px 0;
|
||||
padding: 0;
|
||||
|
||||
&[data-is-highlighted='true'] {
|
||||
&::before {
|
||||
content: '\200B';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
bottom: -4px;
|
||||
background: var(--warning-l1);
|
||||
z-index: -1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-hovered='true'] {
|
||||
color: var(--brand);
|
||||
}
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
composes: g-type-code from global;
|
||||
|
||||
& .method {
|
||||
color: var(--gray-4);
|
||||
|
||||
&[data-is-hovered='true'] {
|
||||
color: var(--brand);
|
||||
}
|
||||
}
|
||||
|
||||
& .path {
|
||||
color: var(--gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
.toggleButton {
|
||||
border: none;
|
||||
color: var(--brand);
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
padding: 0 0 0 24px;
|
||||
transition: color 0.2s;
|
||||
|
||||
& .toggleIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
transition: transform 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& svg {
|
||||
& [fill] {
|
||||
fill: var(--brand);
|
||||
}
|
||||
& [stroke] {
|
||||
stroke: var(--brand);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-collapsed='false'] {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-hovered='true'] {
|
||||
color: var(--brand);
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
padding-bottom: 80px;
|
||||
|
||||
& .summary {
|
||||
color: var(--gray-2);
|
||||
margin: 8px 0 24px 0;
|
||||
}
|
||||
|
||||
& .columnHeading {
|
||||
margin: 0 0 1em 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& .columnSectionHeading {
|
||||
margin: 0 !important; /* Needed to override ".g-content p" margins */
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--gray-6);
|
||||
color: var(--gray-4);
|
||||
}
|
||||
}
|
||||
|
||||
.twoColumnLayout {
|
||||
display: flex;
|
||||
|
||||
& > div:nth-child(1),
|
||||
& > div:nth-child(3) {
|
||||
position: relative;
|
||||
width: calc(50% - 16px);
|
||||
}
|
||||
|
||||
& > div:nth-child(2) {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react'
|
||||
import Collapsible from '../collapsible'
|
||||
import SvgrChevronDown from '../chevron-down/black'
|
||||
import styles from './property-object.module.css'
|
||||
|
||||
function PropertyObject({
|
||||
name,
|
||||
data,
|
||||
isFirstItem,
|
||||
isLastItem,
|
||||
arrayDepth = 0,
|
||||
}) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(true)
|
||||
const isArray = data.type === 'array'
|
||||
if (isArray)
|
||||
return (
|
||||
<PropertyObject
|
||||
name={name}
|
||||
data={data.items}
|
||||
arrayDepth={arrayDepth + 1}
|
||||
isFirstItem={isFirstItem}
|
||||
isLastItem={isLastItem}
|
||||
/>
|
||||
)
|
||||
const hasProperties = data.type === 'object' && Boolean(data.properties)
|
||||
|
||||
const typeArraySuffix =
|
||||
arrayDepth > 0 ? arrayFrom(arrayDepth, '[]').join('') : ''
|
||||
const typeString = `${data.type}${typeArraySuffix}`
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
data-is-first-item={isFirstItem}
|
||||
data-is-last-item={isLastItem}
|
||||
>
|
||||
<code className={`${styles.name} g-type-code`}>{name}</code>{' '}
|
||||
<code className={`${styles.typeString} g-type-code`}>{typeString}</code>{' '}
|
||||
{data.required ? (
|
||||
<span className={`${styles.requiredFlag} g-type-label-strong`}>
|
||||
Required
|
||||
</span>
|
||||
) : null}
|
||||
{data.title && (
|
||||
<p className={`${styles.title} g-type-body-small`}>{data.title}</p>
|
||||
)}
|
||||
{data.description && (
|
||||
<p className={`${styles.description} g-type-body-small`}>
|
||||
{data.description}
|
||||
</p>
|
||||
)}
|
||||
{hasProperties && (
|
||||
<div>
|
||||
<button
|
||||
className={`${styles.toggleButton} g-type-body-small`}
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
<span className={styles.toggleIcon} data-is-collapsed={isCollapsed}>
|
||||
<SvgrChevronDown width={16} />
|
||||
</span>
|
||||
{isCollapsed ? 'Show properties' : 'Hide properties'}
|
||||
</button>
|
||||
<Collapsible isCollapsed={isCollapsed}>
|
||||
<div className={styles.propertiesContainer}>
|
||||
{Object.keys(data.properties).map((propertyKey, idx) => {
|
||||
return (
|
||||
<PropertyObject
|
||||
key={propertyKey}
|
||||
name={propertyKey}
|
||||
data={data.properties[propertyKey]}
|
||||
isFirstItem={idx === 0}
|
||||
isLastItem={idx === Object.keys(data.properties).length - 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function arrayFrom(length, value = null) {
|
||||
let array = []
|
||||
for (var i = 0; i < length; i++) {
|
||||
array.push(value)
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
export default PropertyObject
|
||||
@@ -0,0 +1,85 @@
|
||||
.root {
|
||||
border-top: 1px solid var(--gray-6);
|
||||
border-bottom: 1px solid var(--gray-6);
|
||||
padding: 16px 0;
|
||||
margin: -1px 0 0 0;
|
||||
|
||||
&[data-is-first-item='true'] {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&[data-is-last-item='true'] {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
& .name {
|
||||
color: var(--brand);
|
||||
background: rgba(29, 30, 35, 0.07);
|
||||
border-radius: 3px;
|
||||
padding: 0.3em 0.625em;
|
||||
}
|
||||
|
||||
& .typeString {
|
||||
color: var(--gray-2);
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
& .requiredFlag {
|
||||
color: var(--danger-d1);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
& .title,
|
||||
& .description {
|
||||
color: var(--gray-3);
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
& .toggleButton {
|
||||
border: 1px solid red;
|
||||
border: none;
|
||||
color: var(--brand);
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0 0 0;
|
||||
padding: 0;
|
||||
|
||||
& .toggleIcon {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
transition: transform 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& svg {
|
||||
& [fill] {
|
||||
fill: var(--brand);
|
||||
}
|
||||
& [stroke] {
|
||||
stroke: var(--brand);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-collapsed='false'] {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
& .propertiesContainer {
|
||||
margin: 8px 0 0 8px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--gray-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import PropertyObject from '../property-object'
|
||||
|
||||
function ResponseObject({ data }) {
|
||||
// `schema` can be empty, which means the response does not return content
|
||||
// We currently only support object responses (ie those that have schema.properties) in the UI
|
||||
// Ref: https://swagger.io/specification/v2/#response-object
|
||||
if (!data || !data.schema || !data.schema.properties) {
|
||||
return <div>No content.</div>
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(data.schema.properties).map((propertyKey, idx) => {
|
||||
return (
|
||||
<PropertyObject
|
||||
key={propertyKey}
|
||||
name={propertyKey}
|
||||
data={data.schema.properties[propertyKey]}
|
||||
isFirstItem={idx === 0}
|
||||
isLastItem={idx === Object.keys(data.schema.properties).length - 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResponseObject
|
||||
@@ -0,0 +1,16 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import ResponseObject from '.'
|
||||
|
||||
function ResponseObjectProps(props) {
|
||||
return <ResponseObject {...props} />
|
||||
}
|
||||
|
||||
ResponseObjectProps.propTypes = {
|
||||
/** [Response Object](https://swagger.io/specification/v2/#response-object) data. */
|
||||
data: PropTypes.shape({
|
||||
/** A [Schema Object](https://swagger.io/specification/v2/#schema-object) that describes the response. Note that we currently only support objects (with schema.properties) responses in the UI. */
|
||||
schema: PropTypes.object,
|
||||
}),
|
||||
}
|
||||
|
||||
export default ResponseObjectProps
|
||||
@@ -0,0 +1,117 @@
|
||||
import { capitalCase } from 'change-case'
|
||||
import slugify from '@hashicorp/remark-plugins/generate_slug'
|
||||
|
||||
/* Gieven a schema, current service slug, and parentPath, return all props needed for an openapi component page */
|
||||
export function getPropsForPage(schema, params) {
|
||||
// parse the data we'll show to the user from the schema
|
||||
const operationObjects = getOperationObjects(schema)
|
||||
const serviceIds = getServiceIds(operationObjects)
|
||||
// info and sidenav data are needed on all pages
|
||||
const info = schema.info
|
||||
const sidenavOrder = getSidenavData(serviceIds)
|
||||
// If there's no "page" param, then this is the landing page
|
||||
const isLanding = !params || !params.page || params.page.length == 0
|
||||
// Otherwise, we should have an operationCategory that matches the slug-ified ID from the URL path
|
||||
const operationCategory = isLanding
|
||||
? false
|
||||
: serviceIds
|
||||
.filter((id) => getServiceSlug(id) === params.page[0])
|
||||
.map((serviceId) => {
|
||||
const name = capitalCase(serviceId)
|
||||
const slug = getServiceSlug(serviceId)
|
||||
const operations = operationObjects.filter(
|
||||
(o) => getServiceId(o.operationId) === serviceId
|
||||
)
|
||||
return { name, slug, operations }
|
||||
})[0]
|
||||
return { info, sidenavOrder, operationCategory }
|
||||
}
|
||||
|
||||
/* Given a schema, return all the paths we'll render for our openapi generated docs */
|
||||
export function getPathsFromSchema(schema) {
|
||||
// Assign each operation category to a URL using its slug-ified ID
|
||||
const operationObjects = getOperationObjects(schema)
|
||||
const slugs = getServiceIds(operationObjects).map(getServiceSlug)
|
||||
// We need a path for each "service"
|
||||
const paths = slugs.map((slug) => ({ params: { page: [slug] } }))
|
||||
// We also push a path for an "/" index page
|
||||
paths.push({ params: { page: [] } })
|
||||
return paths
|
||||
}
|
||||
|
||||
/* Given a list of service ids, return an object suitable to pass to <DocsSidenav />
|
||||
to render a flat list of services */
|
||||
function getSidenavData(serviceIds) {
|
||||
const order = serviceIds.map((serviceId) => {
|
||||
return {
|
||||
sidebar_title: capitalCase(serviceId),
|
||||
indexData: true,
|
||||
path: getServiceSlug(serviceId),
|
||||
}
|
||||
})
|
||||
return order
|
||||
}
|
||||
|
||||
/* Given a serviceId, which is typically PascalCase string, return a slugified kebab-case string (ie: pascal-case rather than pascalcase) */
|
||||
function getServiceSlug(serviceId) {
|
||||
return slugify(capitalCase(serviceId))
|
||||
}
|
||||
|
||||
/* Given an operationId, return the "serviceId" */
|
||||
function getServiceId(operationId) {
|
||||
// We expect operationIds to have two parts, separated by an underscore
|
||||
// The "serviceId" is the first part of the value
|
||||
return operationId.split('_')[0]
|
||||
}
|
||||
|
||||
/* Given a schema, return an array of unique operation "category" strings */
|
||||
export function getServiceIds(operationObjects) {
|
||||
const operationIdCategories = operationObjects
|
||||
.map((o) => getServiceId(o.operationId))
|
||||
.sort()
|
||||
// Several related operationIds may have the same "category" part,
|
||||
// so we filter for unique values before returning
|
||||
return filterUnique(operationIdCategories)
|
||||
}
|
||||
|
||||
/* Given an array of values, return an array without duplicate items */
|
||||
function filterUnique(array) {
|
||||
return array.filter((value, idx) => array.indexOf(value) === idx)
|
||||
}
|
||||
|
||||
/* Given a schema, return a flattened list of operation objects */
|
||||
function getOperationObjects(schema) {
|
||||
const pathItemObjects = Object.keys(schema.paths).reduce((acc, path) => {
|
||||
acc.push({ __path: path, ...schema.paths[path] })
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const operationObjects = pathItemObjects.reduce((acc, pathItemObject) => {
|
||||
// Each path can support many operations through different request types
|
||||
const requestTypes = [
|
||||
'get',
|
||||
'put',
|
||||
'post',
|
||||
'delete',
|
||||
'options',
|
||||
'head',
|
||||
'patch',
|
||||
]
|
||||
const pathOperations = requestTypes.reduce((acc, type) => {
|
||||
// Not all paths will support every request type
|
||||
if (!pathItemObject[type]) return acc
|
||||
// If the request type is supported, push the associated operation
|
||||
acc.push({
|
||||
__type: type,
|
||||
__path: pathItemObject.__path,
|
||||
...pathItemObject[type],
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
return acc.concat(pathOperations)
|
||||
}, [])
|
||||
|
||||
return operationObjects
|
||||
}
|
||||
|
||||
export default getPathsFromSchema
|
||||
@@ -22,7 +22,7 @@ export default function ProductSubnav() {
|
||||
url: 'https://github.com/hashicorp/boundary/releases',
|
||||
},
|
||||
]}
|
||||
currentPath={router.pathname}
|
||||
currentPath={router.asPath}
|
||||
menuItemsAlign="right"
|
||||
menuItems={subnavItems}
|
||||
constrainWidth
|
||||
|
||||
@@ -2,6 +2,9 @@ export default [
|
||||
{
|
||||
text: 'Docs',
|
||||
url: '/docs/getting-started',
|
||||
type: 'inbound',
|
||||
},
|
||||
{
|
||||
text: 'API',
|
||||
url: '/api-docs',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
|
||||
function useHover() {
|
||||
const [value, setValue] = useState(false)
|
||||
|
||||
const ref = useRef(null)
|
||||
const handleMouseOver = () => setValue(true)
|
||||
const handleMouseOut = () => setValue(false)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const node = ref.current
|
||||
if (node) {
|
||||
node.addEventListener('mouseover', handleMouseOver)
|
||||
node.addEventListener('mouseout', handleMouseOut)
|
||||
|
||||
return () => {
|
||||
node.removeEventListener('mouseover', handleMouseOver)
|
||||
node.removeEventListener('mouseout', handleMouseOut)
|
||||
}
|
||||
}
|
||||
},
|
||||
[ref.current] // Recall only if ref changes
|
||||
)
|
||||
|
||||
return [ref, value]
|
||||
}
|
||||
|
||||
export default useHover
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* Hook that runs a callback on clicks outside the ref
|
||||
*/
|
||||
function useOnClickOutside(ref, callbackFn) {
|
||||
useEffect(() => {
|
||||
function handleClick(event) {
|
||||
const isOutside = ref.current && !ref.current.contains(event.target)
|
||||
if (isOutside) callbackFn(event)
|
||||
}
|
||||
|
||||
// Bind the event listener
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener('mousedown', handleClick)
|
||||
}
|
||||
}, [ref])
|
||||
}
|
||||
|
||||
export default useOnClickOutside
|
||||
@@ -0,0 +1,8 @@
|
||||
const fs = require('fs')
|
||||
const RefParser = require('@apidevtools/json-schema-ref-parser')
|
||||
|
||||
export default async function parseSchema(filePath) {
|
||||
const content = JSON.parse(fs.readFileSync(filePath))
|
||||
const schema = await RefParser.dereference(content)
|
||||
return schema
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
# This file sets configuration for Netlify
|
||||
# ref: https://www.netlify.com/docs/netlify-toml-reference/
|
||||
|
||||
[build]
|
||||
publish = "out"
|
||||
command = "npm run static"
|
||||
|
||||
[context.production]
|
||||
environment = { HASHI_ENV = "production", NODE_ENV = "production"}
|
||||
|
||||
[context.deploy-preview]
|
||||
environment = { HASHI_ENV = "staging" }
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "SAMEORIGIN"
|
||||
Generated
+160
@@ -224,6 +224,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz",
|
||||
"integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==",
|
||||
"requires": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"call-me-maybe": "^1.0.1",
|
||||
"js-yaml": "^3.13.1"
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
||||
@@ -1671,6 +1681,11 @@
|
||||
"unist-util-visit": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
|
||||
},
|
||||
"@mapbox/rehype-prism": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/rehype-prism/-/rehype-prism-0.5.0.tgz",
|
||||
@@ -3770,6 +3785,15 @@
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
|
||||
},
|
||||
"camel-case": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz",
|
||||
"integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==",
|
||||
"requires": {
|
||||
"pascal-case": "^3.1.1",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
|
||||
@@ -3794,6 +3818,16 @@
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001135.tgz",
|
||||
"integrity": "sha512-ziNcheTGTHlu9g34EVoHQdIu5g4foc8EsxMGC7Xkokmvw0dqNtX8BS8RgCgFBaAiSp2IdjvBxNdh0ssib28eVQ=="
|
||||
},
|
||||
"capital-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.3.tgz",
|
||||
"integrity": "sha512-OlUSJpUr7SY0uZFOxcwnDOU7/MpHlKTZx2mqnDYQFrDudXLFm0JJ9wr/l4csB+rh2Ug0OPuoSO53PqiZBqno9A==",
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0",
|
||||
"upper-case-first": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"caw": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz",
|
||||
@@ -3856,6 +3890,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"change-case": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.1.tgz",
|
||||
"integrity": "sha512-qRlUWn/hXnX1R1LBDF/RelJLiqNjKjUqlmuBVSEIyye8kq49CXqkZWKmi8XeUAdDXWFOcGLUMZ+aHn3Q5lzUXw==",
|
||||
"requires": {
|
||||
"camel-case": "^4.1.1",
|
||||
"capital-case": "^1.0.3",
|
||||
"constant-case": "^3.0.3",
|
||||
"dot-case": "^3.0.3",
|
||||
"header-case": "^2.0.3",
|
||||
"no-case": "^3.0.3",
|
||||
"param-case": "^3.0.3",
|
||||
"pascal-case": "^3.1.1",
|
||||
"path-case": "^3.0.3",
|
||||
"sentence-case": "^3.0.3",
|
||||
"snake-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"character-entities": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
|
||||
@@ -4538,6 +4591,16 @@
|
||||
"resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz",
|
||||
"integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ="
|
||||
},
|
||||
"constant-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.3.tgz",
|
||||
"integrity": "sha512-FXtsSnnrFYpzDmvwDGQW+l8XK3GV1coLyBN0eBz16ZUzGaZcT2ANVCJmLeuw2GQgxKHQIe9e0w2dzkSfaRlUmA==",
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0",
|
||||
"upper-case": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"constants-browserify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
|
||||
@@ -5288,6 +5351,15 @@
|
||||
"domelementtype": "1"
|
||||
}
|
||||
},
|
||||
"dot-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz",
|
||||
"integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==",
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
@@ -6903,6 +6975,15 @@
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"header-case": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.3.tgz",
|
||||
"integrity": "sha512-LChe/V32mnUQnTwTxd3aAlNMk8ia9tjCDb/LjYtoMrdAPApxLB+azejUk5ERZIZdIqvinwv6BAUuFXH/tQPdZA==",
|
||||
"requires": {
|
||||
"capital-case": "^1.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.18.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz",
|
||||
@@ -8352,6 +8433,14 @@
|
||||
"signal-exit": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lower-case": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz",
|
||||
"integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==",
|
||||
"requires": {
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
|
||||
@@ -9116,6 +9205,15 @@
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"no-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz",
|
||||
"integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==",
|
||||
"requires": {
|
||||
"lower-case": "^2.0.1",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
@@ -9650,6 +9748,15 @@
|
||||
"readable-stream": "^2.1.5"
|
||||
}
|
||||
},
|
||||
"param-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz",
|
||||
"integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==",
|
||||
"requires": {
|
||||
"dot-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -9714,6 +9821,15 @@
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||
},
|
||||
"pascal-case": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz",
|
||||
"integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==",
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"pascalcase": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
|
||||
@@ -9724,6 +9840,15 @@
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
|
||||
},
|
||||
"path-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.3.tgz",
|
||||
"integrity": "sha512-UMFU6UETFpCNWbIWNczshPrnK/7JAXBP2NYw80ojElbQ2+JYxdqWDBkvvqM93u4u6oLmuJ/tPOf2tM8KtXv4eg==",
|
||||
"requires": {
|
||||
"dot-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"path-dirname": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
|
||||
@@ -11595,6 +11720,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sentence-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.3.tgz",
|
||||
"integrity": "sha512-ZPr4dgTcNkEfcGOMFQyDdJrTU9uQO1nb1cjf+nuzb6FxgMDgKddZOM29qEsB7jvsZSMruLRcL2KfM4ypKpa0LA==",
|
||||
"requires": {
|
||||
"no-case": "^3.0.3",
|
||||
"tslib": "^1.10.0",
|
||||
"upper-case-first": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
|
||||
@@ -11760,6 +11895,15 @@
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.5.tgz",
|
||||
"integrity": "sha512-WpECLAgYaxHoEAJ8Q1Lo8HOs1ngn7LN7QjXgOLbmmfkcWvosyk4ZTXkTzKyhngK640USTZUlgoQJfED1kz5fnQ=="
|
||||
},
|
||||
"snake-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.3.tgz",
|
||||
"integrity": "sha512-WM1sIXEO+rsAHBKjGf/6R1HBBcgbncKS08d2Aqec/mrDSpU80SiOU41hO7ny6DToHSyrlwTYzQBIK1FPSx4Y3Q==",
|
||||
"requires": {
|
||||
"dot-case": "^3.0.3",
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@@ -13687,6 +13831,22 @@
|
||||
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
|
||||
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
|
||||
},
|
||||
"upper-case": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.1.tgz",
|
||||
"integrity": "sha512-laAsbea9SY5osxrv7S99vH9xAaJKrw5Qpdh4ENRLcaxipjKsiaBwiAsxfa8X5mObKNTQPsupSq0J/VIxsSJe3A==",
|
||||
"requires": {
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"upper-case-first": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.1.tgz",
|
||||
"integrity": "sha512-105J8XqQ+9RxW3l9gHZtgve5oaiR9TIwvmZAMAIZWRHe00T21cdvewKORTlOJf/zXW6VukuTshM+HXZNWz7N5w==",
|
||||
"requires": {
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"version": "1.0.0",
|
||||
"author": "HashiCorp",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||
"@hashicorp/nextjs-scripts": "13.0.0-canary.1",
|
||||
"@hashicorp/react-content": "4.0.2",
|
||||
"@hashicorp/react-docs-page": "5.0.3",
|
||||
@@ -16,6 +17,7 @@
|
||||
"@hashicorp/react-subnav": "3.3.1",
|
||||
"@hashicorp/react-vertical-text-block-list": "2.0.3",
|
||||
"babel-plugin-import-glob-array": "0.2.0",
|
||||
"change-case": "^4.1.1",
|
||||
"fs-exists-sync": "0.1.0",
|
||||
"gray-matter": "4.0.2",
|
||||
"js-yaml": "3.14.0",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { productName, productSlug } from 'data/metadata'
|
||||
import path from 'path'
|
||||
import parseSwagger from '../../lib/swagger-parser'
|
||||
import OpenApiPage, {
|
||||
getPathsFromSchema,
|
||||
getPropsForPage,
|
||||
} from '../../components/openapi-page'
|
||||
|
||||
const targetFile = '../internal/gen/controller.swagger.json'
|
||||
const pathFromRoot = 'api-docs'
|
||||
|
||||
export default function ExamplePage(props) {
|
||||
return (
|
||||
<OpenApiPage
|
||||
{...props}
|
||||
productName={productName}
|
||||
productSlug={productSlug}
|
||||
pathFromRoot={pathFromRoot}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const schema = await parseSwagger(path.join(process.cwd(), targetFile))
|
||||
const paths = getPathsFromSchema(schema)
|
||||
return { paths, fallback: false }
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
const schema = await parseSwagger(path.join(process.cwd(), targetFile))
|
||||
const props = getPropsForPage(schema, params)
|
||||
return { props }
|
||||
}
|
||||
@@ -9,6 +9,12 @@ module.exports = [
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
// `/<path>/index.html` to /<path>
|
||||
{
|
||||
source: '/:splat*/index.html',
|
||||
destination: '/:splat*',
|
||||
permanent: true,
|
||||
},
|
||||
/////////////////////////////////
|
||||
// DOMAIN MODEL CONCEPTS
|
||||
/////////////////////////////////
|
||||
|
||||
Reference in new issue
Block a user