index.tsx 5.2 KB

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