-
- {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 (
+
+ );
+}
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
-
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