Skip to content

Commit 1727034

Browse files
authored
feat(cell-action): add external linking as action (#95892)
### Changes Have external linking as a dropdown menu option for `CellAction`. Removed cell actions for some ID linked fields (since it conflicts with the dropdown & should just directly open the link) ### Video https://github.com/user-attachments/assets/d86c4b1f-f33f-4f0f-8c82-ea14ed62b410
1 parent 9812e72 commit 1727034

File tree

7 files changed

+103
-39
lines changed

7 files changed

+103
-39
lines changed

static/app/utils/discover/fieldRenderers.tsx

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

332-
if (isUrl(value)) {
332+
// In the future, external linking will be done through CellAction component instead of the default renderer
333+
if (
334+
!baggage?.organization.features.includes('discover-cell-actions-v2') &&
335+
isUrl(value)
336+
) {
333337
return (
334338
<Tooltip title={value} containerDisplayMode="block" showOnlyOnOverflow>
335339
<Container>
@@ -495,7 +499,7 @@ const SPECIAL_FIELDS: Record<string, SpecialField> = {
495499
},
496500
'span.description': {
497501
sortField: 'span.description',
498-
renderFunc: data => {
502+
renderFunc: (data, {organization}) => {
499503
const value = data[SpanFields.SPAN_DESCRIPTION];
500504
const op: string = data[SpanFields.SPAN_OP];
501505
const projectId =
@@ -523,7 +527,8 @@ const SPECIAL_FIELDS: Record<string, SpecialField> = {
523527
maxWidth={400}
524528
>
525529
<Container>
526-
{isUrl(value) ? (
530+
{!organization.features.includes('discover-cell-actions-v2') &&
531+
isUrl(value) ? (
527532
<ExternalLink href={value}>{value}</ExternalLink>
528533
) : (
529534
nullableValue(value)

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

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {t, tct} from 'sentry/locale';
99
import {defined} from 'sentry/utils';
1010
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
1111
import {
12+
fieldAlignment,
1213
isEquationAlias,
1314
isRelativeSpanOperationBreakdownField,
1415
} from 'sentry/utils/discover/fields';
1516
import getDuration from 'sentry/utils/duration/getDuration';
17+
import {isUrl} from 'sentry/utils/string/isUrl';
1618
import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
1719
import useOrganization from 'sentry/utils/useOrganization';
1820

@@ -27,19 +29,7 @@ export enum Actions {
2729
DRILLDOWN = 'drilldown',
2830
EDIT_THRESHOLD = 'edit_threshold',
2931
COPY_TO_CLIPBOARD = 'copy_to_clipboard',
30-
}
31-
32-
export function copyToClipBoard(data: any) {
33-
function stringifyValue(value: any): string {
34-
if (!value) return '';
35-
if (typeof value !== 'object') {
36-
return value.toString();
37-
}
38-
return JSON.stringify(value) ?? value.toString();
39-
}
40-
navigator.clipboard.writeText(stringifyValue(data)).catch(_ => {
41-
addErrorMessage('Error copying to clipboard');
42-
});
32+
OPEN_EXTERNAL_LINK = 'open_external_link',
4333
}
4434

4535
export function updateQuery(
@@ -98,7 +88,10 @@ export function updateQuery(
9888
// these actions do not modify the query in any way,
9989
// instead they have side effects
10090
case Actions.COPY_TO_CLIPBOARD:
101-
copyToClipBoard(value);
91+
copyToClipboard(value);
92+
break;
93+
case Actions.OPEN_EXTERNAL_LINK:
94+
openExternalLink(value);
10295
break;
10396
case Actions.RELEASE:
10497
case Actions.DRILLDOWN:
@@ -151,6 +144,35 @@ export function excludeFromFilter(
151144
oldFilter.addFilterValues(negation, value);
152145
}
153146

147+
/**
148+
* Copies the provided value to a user's clipboard.
149+
* @param value
150+
*/
151+
export function copyToClipboard(value: string | number | string[]) {
152+
function stringifyValue(val: string | number | string[]): string {
153+
if (!val) return '';
154+
if (typeof val !== 'object') {
155+
return val.toString();
156+
}
157+
return JSON.stringify(val) ?? val.toString();
158+
}
159+
navigator.clipboard.writeText(stringifyValue(value)).catch(_ => {
160+
addErrorMessage('Error copying to clipboard');
161+
});
162+
}
163+
164+
/**
165+
* If provided value is a valid url, opens the url in a new tab
166+
* @param value
167+
*/
168+
export function openExternalLink(value: string | number | string[]) {
169+
if (typeof value === 'string' && isUrl(value)) {
170+
window.open(value, '_blank', 'noopener,noreferrer');
171+
} else {
172+
addErrorMessage('Could not open link');
173+
}
174+
}
175+
154176
type CellActionsOpts = {
155177
column: TableColumn<keyof TableDataRow>;
156178
dataRow: TableDataRow;
@@ -251,21 +273,47 @@ function makeCellActions({
251273

252274
if (value) addMenuItem(Actions.COPY_TO_CLIPBOARD, t('Copy to clipboard'));
253275

276+
if (isUrl(value)) addMenuItem(Actions.OPEN_EXTERNAL_LINK, t('Open external link'));
277+
254278
if (actions.length === 0) {
255279
return null;
256280
}
257281

258282
return actions;
259283
}
260284

261-
type Props = React.PropsWithoutRef<CellActionsOpts>;
285+
/**
286+
* Potentially temporary as design and product need more time to determine how logs table should trigger the dropdown.
287+
* Currently, the agreed default for every table should be bold hover. Logs is the only table to use the ellipsis trigger.
288+
*/
289+
export enum ActionTriggerType {
290+
ELLIPSIS = 'ellipsis',
291+
BOLD_HOVER = 'bold_hover',
292+
}
293+
294+
type Props = React.PropsWithoutRef<CellActionsOpts> & {
295+
triggerType?: ActionTriggerType;
296+
};
262297

263-
function CellAction(props: Props) {
298+
function CellAction({
299+
triggerType = ActionTriggerType.BOLD_HOVER,
300+
allowActions,
301+
...props
302+
}: Props) {
264303
const organization = useOrganization();
265-
const {children} = props;
266-
const cellActions = makeCellActions(props);
304+
const {children, column} = props;
305+
306+
const useCellActionsV2 = organization.features.includes('discover-cell-actions-v2');
307+
let filteredActions = allowActions;
308+
if (!useCellActionsV2)
309+
filteredActions = filteredActions?.filter(
310+
action => action !== Actions.OPEN_EXTERNAL_LINK
311+
);
312+
313+
const cellActions = makeCellActions({...props, allowActions: filteredActions});
314+
const align = fieldAlignment(column.key as string, column.type);
267315

268-
if (organization.features.includes('organizations:discover-cell-actions-v2'))
316+
if (useCellActionsV2 && triggerType === ActionTriggerType.BOLD_HOVER)
269317
return (
270318
<Container
271319
data-test-id={cellActions === null ? undefined : 'cell-action-container'}
@@ -276,10 +324,11 @@ function CellAction(props: Props) {
276324
usePortal
277325
size="sm"
278326
offset={4}
279-
position="bottom-start"
327+
position={align === 'left' ? 'bottom-start' : 'bottom-end'}
280328
preventOverflowOptions={{padding: 4}}
281329
flipOptions={{
282330
fallbackPlacements: [
331+
'bottom-start',
283332
'bottom-end',
284333
'top',
285334
'right-start',

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import {makeReleasesPathname} from 'sentry/views/releases/utils/pathnames';
6767

6868
import {QuickContextHoverWrapper} from './quickContext/quickContextWrapper';
6969
import {ContextType} from './quickContext/utils';
70-
import CellAction, {Actions, copyToClipBoard, updateQuery} from './cellAction';
70+
import CellAction, {Actions, updateQuery} from './cellAction';
7171
import ColumnEditModal, {modalCss} from './columnEditModal';
7272
import TableActions from './tableActions';
7373
import {TopResultsIndicator} from './topResultsIndicator';
@@ -615,10 +615,6 @@ function TableView(props: TableViewProps) {
615615

616616
return;
617617
}
618-
case Actions.COPY_TO_CLIPBOARD: {
619-
copyToClipBoard(value);
620-
break;
621-
}
622618
default: {
623619
// Some custom perf metrics have units.
624620
// These custom perf metrics need to be adjusted to the correct value.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const ALLOWED_CELL_ACTIONS: Actions[] = [
5454
Actions.SHOW_GREATER_THAN,
5555
Actions.SHOW_LESS_THAN,
5656
Actions.COPY_TO_CLIPBOARD,
57+
Actions.OPEN_EXTERNAL_LINK,
5758
];
5859

5960
const MINIMUM_COLUMN_WIDTH = COL_WIDTH_MINIMUM;

static/app/views/explore/logs/tables/logsTableRow.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import useOrganization from 'sentry/utils/useOrganization';
1717
import useProjectFromId from 'sentry/utils/useProjectFromId';
1818
import CellAction, {
1919
Actions,
20-
copyToClipBoard,
20+
ActionTriggerType,
21+
copyToClipboard,
22+
openExternalLink,
2123
} from 'sentry/views/discover/table/cellAction';
2224
import type {TableColumn} from 'sentry/views/discover/table/types';
2325
import {AttributesTree} from 'sentry/views/explore/components/traceItemAttributes/attributesTree';
@@ -315,7 +317,10 @@ export const LogRowContent = memo(function LogRowContent({
315317
});
316318
break;
317319
case Actions.COPY_TO_CLIPBOARD:
318-
copyToClipBoard(cellValue);
320+
copyToClipboard(cellValue);
321+
break;
322+
case Actions.OPEN_EXTERNAL_LINK:
323+
openExternalLink(cellValue);
319324
break;
320325
default:
321326
break;
@@ -326,6 +331,7 @@ export const LogRowContent = memo(function LogRowContent({
326331
? []
327332
: ALLOWED_CELL_ACTIONS
328333
}
334+
triggerType={ActionTriggerType.ELLIPSIS}
329335
>
330336
{renderedField}
331337
</CellAction>

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ 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';
1011
import type {Project} from 'sentry/types/project';
1112
import {defined} from 'sentry/utils';
1213
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
@@ -121,7 +122,7 @@ function BaseExploreFieldRenderer({
121122

122123
const field = String(column.key);
123124

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

126127
let rendered = renderer(data, {
127128
location,
@@ -145,7 +146,7 @@ function BaseExploreFieldRenderer({
145146
source: TraceViewSources.TRACES,
146147
});
147148

148-
rendered = <Link to={target}>{rendered}</Link>;
149+
return <Link to={target}>{rendered}</Link>;
149150
}
150151

151152
if (['id', 'span_id', 'transaction.id'].includes(field)) {
@@ -161,7 +162,7 @@ function BaseExploreFieldRenderer({
161162
source: TraceViewSources.TRACES,
162163
});
163164

164-
rendered = <Link to={target}>{rendered}</Link>;
165+
return <Link to={target}>{rendered}</Link>;
165166
}
166167

167168
if (field === 'profile.id') {
@@ -170,7 +171,7 @@ function BaseExploreFieldRenderer({
170171
projectSlug: data.project,
171172
profileId: data['profile.id'],
172173
});
173-
rendered = <Link to={target}>{rendered}</Link>;
174+
return <Link to={target}>{rendered}</Link>;
174175
}
175176

176177
return (
@@ -191,13 +192,14 @@ function BaseExploreFieldRenderer({
191192
function getExploreFieldRenderer(
192193
field: string,
193194
meta: MetaType,
194-
projects: Record<string, Project>
195+
projects: Record<string, Project>,
196+
organization: Organization
195197
): ReturnType<typeof getFieldRenderer> {
196198
if (field === 'id' || field === 'span_id') {
197199
return eventIdRenderFunc(field);
198200
}
199201
if (field === 'span.description') {
200-
return spanDescriptionRenderFunc(projects);
202+
return spanDescriptionRenderFunc(projects, organization);
201203
}
202204
return getFieldRenderer(field, meta, false);
203205
}
@@ -214,7 +216,10 @@ function eventIdRenderFunc(field: string) {
214216
return renderer;
215217
}
216218

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

@@ -238,7 +243,8 @@ function spanDescriptionRenderFunc(projects: Record<string, Project>) {
238243
/>
239244
)}
240245
<WrappingText>
241-
{isUrl(value) ? (
246+
{!organization.features.includes('discover-cell-actions-v2') &&
247+
isUrl(value) ? (
242248
<ExternalLink href={value}>{value}</ExternalLink>
243249
) : (
244250
nullableValue(value)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ function EventsTable({
220220
Actions.EXCLUDE,
221221
Actions.SHOW_GREATER_THAN,
222222
Actions.SHOW_LESS_THAN,
223+
Actions.OPEN_EXTERNAL_LINK,
223224
];
224225

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

0 commit comments

Comments
 (0)