Skip to content

Commit

Permalink
feat(explore): Add group by to explore toolbar (#76879)
Browse files Browse the repository at this point in the history
This adds a basic way for users to add group bys by selecting span
attributes.
  • Loading branch information
Zylphrex authored Sep 3, 2024
1 parent 448060a commit edd7bc3
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 18 deletions.
42 changes: 42 additions & 0 deletions static/app/views/explore/hooks/useGroupBys.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {createMemoryHistory, Route, Router, RouterContext} from 'react-router';

import {act, render} from 'sentry-test/reactTestingLibrary';

import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
import {RouteContext} from 'sentry/views/routeContext';

describe('useGroupBys', function () {
it('allows changing group bys', function () {
let groupBys, setGroupBys;

function TestPage() {
[groupBys, setGroupBys] = useGroupBys();
return null;
}

const memoryHistory = createMemoryHistory();

render(
<Router
history={memoryHistory}
render={props => {
return (
<RouteContext.Provider value={props}>
<RouterContext {...props} />
</RouteContext.Provider>
);
}}
>
<Route path="/" component={TestPage} />
</Router>
);

expect(groupBys).toEqual(['']); // default

act(() => setGroupBys(['foo', 'bar']));
expect(groupBys).toEqual(['foo', 'bar']);

act(() => setGroupBys([]));
expect(groupBys).toEqual(['']); // default
});
});
50 changes: 50 additions & 0 deletions static/app/views/explore/hooks/useGroupBys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {useCallback, useMemo} from 'react';
import type {Location} from 'history';

import {decodeList} from 'sentry/utils/queryString';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import type {Field} from 'sentry/views/explore/hooks/useSampleFields';

interface Options {
location: Location;
navigate: ReturnType<typeof useNavigate>;
}

export function useGroupBys(): [Field[], (fields: Field[]) => void] {
const location = useLocation();
const navigate = useNavigate();
const options = {location, navigate};

return useGroupBysImpl(options);
}

function useGroupBysImpl({
location,
navigate,
}: Options): [Field[], (fields: Field[]) => void] {
const groupBys = useMemo(() => {
const rawGroupBys = decodeList(location.query.groupBy);

if (rawGroupBys.length) {
return rawGroupBys;
}

return [''];
}, [location.query.groupBy]);

const setGroupBys = useCallback(
(newGroupBys: Field[]) => {
navigate({
...location,
query: {
...location.query,
groupBy: newGroupBys,
},
});
},
[location, navigate]
);

return [groupBys, setGroupBys];
}
39 changes: 39 additions & 0 deletions static/app/views/explore/toolbar/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {createMemoryHistory, Route, Router, RouterContext} from 'react-router';

import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';

import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
import {useResultMode} from 'sentry/views/explore/hooks/useResultsMode';
import {useSampleFields} from 'sentry/views/explore/hooks/useSampleFields';
import {useSorts} from 'sentry/views/explore/hooks/useSorts';
Expand Down Expand Up @@ -116,4 +117,42 @@ describe('ExploreToolbar', function () {
expect(within(section).getByRole('button', {name: 'Ascending'})).toBeInTheDocument();
expect(sorts).toEqual([{field: 'span.op', kind: 'asc'}]);
});

it('allows changing group bys', async function () {
let groupBys;

function Component() {
[groupBys] = useGroupBys();
return <ExploreToolbar />;
}
renderWithRouter(Component);

const section = screen.getByTestId('section-group-by');

expect(within(section).getByRole('button', {name: '(none)'})).toBeInTheDocument();
expect(groupBys).toEqual(['']);

await userEvent.click(within(section).getByRole('button', {name: '(none)'}));
const groupByOptions1 = await within(section).findAllByRole('option');
expect(groupByOptions1.length).toBeGreaterThan(0);

await userEvent.click(within(section).getByRole('option', {name: 'span.op'}));
expect(within(section).getByRole('button', {name: 'span.op'})).toBeInTheDocument();
expect(groupBys).toEqual(['span.op']);

await userEvent.click(within(section).getByRole('button', {name: '+Add Group By'}));
expect(groupBys).toEqual(['span.op', '']);

await userEvent.click(within(section).getByRole('button', {name: '(none)'}));
const groupByOptions2 = await within(section).findAllByRole('option');
expect(groupByOptions2.length).toBeGreaterThan(0);

await userEvent.click(
within(section).getByRole('option', {name: 'span.description'})
);
expect(
within(section).getByRole('button', {name: 'span.description'})
).toBeInTheDocument();
expect(groupBys).toEqual(['span.op', 'span.description']);
});
});
2 changes: 1 addition & 1 deletion static/app/views/explore/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function ExploreToolbar({extras}: ExploreToolbarProps) {
<ToolbarVisualize visualize={visualize} setVisualize={setVisualize} />
<ToolbarSortBy fields={sampleFields} sorts={sorts} setSorts={setSorts} />
<ToolbarLimitTo />
<ToolbarGroupBy disabled />
<ToolbarGroupBy />
</div>
);
}
12 changes: 12 additions & 0 deletions static/app/views/explore/toolbar/styles.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import styled from '@emotion/styled';

