Skip to content

Commit 3122883

Browse files
feat(replay): add hovercard to release tag and filter dropdown (#90701)
closes #90633 - adds a release hovercard to the `release` tag in replay details - previous behavior was that if the tag was clicked, it would populate the replay search bar with the `release:xxx` filter - new behavior is a dropdown that has 2 options: same search behavior as before, or going to release details - also removes deprecated `browserHistory` from the replay index table cell dropdown filters https://github.com/user-attachments/assets/6d5a0fa8-aea2-4c72-989d-84a31ba8e05b
1 parent e4b3974 commit 3122883

File tree

3 files changed

+121
-5
lines changed

3 files changed

+121
-5
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import styled from '@emotion/styled';
2+
3+
import {Button} from 'sentry/components/core/button';
4+
import {DropdownMenu} from 'sentry/components/dropdownMenu';
5+
import {IconEllipsis} from 'sentry/icons';
6+
import {t} from 'sentry/locale';
7+
import {space} from 'sentry/styles/space';
8+
import {useLocation} from 'sentry/utils/useLocation';
9+
import {useNavigate} from 'sentry/utils/useNavigate';
10+
import useOrganization from 'sentry/utils/useOrganization';
11+
import {makeReleasesPathname} from 'sentry/views/releases/utils/pathnames';
12+
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
13+
import type {ReplayListLocationQuery} from 'sentry/views/replays/types';
14+
15+
export default function ReleaseDropdownFilter({val}: {val: string}) {
16+
const location = useLocation<ReplayListLocationQuery>();
17+
const navigate = useNavigate();
18+
const organization = useOrganization();
19+
20+
return (
21+
<DropdownMenu
22+
items={[
23+
{
24+
key: 'search',
25+
label: t('Search for replays in this release'),
26+
onAction: () =>
27+
navigate({
28+
pathname: makeReplaysPathname({
29+
path: '/',
30+
organization,
31+
}),
32+
query: {
33+
...location.query,
34+
query: `release:"${val}"`,
35+
},
36+
}),
37+
},
38+
{
39+
key: 'details',
40+
label: t('Go to release details'),
41+
onAction: () =>
42+
navigate(
43+
makeReleasesPathname({
44+
organization,
45+
path: `/${encodeURIComponent(val)}/`,
46+
})
47+
),
48+
},
49+
]}
50+
usePortal
51+
size="xs"
52+
offset={4}
53+
position="bottom"
54+
preventOverflowOptions={{padding: 4}}
55+
flipOptions={{
56+
fallbackPlacements: ['top', 'right-start', 'right-end', 'left-start', 'left-end'],
57+
}}
58+
trigger={triggerProps => (
59+
<TriggerButton
60+
{...triggerProps}
61+
aria-label={t('Actions')}
62+
icon={<IconEllipsis size="xs" />}
63+
size="zero"
64+
/>
65+
)}
66+
/>
67+
);
68+
}
69+
70+
const TriggerButton = styled(Button)`
71+
padding: ${space(0.5)};
72+
`;

static/app/components/replays/replayTagsTableRow.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import {Tooltip} from 'sentry/components/core/tooltip';
77
import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
88
import {KeyValueTableRow} from 'sentry/components/keyValueTable';
99
import Link from 'sentry/components/links/link';
10+
import ReleaseDropdownFilter from 'sentry/components/replays/releaseDropdownFilter';
1011
import {CollapsibleValue} from 'sentry/components/structuredEventData/collapsibleValue';
1112
import Version from 'sentry/components/version';
1213
import {space} from 'sentry/styles/space';
14+
import useOrganization from 'sentry/utils/useOrganization';
15+
import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
16+
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
1317

1418
interface Props {
1519
name: string;
@@ -25,6 +29,8 @@ const expandedViewKeys = [
2529
'sdk.replay.maskingRules',
2630
];
2731

32+
const releaseKeys = ['release', 'releases'];
33+
2834
function renderValueList(values: ReactNode[]) {
2935
if (typeof values[0] === 'string') {
3036
return values[0];
@@ -44,15 +50,32 @@ function renderValueList(values: ReactNode[]) {
4450
}
4551

4652
function ReplayTagsTableRow({name, values, generateUrl}: Props) {
53+
const organization = useOrganization();
54+
4755
const renderTagValue = useMemo(() => {
48-
if (name === 'release') {
56+
if (releaseKeys.includes(name)) {
4957
return values.map((value, index) => (
5058
<Fragment key={`${name}-${index}-${value}`}>
5159
{index > 0 && ', '}
52-
<Version key={index} version={String(value)} anchor={false} withPackage />
60+
<StyledVersionContainer>
61+
<ReleaseDropdownFilter val={String(value)} />
62+
<QuickContextHoverWrapper
63+
dataRow={{release: String(value)}}
64+
contextType={ContextType.RELEASE}
65+
organization={organization}
66+
>
67+
<Version
68+
key={index}
69+
version={String(value)}
70+
truncate={false}
71+
anchor={false}
72+
/>
73+
</QuickContextHoverWrapper>
74+
</StyledVersionContainer>
5375
</Fragment>
5476
));
5577
}
78+
5679
if (
5780
expandedViewKeys.includes(name) &&
5881
renderValueList(values) &&
@@ -75,7 +98,7 @@ function ReplayTagsTableRow({name, values, generateUrl}: Props) {
7598
</Fragment>
7699
);
77100
});
78-
}, [name, values, generateUrl]);
101+
}, [name, values, generateUrl, organization]);
79102

80103
return (
81104
<KeyValueTableRow
@@ -87,6 +110,7 @@ function ReplayTagsTableRow({name, values, generateUrl}: Props) {
87110
value={
88111
<ValueContainer>
89112
<StyledTooltip
113+
disabled={releaseKeys.includes(name)}
90114
overlayStyle={
91115
expandedViewKeys.includes(name) ? {textAlign: 'left'} : undefined
92116
}
@@ -118,3 +142,9 @@ const ValueContainer = styled('div')`
118142
const StyledTooltip = styled(Tooltip)`
119143
${p => p.theme.overflowEllipsis};
120144
`;
145+
146+
const StyledVersionContainer = styled('div')`
147+
display: flex;
148+
justify-content: flex-end;
149+
gap: ${space(0.75)};
150+
`;

static/app/views/replays/replayTable/tableCell.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ import type {ValidSize} from 'sentry/styles/space';
2626
import {space} from 'sentry/styles/space';
2727
import type {Organization} from 'sentry/types/organization';
2828
import {trackAnalytics} from 'sentry/utils/analytics';
29-
import {browserHistory} from 'sentry/utils/browserHistory';
3029
import type EventView from 'sentry/utils/discover/eventView';
3130
import {spanOperationRelativeBreakdownRenderer} from 'sentry/utils/discover/fieldRenderers';
3231
import {getShortEventId} from 'sentry/utils/events';
3332
import {decodeScalar} from 'sentry/utils/queryString';
3433
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
3534
import {useLocation} from 'sentry/utils/useLocation';
3635
import useMedia from 'sentry/utils/useMedia';
36+
import {useNavigate} from 'sentry/utils/useNavigate';
3737
import useProjects from 'sentry/utils/useProjects';
3838
import type {ReplayListRecordWithTx} from 'sentry/views/performance/transactionSummary/transactionReplays/useReplaysWithTxData';
3939
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';
@@ -53,10 +53,12 @@ function generateAction({
5353
value,
5454
edit,
5555
location,
56+
navigate,
5657
}: {
5758
edit: EditType;
5859
key: string;
5960
location: Location<ReplayListLocationQuery>;
61+
navigate: ReturnType<typeof useNavigate>;
6062
value: string;
6163
}) {
6264
const search = new MutableSearch(decodeScalar(location.query.query) || '');
@@ -65,7 +67,7 @@ function generateAction({
6567
edit === 'set' ? search.setFilterValues(key, [value]) : search.removeFilter(key);
6668

6769
const onAction = () => {
68-
browserHistory.push({
70+
navigate({
6971
pathname: location.pathname,
7072
query: {
7173
...location.query,
@@ -87,6 +89,8 @@ function OSBrowserDropdownFilter({
8789
version: string | null;
8890
}) {
8991
const location = useLocation<ReplayListLocationQuery>();
92+
const navigate = useNavigate();
93+
9094
return (
9195
<DropdownMenu
9296
items={[
@@ -107,6 +111,7 @@ function OSBrowserDropdownFilter({
107111
value: name ?? '',
108112
edit: 'set',
109113
location,
114+
navigate,
110115
}),
111116
},
112117
{
@@ -117,6 +122,7 @@ function OSBrowserDropdownFilter({
117122
value: name ?? '',
118123
edit: 'remove',
119124
location,
125+
navigate,
120126
}),
121127
},
122128
],
@@ -140,6 +146,7 @@ function OSBrowserDropdownFilter({
140146
value: version ?? '',
141147
edit: 'set',
142148
location,
149+
navigate,
143150
}),
144151
},
145152
{
@@ -150,6 +157,7 @@ function OSBrowserDropdownFilter({
150157
value: version ?? '',
151158
edit: 'remove',
152159
location,
160+
navigate,
153161
}),
154162
},
155163
],
@@ -192,6 +200,8 @@ function NumericDropdownFilter({
192200
triggerOverlay?: boolean;
193201
}) {
194202
const location = useLocation<ReplayListLocationQuery>();
203+
const navigate = useNavigate();
204+
195205
return (
196206
<DropdownMenu
197207
items={[
@@ -203,6 +213,7 @@ function NumericDropdownFilter({
203213
value: formatter(val),
204214
edit: 'set',
205215
location,
216+
navigate,
206217
}),
207218
},
208219
{
@@ -213,6 +224,7 @@ function NumericDropdownFilter({
213224
value: '>' + formatter(val),
214225
edit: 'set',
215226
location,
227+
navigate,
216228
}),
217229
},
218230
{
@@ -223,6 +235,7 @@ function NumericDropdownFilter({
223235
value: '<' + formatter(val),
224236
edit: 'set',
225237
location,
238+
navigate,
226239
}),
227240
},
228241
{
@@ -233,6 +246,7 @@ function NumericDropdownFilter({
233246
value: formatter(val),
234247
edit: 'remove',
235248
location,
249+
navigate,
236250
}),
237251
},
238252
]}

0 commit comments

Comments
 (0)