Skip to content

Commit

Permalink
Schema Browser UI and memoize update (#1268)
Browse files Browse the repository at this point in the history
  • Loading branch information
dakota002 authored Aug 30, 2023
1 parent f8a298c commit 839b93c
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 172 deletions.
169 changes: 88 additions & 81 deletions app/lib/schema-data.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/
import { RequestError } from '@octokit/request-error'
import { Octokit } from '@octokit/rest'
import { extname, join } from 'path'
import memoizee from 'memoizee'
import { basename, dirname, extname, join } from 'path'
import { relative } from 'path/posix'

import { getEnvOrDieInProduction } from './env.server'
Expand All @@ -23,44 +24,44 @@ const repoData = {
owner: 'nasa-gcn',
}

function isErrnoException(e: unknown): e is NodeJS.ErrnoException {
return e instanceof Error && 'code' in e && 'errno' in e
}

export async function getVersionRefs() {
const releases = (await octokit.rest.repos.listReleases(repoData)).data.map(
(x) => ({ name: x.name, ref: x.tag_name })
)
return [...releases, { name: 'main', ref: 'main' }]
}

export async function loadJson(filePath: string, ref: string): Promise<Schema> {
if (!filePath) throw new Error('path must be defined')

if (extname(filePath) !== '.json')
throw new Response('not found', { status: 404 })

let body: Schema
try {
body = await loadContentFromGithub(filePath, ref)
if (body.allOf?.find((x) => x.$ref)) {
await loadSubSchema(body.allOf, body.$id)
}
if (body.anyOf?.find((x) => x.$ref)) {
await loadSubSchema(body.anyOf, body.$id)
}
if (body.oneOf?.find((x) => x.$ref, body.$id)) {
await loadSubSchema(body.oneOf, body.$id)
}
} catch (e) {
if (isErrnoException(e) && e.code === 'ENOENT') {
export const getVersionRefs = memoizee(
async function () {
const releases = (await octokit.rest.repos.listReleases(repoData)).data.map(
(x) => ({ name: x.name, ref: x.tag_name })
)
const defaultBranch = await getDefaultBranch()
return [...releases, { name: defaultBranch, ref: defaultBranch }]
},
{ promise: true }
)

export const loadJson = memoizee(
async function (filePath: string, ref: string): Promise<Schema> {
if (!filePath) throw new Error('path must be defined')

if (extname(filePath) !== '.json')
throw new Response('not found', { status: 404 })

let body: Schema
try {
body = await loadContentFromGithub(filePath, ref)
if (body.allOf?.find((x) => x.$ref)) {
await loadSubSchema(body.allOf, body.$id)
}
if (body.anyOf?.find((x) => x.$ref)) {
await loadSubSchema(body.anyOf, body.$id)
}
if (body.oneOf?.find((x) => x.$ref, body.$id)) {
await loadSubSchema(body.oneOf, body.$id)
}
} catch (e) {
throw new Response('Not found', { status: 404 })
}
throw e
}

return body
}
return body
},
{ promise: true }
)

async function loadContentFromGithub(path: string, ref?: string) {
const ghData = (
Expand Down Expand Up @@ -118,53 +119,59 @@ export type GitContentDataResponse = {
children?: GitContentDataResponse[]
}

export async function loadSchemaExamples(
path: string,
ref: string
): Promise<ExampleFiles[]> {
const dirPath = path.substring(0, path.lastIndexOf('/'))
const schemaName = path.substring(path.lastIndexOf('/') + 1)
const exampleFiles = (await getGithubDir(dirPath, ref)).filter(
(x) =>
x.name.startsWith(`${schemaName.split('.')[0]}.`) &&
x.name.endsWith('.example.json')
)

const result: ExampleFiles[] = []
for (const exampleFile of exampleFiles) {
const exPath = join(dirPath, '/', exampleFile.name)
const example = await loadContentFromGithub(exPath)
result.push({
name: exampleFile.name.replace('.example.json', ''),
content: example,
})
}
return result
}

export async function getGithubDir(
path?: string,
ref = 'main'
): Promise<GitContentDataResponse[]> {
return (
await octokit.repos.getContent({
...repoData,
path: path ?? 'gcn',
ref,
})
).data as GitContentDataResponse[]
}
export const loadSchemaExamples = memoizee(
async function (schemaPath: string, ref: string): Promise<ExampleFiles[]> {
const dirPath = dirname(schemaPath)
const schemaName = basename(schemaPath)
const exampleFiles = (await getGithubDir(dirPath, ref)).filter(
(x) =>
x.name.startsWith(`${schemaName.split('.')[0]}.`) &&
x.name.endsWith('.example.json')
)

const result: ExampleFiles[] = []
for (const exampleFile of exampleFiles) {
const exPath = join(dirPath, exampleFile.name)
const example = await loadContentFromGithub(exPath, ref)
result.push({
name: exampleFile.name.replace('.example.json', ''),
content: example,
})
}
return result
},
{ promise: true }
)

export const getGithubDir = memoizee(
async function (
path?: string,
ref = 'main'
): Promise<GitContentDataResponse[]> {
return (
await octokit.repos.getContent({
...repoData,
path: path ?? 'gcn',
ref,
})
).data as GitContentDataResponse[]
},
{ promise: true }
)

async function getDefaultBranch() {
return (await octokit.rest.repos.get(repoData)).data.default_branch
}

export async function getLatestRelease() {
try {
return (await octokit.rest.repos.getLatestRelease(repoData)).data.tag_name
} catch (error) {
if (error instanceof RequestError && error.status === 404)
return await getDefaultBranch()
throw error
}
}
export const getLatestRelease = memoizee(
async function () {
try {
return (await octokit.rest.repos.getLatestRelease(repoData)).data.tag_name
} catch (error) {
if (error instanceof RequestError && error.status === 404)
return await getDefaultBranch()
throw error
}
},
{ promise: true }
)
2 changes: 1 addition & 1 deletion app/routes/docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export default function () {
</>,
...(enableSchemaBrowser
? [
<NavLink key="schema" to="schema/latest/gcn">
<NavLink key="schema" to="schema/stable/gcn">
Schema Browser
</NavLink>,
]
Expand Down
48 changes: 48 additions & 0 deletions app/routes/docs_.schema.$version.$/BreadcrumbNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*!
* Copyright © 2023 United States Government as represented by the
* Administrator of the National Aeronautics and Space Administration.
* All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/
import {
Breadcrumb,
BreadcrumbBar,
BreadcrumbLink,
} from '@trussworks/react-uswds'

export default function BreadcrumbNav({
path,
pathPrepend,
className,
}: {
path: string
pathPrepend?: string
className?: string
}) {
const breadcrumbs = path.split('/')
const lastBreadcrumb = breadcrumbs.pop()

let cumulativePath = pathPrepend
return (
<BreadcrumbBar className={className}>
<>
{...breadcrumbs.map((breadcrumb) => {
cumulativePath = `${cumulativePath}/${breadcrumb}`
return (
<Breadcrumb key={cumulativePath}>
<BreadcrumbLink href={cumulativePath}>
{breadcrumb}
</BreadcrumbLink>
</Breadcrumb>
)
})}
</>
<Breadcrumb>
<Breadcrumb current>
<h2 className="margin-y-0 display-inline">{lastBreadcrumb}</h2>
</Breadcrumb>
</Breadcrumb>
</BreadcrumbBar>
)
}
15 changes: 9 additions & 6 deletions app/routes/docs_.schema.$version.$/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
import { Link } from '@remix-run/react'
import { Link, useResolvedPath } from '@remix-run/react'
import { Icon, Table } from '@trussworks/react-uswds'
import { useState } from 'react'

Expand Down Expand Up @@ -36,9 +36,16 @@ export type Schema = {
required?: string[]
}

function useLinkString(path: string) {
return useResolvedPath(`../${path}`, {
relative: 'path',
})
}

function ReferencedElementRow({ item }: { item: ReferencedSchema }) {
const [showHiddenRow, toggleHiddenRow] = useState(false)
const locallyDefined = item.$ref?.startsWith('#')
const linkString = useLinkString(item.$ref ?? '')
return (
<>
<tr onClick={() => toggleHiddenRow(!showHiddenRow)}>
Expand All @@ -57,7 +64,7 @@ function ReferencedElementRow({ item }: { item: ReferencedSchema }) {
) : (
<Icon.ExpandMore aria-label="Expand" />
)}
<Link to={formatLinkString(item.$ref ?? '')}>
<Link to={linkString}>
{item.$ref && item.$ref.split('/').slice(-1)[0]}
</Link>
</td>
Expand Down Expand Up @@ -131,10 +138,6 @@ export function SchemaPropertiesTableBody({
)
}

function formatLinkString(schemaLinkString: string) {
return schemaLinkString.replace('schema', 'docs/schema')
}

export function formatFieldName(name: string, requiredProps?: string[]) {
let formattedName = name
if (requiredProps && requiredProps.includes(name)) formattedName += '*'
Expand Down
Loading

0 comments on commit 839b93c

Please sign in to comment.