Skip to content

Commit d3371a2

Browse files
authored
feat(schema-hints): Add search functionality to drawer (#87201)
I've added a simple search function on the drawer which allows users to search for tags. The search input is invisible until clicked on and the logic resembles the breadcrumbs search logic found [here](https://github.com/getsentry/sentry/blob/934c5a1769cd23eaefaa31b17173c885532d6fee/static/app/components/events/interfaces/breadcrumbs/index.tsx#L56-L89). Here's what it looks like: https://github.com/user-attachments/assets/15372d66-f786-45e8-af5c-2d098d861847 Closes #87029
1 parent 9e9d090 commit d3371a2

File tree

3 files changed

+116
-25
lines changed

3 files changed

+116
-25
lines changed

static/app/views/explore/components/schemaHintsDrawer.tsx

+90-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {Fragment, useCallback, useMemo} from 'react';
1+
import {Fragment, memo, useCallback, useMemo, useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import {Tag as Badge} from 'sentry/components/core/badge/tag';
5+
import {InputGroup} from 'sentry/components/core/input/inputGroup';
56
import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox';
67
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
78
import {IconSearch} from 'sentry/icons';
@@ -24,6 +25,8 @@ function SchemaHintsDrawer({hints}: SchemaHintsDrawerProps) {
2425
const exploreQuery = useExploreQuery();
2526
const setExploreQuery = useSetExploreQuery();
2627

28+
const [searchQuery, setSearchQuery] = useState('');
29+
2730
const selectedFilterKeys = useMemo(() => {
2831
const filterQuery = new MutableSearch(exploreQuery);
2932
return filterQuery.getFilterKeys();
@@ -52,6 +55,18 @@ function SchemaHintsDrawer({hints}: SchemaHintsDrawerProps) {
5255
];
5356
}, [hints, sortedSelectedHints]);
5457

58+
const sortedAndFilteredHints = useMemo(() => {
59+
if (!searchQuery.trim()) {
60+
return sortedHints;
61+
}
62+
63+
const searchFor = searchQuery.toLocaleLowerCase().trim();
64+
65+
return sortedHints.filter(hint =>
66+
prettifyTagKey(hint.key).toLocaleLowerCase().trim().includes(searchFor)
67+
);
68+
}, [sortedHints, searchQuery]);
69+
5570
const handleCheckboxChange = useCallback(
5671
(hint: Tag) => {
5772
const filterQuery = new MutableSearch(exploreQuery);
@@ -73,36 +88,68 @@ function SchemaHintsDrawer({hints}: SchemaHintsDrawerProps) {
7388
[exploreQuery, setExploreQuery]
7489
);
7590

91+
const noAttributesMessage = (
92+
<NoAttributesMessage>
93+
<p>{t('No attributes found.')}</p>
94+
</NoAttributesMessage>
95+
);
96+
97+
const HintItem = memo(
98+
({hint}: {hint: Tag}) => {
99+
const hintFieldDefinition = useMemo(
100+
() => getFieldDefinition(hint.key, 'span', hint.kind),
101+
[hint.key, hint.kind]
102+
);
103+
104+
const hintType = useMemo(
105+
() =>
106+
hintFieldDefinition?.valueType === FieldValueType.BOOLEAN
107+
? t('boolean')
108+
: hint.kind === FieldKind.MEASUREMENT
109+
? t('number')
110+
: t('string'),
111+
[hintFieldDefinition?.valueType, hint.kind]
112+
);
113+
return (
114+
<StyledMultipleCheckboxItem
115+
key={hint.key}
116+
value={hint.key}
117+
onChange={() => handleCheckboxChange(hint)}
118+
>
119+
<CheckboxLabelContainer>
120+
<CheckboxLabel>{prettifyTagKey(hint.key)}</CheckboxLabel>
121+
<Badge>{hintType}</Badge>
122+
</CheckboxLabelContainer>
123+
</StyledMultipleCheckboxItem>
124+
);
125+
},
126+
(prevProps, nextProps) => {
127+
return prevProps.hint.key === nextProps.hint.key;
128+
}
129+
);
130+
76131
return (
77132
<Fragment>
78133
<DrawerHeader hideBar />
79134
<DrawerBody>
80135
<HeaderContainer>
81136
<SchemaHintsHeader>{t('Filter Attributes')}</SchemaHintsHeader>
82-
<IconSearch size="md" />
137+
<StyledInputGroup>
138+
<SearchInput
139+
size="sm"
140+
value={searchQuery}
141+
onChange={e => setSearchQuery(e.target.value)}
142+
aria-label={t('Search attributes')}
143+
/>
144+
<InputGroup.TrailingItems disablePointerEvents>
145+
<IconSearch size="md" />
146+
</InputGroup.TrailingItems>
147+
</StyledInputGroup>
83148
</HeaderContainer>
84149
<StyledMultipleCheckbox name={t('Filter keys')} value={selectedFilterKeys}>
85-
{sortedHints.map(hint => {
86-
const hintFieldDefinition = getFieldDefinition(hint.key, 'span', hint.kind);
87-
const hintType =
88-
hintFieldDefinition?.valueType === FieldValueType.BOOLEAN
89-
? t('boolean')
90-
: hint.kind === FieldKind.MEASUREMENT
91-
? t('number')
92-
: t('string');
93-
return (
94-
<StyledMultipleCheckboxItem
95-
key={hint.key}
96-
value={hint.key}
97-
onChange={() => handleCheckboxChange(hint)}
98-
>
99-
<CheckboxLabelContainer>
100-
<CheckboxLabel>{prettifyTagKey(hint.key)}</CheckboxLabel>
101-
<Badge>{hintType}</Badge>
102-
</CheckboxLabelContainer>
103-
</StyledMultipleCheckboxItem>
104-
);
105-
})}
150+
{sortedAndFilteredHints.length === 0
151+
? noAttributesMessage
152+
: sortedAndFilteredHints.map(hint => <HintItem key={hint.key} hint={hint} />)}
106153
</StyledMultipleCheckbox>
107154
</DrawerBody>
108155
</Fragment>
@@ -174,3 +221,23 @@ const StyledMultipleCheckboxItem = styled(MultipleCheckbox.Item)`
174221
${p => p.theme.overflowEllipsis};
175222
}
176223
`;
224+
225+
const SearchInput = styled(InputGroup.Input)`
226+
border: 0;
227+
box-shadow: unset;
228+
color: inherit;
229+
`;
230+
231+
const NoAttributesMessage = styled('div')`
232+
display: flex;
233+
justify-content: center;
234+
align-items: center;
235+
margin-top: ${space(4)};
236+
color: ${p => p.theme.subText};
237+
`;
238+
239+
const StyledInputGroup = styled(InputGroup)`
240+
@media (max-width: ${p => p.theme.breakpoints.medium}) {
241+
max-width: 175px;
242+
}
243+
`;

static/app/views/explore/components/schemaHintsList.spec.tsx

+24
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,28 @@ describe('SchemaHintsList', () => {
229229

230230
expect(screen.getByLabelText('Schema Hints Drawer')).toBeInTheDocument();
231231
});
232+
233+
it('should show correct search results when query is updated', async () => {
234+
render(
235+
<PageParamsProvider>
236+
<SchemaHintsList
237+
stringTags={mockStringTags}
238+
numberTags={mockNumberTags}
239+
supportedAggregates={[]}
240+
/>
241+
</PageParamsProvider>,
242+
{organization, router}
243+
);
244+
245+
const seeFullList = screen.getByText('See full list');
246+
await userEvent.click(seeFullList);
247+
248+
const searchInput = screen.getByLabelText('Search attributes');
249+
await userEvent.type(searchInput, 'stringTag');
250+
251+
expect(screen.getByText('stringTag1')).toBeInTheDocument();
252+
expect(screen.getByText('stringTag2')).toBeInTheDocument();
253+
expect(screen.queryByText('numberTag1')).not.toBeInTheDocument();
254+
expect(screen.queryByText('numberTag2')).not.toBeInTheDocument();
255+
});
232256
});

static/app/views/explore/components/schemaHintsList.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ function SchemaHintsList({
9797
}
9898

9999
const container = schemaHintsContainerRef.current;
100-
const containerRect = container.getBoundingClientRect();
101100

102101
// Create a temporary div to measure items without rendering them
103102
const measureDiv = document.createElement('div');
@@ -124,11 +123,12 @@ function SchemaHintsList({
124123
Array.from(measureDiv.children).length - 1
125124
]?.getBoundingClientRect();
126125

126+
const measureDivRect = measureDiv.getBoundingClientRect();
127127
// Find the last item that fits within the container
128128
let lastVisibleIndex =
129129
items.findIndex(item => {
130130
const itemRect = item.getBoundingClientRect();
131-
return itemRect.right > containerRect.right - (seeFullListTagRect?.width ?? 0);
131+
return itemRect.right > measureDivRect.right - (seeFullListTagRect?.width ?? 0);
132132
}) - 1;
133133

134134
// If all items fit, show them all

0 commit comments

Comments
 (0)