Files
fusion/packages/dashboard/app/hooks/usePluginDashboardViews.ts
Fusion d7880c66b0 feat(FN-3157): add plugin dashboard view registry with nav integration
Merged FN-3157 to add a plugin dashboard views system, including a plugin view registry with lazy loading, navigation integration for Header and MobileNavBar, a usePluginDashboardViews hook with cache and refetch support, and tests covering the no-loader path. Also added documentation in `docs/PLUGI

Fusion-Task-Id: FN-3157
2026-05-05 12:14:20 -07:00

77 lines
2.4 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { fetchPluginDashboardViews } from "../api";
import type { PluginDashboardViewEntry } from "../api";
const dashboardViewsCache = new Map<string, { views: PluginDashboardViewEntry[]; expiresAt: number }>();
const CACHE_TTL_MS = 60_000;
/** Clear module cache for deterministic hook tests. */
export function __test_clearDashboardViewsCache(): void {
dashboardViewsCache.clear();
}
/**
* Fetch plugin dashboard views with a 60s project-scoped cache.
* Loading is only true for the first fetch of each hook lifecycle.
*/
export function usePluginDashboardViews(projectId?: string): {
views: PluginDashboardViewEntry[];
loading: boolean;
error: string | null;
refetch: () => void;
} {
const [views, setViews] = useState<PluginDashboardViewEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [reloadKey, setReloadKey] = useState(0);
const initialLoadCompleteRef = useRef(false);
const refetch = useCallback(() => {
const cacheKey = projectId ?? "default";
dashboardViewsCache.delete(cacheKey);
setReloadKey((key) => key + 1);
}, [projectId]);
useEffect(() => {
const cacheKey = projectId ?? "default";
let cancelled = false;
async function load(): Promise<void> {
const cached = dashboardViewsCache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt) {
if (cancelled) return;
setViews(cached.views);
setLoading(false);
return;
}
if (!initialLoadCompleteRef.current) {
setLoading(true);
}
setError(null);
try {
const data = await fetchPluginDashboardViews(projectId);
if (cancelled) return;
dashboardViewsCache.set(cacheKey, { views: data, expiresAt: Date.now() + CACHE_TTL_MS });
setViews(data);
} catch (err) {
if (cancelled) return;
setError(err instanceof Error ? err.message : "Failed to fetch plugin dashboard views");
} finally {
if (!cancelled) {
setLoading(false);
initialLoadCompleteRef.current = true;
}
}
}
void load();
return () => {
cancelled = true;
};
}, [projectId, reloadKey]);
return useMemo(() => ({ views, loading, error, refetch }), [views, loading, error, refetch]);
}