Skip to content

Commit 4d82cb2

Browse files
fix(search): render escaped asterisks as escaped (#118019)
In the shared `SearchQueryBuilder`, filter token values containing an escaped asterisk (`\*`) were rendered identically to an active wildcard (`*`). Eek. This PR adds a visual `\` to differentiate. Given the `message` contains query with `a * b \* c`: <table> <thead> <tr> <th>Before</th> <th>After</th> </tr> </thead> <tbody> <tr> <td> <img width="357" height="131" alt="image" src="https://github.com/user-attachments/assets/dcb8040d-e61c-4297-a21f-8ed4f3edf28a" /> </td> <td> <img width="357" height="131" alt="image" src="https://github.com/user-attachments/assets/56e4723a-c037-4ebf-8979-f5b5ef50e183" /> </td> </tr> </tbody> </table> Closes LOGS-880.
1 parent 9db35ca commit 4d82cb2

4 files changed

Lines changed: 67 additions & 11 deletions

File tree

static/app/components/searchQueryBuilder/formattedQuery.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,16 @@ describe('FormattedQuery', () => {
102102

103103
expect(screen.getByText(textWithMarkupMatcher('has foo'))).toBeInTheDocument();
104104
});
105+
106+
it('renders an escaped asterisk with the escape visible', () => {
107+
render(<FormattedQuery {...defaultProps} query={'message:foo\\*bar'} />);
108+
109+
expect(screen.getByText('foo\\*bar')).toBeInTheDocument();
110+
});
111+
112+
it('renders a wildcard asterisk without an escape', () => {
113+
render(<FormattedQuery {...defaultProps} query="message:foo*bar" />);
114+
115+
expect(screen.getByText('foo*bar')).toBeInTheDocument();
116+
});
105117
});

static/app/components/searchQueryBuilder/index.spec.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,11 +2748,21 @@ describe('SearchQueryBuilder', () => {
27482748
});
27492749
});
27502750

2751-
it('renders escaped asterisks as a bare asterisk in the filter chip', async () => {
2751+
it('renders an escaped asterisk with the escape visible in the filter chip', async () => {
27522752
render(
27532753
<SearchQueryBuilder {...defaultProps} initialQuery={'browser.name:foo\\*'} />
27542754
);
27552755

2756+
expect(
2757+
await within(
2758+
screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
2759+
).findByText('foo\\*')
2760+
).toBeInTheDocument();
2761+
});
2762+
2763+
it('renders a wildcard asterisk without an escape in the filter chip', async () => {
2764+
render(<SearchQueryBuilder {...defaultProps} initialQuery="browser.name:foo*" />);
2765+
27562766
expect(
27572767
await within(
27582768
screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
@@ -5621,7 +5631,7 @@ describe('SearchQueryBuilder', () => {
56215631
).toBeInTheDocument();
56225632
});
56235633

5624-
it('escapes * for is op but not contains op', async () => {
5634+
it('renders the escaped asterisk for the contains suggestion but a wildcard for the is suggestion', async () => {
56255635
render(
56265636
<SearchQueryBuilder
56275637
{...defaultProps}
@@ -5635,7 +5645,7 @@ describe('SearchQueryBuilder', () => {
56355645
const options = within(screen.getByRole('listbox')).getAllByRole('option');
56365646
expect(options).toHaveLength(2);
56375647

5638-
expect(options[0]).toHaveTextContent('span.description contains test*');
5648+
expect(options[0]).toHaveTextContent('span.description contains test\\*');
56395649
expect(options[1]).toHaveTextContent('span.description is test*');
56405650
});
56415651

static/app/components/searchQueryBuilder/tokens/filter/utils.spec.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ describe('unescapeAsteriskSearchValue', () => {
196196
});
197197

198198
describe('formatFilterValue', () => {
199-
it('unescapes asterisks for unquoted values', () => {
199+
it('preserves escaped asterisks for unquoted values', () => {
200200
expect(
201201
formatFilterValue({
202202
token: {
@@ -206,19 +206,55 @@ describe('formatFilterValue', () => {
206206
quoted: false,
207207
} as any,
208208
})
209-
).toBe('****');
209+
).toBe('\\*\\*\\*\\*');
210210
});
211211

212-
it('unescapes asterisks for quoted values', () => {
212+
it('preserves escaped asterisks for quoted values', () => {
213213
expect(
214214
formatFilterValue({
215215
token: {
216216
type: Token.VALUE_TEXT,
217-
text: '"foo\\\\*bar"',
217+
text: '"foo\\*bar"',
218218
value: 'foo\\*bar',
219219
quoted: true,
220220
} as any,
221221
})
222-
).toBe('foo*bar');
222+
).toBe('foo\\*bar');
223+
});
224+
225+
it('renders an escaped asterisk differently from a wildcard asterisk', () => {
226+
const escaped = formatFilterValue({
227+
token: {
228+
type: Token.VALUE_TEXT,
229+
text: 'foo\\*bar',
230+
value: 'foo\\*bar',
231+
quoted: false,
232+
} as any,
233+
});
234+
const wildcard = formatFilterValue({
235+
token: {
236+
type: Token.VALUE_TEXT,
237+
text: 'foo*bar',
238+
value: 'foo*bar',
239+
quoted: false,
240+
} as any,
241+
});
242+
243+
expect(escaped).toBe('foo\\*bar');
244+
expect(wildcard).toBe('foo*bar');
245+
expect(escaped).not.toBe(wildcard);
246+
});
247+
248+
it('preserves a literal backslash before a wildcard asterisk', () => {
249+
expect(
250+
formatFilterValue({
251+
token: {
252+
type: Token.VALUE_TEXT,
253+
text: 'foo\\\\*bar',
254+
value: 'foo\\\\*bar',
255+
quoted: false,
256+
} as any,
257+
})
258+
).toBe('foo\\\\*bar');
223259
});
224260
});

static/app/components/searchQueryBuilder/tokens/filter/utils.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,7 @@ export function formatFilterValue({
209209
return content;
210210
}
211211

212-
return unescapeAsteriskSearchValue(
213-
token.quoted ? unescapeTagValue(content) : content
214-
);
212+
return token.quoted ? unescapeTagValue(content) : content;
215213
}
216214
case Token.VALUE_RELATIVE_DATE:
217215
return t('%s', `${token.value}${token.unit} ago`);

0 commit comments

Comments
 (0)