Add regex search to logs plugin
Summary:
Adds regex support to the logs plugin.
You can now do the same for any other SearchableTable, just by adding the prop `regexSupported={true}`
For other types of searchables, like maybe the layout hierarchy, we'll need to modify the class that does the filtering for that type.
I've done what I can with the UI to make it usable, though it would be awesome to have some proper regex syntax parsing and highlighting going on. It will go red if it's not valid at least.
Reviewed By: danielbuechele
Differential Revision: D15898806
fbshipit-source-id: 425edb1834dcc14ca741ac7fc8d566b4f2763c63
This commit is contained in:
committed by
Facebook Github Bot
parent
5a6d978536
commit
b7229b40ac
@@ -637,6 +637,7 @@ export default class LogTable extends FlipperDevicePlugin<
|
||||
defaultFilters={DEFAULT_FILTERS}
|
||||
zebra={false}
|
||||
actions={<Button onClick={this.clearLogs}>Clear Logs</Button>}
|
||||
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)
|
||||
|
||||
@@ -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<Props> {
|
||||
render() {
|
||||
return (
|
||||
<StyledButton
|
||||
className={this.props.className}
|
||||
toggled={this.props.toggled}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
<>
|
||||
<StyledButton
|
||||
className={this.props.className}
|
||||
toggled={this.props.toggled}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
{this.props.label && <Label>{this.props.label}</Label>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Filter>,
|
||||
regexSupported?: boolean,
|
||||
regexEnabled?: boolean,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
@@ -93,6 +97,7 @@ type Props = {|
|
||||
columns?: TableColumns,
|
||||
onFilterChange: (filters: Array<Filter>) => void,
|
||||
defaultFilters: Array<Filter>,
|
||||
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<any>,
|
||||
): React.ComponentType<any> =>
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
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) && (
|
||||
<Clear onClick={this.clear}>×</Clear>
|
||||
)}
|
||||
</SearchBox>
|
||||
{this.props.regexSupported ? (
|
||||
<ToggleSwitch
|
||||
toggled={this.state.regexEnabled}
|
||||
onClick={this.onRegexToggled}
|
||||
label={'Regex'}
|
||||
/>
|
||||
) : null}
|
||||
{(this.state.searchTerm || this.state.filters.length > 0) && (
|
||||
<Clear onClick={this.clear}>×</Clear>
|
||||
)}
|
||||
{actions != null && <Actions>{actions}</Actions>}
|
||||
</SearchBar>,
|
||||
<Component
|
||||
@@ -412,6 +456,7 @@ const Searchable = (
|
||||
key="table"
|
||||
addFilter={this.addFilter}
|
||||
searchTerm={this.state.searchTerm}
|
||||
regexEnabled={this.state.regexEnabled}
|
||||
filters={this.state.filters}
|
||||
/>,
|
||||
];
|
||||
|
||||
@@ -27,9 +27,7 @@ type State = {
|
||||
filterRows: (row: TableBodyRow) => boolean,
|
||||
};
|
||||
|
||||
const filterRowsFactory = (filters: Array<Filter>, searchTerm: string) => (
|
||||
row: TableBodyRow,
|
||||
): boolean =>
|
||||
const rowMatchesFilters = (filters: Array<Filter>, row: TableBodyRow) =>
|
||||
filters
|
||||
.map((filter: Filter) => {
|
||||
if (filter.type === 'enum' && row.type != null) {
|
||||
@@ -48,14 +46,43 @@ const filterRowsFactory = (filters: Array<Filter>, 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<string>, 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<Filter>,
|
||||
searchTerm: string,
|
||||
regexSearch: boolean,
|
||||
) => (row: TableBodyRow): boolean =>
|
||||
rowMatchesFilters(filters, row) &&
|
||||
rowMatchesSearchTerm(searchTerm, regexSearch, row);
|
||||
|
||||
class SearchableManagedTable extends PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
@@ -63,7 +90,11 @@ class SearchableManagedTable extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
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<Props, State> {
|
||||
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,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user