Skip to content

Commit cd9e1b3

Browse files
L2D2Grafanamatyax
andauthored
feat(patterns): alert to explain only index labels are applied to patterns (#1769)
* Apply suggestion from @matyax Co-authored-by: Matias Chomicki <matyax@gmail.com> * feat(patterns): update test copy to match --------- Co-authored-by: Matias Chomicki <matyax@gmail.com>
1 parent 72dd64d commit cd9e1b3

File tree

2 files changed

+196
-11
lines changed

2 files changed

+196
-11
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import React, { useMemo, useState } from 'react';
2+
3+
import { render, screen } from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
5+
6+
import { sceneGraph } from '@grafana/scenes';
7+
8+
import { PatternTextSearchComponent, PatternsViewTextSearch } from './PatternsViewTextSearch';
9+
10+
const NON_INDEXED_FILTERS_ALERT_TEXT =
11+
/Parsed fields, structured metadata, and string filters are not supported for the pattern list/;
12+
13+
const mockGetAncestor = jest.spyOn(sceneGraph, 'getAncestor');
14+
const mockGetFieldsVariable = jest.fn();
15+
const mockGetMetadataVariable = jest.fn();
16+
const mockGetLineFiltersVariable = jest.fn();
17+
18+
jest.mock('../../../../services/variableGetters', () => ({
19+
getFieldsVariable: (...args: unknown[]) => mockGetFieldsVariable(...args),
20+
getMetadataVariable: (...args: unknown[]) => mockGetMetadataVariable(...args),
21+
getLineFiltersVariable: (...args: unknown[]) => mockGetLineFiltersVariable(...args),
22+
}));
23+
24+
function createVariableMock(filters: Array<{ key: string; operator: string; value: string }>) {
25+
return {
26+
useState: () => ({ filters }),
27+
};
28+
}
29+
30+
function TestWrapper({ model, onSetState }: { model: PatternsViewTextSearch; onSetState: jest.Mock }) {
31+
const [patternFilter, setPatternFilter] = useState('');
32+
const breakdownScene = useMemo(
33+
() => ({
34+
setState: (update: { patternFilter?: string }) => {
35+
if (update.patternFilter !== undefined) {
36+
setPatternFilter(update.patternFilter);
37+
onSetState(update);
38+
}
39+
},
40+
useState: () => ({ patternFilter }),
41+
}),
42+
[patternFilter, onSetState]
43+
);
44+
mockGetAncestor.mockReturnValue(breakdownScene);
45+
return <PatternTextSearchComponent model={model} />;
46+
}
47+
48+
describe('PatternsViewTextSearch', () => {
49+
let setStateSpy: jest.Mock;
50+
let model: PatternsViewTextSearch;
51+
52+
beforeEach(() => {
53+
jest.clearAllMocks();
54+
setStateSpy = jest.fn();
55+
mockGetFieldsVariable.mockReturnValue(createVariableMock([]));
56+
mockGetMetadataVariable.mockReturnValue(createVariableMock([]));
57+
mockGetLineFiltersVariable.mockReturnValue(createVariableMock([]));
58+
model = new PatternsViewTextSearch();
59+
});
60+
61+
describe('PatternTextSearchComponent', () => {
62+
it('renders search input with placeholder', () => {
63+
mockGetAncestor.mockReturnValue({
64+
setState: jest.fn(),
65+
useState: () => ({ patternFilter: '' }),
66+
});
67+
render(<PatternTextSearchComponent model={model} />);
68+
69+
expect(screen.getByPlaceholderText('Search patterns')).toBeInTheDocument();
70+
});
71+
72+
it('does not show alert when no non-indexed filters are present', () => {
73+
mockGetAncestor.mockReturnValue({
74+
setState: jest.fn(),
75+
useState: () => ({ patternFilter: '' }),
76+
});
77+
render(<PatternTextSearchComponent model={model} />);
78+
79+
expect(screen.queryByText(NON_INDEXED_FILTERS_ALERT_TEXT)).not.toBeInTheDocument();
80+
});
81+
82+
it('shows alert when field filters are present', () => {
83+
mockGetAncestor.mockReturnValue({
84+
setState: jest.fn(),
85+
useState: () => ({ patternFilter: '' }),
86+
});
87+
mockGetFieldsVariable.mockReturnValue(createVariableMock([{ key: 'caller', operator: '=', value: 'main' }]));
88+
89+
render(<PatternTextSearchComponent model={model} />);
90+
91+
expect(screen.getByText(NON_INDEXED_FILTERS_ALERT_TEXT)).toBeInTheDocument();
92+
});
93+
94+
it('shows alert when metadata filters are present', () => {
95+
mockGetAncestor.mockReturnValue({
96+
setState: jest.fn(),
97+
useState: () => ({ patternFilter: '' }),
98+
});
99+
mockGetMetadataVariable.mockReturnValue(createVariableMock([{ key: 'level', operator: '=', value: 'info' }]));
100+
101+
render(<PatternTextSearchComponent model={model} />);
102+
103+
expect(screen.getByText(NON_INDEXED_FILTERS_ALERT_TEXT)).toBeInTheDocument();
104+
});
105+
106+
it('shows alert when line filters are present', () => {
107+
mockGetAncestor.mockReturnValue({
108+
setState: jest.fn(),
109+
useState: () => ({ patternFilter: '' }),
110+
});
111+
mockGetLineFiltersVariable.mockReturnValue(createVariableMock([{ key: 'match', operator: '=', value: 'error' }]));
112+
113+
render(<PatternTextSearchComponent model={model} />);
114+
115+
expect(screen.getByText(NON_INDEXED_FILTERS_ALERT_TEXT)).toBeInTheDocument();
116+
});
117+
118+
it('shows alert when multiple filter types are present', () => {
119+
mockGetAncestor.mockReturnValue({
120+
setState: jest.fn(),
121+
useState: () => ({ patternFilter: '' }),
122+
});
123+
mockGetFieldsVariable.mockReturnValue(createVariableMock([{ key: 'caller', operator: '=', value: 'main' }]));
124+
mockGetLineFiltersVariable.mockReturnValue(createVariableMock([{ key: 'match', operator: '=', value: 'error' }]));
125+
126+
render(<PatternTextSearchComponent model={model} />);
127+
128+
expect(screen.getByText(NON_INDEXED_FILTERS_ALERT_TEXT)).toBeInTheDocument();
129+
});
130+
});
131+
132+
describe('model interactions', () => {
133+
it('calls parent setState with patternFilter when handleSearchChange is triggered', async () => {
134+
render(<TestWrapper model={model} onSetState={setStateSpy} />);
135+
136+
const input = screen.getByPlaceholderText('Search patterns');
137+
await userEvent.type(input, 'foo');
138+
139+
expect(setStateSpy).toHaveBeenCalledWith({ patternFilter: 'f' });
140+
expect(setStateSpy).toHaveBeenCalledWith({ patternFilter: 'fo' });
141+
expect(setStateSpy).toHaveBeenCalledWith({ patternFilter: 'foo' });
142+
});
143+
144+
it('calls parent setState to clear patternFilter when clearSearch is triggered', async () => {
145+
mockGetAncestor.mockReturnValue({
146+
setState: setStateSpy,
147+
useState: () => ({ patternFilter: 'existing' }),
148+
});
149+
render(<PatternTextSearchComponent model={model} />);
150+
151+
const clearButton = screen.getByLabelText('Clear search');
152+
await userEvent.click(clearButton);
153+
154+
expect(setStateSpy).toHaveBeenCalledWith({ patternFilter: '' });
155+
});
156+
});
157+
});

src/Components/ServiceScene/Breakdowns/Patterns/PatternsViewTextSearch.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import React, { ChangeEvent } from 'react';
22

33
import { css } from '@emotion/css';
44

5+
import { GrafanaTheme2 } from '@grafana/data';
6+
import { t } from '@grafana/i18n';
57
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
6-
import { Field } from '@grafana/ui';
8+
import { Alert, Field, useStyles2 } from '@grafana/ui';
79

810
import { areArraysEqual } from '../../../../services/comparison';
911
import { debouncedFuzzySearch, fuzzySearch } from '../../../../services/search';
12+
import { getFieldsVariable, getLineFiltersVariable, getMetadataVariable } from '../../../../services/variableGetters';
1013
import { SearchInput } from '../SearchInput';
1114
import { PatternFrame, PatternsBreakdownScene } from './PatternsBreakdownScene';
1215

@@ -133,27 +136,52 @@ export class PatternsViewTextSearch extends SceneObjectBase<PatternsViewTextSear
133136
}
134137
}
135138

