-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: stub in OpenApiDocsView preview tool (#2156)
* feat: add api route to generate props * feat: add necessary basic input components * fix: make statusIndicatorConfig optional * feat: function to call API route for static props * feat: rough in input components, set up the view * feat: add page file, don't render in prod * chore: tidy up breadcrumb links * chore: fix up inaccurate basePath * fix: IS_PRODUCTION logic * chore: use HASHI_ENV to follow convention * fix: fix token for visible text in light mode * feat: support helperText on inputs * feat: add helperText to inputs * fix: better error handling for invalid schemas * fix: bad assumption around response definition * fix: support arbitrary content type keys * chore: minor form improvements * style: rename props state for clarity
- Loading branch information
Showing
13 changed files
with
561 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { getStaticProps } from 'views/open-api-docs-view/server' | ||
// Types | ||
import type { NextApiRequest, NextApiResponse } from 'next' | ||
import type { OpenAPIV3 } from 'openapi-types' | ||
import type { ProductSlug } from 'types/products' | ||
|
||
/** | ||
* Boilerplate page configuration, we could in theory expose this so visitors | ||
* to the preview tool could manipulate it, but we intentionally just | ||
* hard-code here to keep the focus of the preview tool on OpenAPI spec | ||
* contents. | ||
*/ | ||
const GENERIC_PAGE_CONFIG = { | ||
// basePath same no matter what, preview tool is on static route | ||
basePath: '/open-api-docs-preview', | ||
// No versioning in the preview tool, focus on one spec file at a time | ||
context: { params: { page: [] } }, | ||
// Product slug, using HCP to just show a generic HashiCorp logo, | ||
// so that the preview tool's focus can remain on the spec file contents | ||
productSlug: 'hcp' as ProductSlug, | ||
// Generic resource items, we can set more specific ones closer to launch | ||
navResourceItems: [ | ||
{ | ||
title: 'Tutorial Library', | ||
href: '/tutorials/library', | ||
}, | ||
{ | ||
title: 'Certifications', | ||
href: '/certifications', | ||
}, | ||
{ | ||
title: 'Community', | ||
href: 'https://discuss.hashicorp.com/', | ||
}, | ||
{ | ||
title: 'Support', | ||
href: 'https://www.hashicorp.com/customer-success', | ||
}, | ||
], | ||
} | ||
|
||
/** | ||
* We expected posted data to be an OpenAPI spec in JSON format. | ||
* We also allow an optional schema `info.description`, which normally would be | ||
* included in the spec content, so that authors can more easily develop | ||
* and preview their `info.description` content using our preview tool. | ||
*/ | ||
type ExpectedBody = { | ||
openApiJsonString: string | ||
openApiDescription: string | ||
} | ||
|
||
export default async function handler( | ||
req: NextApiRequest, | ||
res: NextApiResponse | ||
) { | ||
// Reject non-POST requests, only POST is allowed | ||
if (req.method !== 'POST') { | ||
res.setHeader('Allow', ['POST']) | ||
res.status(405).json({ error: 'Method not allowed' }) | ||
} | ||
|
||
/** | ||
* Build the static props from the POST'ed page configuration data, | ||
* which includes the full OpenAPI spec as a string. | ||
*/ | ||
const { openApiDescription, openApiJsonString } = JSON.parse( | ||
req.body | ||
) as ExpectedBody | ||
|
||
/** | ||
* Construct some preview data just to match the expected `getStaticProps` | ||
* signature. The `versionId` and `releaseStage` don't really matter here. | ||
*/ | ||
const versionData = [ | ||
{ | ||
versionId: 'preview', | ||
releaseStage: 'preview', | ||
sourceFile: openApiJsonString, | ||
}, | ||
] | ||
|
||
/** | ||
* Build static props for the page | ||
*/ | ||
try { | ||
const staticProps = await getStaticProps({ | ||
// Pass the bulk of the page config | ||
...GENERIC_PAGE_CONFIG, | ||
// Pass the constructed version data | ||
versionData, | ||
/** | ||
* Massage the schema data a little bit, replacing | ||
* "HashiCorp Cloud Platform" in the title with "HCP". | ||
*/ | ||
massageSchemaForClient: (schemaData: OpenAPIV3.Document) => { | ||
// Replace the schema description with the POST'ed description, if present | ||
if (openApiDescription) { | ||
schemaData.info.description = openApiDescription | ||
} | ||
// Replace "HashiCorp Cloud Platform" with "HCP" in the title | ||
const massagedTitle = schemaData.info.title.replace( | ||
'HashiCorp Cloud Platform', | ||
'HCP' | ||
) | ||
// Return the schema data with the revised title | ||
const massagedInfo = { ...schemaData.info, title: massagedTitle } | ||
return { ...schemaData, info: massagedInfo } | ||
}, | ||
}) | ||
// Return the static props as JSON, these can be passed to OpenApiDocsView | ||
res.status(200).json(staticProps) | ||
} catch (error) { | ||
res.status(200).json({ error: error.toString() }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
|
||
import { GetStaticPropsResult } from 'next' | ||
import OpenApiDocsPreviewView from 'views/open-api-docs-preview' | ||
|
||
const IS_PRODUCTION = process.env.HASHI_ENV === 'production' | ||
|
||
/** | ||
* We don't actually need static props for this page, | ||
* we use `getStaticProps` here to prevent the page from being rendered | ||
* in production. | ||
*/ | ||
export async function getStaticProps(): Promise< | ||
GetStaticPropsResult<Record<string, never>> | ||
> { | ||
/** | ||
* In production, return a 404 not found for this page. | ||
* In other environments (local, preview, and staging), show the page. | ||
*/ | ||
return IS_PRODUCTION ? { notFound: true } : { props: {} } | ||
} | ||
|
||
export default OpenApiDocsPreviewView |
9 changes: 9 additions & 0 deletions
9
src/views/open-api-docs-preview/components/file-string-input/file-string-input.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.root { | ||
display: flex; | ||
flex-direction: column; | ||
gap: 8px; | ||
} | ||
|
||
.helperText { | ||
color: var(--token-form-helper-text-color); | ||
} |
46 changes: 46 additions & 0 deletions
46
src/views/open-api-docs-preview/components/file-string-input/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { useId, type ChangeEvent } from 'react' | ||
import s from './file-string-input.module.css' | ||
|
||
/** | ||
* Render a very basic file input. | ||
* | ||
* This is a temporary solution until we have a React version of FileInput | ||
* set up, which we'd likely do in the `web` monorepo. | ||
* | ||
* For now, this felt sufficient for an internal preview tool for OpenAPI specs. | ||
*/ | ||
export function FileStringInput({ | ||
label, | ||
helperText, | ||
accept, | ||
setValue, | ||
}: { | ||
label: string | ||
helperText?: string | ||
accept: string | ||
setValue: (fileString: string) => void | ||
}) { | ||
const id = useId() | ||
|
||
function handleFileInputChange(e: ChangeEvent<HTMLInputElement>) { | ||
const fileReader = new FileReader() | ||
fileReader.readAsText(e.target.files[0], 'UTF-8') | ||
fileReader.onload = (e: ProgressEvent<FileReader>) => | ||
setValue(e.target.result.toString()) | ||
} | ||
|
||
return ( | ||
<div className={s.root}> | ||
<div> | ||
<label htmlFor={id}>{label}</label> | ||
{helperText ? <div className={s.helperText}>{helperText}</div> : null} | ||
</div> | ||
<input | ||
id={id} | ||
type="file" | ||
accept={accept} | ||
onChange={handleFileInputChange} | ||
/> | ||
</div> | ||
) | ||
} |
149 changes: 149 additions & 0 deletions
149
src/views/open-api-docs-preview/components/open-api-preview-inputs/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
// Third-party | ||
import { useEffect, useState } from 'react' | ||
import classNames from 'classnames' | ||
// Components | ||
import Button from 'components/button' | ||
import InlineAlert from 'components/inline-alert' | ||
// Inputs | ||
import { FileStringInput } from '../file-string-input' | ||
import { TextareaInput } from '../textarea-input' | ||
// Utils | ||
import { fetchOpenApiStaticProps } from './utils/fetch-open-api-static-props' | ||
// Types | ||
import type { OpenApiDocsViewProps } from 'views/open-api-docs-view/types' | ||
// Styles | ||
import s from './open-api-preview-inputs.module.css' | ||
|
||
interface InputValues { | ||
openApiJsonString: string | ||
openApiDescription: string | ||
} | ||
|
||
/** | ||
* Render a fixed panel container with inputs that allow control over the | ||
* static props for `OpenApiDocsView`. | ||
*/ | ||
export function OpenApiPreviewInputs({ | ||
setStaticProps, | ||
}: { | ||
setStaticProps: (v: OpenApiDocsViewProps) => void | ||
}) { | ||
const [error, setError] = useState<{ | ||
title: string | ||
description: string | ||
error: string | ||
}>() | ||
const [isLoading, setIsLoading] = useState(false) | ||
const [isCollapsed, setIsCollapsed] = useState(false) | ||
const [inputValues, setInputValues] = useState<InputValues>({ | ||
openApiJsonString: '', | ||
openApiDescription: '', | ||
}) | ||
|
||
/** | ||
* Helper to set a specific input data value. | ||
*/ | ||
function setInputValue(key: keyof InputValues, value: string) { | ||
setInputValues((p: InputValues) => ({ ...p, [key]: value })) | ||
} | ||
|
||
/** | ||
* Whenever an input value changes, reset the error | ||
*/ | ||
useEffect(() => { | ||
if (error) { | ||
setError(undefined) | ||
} | ||
// Note: intentionally not using exhaustive deps, reset based on input only | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [inputValues]) | ||
|
||
/** | ||
* Pre-fill the description markdown when a new OpenAPI JSON string is set. | ||
*/ | ||
useEffect(() => { | ||
// Avoid overwriting an in-progress description. | ||
if (inputValues.openApiDescription !== '') { | ||
return | ||
} | ||
try { | ||
// try to parse the input JSON, and set the description accordingly | ||
const parsed = JSON.parse(inputValues.openApiJsonString) | ||
const parsedValue = parsed?.info?.description | ||
if (parsedValue && inputValues.openApiDescription !== parsedValue) { | ||
setInputValue('openApiDescription', parsedValue) | ||
} | ||
} catch (e) { | ||
// do nothing if parsing fails | ||
} | ||
}, [inputValues]) | ||
|
||
/** | ||
* Fetch static props for the page and update state when | ||
* the provided `inputValues` is submitted via a button activation. | ||
*/ | ||
async function updateStaticProps() { | ||
setIsLoading(true) | ||
const [err, result] = await fetchOpenApiStaticProps(inputValues) | ||
err ? setError(err) : setStaticProps(result) | ||
setIsLoading(false) | ||
} | ||
|
||
/** | ||
* Render the input panel | ||
*/ | ||
return ( | ||
<div className={classNames(s.root, { [s.isCollapsed]: isCollapsed })}> | ||
<div className={s.scrollableContent}> | ||
<div className={s.inputs}> | ||
<FileStringInput | ||
label="OpenAPI File" | ||
helperText='Upload your OpenAPI specification file, in ".json" format.' | ||
accept=".json" | ||
setValue={(v: string) => setInputValue('openApiJsonString', v)} | ||
/> | ||
<Button | ||
className={s.generateButton} | ||
text={isLoading ? 'Loading...' : 'Generate preview'} | ||
size="large" | ||
onClick={() => updateStaticProps()} | ||
/> | ||
{error ? ( | ||
<InlineAlert | ||
color="critical" | ||
title={error.title} | ||
description={ | ||
<> | ||
<div>{error.description}</div> | ||
<pre style={{ whiteSpace: 'pre-wrap' }}> | ||
<code>{error.error}</code> | ||
</pre> | ||
</> | ||
} | ||
/> | ||
) : null} | ||
<TextareaInput | ||
label="Schema source" | ||
helperText="Test out edits to the uploaded OpenAPI specification file." | ||
value={inputValues.openApiJsonString} | ||
setValue={(v: string) => setInputValue('openApiJsonString', v)} | ||
/> | ||
<TextareaInput | ||
label="Description Markdown" | ||
helperText='Enter markdown here to override the "schema.info.description" field of your schema.' | ||
value={inputValues.openApiDescription} | ||
setValue={(v: string) => setInputValue('openApiDescription', v)} | ||
/> | ||
</div> | ||
</div> | ||
<div className={s.collapseButtonLayout}> | ||
<button | ||
className={s.collapseButton} | ||
onClick={() => setIsCollapsed((p: boolean) => !p)} | ||
> | ||
{isCollapsed ? 'Show' : 'Hide'} preview inputs | ||
</button> | ||
</div> | ||
</div> | ||
) | ||
} |
Oops, something went wrong.
89b4271
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
dev-portal – ./
docs.hashicorp.com
dev-portal-git-main-hashicorp.vercel.app
developer.hashicorp.com
dev-portal-hashicorp.vercel.app