toolbarGroupBy.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import {useMemo} from 'react';
  2. import {useSortable} from '@dnd-kit/sortable';
  3. import {CSS} from '@dnd-kit/utilities';
  4. import styled from '@emotion/styled';
  5. import {Button} from 'sentry/components/button';
  6. import type {SelectKey, SelectOption} from 'sentry/components/compactSelect';
  7. import {CompactSelect} from 'sentry/components/compactSelect';
  8. import {Tooltip} from 'sentry/components/tooltip';
  9. import {IconAdd} from 'sentry/icons/iconAdd';
  10. import {IconDelete} from 'sentry/icons/iconDelete';
  11. import {IconGrabbable} from 'sentry/icons/iconGrabbable';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {defined} from 'sentry/utils';
  15. import {useGroupBys} from 'sentry/views/explore/hooks/useGroupBys';
  16. import {DragNDropContext} from '../contexts/dragNDropContext';
  17. import {useSpanTags} from '../contexts/spanTagsContext';
  18. import type {Column} from '../hooks/useDragNDropColumns';
  19. import {useResultMode} from '../hooks/useResultsMode';
  20. import {ToolbarHeader, ToolbarHeaderButton, ToolbarLabel, ToolbarSection} from './styles';
  21. interface ToolbarGroupByProps {
  22. disabled?: boolean;
  23. }
  24. export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) {
  25. const tags = useSpanTags();
  26. const [resultMode] = useResultMode();
  27. const {groupBys, setGroupBys} = useGroupBys();
  28. const options: SelectOption<string>[] = useMemo(() => {
  29. // These options aren't known to exist on this project but it was inserted into
  30. // the group bys somehow so it should be a valid options in the group bys.
  31. //
  32. // One place this may come from is when switching projects/environment/date range,
  33. // a tag may disappear based on the selection.
  34. const unknownOptions = groupBys
  35. .filter(groupBy => groupBy && !tags.hasOwnProperty(groupBy))
  36. .map(groupBy => {
  37. return {
  38. label: groupBy,
  39. value: groupBy,
  40. textValue: groupBy,
  41. };
  42. });
  43. const knownOptions = Object.keys(tags).map(tagKey => {
  44. return {
  45. label: tagKey,
  46. value: tagKey,
  47. textValue: tagKey,
  48. };
  49. });
  50. return [
  51. // hard code in an empty option
  52. {label: t('None'), value: '', textValue: t('none')},
  53. ...unknownOptions,
  54. ...knownOptions,
  55. ];
  56. }, [groupBys, tags]);
  57. return (
  58. <DragNDropContext columns={groupBys} setColumns={setGroupBys}>
  59. {({editableColumns, insertColumn, updateColumnAtIndex, deleteColumnAtIndex}) => (
  60. <ToolbarSection data-test-id="section-group-by">
  61. <StyledToolbarHeader>
  62. <Tooltip
  63. position="right"
  64. title={t(
  65. 'Aggregated data by a key attribute to calculate averages, percentiles, count and more'
  66. )}
  67. >
  68. <ToolbarLabel disabled={disabled}>{t('Group By')}</ToolbarLabel>
  69. </Tooltip>
  70. <ToolbarHeaderButton
  71. disabled={disabled}
  72. size="zero"
  73. onClick={insertColumn}
  74. borderless
  75. aria-label={t('Add Group')}
  76. icon={<IconAdd />}
  77. />
  78. </StyledToolbarHeader>
  79. {editableColumns.map((column, i) => (
  80. <ColumnEditorRow
  81. disabled={resultMode === 'samples'}
  82. key={column.id}
  83. canDelete={
  84. editableColumns.length > 1 || !['', undefined].includes(column.column)
  85. }
  86. column={column}
  87. options={options}
  88. onColumnChange={c => updateColumnAtIndex(i, c)}
  89. onColumnDelete={() => deleteColumnAtIndex(i)}
  90. />
  91. ))}
  92. </ToolbarSection>
  93. )}
  94. </DragNDropContext>
  95. );
  96. }
  97. const StyledToolbarHeader = styled(ToolbarHeader)`
  98. margin-bottom: ${space(1)};
  99. `;
  100. interface ColumnEditorRowProps {
  101. canDelete: boolean;
  102. column: Column;
  103. onColumnChange: (column: string) => void;
  104. onColumnDelete: () => void;
  105. options: SelectOption<string>[];
  106. disabled?: boolean;
  107. }
  108. function ColumnEditorRow({
  109. canDelete,
  110. column,
  111. options,
  112. onColumnChange,
  113. onColumnDelete,
  114. disabled = false,
  115. }: ColumnEditorRowProps) {
  116. const {attributes, listeners, setNodeRef, transform, transition} = useSortable({
  117. id: column.id,
  118. });
  119. function handleColumnChange(option: SelectOption<SelectKey>) {
  120. if (defined(option) && typeof option.value === 'string') {
  121. onColumnChange(option.value);
  122. }
  123. }
  124. const label = useMemo(() => {
  125. const tag = options.find(option => option.value === column.column);
  126. return <TriggerLabel>{tag?.label ?? t('None')}</TriggerLabel>;
  127. }, [column.column, options]);
  128. return (
  129. <RowContainer
  130. key={column.id}
  131. ref={setNodeRef}
  132. style={{
  133. transform: CSS.Transform.toString(transform),
  134. transition,
  135. }}
  136. {...attributes}
  137. >
  138. <Button
  139. aria-label={t('Drag to reorder')}
  140. borderless
  141. size="zero"
  142. disabled={disabled}
  143. icon={<IconGrabbable size="sm" />}
  144. {...listeners}
  145. />
  146. <StyledCompactSelect
  147. data-test-id="editor-column"
  148. options={options}
  149. triggerLabel={label}
  150. disabled={disabled}
  151. value={column.column ?? ''}
  152. onChange={handleColumnChange}
  153. searchable
  154. triggerProps={{
  155. style: {
  156. width: '100%',
  157. },
  158. }}
  159. />
  160. <Button
  161. aria-label={t('Remove Column')}
  162. borderless
  163. disabled={!canDelete || disabled}
  164. size="zero"
  165. icon={<IconDelete size="sm" />}
  166. onClick={() => onColumnDelete()}
  167. />
  168. </RowContainer>
  169. );
  170. }
  171. const RowContainer = styled('div')`
  172. display: flex;
  173. flex-direction: row;
  174. gap: ${space(0.5)};
  175. :not(:first-child) {
  176. margin-top: ${space(1)};
  177. }
  178. `;
  179. const StyledCompactSelect = styled(CompactSelect)`
  180. flex-grow: 1;
  181. min-width: 0;
  182. `;
  183. const TriggerLabel = styled('span')`
  184. ${p => p.theme.overflowEllipsis}
  185. text-align: left;
  186. line-height: normal;
  187. position: relative;
  188. font-weight: normal;
  189. `;