From 3ca98db6130623703a892875c3e5c72d16455833 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:44:23 -0500 Subject: [PATCH] OpenAPI V2 - implement landing content (#2606) * port over components from existing view * implement landing-content to match upstream * add demo data to preview --- .../utils/get-props-from-preview-data.ts | 14 ++++ .../description-mdx.module.css | 16 ++++ .../components/description-mdx/index.tsx | 60 ++++++++++++++ .../components/status/index.tsx | 44 +++++++++++ .../components/status/status.module.css | 9 +++ .../status/utils/use-service-status.ts | 71 +++++++++++++++++ .../components/landing-content/index.tsx | 79 +++++++++++++++---- .../components/landing-content/server.ts | 22 ------ .../landing-content/style.module.css | 72 +++++++++++++++++ .../components/open-api-overview/index.tsx | 75 ++++++++++++++++++ .../open-api-overview/style.module.css | 67 ++++++++++++++++ src/views/open-api-docs-view-v2/index.tsx | 30 +++++-- src/views/open-api-docs-view-v2/server.ts | 34 +++++++- .../open-api-docs-view-v2/style.module.css | 21 +++++ src/views/open-api-docs-view-v2/types.ts | 73 ++++++++++++++++- 15 files changed, 641 insertions(+), 46 deletions(-) create mode 100644 src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/description-mdx.module.css create mode 100644 src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/index.tsx create mode 100644 src/views/open-api-docs-view-v2/components/landing-content/components/status/index.tsx create mode 100644 src/views/open-api-docs-view-v2/components/landing-content/components/status/status.module.css create mode 100644 src/views/open-api-docs-view-v2/components/landing-content/components/status/utils/use-service-status.ts delete mode 100644 src/views/open-api-docs-view-v2/components/landing-content/server.ts create mode 100644 src/views/open-api-docs-view-v2/components/landing-content/style.module.css create mode 100644 src/views/open-api-docs-view-v2/components/open-api-overview/index.tsx create mode 100644 src/views/open-api-docs-view-v2/components/open-api-overview/style.module.css create mode 100644 src/views/open-api-docs-view-v2/style.module.css diff --git a/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts b/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts index 2701fb4077..fe8d09b246 100644 --- a/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts +++ b/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts @@ -42,6 +42,12 @@ export default async function getPropsFromPreviewData( // Build page configuration based on the input values const pageConfig: OpenApiDocsViewV2Config = { basePath: '/open-api-docs-preview-v2', + breadcrumbLinksPrefix: [ + { + title: 'Developer', + url: '/', + }, + ], operationSlug, openApiJsonString: previewData.openApiJsonString, schemaTransforms, @@ -65,6 +71,14 @@ export default async function getPropsFromPreviewData( href: 'https://www.hashicorp.com/customer-success', }, ], + // Release stage badge, to demo this feature + releaseStage: 'Preview', + // Status indicator for HCP Services generally, to demo this feature + statusIndicatorConfig: { + pageUrl: 'https://status.hashicorp.com', + endpointUrl: + 'https://status.hashicorp.com/api/v2/components/0q55nwmxngkc.json', + }, } // If the user has requested to group operations by path, we'll do so // by providing a custom `getOperationGroupKey` function. If this is omitted, diff --git a/src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/description-mdx.module.css b/src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/description-mdx.module.css new file mode 100644 index 0000000000..7a3eeeb410 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/description-mdx.module.css @@ -0,0 +1,16 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +.root { + composes: hds-typography-body-300 from global; + + & > *:first-child { + margin-top: 0; + } + + & > *:last-child { + margin-bottom: 0; + } +} diff --git a/src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/index.tsx b/src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/index.tsx new file mode 100644 index 0000000000..47caae0db3 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/landing-content/components/description-mdx/index.tsx @@ -0,0 +1,60 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { MDXRemote, MDXRemoteSerializeResult } from 'lib/next-mdx-remote' +import { + MdxA, + MdxOrderedList, + MdxUnorderedList, + MdxListItem, + MdxTable, + MdxH1, + MdxH2, + MdxH3, + MdxH4, + MdxH5, + MdxH6, + MdxP, + MdxInlineCode, + MdxBlockquote, + MdxPre, +} from 'components/dev-dot-content/mdx-components' +import s from './description-mdx.module.css' + +type MdxComponents = Record JSX.Element> + +const DEFAULT_MDX_COMPONENTS: MdxComponents = { + a: MdxA, + blockquote: MdxBlockquote, + h1: MdxH1, + h2: MdxH2, + h3: MdxH3, + h4: MdxH4, + h5: MdxH5, + h6: MdxH6, + inlineCode: MdxInlineCode, + li: MdxListItem, + ol: MdxOrderedList, + p: MdxP, + pre: MdxPre, + table: MdxTable, + ul: MdxUnorderedList, +} + +/** + * Renders CommonMark-compliant markdown content using our established set + * of MDX custom components, via next-mdx-remote. + */ +export function DescriptionMdx({ + mdxRemoteProps, +}: { + mdxRemoteProps: MDXRemoteSerializeResult +}) { + return ( +
+ +
+ ) +} diff --git a/src/views/open-api-docs-view-v2/components/landing-content/components/status/index.tsx b/src/views/open-api-docs-view-v2/components/landing-content/components/status/index.tsx new file mode 100644 index 0000000000..070ff62c65 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/landing-content/components/status/index.tsx @@ -0,0 +1,44 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// Third-party +import { IconExternalLink16 } from '@hashicorp/flight-icons/svg-react/external-link-16' +// Components +import ServiceStatusBadge from 'components/service-status-badge' +import StandaloneLink from 'components/standalone-link' +// Local +import { useServiceStatus } from './utils/use-service-status' +// Types +import { StatusIndicatorConfig } from 'views/open-api-docs-view/types' +// Styles +import s from './status.module.css' + +/** + * Displays a `ServiceStatusBadge` with data from the provided `endpointUrl`, + * alongside an external link to the provided `pageUrl`. + * + * We expect the `endpointUrl` to be a status-page component data URL, like: + * - https://status.hashicorp.com/api/v2/components/{componentId}.json + * + * We expect the `pageUrl` to be a browser-friendly status page URL, such as: + * - https://status.hashicorp.com + */ +export function Status({ endpointUrl, pageUrl }: StatusIndicatorConfig) { + const status = useServiceStatus(endpointUrl) + return ( +
+ + } + iconPosition="trailing" + color="secondary" + href={pageUrl} + size="small" + opensInNewTab + /> +
+ ) +} diff --git a/src/views/open-api-docs-view-v2/components/landing-content/components/status/status.module.css b/src/views/open-api-docs-view-v2/components/landing-content/components/status/status.module.css new file mode 100644 index 0000000000..298b49acb3 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/landing-content/components/status/status.module.css @@ -0,0 +1,9 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +.wrapper { + display: flex; + gap: 8px; +} diff --git a/src/views/open-api-docs-view-v2/components/landing-content/components/status/utils/use-service-status.ts b/src/views/open-api-docs-view-v2/components/landing-content/components/status/utils/use-service-status.ts new file mode 100644 index 0000000000..36c163d50b --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/landing-content/components/status/utils/use-service-status.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { useEffect, useState } from 'react' +import { ServiceStatus } from 'components/service-status-badge' + +/** + * Hook to use the StatusPage service status at a given endpoint. + */ +export function useServiceStatus(endpointUrl: string) { + const [status, setStatus] = useState('loading') + + useEffect(() => { + const asyncEffect = async () => { + setStatus(await fetchServiceStatus(endpointUrl)) + } + asyncEffect() + }, [endpointUrl]) + + return status +} + +/** + * Fetches the service status from a given StatusPage component + * endpoint URL. URLs are expected to be something like: + * https://status.hashicorp.com/api/v2/components/{componentId}.json + * + * If the retrieved data does not match the expected shape + */ +async function fetchServiceStatus(url: string): Promise { + let status: ServiceStatus + try { + const data = (await fetchJson(url)) as ExpectedStatusPageData + status = data.component?.status + if (typeof status !== 'string') { + throw new Error( + `In the "useServiceStatus" hook, the status data did not match expected shape. Please ensure GET requests to the endpoint ${url} yield data with a string at "responseData.component.status".` + ) + } + } catch (e) { + console.error(`Failed to parse valid status page data from ${url}.`) + console.error(e) + // Return 'unknown' as a fallback. + status = 'unknown' + } + return status +} + +/** + * The shape of data we expect to receive from a provided `endpointUrl`. + */ +interface ExpectedStatusPageData { + component: { + status: ServiceStatus + } +} + +/** + * Fetch JSON data from a provided URL. + */ +async function fetchJson(url: string) { + const response = await fetch(url) + if (!response.ok) { + throw new Error( + `HTTP error when fetching from ${url}. Status: ${response.status}` + ) + } + return await response.json() +} diff --git a/src/views/open-api-docs-view-v2/components/landing-content/index.tsx b/src/views/open-api-docs-view-v2/components/landing-content/index.tsx index 0697ca00a1..c038ea497f 100644 --- a/src/views/open-api-docs-view-v2/components/landing-content/index.tsx +++ b/src/views/open-api-docs-view-v2/components/landing-content/index.tsx @@ -3,25 +3,76 @@ * SPDX-License-Identifier: MPL-2.0 */ +// Components +import Badge from 'components/badge' +import IconTile from 'components/icon-tile' +import ProductIcon from 'components/product-icon' +import StandaloneLink from '@components/standalone-link' +import { IconDownload16 } from '@hashicorp/flight-icons/svg-react/download-16' +// Local +import { Status } from './components/status' +import { DescriptionMdx } from './components/description-mdx' // Types -import type { OpenAPIV3 } from 'openapi-types' +import type { MDXRemoteSerializeResult } from 'lib/next-mdx-remote' +import type { StatusIndicatorConfig } from 'views/open-api-docs-view-v2/types' + +import type { ProductSlug } from 'types/products' +// Styles +import s from './style.module.css' export interface LandingContentProps { - /** - * TODO: discard once view can be identified without this - */ - _placeholder: any + badgeText: string + descriptionMdx?: MDXRemoteSerializeResult + heading: string + serviceProductSlug: ProductSlug + statusIndicatorConfig?: StatusIndicatorConfig + schemaFileString: string } -/** - * TODO: implement this content area - */ -export default function LandingContent(props: LandingContentProps) { +export function LandingContent({ + badgeText, + descriptionMdx, + heading, + serviceProductSlug, + statusIndicatorConfig, + schemaFileString, +}: LandingContentProps) { return ( - <> -
-				{JSON.stringify(props, null, 2)}
-			
- +
+
+
+ + + + +

{heading}

+ {statusIndicatorConfig ? ( + + ) : null} +
+ +
+
+ {descriptionMdx ? ( + + ) : null} + } + iconPosition="leading" + download="hcp.swagger.json" + href={`data:text/json;charset=utf-8,${encodeURIComponent( + schemaFileString + )}`} + /> +
) } diff --git a/src/views/open-api-docs-view-v2/components/landing-content/server.ts b/src/views/open-api-docs-view-v2/components/landing-content/server.ts deleted file mode 100644 index c245c8a958..0000000000 --- a/src/views/open-api-docs-view-v2/components/landing-content/server.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -// Types -import type { OpenAPIV3 } from 'openapi-types' -import type { LandingContentProps } from '.' - -/** - * TODO: transform the schemaData into useful props - */ -export default async function getLandingContentProps( - schemaData: OpenAPIV3.Document -): Promise { - return { - _placeholder: { - viewToImplement: 'Landing view for OpenAPI spec', - schemaSample: schemaData.info, - }, - } -} diff --git a/src/views/open-api-docs-view-v2/components/landing-content/style.module.css b/src/views/open-api-docs-view-v2/components/landing-content/style.module.css new file mode 100644 index 0000000000..6b15c6ebd6 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/landing-content/style.module.css @@ -0,0 +1,72 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +.overviewWrapper { + 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 { + display: flex; + align-items: flex-start; + flex-direction: column-reverse; + gap: 8px; + + /* stylelint-disable-next-line at-rule-no-unknown */ + @container (min-width: 640px) { + flex-direction: row; + gap: 16px; + } +} + +.heading { + composes: hds-typography-display-600 from global; + + /* Ensure we jump to the very top of the page when linking to this item */ + scroll-margin-top: calc(var(--total-scroll-offset) + 999vh); + font-weight: var(--token-typography-font-weight-bold); + color: var(--token-color-foreground-strong); + margin: 0 0 9px 0; +} + +.icon { + display: none; + + /* stylelint-disable-next-line at-rule-no-unknown */ + @container (min-width: 640px) { + display: block; + margin-top: 3px; + } +} + +.releaseStageBadge { + text-transform: capitalize; + + /* stylelint-disable-next-line at-rule-no-unknown */ + @container (min-width: 640px) { + margin-top: 13px; + } +} + +.versionSwitcherSlot { + flex-shrink: 0; +} diff --git a/src/views/open-api-docs-view-v2/components/open-api-overview/index.tsx b/src/views/open-api-docs-view-v2/components/open-api-overview/index.tsx new file mode 100644 index 0000000000..ba35eba660 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/open-api-overview/index.tsx @@ -0,0 +1,75 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// Components +import Badge from 'components/badge' +import IconTile from 'components/icon-tile' +import ProductIcon from 'components/product-icon' +// Local +import { Status } from '../landing-content/components/status' +// Types +import type { StatusIndicatorConfig } from 'views/open-api-docs-view/types' +import type { ReactNode } from 'react' +import type { ProductSlug } from 'types/products' +// Styles +import s from './open-api-overview.module.css' + +export interface OpenApiOverviewProps { + heading: { + text: string + id: string + } + badgeText: string + serviceProductSlug: ProductSlug + statusIndicatorConfig?: StatusIndicatorConfig + contentSlot?: ReactNode + versionSwitcherSlot?: ReactNode + className?: string +} + +/** + * Render an overview section for an Open API landing view. + */ +export function OpenApiOverview({ + heading, + badgeText, + serviceProductSlug, + statusIndicatorConfig, + contentSlot, + versionSwitcherSlot, +}: OpenApiOverviewProps) { + return ( +
+
+
+ + + + +

