Skip to content

Commit

Permalink
feat(openapi): support versioned docs (#2177)
Browse files Browse the repository at this point in the history
* fix: width style issue with version switcher

* feat: add versionSwitcherSlot in OpenApiOverview

* feat: implement get-version-switcher-props

* feat: generate versionSwitcherProps, pass to view

* chore: rename prop for clarity

* feat: add NoIndexTagIfVersioned

* refactor: rethink and clarify missing versionData

* fix: consolidated ApiDocsVersionData type use
  • Loading branch information
zchsh authored Sep 27, 2023
1 parent 6fe67e6 commit a8b442c
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 37 deletions.
3 changes: 2 additions & 1 deletion src/components/version-switcher/version-switcher.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ https://app.asana.com/0/1202097197789424/1204412156894162/f

/* Make dropdown list fill container */
& > div {
width: 100%;
min-width: 100%;
width: max-content;
}

/* Make dropdown list fill container */
Expand Down
19 changes: 19 additions & 0 deletions src/lib/api-docs/fetch-cloud-api-version-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ async function fetchCloudApiVersionData(
}
return { versionId, releaseStage, sourceFile }
})

/**
* If we can't find _any version data at all_, this is unexpected. It means
* that while the target directory may exist in `hcp-specs` (since Octokit
* itself didn't 404 during the call to `getJsonFilesFromGithubDir), the
* `.json` OpenAPI files we need weren't found.
*
* If this is the case, we throw an error! We want to know when this route is
* completely failing to fetch the expected OpenAPI spec data.
*
* Note that within `getOpenApiDocsStaticProps`, we handle the subtler case
* of _specific version data_ not existing, and 404 rather than error.
*/
if (!versionData || versionData.length === 0) {
throw new Error(
`Unexpected error fetching HCP OpenAPI spec data from "${githubDir.path}" in the "${githubDir.owner}${githubDir.repo}" repo, at ref "${githubDir.ref}". The configured "githubDir" did not seem to return any OpenAPI "*.json" specs in the expected location or folder structure. Please ensure the "githubDir" points to the correct owner, repo, path, and ref. Under the target path, specs should be found in folders structured like "<releaseStage>/<versionId>/*.json".`
)
}

