Files
supabase/apps/docs/features/docs/Troubleshooting.utils.ts
T
Charis a29dc5de4b feat(docs): main troubleshooting page (#29120)
* feat(docs): main troubleshooting page

* feat(docs): global troubleshooting page layout

* feat(docs): global troubleshooting page filter logic

* enhance(docs): troubleshooting page ui & filters

Improve UX and accessibility of global troubleshooting search page.

- Implement TroubleshootingFilterStateProvider for managing filter state
- Add TroubleshootingFilterEmptyState component
- Improve TroubleshootingFilter UI with applied filters display
- Update TroubleshootingPreview component with better layout and accessibility
- Add TroubleshootingEntryAssociatedErrors component for displaying related errors
- Refactor Troubleshooting utils and schemas
- Update troubleshooting content template

* feat(docs): individual troubleshooting pages

Add individual pages for detailed information on troubleshooting
entries. Small fixes to UI for troubleshooting displays.

- Wrap GlobalTroubleshootingPage with SidebarSkeleton
- Add TroubleshootingErrorListDetailed component for detailed error display
- Implement TroubleshootingBackLink component for navigation
- Update TroubleshootingPreview to use Next.js Link and include parent page
- Refactor Footer component to accept className prop
- Add hideFooter option to SidebarSkeleton
- Minor updates to TopNavBar and other utility functions

* fix: minor styling issues

* tweak(troubleshooting docs): use breadcrumbs

* tweak(troubleshooting docs): use sidebar for filter

* enhance(troubleshooting docs): navigation, keywords

Better navigation for individual troubleshooting entries, using a
version of the sidebar + keywords handling via search params for good
linking behavior

Details:

- Add TroubleshootingSidebar to TroubleshootingPage
- Implement query state management for keywords using nuqs
- Improve accessibility and styling of troubleshooting components
- Update empty state handling and related keywords display
- Upgrade nuqs package to version 1.19.1

* fix(docs): wrap useSearchParams in Suspense

* seo(docs): add canonical link to troubleshooting

* enhance(docs,troubleshooting): auto-reveal errors

On troubleshooting search pages, if the searched term matches a hidden
error, auto-reveal it when searching. When the search term is cleared,
hide programmatically-opened errors while preserving user-opened ones.

* enhance(docs,troubleshooting): search bar

Add search bar to individual troubleshooting pages

* fix(docs): breadcrumb links

Fix breadcrumb linking so it uses next/link instead of reloading entire
app on navigation.

* fix(docs): typo

* enh(docs): new troubleshooting design

* enh(docs): use new multi-select

* enh(docs): troubleshooting responsive styles

* Update apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx
2024-10-02 17:10:12 +00:00

193 lines
5.7 KiB
TypeScript

import matter from 'gray-matter'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { mdxFromMarkdown } from 'mdast-util-mdx'
import { toMarkdown } from 'mdast-util-to-markdown'
import { mdxjs } from 'micromark-extension-mdxjs'
import { readdir, readFile, stat } from 'node:fs/promises'
import { join } from 'node:path'
import toml from 'toml'
import { visit } from 'unist-util-visit'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { DOCS_DIRECTORY } from 'lib/docs'
import { cache_fullProcess_withDevCacheBust } from '~/features/helpers.fs'
import { formatError } from './Troubleshooting.utils.shared'
const TROUBLESHOOTING_DIRECTORY = join(DOCS_DIRECTORY, 'content/troubleshooting')
const TroubleshootingSchema = z
.object({
title: z.string(),
topics: z.array(
z.enum([
'ai',
'auth',
'branching',
'cli',
'database',
'functions',
'platform',
'realtime',
'self-hosting',
'storage',
'studio',
'supavisor',
'terraform',
])
),
keywords: z.array(z.string()).optional(),
api: z
.object({
sdk: z.array(z.string()).optional(),
management_api: z.array(z.string()).optional(),
cli: z.array(z.string()).optional(),
})
.strict()
.optional(),
errors: z
.array(
z
.object({
http_status_code: z.number().optional(),
code: z.string().optional(),
message: z.string().optional(),
})
.strict()
)
.optional(),
database_id: z.string().uuid().default(uuidv4()),
created_at: z.date({ coerce: true }).optional(),
updated_at: z.date({ coerce: true }).optional(),
})
.strict()
export type ITroubleshootingMetadata = z.infer<typeof TroubleshootingSchema>
const validateTroubleshootingMetadata = (troubleshootingMetadata: unknown) => {
return TroubleshootingSchema.safeParse(troubleshootingMetadata)
}
async function getAllTroubleshootingEntriesInternal() {
const troubleshootingDirectoryContents = await readdir(TROUBLESHOOTING_DIRECTORY, {
recursive: true,
})
const troubleshootingFiles = troubleshootingDirectoryContents.map(async (entry) => {
const isHidden = entry.startsWith('_')
if (isHidden) return null
const isFile = (await stat(join(TROUBLESHOOTING_DIRECTORY, entry))).isFile()
if (!isFile) return null
const fileContents = await readFile(join(TROUBLESHOOTING_DIRECTORY, entry), 'utf-8')
const { content, data: frontmatter } = matter(fileContents, {
language: 'toml',
engines: { toml: toml.parse.bind(toml) },
})
const parseResult = validateTroubleshootingMetadata(frontmatter)
if ('error' in parseResult) {
console.error(`Error validating troubleshooting metadata for ${entry}:`, parseResult.error)
return null
}
const mdxTree = fromMarkdown(content, {
extensions: [mdxjs()],
mdastExtensions: [mdxFromMarkdown()],
})
visit(mdxTree, (node) => {
if ('children' in node) {
node.children = node.children.filter(
(child) =>
![
'mdxJsxFlowExpression',
'mdxJsxTextExpression',
'mdxFlowExpression',
'mdxTextExpression',
'mdxJsxFlowElement',
'mdxJsxTextElement',
'mdxJsxExpressionAttribute',
'mdxJsxAttribute',
'mdxJsxAttributeValueExpression',
'mdxjsEsm',
].includes(child.type)
)
}
})
const contentWithoutJsx = toMarkdown(mdxTree)
return {
content,
contentWithoutJsx,
data: parseResult.data,
}
})
return (await Promise.all(troubleshootingFiles)).filter(Boolean)
}
export const getAllTroubleshootingEntries = cache_fullProcess_withDevCacheBust(
getAllTroubleshootingEntriesInternal,
TROUBLESHOOTING_DIRECTORY,
() => JSON.stringify([])
)
export type ITroubleshootingEntry = Awaited<ReturnType<typeof getAllTroubleshootingEntries>>[number]
export async function getAllTroubleshootingKeywords() {
const entries = await getAllTroubleshootingEntries()
const keywords = new Set<string>()
for (const entry of entries) {
for (const topic of entry.data.topics) {
keywords.add(topic)
}
for (const keyword of entry.data.keywords ?? []) {
keywords.add(keyword)
}
}
return Array.from(keywords).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
}
export async function getAllTroubleshootingProducts() {
const entries = await getAllTroubleshootingEntries()
const products = new Set<string>()
for (const entry of entries) {
for (const topic of entry.data.topics) {
products.add(topic)
}
}
return Array.from(products).sort((a, b) => a.localeCompare(b))
}
export async function getAllTroubleshootingErrors() {
const entries = await getAllTroubleshootingEntries()
const allErrors = new Set(
entries
.flatMap((entry) => entry.data.errors)
.filter((error) => error?.http_status_code || error?.code)
)
const seen = new Set<string>()
for (const error of allErrors) {
const key = formatError(error)
if (seen.has(key)) {
allErrors.delete(error)
}
seen.add(key)
}
function sortErrors(
a: ITroubleshootingMetadata['errors'][number],
b: ITroubleshootingMetadata['errors'][number]
) {
return formatError(a).localeCompare(formatError(b))
}
return Array.from(allErrors).sort(sortErrors)
}
export function getArticleSlug(entry: ITroubleshootingMetadata) {
const slugifiedTitle = entry.title.toLowerCase().replace(/\s+/g, '-')
const escapedTitle = encodeURIComponent(slugifiedTitle)
return escapedTitle
}