+ {heading.text} +

+ {statusIndicatorConfig ? ( + + ) : null} +
+ +
+ {versionSwitcherSlot ? ( +
{versionSwitcherSlot}
+ ) : null} +
+ {contentSlot ?
{contentSlot}
: null} +
+ ) +} diff --git a/src/views/open-api-docs-view-v2/components/open-api-overview/style.module.css b/src/views/open-api-docs-view-v2/components/open-api-overview/style.module.css new file mode 100644 index 0000000000..a50e64c87e --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/open-api-overview/style.module.css @@ -0,0 +1,67 @@ +.overviewWrapper { + 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 { + display: flex; + align-items: flex-start; + flex-direction: column-reverse; + gap: 8px; + + /* stylelint-disable-next-line at-rule-no-unknown */ + @container (min-width: 640px) { + flex-direction: row; + gap: 16px; + } +} + +.heading { + composes: hds-typography-display-600 from global; + + /* Ensure we jump to the very top of the page when linking to this item */ + scroll-margin-top: calc(var(--total-scroll-offset) + 999vh); + font-weight: var(--token-typography-font-weight-bold); + color: var(--token-color-foreground-strong); + margin: 0 0 9px 0; +} + +.icon { + display: none; + + /* stylelint-disable-next-line at-rule-no-unknown */ + @container (min-width: 640px) { + display: block; + margin-top: 3px; + } +} + +.releaseStageBadge { + text-transform: capitalize; + + /* stylelint-disable-next-line at-rule-no-unknown */ + @container (min-width: 640px) { + margin-top: 13px; + } +} + +.versionSwitcherSlot { + flex-shrink: 0; +} diff --git a/src/views/open-api-docs-view-v2/index.tsx b/src/views/open-api-docs-view-v2/index.tsx index 9d02e86b80..bae12dc389 100644 --- a/src/views/open-api-docs-view-v2/index.tsx +++ b/src/views/open-api-docs-view-v2/index.tsx @@ -13,12 +13,15 @@ import { import { OpenApiV2SidebarContents } from './components/sidebar' import { SidebarHorizontalRule } from '@components/sidebar/components' import { SidebarResourceLinks } from './components/sidebar-resource-links' -import LandingContent from './components/landing-content' +import { LandingContent } from './components/landing-content' import MobileMenuLevels from '@components/mobile-menu-levels' import OperationContent from './components/operation-content' import SidebarBackToLink from '@components/sidebar/components/sidebar-back-to-link' +import BreadcrumbBar from '@components/breadcrumb-bar' // Types import type { OpenApiDocsViewV2Props } from './types' +// Styles +import s from './style.module.css' /** * Placeholder view component for a new OpenAPI docs setup. @@ -29,6 +32,7 @@ import type { OpenApiDocsViewV2Props } from './types' export default function OpenApiDocsViewV2({ basePath, backToLink, + breadcrumbLinks, landingLink, operationLinkGroups, resourceLinks, @@ -89,12 +93,28 @@ export default function OpenApiDocsViewV2({ /> } > -
-
+
+
+ + {/** + * TODO: implement version selector, likely alongside breadcrumbs. + * Previously was part of "overview content", but now version + * selector will need to be present on individual operation pages + * as well. + */} {'operationContentProps' in restProps ? ( - ) : 'landingContentProps' in restProps ? ( - + ) : 'landingProps' in restProps ? ( + ) : null}
diff --git a/src/views/open-api-docs-view-v2/server.ts b/src/views/open-api-docs-view-v2/server.ts index 8e7a0214c0..f2b3912a44 100644 --- a/src/views/open-api-docs-view-v2/server.ts +++ b/src/views/open-api-docs-view-v2/server.ts @@ -8,7 +8,7 @@ import { stripUndefinedProperties } from 'lib/strip-undefined-props' import isAbsoluteUrl from 'lib/is-absolute-url' // Utils import getOperationContentProps from './components/operation-content/server' -import getLandingContentProps from './components/landing-content/server' +import { serialize } from 'lib/next-mdx-remote/serialize' import { getOperationObjects, OperationObject, @@ -32,6 +32,7 @@ import type { */ export async function getStaticProps({ basePath, + breadcrumbLinksPrefix, getOperationGroupKey = (o: OperationObject) => (o.tags.length && o.tags[0]) ?? 'Other', openApiJsonString, @@ -39,6 +40,8 @@ export async function getStaticProps({ schemaTransforms, theme = 'hcp', backToLink, + statusIndicatorConfig, + releaseStage, resourceLinks = [], }: OpenApiDocsViewV2Config): Promise { /** @@ -105,9 +108,27 @@ export async function getStaticProps({ /** * Gather props shared between the landing and individual operation views. */ + // Build breadcrumb links + const urlPath = [basePath, operationSlug].filter(Boolean).join('/') + const breadcrumbLinks = [...breadcrumbLinksPrefix] + breadcrumbLinks.push({ + title: schemaData.info.title, + url: basePath, + }) + // If we're on a specific operation page, add a breadcrumb link accordingly + if (operationSlug) { + breadcrumbLinks.push({ + title: operationSlug, + url: urlPath, + }) + } + // Mark the last breadcrumb link as the current page + breadcrumbLinks[breadcrumbLinks.length - 1].isCurrentPage = true + // Build shared props const sharedProps: SharedProps = { basePath, backToLink, + breadcrumbLinks, landingLink, operationLinkGroups, productData, @@ -127,7 +148,14 @@ export async function getStaticProps({ ) return stripUndefinedProperties({ ...sharedProps, operationContentProps }) } else { - const landingContentProps = await getLandingContentProps(schemaData) - return stripUndefinedProperties({ ...sharedProps, landingContentProps }) + const landingProps = { + heading: schemaData.info.title, + badgeText: releaseStage, + serviceProductSlug: theme, + statusIndicatorConfig, + descriptionMdx: await serialize(schemaData.info.description), + schemaFileString: openApiJsonString, + } + return stripUndefinedProperties({ ...sharedProps, landingProps }) } } diff --git a/src/views/open-api-docs-view-v2/style.module.css b/src/views/open-api-docs-view-v2/style.module.css new file mode 100644 index 0000000000..89ac6beb99 --- /dev/null +++ b/src/views/open-api-docs-view-v2/style.module.css @@ -0,0 +1,21 @@ +.paddedContainer { + display: flex; + flex-direction: column; + gap: 56px; + padding: 32px 24px 128px 24px; + + @media (--dev-dot-hide-mobile-menu) { + padding-right: 48px; + padding-left: 48px; + } + + @media (--dev-dot-desktop) { + gap: 64px; + } +} + +.spaceBreadcrumbsContent { + display: flex; + flex-direction: column; + gap: 16px; +} diff --git a/src/views/open-api-docs-view-v2/types.ts b/src/views/open-api-docs-view-v2/types.ts index 33a0a5d287..876cdd33cb 100644 --- a/src/views/open-api-docs-view-v2/types.ts +++ b/src/views/open-api-docs-view-v2/types.ts @@ -4,11 +4,12 @@ */ // Types -import type { LandingContentProps } from './components/landing-content' import type { OpenAPIV3 } from 'openapi-types' import type { OperationContentProps } from './components/operation-content' +import type { LandingContentProps } from './components/landing-content' import type { OperationObject } from './utils/get-operation-objects' import type { ProductData, ProductSlug } from 'types/products' +import type { BreadcrumbLink } from '@components/breadcrumb-bar' /** * Shared props are common to both the "landing" and "operation" views. @@ -19,6 +20,32 @@ export interface SharedProps { text: string href: string } + breadcrumbLinks: { + title: string + /** + * Note: our BreadcrumbBar component has an interface that implies it + * supports items without `url` values, but it actually filters out those + * items and never renders them. We likely want to update either the + * interface, to require URLs, or the component, to render items without + * URLs. The latter might make sense if we're conceptualizing breadcrumb + * links as serving both a navigational AND "orienting" purpose, as we + * can then show an accurate hierarchical structure, even if a given page + * in that structure doesn't exist (eg `/hcp/api-docs`). + * + * For example... + * + * When strictly requiring URLs, we might have: + * URL: `/hcp/api-docs/vault-secrets` + * Breadcrumb: `Developer > HCP > Vault Secrets` + * This breadcrumb does NOT show that we're looking at API docs. + * + * When allowing items without URLs, we might have: + * URL: `/hcp/api-docs/vault-secrets` + * Breadcrumb: `Developer > HCP > API Docs > Vault Secrets` + */ + url?: string + isCurrentPage?: boolean + }[] landingLink: { text: string href: string @@ -41,6 +68,21 @@ export interface SharedProps { }[] } +/** + * Configure a status indicator for a status-page service. + */ +export interface StatusIndicatorConfig { + /** + * A status-page component URL we can GET JSON data from, in the format + * `https://status.hashicorp.com/api/v2/components/{componentId}.json`. + */ + endpointUrl: string + /** + * A browser-friendly status page URL, like `https://status.hashicorp.com` + */ + pageUrl: string +} + /** * OpenApiDocsViewV2 props are used to render either a "landing" view, which * includes some introductory content to the API generally, or an "operation" @@ -48,7 +90,7 @@ export interface SharedProps { */ export type OpenApiDocsViewV2Props = | (SharedProps & { operationContentProps: OperationContentProps }) - | (SharedProps & { landingContentProps: LandingContentProps }) + | (SharedProps & { landingProps: LandingContentProps }) /** * OpenApiDocsViewV2Config is used to set up and configure a set of @@ -65,6 +107,24 @@ export interface OpenApiDocsViewV2Config { * `developer.hashicorp.com/hcp/api-docs/hcp-vault-secrets` has the basePath * `/hcp/api-docs/hcp-vault-secrets`. */ basePath: string + /** + * Optional breadcrumb links to render before the generated breadcrumb links. + * We build breadcrumbs like `[...breadcrumbLinksPrefix, ...generatedLinks]`, + * where `generatedLinks` are: + * - `` - the OpenAPI spec's `title` will be used as text + * - `/` - operation slugs used as text + * + * As an example, for HCP Vault Secrets, we might set `breadcrumbLinksPrefix` + * to include links leading up to the basePath, like: + * - { title: 'Developer', url: '/' }, + * - { title: 'HCP', url: '/hcp' }, + * - { title: 'API Docs', url: '/hcp/api-docs' } + * And, for example, the generated breadcrumbs that would be added for a + * `SetTier` operation page might look like: + * - { title: 'HCP Vault Secrets', url: '/hcp/api-docs/vault-secrets' } + * - { title: 'SetTier', url: '/hcp/api-docs/vault-secrets/SetTier' } + */ + breadcrumbLinksPrefix?: BreadcrumbLink[] /** * Optional link to move up a level of context. Typically used to link * back to higher level documentation for the subject of the OpenAPI docs. k @@ -111,4 +171,13 @@ export interface OpenApiDocsViewV2Config { text: string href: string }[] + /** + * Configuration to set up a status indicator. + */ + statusIndicatorConfig?: StatusIndicatorConfig + /** + * Optional release stage to display in a badge at the top of the page. + * Typical values include `Stable`, `Preview`, etc. + */ + releaseStage?: string }