Skip to content

Commit 1225b43

Browse files
authored
feat(cell-actions): add back cell actions for internal links (#97381)
### Changes See #96897 for details
1 parent 0f9568c commit 1225b43

File tree

10 files changed

+102
-35
lines changed

10 files changed

+102
-35
lines changed

static/app/utils/discover/fieldRenderers.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,19 +327,15 @@ export const FIELD_FORMATTERS: FieldFormatters = {
327327
},
328328
string: {
329329
isSortable: true,
330-
renderFunc: (field, data, baggage) => {
330+
renderFunc: (field, data) => {
331331
// Some fields have long arrays in them, only show the tail of the data.
332332
const value = Array.isArray(data[field])
333333
? data[field].slice(-1)
334334
: defined(data[field])
335335
? data[field]
336336
: emptyValue;
337337

338-
// In the future, external linking will be done through CellAction component instead of the default renderer
339-
if (
340-
!baggage?.organization.features.includes('discover-cell-actions-v2') &&
341-
isUrl(value)
342-
) {
338+
if (isUrl(value)) {
343339
return (
344340
<Tooltip title={value} containerDisplayMode="block" showOnlyOnOverflow>
345341
<Container>
@@ -505,7 +501,7 @@ const SPECIAL_FIELDS: Record<string, SpecialField> = {
505501
},
506502
'span.description': {
507503
sortField: 'span.description',
508-
renderFunc: (data, {organization}) => {
504+
renderFunc: data => {
509505
const value = data[SpanFields.SPAN_DESCRIPTION];
510506
const op: string = data[SpanFields.SPAN_OP];
511507
const projectId =
@@ -533,8 +529,7 @@ const SPECIAL_FIELDS: Record<string, SpecialField> = {
533529
maxWidth={400}
534530
>
535531
<Container>
536-
{!organization.features.includes('discover-cell-actions-v2') &&
537-
isUrl(value) ? (
532+
{isUrl(value) ? (
538533
<ExternalLink href={value}>{value}</ExternalLink>
539534
) : (
540535
nullableValue(value)

static/app/views/dashboards/widgets/tableWidget/tableWidgetVisualization.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,11 @@ export function TableWidgetVisualization(props: TableWidgetVisualizationProps) {
145145
onResizeColumn,
146146
resizable = true,
147147
onTriggerCellAction,
148-
allowedCellActions = [Actions.COPY_TO_CLIPBOARD, Actions.OPEN_EXTERNAL_LINK],
148+
allowedCellActions = [
149+
Actions.COPY_TO_CLIPBOARD,
150+
Actions.OPEN_EXTERNAL_LINK,
151+
Actions.OPEN_INTERNAL_LINK,
152+
],
149153
} = props;
150154

151155
const theme = useTheme();

static/app/views/discover/table/cellAction.tsx

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {useState} from 'react';
12
import styled from '@emotion/styled';
23

34
import {addErrorMessage} from 'sentry/actionCreators/indicator';
@@ -14,8 +15,10 @@ import {
1415
isRelativeSpanOperationBreakdownField,
1516
} from 'sentry/utils/discover/fields';
1617
import getDuration from 'sentry/utils/duration/getDuration';
18+
import {FieldKey} from 'sentry/utils/fields';
1719
import {isUrl} from 'sentry/utils/string/isUrl';
1820
import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
21+
import stripURLOrigin from 'sentry/utils/url/stripURLOrigin';
1922
import useOrganization from 'sentry/utils/useOrganization';
2023

2124
import type {TableColumn} from './types';
@@ -30,6 +33,7 @@ export enum Actions {
3033
EDIT_THRESHOLD = 'edit_threshold',
3134
COPY_TO_CLIPBOARD = 'copy_to_clipboard',
3235
OPEN_EXTERNAL_LINK = 'open_external_link',
36+
OPEN_INTERNAL_LINK = 'open_internal_link',
3337
}
3438

3539
export function updateQuery(
@@ -95,6 +99,7 @@ export function updateQuery(
9599
break;
96100
case Actions.RELEASE:
97101
case Actions.DRILLDOWN:
102+
case Actions.OPEN_INTERNAL_LINK:
98103
break;
99104
default:
100105
throw new Error(`Unknown action type. ${action}`);
@@ -182,13 +187,18 @@ type CellActionsOpts = {
182187
*/
183188
allowActions?: Actions[];
184189
children?: React.ReactNode;
190+
/**
191+
* Any parsed out internal links that should be added to the menu as an option
192+
*/
193+
to?: string;
185194
};
186195

187196
function makeCellActions({
188197
dataRow,
189198
column,
190199
handleCellAction,
191200
allowActions,
201+
to,
192202
}: CellActionsOpts) {
193203
// Do not render context menu buttons for the span op breakdown field.
194204
if (isRelativeSpanOperationBreakdownField(column.name)) {
@@ -224,10 +234,22 @@ function makeCellActions({
224234
label: itemLabel,
225235
textValue: itemTextValue,
226236
onAction: () => handleCellAction(action, value!),
237+
to: action === Actions.OPEN_INTERNAL_LINK && to ? stripURLOrigin(to) : undefined,
227238
});
228239
}
229240
}
230241

242+
if (to && to !== value) {
243+
const field = String(column.key);
244+
addMenuItem(Actions.OPEN_INTERNAL_LINK, getInternalLinkActionLabel(field));
245+
}
246+
247+
if (isUrl(value)) {
248+
addMenuItem(Actions.OPEN_EXTERNAL_LINK, t('Open external link'));
249+
}
250+
251+
if (value) addMenuItem(Actions.COPY_TO_CLIPBOARD, t('Copy to clipboard'));
252+
231253
if (
232254
!['duration', 'number', 'percentage'].includes(column.type) ||
233255
(value === null && column.column.kind === 'field')
@@ -271,17 +293,39 @@ function makeCellActions({
271293
);
272294
}
273295

274-
if (value) addMenuItem(Actions.COPY_TO_CLIPBOARD, t('Copy to clipboard'));
275-
276-
if (isUrl(value)) addMenuItem(Actions.OPEN_EXTERNAL_LINK, t('Open external link'));
277-
278296
if (actions.length === 0) {
279297
return null;
280298
}
281299

282300
return actions;
283301
}
284302

303+
/**
304+
* Provides the correct text for the dropdown menu based on the field.
305+
* @param field column field name
306+
*/
307+
function getInternalLinkActionLabel(field: string): string {
308+
switch (field) {
309+
case FieldKey.ID:
310+
return t('Open view');
311+
case FieldKey.TRACE:
312+
return t('Open trace');
313+
case FieldKey.PROJECT:
314+
case 'project_id':
315+
case 'project.id':
316+
return t('Open project');
317+
case FieldKey.RELEASE:
318+
return t('View details');
319+
case FieldKey.ISSUE:
320+
return t('Open issue');
321+
case FieldKey.REPLAY_ID:
322+
return t('Open replay');
323+
default:
324+
break;
325+
}
326+
return t('Open link');
327+
}
328+
285329
/**
286330
* Potentially temporary as design and product need more time to determine how logs table should trigger the dropdown.
287331
* Currently, the agreed default for every table should be bold hover. Logs is the only table to use the ellipsis trigger.
@@ -291,7 +335,7 @@ export enum ActionTriggerType {
291335
BOLD_HOVER = 'bold_hover',
292336
}
293337

294-
type Props = React.PropsWithoutRef<CellActionsOpts> & {
338+
type Props = React.PropsWithoutRef<Omit<CellActionsOpts, 'to'>> & {
295339
triggerType?: ActionTriggerType;
296340
};
297341

@@ -302,15 +346,24 @@ function CellAction({
302346
}: Props) {
303347
const organization = useOrganization();
304348
const {children, column} = props;
349+
// The menu is activated by clicking the value, which doesn't work if the value is rendered as a link
350+
// So, `target` contains an internal link extracted from the DOM on click and that link is added dropdown menu.
351+
const [target, setTarget] = useState<string>();
305352

306353
const useCellActionsV2 = organization.features.includes('discover-cell-actions-v2');
307354
let filteredActions = allowActions;
308-
if (!useCellActionsV2)
309-
filteredActions = filteredActions?.filter(
310-
action => action !== Actions.OPEN_EXTERNAL_LINK
355+
if (!useCellActionsV2 && filteredActions) {
356+
// New dropdown menu options should not be allowed if the feature flag is not on
357+
filteredActions = filteredActions.filter(
358+
action =>
359+
action !== Actions.OPEN_EXTERNAL_LINK && action !== Actions.OPEN_INTERNAL_LINK
311360
);
312-
313-
const cellActions = makeCellActions({...props, allowActions: filteredActions});
361+
}
362+
const cellActions = makeCellActions({
363+
...props,
364+
allowActions: filteredActions,
365+
to: target,
366+
});
314367
const align = fieldAlignment(column.key as string, column.type);
315368

316369
if (useCellActionsV2 && triggerType === ActionTriggerType.BOLD_HOVER)
@@ -338,11 +391,26 @@ function CellAction({
338391
],
339392
}}
340393
trigger={triggerProps => (
341-
<ActionMenuTriggerV2 {...triggerProps} aria-label={t('Actions')}>
394+
<ActionMenuTriggerV2
395+
{...triggerProps}
396+
aria-label={t('Actions')}
397+
onClickCapture={e => {
398+
// Allow for users to hold shift, ctrl or cmd to open links instead of the menu
399+
if (e.metaKey || e.shiftKey || e.ctrlKey) {
400+
e.stopPropagation();
401+
} else {
402+
const aTags = e.currentTarget.getElementsByTagName('a');
403+
if (aTags?.[0]) {
404+
const href = aTags[0].href;
405+
setTarget(href);
406+
}
407+
e.preventDefault();
408+
}
409+
}}
410+
>
342411
{children}
343412
</ActionMenuTriggerV2>
344413
)}
345-
// So the menu doesn't fill the entire row which can lead to extremely wide menus
346414
minMenuWidth={0}
347415
/>
348416
) : (

static/app/views/explore/components/table.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const ALLOWED_CELL_ACTIONS: Actions[] = [
5555
Actions.SHOW_LESS_THAN,
5656
Actions.COPY_TO_CLIPBOARD,
5757
Actions.OPEN_EXTERNAL_LINK,
58+
Actions.OPEN_INTERNAL_LINK,
5859
];
5960

6061
const MINIMUM_COLUMN_WIDTH = COL_WIDTH_MINIMUM;

static/app/views/explore/tables/fieldRenderer.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {Tooltip} from 'sentry/components/core/tooltip';
77
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
88
import TimeSince from 'sentry/components/timeSince';
99
import {space} from 'sentry/styles/space';
10-
import type {Organization} from 'sentry/types/organization';
1110
import type {Project} from 'sentry/types/project';
1211
import {defined} from 'sentry/utils';
1312
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
@@ -122,7 +121,7 @@ function BaseExploreFieldRenderer({
122121

123122
const field = String(column.key);
124123

125-
const renderer = getExploreFieldRenderer(field, meta, projectsMap, organization);
124+
const renderer = getExploreFieldRenderer(field, meta, projectsMap);
126125

127126
let rendered = renderer(data, {
128127
location,
@@ -192,14 +191,13 @@ function BaseExploreFieldRenderer({
192191
function getExploreFieldRenderer(
193192
field: string,
194193
meta: MetaType,
195-
projects: Record<string, Project>,
196-
organization: Organization
194+
projects: Record<string, Project>
197195
): ReturnType<typeof getFieldRenderer> {
198196
if (field === 'id' || field === 'span_id') {
199197
return eventIdRenderFunc(field);
200198
}
201199
if (field === 'span.description') {
202-
return spanDescriptionRenderFunc(projects, organization);
200+
return spanDescriptionRenderFunc(projects);
203201
}
204202
return getFieldRenderer(field, meta, false);
205203
}
@@ -216,10 +214,7 @@ function eventIdRenderFunc(field: string) {
216214
return renderer;
217215
}
218216

219-
function spanDescriptionRenderFunc(
220-
projects: Record<string, Project>,
221-
organization: Organization
222-
) {
217+
function spanDescriptionRenderFunc(projects: Record<string, Project>) {
223218
function renderer(data: EventData) {
224219
const project = projects[data.project];
225220

@@ -243,8 +238,7 @@ function spanDescriptionRenderFunc(
243238
/>
244239
)}
245240
<WrappingText>
246-
{!organization.features.includes('discover-cell-actions-v2') &&
247-
isUrl(value) ? (
241+
{isUrl(value) ? (
248242
<ExternalLink href={value}>{value}</ExternalLink>
249243
) : (
250244
nullableValue(value)

static/app/views/insights/pages/platform/shared/table/TransactionCell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function TransactionCell({
5050
column: {kind: 'field', field: 'transaction'},
5151
}}
5252
dataRow={dataRow as any}
53-
allowActions={[Actions.ADD]}
53+
allowActions={[Actions.ADD, Actions.OPEN_INTERNAL_LINK]}
5454
handleCellAction={() => setTransactionFilter(transaction)}
5555
>
5656
<CellWrapper>

static/app/views/performance/table.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ class _Table extends Component<Props, State> {
280280
Actions.SHOW_GREATER_THAN,
281281
Actions.SHOW_LESS_THAN,
282282
Actions.EDIT_THRESHOLD,
283+
Actions.OPEN_EXTERNAL_LINK,
284+
Actions.OPEN_INTERNAL_LINK,
283285
];
284286

285287
const cellActions = withStaticFilters ? [] : allowActions;

static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ function EventsTable({
221221
Actions.SHOW_GREATER_THAN,
222222
Actions.SHOW_LESS_THAN,
223223
Actions.OPEN_EXTERNAL_LINK,
224+
Actions.OPEN_INTERNAL_LINK,
224225
];
225226

226227
if (['attachments', 'minidump'].includes(field)) {

static/app/views/performance/transactionSummary/transactionTags/tagValueTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export function TagValueTable({
182182
return dataRow.tags_key;
183183
}
184184

185-
const allowActions = [Actions.ADD, Actions.EXCLUDE];
185+
const allowActions = [Actions.ADD, Actions.EXCLUDE, Actions.OPEN_INTERNAL_LINK];
186186

187187
if (column.key === 'tagValue') {
188188
const actionRow = {...dataRow, id: dataRow.tags_key};

static/app/views/performance/vitalDetail/table.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ class Table extends Component<Props, State> {
153153
Actions.EXCLUDE,
154154
Actions.SHOW_GREATER_THAN,
155155
Actions.SHOW_LESS_THAN,
156+
Actions.OPEN_EXTERNAL_LINK,
157+
Actions.OPEN_INTERNAL_LINK,
156158
];
157159

158160
if (field === 'transaction') {

0 commit comments

Comments
 (0)