Skip to content

Commit 8ffb38c

Browse files
authored
feat(searchbar): Add tooltip descriptions to filter keys (#92449)
1 parent d4a328e commit 8ffb38c

File tree

5 files changed

+74
-34
lines changed

5 files changed

+74
-34
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const FILTER_KEYS: TagCollection = {
1717
jest.mock('sentry/components/searchQueryBuilder/context', () => ({
1818
useSearchQueryBuilder: () => ({
1919
size: 'normal',
20+
getFieldDefinition: () => null,
2021
}),
2122
}));
2223

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,6 +1638,16 @@ describe('SearchQueryBuilder', function () {
16381638
await screen.findByRole('row', {name: 'is:unresolved'})
16391639
).toBeInTheDocument();
16401640
});
1641+
1642+
it('shows tooltip with field description when hovering over operator label', async function () {
1643+
render(<SearchQueryBuilder {...defaultProps} initialQuery="assigned:me" />);
1644+
1645+
await userEvent.hover(screen.getByText('assigned'));
1646+
1647+
expect(
1648+
await screen.findByText('Assignee of the issue as a user ID')
1649+
).toBeInTheDocument();
1650+
});
16411651
});
16421652

16431653
describe('has', function () {

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

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {mergeProps} from '@react-aria/utils';
55
import type {ListState} from '@react-stately/list';
66
import type {Node} from '@react-types/shared';
77

8+
import {Tooltip} from 'sentry/components/core/tooltip';
89
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
910
import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
1011
import {FilterKeyCombobox} from 'sentry/components/searchQueryBuilder/tokens/filter/filterKeyCombobox';
@@ -29,7 +30,8 @@ type FilterKeyProps = {
2930

3031
export function FilterKey({item, state, token, onActiveChange}: FilterKeyProps) {
3132
const ref = useRef<HTMLDivElement>(null);
32-
const {disabled} = useSearchQueryBuilder();
33+
const {disabled, getFieldDefinition} = useSearchQueryBuilder();
34+
const fieldDefinition = getFieldDefinition(token.key.text);
3335

3436
const [isEditing, setIsEditing] = useState(false);
3537

@@ -59,19 +61,22 @@ export function FilterKey({item, state, token, onActiveChange}: FilterKeyProps)
5961
}
6062

6163
return (
62-
<KeyButton
63-
aria-label={t('Edit key for filter: %s', getKeyName(token.key))}
64-
onClick={() => {
65-
setIsEditing(true);
66-
onActiveChange(true);
67-
}}
68-
disabled={disabled}
69-
{...filterButtonProps}
70-
>
71-
<InteractionStateLayer />
72-
{/* Filter keys have no expected format, so we attempt to split by whitespace, dash, colon, and underscores. */}
73-
{middleEllipsis(getKeyLabel(token.key), 40, /[\s-_:]/)}
74-
</KeyButton>
64+
<Tooltip title={fieldDefinition?.desc} skipWrapper>
65+
<KeyButton
66+
aria-label={t('Edit key for filter: %s', getKeyName(token.key))}
67+
onClick={() => {
68+
setIsEditing(true);
69+
onActiveChange(true);
70+
}}
71+
disabled={disabled}
72+
title="hello"
73+
{...filterButtonProps}
74+
>
75+
<InteractionStateLayer />
76+
{/* Filter keys have no expected format, so we attempt to split by whitespace, dash, colon, and underscores. */}
77+
{middleEllipsis(getKeyLabel(token.key), 40, /[\s-_:]/)}
78+
</KeyButton>
79+
</Tooltip>
7580
);
7681
}
7782

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {ListState} from '@react-stately/list';
55
import type {Node} from '@react-types/shared';
66

77
import {CompactSelect, type SelectOption} from 'sentry/components/core/compactSelect';
8+
import {Tooltip} from 'sentry/components/core/tooltip';
89
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
910
import {useSearchQueryBuilder} from 'sentry/components/searchQueryBuilder/context';
1011
import {UnstyledButton} from 'sentry/components/searchQueryBuilder/tokens/filter/unstyledButton';
@@ -85,22 +86,29 @@ function getTermOperatorFromToken(token: TokenResult<Token.FILTER>) {
8586
}
8687

8788
function FilterKeyOperatorLabel({
89+
keyValue,
8890
keyLabel,
8991
opLabel,
9092
includeKeyLabel,
9193
}: {
94+
keyLabel: string;
95+
keyValue: string;
9296
includeKeyLabel?: boolean;
93-
keyLabel?: string;
9497
opLabel?: string;
9598
}) {
99+
const {getFieldDefinition} = useSearchQueryBuilder();
100+
const fieldDefinition = getFieldDefinition(keyValue);
101+
96102
if (!includeKeyLabel) {
97103
return <OpLabel>{opLabel}</OpLabel>;
98104
}
99105

100106
return (
101107
<KeyOpLabelWrapper>
102-
<span>{keyLabel}</span>
103-
{opLabel ? <OpLabel> {opLabel}</OpLabel> : null}
108+
<Tooltip title={fieldDefinition?.desc}>
109+
<span>{keyLabel}</span>
110+
{opLabel ? <OpLabel> {opLabel}</OpLabel> : null}
111+
</Tooltip>
104112
</KeyOpLabelWrapper>
105113
);
106114
}
@@ -138,6 +146,7 @@ export function getOperatorInfo(token: TokenResult<Token.FILTER>): {
138146
operator,
139147
label: (
140148
<FilterKeyOperatorLabel
149+
keyValue={token.key.value}
141150
keyLabel={token.key.text}
142151
opLabel={operator === TermOperator.NOT_EQUAL ? 'not' : undefined}
143152
includeKeyLabel
@@ -146,14 +155,21 @@ export function getOperatorInfo(token: TokenResult<Token.FILTER>): {
146155
options: [
147156
{
148157
value: TermOperator.DEFAULT,
149-
label: <FilterKeyOperatorLabel keyLabel={token.key.text} includeKeyLabel />,
158+
label: (
159+
<FilterKeyOperatorLabel
160+
keyLabel={token.key.text}
161+
keyValue={token.key.value}
162+
includeKeyLabel
163+
/>
164+
),
150165
textValue: 'is',
151166
},
152167
{
153168
value: TermOperator.NOT_EQUAL,
154169
label: (
155170
<FilterKeyOperatorLabel
156171
keyLabel={token.key.text}
172+
keyValue={token.key.value}
157173
opLabel="not"
158174
includeKeyLabel
159175
/>
@@ -170,18 +186,31 @@ export function getOperatorInfo(token: TokenResult<Token.FILTER>): {
170186
label: (
171187
<FilterKeyOperatorLabel
172188
keyLabel={operator === TermOperator.NOT_EQUAL ? 'does not have' : 'has'}
189+
keyValue={token.key.value}
173190
includeKeyLabel
174191
/>
175192
),
176193
options: [
177194
{
178195
value: TermOperator.DEFAULT,
179-
label: <FilterKeyOperatorLabel keyLabel="has" includeKeyLabel />,
196+
label: (
197+
<FilterKeyOperatorLabel
198+
keyLabel="has"
199+
keyValue={token.key.value}
200+
includeKeyLabel
201+
/>
202+
),
180203
textValue: 'has',
181204
},
182205
{
183206
value: TermOperator.NOT_EQUAL,
184-
label: <FilterKeyOperatorLabel keyLabel="does not have" includeKeyLabel />,
207+
label: (
208+
<FilterKeyOperatorLabel
209+
keyLabel="does not have"
210+
keyValue={token.key.value}
211+
includeKeyLabel
212+
/>
213+
),
185214
textValue: 'does not have',
186215
},
187216
],

static/app/utils/fields/index.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,33 +2582,28 @@ export const getFieldDefinition = (
25822582
switch (type) {
25832583
case 'replay':
25842584
if (REPLAY_FIELD_DEFINITIONS.hasOwnProperty(key)) {
2585-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2586-
return REPLAY_FIELD_DEFINITIONS[key];
2585+
return REPLAY_FIELD_DEFINITIONS[key as keyof typeof REPLAY_FIELD_DEFINITIONS];
25872586
}
25882587
if (REPLAY_CLICK_FIELD_DEFINITIONS.hasOwnProperty(key)) {
2589-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2590-
return REPLAY_CLICK_FIELD_DEFINITIONS[key];
2588+
return REPLAY_CLICK_FIELD_DEFINITIONS[
2589+
key as keyof typeof REPLAY_CLICK_FIELD_DEFINITIONS
2590+
];
25912591
}
25922592
if (REPLAY_FIELDS.includes(key as FieldKey)) {
2593-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2594-
return EVENT_FIELD_DEFINITIONS[key];
2593+
return EVENT_FIELD_DEFINITIONS[key as FieldKey];
25952594
}
25962595
return null;
25972596
case 'feedback':
25982597
if (FEEDBACK_FIELD_DEFINITIONS.hasOwnProperty(key)) {
2599-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2600-
return FEEDBACK_FIELD_DEFINITIONS[key];
2598+
return FEEDBACK_FIELD_DEFINITIONS[key as keyof typeof FEEDBACK_FIELD_DEFINITIONS];
26012599
}
26022600
if (FEEDBACK_FIELDS.includes(key as FieldKey)) {
2603-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2604-
return EVENT_FIELD_DEFINITIONS[key];
2601+
return EVENT_FIELD_DEFINITIONS[key as FieldKey];
26052602
}
26062603
return null;
26072604
case 'span':
2608-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2609-
if (SPAN_FIELD_DEFINITIONS[key]) {
2610-
// @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
2611-
return SPAN_FIELD_DEFINITIONS[key];
2605+
if (SPAN_FIELD_DEFINITIONS[key as keyof typeof SPAN_FIELD_DEFINITIONS]) {
2606+
return SPAN_FIELD_DEFINITIONS[key as keyof typeof SPAN_FIELD_DEFINITIONS];
26122607
}
26132608

26142609
// In EAP we have numeric tags that can be passed as parameters to

0 commit comments

Comments
 (0)