Support RegEx search

Summary:
Changelog: Restored the possibility to use Regex in logs search

Fixes:
https://github.com/facebook/flipper/issues/2076
https://fb.workplace.com/groups/flipperfyi/permalink/912753022824327/

Reviewed By: passy

Differential Revision: D27188241

fbshipit-source-id: 38ae2972c7dd3dd5cf24df87535d5ad74598cd88
This commit is contained in:
Michel Weststrate
2021-03-19 08:57:24 -07:00
committed by Facebook GitHub Bot
parent 2ae7d13a64
commit c648c58825
4 changed files with 133 additions and 22 deletions

View File

@@ -249,19 +249,29 @@ export function DataTable<T extends object>(
// we don't want to trigger filter changes too quickly, as they can be pretty expensive
// and would block the user from entering text in the search bar for example
// (and in the future would really benefit from concurrent mode here :))
const setFilter = (search: string, columns: DataTableColumn<T>[]) => {
dataSource.view.setFilter(computeDataTableFilter(search, columns));
const setFilter = (
search: string,
useRegex: boolean,
columns: DataTableColumn<T>[],
) => {
dataSource.view.setFilter(
computeDataTableFilter(search, useRegex, columns),
);
};
return props._testHeight ? setFilter : debounce(setFilter, 250);
});
useEffect(
function updateFilter() {
debouncedSetFilter(tableState.searchValue, tableState.columns);
debouncedSetFilter(
tableState.searchValue,
tableState.useRegex,
tableState.columns,
);
},
// Important dep optimization: we don't want to recalc filters if just the width or visibility changes!
// We pass entire state.columns to computeDataTableFilter, but only changes in the filter are a valid cause to compute a new filter function
// eslint-disable-next-line
[tableState.searchValue, ...tableState.columns.map((c) => c.filters)],
[tableState.searchValue, tableState.useRegex, ...tableState.columns.map((c) => c.filters)],
);
useEffect(
@@ -367,6 +377,7 @@ export function DataTable<T extends object>(
<Layout.Container>
<TableSearch
searchValue={searchValue}
useRegex={tableState.useRegex}
dispatch={dispatch as any}
contextMenu={contexMenu}
extraActions={props.extraActions}

View File

@@ -32,6 +32,7 @@ const emptySelection: Selection = {
type PersistedState = {
/** Active search value */
search: string;
useRegex: boolean;
/** current selection, describes the index index in the datasources's current output (not window!) */
selection: {current: number; items: number[]};
/** The currently applicable sorting, if any */
@@ -77,7 +78,8 @@ type DataManagerActions<T> =
| Action<'removeColumnFilter', {column: keyof T; index: number}>
| Action<'toggleColumnFilter', {column: keyof T; index: number}>
| Action<'setColumnFilterFromSelection', {column: keyof T}>
| Action<'appliedInitialScroll'>;
| Action<'appliedInitialScroll'>
| Action<'toggleUseRegex'>;
type DataManagerConfig<T> = {
dataSource: DataSource<T>;
@@ -96,6 +98,7 @@ type DataManagerState<T> = {
sorting: Sorting<T> | undefined;
selection: Selection;
searchValue: string;
useRegex: boolean;
};
export type DataTableReducer<T> = Reducer<
@@ -142,6 +145,10 @@ export const dataTableManagerReducer = produce(function <T>(
draft.searchValue = action.value;
break;
}
case 'toggleUseRegex': {
draft.useRegex = !draft.useRegex;
break;
}
case 'selectItem': {
const {nextIndex, addToSelection} = action;
draft.selection = computeSetSelection(
@@ -301,6 +308,7 @@ export function createInitialState<T>(
}
: emptySelection,
searchValue: prefs?.search ?? '',
useRegex: prefs?.useRegex ?? false,
};
// @ts-ignore
res.config[immerable] = false; // optimization: never proxy anything in config
@@ -363,6 +371,7 @@ export function savePreferences(
}
const prefs: PersistedState = {
search: state.searchValue,
useRegex: state.useRegex,
selection: {
current: state.selection.current,
items: Array.from(state.selection.items),
@@ -411,9 +420,11 @@ function computeInitialColumns(
export function computeDataTableFilter(
searchValue: string,
useRegex: boolean,
columns: DataTableColumn[],
) {
const searchString = searchValue.toLowerCase();
const searchRegex = useRegex ? safeCreateRegExp(searchValue) : undefined;
// the columns with an active filter are those that have filters defined,
// with at least one enabled
const filteringColumns = columns.filter((c) =>
@@ -438,11 +449,21 @@ export function computeDataTableFilter(
}
}
return Object.values(item).some((v) =>
String(v).toLowerCase().includes(searchString),
searchRegex
? searchRegex.test(String(v))
: String(v).toLowerCase().includes(searchString),
);
};
}
export function safeCreateRegExp(source: string): RegExp | undefined {
try {
return new RegExp(source);
} catch (_e) {
return undefined;
}
}
export function computeSetSelection(
base: Selection,
nextIndex: number | ((currentIndex: number) => number),

View File

@@ -9,7 +9,7 @@
import {MenuOutlined} from '@ant-design/icons';
import {Button, Dropdown, Input} from 'antd';
import React, {memo, useCallback} from 'react';
import React, {memo, useCallback, useMemo} from 'react';
import styled from '@emotion/styled';
import {Layout} from '../Layout';
@@ -18,11 +18,13 @@ import type {DataTableDispatch} from './DataTableManager';
export const TableSearch = memo(function TableSearch({
searchValue,
useRegex,
dispatch,
extraActions,
contextMenu,
}: {
searchValue: string;
useRegex: boolean;
dispatch: DataTableDispatch<any>;
extraActions?: React.ReactElement;
contextMenu: undefined | (() => JSX.Element);
@@ -33,6 +35,25 @@ export const TableSearch = memo(function TableSearch({
},
[dispatch],
);
const onToggleRegex = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
dispatch({type: 'toggleUseRegex'});
},
[dispatch],
);
const regexError = useMemo(() => {
if (!useRegex || !searchValue) {
return;
}
try {
new RegExp(searchValue);
} catch (e) {
return '' + e;
}
}, [useRegex, searchValue]);
return (
<Searchbar gap>
<Input.Search
@@ -40,6 +61,27 @@ export const TableSearch = memo(function TableSearch({
placeholder="Search..."
onSearch={onSearch}
value={searchValue}
suffix={
<RegexButton
size="small"
onClick={onToggleRegex}
style={
useRegex
? {
background: regexError
? theme.errorColor
: theme.successColor,
color: theme.white,
}
: {
color: theme.disabledColor,
}
}
type="default"
title={regexError || 'Search using Regex'}>
.*
</RegexButton>
}
onChange={(e) => {
onSearch(e.target.value);
}}
@@ -59,8 +101,25 @@ export const TableSearch = memo(function TableSearch({
const Searchbar = styled(Layout.Horizontal)({
backgroundColor: theme.backgroundWash,
padding: theme.space.small,
'.ant-input-affix-wrapper': {
height: 32,
},
'.ant-btn': {
padding: `${theme.space.tiny}px ${theme.space.small}px`,
background: 'transparent',
},
});
const RegexButton = styled(Button)({
padding: '0px !important',
borderRadius: 4,
marginRight: -6,
marginLeft: 4,
lineHeight: '20px',
width: 20,
height: 20,
border: 'none',
'& :hover': {
color: theme.primaryColor,
},
});

View File

@@ -307,9 +307,9 @@ test('compute filters', () => {
const data = [coffee, espresso, meet];
// results in empty filter
expect(computeDataTableFilter('', [])).toBeUndefined();
expect(computeDataTableFilter('', false, [])).toBeUndefined();
expect(
computeDataTableFilter('', [
computeDataTableFilter('', false, [
{
key: 'title',
filters: [
@@ -324,32 +324,52 @@ test('compute filters', () => {
).toBeUndefined();
{
const filter = computeDataTableFilter('tEsT', [])!;
const filter = computeDataTableFilter('tEsT', false, [])!;
expect(data.filter(filter)).toEqual([]);
}
{
const filter = computeDataTableFilter('EE', [])!;
const filter = computeDataTableFilter('EE', false, [])!;
expect(data.filter(filter)).toEqual([coffee, meet]);
}
{
const filter = computeDataTableFilter('D', [])!;
const filter = computeDataTableFilter('D', false, [])!;
expect(data.filter(filter)).toEqual([coffee]);
}
{
const filter = computeDataTableFilter('true', [])!;
// regex, positive (mind the double escaping of \\b)
const filter = computeDataTableFilter('..t', true, [])!;
expect(data.filter(filter)).toEqual([meet]);
}
{
// regex, words with 6 chars
const filter = computeDataTableFilter('\\w{6}', true, [])!;
expect(data.filter(filter)).toEqual([coffee, espresso]);
}
{
// no match
const filter = computeDataTableFilter('\\w{18}', true, [])!;
expect(data.filter(filter)).toEqual([]);
}
{
// invalid regex
const filter = computeDataTableFilter('bla/[', true, [])!;
expect(data.filter(filter)).toEqual([]);
}
{
const filter = computeDataTableFilter('true', false, [])!;
expect(data.filter(filter)).toEqual([coffee]);
}
{
const filter = computeDataTableFilter('false', [])!;
const filter = computeDataTableFilter('false', false, [])!;
expect(data.filter(filter)).toEqual([espresso, meet]);
}
{
const filter = computeDataTableFilter('EE', [
const filter = computeDataTableFilter('EE', false, [
{
key: 'level',
filters: [
@@ -364,7 +384,7 @@ test('compute filters', () => {
expect(data.filter(filter)).toEqual([meet]);
}
{
const filter = computeDataTableFilter('EE', [
const filter = computeDataTableFilter('EE', false, [
{
key: 'level',
filters: [
@@ -384,7 +404,7 @@ test('compute filters', () => {
expect(data.filter(filter)).toEqual([coffee, meet]);
}
{
const filter = computeDataTableFilter('', [
const filter = computeDataTableFilter('', false, [
{
key: 'level',
filters: [
@@ -404,7 +424,7 @@ test('compute filters', () => {
expect(data.filter(filter)).toEqual([coffee, espresso]);
}
{
const filter = computeDataTableFilter('', [
const filter = computeDataTableFilter('', false, [
{
key: 'done',
filters: [
@@ -420,7 +440,7 @@ test('compute filters', () => {
}
{
// nothing selected anything will not filter anything out for that column
const filter = computeDataTableFilter('', [
const filter = computeDataTableFilter('', false, [
{
key: 'level',
filters: [
@@ -440,7 +460,7 @@ test('compute filters', () => {
expect(filter).toBeUndefined();
}
{
const filter = computeDataTableFilter('', [
const filter = computeDataTableFilter('', false, [
{
key: 'level',
filters: [
@@ -460,7 +480,7 @@ test('compute filters', () => {
expect(data.filter(filter)).toEqual([coffee, espresso, meet]);
}
{
const filter = computeDataTableFilter('', [
const filter = computeDataTableFilter('', false, [
{
key: 'level',
filters: [
@@ -485,7 +505,7 @@ test('compute filters', () => {
expect(data.filter(filter)).toEqual([espresso]);
}
{
const filter = computeDataTableFilter('nonsense', [
const filter = computeDataTableFilter('nonsense', false, [
{
key: 'level',
filters: [