123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- import {useMemo} from 'react';
- import {useSortable} from '@dnd-kit/sortable';
- import {CSS} from '@dnd-kit/utilities';
- import styled from '@emotion/styled';
- import {Button} from 'sentry/components/button';
- import type {SelectKey, SelectOption} from 'sentry/components/compactSelect';
- import {CompactSelect} from 'sentry/components/compactSelect';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconAdd} from 'sentry/icons/iconAdd';
- import {IconDelete} from 'sentry/icons/iconDelete';
- import {IconGrabbable} from 'sentry/icons/iconGrabbable';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {defined} from 'sentry/utils';
- import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
- import {DragNDropContext} from '../contexts/dragNDropContext';
- import {useSpanTags} from '../contexts/spanTagsContext';
- import type {Column} from '../hooks/useDragNDropColumns';
- import {useResultMode} from '../hooks/useResultsMode';
- import {ToolbarHeader, ToolbarHeaderButton, ToolbarLabel, ToolbarSection} from './styles';
- interface ToolbarGroupByProps {
- disabled?: boolean;
- }
- export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) {
- const tags = useSpanTags();
- const [resultMode] = useResultMode();
- const {groupBys, setGroupBys} = useGroupBys();
- const options: SelectOption<string>[] = useMemo(() => {
- // These options aren't known to exist on this project but it was inserted into
- // the group bys somehow so it should be a valid options in the group bys.
- //
- // One place this may come from is when switching projects/environment/date range,
- // a tag may disappear based on the selection.
- const unknownOptions = groupBys
- .filter(groupBy => groupBy && !tags.hasOwnProperty(groupBy))
- .map(groupBy => {
- return {
- label: groupBy,
- value: groupBy,
- textValue: groupBy,
- };
- });
- const knownOptions = Object.keys(tags).map(tagKey => {
- return {
- label: tagKey,
- value: tagKey,
- textValue: tagKey,
- };
- });
- return [
- // hard code in an empty option
- {label: t('None'), value: '', textValue: t('none')},
- ...unknownOptions,
- ...knownOptions,
- ];
- }, [groupBys, tags]);
- return (
- <DragNDropContext columns={groupBys} setColumns={setGroupBys}>
- {({editableColumns, insertColumn, updateColumnAtIndex, deleteColumnAtIndex}) => (
- <ToolbarSection data-test-id="section-group-by">
- <StyledToolbarHeader>
- <Tooltip
- position="right"
- title={t(
- 'Aggregated data by a key attribute to calculate averages, percentiles, count and more'
- )}
- >
- <ToolbarLabel disabled={disabled}>{t('Group By')}</ToolbarLabel>
- </Tooltip>
- <ToolbarHeaderButton
- disabled={disabled}
- size="zero"
- onClick={insertColumn}
- borderless
- aria-label={t('Add Group')}
- icon={<IconAdd />}
- />
- </StyledToolbarHeader>
- {editableColumns.map((column, i) => (
- <ColumnEditorRow
- disabled={resultMode === 'samples'}
- key={column.id}
- canDelete={
- editableColumns.length > 1 || !['', undefined].includes(column.column)
- }
- column={column}
- options={options}
- onColumnChange={c => updateColumnAtIndex(i, c)}
- onColumnDelete={() => deleteColumnAtIndex(i)}
- />
- ))}
- </ToolbarSection>
- )}
- </DragNDropContext>
- );
- }
- const StyledToolbarHeader = styled(ToolbarHeader)`
- margin-bottom: ${space(1)};
- `;
- interface ColumnEditorRowProps {
- canDelete: boolean;
- column: Column;
- onColumnChange: (column: string) => void;
- onColumnDelete: () => void;
- options: SelectOption<string>[];
- disabled?: boolean;
- }
- function ColumnEditorRow({
- canDelete,
- column,
- options,
- onColumnChange,
- onColumnDelete,
- disabled = false,
- }: ColumnEditorRowProps) {
- const {attributes, listeners, setNodeRef, transform, transition} = useSortable({
- id: column.id,
- });
- function handleColumnChange(option: SelectOption<SelectKey>) {
- if (defined(option) && typeof option.value === 'string') {
- onColumnChange(option.value);
- }
- }
- const label = useMemo(() => {
- const tag = options.find(option => option.value === column.column);
- return <TriggerLabel>{tag?.label ?? t('None')}</TriggerLabel>;
- }, [column.column, options]);
- return (
- <RowContainer
- key={column.id}
- ref={setNodeRef}
- style={{
- transform: CSS.Transform.toString(transform),
- transition,
- }}
- {...attributes}
- >
- <Button
- aria-label={t('Drag to reorder')}
- borderless
- size="zero"
- disabled={disabled}
- icon={<IconGrabbable size="sm" />}
- {...listeners}
- />
- <StyledCompactSelect
- data-test-id="editor-column"
- options={options}
- triggerLabel={label}
- disabled={disabled}
- value={column.column ?? ''}
- onChange={handleColumnChange}
- searchable
- triggerProps={{
- style: {
- width: '100%',
- },
- }}
- />
- <Button
- aria-label={t('Remove Column')}
- borderless
- disabled={!canDelete || disabled}
- size="zero"
- icon={<IconDelete size="sm" />}
- onClick={() => onColumnDelete()}
- />
- </RowContainer>
- );
- }
- const RowContainer = styled('div')`
- display: flex;
- flex-direction: row;
- gap: ${space(0.5)};
- :not(:first-child) {
- margin-top: ${space(1)};
- }
- `;
- const StyledCompactSelect = styled(CompactSelect)`
- flex-grow: 1;
- min-width: 0;
- `;
- const TriggerLabel = styled('span')`
- ${p => p.theme.overflowEllipsis}
- text-align: left;
- line-height: normal;
- position: relative;
- font-weight: normal;
- `;
|