// Return the version data, sorted in descending order
return sortDateVersionData(versionData)
}
Expand Down
8 changes: 1 addition & 7 deletions src/pages/hcp/api-docs/vault-secrets/[[...page]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,9 @@ export const getStaticProps: GetStaticProps<
const versionData = await fetchCloudApiVersionData(
PAGE_CONFIG.githubSourceDirectory
)
// If we can't find any version data at all, render a 404 page.
if (!versionData) {
return { notFound: true }
}

// Generate static props based on page configuration, params, and versionData
return await getOpenApiDocsStaticProps({
// Pass page configuration
...PAGE_CONFIG,
// Pass context and versionData to getStaticProps, needed for versioning
context: { params },
versionData,
})
Expand Down
51 changes: 29 additions & 22 deletions src/views/open-api-docs-view/components/open-api-overview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface OpenApiOverviewProps {
serviceProductSlug: ProductSlug
statusIndicatorConfig?: StatusIndicatorConfig
contentSlot?: ReactNode
versionSwitcherSlot?: ReactNode
className?: string
}

Expand All @@ -42,31 +43,37 @@ export function OpenApiOverview({
serviceProductSlug,
statusIndicatorConfig,
contentSlot,
versionSwitcherSlot,
}: OpenApiOverviewProps) {
return (
<div className={s.overviewWrapper}>
<header className={s.header}>
<IconTile size="medium" className={s.icon}>
<ProductIcon productSlug={serviceProductSlug} />
</IconTile>
<span>
<h1 id={heading.id} className={s.heading}>
{heading.text}
</h1>
{statusIndicatorConfig ? (
<Status
endpointUrl={statusIndicatorConfig.endpointUrl}
pageUrl={statusIndicatorConfig.pageUrl}
/>
) : null}
</span>
<Badge
className={s.releaseStageBadge}
text={badgeText}
type="outlined"
size="small"
/>
</header>
<div className={s.headerAndVersionSwitcher}>
<header className={s.header}>
<IconTile size="medium" className={s.icon}>
<ProductIcon productSlug={serviceProductSlug} />
</IconTile>
<span>
<h1 id={heading.id} className={s.heading}>
{heading.text}
</h1>
{statusIndicatorConfig ? (
<Status
endpointUrl={statusIndicatorConfig.endpointUrl}
pageUrl={statusIndicatorConfig.pageUrl}
/>
) : null}
</span>
<Badge
className={s.releaseStageBadge}
text={badgeText}
type="outlined"
size="small"
/>
</header>
{versionSwitcherSlot ? (
<div className={s.versionSwitcherSlot}>{versionSwitcherSlot}</div>
) : null}
</div>
{contentSlot ? <section>{contentSlot}</section> : null}
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
display: flex;
flex-direction: column;
gap: 24px;

/* stylelint-disable-next-line property-no-unknown */
container-type: inline-size;
}

.headerAndVersionSwitcher {
display: flex;
flex-direction: column-reverse;
gap: 24px;

/* stylelint-disable-next-line at-rule-no-unknown */
@container (min-width: 640px) {
flex-direction: row;
justify-content: space-between;
gap: 16px;
}
}

.header {
Expand All @@ -15,7 +31,8 @@
flex-direction: column-reverse;
gap: 8px;

@media (--dev-dot-tablet-up) {
/* stylelint-disable-next-line at-rule-no-unknown */
@container (min-width: 640px) {
flex-direction: row;
gap: 16px;
}
Expand All @@ -34,7 +51,8 @@
.icon {
display: none;

@media (--dev-dot-tablet-up) {
/* stylelint-disable-next-line at-rule-no-unknown */
@container (min-width: 640px) {
display: block;
margin-top: 3px;
}
Expand All @@ -43,7 +61,12 @@
.releaseStageBadge {
text-transform: capitalize;

@media (--dev-dot-tablet-up) {
/* stylelint-disable-next-line at-rule-no-unknown */
@container (min-width: 640px) {
margin-top: 13px;
}
}

.versionSwitcherSlot {
flex-shrink: 0;
}
13 changes: 13 additions & 0 deletions src/views/open-api-docs-view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import SidebarLayout from 'layouts/sidebar-layout'
// Components
import BreadcrumbBar from 'components/breadcrumb-bar'
import NoIndexTagIfVersioned from 'components/no-index-tag-if-versioned'
import SidebarBackToLink from 'components/sidebar/components/sidebar-back-to-link'
import VersionSwitcher from 'components/version-switcher'
// Local
import {
OpenApiDocsMobileMenuLevels,
Expand All @@ -34,6 +36,8 @@ function OpenApiDocsView({
breadcrumbLinks,
statusIndicatorConfig,
serviceProductSlug,
versionSwitcherProps,
isVersionedUrl,
}: OpenApiDocsViewProps) {
return (
<SidebarLayout
Expand All @@ -57,11 +61,20 @@ function OpenApiDocsView({
<div className={s.paddedContainer}>
<div className={s.spaceBreadcrumbsOverview}>
<BreadcrumbBar links={breadcrumbLinks} />
<NoIndexTagIfVersioned isVersioned={isVersionedUrl} />
<OpenApiOverview
heading={topOfPageHeading}
badgeText={releaseStage}
serviceProductSlug={serviceProductSlug}
statusIndicatorConfig={statusIndicatorConfig}
versionSwitcherSlot={
versionSwitcherProps ? (
<VersionSwitcher
label={versionSwitcherProps.label}
options={versionSwitcherProps.options}
/>
) : null
}
contentSlot={
descriptionMdx ? (
<DescriptionMdx mdxRemoteProps={descriptionMdx} />
Expand Down
19 changes: 15 additions & 4 deletions src/views/open-api-docs-view/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getOperationProps,
groupOperations,
parseAndValidateOpenApiSchema,
getVersionSwitcherProps,
} from './utils'
// Types
import type {
Expand Down Expand Up @@ -47,10 +48,11 @@ export const getStaticPaths: GetStaticPaths<OpenApiDocsParams> = async () => {
if (isDeployPreview()) {
return { paths: [], fallback: 'blocking' }
}
// If we're in production, statically render the single view.
// If we're in production, statically render the single view,
// and use `fallback: blocking` for versioned views.
return {
paths: [{ params: { page: [] } }],
fallback: false,
fallback: 'blocking',
}
}

Expand Down Expand Up @@ -84,14 +86,15 @@ export async function getStaticProps({
* Parse the version to render, or 404 if a non-existent version is requested.
*/
const pathParts = context.params?.page
const versionId = pathParts?.length > 1 ? pathParts[0] : null
const versionId = pathParts?.length > 0 ? pathParts[0] : null
const isVersionedUrl = typeof versionId === 'string'
const defaultVersion = findDefaultVersion(versionData)
// Resolve the current version
let targetVersion: ApiDocsVersionData | undefined
if (isVersionedUrl) {
targetVersion = versionData.find((v) => v.versionId === versionId)
} else {
targetVersion = findDefaultVersion(versionData)
targetVersion = defaultVersion
}
// If we can't resolve the current version, render a 404 page
if (!targetVersion) {
Expand Down Expand Up @@ -147,6 +150,14 @@ export async function getStaticProps({
},
releaseStage: targetVersion.releaseStage,
descriptionMdx,
isVersionedUrl,
versionSwitcherProps: getVersionSwitcherProps({
projectName: schemaData.info.title,
versionData,
targetVersion,
defaultVersion,
basePath,
}),
operationGroups: stripUndefinedProperties(operationGroups),
navItems,
navResourceItems,
Expand Down
13 changes: 13 additions & 0 deletions src/views/open-api-docs-view/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { ProductData, ProductSlug } from 'types/products'
import type { GithubFile } from 'lib/fetch-github-file'
import type { GithubDir } from 'lib/fetch-github-file-tree'
import type { BreadcrumbLink } from 'components/breadcrumb-bar'
import type { VersionSwitcherProps } from 'components/version-switcher'
// Local
import type { PropertyDetailsSectionProps } from './components/operation-details'

Expand Down Expand Up @@ -158,6 +159,18 @@ export interface OpenApiDocsViewProps {
* `productData` for `hcp`, and a different `serviceProductSlug` here.
*/
serviceProductSlug: ProductSlug

/**
* Boolean to indicate whether the URL being rendered is a versioned URL,
* in which case we want to no-index the page.
*/
isVersionedUrl: boolean

/**
* Optional version data. Use this for API docs with multiple versions, the
* `label` and `options` here will be passed directly to `VersionSwitcher`.
*/
versionSwitcherProps?: VersionSwitcherProps
}

/**
Expand Down
69 changes: 69 additions & 0 deletions src/views/open-api-docs-view/utils/get-version-switcher-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

// Types
import type { VersionSwitcherProps } from 'components/version-switcher'
import type { ApiDocsVersionData } from 'lib/api-docs/types'

/**
* Given version data and other OpenAPI docs details,
* Return version switcher dropdown props for use in `OpenApiDocsView`.
*
* Note: If there is only one version, we return null.
*/
export function getVersionSwitcherProps({
projectName,
versionData,
targetVersion,
defaultVersion,
basePath,
}: {
projectName: string
versionData: ApiDocsVersionData[]
targetVersion: ApiDocsVersionData
defaultVersion: ApiDocsVersionData
basePath: string
}): VersionSwitcherProps | null {
// Return null early if we only have one version
if (versionData.length === 1) {
return null
}

// Otherwise, we have multiple versions, we need to build dropdown props
const label = projectName

// Each version becomes an option in the dropdown
const options = versionData.map(
({ versionId, releaseStage }: ApiDocsVersionData) => {
/**
* Determine the version label suffix to show.
* - Default case is to show the version only, no (suffix)
* - If this is the default version, show 'latest'. For information on
* what "default version" means, see `findDefaultVersion`.
* - If we have a defined releaseStage that isn't 'stable', show it
*/
const isLatest = versionId === defaultVersion.versionId
let versionLabelSuffix = ''
if (isLatest) {
versionLabelSuffix = ' (latest)'
} else if (releaseStage && releaseStage !== 'stable') {
versionLabelSuffix = ` (${releaseStage})`
}
// Construct the label to show in the dropdown
const label = versionId + versionLabelSuffix
// Construct the aria-label for the version dropdown.
const ariaLabel = `Choose a version of the API docs for ${projectName}. Currently viewing version ${label}.`
// Construct the `href` for this version, which is special if latest
const href = isLatest ? basePath : `${basePath}/${versionId}`
// Mark this version option as selected if it's the current option
const isSelected = versionId === targetVersion.versionId
// Return all the props
return { ariaLabel, href, isLatest, isSelected, label }
}
)

// Return the dropdown props
return { label, options }
}
1 change: 1 addition & 0 deletions src/views/open-api-docs-view/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export { groupOperations } from './group-operations'
export { parseAndValidateOpenApiSchema } from './parse-and-validate-schema'
export { sortDateVersionData } from './sort-date-version-data'
export { truncateHcpOperationPath } from './truncate-hcp-operation-path'
export { getVersionSwitcherProps } from './get-version-switcher-props'

1 comment on commit a8b442c

@vercel
Copy link

@vercel vercel bot commented on a8b442c Sep 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.