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:
committed by
Facebook GitHub Bot
parent
2ae7d13a64
commit
c648c58825
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user