Skip to content
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

ref - WIP - replay all the things #75175

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions static/app/components/duration/duration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {css} from '@emotion/react';

import formatDuration from 'sentry/utils/duration/formatDuration';
import type {Duration as TDuration} from 'sentry/utils/duration/types';

export default function Duration({duration}: {duration: TDuration}) {
return (
<time
css={css`
font-variant-numeric: tabular-nums;
`}
dateTime={formatDuration({duration, precision: 'ms', style: 'HTML duration'})}
title={formatDuration({duration, precision: 'ms', style: 'hh:mm:ss.sss'})}
>
{formatDuration({duration, precision: 'sec', style: 'h:mm:ss'})}
</time>
);
}
Empty file removed static/app/components/parser
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Button} from 'sentry/components/button';
import ButtonBar from 'sentry/components/buttonBar';
import formatDuration from 'sentry/utils/duration/formatDuration';
import {intervalToMilliseconds} from 'sentry/utils/duration/intervalToMilliseconds';
import {useReplayUserAction} from 'sentry/utils/replays/playback/providers/useReplayPlayerState';

interface Props {
intervals: string[];
}

export default function JumpToOffsetButtonBar({intervals}: Props) {
const userAction = useReplayUserAction();

return (
<ButtonBar merged>
{intervals.map(interval => {
const intervalMs = intervalToMilliseconds(interval);
return (
<Button
key={interval}
onClick={() => userAction({type: 'jumpToOffset', offsetMs: intervalMs})}
size="sm"
>
{formatDuration({
duration: [intervalMs, 'ms'],
style: 'h:mm:ss',
precision: 'sec',
})}
</Button>
);
})}
</ButtonBar>
);
}
26 changes: 26 additions & 0 deletions static/app/components/replays/player/__stories__/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {ReactNode} from 'react';

