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

feat(replay): Let the a11y analysis run at any point inside the replay #61012

Merged
merged 1 commit into from
Dec 4, 2023
Merged
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
28 changes: 16 additions & 12 deletions static/app/utils/replays/hooks/useA11yData.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import {useMemo} from 'react';

import {useReplayContext} from 'sentry/components/replays/replayContext';
import {useApiQuery} from 'sentry/utils/queryClient';
import {useQuery} from 'sentry/utils/queryClient';
import hydrateA11yFrame, {RawA11yResponse} from 'sentry/utils/replays/hydrateA11yFrame';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import useProjects from 'sentry/utils/useProjects';

export default function useA11yData() {
const api = useApi();
const organization = useOrganization();
const {replay} = useReplayContext();
const {currentTime, replay} = useReplayContext();
const {projects} = useProjects();

const replayRecord = replay?.getReplay();
const startTimestampMs = replayRecord?.started_at.getTime();
const project = projects.find(p => p.id === replayRecord?.project_id);

const {data, ...rest} = useApiQuery<RawA11yResponse>(
[
const unixTimestamp = ((startTimestampMs || 0) + currentTime) / 1000;
const {data, ...rest} = useQuery<RawA11yResponse>({
queryKey: [
`/projects/${organization.slug}/${project?.slug}/replays/${replayRecord?.id}/accessibility-issues/`,
],
{
staleTime: 0,
enabled: Boolean(project) && Boolean(replayRecord),
}
);
queryFn: ({queryKey: [url]}) =>
api.requestPromise(String(url), {
method: 'GET',
query: {timestamp: unixTimestamp},
}),
staleTime: 0,
enabled: Boolean(project) && Boolean(replayRecord),
});

const hydrated = useMemo(
() => data?.data?.flatMap(record => hydrateA11yFrame(record, startTimestampMs ?? 0)),
[data?.data, startTimestampMs]
);
return {data: hydrated, ...rest};
return {data: hydrated, dataOffsetMs: currentTime, ...rest};
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ const COLUMNS: {
label: t('Type'),
},
{field: 'element', label: t('Element')},
{field: '', label: ''},
];

export const COLUMN_COUNT = COLUMNS.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {useCallback, useState} from 'react';
import styled from '@emotion/styled';

import {Button} from 'sentry/components/button';
import {Flex} from 'sentry/components/profiling/flex';
import {useReplayContext} from 'sentry/components/replays/replayContext';
import {showPlayerTime} from 'sentry/components/replays/utils';
import Well from 'sentry/components/well';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import TimestampButton from 'sentry/views/replays/detail/timestampButton';

interface Props {
initialOffsetMs: number;
refetch: () => void;
}

export default function AccessibilityRefetchBanner({initialOffsetMs, refetch}: Props) {
const {currentTime, replay, setCurrentTime, isPlaying, togglePlayPause} =
useReplayContext();

const startTimestampMs = replay?.getReplay()?.started_at?.getTime() ?? 0;
const [lastOffsetMs, setLastOffsetMs] = useState(initialOffsetMs);

const handleClickRefetch = useCallback(() => {
togglePlayPause(false);
setLastOffsetMs(currentTime);
refetch();
}, [currentTime, refetch, togglePlayPause]);

const handleClickTimestamp = useCallback(() => {
setCurrentTime(lastOffsetMs);
}, [setCurrentTime, lastOffsetMs]);

const now = showPlayerTime(startTimestampMs + currentTime, startTimestampMs, false);

return (
<StyledWell>
<Flex
gap={space(1)}
justify="space-between"
align="center"
wrap="nowrap"
style={{overflow: 'auto'}}
>
<Flex gap={space(1)} wrap="nowrap" style={{whiteSpace: 'nowrap'}}>
{tct('Results as of [lastRuntime]', {
lastRuntime: (
<StyledTimestampButton
aria-label={t('See in replay')}
onClick={handleClickTimestamp}
startTimestampMs={startTimestampMs}
timestampMs={startTimestampMs + lastOffsetMs}
/>
),
})}
</Flex>
<Button
size="xs"
priority="primary"
onClick={handleClickRefetch}
disabled={currentTime === lastOffsetMs}
>
{isPlaying
? tct('Pause and Run validation for [now]', {now})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
? tct('Pause and Run validation for [now]', {now})
? tct('Pause and run validation for [now]', {now})

super nit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well damnit. the auto-merge got it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😭

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

: tct('Run validation for [now]', {now})}
</Button>
</Flex>
</StyledWell>
);
}

const StyledWell = styled(Well)`
margin-bottom: 0;
border-radius: ${p => p.theme.borderRadiusTop};
`;

const StyledTimestampButton = styled(TimestampButton)`
align-self: center;
align-items: center;
`;
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {ComponentProps, CSSProperties, forwardRef} from 'react';
import classNames from 'classnames';

import {Button} from 'sentry/components/button';
import {
Cell,
CodeHighlightCell,
Text,
} from 'sentry/components/replays/virtualizedGrid/bodyCell';
import {Tooltip} from 'sentry/components/tooltip';
import {IconFire, IconInfo, IconPlay, IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import {IconFire, IconInfo, IconWarning} from 'sentry/icons';
import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
import {Color} from 'sentry/utils/theme';
Expand Down Expand Up @@ -44,7 +42,6 @@ const AccessibilityTableCell = forwardRef<HTMLDivElement, Props>(
currentHoverTime,
currentTime,
onClickCell,
onClickTimestamp,
onMouseEnter,
onMouseLeave,
rowIndex,
Expand Down Expand Up @@ -125,20 +122,6 @@ const AccessibilityTableCell = forwardRef<HTMLDivElement, Props>(
</CodeHighlightCell>
</Cell>
),
() => (
<Cell {...columnProps}>
<Button
size="xs"
borderless
aria-label={t('See in replay')}
icon={<IconPlay size="xs" color={isSelected ? 'white' : 'black'} />}
onClick={e => {
e.stopPropagation();
onClickTimestamp(a11yIssue);
}}
/>
</Cell>
),
];

return renderFns[columnIndex]();
Expand Down
32 changes: 26 additions & 6 deletions static/app/views/replays/detail/accessibility/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useCallback, useMemo, useRef, useState} from 'react';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {AutoSizer, CellMeasurer, GridCellProps, MultiGrid} from 'react-virtualized';
import styled from '@emotion/styled';

import Placeholder from 'sentry/components/placeholder';
import JumpButtons from 'sentry/components/replays/jumpButtons';
Expand All @@ -18,6 +19,7 @@ import AccessibilityFilters from 'sentry/views/replays/detail/accessibility/acce
import AccessibilityHeaderCell, {
COLUMN_COUNT,
} from 'sentry/views/replays/detail/accessibility/accessibilityHeaderCell';
import AccessibilityRefetchBanner from 'sentry/views/replays/detail/accessibility/accessibilityRefetchBanner';
import AccessibilityTableCell from 'sentry/views/replays/detail/accessibility/accessibilityTableCell';
import AccessibilityDetails from 'sentry/views/replays/detail/accessibility/details';
import useAccessibilityFilters from 'sentry/views/replays/detail/accessibility/useAccessibilityFilters';
Expand All @@ -43,7 +45,13 @@ function AccessibilityList() {
const {currentTime, currentHoverTime} = useReplayContext();
const {onMouseEnter, onMouseLeave, onClickTimestamp} = useCrumbHandlers();

const {data: accessibilityData, isLoading} = useA11yData();
const {
dataOffsetMs,
data: accessibilityData,
isLoading,
isRefetching,
refetch,
} = useA11yData();

const [scrollToRow, setScrollToRow] = useState<undefined | number>(undefined);

Expand Down Expand Up @@ -92,6 +100,12 @@ function AccessibilityList() {
),
});

