index.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import styled from '@emotion/styled';
  2. import ButtonBar from 'sentry/components/buttonBar';
  3. import {RadioLineItem} from 'sentry/components/forms/controls/radioGroup';
  4. import FieldGroup from 'sentry/components/forms/fieldGroup';
  5. import Radio from 'sentry/components/radio';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {TagCollection} from 'sentry/types/group';
  9. import type {QueryFieldValue} from 'sentry/utils/discover/fields';
  10. import useCustomMeasurements from 'sentry/utils/useCustomMeasurements';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
  13. import type {Widget} from 'sentry/views/dashboards/types';
  14. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  15. import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
  16. import {addIncompatibleFunctions} from 'sentry/views/dashboards/widgetBuilder/utils';
  17. import {QueryField} from 'sentry/views/discover/table/queryField';
  18. import {FieldValueKind} from 'sentry/views/discover/table/types';
  19. import {AddButton} from './addButton';
  20. import {DeleteButton} from './deleteButton';
  21. export const MAX_NUM_Y_AXES = 3;
  22. interface Props {
  23. aggregates: QueryFieldValue[];
  24. displayType: DisplayType;
  25. /**
  26. * Fired when aggregates are added/removed/modified/reordered.
  27. */
  28. onChange: (aggregates: QueryFieldValue[], selectedAggregate?: number) => void;
  29. tags: TagCollection;
  30. widgetType: Widget['widgetType'];
  31. errors?: Record<string, any>;
  32. noFieldsMessage?: string;
  33. selectedAggregate?: number;
  34. }
  35. export function YAxisSelector({
  36. displayType,
  37. widgetType,
  38. aggregates,
  39. tags,
  40. onChange,
  41. errors,
  42. noFieldsMessage,
  43. selectedAggregate,
  44. }: Props) {
  45. const organization = useOrganization();
  46. const datasetConfig = getDatasetConfig(widgetType);
  47. const {customMeasurements} = useCustomMeasurements();
  48. function handleAddFields(event: React.MouseEvent) {
  49. event.preventDefault();
  50. const newAggregates = [
  51. ...aggregates,
  52. {kind: FieldValueKind.FIELD, field: ''} as QueryFieldValue,
  53. ];
  54. if (displayType === DisplayType.BIG_NUMBER) {
  55. onChange(newAggregates, newAggregates.length - 1);
  56. } else {
  57. onChange(newAggregates);
  58. }
  59. }
  60. function handleAddEquation(event: React.MouseEvent) {
  61. event.preventDefault();
  62. const newAggregates = [
  63. ...aggregates,
  64. {kind: FieldValueKind.EQUATION, field: ''} as QueryFieldValue,
  65. ];
  66. if (displayType === DisplayType.BIG_NUMBER) {
  67. const newSelectedAggregate = newAggregates.length - 1;
  68. onChange(newAggregates, newSelectedAggregate);
  69. } else {
  70. onChange(newAggregates);
  71. }
  72. }
  73. function handleRemoveQueryField(event: React.MouseEvent, fieldIndex: number) {
  74. event.preventDefault();
  75. const newAggregates = [...aggregates];
  76. newAggregates.splice(fieldIndex, 1);
  77. if (displayType === DisplayType.BIG_NUMBER) {
  78. const newSelectedAggregate = newAggregates.length - 1;
  79. onChange(newAggregates, newSelectedAggregate);
  80. } else {
  81. onChange(newAggregates);
  82. }
  83. }
  84. function handleChangeQueryField(value: QueryFieldValue, fieldIndex: number) {
  85. const newAggregates = [...aggregates];
  86. newAggregates[fieldIndex] = value;
  87. onChange(newAggregates);
  88. }
  89. function handleSelectField(newSelectedAggregate: number) {
  90. onChange(aggregates, newSelectedAggregate);
  91. }
  92. const fieldError = errors?.find((error: any) => error?.aggregates)?.aggregates;
  93. const canDelete = aggregates.length > 1;
  94. const hideAddYAxisButtons =
  95. ([DisplayType.LINE, DisplayType.AREA, DisplayType.BAR].includes(displayType) &&
  96. aggregates.length === MAX_NUM_Y_AXES) ||
  97. (displayType === DisplayType.BIG_NUMBER && widgetType === WidgetType.RELEASE);
  98. let injectedFunctions: Set<string> = new Set();
  99. const fieldOptions = datasetConfig.getTableFieldOptions(
  100. organization,
  101. tags,
  102. customMeasurements
  103. );
  104. // We need to persist the form values across Errors and Transactions datasets
  105. // for the discover dataset split, so functions that are not compatible with
  106. // errors should still appear in the field options to gracefully handle incorrect
  107. // dataset splitting.
  108. if (
  109. hasDatasetSelector(organization) &&
  110. widgetType &&
  111. [WidgetType.ERRORS, WidgetType.TRANSACTIONS].includes(widgetType)
  112. ) {
  113. injectedFunctions = addIncompatibleFunctions(aggregates, fieldOptions);
  114. }
  115. return (
  116. <FieldGroup inline={false} flexibleControlStateSize error={fieldError} stacked>
  117. {aggregates.map((fieldValue, i) => (
  118. <QueryFieldWrapper key={`${fieldValue}:${i}`}>
  119. {aggregates.length > 1 && displayType === DisplayType.BIG_NUMBER && (
  120. <RadioLineItem index={i} role="radio" aria-label="aggregate-selector">
  121. <Radio
  122. checked={i === selectedAggregate}
  123. onChange={() => handleSelectField(i)}
  124. aria-label={'field' + i}
  125. />
  126. </RadioLineItem>
  127. )}
  128. <QueryField
  129. fieldValue={fieldValue}
  130. fieldOptions={fieldOptions}
  131. onChange={value => handleChangeQueryField(value, i)}
  132. filterPrimaryOptions={option =>
  133. datasetConfig.filterYAxisOptions?.(displayType)(option) ||
  134. injectedFunctions.has(`${option.value.kind}:${option.value.meta.name}`)
  135. }
  136. filterAggregateParameters={datasetConfig.filterYAxisAggregateParams?.(
  137. fieldValue,
  138. displayType
  139. )}
  140. otherColumns={aggregates}
  141. noFieldsMessage={noFieldsMessage}
  142. />
  143. {aggregates.length > 1 &&
  144. (canDelete || fieldValue.kind === FieldValueKind.EQUATION) && (
  145. <DeleteButton onDelete={event => handleRemoveQueryField(event, i)} />
  146. )}
  147. </QueryFieldWrapper>
  148. ))}
  149. {!hideAddYAxisButtons && (
  150. <Actions gap={1}>
  151. <AddButton
  152. title={
  153. displayType === DisplayType.BIG_NUMBER ? t('Add Field') : t('Add Series')
  154. }
  155. onAdd={handleAddFields}
  156. />
  157. {datasetConfig.enableEquations && (
  158. <AddButton title={t('Add an Equation')} onAdd={handleAddEquation} />
  159. )}
  160. </Actions>
  161. )}
  162. </FieldGroup>
  163. );
  164. }
  165. const QueryFieldWrapper = styled('div')`
  166. display: flex;
  167. align-items: center;
  168. justify-content: space-between;
  169. :not(:last-child) {
  170. margin-bottom: ${space(1)};
  171. }
  172. > * + * {
  173. margin-left: ${space(1)};
  174. }
  175. `;
  176. const Actions = styled(ButtonBar)`
  177. justify-content: flex-start;
  178. `;