@@ -9,10 +9,12 @@ import {t, tct} from 'sentry/locale';
9
9
import { defined } from 'sentry/utils' ;
10
10
import type { TableDataRow } from 'sentry/utils/discover/discoverQuery' ;
11
11
import {
12
+ fieldAlignment ,
12
13
isEquationAlias ,
13
14
isRelativeSpanOperationBreakdownField ,
14
15
} from 'sentry/utils/discover/fields' ;
15
16
import getDuration from 'sentry/utils/duration/getDuration' ;
17
+ import { isUrl } from 'sentry/utils/string/isUrl' ;
16
18
import type { MutableSearch } from 'sentry/utils/tokenizeSearch' ;
17
19
import useOrganization from 'sentry/utils/useOrganization' ;
18
20
@@ -27,19 +29,7 @@ export enum Actions {
27
29
DRILLDOWN = 'drilldown' ,
28
30
EDIT_THRESHOLD = 'edit_threshold' ,
29
31
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' ,
43
33
}
44
34
45
35
export function updateQuery (
@@ -98,7 +88,10 @@ export function updateQuery(
98
88
// these actions do not modify the query in any way,
99
89
// instead they have side effects
100
90
case Actions . COPY_TO_CLIPBOARD :
101
- copyToClipBoard ( value ) ;
91
+ copyToClipboard ( value ) ;
92
+ break ;
93
+ case Actions . OPEN_EXTERNAL_LINK :
94
+ openExternalLink ( value ) ;
102
95
break ;
103
96
case Actions . RELEASE :
104
97
case Actions . DRILLDOWN :
@@ -151,6 +144,35 @@ export function excludeFromFilter(
151
144
oldFilter . addFilterValues ( negation , value ) ;
152
145
}
153
146
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
+
154
176
type CellActionsOpts = {
155
177
column : TableColumn < keyof TableDataRow > ;
156
178
dataRow : TableDataRow ;
@@ -251,21 +273,47 @@ function makeCellActions({
251
273
252
274
if ( value ) addMenuItem ( Actions . COPY_TO_CLIPBOARD , t ( 'Copy to clipboard' ) ) ;
253
275
276
+ if ( isUrl ( value ) ) addMenuItem ( Actions . OPEN_EXTERNAL_LINK , t ( 'Open external link' ) ) ;
277
+
254
278
if ( actions . length === 0 ) {
255
279
return null ;
256
280
}
257
281
258
282
return actions ;
259
283
}
260
284
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
+ } ;
262
297
263
- function CellAction ( props : Props ) {
298
+ function CellAction ( {
299
+ triggerType = ActionTriggerType . BOLD_HOVER ,
300
+ allowActions,
301
+ ...props
302
+ } : Props ) {
264
303
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 ) ;
267
315
268
- if ( organization . features . includes ( 'organizations:discover-cell-actions-v2' ) )
316
+ if ( useCellActionsV2 && triggerType === ActionTriggerType . BOLD_HOVER )
269
317
return (
270
318
< Container
271
319
data-test-id = { cellActions === null ? undefined : 'cell-action-container' }
@@ -276,10 +324,11 @@ function CellAction(props: Props) {
276
324
usePortal
277
325
size = "sm"
278
326
offset = { 4 }
279
- position = " bottom-start"
327
+ position = { align === 'left' ? ' bottom-start' : 'bottom-end' }
280
328
preventOverflowOptions = { { padding : 4 } }
281
329
flipOptions = { {
282
330
fallbackPlacements : [
331
+ 'bottom-start' ,
283
332
'bottom-end' ,
284
333
'top' ,
285
334
'right-start' ,
0 commit comments