timeSince.tsx 5.6 KB

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