Skip to content

Commit

Permalink
feat(openapi): implement tag-based grouping as default (#2172)
Browse files Browse the repository at this point in the history
* feat(openapi): implement tag-based grouping

* fix: clean up service in operationId

* fix: handle non-service operation ids

* chore: clean up stray placeholder data

* feat: implement groupOperationsByPath config
  • Loading branch information
zchsh authored Sep 25, 2023
1 parent 5dd2f74 commit 4497649
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 44 deletions.
8 changes: 5 additions & 3 deletions src/pages/api/get-open-api-docs-view-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const GENERIC_PAGE_CONFIG = {
type ExpectedBody = {
openApiJsonString: string
openApiDescription: string
groupOperationsByPath: boolean
}

export default async function handler(
Expand All @@ -69,9 +70,8 @@ export default async function handler(
* 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
const { openApiDescription, openApiJsonString, groupOperationsByPath } =
JSON.parse(req.body) as ExpectedBody

/**
* Construct some preview data just to match the expected `getStaticProps`
Expand All @@ -94,6 +94,8 @@ export default async function handler(
...GENERIC_PAGE_CONFIG,
// Pass the constructed version data
versionData,
// Pass options
groupOperationsByPath,
/**
* Massage the schema data a little bit, replacing
* "HashiCorp Cloud Platform" in the title with "HCP".
Expand Down
2 changes: 2 additions & 0 deletions src/pages/hcp/api-docs/vault-secrets/[[...page]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const PAGE_CONFIG: OpenApiDocsPageConfig = {
path: 'specs/cloud-vault-secrets',
ref: 'main',
},
groupOperationsByPath: true,
statusIndicatorConfig: {
pageUrl: 'https://status.hashicorp.com',
endpointUrl:
Expand Down Expand Up @@ -102,6 +103,7 @@ export const getStaticProps: GetStaticProps<
serviceProductSlug: PAGE_CONFIG.serviceProductSlug,
statusIndicatorConfig: PAGE_CONFIG.statusIndicatorConfig,
navResourceItems: PAGE_CONFIG.navResourceItems,
groupOperationsByPath: PAGE_CONFIG.groupOperationsByPath,
massageSchemaForClient: PAGE_CONFIG.massageSchemaForClient,
// Pass params to getStaticProps, this is used for versioning
context: { params },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import classNames from 'classnames'
// Components
import Button from 'components/button'
import InlineAlert from 'components/inline-alert'
import { CheckboxField } from 'components/form/field-controls'
// Inputs
import { FileStringInput } from '../file-string-input'
import { TextareaInput } from '../textarea-input'
Expand All @@ -22,6 +23,7 @@ import s from './open-api-preview-inputs.module.css'
interface InputValues {
openApiJsonString: string
openApiDescription: string
groupOperationsByPath: boolean
}

/**
Expand All @@ -43,12 +45,13 @@ export function OpenApiPreviewInputs({
const [inputValues, setInputValues] = useState<InputValues>({
openApiJsonString: '',
openApiDescription: '',
groupOperationsByPath: false,
})

/**
* Helper to set a specific input data value.
*/
function setInputValue(key: keyof InputValues, value: string) {
function setInputValue(key: keyof InputValues, value: unknown) {
setInputValues((p: InputValues) => ({ ...p, [key]: value }))
}

Expand Down Expand Up @@ -127,6 +130,17 @@ export function OpenApiPreviewInputs({
}
/>
) : null}
<CheckboxField
label="Group operations by path"
helperText="By default, operations are organized by their first tag, which is expected to correspond to a service name. In some cases, spec files may have only a single tag, in which case this option can be used to group operations by their paths instead."
checked={inputValues.groupOperationsByPath}
onChange={() =>
setInputValue(
'groupOperationsByPath',
!inputValues.groupOperationsByPath
)
}
/>
<TextareaInput
label="Schema source"
helperText="Test out edits to the uploaded OpenAPI specification file."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Error = {
export async function fetchOpenApiStaticProps(inputValues: {
openApiJsonString: string
openApiDescription?: string
groupOperationsByPath?: boolean
}): Promise<[Error | null, OpenApiDocsViewProps | null]> {
try {
const result = await fetch(API_ROUTE, {
Expand Down
20 changes: 4 additions & 16 deletions src/views/open-api-docs-view/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
OpenApiDocsVersionData,
StatusIndicatorConfig,
OpenApiNavItem,
OpenApiDocsPageConfig,
} from './types'

/**
Expand Down Expand Up @@ -72,20 +73,12 @@ export async function getStaticProps({
basePath,
statusIndicatorConfig,
topOfPageId = 'overview',
groupOperationsByPath = false,
massageSchemaForClient = (s: OpenAPIV3.Document) => s,
navResourceItems = [],
}: {
}: Omit<OpenApiDocsPageConfig, 'githubSourceDirectory'> & {
context: GetStaticPropsContext<OpenApiDocsParams>
productSlug: ProductSlug
serviceProductSlug?: ProductSlug
versionData: OpenApiDocsVersionData[]
basePath: string
statusIndicatorConfig?: StatusIndicatorConfig
topOfPageId?: string
massageSchemaForClient?: (
schemaData: OpenAPIV3.Document
) => OpenAPIV3.Document
navResourceItems: OpenApiNavItem[]
}): Promise<GetStaticPropsResult<OpenApiDocsViewProps>> {
// Get the product data
const productData = cachedGetProductData(productSlug)
Expand Down Expand Up @@ -120,7 +113,7 @@ export async function getStaticProps({
const rawSchemaData = await parseAndValidateOpenApiSchema(schemaFileString)
const schemaData = massageSchemaForClient(rawSchemaData)
const operationProps = await getOperationProps(schemaData)
const operationGroups = groupOperations(operationProps)
const operationGroups = groupOperations(operationProps, groupOperationsByPath)
const navItems = getNavItems({
operationGroups,
topOfPageId,
Expand Down Expand Up @@ -158,11 +151,6 @@ export async function getStaticProps({
},
releaseStage: targetVersion.releaseStage,
descriptionMdx,
_placeholder: {
productSlug,
targetVersion,
schemaData,
},
operationGroups: stripUndefinedProperties(operationGroups),
navItems,
navResourceItems,
Expand Down
23 changes: 13 additions & 10 deletions src/views/open-api-docs-view/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { PropertyDetailsSectionProps } from './components/operation-details
*/
export interface OperationProps {
operationId: string
tags: string[]
slug: string
type: string
path: {
Expand All @@ -35,11 +36,6 @@ export interface OperationProps {
* word breaks to allow long URLs to wrap to multiple lines.
*/
urlPathForCodeBlock: string
/**
* Some temporary data to mess around with during prototyping.
* TODO: remove this for the production implementation.
*/
_placeholder: $TSFixMe
}

/**
Expand Down Expand Up @@ -167,11 +163,6 @@ export interface OpenApiDocsViewProps {
*/
statusIndicatorConfig: StatusIndicatorConfig

/**
* Some temporary data we'll remove for the production implementation.
*/
_placeholder: $TSFixMe

/**
* Product slug to use for the theming of the service itself.
* For example, many product-themed services exist within the broader
Expand Down Expand Up @@ -224,4 +215,16 @@ export interface OpenApiDocsPageConfig {
* but before we translate the schema into page props.
*/
massageSchemaForClient?: (schema: OpenAPIV3.Document) => OpenAPIV3.Document
/**
* The top-of-page heading optionally have an id other than "overview".
* This heading ID is used to jump to the top of the page
*/
topOfPageId?: string
/**
* Optionally group operations by their URL path. By default, operations are
* grouped by their first `tag`, which is expected to correspond to a service.
* In some cases, a spec may only have a single service, rendering this
* tag-based grouping less useful.
*/
groupOperationsByPath?: boolean
}
17 changes: 9 additions & 8 deletions src/views/open-api-docs-view/utils/get-operation-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,20 @@ export async function getOperationProps(
/**
* Build a fallback summary for the operation, which is just
* the operationId with some formatting for better line-breaks.
* We also apply logic to remove the first part of the operationId,
* which by convention will be formatted `ServiceName_OperationName`.
*
* TODO: update to actually use `summary`, for now we only use
* `operationId` as `summary` values are not yet reliably present
* & accurate. Task:
* https://app.asana.com/0/1204678746647847/1205338583217309/f
*/
const summary = addWordBreaks(
splitOnCapitalLetters(operation.operationId)
)
const operationIdParts = operation.operationId.split('_')
const hasServicePart = operationIdParts.length > 1
const idForSummary = hasServicePart
? operationIdParts.slice(1).join('_')
: operation.operationId
const summary = addWordBreaks(splitOnCapitalLetters(idForSummary))

/**
* Format and push the operation props
Expand All @@ -95,6 +100,7 @@ export async function getOperationProps(
operationId: operation.operationId,
slug: operationSlug,
type,
tags: operation.tags ?? [],
path: {
full: path,
truncated: addWordBreaksToUrl(truncateHcpOperationPath(path)),
Expand All @@ -103,11 +109,6 @@ export async function getOperationProps(
requestData,
responseData,
urlPathForCodeBlock: getUrlPathCodeHtml(serverUrl + path),
_placeholder: {
__type: type,
__path: path,
...operation,
},
})
}
}
Expand Down
25 changes: 19 additions & 6 deletions src/views/open-api-docs-view/utils/group-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import { OperationProps, OperationGroup } from '../types'
import { truncateHcpOperationPath } from '../utils'
import { addWordBreaksToUrl } from './add-word-breaks-to-url'

/**
Expand Down Expand Up @@ -51,17 +50,31 @@ import { addWordBreaksToUrl } from './add-word-breaks-to-url'
* }
*/
export function groupOperations(
operationObjects: OperationProps[]
operationObjects: OperationProps[],
groupOperationsByPath: boolean
): OperationGroup[] {
// Group operations, either by tags where specified, or automatically by paths
// or by their paths otherwise.
const operationGroupsMap = operationObjects.reduce(
(
acc: Record<string, { heading: string; items: OperationProps[] }>,
o: OperationProps
) => {
// Truncate the common HCP-related prefix from the path, if applicable
const truncatedPath = truncateHcpOperationPath(o._placeholder.__path)
// Grab the first two path segments, to use as a group slug
const groupSlug = truncatedPath.split('/').slice(0, 3).join('/')
/**
* Determine the grouping slug for this operation.
*
* If path-based grouping has been specified, we ignore tags and group
* based on the operation URL paths (truncated to remove common parts).
*
* If tag-based grouping is used, note that we may need to fall back
* to an "Other" tag for potentially untagged operations.
*/
let groupSlug: string
if (groupOperationsByPath) {
groupSlug = o.path.truncated.split('/').slice(0, 3).join('/')
} else {
groupSlug = (o.tags.length && o.tags[0]) ?? 'Other'
}
if (!acc[groupSlug]) {
acc[groupSlug] = {
heading: addWordBreaksToUrl(groupSlug),
Expand Down

1 comment on commit 4497649

@vercel
Copy link

@vercel vercel bot commented on 4497649 Sep 25, 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.