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

Activity Log search by metadata #3056

Open
wants to merge 5 commits into
base: searching-by-metadata-impl
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions assets/js/common/ComposedFilter/ComposedFilter.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import React, { useEffect, useState } from 'react';
import { EOS_SEARCH } from 'eos-icons-react';
import Button from '@common/Button';
import Input from '@common/Input';
import Filter from '@common/Filter';
import DateFilter from '@common/DateFilter';

const renderFilter = (key, { type, ...filterProps }, value, onChange) => {
switch (type) {
case 'search_box':
nelsonkopliku marked this conversation as resolved.
Show resolved Hide resolved
return (
<Input
key={key}
{...filterProps}
value={value}
onChange={(e) => onChange(e.target.value)}
prefix={<EOS_SEARCH size="l" />}
/>
);
case 'select':
return (
<Filter key={key} {...filterProps} value={value} onChange={onChange} />
Expand Down
20 changes: 20 additions & 0 deletions assets/js/common/ComposedFilter/ComposedFilter.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,23 @@ export const WithDateFilter = {
onChange: action('onChange'),
},
};

export const WithSearchBox = {
args: {
filters: [
{
key: 'filter1',
type: 'select',
title: 'Pasta',
options: ['Carbonara', 'Amatriciana', 'Ajo & Ojo', 'Gricia'],
},
{
key: 'filter2',
type: 'search_box',
title: 'Search Query',
placeholder: 'Search...',
},
],
onChange: action('onChange'),
},
};
85 changes: 81 additions & 4 deletions assets/js/common/ComposedFilter/ComposedFilter.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { act, useState } from 'react';
import { render, screen } from '@testing-library/react';
import React, { useState } from 'react';
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import ComposedFilter from '.';
Expand All @@ -20,12 +20,19 @@ describe('ComposedFilter component', () => {
title: 'Pizza',
options: ['Margherita', 'Marinara', 'Diavola', 'Bufalina'],
},
{
key: 'filter3',
type: 'search_box',
title: 'Frutta',
placeholder: 'Filter by frutta',
},
];

render(<ComposedFilter filters={filters} />);

expect(screen.getByText('Filter Pasta...')).toBeInTheDocument();
expect(screen.getByText('Filter Pizza...')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Filter by frutta')).toBeInTheDocument();
});

it('should select value', async () => {
Expand All @@ -42,17 +49,24 @@ describe('ComposedFilter component', () => {
title: 'Pizza',
options: ['Margherita', 'Marinara', 'Diavola', 'Bufalina'],
},
{
key: 'filter3',
type: 'search_box',
title: 'Frutta',
},
];

const value = {
filter1: ['Carbonara', 'Gricia'],
filter2: ['Diavola'],
filter3: 'Papaya',
};

render(<ComposedFilter filters={filters} value={value} />);

expect(screen.getByText('Carbonara, Gricia')).toBeInTheDocument();
expect(screen.getByText('Diavola')).toBeInTheDocument();
expect(screen.getByDisplayValue('Papaya')).toBeInTheDocument();
});

it('should change selected value', async () => {
Expand All @@ -69,11 +83,18 @@ describe('ComposedFilter component', () => {
title: 'Pizza',
options: ['Margherita', 'Marinara', 'Diavola', 'Bufalina'],
},
{
key: 'filter3',
type: 'search_box',
title: 'Frutta',
placeholder: 'Filter by frutta',
},
];

const initialValue = {
filter1: ['Carbonara', 'Gricia'],
filter2: ['Diavola'],
filter3: 'Banana',
};

const nextValue = {};
Expand All @@ -100,11 +121,13 @@ describe('ComposedFilter component', () => {

expect(screen.getByText('Carbonara, Gricia')).toBeInTheDocument();
expect(screen.getByText('Diavola')).toBeInTheDocument();
expect(screen.getByDisplayValue('Banana')).toBeInTheDocument();

await act(() => userEvent.click(screen.getByText('Click me')));

expect(screen.getByText('Filter Pasta...')).toBeInTheDocument();
expect(screen.getByText('Filter Pizza...')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Filter by frutta')).toBeInTheDocument();
});

it('should call onChange once for each selection when a filter is changed and it is applied automatically', async () => {
Expand All @@ -122,6 +145,12 @@ describe('ComposedFilter component', () => {
title: 'Pizza',
options: ['Margherita', 'Marinara', 'Diavola', 'Bufalina'],
},
{
key: 'filter3',
type: 'search_box',
title: 'Frutta',
placeholder: 'Filter by frutta',
},
];

render(
Expand All @@ -139,7 +168,15 @@ describe('ComposedFilter component', () => {
await act(() => userEvent.click(screen.getByText('Diavola')));
await act(() => userEvent.click(screen.getAllByText('Diavola')[0]));

expect(mockOnChange).toHaveBeenCalledTimes(3);
// type a query in the search box
await act(() =>
userEvent.type(
screen.getByPlaceholderText('Filter by frutta'),
'Dragonfruit'
)
);

expect(mockOnChange).toHaveBeenCalledTimes(14);
expect(mockOnChange).toHaveBeenNthCalledWith(1, { filter1: ['Carbonara'] });
expect(mockOnChange).toHaveBeenNthCalledWith(2, {
filter1: ['Carbonara', 'Gricia'],
Expand All @@ -148,9 +185,19 @@ describe('ComposedFilter component', () => {
filter1: ['Carbonara', 'Gricia'],
filter2: ['Diavola'],
});
expect(mockOnChange).toHaveBeenNthCalledWith(10, {
filter1: ['Carbonara', 'Gricia'],
filter2: ['Diavola'],
filter3: 'Dragonf',
});
expect(mockOnChange).toHaveBeenNthCalledWith(14, {
filter1: ['Carbonara', 'Gricia'],
filter2: ['Diavola'],
filter3: 'Dragonfruit',
});
});

it('should call onChange when a filter is changed and it is applied automatically', async () => {
it('should call onChange when a filter is changed and it is explicitly applied', async () => {
const mockOnChange = jest.fn();
const filters = [
{
Expand All @@ -165,6 +212,12 @@ describe('ComposedFilter component', () => {
title: 'Pizza',
options: ['Margherita', 'Marinara', 'Diavola', 'Bufalina'],
},
{
key: 'filter3',
type: 'search_box',
title: 'Frutta',
placeholder: 'Filter by frutta',
},
];

render(<ComposedFilter filters={filters} onChange={mockOnChange} />);
Expand All @@ -180,6 +233,14 @@ describe('ComposedFilter component', () => {
await act(() => userEvent.click(screen.getByText('Diavola')));
await act(() => userEvent.click(screen.getAllByText('Diavola')[0]));

// type a query in the search box
await act(() =>
userEvent.type(
screen.getByPlaceholderText('Filter by frutta'),
'Dragonfruit'
)
);

// not applied yet
expect(mockOnChange).not.toHaveBeenCalled();

Expand All @@ -191,6 +252,7 @@ describe('ComposedFilter component', () => {
expect(mockOnChange).toHaveBeenCalledWith({
filter1: ['Carbonara', 'Gricia'],
filter2: ['Diavola'],
filter3: 'Dragonfruit',
});
});

Expand All @@ -209,6 +271,12 @@ describe('ComposedFilter component', () => {
title: 'Pizza',
options: ['Margherita', 'Marinara', 'Diavola', 'Bufalina'],
},
{
key: 'filter3',
type: 'search_box',
title: 'Frutta',
placeholder: 'Filter by frutta',
},
];

render(<ComposedFilter filters={filters} onChange={mockOnChange} />);
Expand All @@ -224,6 +292,14 @@ describe('ComposedFilter component', () => {
await act(() => userEvent.click(screen.getByText('Diavola')));
await act(() => userEvent.click(screen.getAllByText('Diavola')[0]));

// type a query in the search box
await act(() =>
userEvent.type(
screen.getByPlaceholderText('Filter by frutta'),
'Dragonfruit'
)
);

await act(() => userEvent.click(screen.getByText('Reset')));

expect(mockOnChange).toHaveBeenCalledTimes(1);
Expand All @@ -232,5 +308,6 @@ describe('ComposedFilter component', () => {
// filters are reset
expect(screen.getByText('Filter Pasta...')).toBeInTheDocument();
expect(screen.getByText('Filter Pizza...')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Filter by frutta')).toBeInTheDocument();
});
});
64 changes: 33 additions & 31 deletions assets/js/common/Input/Input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,39 @@ function Input({
const clearIcon = <EOS_CANCEL_OUTLINED className="inline" size="l" />;

return (
<RcInput
className={classNames(
'rounded-md w-full block relative placeholder-gray-400 outline-none bg-white border disabled:bg-gray-50',
{
'has-prefix': hasPrefix,
'border-gray-200': !error,
'focus:border-gray-500': !error,
'focus-visible:border-red-500': error,
'border-red-500': error,
},
className
)}
id={id}
name={name}
type={type}
value={value}
defaultValue={initialValue}
placeholder={placeholder}
disabled={disabled}
suffix={disabled && allowClear ? clearIcon : suffix}
prefix={prefix}
allowClear={
allowClear && !disabled
? {
clearIcon,
}
: false
}
onChange={onChange}
{...props}
/>
<div className={className}>
<RcInput
className={classNames(
'rounded-md w-full block relative placeholder-gray-400 outline-none bg-white border disabled:bg-gray-50',
{
'has-prefix': hasPrefix,
'border-gray-200': !error,
'focus:border-gray-500': !error,
'focus-visible:border-red-500': error,
'border-red-500': error,
},
className
)}
id={id}
name={name}
type={type}
value={value}
defaultValue={initialValue}
placeholder={placeholder}
disabled={disabled}
suffix={disabled && allowClear ? clearIcon : suffix}
prefix={prefix}
allowClear={
allowClear && !disabled
? {
clearIcon,
}
: false
}
onChange={onChange}
{...props}
/>
</div>
);
}

Expand Down
2 changes: 1 addition & 1 deletion assets/js/lib/test-utils/factories/activityLog.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const activityLogEntryFactory = Factory.define(({ params }) => {

return {
id: faker.string.uuid(),
actor: faker.internet.userName(),
actor: faker.internet.username(),
type: activityType,
occurred_on: faker.date.anytime(),
metadata,
Expand Down
2 changes: 1 addition & 1 deletion assets/js/lib/test-utils/factories/advisoryErrata.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const advisoryErrataFactory = Factory.define(({ params }) => ({
advisory_status: 'stable',
vendor_advisory: faker.lorem.word(),
type: faker.helpers.arrayElement(advisoryType),
product: faker.internet.userName(),
product: faker.internet.username(),
errata_from: faker.lorem.word(),
topic: faker.animal.cat(),
description: faker.lorem.sentence(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { Factory } from 'fishery';

export const softwareUpdatesSettingsFactory = Factory.define(() => ({
url: faker.internet.url(),
username: faker.internet.userName(),
username: faker.internet.username(),
ca_uploaded_at: formatISO(faker.date.recent()),
}));
6 changes: 3 additions & 3 deletions assets/js/lib/test-utils/factories/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const abilityFactory = Factory.define(() => ({

export const userFactory = Factory.define(() => ({
id: faker.number.int(),
username: faker.internet.userName(),
username: faker.internet.username(),
enabled: faker.datatype.boolean(),
fullname: faker.internet.displayName(),
email: faker.internet.email(),
Expand All @@ -24,7 +24,7 @@ export const userFactory = Factory.define(() => ({

export const profileFactory = Factory.define(() => ({
id: faker.number.int(),
username: faker.internet.userName(),
username: faker.internet.username(),
fullname: faker.internet.displayName(),
email: faker.internet.email(),
abilities: abilityFactory.buildList(2),
Expand All @@ -47,7 +47,7 @@ export const createUserRequestFactory = Factory.define(() => {
const password = 'password';

return {
username: faker.internet.userName().toLowerCase(),
username: faker.internet.username().toLowerCase(),
enabled: true,
fullname: faker.internet.displayName(),
email: faker.internet.email(),
Expand Down
9 changes: 9 additions & 0 deletions assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ function ActivityLogPage() {
const { abilities } = useSelector(getUserProfile);

const filters = [
{
key: 'search',
title: 'Search',
type: 'search_box',
name: 'metadata-search',
placeholder: 'Filter by metadata',
allowClear: true,
className: 'col-span-8',
},
{
key: 'type',
type: 'select',
Expand Down
Loading
Loading