useEffect(() => {
if (isRefetching) {
onCloseDetailsSplit();
}
}, [isRefetching, onCloseDetailsSplit]);

const {
handleClick: onClickToJump,
onSectionRendered,
Expand Down Expand Up @@ -147,16 +161,17 @@ function AccessibilityList() {

return (
<FluidHeight>
<FilterLoadingIndicator isLoading={isLoading}>
<FilterLoadingIndicator isLoading={isLoading || isRefetching}>
<AccessibilityFilters accessibilityData={accessibilityData} {...filterProps} />
</FilterLoadingIndicator>
<GridTable ref={containerRef} data-test-id="replay-details-accessibility-tab">
<AccessibilityRefetchBanner initialOffsetMs={dataOffsetMs} refetch={refetch} />
<StyledGridTable ref={containerRef} data-test-id="replay-details-accessibility-tab">
<SplitPanel
style={{
gridTemplateRows: splitSize !== undefined ? `1fr auto ${splitSize}px` : '1fr',
}}
>
{accessibilityData ? (
{accessibilityData && !isRefetching ? (
<OverflowHidden>
<AutoSizer onResize={onWrapperResize}>
{({height, width}) => (
Expand Down Expand Up @@ -211,9 +226,14 @@ function AccessibilityList() {
onClose={onCloseDetailsSplit}
/>
</SplitPanel>
</GridTable>
</StyledGridTable>
</FluidHeight>
);
}

const StyledGridTable = styled(GridTable)`
border-radius: ${p => p.theme.borderRadiusBottom};
border-top: none;
`;

export default AccessibilityList;
Loading