diff --git a/packages/commons/docs-loader/src/editable-docs-loader.ts b/packages/commons/docs-loader/src/editable-docs-loader.ts index 866d0ba2cd..b4bb136c1a 100644 --- a/packages/commons/docs-loader/src/editable-docs-loader.ts +++ b/packages/commons/docs-loader/src/editable-docs-loader.ts @@ -195,8 +195,6 @@ export const createEditableDocsLoader = async ({ const docsLoader = await createCachedDocsLoader(host, encodeDocsLoaderDomain(domain, branchName), fernToken, { returnRawMarkdown: true, cacheConfig: { - // For editable docs, we want shorter TTL so that cache stays fresh - kvTtl: 5 * 60, // 5 minutes cacheKeySuffix: "editable", forceRevalidate }, diff --git a/packages/fern-dashboard/package.json b/packages/fern-dashboard/package.json index 84970e3ac0..6caeef0503 100644 --- a/packages/fern-dashboard/package.json +++ b/packages/fern-dashboard/package.json @@ -49,7 +49,6 @@ "@fern-api/fdr-sdk": "workspace:*", "@fern-api/ui-core-utils": "workspace:*", "@fern-api/venus-api-sdk": "0.20.0", - "@fern-api/visual-editor-server": "workspace:*", "@fern-docs/components": "workspace:*", "@fern-docs/edge-config": "workspace:*", "@fern-docs/mdx": "workspace:*", diff --git a/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/loading.tsx b/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/loading.tsx deleted file mode 100644 index 35e1d2a3f5..0000000000 --- a/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/loading.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton"; - -export default function Loading() { - return ( -
-
- -
-
- -
-
- ); -} diff --git a/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/page.tsx b/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/page.tsx index 3e1624d60a..db5da6485c 100644 --- a/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/page.tsx +++ b/packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/page.tsx @@ -1,34 +1,25 @@ import "server-only"; import { notFound } from "next/navigation"; - -import getGithubSourceMetadataHandler from "@/app/api/get-github-source-metadata/handler"; +import { Suspense } from "react"; +import { getGitHubAuthState } from "@/app/actions/getGithubMetadata"; import type { Auth0OrgName } from "@/app/services/auth0/types"; import getDocsSitesForOrg from "@/app/services/dal/fdr/getDocsSitesForOrg"; -import getDocsGithubUrl from "@/app/services/dal/github/getDocsGithubUrl"; -import { validateGithubRepoAccess } from "@/app/services/dal/github/validators"; +import { getDocsGithubUrl } from "@/app/services/dal/github/getDocsGithubUrl"; import { getAuthenticatedSessionOrRedirect } from "@/app/services/dal/organization"; import { DocsSiteOverviewCard } from "@/components/docs-page/DocsSiteOverviewCard"; -import { FernCliVersionDisplay } from "@/components/docs-page/FernCliVersionDisplay"; -import { type GithubAuthState, GithubSource } from "@/components/docs-page/GithubSource"; +import { VisualEditorLoadingCard } from "@/components/docs-page/visual-editor-section/VisualEditorLoadingCard"; import { VisualEditorSection } from "@/components/docs-page/visual-editor-section/VisualEditorSection"; import { getDocsSiteUrl } from "@/utils/getDocsSiteUrl"; import { parseDocsUrlParam } from "@/utils/parseDocsUrlParam"; import type { EncodedDocsUrl } from "@/utils/types"; -export default async function Page(props: { - params: Promise<{ orgName: Auth0OrgName; docsUrl: EncodedDocsUrl }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { +export default async function Page(props: { params: Promise<{ orgName: Auth0OrgName; docsUrl: EncodedDocsUrl }> }) { const { orgName, docsUrl: encodedDocsUrl } = await props.params; - const searchParams = await props.searchParams; const session = await getAuthenticatedSessionOrRedirect(orgName); const docsUrl = parseDocsUrlParam({ docsUrl: encodedDocsUrl }); - // Only skip cache if explicitly requested via query param (e.g., ?refresh=true) - const skipCache = searchParams.refresh === "true"; - // Validate that the docsUrl belongs to this organization so that we avoid errors in the page const response = await getDocsSitesForOrg({ orgName, @@ -43,106 +34,18 @@ export default async function Page(props: { notFound(); } - let githubUrl = undefined; - let githubAuthState: GithubAuthState = { - validationResult: { - ok: false, - error: { - type: "UNEXPECTED_ERROR", - message: "" - } - }, - sourceRepo: undefined, - isLoading: false - }; - - try { - const urlResult = await getDocsGithubUrl({ - url: encodedDocsUrl, - token: session.accessToken - }); - - if (!urlResult.success) { - if (urlResult.error.type === "DOMAIN_NOT_REGISTERED") { - githubAuthState.validationResult = { - ok: false, - error: { - type: "UNEXPECTED_ERROR", - message: "Domain not registered." - } - }; - } else { - githubAuthState.validationResult = { - ok: false, - error: urlResult.error - }; - } - } else { - githubUrl = urlResult.githubUrl; - - try { - // Parallelize validation and metadata fetching for better performance - const [validation, sourceRepo] = await Promise.all([ - validateGithubRepoAccess( - orgName, - docsUrl, - { - type: "url", - githubUrl - }, - true // Skip cache for now, since this cache was causing issues with validating repos - ), - // Optimistically fetch metadata in parallel (will be used if validation succeeds) - getGithubSourceMetadataHandler({ - githubUrl, - userId: session.user.sub - }).catch((error) => { - console.error("Failed to fetch source repo metadata:", error); - return undefined; - }) - ]); - - githubAuthState = { - validationResult: validation, - // Only include sourceRepo if validation succeeded - sourceRepo: validation.ok ? sourceRepo : undefined, - isLoading: false - }; - } catch (error) { - console.error("Failed to validate GitHub access:", error); - // Keep default false state - } - } - } catch (error) { - console.error(error); - } + // Start expensive operations in parallel without awaiting + Promise.all([ + getDocsGithubUrl(docsUrl, session.accessToken), + getGitHubAuthState(docsUrl, session.accessToken, orgName, session) + ]); return (
- -
-

Source

- -
- -
- } - /> - + + }> + + ); } diff --git a/packages/fern-dashboard/src/app/[orgName]/(visual-editor)/editor/[docsUrl]/[branch]/layout.tsx b/packages/fern-dashboard/src/app/[orgName]/(visual-editor)/editor/[docsUrl]/[branch]/layout.tsx index 903205fe12..a2627fc972 100644 --- a/packages/fern-dashboard/src/app/[orgName]/(visual-editor)/editor/[docsUrl]/[branch]/layout.tsx +++ b/packages/fern-dashboard/src/app/[orgName]/(visual-editor)/editor/[docsUrl]/[branch]/layout.tsx @@ -5,7 +5,7 @@ import type React from "react"; import { ClientMDXProvider } from "@/app/[orgName]/context/ClientMDXProvider"; import { OrgNameProvider } from "@/app/[orgName]/context/OrgNameContext"; -import getGithubSourceMetadata from "@/app/api/get-github-source-metadata/handler"; +import { getGithubSourceMetadata } from "@/app/actions/getGithubSourceMetadata"; import type { Auth0OrgName } from "@/app/services/auth0/types"; import { assertAuthAndFetchGithubUrl } from "@/app/services/dal/github/assertAuthAndFetchGithubUrl"; import { getAuthenticatedSessionOrRedirect } from "@/app/services/dal/organization"; diff --git a/packages/fern-dashboard/src/app/actions/getDocsGithubMetadata.ts b/packages/fern-dashboard/src/app/actions/getDocsGithubMetadata.ts index 2e93fe2e39..24f3113bf9 100644 --- a/packages/fern-dashboard/src/app/actions/getDocsGithubMetadata.ts +++ b/packages/fern-dashboard/src/app/actions/getDocsGithubMetadata.ts @@ -2,16 +2,16 @@ import { fernToken_admin } from "@fern-api/docs-server"; -import type { GithubAuthState } from "@/components/docs-page/GithubSource"; - -import getGithubSourceMetadataHandler from "../api/get-github-source-metadata/handler"; +import type { GithubAuthState } from "@/components/docs-page/GithubSourceClient"; +import type { DocsUrl } from "@/utils/types"; import { getDocsUrlMetadata } from "../api/utils/getDocsUrlMetadata"; import { type Auth0SessionData, getCurrentSessionOrThrow } from "../services/auth0/getCurrentSession"; import type { Auth0OrgName } from "../services/auth0/types"; -import getDocsGithubUrl from "../services/dal/github/getDocsGithubUrl"; +import { getDocsGithubUrl } from "../services/dal/github/getDocsGithubUrl"; import { validateGithubRepoAccess } from "../services/dal/github/validators"; +import { getGithubSourceMetadata } from "./getGithubSourceMetadata"; -async function getMetadata(encodedDocsUrl: string, session: Auth0SessionData, orgName: Auth0OrgName, docsUrl: string) { +async function getMetadata(encodedDocsUrl: DocsUrl, session: Auth0SessionData, orgName: Auth0OrgName, docsUrl: string) { let githubAuthState: GithubAuthState = { validationResult: { ok: false, @@ -25,10 +25,7 @@ async function getMetadata(encodedDocsUrl: string, session: Auth0SessionData, or }; let githubUrl: string | undefined; try { - const urlResult = await getDocsGithubUrl({ - url: encodedDocsUrl, - token: session.accessToken - }); + const urlResult = await getDocsGithubUrl(encodedDocsUrl, session.accessToken); if (!urlResult.success) { if (urlResult.error.type === "DOMAIN_NOT_REGISTERED") { @@ -63,7 +60,7 @@ async function getMetadata(encodedDocsUrl: string, session: Auth0SessionData, or true // Skip cache for now, since this cache was causing issues with validating repos ), // Optimistically fetch metadata in parallel (will be used if validation succeeds) - getGithubSourceMetadataHandler({ + getGithubSourceMetadata({ githubUrl, userId: session.user.sub }).catch((error: unknown) => { @@ -88,7 +85,7 @@ async function getMetadata(encodedDocsUrl: string, session: Auth0SessionData, or } } -export async function getDocsGithubMetadata(docsUrl: string): Promise<{ +export async function getDocsGithubMetadata(docsUrl: DocsUrl): Promise<{ success: boolean; orgName?: Auth0OrgName; githubUrl?: string; @@ -102,10 +99,7 @@ export async function getDocsGithubMetadata(docsUrl: string): Promise<{ url: decodedUrl, token: fernToken_admin() ?? session.accessToken }); - const githubMetadata = await getDocsGithubUrl({ - url: decodedUrl, - token: fernToken_admin() ?? session.accessToken - }); + const githubMetadata = await getDocsGithubUrl(docsUrl, fernToken_admin() ?? session.accessToken); if (!githubMetadata.success) { return { success: false, error: "Failed to fetch github metadata" }; } @@ -116,7 +110,7 @@ export async function getDocsGithubMetadata(docsUrl: string): Promise<{ const orgName = docsMetadata.body.org as unknown as Auth0OrgName; - const metadata = await getMetadata(decodedUrl, session, orgName, decodedUrl); + const metadata = await getMetadata(docsUrl, session, orgName, decodedUrl); if (!metadata?.success) { return { success: false, error: "Failed to fetch metadata" }; } diff --git a/packages/fern-dashboard/src/app/actions/getGithubMetadata.ts b/packages/fern-dashboard/src/app/actions/getGithubMetadata.ts new file mode 100644 index 0000000000..d7d392c6f1 --- /dev/null +++ b/packages/fern-dashboard/src/app/actions/getGithubMetadata.ts @@ -0,0 +1,75 @@ +import "server-only"; +import { cache } from "react"; +import type { GithubAuthState } from "@/components/docs-page/GithubSourceClient"; +import type { DocsUrl } from "@/utils/types"; +import type { Auth0SessionData } from "../services/auth0/getCurrentSession"; +import type { Auth0OrgName } from "../services/auth0/types"; +import { type GetDocsGithubUrlResult, getDocsGithubUrl } from "../services/dal/github/getDocsGithubUrl"; +import { validateGithubRepoAccess } from "../services/dal/github/validators"; +import { getGithubSourceMetadata } from "./getGithubSourceMetadata"; + +type GetDocsGithubUrlError = Extract["error"]; + +export type GetGitHubAuthStateResult = GithubAuthState | { success: false; error: GetDocsGithubUrlError }; + +export const getGitHubAuthState = cache( + async ( + docsUrl: DocsUrl, + token: string, + orgName: Auth0OrgName, + session: Auth0SessionData + ): Promise => { + const urlResult = await getDocsGithubUrl(docsUrl, token); + if (!urlResult.success) { + return { success: false, error: urlResult.error }; + } + + const githubUrl = urlResult.githubUrl; + let githubAuthState: GithubAuthState = { + validationResult: { + ok: false, + error: { + type: "UNEXPECTED_ERROR", + message: "" + } + }, + sourceRepo: undefined, + isLoading: false + }; + + try { + // Parallelize validation and metadata fetching for better performance + const [validation, sourceRepo] = await Promise.all([ + validateGithubRepoAccess( + orgName, + docsUrl, + { + type: "url", + githubUrl + }, + true // Skip cache for now, since this cache was causing issues with validating repos + ), + // Optimistically fetch metadata in parallel (will be used if validation succeeds) + getGithubSourceMetadata({ + githubUrl, + userId: session.user.sub + }).catch((error) => { + console.error("Failed to fetch source repo metadata:", error); + return undefined; + }) + ]); + + githubAuthState = { + validationResult: validation, + // Only include sourceRepo if validation succeeded + sourceRepo: validation.ok ? sourceRepo : undefined, + isLoading: false + }; + } catch (error) { + console.error("Failed to validate GitHub access:", error); + // Keep default false state + } + + return githubAuthState; + } +); diff --git a/packages/fern-dashboard/src/app/api/get-github-source-metadata/handler.ts b/packages/fern-dashboard/src/app/actions/getGithubSourceMetadata.ts similarity index 89% rename from packages/fern-dashboard/src/app/api/get-github-source-metadata/handler.ts rename to packages/fern-dashboard/src/app/actions/getGithubSourceMetadata.ts index b7ba5ce2dd..22ff57ef89 100644 --- a/packages/fern-dashboard/src/app/api/get-github-source-metadata/handler.ts +++ b/packages/fern-dashboard/src/app/actions/getGithubSourceMetadata.ts @@ -1,3 +1,5 @@ +"use server"; + import { unstable_cache } from "next/cache"; import { getFernBotInstallationId, getFernBotOctokitForRepo } from "@/app/services/auth0/fernBotOctokit"; @@ -13,7 +15,7 @@ const EMPTY_RESPONSE: GithubSourceRepo = { fernBotHasInstallationId: undefined }; -export default async function getGithubSourceMetadataHandler({ +export async function getGithubSourceMetadata({ githubUrl, userId, skipCache = false @@ -22,7 +24,7 @@ export default async function getGithubSourceMetadataHandler({ userId: string; skipCache?: boolean; }): Promise { - async function getGithubSourceMetadata() { + async function fetchGithubSourceMetadata() { if (githubUrl == null) { throw new Error("NoGithubUrl"); } @@ -67,14 +69,14 @@ export default async function getGithubSourceMetadataHandler({ try { // Only cache successful responses; do not cache failures const result = skipCache - ? getGithubSourceMetadata() - : unstable_cache(getGithubSourceMetadata, [`github-source-${githubUrl}-${userId}`], { + ? fetchGithubSourceMetadata() + : unstable_cache(fetchGithubSourceMetadata, [`github-source-${githubUrl}-${userId}`], { revalidate: 300, // 5 minutes tags: [`github-source-${githubUrl}`] })(); return await result; } catch (error) { - console.error("[getDocsGithubSourceHandler]", error); + console.error("[getGithubSourceMetadata]", error); // On any error, return EMPTY_RESPONSE (but don't cache the error) return EMPTY_RESPONSE; } diff --git a/packages/fern-dashboard/src/app/api/get-github-source-metadata/route.ts b/packages/fern-dashboard/src/app/api/get-github-source-metadata/route.ts deleted file mode 100644 index c3ce9c5831..0000000000 --- a/packages/fern-dashboard/src/app/api/get-github-source-metadata/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; - -import { z } from "zod"; - -import { orgNameValidator } from "@/app/api/utils/validators"; -import { withGithubAuthNextRoute } from "@/app/services/dal/github/middleware"; -import { GithubIdentificationScheme } from "@/app/services/dal/github/types"; -import { withZodValidation } from "@/app/services/dal/zod/middleware"; -import type { ResolvedReturnType } from "@/utils/types"; - -import handler from "./handler"; - -export declare namespace getGithubSourceMetadata { - export type Request = z.infer; - export type Response = ResolvedReturnType; -} - -const GetGithubSourceMetadataRequest = GithubIdentificationScheme.and( - z.object({ - orgName: orgNameValidator, - skipCache: z.boolean().optional() - }) -); - -export const POST = withZodValidation( - GetGithubSourceMetadataRequest, - async (req: NextRequest, validatedBody: z.infer) => { - const { orgName, skipCache, ...repoData } = validatedBody; - - return withGithubAuthNextRoute(req, orgName, repoData, async ({ githubUrl }) => { - const { maybeGetCurrentSession } = await import("@/app/api/utils/maybeGetCurrentSession"); - const sessionResult = await maybeGetCurrentSession(req); - if (sessionResult.errorResponse != null) { - return sessionResult.errorResponse; - } - const { userId } = sessionResult.data; - - const response = await handler({ userId, githubUrl, skipCache }); - return NextResponse.json(response); - }); - } -); diff --git a/packages/fern-dashboard/src/app/services/dal/github/assertAuthAndFetchGithubUrl.ts b/packages/fern-dashboard/src/app/services/dal/github/assertAuthAndFetchGithubUrl.ts index 3961dd59af..288c888e52 100644 --- a/packages/fern-dashboard/src/app/services/dal/github/assertAuthAndFetchGithubUrl.ts +++ b/packages/fern-dashboard/src/app/services/dal/github/assertAuthAndFetchGithubUrl.ts @@ -6,7 +6,7 @@ import type { DocsUrl } from "@/utils/types"; import { getCurrentSession } from "../../auth0/getCurrentSession"; import type { Auth0OrgName } from "../../auth0/types"; import { assertUserHasOrganizationAccess } from "../organization"; -import getDocsGithubUrl from "./getDocsGithubUrl"; +import { getDocsGithubUrl } from "./getDocsGithubUrl"; import { assertGithubAccessByUrl } from "./validators"; export const assertAuthAndFetchGithubUrl = cache( @@ -21,10 +21,7 @@ export const assertAuthAndFetchGithubUrl = cache( await assertUserHasOrganizationAccess(session.accessToken, orgName); // Validate GitHub access - const urlResult = await getDocsGithubUrl({ - url: docsUrl, - token: session.accessToken - }); + const urlResult = await getDocsGithubUrl(docsUrl, session.accessToken); if (!urlResult.success) { redirect(`/${orgName}/docs`); } diff --git a/packages/fern-dashboard/src/app/services/dal/github/getDocsGithubUrl.ts b/packages/fern-dashboard/src/app/services/dal/github/getDocsGithubUrl.ts index 0de5530741..73de4d5b1a 100644 --- a/packages/fern-dashboard/src/app/services/dal/github/getDocsGithubUrl.ts +++ b/packages/fern-dashboard/src/app/services/dal/github/getDocsGithubUrl.ts @@ -1,8 +1,9 @@ import "server-only"; import { fernToken_admin } from "@fern-api/docs-server"; - +import { cache } from "react"; import { getDocsUrlMetadata } from "@/app/api/utils/getDocsUrlMetadata"; +import type { DocsUrl } from "@/utils/types"; interface GetDocsGithubUrlSuccess { success: true; @@ -17,14 +18,9 @@ interface GetDocsGithubUrlError { | { type: "REPO_NOT_CONNECTED" }; } -type GetDocsGithubUrlResult = GetDocsGithubUrlSuccess | GetDocsGithubUrlError; -export default async function getDocsGithubUrl({ - url, - token -}: { - url: string; - token: string; -}): Promise { +export type GetDocsGithubUrlResult = GetDocsGithubUrlSuccess | GetDocsGithubUrlError; + +export const getDocsGithubUrl = cache(async (url: DocsUrl, token: string): Promise => { const docsUrlMetadata = await getDocsUrlMetadata({ url: decodeURIComponent(url), token: fernToken_admin() ?? token @@ -59,4 +55,4 @@ export default async function getDocsGithubUrl({ } return { success: true, githubUrl: docsUrlMetadata.body.gitUrl }; -} +}); diff --git a/packages/fern-dashboard/src/app/services/dal/github/getFernVersionUpdateInfo.ts b/packages/fern-dashboard/src/app/services/dal/github/getFernVersionUpdateInfo.ts index 10d2f46515..eb5ea3e7f5 100644 --- a/packages/fern-dashboard/src/app/services/dal/github/getFernVersionUpdateInfo.ts +++ b/packages/fern-dashboard/src/app/services/dal/github/getFernVersionUpdateInfo.ts @@ -20,49 +20,48 @@ export type GetFernVersionUpdateInfoResult = { export type GetFernVersionUpdateInfoError = GetFernVersionFromRepoError | { type: "MALFORMED_INPUT" }; -export async function getFernVersionUpdateInfo({ - githubUrl, - docsUrl, - baseBranch -}: { - githubUrl?: string; - docsUrl?: DocsUrl; - baseBranch?: string; -}): Promise< - { ok: true; result: GetFernVersionUpdateInfoResult } | { ok: false; error: GetFernVersionUpdateInfoError } -> { - "use cache"; - if (githubUrl == null || baseBranch == null || docsUrl == null) { - return { ok: false, error: { type: "MALFORMED_INPUT" } }; - } +import { cache } from "react"; + +export const getFernVersionUpdateInfo = cache( + async ( + githubUrl: string, + docsUrl: DocsUrl, + baseBranch: string + ): Promise< + { ok: true; result: GetFernVersionUpdateInfoResult } | { ok: false; error: GetFernVersionUpdateInfoError } + > => { + if (githubUrl == null || baseBranch == null || docsUrl == null) { + return { ok: false, error: { type: "MALFORMED_INPUT" } }; + } - const [fernVersionResult, latestVersion] = await Promise.all([ - getFernVersionFromRepo(githubUrl, docsUrl), - getLatestFernCliVersion() - ]); + const [fernVersionResult, latestVersion] = await Promise.all([ + getFernVersionFromRepo(githubUrl, docsUrl), + getLatestFernCliVersion() + ]); - if (!fernVersionResult.ok) { - return { ok: false, error: fernVersionResult.error }; - } + if (!fernVersionResult.ok) { + return { ok: false, error: fernVersionResult.error }; + } - const needsUpgrade = compareVersions(fernVersionResult.version, latestVersion); - const isBelowMinimum = compareVersions(fernVersionResult.version, MIN_VE_CLI_VERSION); + const needsUpgrade = compareVersions(fernVersionResult.version, latestVersion); + const isBelowMinimum = compareVersions(fernVersionResult.version, MIN_VE_CLI_VERSION); - let existingPr; + let existingPr; - // Show version info if any update is available - if (needsUpgrade) { - // Check if there's already an existing upgrade PR - existingPr = await checkUpgradePrStatus(githubUrl, fernVersionResult.version, latestVersion, baseBranch); - } - return { - ok: true, - result: { - current: fernVersionResult.version, - latest: latestVersion, - needsUpgrade, - isBelowMinimum, - existingPr + // Show version info if any update is available + if (needsUpgrade) { + // Check if there's already an existing upgrade PR + existingPr = await checkUpgradePrStatus(githubUrl, fernVersionResult.version, latestVersion, baseBranch); } - }; -} + return { + ok: true, + result: { + current: fernVersionResult.version, + latest: latestVersion, + needsUpgrade, + isBelowMinimum, + existingPr + } + }; + } +); diff --git a/packages/fern-dashboard/src/app/services/dal/mongodb/getRelevantUserBranchesForSite.ts b/packages/fern-dashboard/src/app/services/dal/mongodb/getRelevantUserBranchesForSite.ts deleted file mode 100644 index ec017d4ae6..0000000000 --- a/packages/fern-dashboard/src/app/services/dal/mongodb/getRelevantUserBranchesForSite.ts +++ /dev/null @@ -1,63 +0,0 @@ -"use server"; - -import { FdrAPI } from "@fern-api/fdr-sdk/client/types"; -import { type UnzippedEditorDocument, visualEditorStorage } from "@fern-api/visual-editor-server"; -import { branchMatchesUser } from "@fern-docs/components/navigation"; - -import type { DocsUrl } from "@/utils/types"; - -import { getCurrentSession } from "../../auth0/getCurrentSession"; -import type { Auth0OrgName } from "../../auth0/types"; -import { assertUserHasOrganizationAccess } from "../organization"; - -/** - * Gets relevant branches for a user by filtering all branches received from NavigationStorage - * to only includes branches that match the user's branch naming format (date-username-shortSubHash-randomHash) - * - * @param userId - Auth0 user ID (sub) for branch filtering - * @returns Array of branch names, sorted by relevance - */ -export async function getRelevantUserBranchesForSite( - orgName: Auth0OrgName, - docsUrl: DocsUrl, - branchNamesInClientStorage: string[] -): Promise { - try { - const session = await getCurrentSession(); - if (session == null) { - return []; - } - await assertUserHasOrganizationAccess(session.accessToken, orgName); - - // Filter local branches to only include branches that match the user's branch naming format - const userBranches = branchNamesInClientStorage.filter((branchName) => - branchMatchesUser(branchName, session.user.sub) - ); - - // Get the branch documents from the database - const branchDocuments = await visualEditorStorage.getDocumentsForBranches(userBranches); - - // Filter the branch documents to only include branches that match the site and org - const filteredBranchesForSiteAndOrg = branchDocuments - .filter( - (document) => - document != null && document.domain === docsUrl && document.data.orgId === FdrAPI.OrgId(orgName) - ) - .filter((document) => document != null); - - // Sort by date (newest first) - const sortedBranches = filteredBranchesForSiteAndOrg.sort( - (a: UnzippedEditorDocument, b: UnzippedEditorDocument) => { - const dateA = a.updatedAt; - const dateB = b.updatedAt; - - return dateB.getTime() - dateA.getTime(); - } - ); - - return sortedBranches.map((document) => document.branchName); - } catch (error) { - console.warn("Failed to get relevant branches from stored data:", error); - return []; - } -} diff --git a/packages/fern-dashboard/src/app/services/dashboard-api/client.ts b/packages/fern-dashboard/src/app/services/dashboard-api/client.ts index 8cffc3100d..a79beb5720 100644 --- a/packages/fern-dashboard/src/app/services/dashboard-api/client.ts +++ b/packages/fern-dashboard/src/app/services/dashboard-api/client.ts @@ -1,6 +1,5 @@ import type { generatePrDescription } from "@/app/api/generate-pr-description/route"; import type { getDocsUrlOwner } from "@/app/api/get-docs-url-owner/route"; -import type { getGithubSourceMetadata } from "@/app/api/get-github-source-metadata/route"; import type { getMyOrganizations } from "@/app/api/get-my-organizations/route"; import type { getOrgInvitations } from "@/app/api/get-org-invitations/route"; import type { getOrgMembers } from "@/app/api/get-org-members/route"; @@ -29,8 +28,6 @@ export const DashboardApiClient = { typedFetch("/api/post-git-create-pr", request), generatePrDescription: (request: generatePrDescription.Request) => typedFetch("/api/generate-pr-description", request), - getGithubSourceMetadata: (request: getGithubSourceMetadata.Request) => - typedFetch("/api/get-github-source-metadata", request), postDocsGithubSource: (request: postDocsGithubSource.Request) => typedFetch("/api/post-docs-github-source", request), validateGithubBranch: (request: validateGithubBranch.Request) => diff --git a/packages/fern-dashboard/src/components/docs-page/BranchList.tsx b/packages/fern-dashboard/src/components/docs-page/BranchList.tsx index aa439e5a5c..65444dcdf9 100644 --- a/packages/fern-dashboard/src/components/docs-page/BranchList.tsx +++ b/packages/fern-dashboard/src/components/docs-page/BranchList.tsx @@ -1,41 +1,27 @@ "use client"; import { useRouter } from "@bprogress/next/app"; -import { - constructEditorSlug, - createNavigationBufferedIndexedDBStorage, - ROOT_SLUG_ALIAS -} from "@fern-docs/components/navigation"; +import { constructEditorSlug, ROOT_SLUG_ALIAS } from "@fern-docs/components/navigation"; import { useState } from "react"; import { useOrgName } from "@/app/[orgName]/context/OrgNameContext"; -import type { Auth0SessionData } from "@/app/services/auth0/getCurrentSession"; import type { GithubSourceRepo } from "@/app/services/github/types"; import { Button } from "@/components/ui/button"; -import Card from "@/components/ui/card"; +import { useLocalBranches } from "@/hooks/useLocalBranches"; +import { useLocalBranchesForSite } from "@/hooks/useLocalBranchesForSite"; import type { DocsUrl, EncodedDocsUrl } from "@/utils/types"; import { BranchListItem } from "./BranchListItem"; -import { GoToEditorButton } from "./GoToEditorButton"; -import { VisualEditorHeader } from "./visual-editor-section/VisualEditorHeader"; -export function BranchList({ - docsUrl, - session, - sourceRepo, - branches, - maybeCriticalUpdateWarning -}: { - maybeCriticalUpdateWarning: React.ReactNode; - docsUrl: DocsUrl; - session: Auth0SessionData; - sourceRepo?: GithubSourceRepo; - branches: string[]; -}) { +export function BranchList({ docsUrl, sourceRepo }: { docsUrl: DocsUrl; sourceRepo?: GithubSourceRepo }) { const orgName = useOrgName(); const router = useRouter(); + const [deletedBranches, setDeletedBranches] = useState>(new Set()); + + const { deleteBranch, loading } = useLocalBranches(); + const { filteredBranches } = useLocalBranchesForSite(docsUrl); + // Pagination state const [visibleCount, setVisibleCount] = useState(3); - const [deletedBranches, setDeletedBranches] = useState>(new Set()); const BRANCHES_PER_PAGE = 3; const handleBranchClick = (branchName: string) => { @@ -49,66 +35,50 @@ export function BranchList({ }; const handleBranchDelete = (branchName: string) => { - const deleteBranch = async () => { - const storage = createNavigationBufferedIndexedDBStorage(); - await storage.init(); - storage.removeStore(branchName); - setDeletedBranches((prev) => new Set(prev).add(branchName)); - const remainingBranches = branches.filter((branch) => !deletedBranches.has(branch)); - if (visibleCount > remainingBranches.length) { - setVisibleCount(remainingBranches.length); - } - }; - void deleteBranch(); + deleteBranch(branchName); + setDeletedBranches((prev) => new Set(prev).add(branchName)); + if (visibleCount > filteredBranches.length) { + setVisibleCount(filteredBranches.length); + } }; const handleLoadMore = () => { - setVisibleCount((prev) => Math.min(prev + BRANCHES_PER_PAGE, availableBranches.length)); + setVisibleCount((prev) => Math.min(prev + BRANCHES_PER_PAGE, filteredBranches.length)); }; // Filter out deleted branches and get the branches to display (first N branches) - const availableBranches = branches.filter((branch) => !deletedBranches.has(branch)); + const availableBranches = filteredBranches.filter((branch) => !deletedBranches.has(branch.branchName)); const visibleBranches = availableBranches.slice(0, visibleCount); const hasMoreBranches = visibleCount < availableBranches.length; return ( - -
- {maybeCriticalUpdateWarning} -
- - + <> + {filteredBranches.length > 0 ? ( +
+ {visibleBranches.map((branch, index) => ( + + ))} + {hasMoreBranches && ( +
+ +
+ )}
- - {availableBranches.length > 0 && ( -
- {visibleBranches.map((branch, index) => ( - - ))} - {hasMoreBranches && ( -
- -
- )} -
- )} - - {availableBranches.length === 0 && ( -
-

No open sessions found

-
- )} -
- + ) : ( +
+

{loading ? "Loading..." : "No open sessions found"}

+
+ )} + ); } diff --git a/packages/fern-dashboard/src/components/docs-page/DocsSiteAttribute.tsx b/packages/fern-dashboard/src/components/docs-page/DocsSiteAttribute.tsx new file mode 100644 index 0000000000..59fc6380e3 --- /dev/null +++ b/packages/fern-dashboard/src/components/docs-page/DocsSiteAttribute.tsx @@ -0,0 +1,8 @@ +export function DocsSiteAttribute({ name, children }: { name: string; children: React.ReactNode }) { + return ( +
+

{name}

+ {children} +
+ ); +} diff --git a/packages/fern-dashboard/src/components/docs-page/DocsSiteOverviewCard.tsx b/packages/fern-dashboard/src/components/docs-page/DocsSiteOverviewCard.tsx index 58129f8efe..bef770d5c8 100644 --- a/packages/fern-dashboard/src/components/docs-page/DocsSiteOverviewCard.tsx +++ b/packages/fern-dashboard/src/components/docs-page/DocsSiteOverviewCard.tsx @@ -1,17 +1,23 @@ -"use client"; - import type { FdrAPI } from "@fern-api/fdr-sdk/client/types"; - +import { Suspense } from "react"; +import type { Auth0OrgName } from "@/app/services/auth0/types"; +import type { DocsUrl } from "@/utils/types"; import Card from "../ui/card"; +import { Skeleton } from "../ui/skeleton"; +import { DocsSiteAttribute } from "./DocsSiteAttribute"; import { DocsSiteLink } from "./DocsSiteLink"; import { DocsSiteImage } from "./docs-site-image/DocsSiteImage"; +import { FernCliVersion } from "./FernCliVersion"; +import { GithubSource } from "./GithubSource"; -export function DocsSiteOverviewCard({ +export async function DocsSiteOverviewCard({ docsSite, - githubProtectedArea + docsUrl, + orgName }: { + docsUrl: DocsUrl; docsSite: FdrAPI.dashboard.DocsSite; - githubProtectedArea: React.ReactNode; + orgName: Auth0OrgName; }) { return (
@@ -26,7 +32,18 @@ export function DocsSiteOverviewCard({ ))}
- {githubProtectedArea} +
+ + }> + + + + + }> + + + +
diff --git a/packages/fern-dashboard/src/components/docs-page/FernCliVersion.tsx b/packages/fern-dashboard/src/components/docs-page/FernCliVersion.tsx new file mode 100644 index 0000000000..9698590583 --- /dev/null +++ b/packages/fern-dashboard/src/components/docs-page/FernCliVersion.tsx @@ -0,0 +1,29 @@ +import "server-only"; + +import { getGitHubAuthState } from "@/app/actions/getGithubMetadata"; +import { getCurrentSession } from "@/app/services/auth0/getCurrentSession"; +import type { Auth0OrgName } from "@/app/services/auth0/types"; +import { getDocsGithubUrl } from "@/app/services/dal/github/getDocsGithubUrl"; +import type { DocsUrl } from "@/utils/types"; +import { FernCliVersionDisplay } from "./FernCliVersionDisplay"; + +/** + * Async wrapper component for FernCliVersionDisplay that handles the promise resolution + * This allows the parent to pass promises and let this component await them, + * enabling proper Suspense boundary behavior + */ +export async function FernCliVersion({ orgName, docsUrl }: { orgName: Auth0OrgName; docsUrl: DocsUrl }) { + const session = await getCurrentSession(); + if (session == null) { + return null; + } + const [githubUrlResult, githubAuthStateResult] = await Promise.all([ + getDocsGithubUrl(docsUrl, session.accessToken), + getGitHubAuthState(docsUrl, session.accessToken, orgName, session) + ]); + + const githubUrl = githubUrlResult.success ? githubUrlResult.githubUrl : undefined; + const baseBranch = "sourceRepo" in githubAuthStateResult ? githubAuthStateResult.sourceRepo?.baseBranch : undefined; + + return ; +} diff --git a/packages/fern-dashboard/src/components/docs-page/FernCliVersionDisplay.tsx b/packages/fern-dashboard/src/components/docs-page/FernCliVersionDisplay.tsx index 65003ea206..1efab6c428 100644 --- a/packages/fern-dashboard/src/components/docs-page/FernCliVersionDisplay.tsx +++ b/packages/fern-dashboard/src/components/docs-page/FernCliVersionDisplay.tsx @@ -1,7 +1,6 @@ import type { Auth0OrgName } from "@/app/services/auth0/types"; import { getFernVersionUpdateInfo } from "@/app/services/dal/github/getFernVersionUpdateInfo"; import type { DocsUrl } from "@/utils/types"; - import { FernIcon } from "../theme/FernIcon"; import { UpgradeFernButton } from "./UpgradeFernButton"; @@ -16,36 +15,32 @@ export async function FernCliVersionDisplay({ baseBranch?: string; orgName: Auth0OrgName; }) { - const fernVersionInfoResult = await getFernVersionUpdateInfo({ - githubUrl, - docsUrl, - baseBranch - }); + if (githubUrl == null || baseBranch == null) { + return null; + } + const fernVersionInfoResult = await getFernVersionUpdateInfo(githubUrl, docsUrl, baseBranch); - if (!fernVersionInfoResult.ok || githubUrl == null || baseBranch == null) { + if (!fernVersionInfoResult.ok) { return null; } - const fernVersionInfo = fernVersionInfoResult.ok ? fernVersionInfoResult.result : undefined; + const fernVersionInfo = fernVersionInfoResult.result; return ( -
-

Fern CLI Version

-
- {fernVersionInfo?.current} - {fernVersionInfo?.needsUpgrade && ( - - )} -
+
+ {fernVersionInfo?.current} + {fernVersionInfo?.needsUpgrade && ( + + )}
); } diff --git a/packages/fern-dashboard/src/components/docs-page/GithubSource.tsx b/packages/fern-dashboard/src/components/docs-page/GithubSource.tsx index d58462e648..5c20469f1e 100644 --- a/packages/fern-dashboard/src/components/docs-page/GithubSource.tsx +++ b/packages/fern-dashboard/src/components/docs-page/GithubSource.tsx @@ -1,81 +1,25 @@ -"use client"; +import "server-only"; -import { Loader2, Pencil } from "lucide-react"; -import { useState } from "react"; - -import type { GithubRepoValidationResult } from "@/app/services/dal/github/validators"; -import { getRepoDisplayNameFromUrl } from "@/app/services/github/github"; -import type { GithubSourceRepo } from "@/app/services/github/types"; +import { getCurrentSession } from "@/app/services/auth0/getCurrentSession"; +import { getDocsGithubUrl } from "@/app/services/dal/github/getDocsGithubUrl"; import type { DocsUrl } from "@/utils/types"; +import { GithubSourceClient } from "./GithubSourceClient"; -import { GithubLogo } from "../auth/GithubLogo"; -import { Button } from "../ui/button"; -import { Skeleton } from "../ui/skeleton"; -import { SetGithubSourcePopover } from "./SetGithubSource"; - -export interface GithubAuthState { - validationResult: GithubRepoValidationResult; - sourceRepo?: GithubSourceRepo; - isLoading?: boolean; -} - -export function GithubSource({ - docsUrl, - githubUrl, - isLoading -}: { - docsUrl: DocsUrl; - githubUrl?: string; - isLoading?: boolean; -}) { - const [isSaving, setIsSaving] = useState(false); +/** + * Async wrapper component for GithubSourceClient that handles the fetching of + * the GitHub URL to pass to our display component. + */ +export async function GithubSource({ docsUrl }: { docsUrl: DocsUrl }) { + const session = await getCurrentSession(); + if (session == null) { + return null; + } + const githubUrlResult = await getDocsGithubUrl(docsUrl, session?.accessToken); return ( - <> - {isLoading ? ( - - ) : ( -
-
- {githubUrl ? ( - <> - - - {getRepoDisplayNameFromUrl(githubUrl)} - - - - - - ) : ( - - - - )} -
-
- )} - + ); } diff --git a/packages/fern-dashboard/src/components/docs-page/GithubSourceClient.tsx b/packages/fern-dashboard/src/components/docs-page/GithubSourceClient.tsx new file mode 100644 index 0000000000..3330a8b920 --- /dev/null +++ b/packages/fern-dashboard/src/components/docs-page/GithubSourceClient.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Loader2, Pencil } from "lucide-react"; +import { useState } from "react"; + +import type { GithubRepoValidationResult } from "@/app/services/dal/github/validators"; +import { getRepoDisplayNameFromUrl } from "@/app/services/github/github"; +import type { GithubSourceRepo } from "@/app/services/github/types"; +import type { DocsUrl } from "@/utils/types"; + +import { GithubLogo } from "../auth/GithubLogo"; +import { Button } from "../ui/button"; +import { Skeleton } from "../ui/skeleton"; +import { SetGithubSourcePopover } from "./SetGithubSource"; + +export interface GithubAuthState { + validationResult: GithubRepoValidationResult; + sourceRepo?: GithubSourceRepo; + isLoading?: boolean; +} + +export function GithubSourceClient({ + docsUrl, + githubUrl, + isLoading +}: { + docsUrl: DocsUrl; + githubUrl?: string; + isLoading?: boolean; +}) { + const [isSaving, setIsSaving] = useState(false); + + return ( + <> + {isLoading ? ( + + ) : ( +
+
+ {githubUrl ? ( + <> + + + {getRepoDisplayNameFromUrl(githubUrl)} + + + + + + ) : ( + + + + )} +
+
+ )} + + ); +} diff --git a/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorLoadingCard.tsx b/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorLoadingCard.tsx new file mode 100644 index 0000000000..5807f10f40 --- /dev/null +++ b/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorLoadingCard.tsx @@ -0,0 +1,10 @@ +import Card from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + +export function VisualEditorLoadingCard() { + return ( + + + + ); +} diff --git a/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSection.tsx b/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSection.tsx index 28bf163fa0..7977ed2e98 100644 --- a/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSection.tsx +++ b/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSection.tsx @@ -1,28 +1,53 @@ import "server-only"; +import { getGitHubAuthState } from "@/app/actions/getGithubMetadata"; import type { Auth0SessionData } from "@/app/services/auth0/getCurrentSession"; import type { Auth0OrgName } from "@/app/services/auth0/types"; +import { getDocsGithubUrl } from "@/app/services/dal/github/getDocsGithubUrl"; +import type { GithubAuthState } from "@/components/docs-page/GithubSourceClient"; import type { DocsUrl } from "@/utils/types"; - -import type { GithubAuthState } from "../GithubSource"; import { CriticalUpdateWarning } from "./CriticalUpdateWarning"; import { VisualEditorEmptyCard } from "./VisualEditorEmptyCard"; import { VisualEditorSectionClient } from "./VisualEditorSectionClient"; import { VisualEditorValidationErrorHandler } from "./VisualEditorValidationErrorHandler"; +/** + * Async wrapper component for VisualEditorSection that handles the promise resolution + * This allows the parent to pass promises and let this component await them, + * enabling proper Suspense boundary behavior + */ export async function VisualEditorSection({ docsUrl, session, - githubAuthState, - githubUrl, orgName }: { docsUrl: DocsUrl; session: Auth0SessionData; - githubAuthState: GithubAuthState; - githubUrl?: string; orgName: Auth0OrgName; }) { + const [githubUrlResult, githubAuthStateResult] = await Promise.all([ + getDocsGithubUrl(docsUrl, session.accessToken), + getGitHubAuthState(docsUrl, session.accessToken, orgName, session) + ]); + + const githubUrl = githubUrlResult.success ? githubUrlResult.githubUrl : undefined; + + // Ensure we have a proper GithubAuthState, not an error result + const githubAuthState: GithubAuthState = + "validationResult" in githubAuthStateResult + ? githubAuthStateResult + : { + validationResult: { + ok: false, + error: { + type: "UNEXPECTED_ERROR", + message: "Failed to load GitHub auth state" + } + }, + sourceRepo: undefined, + isLoading: false + }; + if (!githubAuthState.validationResult.ok) { return ( @@ -64,7 +89,6 @@ export async function VisualEditorSection({ baseBranch={baseBranch} /> } - orgName={orgName} session={session} docsUrl={docsUrl} sourceRepo={githubAuthState.sourceRepo} diff --git a/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSectionClient.tsx b/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSectionClient.tsx index d66b2e6df5..c2d06357e8 100644 --- a/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSectionClient.tsx +++ b/packages/fern-dashboard/src/components/docs-page/visual-editor-section/VisualEditorSectionClient.tsx @@ -1,96 +1,58 @@ "use client"; -import { createNavigationBufferedIndexedDBStorage } from "@fern-docs/components/navigation"; -import { useEffect, useState } from "react"; - import type { Auth0SessionData } from "@/app/services/auth0/getCurrentSession"; -import type { Auth0OrgName } from "@/app/services/auth0/types"; -import { getRelevantUserBranchesForSite } from "@/app/services/dal/mongodb/getRelevantUserBranchesForSite"; import type { GithubSourceRepo } from "@/app/services/github/types"; import Card from "@/components/ui/card"; -import { Skeleton } from "@/components/ui/skeleton"; +import { useLocalBranches } from "@/hooks/useLocalBranches"; +import { useLocalBranchesForSite } from "@/hooks/useLocalBranchesForSite"; import type { DocsUrl } from "@/utils/types"; - import { BranchList } from "../BranchList"; import { GoToEditorButton } from "../GoToEditorButton"; import { VisualEditorEmptyCard } from "./VisualEditorEmptyCard"; import { VisualEditorHeader } from "./VisualEditorHeader"; +import { VisualEditorLoadingCard } from "./VisualEditorLoadingCard"; export function VisualEditorSectionClient({ maybeCriticalUpdateWarning, session, docsUrl, - sourceRepo, - orgName + sourceRepo }: { maybeCriticalUpdateWarning: React.ReactNode; session: Auth0SessionData; docsUrl: DocsUrl; sourceRepo?: GithubSourceRepo; - orgName: Auth0OrgName; }) { - const [relevantBranches, setRelevantBranches] = useState([]); - const [loadingBranches, setLoadingBranches] = useState(true); - - // Load relevant branches for user on this site and org - useEffect(() => { - setLoadingBranches(true); - const getBranches = async () => { - const storage = createNavigationBufferedIndexedDBStorage(); - await storage.init(); - const allBranches = storage.getAllStoredBranches(); - const relevantBranches = await getRelevantUserBranchesForSite(orgName, docsUrl, allBranches); - setRelevantBranches(relevantBranches); - setLoadingBranches(false); - }; - void getBranches(); - }, [session.user.sub, docsUrl, orgName]); - - // Loading state - if (loadingBranches) { - const loadingRow = ( -
- - -
- ); - return ( - -
- - -
-
- {loadingRow} -
- {loadingRow} -
- {loadingRow} -
-
- ); - } - - if (relevantBranches.length > 0) { - return ( - - ); - } + const { loading } = useLocalBranches(); + const { filteredBranches } = useLocalBranchesForSite(docsUrl); return ( - - <> - {maybeCriticalUpdateWarning} -
- -
- -
+ <> + {loading ? ( + + ) : ( + <> + {filteredBranches.length === 0 ? ( + + <> + {maybeCriticalUpdateWarning} +
+ +
+ +
+ ) : ( + + {maybeCriticalUpdateWarning} +
+ + +
+ +
+ )} + + )} + ); } diff --git a/packages/fern-dashboard/src/components/web-analytics/WebAnalyticsPage.tsx b/packages/fern-dashboard/src/components/web-analytics/WebAnalyticsPage.tsx index e72c045f07..c2440b1471 100644 --- a/packages/fern-dashboard/src/components/web-analytics/WebAnalyticsPage.tsx +++ b/packages/fern-dashboard/src/components/web-analytics/WebAnalyticsPage.tsx @@ -17,7 +17,7 @@ import SelectDate from "./SelectDate"; import AnalyticsTables from "./Tables"; interface WebAnalyticsPageProps { - docsUrl: string; + docsUrl: DocsUrl; } export default function WebAnalyticsPage({ docsUrl }: WebAnalyticsPageProps) { diff --git a/packages/fern-dashboard/src/hooks/useLocalBranches.ts b/packages/fern-dashboard/src/hooks/useLocalBranches.ts new file mode 100644 index 0000000000..bda74a9fe0 --- /dev/null +++ b/packages/fern-dashboard/src/hooks/useLocalBranches.ts @@ -0,0 +1,47 @@ +import { createNavigationBufferedIndexedDBStorage, type NavigationSnapshot } from "@fern-docs/components/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +export function useLocalBranches() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [allBranches, setAllBranches] = useState([]); + + const fetchAllBranches = useCallback(async () => { + setLoading(true); + setError(null); + const storage = createNavigationBufferedIndexedDBStorage(); + await storage.init(); + const branches = storage.getAllStoredBranches(); + setAllBranches(branches); + setLoading(false); + }, []); + + useEffect(() => { + void fetchAllBranches(); + }, []); + + const userHasCreatedAnyBranch = useMemo(() => { + return allBranches.length > 0; + }, [allBranches]); + + const refetch = async () => { + void fetchAllBranches(); + }; + + const deleteBranch = async (branchName: string) => { + const storage = createNavigationBufferedIndexedDBStorage(); + await storage.init(); + storage.removeStore(branchName); + setAllBranches(allBranches.filter((branch) => branch.branchName !== branchName)); + }; + + return { + allBranches, + loading, + error, + refetch, + deleteBranch, + userHasCreatedAnyBranch + }; +} diff --git a/packages/fern-dashboard/src/hooks/useLocalBranchesForSite.ts b/packages/fern-dashboard/src/hooks/useLocalBranchesForSite.ts new file mode 100644 index 0000000000..54ef2d70dc --- /dev/null +++ b/packages/fern-dashboard/src/hooks/useLocalBranchesForSite.ts @@ -0,0 +1,15 @@ +import { useMemo } from "react"; +import type { DocsUrl } from "@/utils/types"; +import { useLocalBranches } from "./useLocalBranches"; + +export function useLocalBranchesForSite(docsUrl: DocsUrl) { + const { allBranches } = useLocalBranches(); + const filteredBranches = useMemo( + () => + allBranches + .filter((branch) => branch.metadata.docsUrl === docsUrl) + .sort((a, b) => b.branchName.localeCompare(a.branchName)), + [docsUrl, allBranches] + ); + return { filteredBranches }; +} diff --git a/packages/fern-dashboard/src/state/queryKeys.ts b/packages/fern-dashboard/src/state/queryKeys.ts index 0024f2dfc9..75a4e4c124 100644 --- a/packages/fern-dashboard/src/state/queryKeys.ts +++ b/packages/fern-dashboard/src/state/queryKeys.ts @@ -1,10 +1,10 @@ import type { getDocsUrlOwner } from "@/app/api/get-docs-url-owner/route"; -import type { getGithubSourceMetadata } from "@/app/api/get-github-source-metadata/route"; import type { getMyOrganizations } from "@/app/api/get-my-organizations/route"; import type { getOrgMembers } from "@/app/api/get-org-members/route"; import type { getHomepageImageUrl } from "@/app/api/homepage-images/get/route"; import type { Theme } from "@/app/api/homepage-images/types"; import type { Auth0OrgName } from "@/app/services/auth0/types"; +import type { GithubSourceRepo } from "@/app/services/github/types"; import type { DocsUrl } from "@/utils/types"; import type { OrgInvitation } from "./types"; @@ -19,7 +19,7 @@ export const ReactQueryKey = { queryKey("homepage-image-url", orgName, ...docsUrls, theme), docsUrlOwner: (docsUrl: DocsUrl) => queryKey("docs-url-owner", docsUrl), orgSvgLogo: (svgUrl: string) => queryKey("org-svg", svgUrl), - githubSourceRepo: (githubUrl: string) => queryKey("github-source-repo", githubUrl) + githubSourceRepo: (githubUrl: string) => queryKey("github-source-repo", githubUrl) } as const; function queryKey(...key: string[]) { diff --git a/packages/fern-docs/components/src/navigation/NavigationStorage.ts b/packages/fern-docs/components/src/navigation/NavigationStorage.ts index 0d1b1f458e..a0c8bf71ca 100644 --- a/packages/fern-docs/components/src/navigation/NavigationStorage.ts +++ b/packages/fern-docs/components/src/navigation/NavigationStorage.ts @@ -183,22 +183,16 @@ export class NavigationStorage { legacyStorage.clear(); } - getAllStoredBranches(): string[] { + getAllStoredBranches(): NavigationSnapshot[] { this.checkBufferedStorageInitialized(); - const currentBranches = this._storage + const currentBranchNames = this._storage .getAllKeys() .map((key) => getBranchNameFromStorageKey(key)) .filter((key) => key !== undefined); - // Also check legacy LocalStorage for branches not in current storage - const legacyStorage = new LocalStorage(NAVIGATION_STORAGE_KEY); - const legacyBranches = legacyStorage - .getAllKeys() - .map((key) => getBranchNameFromStorageKey(key)) - .filter((key) => key !== undefined); + const currentBranches = currentBranchNames.map((key) => this.getStore(key)); - // Combine and deduplicate - return Array.from(new Set([...currentBranches, ...legacyBranches])); + return currentBranches.filter((snapshot) => snapshot !== null) as NavigationSnapshot[]; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2babf16367..b374e792c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1114,9 +1114,6 @@ importers: '@fern-api/venus-api-sdk': specifier: 0.20.0 version: 0.20.0 - '@fern-api/visual-editor-server': - specifier: workspace:* - version: link:../commons/visual-editor-server '@fern-docs/components': specifier: workspace:* version: link:../fern-docs/components