From f70f3399de2c8f327bfe7efee23df3193f3c8070 Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Fri, 24 Apr 2026 11:23:09 +0200 Subject: [PATCH] EDM-3777: Filter repositories via the API --- apps/ocp-plugin/package.json | 1 - apps/standalone/package.json | 1 - libs/ui-components/package.json | 1 - .../CreateRepository/CreateRepository.tsx | 16 +++++++------ .../DeleteRepositoryModal.tsx | 2 +- .../components/Repository/RepositoryList.tsx | 21 ++++++++--------- .../components/Repository/useRepositories.ts | 21 ++++++++++++++--- .../RepositoryResourceSyncList.tsx | 22 ++++++++---------- .../MassDeleteRepositoryModal.tsx | 4 ++-- .../src/hooks/useTableTextSearch.ts | 23 ------------------- libs/ui-components/src/utils/query.ts | 18 ++++++++++++--- libs/ui-components/src/utils/search.ts | 9 -------- package-lock.json | 9 -------- 13 files changed, 63 insertions(+), 85 deletions(-) delete mode 100644 libs/ui-components/src/hooks/useTableTextSearch.ts diff --git a/apps/ocp-plugin/package.json b/apps/ocp-plugin/package.json index b43915026..3a6ad2391 100644 --- a/apps/ocp-plugin/package.json +++ b/apps/ocp-plugin/package.json @@ -82,7 +82,6 @@ "@patternfly/react-table": "^6.4.0", "@types/react-redux": "^7.1.33", "formik": "^2.4.9", - "fuzzysearch": "^1.0.3", "i18next": "^21.8.14", "js-yaml": "^4.1.1", "monaco-editor": "^0.51.0", diff --git a/apps/standalone/package.json b/apps/standalone/package.json index 1749ede59..8758eae60 100644 --- a/apps/standalone/package.json +++ b/apps/standalone/package.json @@ -48,7 +48,6 @@ "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", "formik": "^2.4.5", - "fuzzysearch": "^1.0.3", "i18next": "^21.8.14", "i18next-browser-languagedetector": "^7.2.1", "i18next-http-backend": "^2.5.0", diff --git a/libs/ui-components/package.json b/libs/ui-components/package.json index 5ec6b3859..c14d7b1fa 100644 --- a/libs/ui-components/package.json +++ b/libs/ui-components/package.json @@ -40,7 +40,6 @@ "@xterm/xterm": "^5.5.0", "file-saver": "^2.0.2", "formik": "^2.4.5", - "fuzzysearch": "^1.0.3", "js-yaml": "^4.1.1", "percent-round": "^2.3.1", "react-markdown": "^8.0.7", diff --git a/libs/ui-components/src/components/Repository/CreateRepository/CreateRepository.tsx b/libs/ui-components/src/components/Repository/CreateRepository/CreateRepository.tsx index 6dec518cb..19b51c7f8 100644 --- a/libs/ui-components/src/components/Repository/CreateRepository/CreateRepository.tsx +++ b/libs/ui-components/src/components/Repository/CreateRepository/CreateRepository.tsx @@ -43,12 +43,12 @@ const CreateRepository = () => { const navigate = useNavigate(); React.useEffect(() => { - const fetchResources = async () => { + const fetchResources = async (id: string) => { setIsLoading(true); try { const results = await Promise.allSettled([ - get(`repositories/${repositoryId}`), - get(commonQueries.getResourceSyncsByRepo(repositoryId as string)), + get(`repositories/${id}`), + get(commonQueries.getResourceSyncsByRepo({ repositoryId: id })), ]); if (isPromiseFulfilled(results[0])) { @@ -66,16 +66,16 @@ const CreateRepository = () => { } }; if (repositoryId) { - void fetchResources(); + void fetchResources(repositoryId); } }, [get, repositoryId]); const reloadResourceSyncs = React.useCallback(() => { - const reload = async () => { + const reload = async (id: string) => { try { setIsLoading(true); - const rsList = await get(commonQueries.getResourceSyncsByRepo(repositoryId as string)); + const rsList = await get(commonQueries.getResourceSyncsByRepo({ repositoryId: id })); setResourceSyncs(rsList.items); setRsError(undefined); } catch (e) { @@ -84,7 +84,9 @@ const CreateRepository = () => { setIsLoading(false); } }; - void reload(); + if (repositoryId) { + void reload(repositoryId); + } }, [get, repositoryId]); let content: React.ReactNode; diff --git a/libs/ui-components/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.tsx b/libs/ui-components/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.tsx index 46a9a0f1d..7f20853bf 100644 --- a/libs/ui-components/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryDetails/DeleteRepositoryModal.tsx @@ -58,7 +58,7 @@ const DeleteRepositoryModal = ({ repositoryId, onClose, onDeleteSuccess }: Delet const loadRS = React.useCallback(async () => { try { - const resourceSyncs = await get(commonQueries.getResourceSyncsByRepo(repositoryId)); + const resourceSyncs = await get(commonQueries.getResourceSyncsByRepo({ repositoryId })); setResourceSyncIds(resourceSyncs.items.map((rs) => rs.metadata.name || '')); setRsError(undefined); } catch (e) { diff --git a/libs/ui-components/src/components/Repository/RepositoryList.tsx b/libs/ui-components/src/components/Repository/RepositoryList.tsx index 51cf5a321..68e78fb1b 100644 --- a/libs/ui-components/src/components/Repository/RepositoryList.tsx +++ b/libs/ui-components/src/components/Repository/RepositoryList.tsx @@ -17,7 +17,6 @@ import { Repository } from '@flightctl/types'; import ListPageBody from '../ListPage/ListPageBody'; import ListPage from '../ListPage/ListPage'; import { getLastTransitionTimeText } from '../../utils/status/repository'; -import { useTableTextSearch } from '../../hooks/useTableTextSearch'; import DeleteRepositoryModal from './RepositoryDetails/DeleteRepositoryModal'; import TableTextSearch from '../Table/TableTextSearch'; import Table, { TableColumn } from '../Table/Table'; @@ -86,8 +85,6 @@ const getColumns = (t: TFunction): TableColumn[] => [ }, ]; -const getSearchText = (repo: Repository) => [repo.metadata.name]; - const RepositoryTableRow = ({ repository, canDelete, @@ -160,7 +157,8 @@ const repositoryTablePermissions = [ ]; const RepositoryTable = () => { const { t } = useTranslation(); - const [repositories, loading, error, isUpdating, refetch, pagination] = useRepositories(); + const [nameSearch, setNameSearch] = React.useState(''); + const [repositories, loading, error, isUpdating, refetch, pagination] = useRepositories(nameSearch); const [deleteModalRepoId, setDeleteModalRepoId] = React.useState(); const [isMassDeleteModalOpen, setIsMassDeleteModalOpen] = React.useState(false); @@ -169,7 +167,6 @@ const RepositoryTable = () => { refetch(); }; - const { search, setSearch, filteredData } = useTableTextSearch(repositories, getSearchText); const columns = React.useMemo(() => getColumns(t), [t]); const { hasSelectedRows, isAllSelected, isRowSelected, setAllSelected, onRowSelect } = useTableSelect(); @@ -183,7 +180,7 @@ const RepositoryTable = () => { - + @@ -207,15 +204,15 @@ const RepositoryTable = () => { data-testid="repositories-table" aria-label={t('Repositories table')} loading={isUpdating} - hasFilters={!!search} - emptyData={filteredData.length === 0} - clearFilters={() => setSearch('')} + hasFilters={!!nameSearch} + emptyData={repositories.length === 0} + clearFilters={() => setNameSearch('')} isAllSelected={isAllSelected} onSelectAll={setAllSelected} columns={columns} > - {filteredData.map((repository, rowIndex) => ( + {repositories.map((repository, rowIndex) => ( { - {repositories.length === 0 && } + {!isUpdating && repositories.length === 0 && !nameSearch && } {!!deleteModalRepoId && ( setDeleteModalRepoId(undefined)} @@ -247,7 +244,7 @@ const RepositoryTable = () => { setAllSelected(false); refetch(); }} - repositories={filteredData.filter(isRowSelected)} + repositories={repositories.filter(isRowSelected)} /> )} diff --git a/libs/ui-components/src/components/Repository/useRepositories.ts b/libs/ui-components/src/components/Repository/useRepositories.ts index b0f745a0f..721a47fed 100644 --- a/libs/ui-components/src/components/Repository/useRepositories.ts +++ b/libs/ui-components/src/components/Repository/useRepositories.ts @@ -9,14 +9,18 @@ import { PaginationDetails, useTablePagination } from '../../hooks/useTablePagin import { PAGE_SIZE } from '../../constants'; type RepositoriesEndpointArgs = { + name?: string; nextContinue?: string; }; -const getRepositoriesEndpoint = ({ nextContinue }: RepositoriesEndpointArgs) => { +const getRepositoriesEndpoint = ({ name, nextContinue }: RepositoriesEndpointArgs) => { const params = new URLSearchParams(); if (nextContinue !== undefined) { params.set('limit', `${PAGE_SIZE}`); } + if (name) { + params.set('fieldSelector', `metadata.name contains ${name}`); + } if (nextContinue) { params.set('continue', nextContinue); } @@ -29,7 +33,9 @@ export const useRepositoriesEndpoint = (args: RepositoriesEndpointArgs): [string return [repositoriesEndpointDebounced, endpoint !== repositoriesEndpointDebounced]; }; -export const useRepositories = (): [ +export const useRepositories = ( + name: string | undefined, +): [ Repository[], boolean, unknown, @@ -38,7 +44,16 @@ export const useRepositories = (): [ Pick, 'currentPage' | 'setCurrentPage' | 'itemCount'>, ] => { const { currentPage, setCurrentPage, itemCount, nextContinue, onPageFetched } = useTablePagination(); - const [repoEndpoint, isDebouncing] = useRepositoriesEndpoint({ nextContinue }); + + const prevNameRef = React.useRef(name); + React.useEffect(() => { + if (prevNameRef.current !== name) { + prevNameRef.current = name; + setCurrentPage(1); + } + }, [name, setCurrentPage]); + + const [repoEndpoint, isDebouncing] = useRepositoriesEndpoint({ name, nextContinue }); const [repoList, isLoading, error, refetch] = useFetchPeriodically( { diff --git a/libs/ui-components/src/components/ResourceSync/RepositoryResourceSyncList.tsx b/libs/ui-components/src/components/ResourceSync/RepositoryResourceSyncList.tsx index 5473623f4..140af948e 100644 --- a/libs/ui-components/src/components/ResourceSync/RepositoryResourceSyncList.tsx +++ b/libs/ui-components/src/components/ResourceSync/RepositoryResourceSyncList.tsx @@ -27,7 +27,6 @@ import { ResourceSync, ResourceSyncList, ResourceSyncType } from '@flightctl/typ import { getObservedHash } from '../../utils/status/repository'; import { useDeleteListAction } from '../ListPage/ListPageActions'; import Table, { TableColumn } from '../Table/Table'; -import { useTableTextSearch } from '../../hooks/useTableTextSearch'; import TableTextSearch from '../Table/TableTextSearch'; import { useTableSelect } from '../../hooks/useTableSelect'; @@ -79,8 +78,6 @@ const getColumns = (t: TFunction): TableColumn[] => [ }, ]; -const getSearchText = (resourceSync: ResourceSync) => [resourceSync.metadata.name]; - const ResourceSyncEmptyState = ({ addResourceSync }: { addResourceSync?: VoidFunction }) => { const { t } = useTranslation(); return ( @@ -177,16 +174,15 @@ const repositoryResourceSyncListPermissions = [ ]; const RepositoryResourceSyncList = ({ repositoryId }: { repositoryId: string }) => { + const [nameSearch, setNameSearch] = React.useState(''); const [rsList, isLoading, error, refetch] = useFetchPeriodically({ - endpoint: commonQueries.getResourceSyncsByRepo(repositoryId), + endpoint: commonQueries.getResourceSyncsByRepo({ repositoryId, rsName: nameSearch }), }); const resourceSyncs = rsList?.items || []; const { t } = useTranslation(); const { remove } = useFetch(); - const { filteredData, search, setSearch } = useTableTextSearch(resourceSyncs, getSearchText); - const columns = React.useMemo(() => getColumns(t), [t]); const { onRowSelect, hasSelectedRows, isAllSelected, isRowSelected, setAllSelected } = useTableSelect(); @@ -210,7 +206,7 @@ const RepositoryResourceSyncList = ({ repositoryId }: { repositoryId: string }) - + {canDelete && ( @@ -242,12 +238,12 @@ const RepositoryResourceSyncList = ({ repositoryId }: { repositoryId: string }) isAllSelected={isAllSelected} onSelectAll={setAllSelected} columns={columns} - hasFilters={!!search} - emptyData={filteredData.length === 0} - clearFilters={() => setSearch('')} + hasFilters={!!nameSearch} + emptyData={resourceSyncs.length === 0} + clearFilters={() => setNameSearch('')} > - {filteredData.map((resourceSync, rowIndex) => { + {resourceSyncs.map((resourceSync, rowIndex) => { const rsName = resourceSync.metadata.name as string; const isSelected = isRowSelected(resourceSync); return ( @@ -277,7 +273,7 @@ const RepositoryResourceSyncList = ({ repositoryId }: { repositoryId: string }) })} - {resourceSyncs.length === 0 && ( + {!isLoading && resourceSyncs.length === 0 && !nameSearch && ( setIsMassDeleteModalOpen(false)} - resources={filteredData.filter(isRowSelected)} + resources={resourceSyncs.filter(isRowSelected)} onDeleteSuccess={() => { setIsMassDeleteModalOpen(false); setAllSelected(false); diff --git a/libs/ui-components/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.tsx b/libs/ui-components/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.tsx index 2a69a1534..a406d1a13 100644 --- a/libs/ui-components/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.tsx +++ b/libs/ui-components/src/components/modals/massModals/MassDeleteRepositoryModal/MassDeleteRepositoryModal.tsx @@ -53,7 +53,7 @@ const MassDeleteRepositoryModal: React.FC = ({ const promises = repositories.map(async (r) => { const repositoryId = r.metadata.name || ''; const resourceSyncs = await get( - commonQueries.getResourceSyncsByRepo(repositoryId, { limit: 1 }), + commonQueries.getResourceSyncsByRepo({ repositoryId, options: { limit: 1 } }), ); rsCount[repositoryId] = getApiListCount(resourceSyncs); }); @@ -68,7 +68,7 @@ const MassDeleteRepositoryModal: React.FC = ({ setProgress(0); const promises = repositories.map(async (r) => { const repositoryId = r.metadata.name || ''; - const resourceSyncs = await get(commonQueries.getResourceSyncsByRepo(repositoryId)); + const resourceSyncs = await get(commonQueries.getResourceSyncsByRepo({ repositoryId })); const rsyncPromises = resourceSyncs.items.map((rsync) => remove(`resourcesyncs/${rsync.metadata.name}`)); const rsyncResults = await Promise.allSettled(rsyncPromises); const rejectedResults = rsyncResults.filter(isPromiseRejected); diff --git a/libs/ui-components/src/hooks/useTableTextSearch.ts b/libs/ui-components/src/hooks/useTableTextSearch.ts deleted file mode 100644 index bce831e8f..000000000 --- a/libs/ui-components/src/hooks/useTableTextSearch.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; -import fuzzy from 'fuzzysearch'; - -export const useTableTextSearch = ( - data: D[], - getText: (datum: D) => Array, -): { search: string; setSearch: (value: string) => void; filteredData: D[] } => { - const [search, setSearch] = React.useState(''); - - const filteredData = React.useMemo(() => { - return data.filter((d) => { - const texts = getText(d); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return texts.some((t) => t && fuzzy(search, t)); - }); - }, [data, search, getText]); - - return { - search, - setSearch, - filteredData, - }; -}; diff --git a/libs/ui-components/src/utils/query.ts b/libs/ui-components/src/utils/query.ts index 6567d5b9a..dcd5a873f 100644 --- a/libs/ui-components/src/utils/query.ts +++ b/libs/ui-components/src/utils/query.ts @@ -65,10 +65,22 @@ export const commonQueries = { } return `fleets?${searchParams.toString()}`; }, - getResourceSyncsByRepo: (repositoryId: string, options?: CommonQueryOptions) => { - const searchParams = new URLSearchParams(); - searchParams.set('fieldSelector', `spec.repository=${repositoryId}`); + getResourceSyncsByRepo: ({ + repositoryId, + rsName, + options, + }: { + repositoryId: string; + rsName?: string; + options?: CommonQueryOptions; + }) => { + const selectors: string[] = [`spec.repository=${repositoryId}`]; + if (rsName) { + selectors.push(`metadata.name contains ${rsName}`); + } + const searchParams = new URLSearchParams(); + searchParams.set('fieldSelector', selectors.join(',')); if (options?.limit) { searchParams.set('limit', `${options.limit}`); } diff --git a/libs/ui-components/src/utils/search.ts b/libs/ui-components/src/utils/search.ts index 830e93dbb..f045be9ca 100644 --- a/libs/ui-components/src/utils/search.ts +++ b/libs/ui-components/src/utils/search.ts @@ -1,17 +1,8 @@ -import fuzzy from 'fuzzysearch'; import { ApiVersion } from '@flightctl/types'; // Must be an even number for "getSearchResultsCount" to work export const MAX_TOTAL_SEARCH_RESULTS = 10; -export const fuzzySeach = (filter: string | undefined, value?: string): boolean => { - if (!filter) { - return true; - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - return fuzzy(filter.toLowerCase(), value ? value.toLowerCase() : value) as boolean; -}; - export const getSearchResultsCount = (labelCount: number, fleetCount: number) => { if (labelCount + fleetCount <= MAX_TOTAL_SEARCH_RESULTS) { return [labelCount, fleetCount]; diff --git a/package-lock.json b/package-lock.json index 8152c4a03..4ab6ada51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,6 @@ "@patternfly/react-table": "^6.4.0", "@types/react-redux": "^7.1.33", "formik": "^2.4.9", - "fuzzysearch": "^1.0.3", "i18next": "^21.8.14", "js-yaml": "^4.1.1", "monaco-editor": "^0.51.0", @@ -202,7 +201,6 @@ "@patternfly/react-styles": "^6.4.0", "@patternfly/react-table": "^6.4.0", "formik": "^2.4.5", - "fuzzysearch": "^1.0.3", "i18next": "^21.8.14", "i18next-browser-languagedetector": "^7.2.1", "i18next-http-backend": "^2.5.0", @@ -295,7 +293,6 @@ "@xterm/xterm": "^5.5.0", "file-saver": "^2.0.2", "formik": "^2.4.5", - "fuzzysearch": "^1.0.3", "js-yaml": "^4.1.1", "percent-round": "^2.3.1", "react-markdown": "^8.0.7", @@ -10509,12 +10506,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuzzysearch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", - "integrity": "sha512-s+kNWQuI3mo9OALw0HJ6YGmMbLqEufCh2nX/zzV5CrICQ/y4AwPxM+6TIiF9ItFCHXFCyM/BfCCmN57NTIJuPg==", - "license": "MIT" - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",