FlameGraph: Add support for regex search patterns and multiple search terms (#106347)
* "or" search terms with commas * Add regex support to search bar * Don't try match empty search terms * Fix lint error
This commit is contained in:
@@ -4,8 +4,9 @@ import { useRef, useCallback } from 'react';
|
||||
|
||||
import { createDataFrame, createTheme } from '@grafana/data';
|
||||
|
||||
import { FlameGraphDataContainer } from './FlameGraph/dataTransform';
|
||||
import { data } from './FlameGraph/testData/dataNestedSet';
|
||||
import FlameGraphContainer from './FlameGraphContainer';
|
||||
import FlameGraphContainer, { labelSearch } from './FlameGraphContainer';
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
@@ -99,21 +100,117 @@ describe('FlameGraphContainer', () => {
|
||||
render(<FlameGraphContainerWithProps />);
|
||||
|
||||
// Checking for presence of this function before filter
|
||||
const matchingText = 'net/http.HandlerFunc.ServeHTTP';
|
||||
const matchingText1 = 'net/http.HandlerFunc.ServeHTTP';
|
||||
const matchingText2 = 'runtime.gcBgMarkWorker';
|
||||
const nonMatchingText = 'runtime.systemstack';
|
||||
|
||||
expect(screen.queryAllByText(matchingText).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText1).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText2).length).toBe(1);
|
||||
expect(screen.queryAllByText(nonMatchingText).length).toBe(1);
|
||||
|
||||
// Apply the filter
|
||||
const searchInput = await screen.getByPlaceholderText('Search...');
|
||||
await userEvent.type(searchInput, 'Handler serve');
|
||||
const searchInput = screen.getByPlaceholderText('Search...');
|
||||
await userEvent.type(searchInput, 'Handler serve,gcBgMarkWorker');
|
||||
|
||||
// We have to wait for filter to take effect
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByText(nonMatchingText).length).toBe(0);
|
||||
});
|
||||
// Check we didn't lose the one that should match
|
||||
expect(screen.queryAllByText(matchingText).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText1).length).toBe(1);
|
||||
expect(screen.queryAllByText(matchingText2).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('labelSearch', () => {
|
||||
let container: FlameGraphDataContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
const df = createDataFrame(data);
|
||||
df.meta = {
|
||||
custom: {
|
||||
ProfileTypeID: 'cpu:foo:bar',
|
||||
},
|
||||
};
|
||||
|
||||
container = new FlameGraphDataContainer(df, { collapsing: false });
|
||||
});
|
||||
|
||||
describe('fuzzy', () => {
|
||||
it('single term', () => {
|
||||
const search = 'test pkg';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(45);
|
||||
});
|
||||
|
||||
it('multiple terms', () => {
|
||||
const search = 'test pkg,compress';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(107);
|
||||
});
|
||||
|
||||
it('falls back to fuzzy with malformed regex', () => {
|
||||
const search = 'deduplicatingSlice[.';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(1);
|
||||
});
|
||||
|
||||
it('no results', () => {
|
||||
const search = 'term_not_found';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regex', () => {
|
||||
it('single pattern', () => {
|
||||
const term = '\\d$';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(61);
|
||||
});
|
||||
|
||||
it('multiple patterns', () => {
|
||||
const term = '\\d$,^go';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(62);
|
||||
});
|
||||
|
||||
it('no results', () => {
|
||||
const term = 'pattern_not_found';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fuzzy and regex', () => {
|
||||
it('regex found, fuzzy found', () => {
|
||||
const term = '\\d$,test pkg';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(98);
|
||||
});
|
||||
|
||||
it('regex not found, fuzzy found', () => {
|
||||
const term = 'not_found_suffix$,test pkg';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(45);
|
||||
});
|
||||
|
||||
it('regex found, fuzzy not found', () => {
|
||||
const term = '\\d$,not_found_fuzzy';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(61);
|
||||
});
|
||||
|
||||
it('regex not found, fuzzy not found', () => {
|
||||
const term = 'not_found_suffix$,not_found_fuzzy';
|
||||
let found = labelSearch(term, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
|
||||
it('does not match empty terms', () => {
|
||||
const search = ',,,,,';
|
||||
let found = labelSearch(search, container);
|
||||
expect(found.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -317,26 +317,71 @@ function useColorScheme(dataContainer: FlameGraphDataContainer | undefined) {
|
||||
/**
|
||||
* Based on the search string it does a fuzzy search over all the unique labels, so we can highlight them later.
|
||||
*/
|
||||
function useLabelSearch(
|
||||
export function useLabelSearch(
|
||||
search: string | undefined,
|
||||
data: FlameGraphDataContainer | undefined
|
||||
): Set<string> | undefined {
|
||||
return useMemo(() => {
|
||||
if (search && data) {
|
||||
const foundLabels = new Set<string>();
|
||||
let idxs = ufuzzy.filter(data.getUniqueLabels(), search);
|
||||
if (!search || !data) {
|
||||
// In this case undefined means there was no search so no attempt to
|
||||
// highlighting anything should be made.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (idxs) {
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(data.getUniqueLabels()[idx]);
|
||||
}
|
||||
return labelSearch(search, data);
|
||||
}, [search, data]);
|
||||
}
|
||||
|
||||
export function labelSearch(search: string, data: FlameGraphDataContainer): Set<string> {
|
||||
const foundLabels = new Set<string>();
|
||||
const terms = search.split(',');
|
||||
|
||||
const regexFilter = (labels: string[], pattern: string): boolean => {
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(pattern);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let foundMatch = false;
|
||||
for (let label of labels) {
|
||||
if (!regex.test(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
foundLabels.add(label);
|
||||
foundMatch = true;
|
||||
}
|
||||
// In this case undefined means there was no search so no attempt to highlighting anything should be made.
|
||||
return undefined;
|
||||
}, [search, data]);
|
||||
return foundMatch;
|
||||
};
|
||||
|
||||
const fuzzyFilter = (labels: string[], term: string): boolean => {
|
||||
let idxs = ufuzzy.filter(labels, term);
|
||||
if (!idxs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let foundMatch = false;
|
||||
for (let idx of idxs) {
|
||||
foundLabels.add(labels[idx]);
|
||||
foundMatch = true;
|
||||
}
|
||||
return foundMatch;
|
||||
};
|
||||
|
||||
for (let term of terms) {
|
||||
if (!term) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const found = regexFilter(data.getUniqueLabels(), term);
|
||||
if (!found) {
|
||||
fuzzyFilter(data.getUniqueLabels(), term);
|
||||
}
|
||||
}
|
||||
|
||||
return foundLabels;
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
|
||||
Reference in New Issue
Block a user