timeSince.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
  2. import isNumber from 'lodash/isNumber';
  3. import moment from 'moment-timezone';
  4. import type {TooltipProps} from 'sentry/components/tooltip';
  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/duration/getDuration';
  9. import getDynamicText from 'sentry/utils/getDynamicText';
  10. import type {ColorOrAlias} from 'sentry/utils/theme';
  11. function getDateObj(date: RelaxedDateType): Date {
  12. return typeof date === 'string' || 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. * Any other props for the <Tooltip>
  64. */
  65. tooltipProps?: Partial<TooltipProps>;
  66. /**
  67. * Include seconds in the tooltip
  68. */
  69. tooltipShowSeconds?: boolean;
  70. /**
  71. * Suffix content to add to the tooltip. Useful to indicate what the relative
  72. * time is for
  73. */
  74. tooltipSuffix?: React.ReactNode;
  75. /**
  76. * Change the color of the underline
  77. */
  78. tooltipUnderlineColor?: ColorOrAlias;
  79. /**
  80. * How much text should be used for the suffix:
  81. *
  82. * human:
  83. * hour, minute, second. Uses 'human' fuzzy foormatting for values such as 'a
  84. * minute' or 'a few seconds'. (This is the default)
  85. *
  86. * regular:
  87. * Shows the full units (hours, minutes, seconds)
  88. *
  89. * short:
  90. * Like exact but uses shorter units (hr, min, sec)
  91. *
  92. * extraShort:
  93. * Like short but uses very short units (h, m, s)
  94. *
  95. * NOTE: shot and extraShort do NOT currently support times in the future.
  96. *
  97. * @default human
  98. */
  99. unitStyle?: UnitStyle;
  100. }
  101. function TimeSince({
  102. date,
  103. disabledAbsoluteTooltip,
  104. tooltipShowSeconds,
  105. tooltipPrefix: tooltipTitle,
  106. tooltipBody,
  107. tooltipSuffix,
  108. tooltipUnderlineColor,
  109. tooltipProps,
  110. isTooltipHoverable = false,
  111. unitStyle,
  112. prefix = t('in'),
  113. suffix = t('ago'),
  114. liveUpdateInterval = 'minute',
  115. ...props
  116. }: Props) {
  117. const tickerRef = useRef<number | undefined>();
  118. const computeRelativeDate = useCallback(
  119. () => getRelativeDate(date, suffix, prefix, unitStyle),
  120. [date, suffix, prefix, unitStyle]
  121. );
  122. const [relative, setRelative] = useState<string>(computeRelativeDate());
  123. useEffect(() => {
  124. // Immediately update if props change
  125. setRelative(computeRelativeDate());
  126. const interval =
  127. liveUpdateInterval === 'minute'
  128. ? 60 * 1000
  129. : liveUpdateInterval === 'second'
  130. ? 1000
  131. : liveUpdateInterval;
  132. // Start a ticker to update the relative time
  133. tickerRef.current = window.setInterval(
  134. () => setRelative(computeRelativeDate()),
  135. interval
  136. );
  137. return () => window.clearInterval(tickerRef.current);
  138. }, [liveUpdateInterval, computeRelativeDate]);
  139. const dateObj = getDateObj(date);
  140. const user = ConfigStore.get('user');
  141. const options = user ? user.options : null;
  142. // Use short months when showing seconds, because "September" causes the
  143. // tooltip to overflow.
  144. const tooltipFormat = tooltipShowSeconds
  145. ? 'MMM D, YYYY h:mm:ss A z'
  146. : 'MMMM D, YYYY h:mm A z';
  147. const format = options?.clock24Hours ? 'MMMM D, YYYY HH:mm z' : tooltipFormat;
  148. const tooltip = getDynamicText({
  149. fixed: options?.clock24Hours
  150. ? 'November 3, 2020 08:57 UTC'
  151. : 'November 3, 2020 8:58 AM UTC',
  152. value: moment.tz(dateObj, options?.timezone ?? '').format(format),
  153. });
  154. return (
  155. <Tooltip
  156. disabled={disabledAbsoluteTooltip}
  157. underlineColor={tooltipUnderlineColor}
  158. showUnderline
  159. isHoverable={isTooltipHoverable}
  160. title={
  161. <Fragment>
  162. {tooltipTitle && <div>{tooltipTitle}</div>}
  163. {tooltipBody ?? tooltip}
  164. {tooltipSuffix && <div>{tooltipSuffix}</div>}
  165. </Fragment>
  166. }
  167. {...tooltipProps}
  168. >
  169. <time dateTime={dateObj?.toISOString()} {...props}>
  170. {relative}
  171. </time>
  172. </Tooltip>
  173. );
  174. }
  175. export default TimeSince;
  176. function getRelativeDate(
  177. currentDateTime: RelaxedDateType,
  178. suffix?: string,
  179. prefix?: string,
  180. unitStyle: UnitStyle = 'human'
  181. ): string {
  182. const momentDate = moment(getDateObj(currentDateTime));
  183. const isFuture = momentDate.isAfter(moment());
  184. let deltaText: string = '';
  185. if (unitStyle === 'human') {
  186. // Moment provides a nice human relative date that uses "a few" for various units
  187. deltaText = momentDate.fromNow(true);
  188. } else {
  189. deltaText = getDuration(
  190. moment().diff(momentDate, 'seconds'),
  191. 0,
  192. unitStyle === 'short',
  193. unitStyle === 'extraShort',
  194. isFuture
  195. );
  196. }
  197. if (!suffix && !prefix) {
  198. return deltaText;
  199. }
  200. return isFuture ? `${prefix} ${deltaText}` : `${deltaText} ${suffix}`;
  201. }