Skip to content

Commit a8b442c

Browse files
authored
feat(openapi): support versioned docs (#2177)
* 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
1 parent 6fe67e6 commit a8b442c

File tree

10 files changed

+188
-37
lines changed

10 files changed

+188
-37
lines changed

src/components/version-switcher/version-switcher.module.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ https://app.asana.com/0/1202097197789424/1204412156894162/f
2828

2929
/* Make dropdown list fill container */
3030
& > div {
31-
width: 100%;
31+
min-width: 100%;
32+
width: max-content;
3233
}
3334

3435
/* Make dropdown list fill container */

src/lib/api-docs/fetch-cloud-api-version-data/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,25 @@ async function fetchCloudApiVersionData(
6161
}
6262
return { versionId, releaseStage, sourceFile }
6363
})
64+
65+
/**
66+
* If we can't find _any version data at all_, this is unexpected. It means
67+
* that while the target directory may exist in `hcp-specs` (since Octokit
68+
* itself didn't 404 during the call to `getJsonFilesFromGithubDir), the
69+
* `.json` OpenAPI files we need weren't found.
70+
*
71+
* If this is the case, we throw an error! We want to know when this route is
72+
* completely failing to fetch the expected OpenAPI spec data.
73+
*
74+
* Note that within `getOpenApiDocsStaticProps`, we handle the subtler case
75+
* of _specific version data_ not existing, and 404 rather than error.
76+
*/
77+
if (!versionData || versionData.length === 0) {
78+
throw new Error(
79+
`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".`
80+
)
81+
}
82+
6483
// Return the version data, sorted in descending order
6584
return sortDateVersionData(versionData)
6685
}

src/pages/hcp/api-docs/vault-secrets/[[...page]].tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,9 @@ export const getStaticProps: GetStaticProps<
9292
const versionData = await fetchCloudApiVersionData(
9393
PAGE_CONFIG.githubSourceDirectory
9494
)
95-
// If we can't find any version data at all, render a 404 page.
96-
if (!versionData) {
97-
return { notFound: true }
98-
}
99-
95+
// Generate static props based on page configuration, params, and versionData
10096
return await getOpenApiDocsStaticProps({
101-
// Pass page configuration
10297
...PAGE_CONFIG,
103-
// Pass context and versionData to getStaticProps, needed for versioning
10498
context: { params },
10599
versionData,
106100
})

src/views/open-api-docs-view/components/open-api-overview/index.tsx

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface OpenApiOverviewProps {
3333
serviceProductSlug: ProductSlug
3434
statusIndicatorConfig?: StatusIndicatorConfig
3535
contentSlot?: ReactNode
36+
versionSwitcherSlot?: ReactNode
3637
className?: string
3738
}
3839

@@ -42,31 +43,37 @@ export function OpenApiOverview({
4243
serviceProductSlug,
4344
statusIndicatorConfig,
4445
contentSlot,
46+
versionSwitcherSlot,
4547
}: OpenApiOverviewProps) {
4648
return (
4749
<div className={s.overviewWrapper}>
48-
<header className={s.header}>
49-
<IconTile size="medium" className={s.icon}>
50-
<ProductIcon productSlug={serviceProductSlug} />
51-
</IconTile>
52-
<span>
53-
<h1 id={heading.id} className={s.heading}>
54-
{heading.text}
55-
</h1>
56-
{statusIndicatorConfig ? (
57-
<Status
58-
endpointUrl={statusIndicatorConfig.endpointUrl}
59-
pageUrl={statusIndicatorConfig.pageUrl}
60-
/>
61-
) : null}
62-
</span>
63-
<Badge
64-
className={s.releaseStageBadge}
65-
text={badgeText}
66-
type="outlined"
67-
size="small"
68-
/>
69-
</header>
50+
<div className={s.headerAndVersionSwitcher}>
51+
<header className={s.header}>
52+
<IconTile size="medium" className={s.icon}>
53+
<ProductIcon productSlug={serviceProductSlug} />
54+
</IconTile>
55+
<span>
56+
<h1 id={heading.id} className={s.heading}>
57+
{heading.text}
58+
</h1>
59+
{statusIndicatorConfig ? (
60+
<Status
61+
endpointUrl={statusIndicatorConfig.endpointUrl}
62+
pageUrl={statusIndicatorConfig.pageUrl}
63+
/>
64+
) : null}
65+
</span>
66+
<Badge
67+
className={s.releaseStageBadge}
68+
text={badgeText}
69+
type="outlined"
70+
size="small"
71+
/>
72+
</header>
73+
{versionSwitcherSlot ? (
74+
<div className={s.versionSwitcherSlot}>{versionSwitcherSlot}</div>
75+
) : null}
76+
</div>
7077
{contentSlot ? <section>{contentSlot}</section> : null}
7178
</div>
7279
)

src/views/open-api-docs-view/components/open-api-overview/open-api-overview.module.css

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@
77
display: flex;
88
flex-direction: column;
99
gap: 24px;
10+
11+
/* stylelint-disable-next-line property-no-unknown */
12+
container-type: inline-size;
13+
}
14+
15+
.headerAndVersionSwitcher {
16+
display: flex;
17+
flex-direction: column-reverse;
18+
gap: 24px;
19+
20+
/* stylelint-disable-next-line at-rule-no-unknown */
21+
@container (min-width: 640px) {
22+
flex-direction: row;
23+
justify-content: space-between;
24+
gap: 16px;
25+
}
1026
}
1127