import {Button} from 'sentry/components/button';
import {space} from 'sentry/styles/space';

export const ToolbarSection = styled('div')`
margin-bottom: ${space(2)};
`;

export const ToolbarHeader = styled('div')`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
`;

export const ToolbarHeading = styled('h6')<{disabled?: boolean}>`
color: ${p => (p.disabled ? p.theme.gray300 : p.theme.purple300)};
height: ${p => p.theme.form.md.height};
Expand All @@ -16,3 +24,7 @@ export const ToolbarHeading = styled('h6')<{disabled?: boolean}>`
${p => (p.disabled ? p.theme.gray300 : p.theme.purple300)};
margin: 0 0 ${space(1)} 0;
`;

export const ToolbarHeaderButton = styled(Button)<{disabled?: boolean}>`
color: ${p => (p.disabled ? p.theme.gray300 : p.theme.purple300)};
`;
6 changes: 4 additions & 2 deletions static/app/views/explore/toolbar/toolbarDataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {SegmentedControl} from 'sentry/components/segmentedControl';
import {t} from 'sentry/locale';
import {DiscoverDatasets} from 'sentry/utils/discover/types';

import {ToolbarHeading, ToolbarSection} from './styles';
import {ToolbarHeader, ToolbarHeading, ToolbarSection} from './styles';

interface ToolbarDatasetProps {
dataset: DiscoverDatasets;
Expand All @@ -12,7 +12,9 @@ interface ToolbarDatasetProps {
export function ToolbarDataset({dataset, setDataset}: ToolbarDatasetProps) {
return (
<ToolbarSection data-test-id="section-dataset">
<ToolbarHeading>{t('Dataset')}</ToolbarHeading>
<ToolbarHeader>
<ToolbarHeading>{t('Dataset')}</ToolbarHeading>
</ToolbarHeader>
<SegmentedControl aria-label={t('Dataset')} value={dataset} onChange={setDataset}>
<SegmentedControl.Item key={DiscoverDatasets.SPANS_INDEXED}>
{t('Indexed Spans')}
Expand Down
67 changes: 60 additions & 7 deletions static/app/views/explore/toolbar/toolbarGroupBy.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,68 @@
import {useCallback, useMemo} from 'react';

import type {SelectOption} from 'sentry/components/compactSelect';
import {CompactSelect} from 'sentry/components/compactSelect';
import {t} from 'sentry/locale';
import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
import type {Field} from 'sentry/views/explore/hooks/useSampleFields';
import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags';

import {ToolbarHeading, ToolbarSection} from './styles';
import {
ToolbarHeader,
ToolbarHeaderButton,
ToolbarHeading,
ToolbarSection,
} from './styles';

interface ToolbarGroupByProps {
disabled?: boolean;
}
export function ToolbarGroupBy() {
// TODO: This should be loaded from context to avoid loading tags twice.
const tags = useSpanFieldSupportedTags();

const [groupBys, setGroupBys] = useGroupBys();

const options: SelectOption<Field>[] = useMemo(() => {
return [
// hard code in an empty option
{label: t('(none)'), value: ''},
...Object.entries(tags).map(([tagKey, tag]) => {
return {
label: tag.name,
value: tagKey,
};
}),
];
}, [tags]);

const addGroupBy = useCallback(() => {
setGroupBys([...groupBys, '']);
}, [setGroupBys, groupBys]);

const setGroupBy = useCallback(
(i: number, {value}: SelectOption<Field>) => {
const newGroupBys = groupBys.slice();
newGroupBys[i] = value;
setGroupBys(newGroupBys);
},
[setGroupBys, groupBys]
);

export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) {
return (
<ToolbarSection>
<ToolbarHeading disabled={disabled}>{t('Group By')}</ToolbarHeading>
<ToolbarSection data-test-id="section-group-by">
<ToolbarHeader>
<ToolbarHeading>{t('Group By')}</ToolbarHeading>
<ToolbarHeaderButton size="xs" onClick={addGroupBy} borderless>
{t('+Add Group By')}
</ToolbarHeaderButton>
</ToolbarHeader>
{groupBys.map((groupBy, index) => (
<CompactSelect
key={index}
size="md"
options={options}
value={groupBy}
onChange={newGroupBy => setGroupBy(index, newGroupBy)}
/>
))}
</ToolbarSection>
);
}
6 changes: 4 additions & 2 deletions static/app/views/explore/toolbar/toolbarLimitTo.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {t} from 'sentry/locale';

import {ToolbarHeading, ToolbarSection} from './styles';
import {ToolbarHeader, ToolbarHeading, ToolbarSection} from './styles';

interface ToolbarLimitToProps {}

export function ToolbarLimitTo({}: ToolbarLimitToProps) {
return (
<ToolbarSection>
<ToolbarHeading>{t('Limit To')}</ToolbarHeading>
<ToolbarHeader>
<ToolbarHeading>{t('Limit To')}</ToolbarHeading>
</ToolbarHeader>
</ToolbarSection>
);
}
6 changes: 4 additions & 2 deletions static/app/views/explore/toolbar/toolbarResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {SegmentedControl} from 'sentry/components/segmentedControl';
import {t} from 'sentry/locale';
import type {ResultMode} from 'sentry/views/explore/hooks/useResultsMode';

import {ToolbarHeading, ToolbarSection} from './styles';
import {ToolbarHeader, ToolbarHeading, ToolbarSection} from './styles';

interface ToolbarResultsProps {
resultMode: ResultMode;
Expand All @@ -12,7 +12,9 @@ interface ToolbarResultsProps {
export function ToolbarResults({resultMode, setResultMode}: ToolbarResultsProps) {
return (
<ToolbarSection data-test-id="section-result-mode">
<ToolbarHeading>{t('Results')}</ToolbarHeading>
<ToolbarHeader>
<ToolbarHeading>{t('Results')}</ToolbarHeading>
</ToolbarHeader>
<SegmentedControl
aria-label={t('Result Mode')}
value={resultMode}
Expand Down
6 changes: 4 additions & 2 deletions static/app/views/explore/toolbar/toolbarSortBy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {t} from 'sentry/locale';
import type {Sort} from 'sentry/utils/discover/fields';
import type {Field} from 'sentry/views/explore/hooks/useSampleFields';

import {ToolbarHeading, ToolbarSection} from './styles';
import {ToolbarHeader, ToolbarHeading, ToolbarSection} from './styles';

interface ToolbarSortByProps {
fields: Field[];
Expand Down Expand Up @@ -68,7 +68,9 @@ export function ToolbarSortBy({fields, setSorts, sorts}: ToolbarSortByProps) {

return (
<ToolbarSection data-test-id="section-sort-by">
<ToolbarHeading>{t('Sort By')}</ToolbarHeading>
<ToolbarHeader>
<ToolbarHeading>{t('Sort By')}</ToolbarHeading>
</ToolbarHeader>
<ToolbarContent>
<CompactSelect
size="md"
Expand Down
6 changes: 4 additions & 2 deletions static/app/views/explore/toolbar/toolbarVisualize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ALLOWED_VISUALIZE_FIELDS,
} from '../hooks/useVisualize';

import {ToolbarHeading, ToolbarSection} from './styles';
import {ToolbarHeader, ToolbarHeading, ToolbarSection} from './styles';

interface ToolbarVisualizeProps {
setVisualize: (visualize: string) => void;
Expand Down Expand Up @@ -43,7 +43,9 @@ export function ToolbarVisualize({visualize, setVisualize}: ToolbarVisualizeProp

return (
<ToolbarSection data-test-id="section-visualize">
<ToolbarHeading>{t('Visualize')}</ToolbarHeading>
<ToolbarHeader>
<ToolbarHeading>{t('Visualize')}</ToolbarHeading>
</ToolbarHeader>
<ToolbarContent>
<CompactSelect
size="md"
Expand Down

0 comments on commit edd7bc3

Please sign in to comment.