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,
+ ),
});
}
}