From 46785c7db5ba5aae5d71a2836efd83a71d52e5d2 Mon Sep 17 00:00:00 2001 From: Martin Hradil Date: Mon, 7 Oct 2024 03:30:25 +0000 Subject: [PATCH] PAUSED: Use `git resume` to continue working. [skip ci] --- cypress/e2e/smoke.js | 9 +- src/app-routes.tsx | 6 + src/containers/index.ts | 1 + src/containers/rpm/search.tsx | 334 ++++++++++++++++++++++++++++++++++ src/menu.tsx | 3 + src/paths.ts | 1 + 6 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 src/containers/rpm/search.tsx diff --git a/cypress/e2e/smoke.js b/cypress/e2e/smoke.js index edeb1806..0860e0f0 100644 --- a/cypress/e2e/smoke.js +++ b/cypress/e2e/smoke.js @@ -63,7 +63,14 @@ describe('UI smoke tests', () => { // TODO }); - it('RPMs', () => { + it('RPM Search', () => { + cy.ui('rpm/search'); + cy.assertTitle('Search'); + + // TODO + }); + + it('RPM Packages', () => { cy.ui('rpm/rpms'); cy.assertTitle('Packages'); diff --git a/src/app-routes.tsx b/src/app-routes.tsx index fdd494be..2d30b7e3 100644 --- a/src/app-routes.tsx +++ b/src/app-routes.tsx @@ -45,6 +45,7 @@ import { Partners, PulpStatus, RPMPackageList, + RPMSearch, RoleCreate, RoleList, Search, @@ -314,6 +315,11 @@ const routes: IRouteConfig[] = [ path: Paths.rpm.package.list, beta: true, }, + { + component: RPMSearch, + path: Paths.rpm.search, + beta: true, + }, ]; const AuthHandler = ({ diff --git a/src/containers/index.ts b/src/containers/index.ts index c75db44e..9bf5dcc4 100644 --- a/src/containers/index.ts +++ b/src/containers/index.ts @@ -38,6 +38,7 @@ export { default as RoleCreate } from './role-management/role-create'; export { default as EditRole } from './role-management/role-edit'; export { default as RoleList } from './role-management/role-list'; export { default as RPMPackageList } from './rpm/package-list'; +export { default as RPMSearch } from './rpm/search'; export { default as MultiSearch } from './search/multi-search'; export { default as Search } from './search/search'; export { default as UserProfile } from './settings/user-profile'; diff --git a/src/containers/rpm/search.tsx b/src/containers/rpm/search.tsx new file mode 100644 index 00000000..237c433f --- /dev/null +++ b/src/containers/rpm/search.tsx @@ -0,0 +1,334 @@ +import { t } from '@lingui/macro'; +import { + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; +import React, { type ReactNode, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { RPMPackageAPI, RPMRepositoryAPI } from 'src/api'; +import { + AlertList, + type AlertType, + AppliedFilters, + BaseHeader, + CompoundFilter, + EmptyStateXs, + LoadingSpinner, + Main, + closeAlert, +} from 'src/components'; +import { Paths, formatPath } from 'src/paths'; +import { + ParamHelper, + type RouteProps, + handleHttpError, + withRouter, +} from 'src/utilities'; + +const PageSection = ({ + children, + ...rest +}: { + children: ReactNode; + style?; +}) => ( +
+ {children} +
+); + +const SectionSeparator = () =>
 
; + +const SectionTitle = ({ children }: { children: ReactNode }) => ( +

{children}

+); + +const Section = ({ + children, + title, +}: { + children: ReactNode; + title: string; +}) => ( + <> + + + {title} + {children} + + +); + +const SearchBar = ({ + params, + style, + updateParams, +}: { + params?; + style?; + updateParams: (p) => void; +}) => { + const [inputText, setInputText] = useState(''); + + const filterConfig = [ + { + id: 'name', + title: t`Name (exact)`, + }, + { + id: 'name__contains', + title: t`Name (contains)`, + }, + { + id: 'name__startswith', + title: t`Name (starts with)`, + }, + { + id: 'epoch', + title: t`Epoch (exact)`, + }, + { + id: 'version', + title: t`Version (exact)`, + }, + { + id: 'release__contains', + title: t`Release (contains)`, + }, + { + id: 'arch__contains', + title: t`Arch (contains)`, + }, + ]; + const niceNames = Object.fromEntries( + filterConfig.map(({ id, title }) => [id, title]), + ); + + return ( + +
+ + + + + updateParams(p)} + params={params || {}} + filterConfig={filterConfig} + /> + {/* FIXME checkbox for only latest version of each repo vs all .. or number for max? */} + + + + +
+
+ { + updateParams(p); + setInputText(''); + }} + params={params || {}} + ignoredParams={['page_size', 'page', 'sort', 'ordering']} + niceNames={niceNames} + /> +
+
+ ); +}; + +// FIXME: `namespace` - eliminate +const PackageListItem = ({ + namespace, +}: { + namespace: { company: string; name: string }; +}) => { + const { name } = namespace; + const namespace_url = formatPath(Paths.ansible.namespace.detail, { + namespace: name, + }); + + return ( + + + +
+ {name} +
+ , + ].filter(Boolean)} + /> +
+
+ ); +}; + +const loading = []; + +function useRepositories({ addAlert }) { + const [repositories, setRepositories] = useState([]); + + useEffect(() => { + setRepositories(loading); + + RPMRepositoryAPI.list({ page_size: 100 }) + .then(({ data: { results } }) => setRepositories(results || [])) + .catch( + handleHttpError( + t`Failed to load repositories`, + () => setRepositories([]), + addAlert, + ), + ); + }, []); + + return repositories; +} + +const RPMSearch = (props: RouteProps) => { + const [alerts, setAlerts] = useState([]); + const [params, setParams] = useState({}); + + //const [collections, setCollections] = useState([]); + const [namespaces, setNamespaces] = useState([]); + + // TODO keywords isn't + const keywords = (params as { keywords: string })?.keywords || ''; + + const repositories = useRepositories({ addAlert }); + + function addAlert(alert: AlertType) { + setAlerts((prevAlerts) => [...prevAlerts, alert]); + } + + function query() { + if (!keywords) { + //setCollections([]); + setNamespaces([]); + return; + } + + const shared = { page_size: 10 }; + + // setCollections(loading); + // FIXME .. query should only call package api .. but per each repo + RPMPackageAPI.list({ ...shared, keywords, is_highest: true }) + // .then(({ data: { data } }) => setCollections(data || [])) + .catch( + handleHttpError( + t`Failed to search collections (${keywords})`, + // () => setCollections([]), + () => null, + addAlert, + ), + ); + } + + function updateParams(params) { + delete params.page; + + props.navigate({ + search: '?' + ParamHelper.getQueryString(params || []), + }); + + setParams(params); + } + + useEffect(() => { + setParams(ParamHelper.parseParamString(props.location.search)); + }, [props.location.search]); + + useEffect(() => { + query(); + }, [keywords]); + + const ResultsSection = ({ + children, + items, + showAllLink, + showMoreLink, + title, + }: { + children: ReactNode; + items; + showAllLink: ReactNode; + showMoreLink: ReactNode; + title: string; + }) => + items === loading || !keywords || items.length ? ( +
+ {items === loading ? ( + + ) : !keywords ? ( + showAllLink + ) : ( + <> + {children} + {showMoreLink} +
+ {showAllLink} + + )} +
+ ) : null; + + return ( + <> + + closeAlert(i, { alerts, setAlerts })} + /> +
+ updateParams(p)} /> + +
+ {repositories === loading ? ( + + ) : !repositories.length ? ( + + ) : ( + t`Found ${repositories.length} repositories` + )} +
+ + {t`Show all namespaces`} + } + showMoreLink={ + {t`Show more namespaces`} + } + > + + {namespaces.map((ns, i) => ( + + ))} + + +
+ + ); +}; + +export default withRouter(RPMSearch); diff --git a/src/menu.tsx b/src/menu.tsx index 880b30dd..4fb59be2 100644 --- a/src/menu.tsx +++ b/src/menu.tsx @@ -108,6 +108,9 @@ function standaloneMenu() { }), ]), menuSection('Pulp RPM', { condition: and(loggedIn, hasPlugin('rpm')) }, [ + menuItem(t`Search`, { + url: formatPath(Paths.rpm.search), + }), menuItem(t`RPMs`, { url: formatPath(Paths.rpm.package.list), }), diff --git a/src/paths.ts b/src/paths.ts index 02a6aff1..9c43fa94 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -140,6 +140,7 @@ export const Paths = { search: '/search', }, rpm: { + search: '/rpm/search', package: { list: '/rpm/rpms' }, }, };