intervalSelector.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import {getInterval} from 'sentry/components/charts/utils';
  2. import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
  3. import autoCompleteFilter from 'sentry/components/dropdownAutoComplete/autoCompleteFilter';
  4. import DropdownButton from 'sentry/components/dropdownButton';
  5. import {
  6. _timeRangeAutoCompleteFilter,
  7. makeItem,
  8. } from 'sentry/components/organizations/timeRangeSelector/utils';
  9. import {t, tn} from 'sentry/locale';
  10. import {parsePeriodToHours} from 'sentry/utils/dates';
  11. import EventView from 'sentry/utils/discover/eventView';
  12. import {INTERVAL_DISPLAY_MODES} from 'sentry/utils/discover/types';
  13. type IntervalUnits = 's' | 'm' | 'h' | 'd';
  14. type RelativeUnitsMapping = {
  15. [Unit: string]: {
  16. convertToDaysMultiplier: number;
  17. label: (num: number) => string;
  18. momentUnit: moment.unitOfTime.DurationConstructor;
  19. searchKey: string;
  20. };
  21. };
  22. const SUPPORTED_RELATIVE_PERIOD_UNITS: RelativeUnitsMapping = {
  23. s: {
  24. label: (num: number) => tn('Second', '%s seconds', num),
  25. searchKey: t('seconds'),
  26. momentUnit: 'seconds',
  27. convertToDaysMultiplier: 1 / (60 * 60 * 24),
  28. },
  29. m: {
  30. label: (num: number) => tn('Minute', '%s minutes', num),
  31. searchKey: t('minutes'),
  32. momentUnit: 'minutes',
  33. convertToDaysMultiplier: 1 / (60 * 24),
  34. },
  35. h: {
  36. label: (num: number) => tn('Hour', '%s hours', num),
  37. searchKey: t('hours'),
  38. momentUnit: 'hours',
  39. convertToDaysMultiplier: 1 / 24,
  40. },
  41. d: {
  42. label: (num: number) => tn('Day', '%s days', num),
  43. searchKey: t('days'),
  44. momentUnit: 'days',
  45. convertToDaysMultiplier: 1,
  46. },
  47. };
  48. const SUPPORTED_RELATIVE_UNITS_LIST = Object.keys(
  49. SUPPORTED_RELATIVE_PERIOD_UNITS
  50. ) as IntervalUnits[];
  51. type Props = {
  52. displayMode: string;
  53. eventView: EventView;
  54. onIntervalChange: (value: string | undefined) => void;
  55. };
  56. type IntervalOption = {
  57. default: string; // The default interval if we go out of bounds
  58. min: number; // The smallest allowed interval in hours, max is implicitly 1/2 of the time range
  59. options: string[]; // The dropdown options
  60. rangeStart: number; // The minimum bound of the time range in hours, options should be in order largest to smallest
  61. };
  62. const INTERVAL_OPTIONS: IntervalOption[] = [
  63. {
  64. rangeStart: 90 * 24,
  65. min: 1,
  66. default: '4h',
  67. options: ['1h', '4h', '1d', '5d'],
  68. },
  69. {
  70. rangeStart: 30 * 24,
  71. min: 0.5,
  72. default: '1h',
  73. options: ['30m', '1h', '4h', '1d', '5d'],
  74. },
  75. {
  76. rangeStart: 14 * 24,
  77. min: 1 / 6,
  78. default: '30m',
  79. options: ['30m', '1h', '4h', '1d'],
  80. },
  81. {
  82. rangeStart: 7 * 24,
  83. min: 1 / 20,
  84. default: '30m',
  85. options: ['30m', '1h', '4h', '1d'],
  86. },
  87. {
  88. rangeStart: 1 * 24, // 1 day
  89. min: 1 / 60,
  90. default: '5m',
  91. options: ['5m', '15m', '1h'],
  92. },
  93. {
  94. rangeStart: 1, // 1 hour
  95. min: 1 / 3600,
  96. default: '1m',
  97. options: ['1m', '5m', '15m'],
  98. },
  99. {
  100. rangeStart: 1 / 60, // 1 minute
  101. min: 1 / 3600,
  102. default: '1s',
  103. options: ['1s', '5s', '30s'],
  104. },
  105. ];
  106. function formatHoursToInterval(hours: number): [number, IntervalUnits] {
  107. if (hours >= 24) {
  108. return [hours / 24, 'd'];
  109. }
  110. if (hours >= 1) {
  111. return [hours, 'h'];
  112. }
  113. return [hours * 60, 'm'];
  114. }
  115. function getIntervalOption(rangeHours: number): IntervalOption {
  116. for (const index in INTERVAL_OPTIONS) {
  117. const currentOption = INTERVAL_OPTIONS[index];
  118. if (currentOption.rangeStart <= rangeHours) {
  119. return currentOption;
  120. }
  121. }
  122. return INTERVAL_OPTIONS[0];
  123. }
  124. function bindInterval(
  125. rangeHours: number,
  126. intervalHours: number,
  127. intervalOption: IntervalOption
  128. ): boolean {
  129. // If the interval is out of bounds for time range reset it to the default
  130. // Bounds are either option.min or half the current
  131. const optionMax = rangeHours / 2;
  132. return intervalHours < intervalOption.min || intervalHours > optionMax;
  133. }
  134. export default function IntervalSelector({
  135. displayMode,
  136. eventView,
  137. onIntervalChange,
  138. }: Props) {
  139. if (!INTERVAL_DISPLAY_MODES.includes(displayMode)) {
  140. return null;
  141. }
  142. // Get the interval from the eventView if one was set, otherwise determine what the default is
  143. // TODO: use the INTERVAL_OPTIONS default instead
  144. // Can't just do usingDefaultInterval ? ... : ...; here cause the type of interval will include undefined
  145. const defaultInterval = getInterval(eventView.getPageFilters().datetime, 'high');
  146. const interval = eventView.interval || defaultInterval;
  147. const usingDefaultInterval =
  148. eventView.interval === undefined || interval === defaultInterval;
  149. const rangeHours = eventView.getDays() * 24;
  150. const intervalHours = parsePeriodToHours(interval);
  151. // Determine the applicable interval option
  152. const intervalOption = getIntervalOption(rangeHours);
  153. // Only bind the interval if we're not using the default
  154. if (!usingDefaultInterval) {
  155. if (bindInterval(rangeHours, intervalHours, intervalOption)) {
  156. onIntervalChange(defaultInterval);
  157. }
  158. }
  159. const intervalAutoComplete: typeof autoCompleteFilter = function (items, filterValue) {
  160. let newItem: number | undefined = undefined;
  161. const results = _timeRangeAutoCompleteFilter(items, filterValue, {
  162. supportedPeriods: SUPPORTED_RELATIVE_PERIOD_UNITS,
  163. supportedUnits: SUPPORTED_RELATIVE_UNITS_LIST,
  164. });
  165. const filteredResults = results.filter(item => {
  166. const itemHours = parsePeriodToHours(item.value);
  167. if (itemHours < intervalOption.min) {
  168. newItem = intervalOption.min;
  169. } else if (itemHours > rangeHours / 2) {
  170. newItem = rangeHours / 2;
  171. } else {
  172. return true;
  173. }
  174. return false;
  175. });
  176. if (newItem) {
  177. const [amount, unit] = formatHoursToInterval(newItem);
  178. filteredResults.push(
  179. makeItem(
  180. amount,
  181. unit,
  182. SUPPORTED_RELATIVE_PERIOD_UNITS[unit].label,
  183. results.length + 1
  184. )
  185. );
  186. }
  187. return filteredResults;
  188. };
  189. return (
  190. <DropdownAutoComplete
  191. onSelect={item => onIntervalChange(item.value)}
  192. items={intervalOption.options.map(option => ({
  193. value: option,
  194. searchKey: option,
  195. label: option,
  196. }))}
  197. searchPlaceholder={t('Provide a time interval')}
  198. autoCompleteFilter={(items, filterValue) =>
  199. intervalAutoComplete(items, filterValue)
  200. }
  201. >
  202. {({isOpen}) => (
  203. <DropdownButton borderless prefix={t('Interval')} isOpen={isOpen}>
  204. {interval}
  205. </DropdownButton>
  206. )}
  207. </DropdownAutoComplete>
  208. );
  209. }