From 415e5ffd000704b8abca150c7a8c041797c1bda9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 02:46:54 +0000 Subject: [PATCH] Refactor: Replace skeleton loading with spinner overlay 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. --- ui/src/components/dataset-list-page.tsx | 41 +++------ ui/src/components/models/model-manager.tsx | 88 ++++-------------- ui/src/components/table-list-page.tsx | 69 ++++++-------- ui/src/components/ui/icons.tsx | 22 ++++- ui/src/components/workflow-list-page.tsx | 101 +++++++++------------ 5 files changed, 124 insertions(+), 197 deletions(-) diff --git a/ui/src/components/dataset-list-page.tsx b/ui/src/components/dataset-list-page.tsx index 3ec5b335..88c393a8 100644 --- a/ui/src/components/dataset-list-page.tsx +++ b/ui/src/components/dataset-list-page.tsx @@ -6,14 +6,8 @@ import { updateDataset, } from "@/actions"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from "@/components/ui/card"; import { CommonCard } from "@/components/ui/common-card"; -import { Skeleton } from "@/components/ui/skeleton"; +import { IconSpinner } from "@/components/ui/icons"; import { toast } from "@/hooks/use-toast"; // Import toast import { PlusIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons"; import { useCallback, useEffect, useState } from "react"; @@ -214,27 +208,18 @@ function DatasetList({ }, [fetchDatasetsInternal, refreshKey]); return ( -
+
+ {loading && ( +
+ +
+ )}
- {loading - ? Array.from({ length: 4 }).map((_, index) => ( - - - - - - - - - - - - - )) - : datasets - .filter((dataset) => - dataset.name.toLowerCase().includes(searchQuery.toLowerCase()), - ) + {!loading && + datasets + .filter((dataset) => + dataset.name.toLowerCase().includes(searchQuery.toLowerCase()), + ) .map((dataset) => ( ))}
- { setIsPreviewDialogOpen(false); diff --git a/ui/src/components/models/model-manager.tsx b/ui/src/components/models/model-manager.tsx index 408bf4bc..ed7e0fb3 100644 --- a/ui/src/components/models/model-manager.tsx +++ b/ui/src/components/models/model-manager.tsx @@ -10,8 +10,7 @@ import { ConfirmationDialog } from "@/components/models/confirmation-dialog"; import { ModelFormDialog } from "@/components/models/model-form-dialog"; import { ProviderCard } from "@/components/models/provider-card"; import { ProviderFormDialog } from "@/components/models/provider-form-dialog"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { Skeleton } from "@/components/ui/skeleton"; +import { IconSpinner } from "@/components/ui/icons"; import { useToast } from "@/hooks/use-toast"; import { PlusCircle, PlusIcon } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -345,70 +344,6 @@ export function ModelManager() { setIsProviderFormOpen(true); }; - const ProviderCardSkeleton = () => ( - - -
- {" "} - -
-
- - -
-
- -
- - -
- {[1, 2].map((i) => ( -
-
- - -
-
- - -
-
- ))} -
-
- ); - - if (isLoading) { - return ( -
- - - -
- ); - } - - if (providers.length === 0 && !searchQuery) { - return ( -
- -
- -

Create a new provider

-
-
- ); - } - return (
@@ -432,8 +367,25 @@ export function ModelManager() {
-
- {filteredProviders.length === 0 && searchQuery ? ( +
+ {isLoading ? ( +
+ +
+ ) : providers.length === 0 && !searchQuery ? ( +
+ {!isProviderFormOpen && ( +
+ +

Create a new provider

+
+ )} +
+ ) : filteredProviders.length === 0 && searchQuery ? (
No providers or models found matching your search.
diff --git a/ui/src/components/table-list-page.tsx b/ui/src/components/table-list-page.tsx index e7d31ae7..8dc14c55 100644 --- a/ui/src/components/table-list-page.tsx +++ b/ui/src/components/table-list-page.tsx @@ -1,13 +1,7 @@ import { deleteTable, getTableSchema, TableCreateRequest } from "@/actions"; import { ImportFileDialog } from "@/components/dialog/import-file"; -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from "@/components/ui/card"; import { CommonCard } from "@/components/ui/common-card"; -import { Skeleton } from "@/components/ui/skeleton"; +import { IconSpinner } from "@/components/ui/icons"; import { useCreateTableDialog } from "@/context/create-table"; import { useTables } from "@/context/tables"; import { JSONObject } from "@/json.ts"; @@ -132,42 +126,33 @@ function TableList({ searchQuery }: TableListProps) { }; return ( -
+
+ {loading && ( +
+ +
+ )}
- {loading - ? Array.from({ length: 4 }).map((_, index) => ( - - - - - - - - - - - - - )) - : tables - .filter((table) => - table.name.toLowerCase().includes(searchQuery.toLowerCase()), - ) - .map((table) => ( - navigate(`/tables/${table.id}`)} - onEdit={() => handleEditTableClick(table.id)} - onDelete={async () => { - await deleteTable(table.id); - await fetchTables(); - refreshTables(); - }} - > -

{table.description}

-
- ))} + {!loading && + tables + .filter((table) => + table.name.toLowerCase().includes(searchQuery.toLowerCase()), + ) + .map((table) => ( + navigate(`/tables/${table.id}`)} + onEdit={() => handleEditTableClick(table.id)} + onDelete={async () => { + await deleteTable(table.id); + await fetchTables(); + refreshTables(); + }} + > +

{table.description}

+
+ ))}
); diff --git a/ui/src/components/ui/icons.tsx b/ui/src/components/ui/icons.tsx index ecf2cd35..df4cfc38 100644 --- a/ui/src/components/ui/icons.tsx +++ b/ui/src/components/ui/icons.tsx @@ -17,4 +17,24 @@ function IconGithub({ className, ...props }: React.ComponentProps<"svg">) { ); } -export { IconGithub }; +function IconSpinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + + + ); +} + +export { IconGithub, IconSpinner }; diff --git a/ui/src/components/workflow-list-page.tsx b/ui/src/components/workflow-list-page.tsx index b9f8e3c2..8760643c 100644 --- a/ui/src/components/workflow-list-page.tsx +++ b/ui/src/components/workflow-list-page.tsx @@ -7,14 +7,8 @@ import { } from "@/actions"; import WorkflowBuilderDialog from "@/components/dialog/workflow/builder"; import WorkflowExecutionDialog from "@/components/dialog/workflow/workflow"; -import { - Card, - CardContent, - CardFooter, - CardHeader, -} from "@/components/ui/card"; import { CommonCard } from "@/components/ui/common-card"; -import { Skeleton } from "@/components/ui/skeleton"; +import { IconSpinner } from "@/components/ui/icons"; import { PlusIcon } from "@radix-ui/react-icons"; import { useCallback, useEffect, useState } from "react"; import { ModeToggle } from "./darkmode"; @@ -90,58 +84,49 @@ export function WorkflowListPage() {
-
- {loading && - Array.from({ length: 4 }).map((_, index) => ( - - - - - - - - - - - - - ))} - {!loading && - workflows - .filter((wf) => +
+ {loading ? ( +
+ +
+ ) : ( + <> + {workflows + .filter((wf) => + wf.name.toLowerCase().includes(searchQuery.toLowerCase()), + ) + .map((wf) => ( + { + const w = await getWorkflow(wf.id); + setWorkflow(w); + setRunWorkflowOpen(true); + }} + onEdit={async () => { + const w = await getWorkflow(wf.id); + setWorkflow(w); + setRunWorkflowBuilderOpen(true); + }} + onDelete={async () => { + await deleteWorkflow(wf.id); + await refreshWorkflows(); + }} + > +

{wf.description}

+
+ ))} + {workflows.filter((wf) => wf.name.toLowerCase().includes(searchQuery.toLowerCase()), - ) - .map((wf) => ( - { - const w = await getWorkflow(wf.id); - setWorkflow(w); - setRunWorkflowOpen(true); - }} - onEdit={async () => { - const w = await getWorkflow(wf.id); - setWorkflow(w); - setRunWorkflowBuilderOpen(true); - }} - onDelete={async () => { - await deleteWorkflow(wf.id); - await refreshWorkflows(); - }} - > -

{wf.description}

-
- ))} - {!loading && - workflows.filter((wf) => - wf.name.toLowerCase().includes(searchQuery.toLowerCase()), - ).length === 0 && - searchQuery && ( -
- No workflows found matching your search. -
- )} + ).length === 0 && + searchQuery && ( +
+ No workflows found matching your search. +
+ )} + + )}