Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Line Filter: Add regex option to line filter #958

Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,4 @@ keycombo
capslock
pageup
pagedown
linefilter
173 changes: 152 additions & 21 deletions src/Components/ServiceScene/LineFilterScene.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { LineFilterScene } from './LineFilterScene';
import userEvent from '@testing-library/user-event';
import { CustomVariable, SceneVariableSet } from '@grafana/scenes';
Expand All @@ -14,38 +14,169 @@ jest.mock('lodash', () => ({
describe('LineFilter', () => {
let scene: LineFilterScene;
let lineFilterVariable: CustomVariable;
beforeEach(() => {
lineFilterVariable = new CustomVariable({ name: VAR_LINE_FILTER, value: '', hide: VariableHide.hideVariable });
scene = new LineFilterScene({
$variables: new SceneVariableSet({
variables: [lineFilterVariable],
}),

describe('case insensitive, no regex', () => {
beforeEach(() => {
lineFilterVariable = new CustomVariable({ name: VAR_LINE_FILTER, value: '', hide: VariableHide.hideVariable });
scene = new LineFilterScene({
caseSensitive: false,
$variables: new SceneVariableSet({
variables: [lineFilterVariable],
}),
});
});

test('Updates the variable with the user input', async () => {
render(<scene.Component model={scene} />);

await act(() => userEvent.type(screen.getByPlaceholderText('Search in log lines'), 'some text'));

expect(await screen.findByDisplayValue('some text')).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe('|~ `(?i)some text`');
});

test('Escapes the regular expression in the variable', async () => {
render(<scene.Component model={scene} />);

await act(() => userEvent.type(screen.getByPlaceholderText('Search in log lines'), '(characters'));

expect(await screen.findByDisplayValue('(characters')).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe('|~ `(?i)\\(characters`');
});

test('Unescapes the regular expression from the variable value', async () => {
lineFilterVariable.changeValueTo('|~ `(?i)\\(characters`');

render(<scene.Component model={scene} />);

expect(await screen.findByDisplayValue('(characters')).toBeInTheDocument();
});
});
describe('case sensitive, no regex', () => {
beforeEach(() => {
lineFilterVariable = new CustomVariable({ name: VAR_LINE_FILTER, value: '', hide: VariableHide.hideVariable });
scene = new LineFilterScene({
caseSensitive: true,
$variables: new SceneVariableSet({
variables: [lineFilterVariable],
}),
});
});

test('Updates the variable with the user input', async () => {
render(<scene.Component model={scene} />);

await act(() => userEvent.type(screen.getByPlaceholderText('Search in log lines'), 'some text'));

expect(await screen.findByDisplayValue('some text')).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe('|= `some text`');
});

test('Escapes the regular expression in the variable', async () => {
render(<scene.Component model={scene} />);

await act(() => userEvent.type(screen.getByPlaceholderText('Search in log lines'), '(characters'));

expect(await screen.findByDisplayValue('(characters')).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe('|= `\\(characters`');
});

test('Updates the variable with the user input', async () => {
render(<scene.Component model={scene} />);
test('Unescapes the regular expression from the variable value', async () => {
lineFilterVariable.changeValueTo('|~ `(?i)\\(characters`');

await act(() => userEvent.type(screen.getByPlaceholderText('Search in log lines'), 'some text'));
render(<scene.Component model={scene} />);

expect(await screen.findByDisplayValue('some text')).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe('|~ `(?i)some text`');
expect(await screen.findByDisplayValue('(characters')).toBeInTheDocument();
});
});
describe('case insensitive, regex', () => {
beforeEach(() => {
lineFilterVariable = new CustomVariable({ name: VAR_LINE_FILTER, value: '', hide: VariableHide.hideVariable });
scene = new LineFilterScene({
caseSensitive: false,
regex: true,
$variables: new SceneVariableSet({
variables: [lineFilterVariable],
}),
});
});

test('Updates the variable with the user input', async () => {
render(<scene.Component model={scene} />);

const string = `((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}`;
const input = screen.getByPlaceholderText('Search in log lines');
// Jest can't type regex apparently
await act(() => fireEvent.change(input, { target: { value: string } }));

expect(await screen.findByDisplayValue(string)).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe(`|~ \`(?i)${string}\``);
});

test('Escapes the regular expression in the variable', async () => {
render(<scene.Component model={scene} />);
test('Does not escape the regular expression', async () => {
render(<scene.Component model={scene} />);

await act(() => userEvent.type(screen.getByPlaceholderText('Search in log lines'), '(characters'));
const string = `((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}`;
const input = screen.getByPlaceholderText('Search in log lines');
// Jest can't type regex apparently
await act(() => fireEvent.change(input, { target: { value: string } }));

expect(await screen.findByDisplayValue('(characters')).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe('|~ `(?i)\\(characters`');
expect(await screen.findByDisplayValue(string)).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe(`|~ \`(?i)${string}\``);
});

test('Unescapes the regular expression from the variable value', async () => {
const string = `((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}`;
lineFilterVariable.changeValueTo(`|~ \`(?i)${string}\``);

render(<scene.Component model={scene} />);

expect(await screen.findByDisplayValue(string)).toBeInTheDocument();
});
});
describe('case sensitive, regex', () => {
beforeEach(() => {
lineFilterVariable = new CustomVariable({ name: VAR_LINE_FILTER, value: '', hide: VariableHide.hideVariable });
scene = new LineFilterScene({
caseSensitive: true,
regex: true,
$variables: new SceneVariableSet({
variables: [lineFilterVariable],
}),
});
});

test('Updates the variable with the user input', async () => {
render(<scene.Component model={scene} />);

const string = `((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}`;
const input = screen.getByPlaceholderText('Search in log lines');
// Jest can't type regex apparently
await act(() => fireEvent.change(input, { target: { value: string } }));

expect(await screen.findByDisplayValue(string)).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe(`|~ \`${string}\``);
});

test('Unescapes the regular expression from the variable value', async () => {
lineFilterVariable.changeValueTo('|~ `(?i)\\(characters`');
test('Does not escape the regular expression', async () => {
render(<scene.Component model={scene} />);

render(<scene.Component model={scene} />);
const string = `((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}`;
const input = screen.getByPlaceholderText('Search in log lines');
// Jest can't type regex apparently
await act(() => fireEvent.change(input, { target: { value: string } }));

expect(await screen.findByDisplayValue('(characters')).toBeInTheDocument();
expect(await screen.findByDisplayValue(string)).toBeInTheDocument();
expect(lineFilterVariable.getValue()).toBe(`|~ \`${string}\``);
});

test('Unescapes the regular expression from the variable value', async () => {
const string = `((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}`;
lineFilterVariable.changeValueTo(`|~ \`${string}\``);

render(<scene.Component model={scene} />);

expect(await screen.findByDisplayValue(string)).toBeInTheDocument();
});
});
});
88 changes: 75 additions & 13 deletions src/Components/ServiceScene/LineFilterScene.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { css } from '@emotion/css';
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { Field } from '@grafana/ui';
import { debounce, escapeRegExp } from 'lodash';
import { debounce, escape, escapeRegExp } from 'lodash';
import React, { ChangeEvent, KeyboardEvent } from 'react';
import { testIds } from 'services/testIds';
import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from 'services/analytics';
import { SearchInput } from './Breakdowns/SearchInput';
import { LineFilterIcon } from './LineFilterIcon';
import { getLineFilterVariable } from '../../services/variableGetters';
import { getLineFilterCase, getLineFilterRegex, setLineFilterCase, setLineFilterRegex } from '../../services/store';
import { RegexIcon, RegexInputValue } from './RegexIcon';
import { logger } from '../../services/logger';

interface LineFilterState extends SceneObjectState {
lineFilter: string;
caseSensitive: boolean;
regex: boolean;
}

export class LineFilterScene extends SceneObjectBase<LineFilterState> {
static Component = LineFilterRenderer;

constructor(state?: Partial<LineFilterState>) {
const caseSensitive = getLineFilterCase(false);
gtk-grafana marked this conversation as resolved.
Show resolved Hide resolved
super({
lineFilter: state?.lineFilter || '',
caseSensitive: false,
caseSensitive,
regex: getLineFilterRegex(false),
gtk-grafana marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue with regex is that we're setting it based on the stored preference and not the filter contents.

...state,
});
this.addActivationHandler(this.onActivate);
Expand All @@ -33,15 +39,42 @@ export class LineFilterScene extends SceneObjectBase<LineFilterState> {
return;
}
const caseSensitive = lineFilterString.includes('|=');
const matches = caseSensitive ? lineFilterString.match(/\|=.`(.+?)`/) : lineFilterString.match(/`\(\?i\)(.+)`/);

if (!matches || matches.length !== 2) {
const caseSensitiveMatches = caseSensitive
? lineFilterString.match(/\|=.`(.+?)`/)
: lineFilterString.match(/`\(\?i\)(.+)`/);

// If the existing query is case sensitive, overwrite the users options for case sensitivity
if (caseSensitiveMatches && caseSensitiveMatches.length === 2) {
// If the current state is not regex, remove escape chars
if (!this.state.regex) {
this.setState({
lineFilter: caseSensitiveMatches[1].replace(/\\(.)/g, '$1'),
caseSensitive,
});
return;
} else {
// If regex, don't remove escape chars
this.setState({
lineFilter: caseSensitiveMatches[1],
caseSensitive,
});
}
return;
}
this.setState({
lineFilter: matches[1].replace(/\\(.)/g, '$1'),
caseSensitive,
});

const regexMatches = lineFilterString.match(/\|~.+\`(.*?)\`/);
if (regexMatches?.length === 2) {
this.setState({
lineFilter: regexMatches[1],
});

return;
} else {
const error = new Error(`Unable to parse line filter: ${lineFilterString}`);
logger.error(error, { msg: `Unable to parse line filter: ${lineFilterString}` });
throw error;
}
};

updateFilter(lineFilter: string, debounced = true) {
Expand All @@ -66,11 +99,31 @@ export class LineFilterScene extends SceneObjectBase<LineFilterState> {
};

onCaseSensitiveToggle = (newState: 'sensitive' | 'insensitive') => {
const caseSensitive = newState === 'sensitive';

// Set value to scene state
this.setState({
caseSensitive: newState === 'sensitive',
caseSensitive,
});

this.updateFilter(this.state.lineFilter);
// Set value in local storage
setLineFilterCase(caseSensitive);

this.updateFilter(this.state.lineFilter, false);
};

onRegexToggle = (newState: RegexInputValue) => {
const regex = newState === 'regex';

// Set value to scene state
this.setState({
regex,
});

// Set value in local storage
setLineFilterRegex(regex);

this.updateFilter(this.state.lineFilter, false);
};

updateVariableDebounced = debounce((search: string) => {
Expand All @@ -82,8 +135,12 @@ export class LineFilterScene extends SceneObjectBase<LineFilterState> {
if (search === '') {
variable.changeValueTo('');
} else {
if (this.state.caseSensitive) {
if (this.state.caseSensitive && !this.state.regex) {
variable.changeValueTo(`|= \`${escapeRegExp(search)}\``);
} else if (this.state.caseSensitive && this.state.regex) {
variable.changeValueTo(`|~ \`${escape(search)}\``);
} else if (!this.state.caseSensitive && this.state.regex) {
variable.changeValueTo(`|~ \`(?i)${escape(search)}\``);
} else {
variable.changeValueTo(`|~ \`(?i)${escapeRegExp(search)}\``);
}
Expand All @@ -101,15 +158,20 @@ export class LineFilterScene extends SceneObjectBase<LineFilterState> {
}

function LineFilterRenderer({ model }: SceneComponentProps<LineFilterScene>) {
const { lineFilter, caseSensitive } = model.useState();
const { lineFilter, caseSensitive, regex } = model.useState();
return (
<Field className={styles.field}>
<SearchInput
data-testid={testIds.exploreServiceDetails.searchLogs}
value={lineFilter}
className={styles.input}
onChange={model.handleChange}
suffix={<LineFilterIcon caseSensitive={caseSensitive} onCaseSensitiveToggle={model.onCaseSensitiveToggle} />}
suffix={
<>
<LineFilterIcon caseSensitive={caseSensitive} onCaseSensitiveToggle={model.onCaseSensitiveToggle} />
<RegexIcon regex={regex} onRegexToggle={model.onRegexToggle} />
</>
}
placeholder="Search in log lines"
onClear={() => {
model.updateFilter('', false);
Expand Down
Loading
Loading