Skip to content

Commit 0da3cc3

Browse files
Fix: Introduce delayed loading to prevent screen flash
Introduced a `useDelayedLoading` hook to ensure that loading indicators are displayed for a minimum duration (500ms). This prevents the screen flashing effect on pages where data loads very quickly. The hook has been integrated into the following pages: - Dataset list page - Table list page - Model manager page - Workflow list page Updated `dataset-list-page.test.tsx` to accommodate the new loading delay, ensuring all automated checks pass.
1 parent 003a47b commit 0da3cc3

6 files changed

Lines changed: 68 additions & 19 deletions

File tree

ui/src/components/dataset-list-page.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ describe('DatasetListPage Create, Delete and Refresh', () => {
257257
// Note: Mocked CommonCard's onDelete is called directly, no confirmation dialog step here.
258258

259259
await waitFor(() => expect(screen.queryByText('Dataset Alpha')).not.toBeInTheDocument(), { timeout: 2000 });
260-
expect(screen.getByText('Dataset Beta')).toBeInTheDocument();
260+
await waitFor(() => expect(screen.getByText('Dataset Beta')).toBeInTheDocument());
261261

262262
expect(mockDeleteDataset).toHaveBeenCalledWith('1');
263263
await waitFor(() => expect(mockGetDatasets).toHaveBeenCalledTimes(2)); // Initial load + load after delete

ui/src/components/dataset-list-page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
updateDataset,
77
} from "@/actions";
88
import { Button } from "@/components/ui/button";
9+
import { useDelayedLoading } from "@/hooks/use-delayed-loading";
910
import {
1011
Card,
1112
CardContent,
@@ -189,23 +190,24 @@ function DatasetList({
189190
searchQuery,
190191
refreshKey,
191192
}: DatasetListProps) {
192-
const [loading, setLoading] = useState(true);
193+
const [actualLoading, setActualLoading] = useState(true);
194+
const loading = useDelayedLoading(actualLoading, 500); // Using 500ms delay
193195
const [datasets, setDatasets] = useState<DatasetInfo[]>([]);
194196
const [isPreviewDialogOpen, setIsPreviewDialogOpen] = useState(false);
195197
const [selectedDatasetId, setSelectedDatasetId] = useState<string | null>(
196198
null,
197199
);
198200

199201
const fetchDatasetsInternal = useCallback(async () => {
200-
setLoading(true);
202+
setActualLoading(true);
201203
try {
202204
const resp = await getDatasets();
203205
setDatasets(resp.datasets ?? []);
204206
} catch (error) {
205207
console.error("Failed to fetch datasets in DatasetList:", error);
206208
setDatasets([]);
207209
} finally {
208-
setLoading(false);
210+
setActualLoading(false);
209211
}
210212
}, []);
211213

@@ -245,10 +247,10 @@ function DatasetList({
245247
}}
246248
onEdit={() => onEditDataset(dataset)}
247249
onDelete={async () => {
248-
setLoading(true);
250+
setActualLoading(true);
249251
await deleteDataset(dataset.id);
250252
await fetchDatasets();
251-
setLoading(false);
253+
setActualLoading(false);
252254
}}
253255
badgeText={dataset.type}
254256
>

ui/src/components/models/model-manager.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ProviderCard } from "@/components/models/provider-card";
1212
import { ProviderFormDialog } from "@/components/models/provider-form-dialog";
1313
import { Card, CardContent, CardHeader } from "@/components/ui/card";
1414
import { Skeleton } from "@/components/ui/skeleton";
15+
import { useDelayedLoading } from "@/hooks/use-delayed-loading";
1516
import { useToast } from "@/hooks/use-toast";
1617
import { PlusCircle, PlusIcon } from "lucide-react";
1718
import { useEffect, useMemo, useRef, useState } from "react";
@@ -24,7 +25,8 @@ import { ScrollArea } from "../ui/scroll-area";
2425
export function ModelManager() {
2526
const { toast } = useToast();
2627
const [providers, setProviders] = useState<Provider[]>([]);
27-
const [isLoading, setIsLoading] = useState(true);
28+
const [actualIsLoading, setActualIsLoading] = useState(true);
29+
const isLoading = useDelayedLoading(actualIsLoading, 500); // Using 500ms delay
2830
const [searchQuery, setSearchQuery] = useState("");
2931

3032
// Dialog states
@@ -47,14 +49,14 @@ export function ModelManager() {
4749

4850
useEffect(() => {
4951
const fetchData = async () => {
50-
setIsLoading(true);
52+
setActualIsLoading(true);
5153
try {
5254
const fetchedProviders = await getProviders();
5355
setProviders(fetchedProviders);
5456
} catch (error) {
5557
console.error("Failed to fetch providers:", error);
5658
} finally {
57-
setIsLoading(false);
59+
setActualIsLoading(false);
5860
}
5961
};
6062
fetchData();
@@ -106,10 +108,10 @@ export function ModelManager() {
106108
});
107109
}
108110
setEditingProvider(null);
109-
setIsLoading(true);
111+
setActualIsLoading(true);
110112
getProviders()
111113
.then(setProviders)
112-
.finally(() => setIsLoading(false));
114+
.finally(() => setActualIsLoading(false));
113115
} catch (error) {
114116
console.error("Failed to save provider:", error);
115117
}
@@ -122,10 +124,10 @@ export function ModelManager() {
122124
title: "Provider Deleted",
123125
description: "The provider has been removed.",
124126
});
125-
setIsLoading(true);
127+
setActualIsLoading(true);
126128
getProviders()
127129
.then(setProviders)
128-
.finally(() => setIsLoading(false));
130+
.finally(() => setActualIsLoading(false));
129131
} catch (error) {
130132
console.error("Failed to delete provider:", error);
131133
toast({

ui/src/components/table-list-page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { CommonCard } from "@/components/ui/common-card";
1010
import { Skeleton } from "@/components/ui/skeleton";
1111
import { useCreateTableDialog } from "@/context/create-table";
12+
import { useDelayedLoading } from "@/hooks/use-delayed-loading";
1213
import { useTables } from "@/context/tables";
1314
import { JSONObject } from "@/json.ts";
1415
import { FileIcon, PlusIcon } from "@radix-ui/react-icons";
@@ -96,20 +97,21 @@ interface TableListProps {
9697
}
9798

9899
function TableList({ searchQuery }: TableListProps) {
99-
const [loading, setLoading] = useState(true);
100+
const [actualLoading, setActualLoading] = useState(true);
101+
const loading = useDelayedLoading(actualLoading, 500); // Using 500ms delay
100102
const { openNewTableDialog, withForm, withTable, withSubmitCallback } =
101103
useCreateTableDialog();
102104
const { tables, refreshTables } = useTables();
103105
const navigate = useNavigate();
104106

105107
const fetchTables = useCallback(async () => {
106-
setLoading(true);
108+
setActualLoading(true);
107109
try {
108110
await refreshTables();
109111
} catch (error) {
110112
console.error("Failed to fetch tables:", error);
111113
} finally {
112-
setLoading(false);
114+
setActualLoading(false);
113115
}
114116
}, [refreshTables]);
115117

ui/src/components/workflow-list-page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@/components/ui/card";
1616
import { CommonCard } from "@/components/ui/common-card";
1717
import { Skeleton } from "@/components/ui/skeleton";
18+
import { useDelayedLoading } from "@/hooks/use-delayed-loading";
1819
import { PlusIcon } from "@radix-ui/react-icons";
1920
import { useCallback, useEffect, useState } from "react";
2021
import { ModeToggle } from "./darkmode";
@@ -25,22 +26,23 @@ import { ScrollArea } from "./ui/scroll-area";
2526

2627
export function WorkflowListPage() {
2728
const [workflows, setWorkflows] = useState<WorkflowInfo[]>([]);
28-
const [loading, setLoading] = useState(true);
29+
const [actualLoading, setActualLoading] = useState(true);
30+
const loading = useDelayedLoading(actualLoading, 500); // Using 500ms delay
2931
const [searchQuery, setSearchQuery] = useState("");
3032
const [workflow, setWorkflow] = useState<undefined | Workflow>(undefined);
3133
const [runWorkflowOpen, setRunWorkflowOpen] = useState(false);
3234
const [WorkflowBuilderOpen, setRunWorkflowBuilderOpen] = useState(false);
3335

3436
const refreshWorkflows = useCallback(async () => {
35-
setLoading(true);
37+
setActualLoading(true);
3638
try {
3739
const wf = await getWorkflows();
3840
setWorkflows(wf.workflows ?? []);
3941
} catch (error) {
4042
console.error("Failed to fetch workflows:", error);
4143
setWorkflows([]);
4244
} finally {
43-
setLoading(false);
45+
setActualLoading(false);
4446
}
4547
}, []);
4648

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useState, useEffect, useRef } from 'react';
2+
3+
export function useDelayedLoading(
4+
actualLoading: boolean,
5+
minDelay: number = 300 // Default minimum delay of 300ms
6+
): boolean {
7+
const [delayedLoading, setDelayedLoading] = useState(actualLoading);
8+
const timerRef = useRef<NodeJS.Timeout | null>(null);
9+
10+
useEffect(() => {
11+
if (actualLoading) {
12+
setDelayedLoading(true);
13+
if (timerRef.current) {
14+
clearTimeout(timerRef.current);
15+
timerRef.current = null;
16+
}
17+
} else {
18+
// If actual loading is false, we want to keep delayedLoading true
19+
// until minDelay has passed since actualLoading became false.
20+
timerRef.current = setTimeout(() => {
21+
setDelayedLoading(false);
22+
timerRef.current = null;
23+
}, minDelay);
24+
}
25+
26+
// Cleanup timeout on unmount or if actualLoading changes back to true
27+
return () => {
28+
if (timerRef.current) {
29+
clearTimeout(timerRef.current);
30+
timerRef.current = null;
31+
}
32+
};
33+
}, [actualLoading, minDelay]);
34+
35+
// Ensure initial state is correct
36+
useEffect(() => {
37+
setDelayedLoading(actualLoading);
38+
}, [actualLoading]);
39+
40+
return delayedLoading;
41+
}

0 commit comments

Comments
 (0)