From f65f5c4821ac96c0ba545205620cd4015d373fe6 Mon Sep 17 00:00:00 2001 From: Maksym Yezhov Date: Wed, 17 Dec 2025 10:34:31 -0800 Subject: [PATCH] refactor: move standard library to yamllibrary --- .../shared/FavoriteComponentToggle.tsx | 28 +------ .../FlowSidebar/components/LibraryStates.tsx | 4 - .../ReactFlow/FlowSidebar/components/index.ts | 2 +- .../FlowSidebar/sections/GraphComponents.tsx | 17 ++-- .../ComponentLibraryProvider.test.tsx | 6 +- .../ComponentLibraryProvider.tsx | 80 ++++--------------- src/services/componentService.test.ts | 66 --------------- src/services/componentService.ts | 77 +----------------- 8 files changed, 26 insertions(+), 254 deletions(-) diff --git a/src/components/shared/FavoriteComponentToggle.tsx b/src/components/shared/FavoriteComponentToggle.tsx index 2867f5fc9..d072a28ac 100644 --- a/src/components/shared/FavoriteComponentToggle.tsx +++ b/src/components/shared/FavoriteComponentToggle.tsx @@ -14,7 +14,6 @@ import { Spinner } from "@/components/ui/spinner"; import { useGuaranteedHydrateComponentReference } from "@/hooks/useHydrateComponentReference"; import { cn } from "@/lib/utils"; import { useComponentLibrary } from "@/providers/ComponentLibraryProvider"; -import { flattenFolders } from "@/providers/ComponentLibraryProvider/componentLibrary"; import { hydrateComponentReference } from "@/services/componentService"; import { type ComponentReference } from "@/utils/componentSpec"; import { MINUTES } from "@/utils/constants"; @@ -143,18 +142,14 @@ const FavoriteToggleButton = withSuspenseWrapper( ); const useComponentFlags = (component: ComponentReference) => { - const { checkIfUserComponent, componentLibrary } = useComponentLibrary(); + const { checkIfUserComponent, getComponentLibrary } = useComponentLibrary(); + const componentLibrary = getComponentLibrary("standard_components"); const isUserComponent = useMemo( () => checkIfUserComponent(component), [component, checkIfUserComponent], ); - const flatComponentList = useMemo( - () => (componentLibrary ? flattenFolders(componentLibrary) : []), - [componentLibrary], - ); - const { data: isInLibrary } = useSuspenseQuery({ queryKey: ["component", "flags", component.digest], queryFn: async () => { @@ -162,24 +157,7 @@ const useComponentFlags = (component: ComponentReference) => { if (isUserComponent) return true; - for (const c of flatComponentList) { - if (component.name === "Chicago Taxi Trips dataset") { - console.log(c.name, c.digest, component.digest); - } - - if (c.name && c.name !== component.name) { - // micro optimization to skip components with different names - continue; - } - - const digest = c.digest ?? (await hydrateComponentReference(c))?.digest; - - if (digest === component.digest) { - return true; - } - } - - return false; + return componentLibrary.hasComponent(component); }, staleTime: 10 * MINUTES, }); diff --git a/src/components/shared/ReactFlow/FlowSidebar/components/LibraryStates.tsx b/src/components/shared/ReactFlow/FlowSidebar/components/LibraryStates.tsx index dfd57e9e6..1bce52517 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/components/LibraryStates.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/components/LibraryStates.tsx @@ -12,7 +12,3 @@ export const LoadingState = () => ( export const ErrorState = ({ message }: { message: string }) => ( Error: {message} ); - -export const EmptyState = () => ( - No components found -); diff --git a/src/components/shared/ReactFlow/FlowSidebar/components/index.ts b/src/components/shared/ReactFlow/FlowSidebar/components/index.ts index 8879d72e8..c789d0e4a 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/components/index.ts +++ b/src/components/shared/ReactFlow/FlowSidebar/components/index.ts @@ -1,5 +1,5 @@ export { default as FolderItem } from "./FolderItem"; export { default as ImportComponent } from "./ImportComponent"; -export { EmptyState, ErrorState, LoadingState } from "./LibraryStates"; +export { ErrorState, LoadingState } from "./LibraryStates"; export { default as SearchInput } from "./SearchInput"; export { default as SearchResults } from "./SearchResults"; diff --git a/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx b/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx index 12a70727c..75b3d2671 100644 --- a/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx +++ b/src/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents.tsx @@ -22,7 +22,6 @@ import { useForcedSearchContext } from "@/providers/ComponentLibraryProvider/For import type { UIComponentFolder } from "@/types/componentLibrary"; import { - EmptyState, ErrorState, FolderItem, ImportComponent, @@ -131,7 +130,6 @@ function ComponentLibrarySection() { const { updateSearchFilter } = useForcedSearchContext(); const { - componentLibrary, usedComponentsFolder, userComponentsFolder, isLoading, @@ -139,6 +137,8 @@ function ComponentLibrarySection() { searchResult, } = useComponentLibrary(); + const standardComponentsLibrary = getComponentLibrary("standard_components"); + const handleFiltersChange = (filters: string[]) => { updateSearchFilter({ filters, @@ -147,7 +147,6 @@ function ComponentLibrarySection() { if (isLoading) return ; if (error) return ; - if (!componentLibrary) return ; if (!remoteComponentLibrarySearchEnabled && searchResult) { // If there's a search result, use the SearchResults component @@ -210,15 +209,9 @@ function ComponentLibrarySection() { icon="Cable" /> - {githubComponentLibraryEnabled && ( diff --git a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.test.tsx b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.test.tsx index ec0342159..10271fe91 100644 --- a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.test.tsx +++ b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.test.tsx @@ -41,15 +41,12 @@ vi.mock("./componentLibrary"); // Import mocked modules import * as componentLibraryUtils from "@/providers/ComponentLibraryProvider/componentLibrary"; -import * as componentService from "@/services/componentService"; import * as componentStore from "@/utils/componentStore"; import * as getComponentName from "@/utils/getComponentName"; import * as localforage from "@/utils/localforage"; // Mock implementations -const mockFetchAndStoreComponentLibrary = vi.mocked( - componentService.fetchAndStoreComponentLibrary, -); + const mockFetchUserComponents = vi.mocked( componentLibraryUtils.fetchUserComponents, ); @@ -134,7 +131,6 @@ describe("ComponentLibraryProvider - Component Management", () => { componentDuplicateDialogProps.handleImportComponent = undefined; // Setup default mock implementations - mockFetchAndStoreComponentLibrary.mockResolvedValue(mockComponentLibrary); mockFetchUserComponents.mockResolvedValue(mockUserComponentsFolder); mockFetchUsedComponents.mockReturnValue({ name: "Used Components", diff --git a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx index 7f60876a1..da76399e9 100644 --- a/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx +++ b/src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx @@ -15,14 +15,10 @@ import { isYamlLibraryConfiguration, } from "@/components/shared/GitHubLibrary/types"; import { - fetchAndStoreComponentLibrary, + COMPONENT_LIBRARY_URL, hydrateComponentReference, } from "@/services/componentService"; -import type { - ComponentFolder, - ComponentLibrary, - SearchResult, -} from "@/types/componentLibrary"; +import type { ComponentFolder, SearchResult } from "@/types/componentLibrary"; import type { ComponentReference, HydratedComponentReference, @@ -69,7 +65,6 @@ type AvailableComponentLibraries = | string; type ComponentLibraryContextType = { - componentLibrary: ComponentLibrary | undefined; userComponentsFolder: ComponentFolder | undefined; usedComponentsFolder: ComponentFolder; isLoading: boolean; @@ -136,6 +131,10 @@ function useComponentLibraryRegistry() { /** * In future we will have other library types, including "standard_library", "favorite_components", "used_components", etc. */ + [ + "standard_components", + new YamlFileLibrary("Standard library", COMPONENT_LIBRARY_URL), + ], ]), [queryClient], ); @@ -187,7 +186,6 @@ export const ComponentLibraryProvider = ({ const { getComponentLibraryObject, existingComponentLibraries } = useComponentLibraryRegistry(); - const [componentLibrary, setComponentLibrary] = useState(); const [userComponentsFolder, setUserComponentsFolder] = useState(); @@ -196,17 +194,6 @@ export const ComponentLibraryProvider = ({ const [newComponent, setNewComponent] = useState(null); - // Fetch main component library - const { - data: rawComponentLibrary, - isLoading: isLibraryLoading, - error: libraryError, - refetch: refetchLibrary, - } = useQuery({ - queryKey: ["componentLibrary"], - queryFn: fetchAndStoreComponentLibrary, - }); - // Fetch user components const { data: rawUserComponentsFolder, @@ -227,14 +214,6 @@ export const ComponentLibraryProvider = ({ ); // Methods - const refreshComponentLibrary = useCallback(async () => { - const { data: updatedLibrary } = await refetchLibrary(); - - if (updatedLibrary) { - setComponentLibrary(updatedLibrary); - } - }, [refetchLibrary]); - const refreshUserComponents = useCallback(async () => { const { data: updatedUserComponents } = await refetchUserComponents(); @@ -273,15 +252,7 @@ export const ComponentLibraryProvider = ({ }, }; - if (componentLibrary) { - const uniqueComponents = filterToUniqueByDigest( - flattenFolders(componentLibrary), - ); - - result.components.standard = uniqueComponents.filter( - (c) => c.spec && componentMatchesSearch(c.spec, search, filters), - ); - } + // classic search is not supported for now if (userComponentsFolder) { const uniqueComponents = filterToUniqueByDigest( @@ -303,13 +274,13 @@ export const ComponentLibraryProvider = ({ return result; }, - [componentLibrary, userComponentsFolder, usedComponentsFolder], + [userComponentsFolder, usedComponentsFolder], ); const internalAddComponentToLibrary = useCallback( async (hydratedComponent: HydratedComponentReference) => { await importComponent(hydratedComponent); - await refreshComponentLibrary(); + await refreshUserComponents(); setNewComponent(null); setExistingComponent(null); @@ -322,7 +293,7 @@ export const ComponentLibraryProvider = ({ }), ); }, - [refreshComponentLibrary, refreshUserComponents, importComponent], + [refreshUserComponents, importComponent], ); const handleImportComponent = useCallback( @@ -341,12 +312,7 @@ export const ComponentLibraryProvider = ({ console.error("Error importing component:", error); } }, - [ - newComponent, - refreshComponentLibrary, - refreshUserComponents, - importComponent, - ], + [newComponent, refreshUserComponents, importComponent], ); const addToComponentLibraryWithDuplicateCheck = useCallback( @@ -376,12 +342,7 @@ export const ComponentLibraryProvider = ({ console.error("Error adding component to library:", error); } }, - [ - userComponentsFolder, - refreshComponentLibrary, - refreshUserComponents, - importComponent, - ], + [userComponentsFolder, refreshUserComponents, importComponent], ); const addToComponentLibrary = useCallback( @@ -420,7 +381,6 @@ export const ComponentLibraryProvider = ({ USER_COMPONENTS_LIST_NAME, component.name, ).then(async () => { - await refreshComponentLibrary(); await refreshUserComponents(); }); } else { @@ -432,7 +392,7 @@ export const ComponentLibraryProvider = ({ console.error("Error deleting component:", error); } }, - [refreshComponentLibrary, refreshUserComponents], + [refreshUserComponents], ); const handleCloseDuplicationDialog = useCallback(() => { @@ -451,14 +411,6 @@ export const ComponentLibraryProvider = ({ [currentSearchFilter, searchComponentLibrary], ); - useEffect(() => { - if (!rawComponentLibrary) { - setComponentLibrary(undefined); - return; - } - setComponentLibrary(rawComponentLibrary); - }, [rawComponentLibrary]); - useEffect(() => { if (!rawUserComponentsFolder) { setUserComponentsFolder(undefined); @@ -476,12 +428,11 @@ export const ComponentLibraryProvider = ({ [], ); - const isLoading = isLibraryLoading || isUserComponentsLoading; - const error = libraryError || userComponentsError; + const isLoading = isUserComponentsLoading; + const error = userComponentsError; const value = useMemo( () => ({ - componentLibrary, userComponentsFolder, usedComponentsFolder, isLoading, @@ -495,7 +446,6 @@ export const ComponentLibraryProvider = ({ checkIfUserComponent, }), [ - componentLibrary, userComponentsFolder, usedComponentsFolder, isLoading, diff --git a/src/services/componentService.test.ts b/src/services/componentService.test.ts index 4815948a7..57ae0f60c 100644 --- a/src/services/componentService.test.ts +++ b/src/services/componentService.test.ts @@ -1,7 +1,6 @@ import yaml from "js-yaml"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import type { ComponentLibrary } from "@/types/componentLibrary"; import type { ComponentReference, ComponentSpec } from "@/utils/componentSpec"; import { generateDigest } from "@/utils/componentStore"; import * as localforage from "@/utils/localforage"; @@ -9,7 +8,6 @@ import * as localforage from "@/utils/localforage"; import { fetchAndStoreComponent, fetchAndStoreComponentByUrl, - fetchAndStoreComponentLibrary, getExistingAndNewUserComponent, inputsWithInvalidArguments, parseComponentData, @@ -489,68 +487,4 @@ describe("componentService", () => { expect(result).toEqual(["required-missing"]); }); }); - - describe("fetchAndStoreComponentLibrary", () => { - const mockLibrary: ComponentLibrary = { - folders: [ - { - name: "test-folder", - components: [ - { name: "component1", url: "https://example.com/comp1.yaml" }, - ], - folders: [], - }, - ], - }; - - it("should fetch and store component library successfully", async () => { - const { loadObjectFromYamlData } = await import("@/utils/cache"); - const componentYaml = yaml.dump({ - name: "component1", - implementation: { container: { image: "test" } }, - }); - - // First mock for fetching the library - mockFetch.mockResolvedValueOnce({ - ok: true, - headers: new Headers(), - arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), - } as Response); - - // Mock for fetching individual component from library - mockFetch.mockResolvedValueOnce({ - ok: true, - headers: new Headers(), - text: () => Promise.resolve(componentYaml), - } as Response); - - vi.mocked(loadObjectFromYamlData).mockReturnValue(mockLibrary); - vi.mocked(localforage.getComponentByUrl).mockResolvedValue(null); - - const result = await fetchAndStoreComponentLibrary(); - - expect(result).toEqual(mockLibrary); - expect(localforage.saveComponent).toHaveBeenCalledWith({ - id: expect.stringMatching(/^library-\d+$/), - url: "/component-library.yaml", - data: JSON.stringify(mockLibrary), - createdAt: expect.any(Number), - updatedAt: expect.any(Number), - }); - }); - - it("should handle fetch errors and fallback to local storage", async () => { - mockFetch.mockResolvedValue({ - ok: false, - headers: new Headers(), - statusText: "Not Found", - } as Response); - - vi.mocked(localforage.componentExistsByUrl).mockResolvedValue(false); - - await expect(fetchAndStoreComponentLibrary()).rejects.toThrow( - "Failed to load component library: Not Found", - ); - }); - }); }); diff --git a/src/services/componentService.ts b/src/services/componentService.ts index 4604c4faa..95163e858 100644 --- a/src/services/componentService.ts +++ b/src/services/componentService.ts @@ -1,9 +1,4 @@ import { getAppSettings } from "@/appSettings"; -import { - type ComponentLibrary, - isValidComponentLibrary, -} from "@/types/componentLibrary"; -import { loadObjectFromYamlData } from "@/utils/cache"; import { type ComponentReference, type ComponentSpec, @@ -42,77 +37,7 @@ interface ExistingAndNewComponent { newComponent: HydratedComponentReference | undefined; } -const COMPONENT_LIBRARY_URL = getAppSettings().componentLibraryUrl; - -/** - * Fetches the component library from local storage - */ -const loadComponentLibraryFromLocalStorage = - async (): Promise => { - const libraryExists = await componentExistsByUrl(COMPONENT_LIBRARY_URL); - - if (libraryExists) { - const storedLibrary = await getComponentByUrl(COMPONENT_LIBRARY_URL); - if (storedLibrary) { - try { - const parsedLibrary = JSON.parse(storedLibrary.data); - if (isValidComponentLibrary(parsedLibrary)) { - return parsedLibrary; - } - } catch (error) { - console.error("Error parsing stored component library:", error); - } - } - } - - return null; - }; - -/** - * Fetches the component library and stores all components in local storage - */ -export const fetchAndStoreComponentLibrary = - async (): Promise => { - // Try fetch from the URL - const response = await fetch(COMPONENT_LIBRARY_URL); - if (!response.ok) { - // Fallback to local storage - await loadComponentLibraryFromLocalStorage().then((library) => { - if (library) { - return library; - } - }); - - throw new Error( - `Failed to load component library: ${response.statusText}`, - ); - } - - const arrayBuffer = await response.arrayBuffer(); - const obj = loadObjectFromYamlData(arrayBuffer); - - if (!isValidComponentLibrary(obj)) { - // Fallback to local storage - await loadComponentLibraryFromLocalStorage().then((library) => { - if (library) { - return library; - } - }); - - throw new Error("Invalid component library structure"); - } - - // Store the fetched library in local storage - await saveComponent({ - id: `library-${Date.now()}`, - url: COMPONENT_LIBRARY_URL, - data: JSON.stringify(obj), - createdAt: Date.now(), - updatedAt: Date.now(), - }); - - return obj; - }; +export const COMPONENT_LIBRARY_URL = getAppSettings().componentLibraryUrl; /** * Fetch and store a single component by URL