Skip to content

Commit

Permalink
feat(KaotoIOgh-115): Tag filtering in catalog
Browse files Browse the repository at this point in the history
* feat(KaotoIOgh-121) Wrap tags in tile component
  • Loading branch information
mkralik3 committed Sep 14, 2023
1 parent 2634f64 commit 2847995
Show file tree
Hide file tree
Showing 16 changed files with 384 additions and 186 deletions.
9 changes: 7 additions & 2 deletions packages/ui/src/components/Catalog/BaseCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface BaseCatalogProps {
tiles: ITile[];
catalogLayout: CatalogLayout;
onTileClick?: (tile: ITile) => void;
onTagClick: (_event: unknown, value: string) => void;
}

export const BaseCatalog: FunctionComponent<BaseCatalogProps> = (props) => {
Expand All @@ -34,12 +35,16 @@ export const BaseCatalog: FunctionComponent<BaseCatalogProps> = (props) => {
</Title>
{props.catalogLayout == CatalogLayout.List && (
<DataList aria-label="Catalog list" onSelectDataListItem={onSelectDataListItem} isCompact>
{props.tiles?.map((tile) => <CatalogDataListItem key={tile.name} tile={tile} />)}
{props.tiles?.map((tile) => (
<CatalogDataListItem key={tile.name} tile={tile} onTagClick={props.onTagClick} />
))}
</DataList>
)}
{props.catalogLayout == CatalogLayout.Gallery && (
<Gallery hasGutter>
{props.tiles?.map((tile) => <Tile key={tile.name} tile={tile} onClick={onTileClick} />)}
{props.tiles?.map((tile) => (
<Tile key={tile.name} tile={tile} onClick={onTileClick} onTagClick={props.onTagClick} />
))}
</Gallery>
)}
</div>
Expand Down
43 changes: 32 additions & 11 deletions packages/ui/src/components/Catalog/Catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,42 @@ interface CatalogProps {
onTileClick?: (tile: ITile) => void;
}

const checkThatArrayContainsAllTags = (arr: string[], tags: string[]) => tags.every((v) => arr.includes(v));

export const Catalog: FunctionComponent<PropsWithChildren<CatalogProps>> = (props) => {
const [searchTerm, setSearchTerm] = useState('');
const [groups, setGroups] = useState<string[]>([]);
const [activeGroup, setActiveGroup] = useState<string>(getFirstActiveGroup(props.tiles));
const [activeLayout, setActiveLayout] = useState(CatalogLayout.Gallery);
const [filteredTiles, setFilteredTiles] = useState<ITile[]>([]);
const [filterTags, setFilterTags] = useState<string[]>([]);

useEffect(() => {
setGroups(Object.keys(props.tiles));
setActiveGroup(getFirstActiveGroup(props.tiles));
}, [props.tiles]);

useEffect(() => {
setFilteredTiles(
props.tiles[activeGroup]?.filter((tile) => {
return (
tile.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tile.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
tile.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
tile.tags.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
}),
);
}, [searchTerm, activeGroup, props.tiles]);
let toBeFiltered: ITile[] = [];
// filter by selected tags
toBeFiltered = filterTags
? props.tiles[activeGroup]?.filter((tile) => {
return checkThatArrayContainsAllTags(tile.tags, filterTags);
})
: props.tiles[activeGroup];
// filter by search term ( name, description, tag )
toBeFiltered = searchTerm
? toBeFiltered?.filter((tile) => {
return (
tile.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tile.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
tile.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
tile.tags.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
})
: toBeFiltered;
setFilteredTiles(toBeFiltered);
}, [searchTerm, activeGroup, props.tiles, filterTags]);

const onFilterChange = useCallback((_event: unknown, value = '') => {
setSearchTerm(value);
Expand All @@ -45,6 +57,12 @@ export const Catalog: FunctionComponent<PropsWithChildren<CatalogProps>> = (prop
[props],
);

const onTagClick = useCallback((_event: unknown, value = '') => {
setFilterTags((previousFilteredTags) => {
return previousFilteredTags.includes(value) ? previousFilteredTags : [...previousFilteredTags, value];
});
}, []);

return (
<>
<CatalogFilter
Expand All @@ -54,15 +72,18 @@ export const Catalog: FunctionComponent<PropsWithChildren<CatalogProps>> = (prop
layouts={[CatalogLayout.Gallery, CatalogLayout.List]}
activeGroup={activeGroup}
activeLayout={activeLayout}
filterTags={filterTags}
onChange={onFilterChange}
setActiveGroup={setActiveGroup}
setActiveLayout={setActiveLayout}
setFilterTags={setFilterTags}
/>
<BaseCatalog
className="catalog__base"
tiles={filteredTiles}
catalogLayout={activeLayout}
onTileClick={onTileClick}
onTagClick={onTagClick}
/>
</>
);
Expand Down
25 changes: 24 additions & 1 deletion packages/ui/src/components/Catalog/CatalogFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Form, FormGroup, Grid, GridItem, SearchInput, ToggleGroup, ToggleGroupItem } from '@patternfly/react-core';
import {
Form,
FormGroup,
Grid,
GridItem,
Label,
LabelGroup,
SearchInput,
ToggleGroup,
ToggleGroupItem,
} from '@patternfly/react-core';
import { FunctionComponent, useEffect, useRef } from 'react';
import { CatalogLayout } from './Catalog.models';
import { CatalogLayoutIcon } from './CatalogLayoutIcon';
Expand All @@ -10,9 +20,11 @@ interface CatalogFilterProps {
layouts: CatalogLayout[];
activeGroup: string;
activeLayout: CatalogLayout;
filterTags: string[];
onChange: (event: unknown, value?: string) => void;
setActiveGroup: (group: string) => void;
setActiveLayout: (layout: CatalogLayout) => void;
setFilterTags: (tags: string[]) => void;
}

export const CatalogFilter: FunctionComponent<CatalogFilterProps> = (props) => {
Expand All @@ -22,6 +34,10 @@ export const CatalogFilter: FunctionComponent<CatalogFilterProps> = (props) => {
inputRef.current?.focus();
}, []);

const onClose = (tag: string) => {
props.setFilterTags(props.filterTags.filter((savedTag) => savedTag !== tag));
};

return (
<Form className={props.className}>
<Grid hasGutter>
Expand Down Expand Up @@ -76,6 +92,13 @@ export const CatalogFilter: FunctionComponent<CatalogFilterProps> = (props) => {
</FormGroup>
</GridItem>
</Grid>
<LabelGroup categoryName="Filtered tags" numLabels={10}>
{props.filterTags.map((tag, index) => (
<Label key={tag + index} id={tag + index} color="blue" onClose={() => onClose(tag)} isCompact>
{tag}
</Label>
))}
</LabelGroup>
</Form>
);
};
4 changes: 0 additions & 4 deletions packages/ui/src/components/Catalog/DataListItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@
flex-flow: wrap;
}

&__tags {
margin-right: 5px;
}

@media (min-width: 768px) {
&__title-div-right {
justify-content: right;
Expand Down
14 changes: 12 additions & 2 deletions packages/ui/src/components/Catalog/DataListItem.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { ITile } from './Catalog.models';
import { CatalogDataListItem } from './DataListItem';

Expand All @@ -15,8 +15,18 @@ describe('DataListItem', () => {
};

it('renders correctly', () => {
const { container } = render(<CatalogDataListItem key={tile.name} tile={tile} />);
const { container } = render(<CatalogDataListItem key={tile.name} tile={tile} onTagClick={jest.fn()} />);

expect(container.firstChild).toMatchSnapshot();
});

it('calls onTagClick prop when clicked', () => {
const onTagClick = jest.fn();

const { getByTestId } = render(<CatalogDataListItem key={tile.name} tile={tile} onTagClick={onTagClick} />);

fireEvent.click(getByTestId('tag-tag1'));

expect(onTagClick).toHaveBeenCalledTimes(1);
});
});
53 changes: 21 additions & 32 deletions packages/ui/src/components/Catalog/DataListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import {
DataListItemRow,
Grid,
GridItem,
Label,
LabelGroup,
} from '@patternfly/react-core';
import { FunctionComponent } from 'react';
import { IconResolver } from '../IconResolver';
import { ITile } from './Catalog.models';
import './DataListItem.scss';
import { getTagColor } from './tag-color-resolver';
import { CatalogTag, CatalogTagsPanel } from './Tags';

interface ICatalogDataListItemProps {
tile: ITile;
onTagClick: (_event: unknown, value: string) => void;
}

const titleElementOrder = {
Expand Down Expand Up @@ -50,40 +51,28 @@ export const CatalogDataListItem: FunctionComponent<ICatalogDataListItemProps> =
<span id="clickable-action-item1" className="catalog-data-list-item__title-div-left__title">
{props.tile.title}
</span>
{props.tile.headerTags?.map((tag, index) => (
<Label
key={`${props.tile.name}-${tag}-${index}`}
className="catalog-data-list-item__tags"
isCompact
color={getTagColor(tag)}
>
{tag}
</Label>
))}
{props.tile.version && (
<Label
key={`${props.tile.version}`}
isCompact
className={'catalog-data-list-item__tags'}
variant="outline"
>
{props.tile.version}
</Label>
)}
<LabelGroup isCompact aria-label="data-list-item-headers-tags">
{props.tile.headerTags?.map((tag, index) => (
<CatalogTag
key={`${props.tile.name}-${tag}-${index}`}
tag={tag}
className="catalog-data-list-item__tags"
/>
))}
{props.tile.version && (
<CatalogTag
key={`${props.tile.version}`}
tag={props.tile.version}
className="catalog-data-list-item__tags"
variant="outline"
/>
)}
</LabelGroup>
</div>
</GridItem>
<GridItem sm={12} md={6} order={tagsElementOrder}>
<div className="catalog-data-list-item__title-div-right">
{props.tile.tags?.map((tag, index) => (
<Label
key={`${props.tile.name}-${tag}-${index}`}
isCompact
className={'catalog-data-list-item__tags'}
color={getTagColor(tag)}
>
{tag}
</Label>
))}
<CatalogTagsPanel tags={props.tile.tags} onTagClick={props.onTagClick} />
</div>
</GridItem>
<GridItem span={12} order={descriptionElementOrder}>
Expand Down
24 changes: 24 additions & 0 deletions packages/ui/src/components/Catalog/Tags/CatalogTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Label } from '@patternfly/react-core';
import { FunctionComponent } from 'react';
import { getTagColor } from './tag-color-resolver';

interface ICatalogTagProps {
tag: string;
className?: string;
variant?: 'outline' | 'filled';
textMaxWidth?: string;
}

export const CatalogTag: FunctionComponent<ICatalogTagProps> = (props) => {
return (
<Label
textMaxWidth={props.textMaxWidth ?? undefined}
isCompact
className={props.className ?? ''}
variant={props.variant ?? 'filled'}
color={getTagColor(props.tag)}
>
{props.tag}
</Label>
);
};
33 changes: 33 additions & 0 deletions packages/ui/src/components/Catalog/Tags/CatalogTagsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Label, LabelGroup } from '@patternfly/react-core';
import { FunctionComponent } from 'react';
import { getTagColor } from './tag-color-resolver';

interface ICatalogTagsPanelProps {
tags: string[];
onTagClick: (_event: unknown, value: string) => void;
}

export const CatalogTagsPanel: FunctionComponent<ICatalogTagsPanelProps> = (props) => {
return (
<LabelGroup isCompact aria-label="data-list-item-tags">
{props.tags.map((tag) => (
<Label
isCompact
key={tag}
data-testid={'tag-' + tag}
color={getTagColor(tag)}
onClick={(ev) => {
ev.stopPropagation(); // ignore root click, e.g. click on tile
props.onTagClick(ev, tag);
}}
render={({ className, content }) => (
// to force PF to render label as button with animation
<a className={className}>{content}</a>
)}
>
{tag}
</Label>
))}
</LabelGroup>
);
};
3 changes: 3 additions & 0 deletions packages/ui/src/components/Catalog/Tags/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './tag-color-resolver';
export * from './CatalogTag';
export * from './CatalogTagsPanel';
8 changes: 3 additions & 5 deletions packages/ui/src/components/Catalog/Tile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@

&__title {
text-align: center;
display: flex;
flex-flow: column;
align-items: center;
}

&__body {
max-height: 150px;
overflow: hidden;
text-overflow: ellipsis;
}

&__tags {
margin-right: 1px;
margin-left: 1px;
}
}
12 changes: 8 additions & 4 deletions packages/ui/src/components/Catalog/Tile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ describe('Tile', () => {
};

it('renders correctly', () => {
const { container } = render(<Tile tile={tile} onClick={jest.fn()} />);
const { container } = render(<Tile tile={tile} onClick={jest.fn()} onTagClick={jest.fn()} />);

expect(container.firstChild).toMatchSnapshot();
});

it('calls onClick prop when clicked', () => {
it('calls onClick and onTagClick prop when clicked', () => {
const onClick = jest.fn();
const { getByRole } = render(<Tile tile={tile} onClick={onClick} />);
const onTagClick = jest.fn();

fireEvent.click(getByRole('button'));
const { getByTestId } = render(<Tile tile={tile} onClick={onClick} onTagClick={onTagClick} />);

fireEvent.click(getByTestId('tile-tile-name'));
fireEvent.click(getByTestId('tag-tag1'));

expect(onClick).toHaveBeenCalledTimes(1);
expect(onTagClick).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 2847995

Please sign in to comment.