refactor: improve loading state management and add live active indicator in RepositoryTable

This commit is contained in:
Arunavo Ray
2025-05-24 21:00:27 +05:30
parent 3d8bdff9af
commit 3ff86de67d
2 changed files with 57 additions and 20 deletions

View File

@@ -34,10 +34,10 @@ import { useNavigation } from "@/components/layout/MainLayout";
export default function Repository() { export default function Repository() {
const [repositories, setRepositories] = useState<Repository[]>([]); const [repositories, setRepositories] = useState<Repository[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isInitialLoading, setIsInitialLoading] = useState(true);
const { user } = useAuth(); const { user } = useAuth();
const { registerRefreshCallback } = useLiveRefresh(); const { registerRefreshCallback, isLiveEnabled } = useLiveRefresh();
const { isGitHubConfigured } = useConfigStatus(); const { isGitHubConfigured, isFullyConfigured } = useConfigStatus();
const { navigationKey } = useNavigation(); const { navigationKey } = useNavigation();
const { filter, setFilter } = useFilterParams({ const { filter, setFilter } = useFilterParams({
searchTerm: "", searchTerm: "",
@@ -80,17 +80,20 @@ export default function Repository() {
onMessage: handleNewMessage, onMessage: handleNewMessage,
}); });
const fetchRepositories = useCallback(async () => { const fetchRepositories = useCallback(async (isLiveRefresh = false) => {
if (!user?.id) return; if (!user?.id) return;
// Don't fetch repositories if GitHub is not configured or still loading config // Don't fetch repositories if GitHub is not configured or still loading config
if (!isGitHubConfigured) { if (!isGitHubConfigured) {
setIsLoading(false); setIsInitialLoading(false);
return false; return false;
} }
try { try {
setIsLoading(true); // Set appropriate loading state based on refresh type
if (!isLiveRefresh) {
setIsInitialLoading(true);
}
const response = await apiRequest<RepositoryApiResponse>( const response = await apiRequest<RepositoryApiResponse>(
`/github/repositories?userId=${user.id}`, `/github/repositories?userId=${user.id}`,
@@ -103,23 +106,31 @@ export default function Repository() {
setRepositories(response.repositories); setRepositories(response.repositories);
return true; return true;
} else { } else {
toast.error(response.error || "Error fetching repositories"); // Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(response.error || "Error fetching repositories");
}
return false; return false;
} }
} catch (error) { } catch (error) {
toast.error( // Only show error toast for manual refreshes to avoid spam during live updates
error instanceof Error ? error.message : "Error fetching repositories" if (!isLiveRefresh) {
); toast.error(
error instanceof Error ? error.message : "Error fetching repositories"
);
}
return false; return false;
} finally { } finally {
setIsLoading(false); if (!isLiveRefresh) {
setIsInitialLoading(false);
}
} }
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object }, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
useEffect(() => { useEffect(() => {
// Reset loading state when component becomes active // Reset loading state when component becomes active
setIsLoading(true); setIsInitialLoading(true);
fetchRepositories(); fetchRepositories(false); // Manual refresh, not live
}, [fetchRepositories, navigationKey]); // Include navigationKey to trigger on navigation }, [fetchRepositories, navigationKey]); // Include navigationKey to trigger on navigation
// Register with global live refresh system // Register with global live refresh system
@@ -130,14 +141,14 @@ export default function Repository() {
} }
const unregister = registerRefreshCallback(() => { const unregister = registerRefreshCallback(() => {
fetchRepositories(); fetchRepositories(true); // Live refresh
}); });
return unregister; return unregister;
}, [registerRefreshCallback, fetchRepositories, isGitHubConfigured]); }, [registerRefreshCallback, fetchRepositories, isGitHubConfigured]);
const handleRefresh = async () => { const handleRefresh = async () => {
const success = await fetchRepositories(); const success = await fetchRepositories(false); // Manual refresh, show loading skeleton
if (success) { if (success) {
toast.success("Repositories refreshed successfully."); toast.success("Repositories refreshed successfully.");
} }
@@ -363,7 +374,7 @@ export default function Repository() {
toast.success(`Repository added successfully`); toast.success(`Repository added successfully`);
setRepositories((prevRepos) => [...prevRepos, response.repository]); setRepositories((prevRepos) => [...prevRepos, response.repository]);
await fetchRepositories(); await fetchRepositories(false); // Manual refresh after adding repository
setFilter((prev) => ({ setFilter((prev) => ({
...prev, ...prev,
@@ -463,7 +474,7 @@ export default function Repository() {
<Button <Button
variant="default" variant="default"
onClick={handleMirrorAllRepos} onClick={handleMirrorAllRepos}
disabled={isLoading || loadingRepoIds.size > 0} disabled={isInitialLoading || loadingRepoIds.size > 0}
> >
<FlipHorizontal className="h-4 w-4 mr-2" /> <FlipHorizontal className="h-4 w-4 mr-2" />
Mirror All Mirror All
@@ -490,7 +501,8 @@ export default function Repository() {
) : ( ) : (
<RepositoryTable <RepositoryTable
repositories={repositories} repositories={repositories}
isLoading={isLoading || !connected} isLoading={isInitialLoading || !connected}
isLiveActive={isLiveEnabled && isFullyConfigured}
filter={filter} filter={filter}
setFilter={setFilter} setFilter={setFilter}
onMirror={handleMirrorRepo} onMirror={handleMirrorRepo}

View File

@@ -13,6 +13,7 @@ import { useGiteaConfig } from "@/hooks/useGiteaConfig";
interface RepositoryTableProps { interface RepositoryTableProps {
repositories: Repository[]; repositories: Repository[];
isLoading: boolean; isLoading: boolean;
isLiveActive?: boolean;
filter: FilterParams; filter: FilterParams;
setFilter: (filter: FilterParams) => void; setFilter: (filter: FilterParams) => void;
onMirror: ({ repoId }: { repoId: string }) => Promise<void>; onMirror: ({ repoId }: { repoId: string }) => Promise<void>;
@@ -24,6 +25,7 @@ interface RepositoryTableProps {
export default function RepositoryTable({ export default function RepositoryTable({
repositories, repositories,
isLoading, isLoading,
isLiveActive = false,
filter, filter,
setFilter, setFilter,
onMirror, onMirror,
@@ -345,15 +347,38 @@ export default function RepositoryTable({
</div> </div>
{/* Status Bar */} {/* Status Bar */}
<div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3"> <div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3 relative">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-1.5 w-1.5 rounded-full bg-primary" /> <div className={`h-1.5 w-1.5 rounded-full ${isLiveActive ? 'bg-emerald-500' : 'bg-primary'}`} />
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
{hasAnyFilter {hasAnyFilter
? `Showing ${filteredRepositories.length} of ${repositories.length} repositories` ? `Showing ${filteredRepositories.length} of ${repositories.length} repositories`
: `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`} : `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
</span> </span>
</div> </div>
{/* Center - Live active indicator */}
{isLiveActive && (
<div className="flex items-center gap-1.5 absolute left-1/2 transform -translate-x-1/2">
<div
className="h-1 w-1 rounded-full bg-emerald-500"
style={{
animation: 'pulse 2s ease-in-out infinite'
}}
/>
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
Live active
</span>
<div
className="h-1 w-1 rounded-full bg-emerald-500"
style={{
animation: 'pulse 2s ease-in-out infinite',
animationDelay: '1s'
}}
/>
</div>
)}
{hasAnyFilter && ( {hasAnyFilter && (
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Filters applied Filters applied