diff --git a/src/plugins/logs/index.js b/src/plugins/logs/index.js index a4c8a5882..05648b426 100644 --- a/src/plugins/logs/index.js +++ b/src/plugins/logs/index.js @@ -637,6 +637,7 @@ export default class LogTable extends FlipperDevicePlugin< defaultFilters={DEFAULT_FILTERS} zebra={false} actions={} + regexSupported={true} // If the logs is opened through deeplink, then don't scroll as the row is highlighted stickyBottom={ !(this.props.deepLinkPayload && this.state.highlightedRows.size > 0) diff --git a/src/ui/components/ToggleSwitch.js b/src/ui/components/ToggleSwitch.js index 3893f24e5..ee17963a5 100644 --- a/src/ui/components/ToggleSwitch.js +++ b/src/ui/components/ToggleSwitch.js @@ -8,6 +8,7 @@ import React from 'react'; import styled from '../styled/index.js'; import {colors} from './colors.js'; +import Text from './Text'; export const StyledButton = styled('div')(props => ({ cursor: 'pointer', @@ -31,6 +32,11 @@ export const StyledButton = styled('div')(props => ({ }, })); +const Label = styled(Text)({ + marginLeft: 7, + marginRight: 7, +}); + type Props = { /** * onClick handler. @@ -41,6 +47,7 @@ type Props = { */ toggled?: boolean, className?: string, + label?: string, }; /** @@ -56,11 +63,14 @@ type Props = { export default class ToggleButton extends React.Component { render() { return ( - + <> + + {this.props.label && } + ); } } diff --git a/src/ui/components/searchable/Searchable.js b/src/ui/components/searchable/Searchable.js index 555804384..73bfa4a4b 100644 --- a/src/ui/components/searchable/Searchable.js +++ b/src/ui/components/searchable/Searchable.js @@ -18,6 +18,7 @@ import Glyph from '../Glyph.js'; import FilterToken from './FilterToken.js'; import styled from '../../styled/index.js'; import debounce from 'lodash.debounce'; +import ToggleSwitch from '../ToggleSwitch'; const SearchBar = styled(Toolbar)({ height: 42, @@ -43,6 +44,7 @@ export const SearchInput = styled(Input)(props => ({ lineHeight: '100%', marginLeft: 2, width: '100%', + color: props.isValidInput ? colors.black : colors.red, '&::-webkit-input-placeholder': { color: colors.placeholder, fontWeight: 300, @@ -84,6 +86,8 @@ export type SearchableProps = {| addFilter: (filter: Filter) => void, searchTerm: string, filters: Array, + regexSupported?: boolean, + regexEnabled?: boolean, |}; type Props = {| @@ -93,6 +97,7 @@ type Props = {| columns?: TableColumns, onFilterChange: (filters: Array) => void, defaultFilters: Array, + regexSupported: boolean, |}; type State = { @@ -100,8 +105,18 @@ type State = { focusedToken: number, searchTerm: string, hasFocus: boolean, + regexEnabled: boolean, + compiledRegex: ?RegExp, }; +function compileRegex(s: string): ?RegExp { + try { + return new RegExp(s); + } catch (e) { + return null; + } +} + const Searchable = ( Component: React.ComponentType, ): React.ComponentType => @@ -115,6 +130,8 @@ const Searchable = ( focusedToken: -1, searchTerm: '', hasFocus: false, + regexEnabled: false, + compiledRegex: null, }; _inputRef: ?HTMLInputElement; @@ -155,9 +172,12 @@ const Searchable = ( } }); } + const searchTerm = savedState.searchTerm || this.state.searchTerm; this.setState({ - searchTerm: savedState.searchTerm || this.state.searchTerm, + searchTerm: searchTerm, filters: savedState.filters || this.state.filters, + regexEnabled: savedState.regexEnabled || this.state.regexEnabled, + compiledRegex: compileRegex(searchTerm), }); } } @@ -166,6 +186,7 @@ const Searchable = ( if ( this.getTableKey() && (prevState.searchTerm !== this.state.searchTerm || + prevState.regexEnabled != this.state.regexEnabled || prevState.filters !== this.state.filters) ) { window.localStorage.setItem( @@ -173,6 +194,7 @@ const Searchable = ( JSON.stringify({ searchTerm: this.state.searchTerm, filters: this.state.filters, + regexEnabled: this.state.regexEnabled, }), ); if (this.props.onFilterChange != null) { @@ -252,7 +274,10 @@ const Searchable = ( }; onChangeSearchTerm = (e: SyntheticInputEvent) => { - this.setState({searchTerm: e.target.value}); + this.setState({ + searchTerm: e.target.value, + compiledRegex: compileRegex(e.target.value), + }); this.matchTags(e.target.value, false); }; @@ -357,6 +382,13 @@ const Searchable = ( onTokenBlur = () => this.setState({focusedToken: -1}); + onRegexToggled = () => { + this.setState({ + regexEnabled: !this.state.regexEnabled, + compiledRegex: compileRegex(this.state.searchTerm), + }); + }; + hasFocus = (): boolean => { return this.state.focusedToken !== -1 || this.state.hasFocus; }; @@ -400,11 +432,23 @@ const Searchable = ( innerRef={this.setInputRef} onFocus={this.onInputFocus} onBlur={this.onInputBlur} + isValidInput={ + this.state.regexEnabled + ? this.state.compiledRegex !== null + : true + } /> - {(this.state.searchTerm || this.state.filters.length > 0) && ( - × - )} + {this.props.regexSupported ? ( + + ) : null} + {(this.state.searchTerm || this.state.filters.length > 0) && ( + × + )} {actions != null && {actions}} , , ]; diff --git a/src/ui/components/searchable/SearchableTable.js b/src/ui/components/searchable/SearchableTable.js index 1426b46f1..0dedeb668 100644 --- a/src/ui/components/searchable/SearchableTable.js +++ b/src/ui/components/searchable/SearchableTable.js @@ -27,9 +27,7 @@ type State = { filterRows: (row: TableBodyRow) => boolean, }; -const filterRowsFactory = (filters: Array, searchTerm: string) => ( - row: TableBodyRow, -): boolean => +const rowMatchesFilters = (filters: Array, row: TableBodyRow) => filters .map((filter: Filter) => { if (filter.type === 'enum' && row.type != null) { @@ -48,14 +46,43 @@ const filterRowsFactory = (filters: Array, searchTerm: string) => ( return true; } }) - .reduce((acc, cv) => acc && cv, true) && - (searchTerm != null && searchTerm.length > 0 - ? Object.keys(row.columns) - .map(key => textContent(row.columns[key].value)) - .join('~~') // prevent from matching text spanning multiple columns - .toLowerCase() - .includes(searchTerm.toLowerCase()) - : true); + .every(x => x === true); + +function rowMatchesRegex(values: Array, regex: string): boolean { + try { + const re = new RegExp(regex); + return values.some(x => re.test(x)); + } catch (e) { + return false; + } +} + +function rowMatchesSearchTerm( + searchTerm: string, + isRegex: boolean, + row: TableBodyRow, +): boolean { + if (searchTerm == null || searchTerm.length === 0) { + return true; + } + const rowValues = Object.keys(row.columns).map(key => + textContent(row.columns[key].value), + ); + if (isRegex) { + return rowMatchesRegex(rowValues, searchTerm); + } + return rowValues.some(x => + x.toLowerCase().includes(searchTerm.toLowerCase()), + ); +} + +const filterRowsFactory = ( + filters: Array, + searchTerm: string, + regexSearch: boolean, +) => (row: TableBodyRow): boolean => + rowMatchesFilters(filters, row) && + rowMatchesSearchTerm(searchTerm, regexSearch, row); class SearchableManagedTable extends PureComponent { static defaultProps = { @@ -63,7 +90,11 @@ class SearchableManagedTable extends PureComponent { }; state = { - filterRows: filterRowsFactory(this.props.filters, this.props.searchTerm), + filterRows: filterRowsFactory( + this.props.filters, + this.props.searchTerm, + this.props.regexEnabled || false, + ), }; componentDidMount() { @@ -73,10 +104,15 @@ class SearchableManagedTable extends PureComponent { componentWillReceiveProps(nextProps: Props) { if ( nextProps.searchTerm !== this.props.searchTerm || + nextProps.regexEnabled != this.props.regexEnabled || !deepEqual(this.props.filters, nextProps.filters) ) { this.setState({ - filterRows: filterRowsFactory(nextProps.filters, nextProps.searchTerm), + filterRows: filterRowsFactory( + nextProps.filters, + nextProps.searchTerm, + nextProps.regexEnabled || false, + ), }); } }