toolbarGroupBy.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  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 {defined} from 'sentry/utils';
  14. import {
  15. useExploreGroupBys,
  16. useExploreMode,
  17. useSetExploreGroupBys,
  18. } from 'sentry/views/explore/contexts/pageParamsContext';
  19. import {UNGROUPED} from 'sentry/views/explore/contexts/pageParamsContext/groupBys';
  20. import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
  21. import {DragNDropContext} from '../contexts/dragNDropContext';
  22. import {useSpanTags} from '../contexts/spanTagsContext';
  23. import type {Column} from '../hooks/useDragNDropColumns';
  24. import {
  25. ToolbarHeader,
  26. ToolbarHeaderButton,
  27. ToolbarLabel,
  28. ToolbarRow,
  29. ToolbarSection,
  30. } from './styles';
  31. interface ToolbarGroupByProps {
  32. disabled?: boolean;
  33. }
  34. export function ToolbarGroupBy({disabled}: ToolbarGroupByProps) {
  35. const tags = useSpanTags();
  36. const mode = useExploreMode();
  37. const groupBys = useExploreGroupBys();
  38. const setGroupBys = useSetExploreGroupBys();
  39. const disabledOptions: Array<SelectOption<string>> = useMemo(() => {
  40. return [
  41. {
  42. label: <Disabled>{t('Samples not grouped')}</Disabled>,
  43. value: UNGROUPED,
  44. textValue: t('none'),
  45. },
  46. ];
  47. }, []);
  48. const enabledOptions: Array<SelectOption<string>> = useMemo(() => {
  49. const potentialOptions = [
  50. ...Object.keys(tags),
  51. // These options aren't known to exist on this project but it was inserted into
  52. // the group bys somehow so it should be a valid options in the group bys.
  53. //
  54. // One place this may come from is when switching projects/environment/date range,
  55. // a tag may disappear based on the selection.
  56. ...groupBys.filter(groupBy => groupBy && !tags.hasOwnProperty(groupBy)),
  57. ];
  58. potentialOptions.sort();
  59. return [
  60. // hard code in an empty option
  61. {
  62. label: <Disabled>{t('None')}</Disabled>,
  63. value: UNGROUPED,
  64. textValue: t('none'),
  65. },
  66. ...potentialOptions.map(key => ({
  67. label: key,
  68. value: key,
  69. textValue: key,
  70. })),
  71. ];
  72. }, [groupBys, tags]);
  73. return (
  74. <DragNDropContext columns={groupBys} setColumns={setGroupBys}>
  75. {({editableColumns, insertColumn, updateColumnAtIndex, deleteColumnAtIndex}) => {
  76. return (
  77. <ToolbarSection data-test-id="section-group-by">
  78. <ToolbarHeader>
  79. <Tooltip
  80. position="right"
  81. title={t(
  82. 'Aggregated data by a key attribute to calculate averages, percentiles, count and more'
  83. )}
  84. >
  85. <ToolbarLabel disabled={disabled}>{t('Group By')}</ToolbarLabel>
  86. </Tooltip>
  87. <Tooltip title={t('Add a new group')}>
  88. <ToolbarHeaderButton
  89. disabled={disabled}
  90. size="zero"
  91. onClick={insertColumn}
  92. borderless
  93. aria-label={t('Add Group')}
  94. icon={<IconAdd />}
  95. />
  96. </Tooltip>
  97. </ToolbarHeader>
  98. {disabled ? (
  99. <ColumnEditorRow
  100. disabled={mode === Mode.SAMPLES}
  101. canDelete={false}
  102. column={{id: 1, column: ''}}
  103. options={disabledOptions}
  104. onColumnChange={() => {}}
  105. onColumnDelete={() => {}}
  106. />
  107. ) : (
  108. editableColumns.map((column, i) => (
  109. <ColumnEditorRow
  110. disabled={mode === Mode.SAMPLES}
  111. key={column.id}
  112. canDelete={
  113. editableColumns.length > 1 || !['', undefined].includes(column.column)
  114. }
  115. column={column}
  116. options={enabledOptions}
  117. onColumnChange={c => updateColumnAtIndex(i, c)}
  118. onColumnDelete={() => deleteColumnAtIndex(i)}
  119. />
  120. ))
  121. )}
  122. </ToolbarSection>
  123. );
  124. }}
  125. </DragNDropContext>
  126. );
  127. }
  128. interface ColumnEditorRowProps {
  129. canDelete: boolean;
  130. column: Column;
  131. onColumnChange: (column: string) => void;
  132. onColumnDelete: () => void;
  133. options: Array<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. <ToolbarRow
  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. </ToolbarRow>
  197. );
  198. }
  199. const StyledCompactSelect = styled(CompactSelect)`
  200. flex-grow: 1;
  201. min-width: 0;
  202. `;
  203. const TriggerLabel = styled('span')`
  204. ${p => p.theme.overflowEllipsis}
  205. text-align: left;
  206. line-height: normal;
  207. position: relative;
  208. font-weight: normal;
  209. `;
  210. const Disabled = styled('span')`
  211. color: ${p => p.theme.gray300};
  212. `;