diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 6decdcac8f9c2d..2d066516681a41 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -322,7 +322,7 @@ export const FIELD_FORMATTERS: FieldFormatters = { }, string: { isSortable: true, - renderFunc: (field, data) => { + renderFunc: (field, data, baggage) => { // Some fields have long arrays in them, only show the tail of the data. const value = Array.isArray(data[field]) ? data[field].slice(-1) @@ -330,7 +330,11 @@ export const FIELD_FORMATTERS: FieldFormatters = { ? data[field] : emptyValue; - if (isUrl(value)) { + // In the future, external linking will be done through CellAction component instead of the default renderer + if ( + !baggage?.organization.features.includes('discover-cell-actions-v2') && + isUrl(value) + ) { return ( @@ -496,7 +500,7 @@ const SPECIAL_FIELDS: Record = { }, 'span.description': { sortField: 'span.description', - renderFunc: data => { + renderFunc: (data, {organization}) => { const value = data[SpanFields.SPAN_DESCRIPTION]; const op: string = data[SpanFields.SPAN_OP]; const projectId = @@ -524,7 +528,8 @@ const SPECIAL_FIELDS: Record = { maxWidth={400} > - {isUrl(value) ? ( + {!organization.features.includes('discover-cell-actions-v2') && + isUrl(value) ? ( {value} ) : ( nullableValue(value) diff --git a/static/app/views/discover/table/cellAction.tsx b/static/app/views/discover/table/cellAction.tsx index 38fc45879332fc..2136887a1e3339 100644 --- a/static/app/views/discover/table/cellAction.tsx +++ b/static/app/views/discover/table/cellAction.tsx @@ -9,10 +9,12 @@ import {t, tct} from 'sentry/locale'; import {defined} from 'sentry/utils'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; import { + fieldAlignment, isEquationAlias, isRelativeSpanOperationBreakdownField, } from 'sentry/utils/discover/fields'; import getDuration from 'sentry/utils/duration/getDuration'; +import {isUrl} from 'sentry/utils/string/isUrl'; import type {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useOrganization from 'sentry/utils/useOrganization'; @@ -27,19 +29,7 @@ export enum Actions { DRILLDOWN = 'drilldown', EDIT_THRESHOLD = 'edit_threshold', COPY_TO_CLIPBOARD = 'copy_to_clipboard', -} - -export function copyToClipBoard(data: any) { - function stringifyValue(value: any): string { - if (!value) return ''; - if (typeof value !== 'object') { - return value.toString(); - } - return JSON.stringify(value) ?? value.toString(); - } - navigator.clipboard.writeText(stringifyValue(data)).catch(_ => { - addErrorMessage('Error copying to clipboard'); - }); + OPEN_EXTERNAL_LINK = 'open_external_link', } export function updateQuery( @@ -98,7 +88,10 @@ export function updateQuery( // these actions do not modify the query in any way, // instead they have side effects case Actions.COPY_TO_CLIPBOARD: - copyToClipBoard(value); + copyToClipboard(value); + break; + case Actions.OPEN_EXTERNAL_LINK: + openExternalLink(value); break; case Actions.RELEASE: case Actions.DRILLDOWN: @@ -151,6 +144,35 @@ export function excludeFromFilter( oldFilter.addFilterValues(negation, value); } +/** + * Copies the provided value to a user's clipboard. + * @param value + */ +export function copyToClipboard(value: string | number | string[]) { + function stringifyValue(val: string | number | string[]): string { + if (!val) return ''; + if (typeof val !== 'object') { + return val.toString(); + } + return JSON.stringify(val) ?? val.toString(); + } + navigator.clipboard.writeText(stringifyValue(value)).catch(_ => { + addErrorMessage('Error copying to clipboard'); + }); +} + +/** + * If provided value is a valid url, opens the url in a new tab + * @param value + */ +export function openExternalLink(value: string | number | string[]) { + if (typeof value === 'string' && isUrl(value)) { + window.open(value, '_blank', 'noopener,noreferrer'); + } else { + addErrorMessage('Could not open link'); + } +} + type CellActionsOpts = { column: TableColumn; dataRow: TableDataRow; @@ -251,6 +273,8 @@ function makeCellActions({ if (value) addMenuItem(Actions.COPY_TO_CLIPBOARD, t('Copy to clipboard')); + if (isUrl(value)) addMenuItem(Actions.OPEN_EXTERNAL_LINK, t('Open external link')); + if (actions.length === 0) { return null; } @@ -258,14 +282,38 @@ function makeCellActions({ return actions; } -type Props = React.PropsWithoutRef; +/** + * Potentially temporary as design and product need more time to determine how logs table should trigger the dropdown. + * Currently, the agreed default for every table should be bold hover. Logs is the only table to use the ellipsis trigger. + */ +export enum ActionTriggerType { + ELLIPSIS = 'ellipsis', + BOLD_HOVER = 'bold_hover', +} + +type Props = React.PropsWithoutRef & { + triggerType?: ActionTriggerType; +}; -function CellAction(props: Props) { +function CellAction({ + triggerType = ActionTriggerType.BOLD_HOVER, + allowActions, + ...props +}: Props) { const organization = useOrganization(); - const {children} = props; - const cellActions = makeCellActions(props); + const {children, column} = props; + + const useCellActionsV2 = organization.features.includes('discover-cell-actions-v2'); + let filteredActions = allowActions; + if (!useCellActionsV2) + filteredActions = filteredActions?.filter( + action => action !== Actions.OPEN_EXTERNAL_LINK + ); + + const cellActions = makeCellActions({...props, allowActions: filteredActions}); + const align = fieldAlignment(column.key as string, column.type); - if (organization.features.includes('organizations:discover-cell-actions-v2')) + if (useCellActionsV2 && triggerType === ActionTriggerType.BOLD_HOVER) return ( {renderedField} diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx index 5ae471eb329355..521f04608a6ee1 100644 --- a/static/app/views/explore/tables/fieldRenderer.tsx +++ b/static/app/views/explore/tables/fieldRenderer.tsx @@ -8,6 +8,7 @@ import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import ExternalLink from 'sentry/components/links/externalLink'; import TimeSince from 'sentry/components/timeSince'; import {space} from 'sentry/styles/space'; +import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; @@ -122,7 +123,7 @@ function BaseExploreFieldRenderer({ const field = String(column.key); - const renderer = getExploreFieldRenderer(field, meta, projectsMap); + const renderer = getExploreFieldRenderer(field, meta, projectsMap, organization); let rendered = renderer(data, { location, @@ -146,7 +147,7 @@ function BaseExploreFieldRenderer({ source: TraceViewSources.TRACES, }); - rendered = {rendered}; + return {rendered}; } if (['id', 'span_id', 'transaction.id'].includes(field)) { @@ -162,7 +163,7 @@ function BaseExploreFieldRenderer({ source: TraceViewSources.TRACES, }); - rendered = {rendered}; + return {rendered}; } if (field === 'profile.id') { @@ -171,7 +172,7 @@ function BaseExploreFieldRenderer({ projectSlug: data.project, profileId: data['profile.id'], }); - rendered = {rendered}; + return {rendered}; } return ( @@ -192,13 +193,14 @@ function BaseExploreFieldRenderer({ function getExploreFieldRenderer( field: string, meta: MetaType, - projects: Record + projects: Record, + organization: Organization ): ReturnType { if (field === 'id' || field === 'span_id') { return eventIdRenderFunc(field); } if (field === 'span.description') { - return spanDescriptionRenderFunc(projects); + return spanDescriptionRenderFunc(projects, organization); } return getFieldRenderer(field, meta, false); } @@ -215,7 +217,10 @@ function eventIdRenderFunc(field: string) { return renderer; } -function spanDescriptionRenderFunc(projects: Record) { +function spanDescriptionRenderFunc( + projects: Record, + organization: Organization +) { function renderer(data: EventData) { const project = projects[data.project]; @@ -239,7 +244,8 @@ function spanDescriptionRenderFunc(projects: Record) { /> )} - {isUrl(value) ? ( + {!organization.features.includes('discover-cell-actions-v2') && + isUrl(value) ? ( {value} ) : ( nullableValue(value) diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 08cae0acdbd361..d2e32e9cbba506 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -220,6 +220,7 @@ function EventsTable({ Actions.EXCLUDE, Actions.SHOW_GREATER_THAN, Actions.SHOW_LESS_THAN, + Actions.OPEN_EXTERNAL_LINK, ]; if (['attachments', 'minidump'].includes(field)) {