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:
Sergio Castaño Arteaga 2023-02-27 11:35:18 +01:00 committed by GitHub
parent 4a20905de7
commit dfb9aec733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 222 additions and 17 deletions

View File

@ -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

View File

@ -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('{

View File

@ -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

View File

@ -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"],

View File

@ -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"},
}

View File

@ -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"`

View File

@ -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

View File

@ -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]);
}
});
}

View File

@ -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()}

View File

@ -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"

View File

@ -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);

View File

@ -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}

View File

@ -284,6 +284,7 @@ export interface BasicQuery {
operators?: boolean | null;
verifiedPublisher?: boolean | null;
official?: boolean | null;
cncf?: boolean | null;
total?: number;
}

View File

@ -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,
};

View File

@ -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;
};