1228
.header {
@@ -15,7 +31,8 @@
1531
flex-direction: column-reverse;
1632
gap: 8px;
1733

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

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

46-
@media (--dev-dot-tablet-up) {
64+
/* stylelint-disable-next-line at-rule-no-unknown */
65+
@container (min-width: 640px) {
4766
margin-top: 13px;
4867
}
4968
}
69+
70+
.versionSwitcherSlot {
71+
flex-shrink: 0;
72+
}

src/views/open-api-docs-view/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import SidebarLayout from 'layouts/sidebar-layout'
88
// Components
99
import BreadcrumbBar from 'components/breadcrumb-bar'
10+
import NoIndexTagIfVersioned from 'components/no-index-tag-if-versioned'
1011
import SidebarBackToLink from 'components/sidebar/components/sidebar-back-to-link'
12+
import VersionSwitcher from 'components/version-switcher'
1113
// Local
1214
import {
1315
OpenApiDocsMobileMenuLevels,
@@ -34,6 +36,8 @@ function OpenApiDocsView({
3436
breadcrumbLinks,
3537
statusIndicatorConfig,
3638
serviceProductSlug,
39+
versionSwitcherProps,
40+
isVersionedUrl,
3741
}: OpenApiDocsViewProps) {
3842
return (
3943
<SidebarLayout
@@ -57,11 +61,20 @@ function OpenApiDocsView({
5761
<div className={s.paddedContainer}>
5862
<div className={s.spaceBreadcrumbsOverview}>
5963
<BreadcrumbBar links={breadcrumbLinks} />
64+
<NoIndexTagIfVersioned isVersioned={isVersionedUrl} />
6065
<OpenApiOverview
6166
heading={topOfPageHeading}
6267
badgeText={releaseStage}
6368
serviceProductSlug={serviceProductSlug}
6469
statusIndicatorConfig={statusIndicatorConfig}
70+
versionSwitcherSlot={
71+
versionSwitcherProps ? (
72+
<VersionSwitcher
73+
label={versionSwitcherProps.label}
74+
options={versionSwitcherProps.options}
75+
/>
76+
) : null
77+
}
6578
contentSlot={
6679
descriptionMdx ? (
6780
<DescriptionMdx mdxRemoteProps={descriptionMdx} />

src/views/open-api-docs-view/server.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
getOperationProps,
1818
groupOperations,
1919
parseAndValidateOpenApiSchema,
20+
getVersionSwitcherProps,
2021
} from './utils'
2122
// Types
2223
import type {
@@ -47,10 +48,11 @@ export const getStaticPaths: GetStaticPaths<OpenApiDocsParams> = async () => {
4748
if (isDeployPreview()) {
4849
return { paths: [], fallback: 'blocking' }
4950
}
50-
// If we're in production, statically render the single view.
51+
// If we're in production, statically render the single view,
52+
// and use `fallback: blocking` for versioned views.
5153
return {
5254
paths: [{ params: { page: [] } }],
53-
fallback: false,
55+
fallback: 'blocking',
5456
}
5557
}
5658

@@ -84,14 +86,15 @@ export async function getStaticProps({
8486
* Parse the version to render, or 404 if a non-existent version is requested.
8587
*/
8688
const pathParts = context.params?.page
87-
const versionId = pathParts?.length > 1 ? pathParts[0] : null
89+
const versionId = pathParts?.length > 0 ? pathParts[0] : null
8890
const isVersionedUrl = typeof versionId === 'string'
91+
const defaultVersion = findDefaultVersion(versionData)
8992
// Resolve the current version
9093
let targetVersion: ApiDocsVersionData | undefined
9194
if (isVersionedUrl) {
9295
targetVersion = versionData.find((v) => v.versionId === versionId)
9396
} else {
94-
targetVersion = findDefaultVersion(versionData)
97+
targetVersion = defaultVersion
9598
}
9699
// If we can't resolve the current version, render a 404 page
97100
if (!targetVersion) {
@@ -147,6 +150,14 @@ export async function getStaticProps({
147150
},
148151
releaseStage: targetVersion.releaseStage,
149152
descriptionMdx,
153+
isVersionedUrl,
154+
versionSwitcherProps: getVersionSwitcherProps({
155+
projectName: schemaData.info.title,
156+
versionData,
157+
targetVersion,
158+
defaultVersion,
159+
basePath,
160+
}),
150161
operationGroups: stripUndefinedProperties(operationGroups),
151162
navItems,
152163
navResourceItems,

src/views/open-api-docs-view/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { ProductData, ProductSlug } from 'types/products'
1212
import type { GithubFile } from 'lib/fetch-github-file'
1313
import type { GithubDir } from 'lib/fetch-github-file-tree'
1414
import type { BreadcrumbLink } from 'components/breadcrumb-bar'
15+
import type { VersionSwitcherProps } from 'components/version-switcher'
1516
// Local
1617
import type { PropertyDetailsSectionProps } from './components/operation-details'
1718

@@ -158,6 +159,18 @@ export interface OpenApiDocsViewProps {
158159
* `productData` for `hcp`, and a different `serviceProductSlug` here.
159160
*/
160161
serviceProductSlug: ProductSlug
162+
163+
/**
164+
* Boolean to indicate whether the URL being rendered is a versioned URL,
165+
* in which case we want to no-index the page.
166+
*/
167+
isVersionedUrl: boolean
168+
169+
/**
170+
* Optional version data. Use this for API docs with multiple versions, the
171+
* `label` and `options` here will be passed directly to `VersionSwitcher`.
172+
*/
173+
versionSwitcherProps?: VersionSwitcherProps
161174
}
162175

163176
/**
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
// Types
7+
import type { VersionSwitcherProps } from 'components/version-switcher'
8+
import type { ApiDocsVersionData } from 'lib/api-docs/types'
9+
10+
/**
11+
* Given version data and other OpenAPI docs details,
12+
* Return version switcher dropdown props for use in `OpenApiDocsView`.
13+
*
14+
* Note: If there is only one version, we return null.
15+
*/
16+
export function getVersionSwitcherProps({
17+
projectName,
18+
versionData,
19+
targetVersion,
20+
defaultVersion,
21+
basePath,
22+
}: {
23+
projectName: string
24+
versionData: ApiDocsVersionData[]
25+
targetVersion: ApiDocsVersionData
26+
defaultVersion: ApiDocsVersionData
27+
basePath: string
28+
}): VersionSwitcherProps | null {
29+
// Return null early if we only have one version
30+
if (versionData.length === 1) {
31+
return null
32+
}
33+
34+
// Otherwise, we have multiple versions, we need to build dropdown props
35+
const label = projectName
36+
37+
// Each version becomes an option in the dropdown
38+
const options = versionData.map(
39+
({ versionId, releaseStage }: ApiDocsVersionData) => {
40+
/**
41+
* Determine the version label suffix to show.
42+
* - Default case is to show the version only, no (suffix)
43+
* - If this is the default version, show 'latest'. For information on
44+
* what "default version" means, see `findDefaultVersion`.
45+
* - If we have a defined releaseStage that isn't 'stable', show it
46+
*/
47+
const isLatest = versionId === defaultVersion.versionId
48+
let versionLabelSuffix = ''
49+
if (isLatest) {
50+
versionLabelSuffix = ' (latest)'
51+
} else if (releaseStage && releaseStage !== 'stable') {
52+
versionLabelSuffix = ` (${releaseStage})`
53+
}
54+
// Construct the label to show in the dropdown
55+
const label = versionId + versionLabelSuffix
56+
// Construct the aria-label for the version dropdown.
57+
const ariaLabel = `Choose a version of the API docs for ${projectName}. Currently viewing version ${label}.`
58+
// Construct the `href` for this version, which is special if latest
59+
const href = isLatest ? basePath : `${basePath}/${versionId}`
60+
// Mark this version option as selected if it's the current option
61+
const isSelected = versionId === targetVersion.versionId
62+
// Return all the props
63+
return { ariaLabel, href, isLatest, isSelected, label }
64+
}
65+
)
66+
67+
// Return the dropdown props
68+
return { label, options }
69+
}

src/views/open-api-docs-view/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export { groupOperations } from './group-operations'
1616
export { parseAndValidateOpenApiSchema } from './parse-and-validate-schema'
1717
export { sortDateVersionData } from './sort-date-version-data'
1818
export { truncateHcpOperationPath } from './truncate-hcp-operation-path'
19+
export { getVersionSwitcherProps } from './get-version-switcher-props'

0 commit comments

Comments
 (0)