Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RPM - search package in mulitple repos #87

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cypress/e2e/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
6 changes: 6 additions & 0 deletions src/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
Partners,
PulpStatus,
RPMPackageList,
RPMSearch,
RoleCreate,
RoleList,
Search,
Expand Down Expand Up @@ -314,6 +315,11 @@ const routes: IRouteConfig[] = [
path: Paths.rpm.package.list,
beta: true,
},
{
component: RPMSearch,
path: Paths.rpm.search,
beta: true,
},
];

const AuthHandler = ({
Expand Down
1 change: 1 addition & 0 deletions src/containers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
334 changes: 334 additions & 0 deletions src/containers/rpm/search.tsx
Original file line number Diff line number Diff line change
@@ -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?;
}) => (
<section className='pulp-section' {...rest}>
{children}
</section>
);

const SectionSeparator = () => <section>&nbsp;</section>;

const SectionTitle = ({ children }: { children: ReactNode }) => (
<h2 className='pf-v5-c-title'>{children}</h2>
);

const Section = ({
children,
title,
}: {
children: ReactNode;
title: string;
}) => (
<>
<SectionSeparator />
<PageSection>
<SectionTitle>{title}</SectionTitle>
{children}
</PageSection>
</>
);

const SearchBar = ({
params,
style,
updateParams,
}: {
params?;
style?;
updateParams: (p) => void;
}) => {
const [inputText, setInputText] = useState<string>('');

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 (
<PageSection style={style}>
<div className='pulp-toolbar'>
<Toolbar>
<ToolbarContent>
<ToolbarGroup>
<ToolbarItem>
<CompoundFilter
inputText={inputText}
onChange={setInputText}
updateParams={(p) => updateParams(p)}
params={params || {}}
filterConfig={filterConfig}
/>
{/* FIXME checkbox for only latest version of each repo vs all .. or number for max? */}
</ToolbarItem>
</ToolbarGroup>
</ToolbarContent>
</Toolbar>
</div>
<div>
<AppliedFilters
updateParams={(p) => {
updateParams(p);
setInputText('');
}}
params={params || {}}
ignoredParams={['page_size', 'page', 'sort', 'ordering']}
niceNames={niceNames}
/>
</div>
</PageSection>
);
};

// 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 (
<DataListItem data-cy='PackageListItem'>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key='content' size={10}>
<div>
<Link to={namespace_url}>{name}</Link>
</div>
</DataListCell>,
].filter(Boolean)}
/>
</DataListItemRow>
</DataListItem>
);
};

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<AlertType[]>([]);
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 ? (
<Section title={title}>
{items === loading ? (
<LoadingSpinner />
) : !keywords ? (
showAllLink
) : (
<>
{children}
{showMoreLink}
<br />
{showAllLink}
</>
)}
</Section>
) : null;

return (
<>
<BaseHeader title={t`Search`} />
<AlertList
alerts={alerts}
closeAlert={(i) => closeAlert(i, { alerts, setAlerts })}
/>
<Main>
<SearchBar params={params} updateParams={(p) => updateParams(p)} />

<Section title={t`Repositories`}>
{repositories === loading ? (
<LoadingSpinner />
) : !repositories.length ? (
<EmptyStateXs
title={t`No repositories found`}
description={t`TODO link to repositories`}
/>
) : (
t`Found ${repositories.length} repositories`
)}
</Section>

<ResultsSection
items={namespaces}
title={t`Namespaces`}
showAllLink={
<Link
to={formatPath(Paths.ansible.namespace.list)}
>{t`Show all namespaces`}</Link>
}
showMoreLink={
<Link
to={formatPath(Paths.ansible.namespace.list, {}, { keywords })}
>{t`Show more namespaces`}</Link>
}
>
<DataList aria-label={t`Available matching namespaces`}>
{namespaces.map((ns, i) => (
<PackageListItem key={i} namespace={ns} />
))}
</DataList>
</ResultsSection>
</Main>
</>
);
};

export default withRouter(RPMSearch);
3 changes: 3 additions & 0 deletions src/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}),
Expand Down
1 change: 1 addition & 0 deletions src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export const Paths = {
search: '/search',
},
rpm: {
search: '/rpm/search',
package: { list: '/rpm/rpms' },
},
};