Skip to content

Commit a631bc8

Browse files
Refactor: Replace skeleton loading with spinner overlay (#60)
I've replaced the skeleton loading UI with a centered spinner icon overlay on the following pages: - Tables list - Datasets list - Models/Providers management - Workflows list This change aims to prevent the "flash" effect that could occur when data loads quickly, providing a smoother loading experience. The implementation involves: - Adding a reusable `IconSpinner` component. - Modifying the respective page/list components to use this spinner during their loading state. - Ensuring the spinner is centered and overlays the content area. Build checks, type checks, and unit tests in the ui/ directory pass with these changes. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 003a47b commit a631bc8

5 files changed

Lines changed: 124 additions & 197 deletions

File tree

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

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,8 @@ import {
66
updateDataset,
77
} from "@/actions";
88
import { Button } from "@/components/ui/button";
9-
import {
10-
Card,
11-
CardContent,
12-
CardFooter,
13-
CardHeader,
14-
} from "@/components/ui/card";
159
import { CommonCard } from "@/components/ui/common-card";
16-
import { Skeleton } from "@/components/ui/skeleton";
10+
import { IconSpinner } from "@/components/ui/icons";
1711
import { toast } from "@/hooks/use-toast"; // Import toast
1812
import { PlusIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
1913
import { useCallback, useEffect, useState } from "react";
@@ -214,27 +208,18 @@ function DatasetList({
214208
}, [fetchDatasetsInternal, refreshKey]);
215209

216210
return (
217-
<div className="grow overflow-auto h-full flex flex-col pt-6">
211+
<div className="grow overflow-auto h-full flex flex-col pt-6 relative">
212+
{loading && (
213+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-20">
214+
<IconSpinner className="w-10 h-10 text-primary" />
215+
</div>
216+
)}
218217
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-6">
219-
{loading
220-
? Array.from({ length: 4 }).map((_, index) => (
221-
<Card key={index} className="w-80">
222-
<CardHeader>
223-
<Skeleton className="h-6 w-3/4 mb-2" />
224-
</CardHeader>
225-
<CardContent>
226-
<Skeleton className="h-4 w-full mb-2" />
227-
<Skeleton className="h-4 w-full" />
228-
</CardContent>
229-
<CardFooter>
230-
<Skeleton className="h-4 w-1/4" />
231-
</CardFooter>
232-
</Card>
233-
))
234-
: datasets
235-
.filter((dataset) =>
236-
dataset.name.toLowerCase().includes(searchQuery.toLowerCase()),
237-
)
218+
{!loading &&
219+
datasets
220+
.filter((dataset) =>
221+
dataset.name.toLowerCase().includes(searchQuery.toLowerCase()),
222+
)
238223
.map((dataset) => (
239224
<CommonCard
240225
key={dataset.id}
@@ -256,7 +241,7 @@ function DatasetList({
256241
</CommonCard>
257242
))}
258243
</div>
259-
<DatasetPreviewDialog // Keep preview dialog here as it's specific to this list's interaction
244+
<DatasetPreviewDialog
260245
isOpen={isPreviewDialogOpen}
261246
onClose={() => {
262247
setIsPreviewDialogOpen(false);

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

Lines changed: 20 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import { ConfirmationDialog } from "@/components/models/confirmation-dialog";
1010
import { ModelFormDialog } from "@/components/models/model-form-dialog";
1111
import { ProviderCard } from "@/components/models/provider-card";
1212
import { ProviderFormDialog } from "@/components/models/provider-form-dialog";
13-
import { Card, CardContent, CardHeader } from "@/components/ui/card";
14-
import { Skeleton } from "@/components/ui/skeleton";
13+
import { IconSpinner } from "@/components/ui/icons";
1514
import { useToast } from "@/hooks/use-toast";
1615
import { PlusCircle, PlusIcon } from "lucide-react";
1716
import { useEffect, useMemo, useRef, useState } from "react";
@@ -345,70 +344,6 @@ export function ModelManager() {
345344
setIsProviderFormOpen(true);
346345
};
347346

348-
const ProviderCardSkeleton = () => (
349-
<Card className="mb-6">
350-
<CardHeader className="flex flex-row items-center justify-between py-4 px-6">
351-
<div>
352-
<Skeleton className="h-6 w-32 mb-2" />{" "}
353-
<Skeleton className="h-4 w-24" />
354-
</div>
355-
<div className="flex space-x-2">
356-
<Skeleton className="h-9 w-20 rounded-md" />
357-
<Skeleton className="h-9 w-9 rounded-md" />
358-
</div>
359-
</CardHeader>
360-
<CardContent className="px-6 pb-6">
361-
<div className="flex justify-between items-center mb-3">
362-
<Skeleton className="h-5 w-28" />
363-
<Skeleton className="h-9 w-32 rounded-md" />
364-
</div>
365-
{[1, 2].map((i) => (
366-
<div key={i} className="p-3 border rounded-md mb-3 bg-background">
367-
<div className="flex justify-between items-center mb-2">
368-
<Skeleton className="h-5 w-4/12" />
369-
<Skeleton className="h-8 w-8 rounded-md" />
370-
</div>
371-
<div className="space-y-1.5">
372-
<Skeleton className="h-3 w-10/12" />
373-
<Skeleton className="h-3 w-8/12" />
374-
</div>
375-
</div>
376-
))}
377-
</CardContent>
378-
</Card>
379-
);
380-
381-
if (isLoading) {
382-
return (
383-
<div>
384-
<ProviderCardSkeleton />
385-
<ProviderCardSkeleton />
386-
<ProviderCardSkeleton />
387-
</div>
388-
);
389-
}
390-
391-
if (providers.length === 0 && !searchQuery) {
392-
return (
393-
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
394-
<ProviderFormDialog
395-
isOpen={isProviderFormOpen}
396-
onOpenChange={setIsProviderFormOpen}
397-
onSubmit={handleProviderSubmit}
398-
initialData={editingProvider}
399-
/>
400-
<div
401-
className="border border-muted rounded-2xl p-12 bg-transparent text-muted-foreground flex flex-col items-center gap-3 hover:bg-muted-foreground/5 cursor-pointer"
402-
onClick={openAddProviderDialogInternal}
403-
aria-label="Add provider"
404-
>
405-
<PlusCircle className="w-8 h-8 mb-4" />
406-
<p>Create a new provider</p>
407-
</div>
408-
</div>
409-
);
410-
}
411-
412347
return (
413348
<div className="grow h-screen flex flex-col">
414349
<ModeToggle hide={true} />
@@ -432,8 +367,25 @@ export function ModelManager() {
432367
</div>
433368

434369
<ScrollArea className="flex-grow">
435-
<div className="max-w-6xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
436-
{filteredProviders.length === 0 && searchQuery ? (
370+
<div className="max-w-6xl mx-auto px-4 py-8 sm:px-6 lg:px-8 relative min-h-[300px]">
371+
{isLoading ? (
372+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-20">
373+
<IconSpinner className="w-10 h-10 text-primary" />
374+
</div>
375+
) : providers.length === 0 && !searchQuery ? (
376+
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
377+
{!isProviderFormOpen && (
378+
<div
379+
className="border border-muted rounded-2xl p-12 bg-transparent text-muted-foreground flex flex-col items-center gap-3 hover:bg-muted-foreground/5 cursor-pointer"
380+
onClick={openAddProviderDialogInternal}
381+
aria-label="Add provider"
382+
>
383+
<PlusCircle className="w-8 h-8 mb-4" />
384+
<p>Create a new provider</p>
385+
</div>
386+
)}
387+
</div>
388+
) : filteredProviders.length === 0 && searchQuery ? (
437389
<div className="text-center text-muted-foreground py-10">
438390
No providers or models found matching your search.
439391
</div>

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

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { deleteTable, getTableSchema, TableCreateRequest } from "@/actions";
22
import { ImportFileDialog } from "@/components/dialog/import-file";
3-
import {
4-
Card,
5-
CardContent,
6-
CardFooter,
7-
CardHeader,
8-
} from "@/components/ui/card";
93
import { CommonCard } from "@/components/ui/common-card";
10-
import { Skeleton } from "@/components/ui/skeleton";
4+
import { IconSpinner } from "@/components/ui/icons";
115
import { useCreateTableDialog } from "@/context/create-table";
126
import { useTables } from "@/context/tables";
137
import { JSONObject } from "@/json.ts";
@@ -132,42 +126,33 @@ function TableList({ searchQuery }: TableListProps) {
132126
};
133127

134128
return (
135-
<div className="grow overflow-auto h-full flex flex-col pt-6">
129+
<div className="grow overflow-auto h-full flex flex-col pt-6 relative">
130+
{loading && (
131+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-20">
132+
<IconSpinner className="w-10 h-10 text-primary" />
133+
</div>
134+
)}
136135
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-6">
137-
{loading
138-
? Array.from({ length: 4 }).map((_, index) => (
139-
<Card key={index} className="w-80">
140-
<CardHeader>
141-
<Skeleton className="h-6 w-3/4 mb-2" />
142-
</CardHeader>
143-
<CardContent>
144-
<Skeleton className="h-4 w-full mb-2" />
145-
<Skeleton className="h-4 w-full" />
146-
</CardContent>
147-
<CardFooter>
148-
<Skeleton className="h-4 w-1/4" />
149-
</CardFooter>
150-
</Card>
151-
))
152-
: tables
153-
.filter((table) =>
154-
table.name.toLowerCase().includes(searchQuery.toLowerCase()),
155-
)
156-
.map((table) => (
157-
<CommonCard
158-
key={table.id}
159-
name={table.name}
160-
onClick={() => navigate(`/tables/${table.id}`)}
161-
onEdit={() => handleEditTableClick(table.id)}
162-
onDelete={async () => {
163-
await deleteTable(table.id);
164-
await fetchTables();
165-
refreshTables();
166-
}}
167-
>
168-
<p className="line-clamp-4">{table.description}</p>
169-
</CommonCard>
170-
))}
136+
{!loading &&
137+
tables
138+
.filter((table) =>
139+
table.name.toLowerCase().includes(searchQuery.toLowerCase()),
140+
)
141+
.map((table) => (
142+
<CommonCard
143+
key={table.id}
144+
name={table.name}
145+
onClick={() => navigate(`/tables/${table.id}`)}
146+
onEdit={() => handleEditTableClick(table.id)}
147+
onDelete={async () => {
148+
await deleteTable(table.id);
149+
await fetchTables();
150+
refreshTables();
151+
}}
152+
>
153+
<p className="line-clamp-4">{table.description}</p>
154+
</CommonCard>
155+
))}
171156
</div>
172157
</div>
173158
);

ui/src/components/ui/icons.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,24 @@ function IconGithub({ className, ...props }: React.ComponentProps<"svg">) {
1717
);
1818
}
1919

20-
export { IconGithub };
20+
function IconSpinner({ className, ...props }: React.ComponentProps<"svg">) {
21+
return (
22+
<svg
23+
xmlns="http://www.w3.org/2000/svg"
24+
width="24"
25+
height="24"
26+
viewBox="0 0 24 24"
27+
fill="none"
28+
stroke="currentColor"
29+
strokeWidth="2"
30+
strokeLinecap="round"
31+
strokeLinejoin="round"
32+
className={cn("animate-spin", className)}
33+
{...props}
34+
>
35+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
36+
</svg>
37+
);
38+
}
39+
40+
export { IconGithub, IconSpinner };

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

Lines changed: 43 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,8 @@ import {
77
} from "@/actions";
88
import WorkflowBuilderDialog from "@/components/dialog/workflow/builder";
99
import WorkflowExecutionDialog from "@/components/dialog/workflow/workflow";
10-
import {
11-
Card,
12-
CardContent,
13-
CardFooter,
14-
CardHeader,
15-
} from "@/components/ui/card";
1610
import { CommonCard } from "@/components/ui/common-card";
17-
import { Skeleton } from "@/components/ui/skeleton";
11+
import { IconSpinner } from "@/components/ui/icons";
1812
import { PlusIcon } from "@radix-ui/react-icons";
1913
import { useCallback, useEffect, useState } from "react";
2014
import { ModeToggle } from "./darkmode";
@@ -90,58 +84,49 @@ export function WorkflowListPage() {
9084
<ScrollArea className="flex-grow">
9185
<div className="max-w-6xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
9286
<div className="tab-content-container">
93-
<div className="max-w-6xl grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-6">
94-
{loading &&
95-
Array.from({ length: 4 }).map((_, index) => (
96-
<Card key={index} className="w-80">
97-
<CardHeader>
98-
<Skeleton className="h-6 w-3/4 mb-2" />
99-
</CardHeader>
100-
<CardContent>
101-
<Skeleton className="h-4 w-full mb-2" />
102-
<Skeleton className="h-4 w-full" />
103-
</CardContent>
104-
<CardFooter>
105-
<Skeleton className="h-4 w-1/4" />
106-
</CardFooter>
107-
</Card>
108-
))}
109-
{!loading &&
110-
workflows
111-
.filter((wf) =>
87+
<div className="max-w-6xl grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-6 pt-6 relative min-h-[300px]">
88+
{loading ? (
89+
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-20 col-span-full">
90+
<IconSpinner className="w-10 h-10 text-primary" />
91+
</div>
92+
) : (
93+
<>
94+
{workflows
95+
.filter((wf) =>
96+
wf.name.toLowerCase().includes(searchQuery.toLowerCase()),
97+
)
98+
.map((wf) => (
99+
<CommonCard
100+
key={wf.id}
101+
name={wf.name}
102+
onClick={async () => {
103+
const w = await getWorkflow(wf.id);
104+
setWorkflow(w);
105+
setRunWorkflowOpen(true);
106+
}}
107+
onEdit={async () => {
108+
const w = await getWorkflow(wf.id);
109+
setWorkflow(w);
110+
setRunWorkflowBuilderOpen(true);
111+
}}
112+
onDelete={async () => {
113+
await deleteWorkflow(wf.id);
114+
await refreshWorkflows();
115+
}}
116+
>
117+
<p className="line-clamp-4">{wf.description}</p>
118+
</CommonCard>
119+
))}
120+
{workflows.filter((wf) =>
112121
wf.name.toLowerCase().includes(searchQuery.toLowerCase()),
113-
)
114-
.map((wf) => (
115-
<CommonCard
116-
key={wf.id}
117-
name={wf.name}
118-
onClick={async () => {
119-
const w = await getWorkflow(wf.id);
120-
setWorkflow(w);
121-
setRunWorkflowOpen(true);
122-
}}
123-
onEdit={async () => {
124-
const w = await getWorkflow(wf.id);
125-
setWorkflow(w);
126-
setRunWorkflowBuilderOpen(true);
127-
}}
128-
onDelete={async () => {
129-
await deleteWorkflow(wf.id);
130-
await refreshWorkflows();
131-
}}
132-
>
133-
<p className="line-clamp-4">{wf.description}</p>
134-
</CommonCard>
135-
))}
136-
{!loading &&
137-
workflows.filter((wf) =>
138-
wf.name.toLowerCase().includes(searchQuery.toLowerCase()),
139-
).length === 0 &&
140-
searchQuery && (
141-
<div className="col-span-full text-center text-muted-foreground py-10">
142-
No workflows found matching your search.
143-
</div>
144-
)}
122+
).length === 0 &&
123+
searchQuery && (
124+
<div className="col-span-full text-center text-muted-foreground py-10">
125+
No workflows found matching your search.
126+
</div>
127+
)}
128+
</>
129+
)}
145130
</div>
146131
</div>
147132
</div>

0 commit comments

Comments
 (0)