-
-
Couldn't load subscription status.
- Fork 4.5k
feat(replays): Add live indicator for ongoing replays and refactor user badge component #102120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
86fb60c
f133128
27f7e37
37d5455
96a4dbe
8c992bd
5c333af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,29 @@ | ||
| import {useEffect, useState} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
| import invariant from 'invariant'; | ||
|
|
||
| import {Tooltip} from '@sentry/scraps/tooltip'; | ||
|
|
||
| import {ProjectAvatar} from 'sentry/components/core/avatar/projectAvatar'; | ||
| import {UserAvatar} from 'sentry/components/core/avatar/userAvatar'; | ||
| import {Grid} from 'sentry/components/core/layout'; | ||
| import {Flex} from 'sentry/components/core/layout/flex'; | ||
| import {Text} from 'sentry/components/core/text'; | ||
| import {DateTime} from 'sentry/components/dateTime'; | ||
| import { | ||
| getLiveDurationMs, | ||
| getReplayExpiresAtMs, | ||
| LIVE_TOOLTIP_MESSAGE, | ||
| LiveIndicator, | ||
| } from 'sentry/components/replays/replayLiveIndicator'; | ||
| import TimeSince from 'sentry/components/timeSince'; | ||
| import {IconCalendar} from 'sentry/icons/iconCalendar'; | ||
| import {IconDelete} from 'sentry/icons/iconDelete'; | ||
| import {t} from 'sentry/locale'; | ||
| import * as events from 'sentry/utils/events'; | ||
| import {useReplayPrefs} from 'sentry/utils/replays/playback/providers/replayPreferencesContext'; | ||
| import useProjectFromId from 'sentry/utils/useProjectFromId'; | ||
| import useTimeout from 'sentry/utils/useTimeout'; | ||
| import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData'; | ||
| import type {ReplayListRecord} from 'sentry/views/replays/types'; | ||
|
|
||
|
|
@@ -26,6 +36,21 @@ export default function ReplayBadge({replay}: Props) { | |
| const [prefs] = useReplayPrefs(); | ||
| const timestampType = prefs.timestampType; | ||
|
|
||
| const [isLive, setIsLive] = useState( | ||
| Date.now() < getReplayExpiresAtMs(replay.started_at) | ||
| ); | ||
|
|
||
| const {start: startTimeout} = useTimeout({ | ||
| timeMs: getLiveDurationMs(replay.finished_at), | ||
| onTimeout: () => { | ||
| setIsLive(false); | ||
| }, | ||
| }); | ||
jerryzhou196 marked this conversation as resolved.
Show resolved
Hide resolved
jerryzhou196 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Live Indicator Timeout Not UpdatedThe timeout duration is calculated only once during initial render using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's fine |
||
|
|
||
| useEffect(() => { | ||
| startTimeout(); | ||
| }, [startTimeout]); | ||
|
|
||
| if (replay.is_archived) { | ||
| return ( | ||
| <Grid columns="24px 1fr" gap="md" align="center" justify="center"> | ||
|
|
@@ -65,10 +90,21 @@ export default function ReplayBadge({replay}: Props) { | |
| }} | ||
| size={24} | ||
| /> | ||
|
|
||
| <Flex direction="column" gap="xs" justify="center"> | ||
| <Text size="md" bold ellipsis data-underline-on-hover> | ||
| {replay.user.display_name || t('Anonymous User')} | ||
| </Text> | ||
| <Flex direction="row" align="center"> | ||
| <div> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this div necessary? |
||
| <Text size="md" bold ellipsis data-underline-on-hover> | ||
| {replay.user.display_name || t('Anonymous User')} | ||
| </Text> | ||
| </div> | ||
| {isLive ? ( | ||
| <Tooltip title={LIVE_TOOLTIP_MESSAGE}> | ||
| <LiveIndicator /> | ||
| </Tooltip> | ||
| ) : null} | ||
| </Flex> | ||
|
|
||
| <Flex gap="xs"> | ||
| {/* Avatar is used instead of ProjectBadge because using ProjectBadge increases spacing, which doesn't look as good */} | ||
| {project ? <ProjectAvatar size={12} project={project} /> : null} | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||
| import {keyframes} from '@emotion/react'; | ||||||||||
| import styled from '@emotion/styled'; | ||||||||||
|
|
||||||||||
| import {t} from 'sentry/locale'; | ||||||||||
| import type {ReplayRecord} from 'sentry/views/replays/types'; | ||||||||||
|
|
||||||||||
| export const LIVE_TOOLTIP_MESSAGE = t('This replay is still in progress.'); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need "still"?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember if we use periods in tooltips
Suggested change
|
||||||||||
|
|
||||||||||
| export function getReplayExpiresAtMs(startedAt: ReplayRecord['started_at']): number { | ||||||||||
| const ONE_HOUR_MS = 3_600_000; | ||||||||||
| return startedAt ? startedAt.getTime() + ONE_HOUR_MS : 0; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export function getLiveDurationMs(finishedAt: ReplayRecord['finished_at']): number { | ||||||||||
| if (!finishedAt) { | ||||||||||
| return 0; | ||||||||||
| } | ||||||||||
| const FIVE_MINUTE_MS = 300_000; | ||||||||||
| return Math.max(finishedAt.getTime() + FIVE_MINUTE_MS - Date.now(), 0); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const pulse = keyframes` | ||||||||||
| 0% { | ||||||||||
| transform: scale(0.1); | ||||||||||
| opacity: 1 | ||||||||||
| } | ||||||||||
| 40%, 100% { | ||||||||||
| transform: scale(1); | ||||||||||
| opacity: 0; | ||||||||||
| } | ||||||||||
| `; | ||||||||||
|
|
||||||||||
| export const LiveIndicator = styled('div')` | ||||||||||
| background: ${p => p.theme.successText}; | ||||||||||
| height: 8px; | ||||||||||
| width: 8px; | ||||||||||
| position: relative; | ||||||||||
| border-radius: 50%; | ||||||||||
| margin-left: ${p => p.theme.space.sm}; | ||||||||||
| margin-right: ${p => p.theme.space.sm}; | ||||||||||
| @media (prefers-reduced-motion: reduce) { | ||||||||||
| &:before { | ||||||||||
| display: none; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| &:before { | ||||||||||
| content: ''; | ||||||||||
| animation: ${pulse} 3s ease-out infinite; | ||||||||||
| border: 6px solid ${p => p.theme.successText}; | ||||||||||
| position: absolute; | ||||||||||
| border-radius: 50%; | ||||||||||
| height: 20px; | ||||||||||
| width: 20px; | ||||||||||
| top: -6px; | ||||||||||
| left: -6px; | ||||||||||
| } | ||||||||||
| `; | ||||||||||
Uh oh!
There was an error while loading. Please reload this page.