diff --git a/cypress/e2e/smoke.js b/cypress/e2e/smoke.js
index edeb180..0860e0f 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 fdd494b..2d30b7e 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 c75db44..9bf5dcc 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 0000000..237c433
--- /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?;
+}) => (
+
+);
+
+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 880b30d..4fb59be 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 02a6aff..9c43fa9 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' },
},
};