timeSince.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
  2. import isNumber from 'lodash/isNumber';
  3. import isString from 'lodash/isString';
  4. import moment from 'moment-timezone';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {t} from 'sentry/locale';
  7. import ConfigStore from 'sentry/stores/configStore';
  8. import {getDuration} from 'sentry/utils/formatters';
  9. import getDynamicText from 'sentry/utils/getDynamicText';
  10. import {ColorOrAlias} from 'sentry/utils/theme';
  11. function getDateObj(date: RelaxedDateType): Date {
  12. return isString(date) || isNumber(date) ? new Date(date) : date;
  13. }
  14. type RelaxedDateType = string | number | Date;
  15. type UnitStyle = 'human' | 'regular' | 'short' | 'extraShort';
  16. interface Props extends React.TimeHTMLAttributes<HTMLTimeElement> {
  17. /**
  18. * The date value, can be string, number (e.g. timestamp), or instance of Date
  19. *
  20. * May be in the future
  21. */
  22. date: RelaxedDateType;
  23. /**
  24. * By default we show tooltip with absolute date on hover, this prop disables
  25. * that
  26. */
  27. disabledAbsoluteTooltip?: boolean;
  28. /**
  29. * Tooltip text to be hoverable when isTooltipHoverable is true
  30. */
  31. isTooltipHoverable?: boolean;
  32. /**
  33. * How often should the component live update the timestamp.
  34. *
  35. * You may specify a custom interval in milliseconds if necissary.
  36. *
  37. * @default minute
  38. */
  39. liveUpdateInterval?: 'minute' | 'second' | number;
  40. /**
  41. * Prefix before upcoming time (when the date is in the future)
  42. *
  43. * @default "in"
  44. */
  45. prefix?: string;
  46. /**
  47. * Suffix after elapsed time e.g. "ago" in "5 minutes ago"
  48. *
  49. * @default "ago"
  50. */
  51. suffix?: string;
  52. /**
  53. * Customize the tooltip content. This replaces the long form of the timestamp
  54. * completely.
  55. */
  56. tooltipBody?: React.ReactNode;
  57. /**
  58. * Prefix content to add to the tooltip. Useful to indicate what the relative
  59. * time is for
  60. */
  61. tooltipPrefix?: React.ReactNode;
  62. /**
  63. * Include seconds in the tooltip
  64. */
  65. tooltipShowSeconds?: boolean;
  66. /**
  67. * Suffix content to add to the tooltip. Useful to indicate what the relative
  68. * time is for
  69. */
  70. tooltipSuffix?: React.ReactNode;
  71. /**
  72. * Change the color of the underline
  73. */
  74. tooltipUnderlineColor?: ColorOrAlias;
  75. /**
  76. * How much text should be used for the suffix:
  77. *
  78. * human:
  79. * hour, minute, second. Uses 'human' fuzzy foormatting for values such as 'a
  80. * minute' or 'a few seconds'. (This is the default)
  81. *
  82. * regular:
  83. * Shows the full units (hours, minutes, seconds)
  84. *
  85. * short:
  86. * Like exact but uses shorter units (hr, min, sec)
  87. *
  88. * extraShort:
  89. * Like short but uses very short units (h, m, s)
  90. *
  91. * NOTE: shot and extraShort do NOT currently support times in the future.
  92. *
  93. * @default human
  94. */
  95. unitStyle?: UnitStyle;
  96. }
  97. function TimeSince({
  98. date,
  99. disabledAbsoluteTooltip,
  100. tooltipShowSeconds,
  101. tooltipPrefix: tooltipTitle,
  102. tooltipBody,
  103. tooltipSuffix,
  104. tooltipUnderlineColor,
  105. isTooltipHoverable = false,
  106. unitStyle,
  107. prefix = t('in'),
  108. suffix = t('ago'),
  109. liveUpdateInterval = 'minute',
  110. ...props
  111. }: Props) {
  112. const tickerRef = useRef<number | undefined>();
  113. const computeRelativeDate = useCallback(
  114. () => getRelativeDate(date, suffix, prefix, unitStyle),
  115. [date, suffix, prefix, unitStyle]
  116. );
  117. const [relative, setRelative] = useState<string>(computeRelativeDate());
  118. useEffect(() => {
  119. // Immediately update if props change
  120. setRelative(computeRelativeDate());
  121. const interval =
  122. liveUpdateInterval === 'minute'
  123. ? 60 * 1000
  124. : liveUpdateInterval === 'second'
  125. ? 1000
  126. : liveUpdateInterval;
  127. // Start a ticker to update the relative time
  128. tickerRef.current = window.setInterval(
  129. () => setRelative(computeRelativeDate()),
  130. interval
  131. );
  132. return () => window.clearInterval(tickerRef.current);
  133. }, [liveUpdateInterval, computeRelativeDate]);
  134. const dateObj = getDateObj(date);
  135. const user = ConfigStore.get('user');
  136. const options = user ? user.options : null;
  137. // Use short months when showing seconds, because "September" causes the
  138. // tooltip to overflow.
  139. const tooltipFormat = tooltipShowSeconds
  140. ? 'MMM D, YYYY h:mm:ss A z'
  141. : 'MMMM D, YYYY h:mm A z';
  142. const format = options?.clock24Hours ? 'MMMM D, YYYY HH:mm z' : tooltipFormat;
  143. const tooltip = getDynamicText({
  144. fixed: options?.clock24Hours
  145. ? 'November 3, 2020 08:57 UTC'
  146. : 'November 3, 2020 8:58 AM UTC',
  147. value: moment.tz(dateObj, options?.timezone ?? '').format(format),
  148. });
  149. return (
  150. <Tooltip
  151. disabled={disabledAbsoluteTooltip}
  152. underlineColor={tooltipUnderlineColor}
  153. showUnderline
  154. isHoverable={isTooltipHoverable}
  155. title={
  156. <Fragment>
  157. {tooltipTitle && <div>{tooltipTitle}</div>}
  158. {tooltipBody ?? tooltip}
  159. {tooltipSuffix && <div>{tooltipSuffix}</div>}
  160. </Fragment>
  161. }
  162. >
  163. <time dateTime={dateObj?.toISOString()} {...props}>
  164. {relative}
  165. </time>
  166. </Tooltip>
  167. );
  168. }
  169. export default TimeSince;
  170. function getRelativeDate(
  171. currentDateTime: RelaxedDateType,
  172. suffix?: string,
  173. prefix?: string,
  174. unitStyle: UnitStyle = 'human'
  175. ): string {
  176. const momentDate = moment(getDateObj(currentDateTime));
  177. const isFuture = momentDate.isAfter(moment());
  178. let deltaText: string = '';
  179. if (unitStyle === 'human') {
  180. // Moment provides a nice human relative date that uses "a few" for various units
  181. deltaText = momentDate.fromNow(true);
  182. } else {
  183. deltaText = getDuration(
  184. moment().diff(momentDate, 'seconds'),
  185. 0,
  186. unitStyle === 'short',
  187. unitStyle === 'extraShort',
  188. isFuture
  189. );
  190. }
  191. if (!suffix && !prefix) {
  192. return deltaText;
  193. }
  194. return isFuture ? `${prefix} ${deltaText}` : `${deltaText} ${suffix}`;
  195. }