Skip to content

Commit 5fbf29c

Browse files
authored
feat(dashboards): open spans table row in explore (#97703)
### Changes Add a cell action that opens the widget table row in explore, which add the table row in the search bar so only relevant samples appear. Also filtered out duplicate columns (from the aggregates) and filtered out `timestamp` from `groupBy` as it was breaking the charts. Only visible when the `discover-cell-actions-v2` flag is enabled. ### Video https://github.com/user-attachments/assets/036b2eff-6369-4cfc-94cd-211d091ee0ad
1 parent 27f8f8b commit 5fbf29c

File tree

8 files changed

+140
-54
lines changed

8 files changed

+140
-54
lines changed

static/app/components/modals/widgetViewerModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ import useProjects from 'sentry/utils/useProjects';
7070
import {useUser} from 'sentry/utils/useUser';
7171
import {useUserTeams} from 'sentry/utils/useUserTeams';
7272
import withPageFilters from 'sentry/utils/withPageFilters';
73+
// eslint-disable-next-line no-restricted-imports
74+
import withSentryRouter from 'sentry/utils/withSentryRouter';
7375
import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
7476
import {checkUserHasEditAccess} from 'sentry/views/dashboards/detail';
7577
import {DiscoverSplitAlert} from 'sentry/views/dashboards/discoverSplitAlert';
@@ -171,7 +173,9 @@ const MemoizedWidgetCardChartContainer = memo(
171173
WidgetCardChartContainer,
172174
shouldWidgetCardChartMemo
173175
);
174-
const MemoizedWidgetCardChart = memo(WidgetCardChart, shouldWidgetCardChartMemo);
176+
const MemoizedWidgetCardChart = withSentryRouter(
177+
memo(WidgetCardChart, shouldWidgetCardChartMemo)
178+
);
175179

176180
async function fetchDiscoverTotal(
177181
api: Client,
@@ -921,7 +925,6 @@ function WidgetViewerModal(props: Props) {
921925
tableResults={tableData}
922926
errorMessage={undefined}
923927
loading={false}
924-
location={location}
925928
widget={widget}
926929
selection={selection}
927930
organization={organization}

static/app/views/dashboards/utils/getWidgetExploreUrl.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from 'sentry/utils/discover/fields';
1212
import {FieldKind, getFieldDefinition} from 'sentry/utils/fields';
1313
import {decodeBoolean, decodeScalar, decodeSorts} from 'sentry/utils/queryString';
14+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
1415
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
1516
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
1617
import {DisplayType} from 'sentry/views/dashboards/types';
@@ -19,6 +20,7 @@ import {
1920
eventViewFromWidget,
2021
getWidgetInterval,
2122
} from 'sentry/views/dashboards/utils';
23+
import type {TabularRow} from 'sentry/views/dashboards/widgets/common/types';
2224
import {
2325
LOGS_AGGREGATE_FN_KEY,
2426
LOGS_AGGREGATE_PARAM_KEY,
@@ -162,7 +164,8 @@ function _getWidgetExploreUrl(
162164
dashboardFilters: DashboardFilters | undefined,
163165
selection: PageFilters,
164166
organization: Organization,
165-
preferMode?: Mode
167+
preferMode?: Mode,
168+
overrideQuery?: MutableSearch
166169
) {
167170
const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection);
168171
const locationQueryParams = eventView.generateQueryStringObject();
@@ -215,7 +218,9 @@ function _getWidgetExploreUrl(
215218

216219
let groupBy: string[] =
217220
defined(query.fields) && widget.displayType === DisplayType.TABLE
218-
? query.fields.filter(field => !isAggregateFieldOrEquation(field))
221+
? query.fields.filter(
222+
field => !isAggregateFieldOrEquation(field) && field !== 'timestamp'
223+
)
219224
: [...query.columns];
220225
if (groupBy && groupBy.length === 0) {
221226
// Force the groupBy to be an array with a single empty string
@@ -226,7 +231,7 @@ function _getWidgetExploreUrl(
226231
}
227232

228233
const yAxisFields: string[] = locationQueryParams.yAxes.flatMap(getAggregateArguments);
229-
const fields = [...groupBy, ...yAxisFields].filter(Boolean);
234+
const fields = [...new Set([...groupBy, ...yAxisFields])].filter(Boolean);
230235

231236
const sortDirection = widget.queries[0]?.orderby?.startsWith('-') ? '-' : '';
232237
const sortColumn = trimStart(widget.queries[0]?.orderby ?? '', '-');
@@ -273,7 +278,7 @@ function _getWidgetExploreUrl(
273278
groupBy: visualize.length > 0 ? groupBy : [],
274279
field: fields,
275280
query: applyDashboardFilters(
276-
decodeScalar(locationQueryParams.query),
281+
overrideQuery?.formatString() ?? decodeScalar(locationQueryParams.query),
277282
dashboardFilters
278283
),
279284
sort: sort || undefined,
@@ -325,3 +330,40 @@ function _getWidgetExploreUrlForMultipleQueries(
325330
interval: getWidgetInterval(widget, currentSelection.datetime),
326331
});
327332
}
333+
334+
export function getWidgetTableRowExploreUrlFunction(
335+
selection: PageFilters,
336+
widget: Widget,
337+
organization: Organization,
338+
dashboardFilters?: DashboardFilters
339+
) {
340+
return (dataRow: TabularRow) => {
341+
let fields: string[] = [];
342+
if (widget.queries[0]?.fields) {
343+
fields = widget.queries[0].fields.filter(
344+
(field: string) => !isAggregateFieldOrEquation(field)
345+
);
346+
}
347+
348+
const query = new MutableSearch('');
349+
fields.map(field => {
350+
const value = dataRow[field];
351+
if (!defined(value)) {
352+
return query.addFilterValue('!has', field);
353+
}
354+
if (Array.isArray(value)) {
355+
return query.addFilterValues(field, value);
356+
}
357+
return query.addFilterValue(field, String(value));
358+
});
359+
360+
return _getWidgetExploreUrl(
361+
widget,
362+
dashboardFilters,
363+
selection,
364+
organization,
365+
Mode.SAMPLES,
366+
query
367+
);
368+
};
369+
}

static/app/views/dashboards/widgetCard/chart.tsx

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {Theme} from '@emotion/react';
44
import {withTheme} from '@emotion/react';
55
import styled from '@emotion/styled';
66
import type {LegendComponentOption} from 'echarts';
7-
import type {Location} from 'history';
87
import isEqual from 'lodash/isEqual';
98
import omit from 'lodash/omit';
109

@@ -31,6 +30,7 @@ import type {
3130
EChartEventHandler,
3231
ReactEchartsRef,
3332
} from 'sentry/types/echarts';
33+
import type {InjectedRouter, WithRouterProps} from 'sentry/types/legacyReactRouter';
3434
import type {Confidence, Organization} from 'sentry/types/organization';
3535
import {defined} from 'sentry/utils';
3636
import {
@@ -58,19 +58,22 @@ import {
5858
import getDynamicText from 'sentry/utils/getDynamicText';
5959
import {decodeSorts} from 'sentry/utils/queryString';
6060
import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
61-
import type {Widget} from 'sentry/views/dashboards/types';
61+
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
6262
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
6363
import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
6464
import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize';
65+
import {getWidgetTableRowExploreUrlFunction} from 'sentry/views/dashboards/utils/getWidgetExploreUrl';
6566
import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
6667
import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
6768
import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization';
69+
import {ALLOWED_CELL_ACTIONS} from 'sentry/views/dashboards/widgets/common/settings';
6870
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
6971
import {TableWidgetVisualization} from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';
7072
import {
7173
convertTableDataToTabularData,
7274
decodeColumnAliases,
7375
} from 'sentry/views/dashboards/widgets/tableWidget/utils';
76+
import {Actions} from 'sentry/views/discover/table/cellAction';
7477
import {decodeColumnOrder} from 'sentry/views/discover/utils';
7578
import {ConfidenceFooter} from 'sentry/views/explore/spans/charts/confidenceFooter';
7679

@@ -87,38 +90,40 @@ type TableResultProps = Pick<
8790
type WidgetCardChartProps = Pick<
8891
GenericWidgetQueriesChildrenProps,
8992
'timeseriesResults' | 'tableResults' | 'errorMessage' | 'loading'
90-
> & {
91-
location: Location;
92-
organization: Organization;
93-
selection: PageFilters;
94-
theme: Theme;
95-
widget: Widget;
96-
widgetLegendState: WidgetLegendSelectionState;
97-
chartGroup?: string;
98-
confidence?: Confidence;
99-
disableTableActions?: boolean;
100-
disableZoom?: boolean;
101-
expandNumbers?: boolean;
102-
isMobile?: boolean;
103-
isSampled?: boolean | null;
104-
legendOptions?: LegendComponentOption;
105-
minTableColumnWidth?: number;
106-
noPadding?: boolean;
107-
onLegendSelectChanged?: EChartEventHandler<{
108-
name: string;
109-
selected: Record<string, boolean>;
110-
type: 'legendselectchanged';
111-
}>;
112-
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
113-
onWidgetTableSort?: (sort: Sort) => void;
114-
onZoom?: EChartDataZoomHandler;
115-
sampleCount?: number;
116-
shouldResize?: boolean;
117-
showConfidenceWarning?: boolean;
118-
showLoadingText?: boolean;
119-
timeseriesResultsTypes?: Record<string, AggregationOutputType>;
120-
windowWidth?: number;
121-
};
93+
> &
94+
WithRouterProps & {
95+
organization: Organization;
96+
selection: PageFilters;
97+
theme: Theme;
98+
widget: Widget;
99+
widgetLegendState: WidgetLegendSelectionState;
100+
chartGroup?: string;
101+
confidence?: Confidence;
102+
dashboardFilters?: DashboardFilters;
103+
disableTableActions?: boolean;
104+
disableZoom?: boolean;
105+
expandNumbers?: boolean;
106+
isMobile?: boolean;
107+
isSampled?: boolean | null;
108+
legendOptions?: LegendComponentOption;
109+
minTableColumnWidth?: number;
110+
noPadding?: boolean;
111+
onLegendSelectChanged?: EChartEventHandler<{
112+
name: string;
113+
selected: Record<string, boolean>;
114+
type: 'legendselectchanged';
115+
}>;
116+
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
117+
onWidgetTableSort?: (sort: Sort) => void;
118+
onZoom?: EChartDataZoomHandler;
119+
router?: InjectedRouter;
120+
sampleCount?: number;
121+
shouldResize?: boolean;
122+
showConfidenceWarning?: boolean;
123+
showLoadingText?: boolean;
124+
timeseriesResultsTypes?: Record<string, AggregationOutputType>;
125+
windowWidth?: number;
126+
};
122127

123128
class WidgetCardChart extends Component<WidgetCardChartProps> {
124129
shouldComponentUpdate(nextProps: WidgetCardChartProps): boolean {
@@ -162,6 +167,8 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
162167
onWidgetTableSort,
163168
onWidgetTableResizeColumn,
164169
disableTableActions,
170+
dashboardFilters,
171+
router,
165172
} = this.props;
166173
if (loading || !tableResults?.[0]) {
167174
// Align height to other charts.
@@ -208,6 +215,13 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
208215
}
209216

210217
const useCellActionsV2 = organization.features.includes('discover-cell-actions-v2');
218+
let cellActions = ALLOWED_CELL_ACTIONS;
219+
if (disableTableActions || !useCellActionsV2) {
220+
cellActions = [];
221+
} else if (widget.widgetType === WidgetType.SPANS) {
222+
cellActions = [...ALLOWED_CELL_ACTIONS, Actions.OPEN_ROW_IN_EXPLORE];
223+
}
224+
211225
return (
212226
<TableWrapper key={`table:${result.title}`}>
213227
{organization.features.includes('dashboards-use-widget-table-visualization') ? (
@@ -242,9 +256,18 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
242256
} satisfies RenderFunctionBaggage;
243257
}}
244258
onResizeColumn={onWidgetTableResizeColumn}
245-
allowedCellActions={
246-
disableTableActions || !useCellActionsV2 ? [] : undefined
247-
}
259+
allowedCellActions={cellActions}
260+
onTriggerCellAction={(action, _value, dataRow) => {
261+
if (action === Actions.OPEN_ROW_IN_EXPLORE) {
262+
const getExploreUrl = getWidgetTableRowExploreUrlFunction(
263+
selection,
264+
widget,
265+
organization,
266+
dashboardFilters
267+
);
268+
router.push(getExploreUrl(dataRow));
269+
}
270+
}}
248271
/>
249272
) : (
250273
<StyledSimpleTableChart

static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import type {Organization} from 'sentry/types/organization';
1919
import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
2020
import type {AggregationOutputType, Sort} from 'sentry/utils/discover/fields';
2121
import {useLocation} from 'sentry/utils/useLocation';
22+
// eslint-disable-next-line no-restricted-imports
23+
import withSentryRouter from 'sentry/utils/withSentryRouter';
2224
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
2325
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
2426
import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
@@ -70,6 +72,8 @@ type Props = {
7072
windowWidth?: number;
7173
};
7274

75+
const WidgetCardChartWithRouter = withSentryRouter(WidgetCardChart);
76+
7377
export function WidgetCardChartContainer({
7478
organization,
7579
selection,
@@ -190,13 +194,12 @@ export function WidgetCardChartContainer({
190194
{typeof renderErrorMessage === 'function'
191195
? renderErrorMessage(errorOrEmptyMessage)
192196
: null}
193-
<WidgetCardChart
197+
<WidgetCardChartWithRouter
194198
disableZoom={disableZoom}
195199
timeseriesResults={modifiedTimeseriesResults}
196200
tableResults={tableResults}
197201
errorMessage={errorOrEmptyMessage}
198202
loading={loading}
199-
location={location}
200203
widget={widget}
201204
selection={selection}
202205
organization={organization}
@@ -227,6 +230,7 @@ export function WidgetCardChartContainer({
227230
onWidgetTableSort={onWidgetTableSort}
228231
onWidgetTableResizeColumn={onWidgetTableResizeColumn}
229232
disableTableActions={disableTableActions}
233+
dashboardFilters={dashboardFilters}
230234
/>
231235
</Fragment>
232236
);

static/app/views/dashboards/widgets/common/settings.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {t} from 'sentry/locale';
22
import {space} from 'sentry/styles/space';
3+
import {Actions} from 'sentry/views/discover/table/cellAction';
34

45
// NOTE: This is a subset! Some types like `"string"`, `"date"`, and
56
// `"percent_change"` are not supported yet. Once we support them, we can remove
@@ -26,3 +27,9 @@ export const DEFAULT_FIELD = 'unknown'; // Numeric data might, in theory, have a
2627
export const MISSING_DATA_MESSAGE = t('No Data');
2728
export const NO_PLOTTABLE_VALUES = t('The data does not contain any plottable values.');
2829
export const NON_FINITE_NUMBER_MESSAGE = t('Value is not a finite number.');
30+
31+
export const ALLOWED_CELL_ACTIONS = [
32+
Actions.OPEN_INTERNAL_LINK,
33+
Actions.COPY_TO_CLIPBOARD,
34+
Actions.OPEN_EXTERNAL_LINK,
35+
];

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ describe('TableWidgetVisualization', function () {
285285

286286
it('Uses onTriggerCellAction if supplied on action click', async function () {
287287
const onTriggerCellActionMock = jest.fn(
288-
(_actions: Actions, _value: string | number) => {}
288+
(_actions: Actions, _value: string | number, _dataRow: TabularRow) => {}
289289
);
290290
Object.assign(navigator, {
291291
clipboard: {
@@ -307,7 +307,8 @@ describe('TableWidgetVisualization', function () {
307307
await waitFor(() =>
308308
expect(onTriggerCellActionMock).toHaveBeenCalledWith(
309309
Actions.COPY_TO_CLIPBOARD,
310-
sampleHTTPRequestTableData.data[0]!['http.request_method']
310+
sampleHTTPRequestTableData.data[0]!['http.request_method'],
311+
sampleHTTPRequestTableData.data[0]
311312
)
312313
);
313314
});

0 commit comments

Comments
 (0)