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
|
// 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 would block the user from entering text in the search bar for example
|
||||||
// (and in the future would really benefit from concurrent mode here :))
|
// (and in the future would really benefit from concurrent mode here :))
|
||||||
const setFilter = (search: string, columns: DataTableColumn<T>[]) => {
|
const setFilter = (
|
||||||
dataSource.view.setFilter(computeDataTableFilter(search, columns));
|
search: string,
|
||||||
|
useRegex: boolean,
|
||||||
|
columns: DataTableColumn<T>[],
|
||||||
|
) => {
|
||||||
|
dataSource.view.setFilter(
|
||||||
|
computeDataTableFilter(search, useRegex, columns),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return props._testHeight ? setFilter : debounce(setFilter, 250);
|
return props._testHeight ? setFilter : debounce(setFilter, 250);
|
||||||
});
|
});
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateFilter() {
|
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!
|
// 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
|
// 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
|
// eslint-disable-next-line
|
||||||
[tableState.searchValue, ...tableState.columns.map((c) => c.filters)],
|
[tableState.searchValue, tableState.useRegex, ...tableState.columns.map((c) => c.filters)],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -367,6 +377,7 @@ export function DataTable<T extends object>(
|
|||||||
<Layout.Container>
|
<Layout.Container>
|
||||||
<TableSearch
|
<TableSearch
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
|
useRegex={tableState.useRegex}
|
||||||
dispatch={dispatch as any}
|
dispatch={dispatch as any}
|
||||||
contextMenu={contexMenu}
|
contextMenu={contexMenu}
|
||||||
extraActions={props.extraActions}
|
extraActions={props.extraActions}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const emptySelection: Selection = {
|
|||||||
type PersistedState = {
|
type PersistedState = {
|
||||||
/** Active search value */
|
/** Active search value */
|
||||||
search: string;
|
search: string;
|
||||||
|
useRegex: boolean;
|
||||||
/** current selection, describes the index index in the datasources's current output (not window!) */
|
/** current selection, describes the index index in the datasources's current output (not window!) */
|
||||||
selection: {current: number; items: number[]};
|
selection: {current: number; items: number[]};
|
||||||
/** The currently applicable sorting, if any */
|
/** The currently applicable sorting, if any */
|
||||||
@@ -77,7 +78,8 @@ type DataManagerActions<T> =
|
|||||||
| Action<'removeColumnFilter', {column: keyof T; index: number}>
|
| Action<'removeColumnFilter', {column: keyof T; index: number}>
|
||||||
| Action<'toggleColumnFilter', {column: keyof T; index: number}>
|
| Action<'toggleColumnFilter', {column: keyof T; index: number}>
|
||||||
| Action<'setColumnFilterFromSelection', {column: keyof T}>
|
| Action<'setColumnFilterFromSelection', {column: keyof T}>
|
||||||
| Action<'appliedInitialScroll'>;
|
| Action<'appliedInitialScroll'>
|
||||||
|
| Action<'toggleUseRegex'>;
|
||||||
|
|
||||||
type DataManagerConfig<T> = {
|
type DataManagerConfig<T> = {
|
||||||
dataSource: DataSource<T>;
|
dataSource: DataSource<T>;
|
||||||
@@ -96,6 +98,7 @@ type DataManagerState<T> = {
|
|||||||
sorting: Sorting<T> | undefined;
|
sorting: Sorting<T> | undefined;
|
||||||
selection: Selection;
|
selection: Selection;
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
|
useRegex: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableReducer<T> = Reducer<
|
export type DataTableReducer<T> = Reducer<
|
||||||
@@ -142,6 +145,10 @@ export const dataTableManagerReducer = produce(function <T>(
|
|||||||
draft.searchValue = action.value;
|
draft.searchValue = action.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'toggleUseRegex': {
|
||||||
|
draft.useRegex = !draft.useRegex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'selectItem': {
|
case 'selectItem': {
|
||||||
const {nextIndex, addToSelection} = action;
|
const {nextIndex, addToSelection} = action;
|
||||||
draft.selection = computeSetSelection(
|
draft.selection = computeSetSelection(
|
||||||
@@ -301,6 +308,7 @@ export function createInitialState<T>(
|
|||||||
}
|
}
|
||||||
: emptySelection,
|
: emptySelection,
|
||||||
searchValue: prefs?.search ?? '',
|
searchValue: prefs?.search ?? '',
|
||||||
|
useRegex: prefs?.useRegex ?? false,
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
res.config[immerable] = false; // optimization: never proxy anything in config
|
res.config[immerable] = false; // optimization: never proxy anything in config
|
||||||
@@ -363,6 +371,7 @@ export function savePreferences(
|
|||||||
}
|
}
|
||||||
const prefs: PersistedState = {
|
const prefs: PersistedState = {
|
||||||
search: state.searchValue,
|
search: state.searchValue,
|
||||||
|
useRegex: state.useRegex,
|
||||||
selection: {
|
selection: {
|
||||||
current: state.selection.current,
|
current: state.selection.current,
|
||||||
items: Array.from(state.selection.items),
|
items: Array.from(state.selection.items),
|
||||||
@@ -411,9 +420,11 @@ function computeInitialColumns(
|
|||||||
|
|
||||||
export function computeDataTableFilter(
|
export function computeDataTableFilter(
|
||||||
searchValue: string,
|
searchValue: string,
|
||||||
|
useRegex: boolean,
|
||||||
columns: DataTableColumn[],
|
columns: DataTableColumn[],
|
||||||
) {
|
) {
|
||||||
const searchString = searchValue.toLowerCase();
|
const searchString = searchValue.toLowerCase();
|
||||||
|
const searchRegex = useRegex ? safeCreateRegExp(searchValue) : undefined;
|
||||||
// the columns with an active filter are those that have filters defined,
|
// the columns with an active filter are those that have filters defined,
|
||||||
// with at least one enabled
|
// with at least one enabled
|
||||||
const filteringColumns = columns.filter((c) =>
|
const filteringColumns = columns.filter((c) =>
|
||||||
@@ -438,11 +449,21 @@ export function computeDataTableFilter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Object.values(item).some((v) =>
|
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(
|
export function computeSetSelection(
|
||||||
base: Selection,
|
base: Selection,
|
||||||
nextIndex: number | ((currentIndex: number) => number),
|
nextIndex: number | ((currentIndex: number) => number),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import {MenuOutlined} from '@ant-design/icons';
|
import {MenuOutlined} from '@ant-design/icons';
|
||||||
import {Button, Dropdown, Input} from 'antd';
|
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 styled from '@emotion/styled';
|
||||||
|
|
||||||
import {Layout} from '../Layout';
|
import {Layout} from '../Layout';
|
||||||
@@ -18,11 +18,13 @@ import type {DataTableDispatch} from './DataTableManager';
|
|||||||
|
|
||||||
export const TableSearch = memo(function TableSearch({
|
export const TableSearch = memo(function TableSearch({
|
||||||
searchValue,
|
searchValue,
|
||||||
|
useRegex,
|
||||||
dispatch,
|
dispatch,
|
||||||
extraActions,
|
extraActions,
|
||||||
contextMenu,
|
contextMenu,
|
||||||
}: {
|
}: {
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
|
useRegex: boolean;
|
||||||
dispatch: DataTableDispatch<any>;
|
dispatch: DataTableDispatch<any>;
|
||||||
extraActions?: React.ReactElement;
|
extraActions?: React.ReactElement;
|
||||||
contextMenu: undefined | (() => JSX.Element);
|
contextMenu: undefined | (() => JSX.Element);
|
||||||
@@ -33,6 +35,25 @@ export const TableSearch = memo(function TableSearch({
|
|||||||
},
|
},
|
||||||
[dispatch],
|
[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 (
|
return (
|
||||||
<Searchbar gap>
|
<Searchbar gap>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
@@ -40,6 +61,27 @@ export const TableSearch = memo(function TableSearch({
|
|||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
value={searchValue}
|
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) => {
|
onChange={(e) => {
|
||||||
onSearch(e.target.value);
|
onSearch(e.target.value);
|
||||||
}}
|
}}
|
||||||
@@ -59,8 +101,25 @@ export const TableSearch = memo(function TableSearch({
|
|||||||
const Searchbar = styled(Layout.Horizontal)({
|
const Searchbar = styled(Layout.Horizontal)({
|
||||||
backgroundColor: theme.backgroundWash,
|
backgroundColor: theme.backgroundWash,
|
||||||
padding: theme.space.small,
|
padding: theme.space.small,
|
||||||
|
'.ant-input-affix-wrapper': {
|
||||||
|
height: 32,
|
||||||
|
},
|
||||||
'.ant-btn': {
|
'.ant-btn': {
|
||||||
padding: `${theme.space.tiny}px ${theme.space.small}px`,
|
padding: `${theme.space.tiny}px ${theme.space.small}px`,
|
||||||
background: 'transparent',
|
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];
|
const data = [coffee, espresso, meet];
|
||||||
|
|
||||||
// results in empty filter
|
// results in empty filter
|
||||||
expect(computeDataTableFilter('', [])).toBeUndefined();
|
expect(computeDataTableFilter('', false, [])).toBeUndefined();
|
||||||
expect(
|
expect(
|
||||||
computeDataTableFilter('', [
|
computeDataTableFilter('', false, [
|
||||||
{
|
{
|
||||||
key: 'title',
|
key: 'title',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -324,32 +324,52 @@ test('compute filters', () => {
|
|||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
|
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('tEsT', [])!;
|
const filter = computeDataTableFilter('tEsT', false, [])!;
|
||||||
expect(data.filter(filter)).toEqual([]);
|
expect(data.filter(filter)).toEqual([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('EE', [])!;
|
const filter = computeDataTableFilter('EE', false, [])!;
|
||||||
expect(data.filter(filter)).toEqual([coffee, meet]);
|
expect(data.filter(filter)).toEqual([coffee, meet]);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('D', [])!;
|
const filter = computeDataTableFilter('D', false, [])!;
|
||||||
expect(data.filter(filter)).toEqual([coffee]);
|
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]);
|
expect(data.filter(filter)).toEqual([coffee]);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('false', [])!;
|
const filter = computeDataTableFilter('false', false, [])!;
|
||||||
expect(data.filter(filter)).toEqual([espresso, meet]);
|
expect(data.filter(filter)).toEqual([espresso, meet]);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('EE', [
|
const filter = computeDataTableFilter('EE', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -364,7 +384,7 @@ test('compute filters', () => {
|
|||||||
expect(data.filter(filter)).toEqual([meet]);
|
expect(data.filter(filter)).toEqual([meet]);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('EE', [
|
const filter = computeDataTableFilter('EE', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -384,7 +404,7 @@ test('compute filters', () => {
|
|||||||
expect(data.filter(filter)).toEqual([coffee, meet]);
|
expect(data.filter(filter)).toEqual([coffee, meet]);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('', [
|
const filter = computeDataTableFilter('', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -404,7 +424,7 @@ test('compute filters', () => {
|
|||||||
expect(data.filter(filter)).toEqual([coffee, espresso]);
|
expect(data.filter(filter)).toEqual([coffee, espresso]);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('', [
|
const filter = computeDataTableFilter('', false, [
|
||||||
{
|
{
|
||||||
key: 'done',
|
key: 'done',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -420,7 +440,7 @@ test('compute filters', () => {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
// nothing selected anything will not filter anything out for that column
|
// nothing selected anything will not filter anything out for that column
|
||||||
const filter = computeDataTableFilter('', [
|
const filter = computeDataTableFilter('', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -440,7 +460,7 @@ test('compute filters', () => {
|
|||||||
expect(filter).toBeUndefined();
|
expect(filter).toBeUndefined();
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('', [
|
const filter = computeDataTableFilter('', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -460,7 +480,7 @@ test('compute filters', () => {
|
|||||||
expect(data.filter(filter)).toEqual([coffee, espresso, meet]);
|
expect(data.filter(filter)).toEqual([coffee, espresso, meet]);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('', [
|
const filter = computeDataTableFilter('', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
@@ -485,7 +505,7 @@ test('compute filters', () => {
|
|||||||
expect(data.filter(filter)).toEqual([espresso]);
|
expect(data.filter(filter)).toEqual([espresso]);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const filter = computeDataTableFilter('nonsense', [
|
const filter = computeDataTableFilter('nonsense', false, [
|
||||||
{
|
{
|
||||||
key: 'level',
|
key: 'level',
|
||||||
filters: [
|
filters: [
|
||||||
|
|||||||
Reference in New Issue
Block a user