mirror of https://github.com/artifacthub/hub.git
Add CNCF filter (#2812)
Signed-off-by: Sergio Castaño Arteaga <tegioz@icloud.com> Signed-off-by: Cintia Sanchez Garcia <cynthiasg@icloud.com> Co-authored-by: Sergio Castaño Arteaga <tegioz@icloud.com> Co-authored-by: Cintia Sanchez Garcia <cynthiasg@icloud.com>
This commit is contained in:
parent
4a20905de7
commit
dfb9aec733
|
|
@ -125,6 +125,12 @@ begin
|
|||
else
|
||||
true
|
||||
end
|
||||
and
|
||||
case when p_input ? 'cncf' and (p_input->>'cncf')::boolean = true then
|
||||
(r.cncf = true or p.cncf = true)
|
||||
else
|
||||
true
|
||||
end
|
||||
and
|
||||
case when p_input ? 'operators' and (p_input->>'operators')::boolean = true then
|
||||
p.is_operator = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
-- Start transaction and plan tests
|
||||
begin;
|
||||
select plan(29);
|
||||
select plan(30);
|
||||
|
||||
-- Declare some variables
|
||||
\set user1ID '00000000-0000-0000-0000-000000000001'
|
||||
|
|
@ -657,6 +657,52 @@ select results_eq(
|
|||
$$,
|
||||
'Official: true Deprecated: true | Packages 1 and 2 expected - No facets expected'
|
||||
);
|
||||
select results_eq(
|
||||
$$
|
||||
select data::jsonb, total_count::integer from search_packages('{
|
||||
"cncf": true
|
||||
}')
|
||||
$$,
|
||||
$$
|
||||
values (
|
||||
'{
|
||||
"packages": [
|
||||
{
|
||||
"package_id": "00000000-0000-0000-0000-000000000001",
|
||||
"name": "package1",
|
||||
"normalized_name": "package1",
|
||||
"category": 1,
|
||||
"stars": 10,
|
||||
"official": false,
|
||||
"cncf": true,
|
||||
"display_name": "Package 1",
|
||||
"description": "description",
|
||||
"logo_image_id": "00000000-0000-0000-0000-000000000001",
|
||||
"version": "1.0.0",
|
||||
"app_version": "12.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"production_organizations_count": 1,
|
||||
"ts": 1592299234,
|
||||
"repository": {
|
||||
"repository_id": "00000000-0000-0000-0000-000000000001",
|
||||
"kind": 0,
|
||||
"name": "repo1",
|
||||
"display_name": "Repo 1",
|
||||
"url": "https://repo1.com",
|
||||
"verified_publisher": true,
|
||||
"official": true,
|
||||
"cncf": true,
|
||||
"scanner_disabled": false,
|
||||
"user_alias": "user1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}'::jsonb,
|
||||
1
|
||||
)
|
||||
$$,
|
||||
'CNCF: true | Packages 1 expected - No facets expected'
|
||||
);
|
||||
select results_eq(
|
||||
$$
|
||||
select data::jsonb, total_count::integer from search_packages('{
|
||||
|
|
|
|||
|
|
@ -951,6 +951,7 @@ paths:
|
|||
- $ref: "#/components/parameters/OperatorsParam"
|
||||
- $ref: "#/components/parameters/VerifiedPublisherParam"
|
||||
- $ref: "#/components/parameters/OfficialParam"
|
||||
- $ref: "#/components/parameters/CNCFParam"
|
||||
- $ref: "#/components/parameters/SortParam"
|
||||
responses:
|
||||
"200":
|
||||
|
|
@ -4807,6 +4808,13 @@ components:
|
|||
type: boolean
|
||||
required: false
|
||||
description: Whether to get only verified publisher
|
||||
CNCFParam:
|
||||
in: query
|
||||
name: cncf
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
description: Whether to get only pacakges published by CNCF projects
|
||||
OfficialParam:
|
||||
in: query
|
||||
name: official
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ func buildSearchInput(qs url.Values) (*hub.SearchPackageInput, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Only display content from official repositories
|
||||
// Only display official packages
|
||||
var official bool
|
||||
if qs.Get("official") != "" {
|
||||
var err error
|
||||
|
|
@ -685,6 +685,16 @@ func buildSearchInput(qs url.Values) (*hub.SearchPackageInput, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Only display packages published by CNCF projects
|
||||
var cncf bool
|
||||
if qs.Get("cncf") != "" {
|
||||
var err error
|
||||
cncf, err = strconv.ParseBool(qs.Get("cncf"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cncf: %s", qs.Get("cncf"))
|
||||
}
|
||||
}
|
||||
|
||||
// Only display operators
|
||||
var operators bool
|
||||
if qs.Get("operators") != "" {
|
||||
|
|
@ -718,6 +728,7 @@ func buildSearchInput(qs url.Values) (*hub.SearchPackageInput, error) {
|
|||
Categories: categories,
|
||||
VerifiedPublisher: verifiedPublisher,
|
||||
Official: official,
|
||||
CNCF: cncf,
|
||||
Operators: operators,
|
||||
Deprecated: deprecated,
|
||||
Licenses: qs["license"],
|
||||
|
|
|
|||
|
|
@ -1395,6 +1395,7 @@ func TestSearch(t *testing.T) {
|
|||
{"invalid kind (one of them)", "kind=0&kind=z"},
|
||||
{"invalid verified publisher", "verified_publisher=z"},
|
||||
{"invalid official", "official=z"},
|
||||
{"invalid cncf", "cncf=z"},
|
||||
{"invalid operators", "operators=z"},
|
||||
{"invalid deprecated", "deprecated=z"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ type SearchPackageInput struct {
|
|||
Categories []PackageCategory `json:"categories,omitempty"`
|
||||
VerifiedPublisher bool `json:"verified_publisher"`
|
||||
Official bool `json:"official"`
|
||||
CNCF bool `json:"cncf"`
|
||||
Operators bool `json:"operators"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
Licenses []string `json:"licenses,omitempty"`
|
||||
|
|
|
|||
|
|
@ -24,9 +24,13 @@ const SIGNATURE_NAME = {
|
|||
};
|
||||
|
||||
const Signed = (props: Props) => {
|
||||
const notSupported = ![RepositoryKind.Helm, RepositoryKind.Container, RepositoryKind.Kubewarden].includes(
|
||||
props.repoKind
|
||||
);
|
||||
const notSupported = ![
|
||||
RepositoryKind.Helm,
|
||||
RepositoryKind.Container,
|
||||
RepositoryKind.Kubewarden,
|
||||
RepositoryKind.TektonPipeline,
|
||||
RepositoryKind.TektonTask,
|
||||
].includes(props.repoKind);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ const onChangeMock = jest.fn();
|
|||
const onFacetExpandableChangeMock = jest.fn();
|
||||
const onVerifiedPublisherChangeMock = jest.fn();
|
||||
const onOfficialChangeMock = jest.fn();
|
||||
const onCNCFChangeMock = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
forceCollapseList: false,
|
||||
|
|
@ -115,6 +116,7 @@ const defaultProps = {
|
|||
onOperatorsChange: jest.fn(),
|
||||
onVerifiedPublisherChange: onVerifiedPublisherChangeMock,
|
||||
onOfficialChange: onOfficialChangeMock,
|
||||
onCNCFChange: onCNCFChangeMock,
|
||||
onResetFilters: onResetFiltersMock,
|
||||
onFacetExpandableChange: onFacetExpandableChangeMock,
|
||||
deprecated: false,
|
||||
|
|
@ -272,9 +274,10 @@ describe('Filters', () => {
|
|||
it('renders component', () => {
|
||||
render(<Filters {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(17);
|
||||
expect(screen.getAllByRole('checkbox')).toHaveLength(18);
|
||||
expect(screen.getByLabelText('Official')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Verified publishers')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('CNCF')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Include deprecated')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -331,6 +334,16 @@ describe('Filters', () => {
|
|||
await waitFor(() => expect(onOfficialChangeMock).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('calls CNCFChange mock when CNCF checkbox is clicked', async () => {
|
||||
render(<Filters {...defaultProps} />);
|
||||
|
||||
const opt = screen.getByLabelText('CNCF');
|
||||
expect(opt).toBeInTheDocument();
|
||||
await userEvent.click(opt);
|
||||
|
||||
await waitFor(() => expect(onCNCFChangeMock).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('calls onchange mock when any checkbox is clicked', async () => {
|
||||
render(<Filters {...defaultProps} />);
|
||||
|
||||
|
|
@ -427,7 +440,7 @@ describe('Filters', () => {
|
|||
const labels = screen.getAllByTestId('checkboxLabel');
|
||||
|
||||
for (let j = 0; j < capabitiesTests[i].output.length; j++) {
|
||||
expect(labels[2 + j]).toHaveTextContent(capabitiesTests[i].output[j]);
|
||||
expect(labels[3 + j]).toHaveTextContent(capabitiesTests[i].output[j]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,11 +26,13 @@ interface Props {
|
|||
onDeprecatedChange: () => void;
|
||||
onOperatorsChange: () => void;
|
||||
onVerifiedPublisherChange: () => void;
|
||||
onCNCFChange: () => void;
|
||||
onOfficialChange: () => void;
|
||||
onResetFilters: () => void;
|
||||
deprecated?: boolean | null;
|
||||
operators?: boolean | null;
|
||||
verifiedPublisher?: boolean | null;
|
||||
cncf?: boolean | null;
|
||||
official?: boolean | null;
|
||||
device: string;
|
||||
}
|
||||
|
|
@ -225,6 +227,7 @@ const Filters = (props: Props) => {
|
|||
props.deprecated ||
|
||||
props.operators ||
|
||||
props.verifiedPublisher ||
|
||||
props.cncf ||
|
||||
props.official) && (
|
||||
<div className={`d-flex align-items-center ${styles.resetBtnWrapper}`}>
|
||||
<IoMdCloseCircleOutline className={`text-dark ${styles.resetBtnDecorator}`} />
|
||||
|
|
@ -288,6 +291,30 @@ const Filters = (props: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row align-items-baseline">
|
||||
<CheckBox
|
||||
name="cncf"
|
||||
value="cncf"
|
||||
device={props.device}
|
||||
className={styles.checkbox}
|
||||
labelClassName="mw-100 text-muted"
|
||||
label="CNCF"
|
||||
checked={props.cncf || false}
|
||||
onChange={props.onCNCFChange}
|
||||
/>
|
||||
|
||||
<div className="d-none d-md-block">
|
||||
<ElementWithTooltip
|
||||
tooltipClassName={styles.tooltipMessage}
|
||||
className={styles.tooltipIcon}
|
||||
element={<MdInfoOutline />}
|
||||
tooltipMessage="This package has been published by a CNCF project"
|
||||
visibleTooltip
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getKindFacets()}
|
||||
{getCategoriesFacets()}
|
||||
{getLicenseFacets()}
|
||||
|
|
|
|||
|
|
@ -127,6 +127,67 @@ exports[`Filters creates snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex flex-row align-items-baseline"
|
||||
>
|
||||
<div
|
||||
class="form-check me-sm-2 mb-2 checkbox"
|
||||
>
|
||||
<input
|
||||
aria-checked="false"
|
||||
class="form-check-input input"
|
||||
id="desktop-cncf-cncf"
|
||||
name="cncf"
|
||||
tabindex="0"
|
||||
type="checkbox"
|
||||
value="cncf"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label label mw-100 text-muted"
|
||||
data-testid="checkboxLabel"
|
||||
for="desktop-cncf-cncf"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-baseline mw-100"
|
||||
>
|
||||
<span
|
||||
class="d-inline-block text-truncate"
|
||||
>
|
||||
CNCF
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="d-none d-md-block"
|
||||
>
|
||||
<div
|
||||
class="position-relative tooltipIcon"
|
||||
>
|
||||
<div
|
||||
data-testid="elementWithTooltip"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
stroke="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 0h24v24H0V0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M11 7h2v2h-2V7zm0 4h2v6h-2v-6zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 mt-sm-2 pt-1 facet"
|
||||
role="menuitem"
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ describe('Search index', () => {
|
|||
|
||||
// Desktop + mobile (sidebar)
|
||||
expect(facets).toHaveLength(2 * 2);
|
||||
expect(options).toHaveLength(5 * 2);
|
||||
expect(options).toHaveLength(6 * 2);
|
||||
});
|
||||
|
||||
it('calls history push on filters change', async () => {
|
||||
|
|
@ -477,7 +477,7 @@ describe('Search index', () => {
|
|||
|
||||
expect(main).toBeInTheDocument();
|
||||
const checks = await screen.findAllByRole('checkbox');
|
||||
expect(checks).toHaveLength(18);
|
||||
expect(checks).toHaveLength(20);
|
||||
|
||||
rerender(
|
||||
<Router>
|
||||
|
|
@ -502,7 +502,7 @@ describe('Search index', () => {
|
|||
expect(noData).toHaveTextContent(
|
||||
`We're sorry! We can't seem to find any packages that match your search for "test1"`
|
||||
);
|
||||
expect(checks).toHaveLength(18);
|
||||
expect(checks).toHaveLength(20);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -698,7 +698,7 @@ describe('Search index', () => {
|
|||
|
||||
render(
|
||||
<Router>
|
||||
<SearchView {...defaultProps} official verifiedPublisher />
|
||||
<SearchView {...defaultProps} official verifiedPublisher cncf />
|
||||
</Router>
|
||||
);
|
||||
|
||||
|
|
@ -706,8 +706,9 @@ describe('Search index', () => {
|
|||
expect(API.searchPackages).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Remove filter: Only official' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Remove filter: Only verified publishers' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('button', { name: 'Remove filter: Official' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Remove filter: Verified publisher' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Remove filter: CNCF' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls history push on filters change', async () => {
|
||||
|
|
@ -724,7 +725,7 @@ describe('Search index', () => {
|
|||
expect(API.searchPackages).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Remove filter: Only official' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Remove filter: Official' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ interface Props {
|
|||
operators?: boolean | null;
|
||||
verifiedPublisher?: boolean | null;
|
||||
official?: boolean | null;
|
||||
cncf?: boolean | null;
|
||||
fromDetail: boolean;
|
||||
visibleModal?: string;
|
||||
sort?: string | null;
|
||||
|
|
@ -158,6 +159,7 @@ const SearchView = (props: Props) => {
|
|||
operators: props.operators,
|
||||
verifiedPublisher: props.verifiedPublisher,
|
||||
official: props.official,
|
||||
cncf: props.cncf,
|
||||
sort: props.sort,
|
||||
};
|
||||
};
|
||||
|
|
@ -218,6 +220,12 @@ const SearchView = (props: Props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const onCNCFChange = (): void => {
|
||||
updateCurrentPage({
|
||||
cncf: !isUndefined(props.cncf) && !isNull(props.cncf) ? !props.cncf : true,
|
||||
});
|
||||
};
|
||||
|
||||
const onOfficialChange = (): void => {
|
||||
updateCurrentPage({
|
||||
official: !isUndefined(props.official) && !isNull(props.official) ? !props.official : true,
|
||||
|
|
@ -276,6 +284,7 @@ const SearchView = (props: Props) => {
|
|||
operators: props.operators,
|
||||
verifiedPublisher: props.verifiedPublisher,
|
||||
official: props.official,
|
||||
cncf: props.cncf,
|
||||
sort: props.sort || DEFAULT_SORT,
|
||||
};
|
||||
|
||||
|
|
@ -330,13 +339,19 @@ const SearchView = (props: Props) => {
|
|||
props.operators,
|
||||
props.verifiedPublisher,
|
||||
props.official,
|
||||
props.cncf,
|
||||
ctx.prefs.search.limit,
|
||||
props.sort,
|
||||
]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
const activeFilters =
|
||||
props.deprecated || props.operators || props.verifiedPublisher || props.official || !isEmpty(props.filters);
|
||||
props.deprecated ||
|
||||
props.operators ||
|
||||
props.verifiedPublisher ||
|
||||
props.official ||
|
||||
props.cncf ||
|
||||
!isEmpty(props.filters);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -395,9 +410,11 @@ const SearchView = (props: Props) => {
|
|||
operators={props.operators}
|
||||
verifiedPublisher={props.verifiedPublisher}
|
||||
official={props.official}
|
||||
cncf={props.cncf}
|
||||
onDeprecatedChange={onDeprecatedChange}
|
||||
onOperatorsChange={onOperatorsChange}
|
||||
onVerifiedPublisherChange={onVerifiedPublisherChange}
|
||||
onCNCFChange={onCNCFChange}
|
||||
onOfficialChange={onOfficialChange}
|
||||
onResetFilters={onResetFilters}
|
||||
visibleTitle={false}
|
||||
|
|
@ -462,10 +479,11 @@ const SearchView = (props: Props) => {
|
|||
<div className="d-none d-lg-inline">
|
||||
<div className="d-flex flex-row flex-wrap align-items-center pt-2">
|
||||
<span className="me-2 pe-1 mb-2">Filters:</span>
|
||||
{props.official && <FilterBadge name="Only official" onClick={onOfficialChange} />}
|
||||
{props.official && <FilterBadge name="Official" onClick={onOfficialChange} />}
|
||||
{props.verifiedPublisher && (
|
||||
<FilterBadge name="Only verified publishers" onClick={onVerifiedPublisherChange} />
|
||||
<FilterBadge name="Verified publisher" onClick={onVerifiedPublisherChange} />
|
||||
)}
|
||||
{props.cncf && <FilterBadge name="CNCF" onClick={onCNCFChange} />}
|
||||
{!isUndefined(props.filters) && (
|
||||
<>
|
||||
{Object.keys(props.filters).map((type: string) => {
|
||||
|
|
@ -517,9 +535,11 @@ const SearchView = (props: Props) => {
|
|||
operators={props.operators}
|
||||
verifiedPublisher={props.verifiedPublisher}
|
||||
official={props.official}
|
||||
cncf={props.cncf}
|
||||
onDeprecatedChange={onDeprecatedChange}
|
||||
onOperatorsChange={onOperatorsChange}
|
||||
onVerifiedPublisherChange={onVerifiedPublisherChange}
|
||||
onCNCFChange={onCNCFChange}
|
||||
onOfficialChange={onOfficialChange}
|
||||
onResetFilters={onResetFilters}
|
||||
visibleTitle
|
||||
|
|
@ -609,6 +629,7 @@ const SearchView = (props: Props) => {
|
|||
operators: props.operators,
|
||||
verifiedPublisher: props.verifiedPublisher,
|
||||
official: props.official,
|
||||
cncf: props.cncf,
|
||||
sort: props.sort,
|
||||
}}
|
||||
saveScrollPosition={saveScrollPosition}
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ export interface BasicQuery {
|
|||
operators?: boolean | null;
|
||||
verifiedPublisher?: boolean | null;
|
||||
official?: boolean | null;
|
||||
cncf?: boolean | null;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const buildSearchParams = (query: string): SearchFiltersURL => {
|
|||
deprecated: p.has('deprecated') ? p.get('deprecated') === 'true' : false,
|
||||
operators: p.has('operators') ? p.get('operators') === 'true' : undefined,
|
||||
verifiedPublisher: p.has('verified_publisher') ? p.get('verified_publisher') === 'true' : undefined,
|
||||
cncf: p.has('cncf') ? p.get('cncf') === 'true' : undefined,
|
||||
official: p.has('official') ? p.get('official') === 'true' : undefined,
|
||||
sort: p.has('sort') ? p.get('sort') : undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ export const getURLSearchParams = (query: BasicQuery): URLSearchParams => {
|
|||
if (!isUndefined(query.official) && query.official) {
|
||||
q.set('official', 'true');
|
||||
}
|
||||
if (!isUndefined(query.cncf) && query.cncf) {
|
||||
q.set('cncf', 'true');
|
||||
}
|
||||
return q;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue