toolbarGroupBy.tsx 6.5 KB

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