import {StaticNoSkipReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
import {ReplayPlayerEventsContextProvider} from 'sentry/utils/replays/playback/providers/useReplayPlayerEvents';
import {ReplayPlayerPluginsContextProvider} from 'sentry/utils/replays/playback/providers/useReplayPlayerPlugins';
import {ReplayPlayerStateContextProvider} from 'sentry/utils/replays/playback/providers/useReplayPlayerState';
import {ReplayPreferencesContextProvider} from 'sentry/utils/replays/playback/providers/useReplayPrefs';
import type ReplayReader from 'sentry/utils/replays/replayReader';

export default function Providers({
children,
replay,
}: {
children: ReactNode;
replay: ReplayReader;
}) {
return (
<ReplayPreferencesContextProvider prefsStrategy={StaticNoSkipReplayPreferences}>
<ReplayPlayerPluginsContextProvider>
<ReplayPlayerEventsContextProvider replay={replay}>
<ReplayPlayerStateContextProvider>{children}</ReplayPlayerStateContextProvider>
</ReplayPlayerEventsContextProvider>
</ReplayPlayerPluginsContextProvider>
</ReplayPreferencesContextProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Fragment, type ReactNode} from 'react';
import {css} from '@emotion/react';

import Providers from 'sentry/components/replays/player/__stories__/providers';
import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
import useOrganization from 'sentry/utils/useOrganization';
import {useSessionStorage} from 'sentry/utils/useSessionStorage';

export default function ReplaySlugChooser({children}: {children: ReactNode}) {
const [replaySlug, setReplaySlug] = useSessionStorage('stories:replaySlug', '');

return (
<Fragment>
<input
defaultValue={replaySlug}
onChange={event => {
setReplaySlug(event.target.value);
}}
placeholder="Paste a replaySlug"
css={css`
font-variant-numeric: tabular-nums;
`}
size={34}
/>
{replaySlug ? <LoadReplay replaySlug={replaySlug}>{children}</LoadReplay> : null}
</Fragment>
);
}

function LoadReplay({children, replaySlug}: {children: ReactNode; replaySlug: string}) {
const organization = useOrganization();
const {replay, fetching} = useReplayReader({
orgSlug: organization.slug,
replaySlug,
});

if (!replay || fetching) {
return 'Loading...';
}

return <Providers replay={replay}>{children}</Providers>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import NegativeSpaceContainer from 'sentry/components/container/negativeSpaceContainer';
import JumpToOffsetButtonBar from 'sentry/components/replays/player/__stories__/jumpToOffsetButtonBar';
import ReplaySlugChooser from 'sentry/components/replays/player/__stories__/replaySlugChooser';
import ReplayCurrentTime from 'sentry/components/replays/player/replayCurrentTime';
import ReplayPlayer from 'sentry/components/replays/player/replayPlayer';
import ReplayPlayerContainment from 'sentry/components/replays/player/replayPlayerContainment';
import ReplayPlayPauseButton from 'sentry/components/replays/player/replayPlayPauseButton';
import SideBySide from 'sentry/components/stories/sideBySide';
import storyBook from 'sentry/stories/storyBook';

export default storyBook(ReplayCurrentTime, story => {
story('Default', () => {
function Example() {
return (
<SideBySide>
<ReplayPlayPauseButton />
<ReplayCurrentTime />
<NegativeSpaceContainer style={{height: 300}}>
<ReplayPlayerContainment measure="both">
{style => <ReplayPlayer style={style} />}
</ReplayPlayerContainment>
</NegativeSpaceContainer>
</SideBySide>
);
}
return (
<ReplaySlugChooser>
<Example />
</ReplaySlugChooser>
);
});

story('Jumping to different times', () => {
function Example() {
return (
<SideBySide>
<ReplayPlayPauseButton />
<ReplayCurrentTime />
<JumpToOffsetButtonBar intervals={['0m', '1m', '12m']} />

<NegativeSpaceContainer style={{height: 300}}>
<ReplayPlayerContainment measure="both">
{style => <ReplayPlayer style={style} />}
</ReplayPlayerContainment>
</NegativeSpaceContainer>
</SideBySide>
);
}
return (
<ReplaySlugChooser>
<Example />
</ReplaySlugChooser>
);
});
});
12 changes: 12 additions & 0 deletions static/app/components/replays/player/replayCurrentTime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {useState} from 'react';

import Duration from 'sentry/components/duration/duration';
import useReplayCurrentTime from 'sentry/utils/replays/playback/hooks/useReplayCurrentTime';

export default function ReplayCurrentTime() {
const [currentTime, setCurrentTime] = useState({timeMs: 0});

useReplayCurrentTime({callback: setCurrentTime});

return <Duration duration={[currentTime.timeMs, 'ms']} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {Fragment} from 'react';

import NegativeSpaceContainer from 'sentry/components/container/negativeSpaceContainer';
import ReplaySlugChooser from 'sentry/components/replays/player/__stories__/replaySlugChooser';
import ReplayPlayer from 'sentry/components/replays/player/replayPlayer';
import ReplayPlayerContainment from 'sentry/components/replays/player/replayPlayerContainment';
import ReplayPlayPauseButton from 'sentry/components/replays/player/replayPlayPauseButton';
import JSXNode from 'sentry/components/stories/jsxNode';
import storyBook from 'sentry/stories/storyBook';

export default storyBook(ReplayPlayer, story => {
story('Default', () => {
function Example() {
return (
<Fragment>
<p>
Include <JSXNode name="ReplayPlayPauseButton" /> inside a{' '}
<JSXNode name="ReplayPlayerStateContextProvider" /> to control the play/pause
state of the replay.
</p>

<NegativeSpaceContainer style={{height: 400}}>
<ReplayPlayerContainment measure="both">
{style => <ReplayPlayer style={style} />}
</ReplayPlayerContainment>
</NegativeSpaceContainer>
<ReplayPlayPauseButton />
</Fragment>
);
}
return (
<ReplaySlugChooser>
<Example />
</ReplaySlugChooser>
);
});

story('Multiple Players', () => {
function Example() {
return (
<Fragment>
<p>
All <JSXNode name="ReplayPlayer" /> instances within the{' '}
<JSXNode name="ReplayPlayerStateContextProvider" /> will play & pause
together.
</p>

<NegativeSpaceContainer style={{height: 200}}>
<ReplayPlayerContainment measure="both">
{style => <ReplayPlayer style={style} />}
</ReplayPlayerContainment>
</NegativeSpaceContainer>
<hr />
<NegativeSpaceContainer style={{height: 200}}>
<ReplayPlayerContainment measure="both">
{style => <ReplayPlayer style={style} />}
</ReplayPlayerContainment>
</NegativeSpaceContainer>
<ReplayPlayPauseButton />
</Fragment>
);
}
return (
<ReplaySlugChooser>
<Example />
</ReplaySlugChooser>
);
});
});
37 changes: 37 additions & 0 deletions static/app/components/replays/player/replayPlayPauseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {BaseButtonProps} from 'sentry/components/button';
import {Button} from 'sentry/components/button';
import {IconPause, IconPlay, IconRefresh} from 'sentry/icons';
import {t} from 'sentry/locale';
import useReplayPlayerState, {
useReplayUserAction,
} from 'sentry/utils/replays/playback/providers/useReplayPlayerState';

export default function ReplayPlayPauseButton(props: BaseButtonProps) {
const userAction = useReplayUserAction();
const {playerState, isFinished} = useReplayPlayerState();

const isPlaying = playerState === 'playing';

return isFinished ? (
<Button
title={t('Restart Replay')}
icon={<IconRefresh />}
onClick={() => {
userAction({type: 'jumpToOffset', offsetMs: 0});
userAction({type: 'play'});
}}
aria-label={t('Restart Replay')}
priority="primary"
{...props}
/>
) : (
<Button
title={isPlaying ? t('Pause') : t('Play')}
icon={isPlaying ? <IconPause /> : <IconPlay />}
onClick={() => userAction(isPlaying ? {type: 'pause'} : {type: 'play'})}
aria-label={isPlaying ? t('Pause') : t('Play')}
priority="primary"
{...props}
/>
);
}
50 changes: 50 additions & 0 deletions static/app/components/replays/player/replayPlayer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import NegativeSpaceContainer from 'sentry/components/container/negativeSpaceContainer';
import ObjectInspector from 'sentry/components/objectInspector';
import JumpToOffsetButtonBar from 'sentry/components/replays/player/__stories__/jumpToOffsetButtonBar';
import ReplaySlugChooser from 'sentry/components/replays/player/__stories__/replaySlugChooser';
import ReplayCurrentTime from 'sentry/components/replays/player/replayCurrentTime';
import ReplayPlayer from 'sentry/components/replays/player/replayPlayer';
import ReplayPlayerContainment from 'sentry/components/replays/player/replayPlayerContainment';
import ReplayPlayPauseButton from 'sentry/components/replays/player/replayPlayPauseButton';
import ReplayPreferenceDropdown from 'sentry/components/replays/preferences/replayPreferenceDropdown';
import SideBySide from 'sentry/components/stories/sideBySide';
import storyBook from 'sentry/stories/storyBook';
import useReplayPlayerState from 'sentry/utils/replays/playback/providers/useReplayPlayerState';
import useReplayPrefs from 'sentry/utils/replays/playback/providers/useReplayPrefs';

export default storyBook(ReplayPlayer, story => {
story('Default', () => {
function Example() {
return (
<SideBySide>
<ReplayPlayPauseButton />
<ReplayCurrentTime />
<ReplayPreferenceDropdown speedOptions={[0.5, 1, 2, 8]} />
<JumpToOffsetButtonBar intervals={['0m', '1ms', '1m', '8m', '12m']} />
<DebugReplayPlayerState />
<DebugReplayPrefsState />
<NegativeSpaceContainer style={{height: 500}}>
<ReplayPlayerContainment measure="both">
{style => <ReplayPlayer style={style} />}
</ReplayPlayerContainment>
</NegativeSpaceContainer>
</SideBySide>
);
}
return (
<ReplaySlugChooser>
<Example />
</ReplaySlugChooser>
);
});
});

function DebugReplayPlayerState() {
const state = useReplayPlayerState();
return <ObjectInspector data={state} expandLevel={1} />;
}

function DebugReplayPrefsState() {
const [prefs] = useReplayPrefs();
return <ObjectInspector data={prefs} expandLevel={1} />;
}
Loading
Loading