toolbarGroupBy.tsx 6.2 KB

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