Skip to content

Commit

Permalink
[TableListView] Enhance tag filtering (#142108)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebelga authored Nov 14, 2022
1 parent b72a9a3 commit a67776b
Show file tree
Hide file tree
Showing 41 changed files with 1,319 additions and 211 deletions.
8 changes: 5 additions & 3 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@
/docs/settings/reporting-settings.asciidoc @elastic/kibana-global-experience
/docs/setup/configuring-reporting.asciidoc @elastic/kibana-global-experience

### Global Experience Tagging
/src/plugins/saved_objects_tagging_oss @elastic/kibana-global-experience
/x-pack/plugins/saved_objects_tagging/ @elastic/kibana-global-experience
/x-pack/test/saved_object_tagging/ @elastic/kibana-global-experience

### Kibana React (to be deprecated)
/src/plugins/kibana_react/ @elastic/kibana-global-experience
/src/plugins/kibana_react/public/code_editor @elastic/kibana-global-experience @elastic/kibana-presentation
Expand Down Expand Up @@ -302,7 +307,6 @@
# Core
/examples/hello_world/ @elastic/kibana-core
/src/core/ @elastic/kibana-core
/src/plugins/saved_objects_tagging_oss @elastic/kibana-core
/config/kibana.yml @elastic/kibana-core
/typings/ @elastic/kibana-core
/x-pack/plugins/global_search_providers @elastic/kibana-core
Expand All @@ -312,9 +316,7 @@
/x-pack/plugins/global_search/ @elastic/kibana-core
/x-pack/plugins/cloud/ @elastic/kibana-core
/x-pack/plugins/cloud_integrations/ @elastic/kibana-core
/x-pack/plugins/saved_objects_tagging/ @elastic/kibana-core
/x-pack/test/saved_objects_field_count/ @elastic/kibana-core
/x-pack/test/saved_object_tagging/ @elastic/kibana-core
/src/plugins/saved_objects_management/ @elastic/kibana-core
/src/plugins/advanced_settings/ @elastic/kibana-core
/x-pack/plugins/global_search_bar/ @elastic/kibana-core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export const getMockServices = (overrides?: Partial<Services>) => {
currentAppId$: from('mockedApp'),
navigateToUrl: () => undefined,
TagList,
getTagList: () => [],
itemHasTags: () => true,
getTagManagementUrl: () => '',
getTagIdsFromReferences: () => [],
...overrides,
};
Expand Down
7 changes: 5 additions & 2 deletions packages/content-management/table_list/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { IHttpFetchError } from '@kbn/core-http-browser';
import type { CriteriaWithPagination, Direction } from '@elastic/eui';
import type { CriteriaWithPagination, Direction, Query } from '@elastic/eui';

import type { SortColumnField } from './components';

Expand Down Expand Up @@ -71,7 +71,10 @@ export interface ShowConfirmDeleteItemsModalAction {
/** Action to update the search bar query text */
export interface OnSearchQueryChangeAction {
type: 'onSearchQueryChange';
data: string;
data: {
query: Query;
text: string;
};
}

export type Action<T> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export { ConfirmDeleteModal } from './confirm_delete_modal';
export { ListingLimitWarning } from './listing_limit_warning';
export { ItemDetails } from './item_details';
export { TableSortSelect } from './table_sort_select';
export { TagFilterPanel } from './tag_filter_panel';

export type { SortColumnField } from './table_sort_select';
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
*/

import React, { useCallback, useMemo } from 'react';
import { EuiText, EuiLink, EuiTitle, EuiSpacer } from '@elastic/eui';
import { EuiText, EuiLink, EuiTitle, EuiSpacer, EuiHighlight } from '@elastic/eui';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';

import type { Tag } from '../types';
import { useServices } from '../services';
import type { UserContentCommonSchema, Props as TableListViewProps } from '../table_list_view';
import { TagBadge } from './tag_badge';

type InheritedProps<T extends UserContentCommonSchema> = Pick<
TableListViewProps<T>,
Expand All @@ -20,21 +22,23 @@ type InheritedProps<T extends UserContentCommonSchema> = Pick<
interface Props<T extends UserContentCommonSchema> extends InheritedProps<T> {
item: T;
searchTerm?: string;
onClickTag: (tag: Tag, isCtrlKey: boolean) => void;
}

/**
* Copied from https://stackoverflow.com/a/9310752
*/
// const escapeRegExp = (text: string) => {
// return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
// };
const escapeRegExp = (text: string) => {
return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
};

export function ItemDetails<T extends UserContentCommonSchema>({
id,
item,
searchTerm = '',
getDetailViewLink,
onClickTitle,
onClickTag,
}: Props<T>) {
const {
references,
Expand Down Expand Up @@ -79,7 +83,9 @@ export function ItemDetails<T extends UserContentCommonSchema>({
onClick={onClickTitleHandler}
data-test-subj={`${id}ListingTitleLink-${item.attributes.title.split(' ').join('-')}`}
>
{title}
<EuiHighlight highlightAll search={escapeRegExp(searchTerm)}>
{title}
</EuiHighlight>
</EuiLink>
</RedirectAppLinks>
);
Expand All @@ -90,6 +96,7 @@ export function ItemDetails<T extends UserContentCommonSchema>({
onClickTitle,
onClickTitleHandler,
redirectAppLinksCoreStart,
searchTerm,
title,
]);

Expand All @@ -100,13 +107,20 @@ export function ItemDetails<T extends UserContentCommonSchema>({
<EuiTitle size="xs">{renderTitle()}</EuiTitle>
{Boolean(description) && (
<EuiText size="s">
<p>{description!}</p>
<p>
<EuiHighlight highlightAll search={escapeRegExp(searchTerm)}>
{description!}
</EuiHighlight>
</p>
</EuiText>
)}
{hasTags && (
<>
<EuiSpacer size="s" />
<TagList references={references} />
<TagList
references={references}
tagRender={(tag) => <TagBadge key={tag.name} tag={tag} onClick={onClickTag} />}
/>
</>
)}
</div>
Expand Down
97 changes: 85 additions & 12 deletions packages/content-management/table_list/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
PropertySort,
SearchFilterConfig,
Direction,
Query,
Ast,
} from '@elastic/eui';

import { useServices } from '../services';
Expand All @@ -26,14 +28,22 @@ import type {
UserContentCommonSchema,
} from '../table_list_view';
import { TableSortSelect } from './table_sort_select';
import { TagFilterPanel } from './tag_filter_panel';
import { useTagFilterPanel } from './use_tag_filter_panel';
import type { Params as UseTagFilterPanelParams } from './use_tag_filter_panel';
import type { SortColumnField } from './table_sort_select';

type State<T extends UserContentCommonSchema> = Pick<
TableListViewState<T>,
'items' | 'selectedIds' | 'searchQuery' | 'tableSort' | 'pagination'
>;

interface Props<T extends UserContentCommonSchema> extends State<T> {
type TagManagementProps = Pick<
UseTagFilterPanelParams,
'addOrRemoveIncludeTagFilter' | 'addOrRemoveExcludeTagFilter' | 'tagsToTableItemMap'
>;

interface Props<T extends UserContentCommonSchema> extends State<T>, TagManagementProps {
dispatch: Dispatch<Action<T>>;
entityName: string;
entityNamePlural: string;
Expand All @@ -44,6 +54,7 @@ interface Props<T extends UserContentCommonSchema> extends State<T> {
deleteItems: TableListViewProps<T>['deleteItems'];
onSortChange: (column: SortColumnField, direction: Direction) => void;
onTableChange: (criteria: CriteriaWithPagination<T>) => void;
clearTagSelection: () => void;
}

export function Table<T extends UserContentCommonSchema>({
Expand All @@ -58,12 +69,16 @@ export function Table<T extends UserContentCommonSchema>({
hasUpdatedAtMetadata,
entityName,
entityNamePlural,
tagsToTableItemMap,
deleteItems,
tableCaption,
onTableChange,
onSortChange,
addOrRemoveExcludeTagFilter,
addOrRemoveIncludeTagFilter,
clearTagSelection,
}: Props<T>) {
const { getSearchBarFilters } = useServices();
const { getTagList } = useServices();

const renderToolsLeft = useCallback(() => {
if (!deleteItems || selectedIds.length === 0) {
Expand Down Expand Up @@ -97,8 +112,37 @@ export function Table<T extends UserContentCommonSchema>({
}
: undefined;

const searchFilters = useMemo(() => {
const tableSortSelectFilter: SearchFilterConfig = {
const {
isPopoverOpen,
isInUse,
closePopover,
onFilterButtonClick,
onSelectChange,
options,
totalActiveFilters,
} = useTagFilterPanel({
query: searchQuery.query,
getTagList,
tagsToTableItemMap,
addOrRemoveExcludeTagFilter,
addOrRemoveIncludeTagFilter,
});

const onSearchQueryChange = useCallback(
(arg: { query: Query | null; queryText: string }) => {
dispatch({
type: 'onSearchQueryChange',
data: {
query: arg.query ?? new Query(Ast.create([]), undefined, arg.queryText),
text: arg.queryText,
},
});
},
[dispatch]
);

const tableSortSelectFilter = useMemo<SearchFilterConfig>(() => {
return {
type: 'custom_component',
component: () => {
return (
Expand All @@ -110,25 +154,53 @@ export function Table<T extends UserContentCommonSchema>({
);
},
};
}, [hasUpdatedAtMetadata, onSortChange, tableSort]);

const tagFilterPanel = useMemo<SearchFilterConfig>(() => {
return {
type: 'custom_component',
component: () => {
return (
<TagFilterPanel
isPopoverOpen={isPopoverOpen}
isInUse={isInUse}
closePopover={closePopover}
options={options}
totalActiveFilters={totalActiveFilters}
onFilterButtonClick={onFilterButtonClick}
onSelectChange={onSelectChange}
clearTagSelection={clearTagSelection}
/>
);
},
};
}, [
isPopoverOpen,
isInUse,
closePopover,
options,
totalActiveFilters,
onFilterButtonClick,
onSelectChange,
clearTagSelection,
]);

return getSearchBarFilters
? [tableSortSelectFilter, ...getSearchBarFilters()]
: [tableSortSelectFilter];
}, [onSortChange, hasUpdatedAtMetadata, tableSort, getSearchBarFilters]);
const searchFilters = useMemo(() => {
return [tableSortSelectFilter, tagFilterPanel];
}, [tableSortSelectFilter, tagFilterPanel]);

const search = useMemo(() => {
return {
onChange: ({ queryText }: { queryText: string }) =>
dispatch({ type: 'onSearchQueryChange', data: queryText }),
onChange: onSearchQueryChange,
toolsLeft: renderToolsLeft(),
defaultQuery: searchQuery,
query: searchQuery.query ?? undefined,
box: {
incremental: true,
'data-test-subj': 'tableListSearchBox',
},
filters: searchFilters,
};
}, [dispatch, renderToolsLeft, searchFilters, searchQuery]);
}, [onSearchQueryChange, renderToolsLeft, searchFilters, searchQuery.query]);

const noItemsMessage = (
<FormattedMessage
Expand All @@ -148,6 +220,7 @@ export function Table<T extends UserContentCommonSchema>({
message={noItemsMessage}
selection={selection}
search={search}
executeQueryOptions={{ enabled: false }}
sorting={tableSort ? { sort: tableSort as PropertySort } : undefined}
onChange={onTableChange}
data-test-subj="itemsInMemTable"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { FC } from 'react';
import { EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import type { Tag } from '../types';

const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;

export interface Props {
tag: Tag;
onClick: (tag: Tag, withModifierKey: boolean) => void;
}

/**
* The badge representation of a Tag, which is the default display to be used for them.
*/
export const TagBadge: FC<Props> = ({ tag, onClick }) => {
return (
<EuiBadge
color={tag.color}
title={tag.description}
data-test-subj={`tag-${tag.id}`}
onClick={(e) => {
const withModifierKey = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
onClick(tag, withModifierKey);
}}
onClickAriaLabel={i18n.translate('contentManagement.tableList.tagBadge.buttonLabel', {
defaultMessage: '{tagName} tag button.',
values: {
tagName: tag.name,
},
})}
>
{tag.name}
</EuiBadge>
);
};
Loading

0 comments on commit a67776b

Please sign in to comment.