diff --git a/src/views/open-api-docs-view-v2/components/operation-content/server.ts b/src/views/open-api-docs-view-v2/components/operation-content/server.ts index cfec358b2b..63006201f5 100644 --- a/src/views/open-api-docs-view-v2/components/operation-content/server.ts +++ b/src/views/open-api-docs-view-v2/components/operation-content/server.ts @@ -6,6 +6,7 @@ // Types import type { OpenAPIV3 } from 'openapi-types' import type { OperationContentProps } from '.' +import { getOperationObjects } from 'views/open-api-docs-view-v2/utils/get-operation-objects' /** * TODO: transform the schemaData into useful props @@ -14,10 +15,15 @@ export default async function getOperationContentProps( operationSlug: string, schemaData: OpenAPIV3.Document ): Promise { + const operationObjects = getOperationObjects(schemaData) + const operationObject = operationObjects.find( + (operationObject) => operationObject.operationId === operationSlug + ) return { _placeholder: { viewToImplement: `Operation view for ${operationSlug}`, schemaSample: schemaData.info, + operationObject, }, } } diff --git a/src/views/open-api-docs-view-v2/components/sidebar/index.tsx b/src/views/open-api-docs-view-v2/components/sidebar/index.tsx new file mode 100644 index 0000000000..d50238f25e --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/sidebar/index.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// Components +import SidebarBackToLink from '@components/sidebar/components/sidebar-back-to-link' +import { SidebarNavMenuItem } from '@components/sidebar/components' +// Types +import type { OpenApiNavItem } from 'views/open-api-docs-view-v2/types' +// Styles +import s from './style.module.css' + +/** + * TODO: lift this content up so it can vary page-to-page + */ +const SHIM_CONTENT = { + backToLink: { + text: 'HashiCorp Cloud Platform', + href: '/hcp', + }, +} + +export function OpenApiV2SidebarContents({ navItemLanding, navItemGroups }) { + /** + * TODO: refine generation of nav items, and then render them properly, + * for now just messily rendering some links to enable navigation. + * + * Note: `next/link` will work in prod, since we'll be doing + * `getStaticProps`... but in the preview tool, `next/link` seems to + * make the preview experience janky, seemingly requiring reloads after + * each navigation, maybe related to use of getServerSideProps? Not yet + * sure how to resolve this, there's probably some clever solution that + * might be possible... + */ + return ( + <> + + + + ) +} +/** + * Renders an unordered list of nav items, with list styles reset. + */ +export function SidebarNavMenuItemsList({ + items, +}: { + items: OpenApiNavItem[] +}) { + return ( + + ) +} diff --git a/src/views/open-api-docs-view-v2/components/sidebar/style.module.css b/src/views/open-api-docs-view-v2/components/sidebar/style.module.css new file mode 100644 index 0000000000..2663d3eff8 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/sidebar/style.module.css @@ -0,0 +1,6 @@ +.listResetStyles { + list-style: none; + margin: 0; + padding: 0; + width: 100%; +} diff --git a/src/views/open-api-docs-view-v2/index.tsx b/src/views/open-api-docs-view-v2/index.tsx index 9b77b06dda..aa71ad9406 100644 --- a/src/views/open-api-docs-view-v2/index.tsx +++ b/src/views/open-api-docs-view-v2/index.tsx @@ -8,6 +8,7 @@ import SidebarLayout from 'layouts/sidebar-layout' // Components import LandingContent from './components/landing-content' import OperationContent from './components/operation-content' +import { OpenApiV2SidebarContents } from './components/sidebar' // Types import type { OpenApiDocsViewV2Props } from './types' @@ -19,40 +20,18 @@ import type { OpenApiDocsViewV2Props } from './types' */ export default function OpenApiDocsViewV2({ basePath, - navItems, + navItemLanding, + navItemGroups, ...restProps }: OpenApiDocsViewV2Props) { + // return ( - {navItems.map((navItem) => { - if (!('fullPath' in navItem)) { - return null - } - return ( -
  • - - {navItem.title} - -
  • - ) - })} - + } /** * TODO: implement mobile menu. May be tempting to try to re-use the data @@ -64,6 +43,15 @@ export default function OpenApiDocsViewV2({ mobileMenuSlot={null} >
    +
    + {'_sidebarPlaceholder' in restProps ? ( +
    +							
    +								{JSON.stringify(restProps._sidebarPlaceholder, null, 2)}
    +							
    +						
    + ) : null} +
    {'operationContentProps' in restProps ? ( diff --git a/src/views/open-api-docs-view-v2/server.ts b/src/views/open-api-docs-view-v2/server.ts index 84b4533cbb..28055f11ee 100644 --- a/src/views/open-api-docs-view-v2/server.ts +++ b/src/views/open-api-docs-view-v2/server.ts @@ -8,11 +8,90 @@ import { parseAndValidateOpenApiSchema } from 'lib/api-docs/parse-and-validate-o // Utils import getOperationContentProps from './components/operation-content/server' import getLandingContentProps from './components/landing-content/server' +import { getOperationObjects } from './utils/get-operation-objects' +import { buildOperationGroups } from './utils/build-operation-groups' +import { wordBreakCamelCase } from './utils/word-break-camel-case' // Types import type { OpenApiDocsViewV2Props, + OpenApiNavItem, SharedProps, } from 'views/open-api-docs-view-v2/types' +// import { stripUndefinedProperties } from 'lib/strip-undefined-props' + +/** + * TODO: lift this content up so it can vary page-to-page + * + * THOUGHT: rather than require custom YAML, maybe we should use a second layer + * of "tags" to group operations further? Maybe the custom YAML could be used + * at the "spec hook" stage... to add that second layer of tags. And then our + * operation grouping logic could be based on tags. This would open the door + * to have these changes made at the content source in the near future (at + * which point we'd remove the "spec hook" stage). + * + * Reference doc: + * https://swagger.io/docs/specification/v3_0/grouping-operations-with-tags/ + */ +const SHIM_CONTENT = { + operationGroupings: [ + { + title: 'Apps', + operationIds: [ + 'CreateApp', + 'ListApps', + 'GetApp', + 'UpdateApp', + 'DeleteApp', + ], + }, + { + title: 'Secrets', + operationIds: [ + 'CreateAppKVSecret', + 'OpenAppSecrets', + 'OpenAppSecret', + 'ListAppSecrets', + 'GetAppSecret', + 'DeleteAppSecret', + ], + }, + { + title: 'Secret Versions', + operationIds: [ + 'ListAppSecretVersions', + 'ListOpenAppSecretVersions', + 'OpenAppSecretVersion', + 'GetAppSecretVersion', + 'DeleteAppSecretVersion', + ], + }, + { + title: 'Sync Integrations', + operationIds: [ + 'CreateAwsSmSyncIntegration', + 'CreateAzureKvSyncIntegration', + ], + }, + { + title: 'Sync Integrations', + operationIds: [ + 'CreateGcpSmSyncIntegration', + 'CreateGhOrgSyncIntegration', + 'CreateGhRepoSyncIntegration', + ], + }, + { + title: 'GitHub Installations', + operationIds: [ + 'ListGitHubInstallations', + 'ConnectGitHubInstallation', + 'GetGitHubInstallLinks', + ], + }, + // ForceSync + // SetTier + ], +} export async function getStaticProps({ basePath, @@ -47,8 +126,31 @@ export async function getStaticProps({ * complex version may end up meaning significant differences in the logic * to generate the version selector depending on landing vs operation view. */ - const navItems = getNavItems(basePath, operationSlug, schemaData) - const sharedProps: SharedProps = { basePath, navItems } + const operationObjects = getOperationObjects(schemaData) + const operationGroups = buildOperationGroups( + SHIM_CONTENT.operationGroupings, + operationObjects + ) + const navItemLanding: OpenApiNavItem = { + title: 'Landing', + fullPath: basePath, + isActive: !operationSlug, + } + const navItemGroups = operationGroups.map((group) => ({ + title: group.title, + items: group.operationObjects.map(({ type, operationId }) => { + return { + title: wordBreakCamelCase(operationId), + fullPath: `${basePath}/${operationId}`, + isActive: operationSlug === operationId, + } + }), + })) + const sharedProps: SharedProps = { + basePath, + navItemLanding, + navItemGroups, + } /** * If we have an operation slug, build and return operation view props. diff --git a/src/views/open-api-docs-view-v2/types.ts b/src/views/open-api-docs-view-v2/types.ts index 86e8c6b0be..99df658a53 100644 --- a/src/views/open-api-docs-view-v2/types.ts +++ b/src/views/open-api-docs-view-v2/types.ts @@ -24,7 +24,8 @@ export type OpenApiNavItem = { */ export interface SharedProps { basePath: string - navItems: OpenApiNavItem[] + navItemLanding: OpenApiNavItem + navItemGroups: { title: string; items: OpenApiNavItem[] }[] } /** diff --git a/src/views/open-api-docs-view-v2/utils/build-operation-groups.ts b/src/views/open-api-docs-view-v2/utils/build-operation-groups.ts new file mode 100644 index 0000000000..1ab257443f --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/build-operation-groups.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +interface OperationGroup { + title: string + operationObjects: $TSFixMe +} + +/** + * TODO: implement properly and add comments, + * messing around for now. + */ +export function buildOperationGroups( + operationGroupings: $TSFixMe, + operationObjects: $TSFixMe +): OperationGroup[] { + const operationGroups: OperationGroup[] = [] + // + for (const { title, operationIds } of operationGroupings) { + /** + * Match each operationId to an operation object + * If given operationId doesn't have a match... error? warn? + */ + const matchedOperationObjects = [] + for (const operationId of operationIds) { + const matchedOperationObject = operationObjects.find( + (operationObject) => operationObject.operationId === operationId + ) + console.log({ operationId, matchedOperationObject }) + if (matchedOperationObject) { + matchedOperationObjects.push(matchedOperationObject) + } else { + console.error( + `No operation object found for operationId: ${operationId}. Skipping.` + ) + } + } + // + if (matchedOperationObjects.length === 0) { + console.error( + `No operation objects found for group: ${title}. Skipping group.` + ) + } else { + operationGroups.push({ title, operationObjects: matchedOperationObjects }) + } + } + // Gather ids from all operation groups + const allUsedOperationIds = operationGroups.reduce( + (acc, group) => + acc.concat(group.operationObjects.map((obj) => obj.operationId)), + [] + ) + // Check for any operation objects that weren't used + const unusedOperationObjects = operationObjects.filter( + (obj) => !allUsedOperationIds.includes(obj.operationId) + ) + /** + * If we have operation objects that were not included in a group, + * create an "Other" group to hold any unused operation objects. + */ + if (unusedOperationObjects.length > 0) { + operationGroups.push({ + title: 'Other', // TODO: make this configurable? Arg with default value? + operationObjects: unusedOperationObjects, + }) + } + // + return operationGroups +} diff --git a/src/views/open-api-docs-view-v2/utils/get-operation-objects.ts b/src/views/open-api-docs-view-v2/utils/get-operation-objects.ts new file mode 100644 index 0000000000..d43e3d5f7d --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/get-operation-objects.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +/** + * TODO: implement properly and add comments, + * messing around for now. + */ +export function getOperationObjects(openApiDocument: $TSFixMe): $TSFixMe[] { + // + const operationObjects = [] + // + for (const [_path, pathItemObject] of Object.entries(openApiDocument.paths)) { + for (const [type, operation] of Object.entries(pathItemObject)) { + // String values are apparently possible, but not sure how to support them + if (typeof operation === 'string') { + continue + } + // We only want operation objects. + if (!('operationId' in operation)) { + continue + } + // + operationObjects.push({ type, ...operation }) + } + } + // + return operationObjects +}