136-
const styles = {
139+
const getStyles = (theme: GrafanaTheme2) => ({
137140
field: css({
138141
label: 'field',
139142
marginBottom: 0,
140143
}),
141144
icon: css({
142145
cursor: 'pointer',
143146
}),
144-
};
147+
infoAlert: css({
148+
marginBottom: 0,
149+
}),
150+
wrapper: css({
151+
display: 'flex',
152+
flexDirection: 'column',
153+
gap: theme.spacing(1),
154+
}),
155+
});
145156

146157
export function PatternTextSearchComponent({ model }: SceneComponentProps<PatternsViewTextSearch>) {
147158
const patternsBreakdownScene = sceneGraph.getAncestor(model, PatternsBreakdownScene);
148159
const { patternFilter } = patternsBreakdownScene.useState();
160+
const { filters: fieldFilters } = getFieldsVariable(model).useState();
161+
const { filters: metadataFilters } = getMetadataVariable(model).useState();
162+
const { filters: lineFilters } = getLineFiltersVariable(model).useState();
163+
164+
const hasNonIndexedFilters = fieldFilters.length > 0 || metadataFilters.length > 0 || lineFilters.length > 0;
165+
const styles = useStyles2(getStyles);
166+
149167
return (
150-
<Field className={styles.field}>
151-
<SearchInput
152-
onChange={model.handleSearchChange}
153-
onClear={model.clearSearch}
154-
value={patternFilter}
155-
placeholder="Search patterns"
156-
/>
157-
</Field>
168+
<div className={styles.wrapper}>
169+
<Field className={styles.field}>
170+
<SearchInput
171+
onChange={model.handleSearchChange}
172+
onClear={model.clearSearch}
173+
value={patternFilter}
174+
placeholder="Search patterns"
175+
/>
176+
</Field>
177+
{hasNonIndexedFilters && (
178+
<Alert severity="info" title="" className={styles.infoAlert}>
179+
{t(
180+
'logs.logs-drilldown.patterns.indexed-labels-only',
181+
'Patterns are selected by label and may be filtered by level. Parsed fields, structured metadata, and string filters are not supported for the pattern list.'
182+
)}
183+
</Alert>
184+
)}
185+
</div>
158186
);
159187
}

0 commit comments

Comments
 (0)