Skip to content

Commit 6f02c50

Browse files
authored
feat(dashboards): add details widget type (#104059)
1. Adds a detail widget. This widget accepts a query and will render the first example that matches the query based on the type of span it is. <img width="1090" height="859" alt="image" src="https://github.com/user-attachments/assets/fb97bdd2-9c3e-4d6f-a670-23b4d8cc4692" /> - For now, this only applies to spans and i'm just using the existing`fullSpanDescription` component to do this, but we can iterate on this later on. - This is conditionally added as an option in the widget type dropdown if the feature flag is on. - There's still some UI jank to sort out, i figured it's easier to split it up so the PR doesn't get too large. - I still need to register this on the backend, this will allow me to save a widget of type `details` and test it properly (#104062) 2. Adds a `isChartDisplayType` function. I noticed there's a TON of places that checks isChartWidget. Now that we have 3 non-chart displays i just consolidated the logic to one place.
1 parent b674a07 commit 6f02c50

19 files changed

+267
-53
lines changed

static/app/views/dashboards/datasetConfig/spans.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export const SpansConfig: DatasetConfig<
193193
DisplayType.LINE,
194194
DisplayType.TABLE,
195195
DisplayType.TOP_N,
196+
DisplayType.DETAILS,
196197
],
197198
getTableRequest: (
198199
api: Client,

static/app/views/dashboards/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export enum DisplayType {
2424
LINE = 'line',
2525
TABLE = 'table',
2626
BIG_NUMBER = 'big_number',
27+
DETAILS = 'details',
2728
TOP_N = 'top_n',
2829
}
2930

static/app/views/dashboards/utils.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,3 +702,12 @@ export function applyDashboardFilters(
702702
}
703703
return baseQuery;
704704
}
705+
706+
export const isChartDisplayType = (displayType?: DisplayType) => {
707+
if (!displayType) {
708+
return true;
709+
}
710+
return ![DisplayType.BIG_NUMBER, DisplayType.TABLE, DisplayType.DETAILS].includes(
711+
displayType
712+
);
713+
};

static/app/views/dashboards/widgetBuilder/components/sortBySelectors.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export function SortBySelectors({
144144
title={disableSortReason}
145145
disabled={!disableSort || (disableSortDirection && disableSort)}
146146
>
147-
{displayType === DisplayType.TABLE ? (
147+
{displayType === DisplayType.TABLE || displayType === DisplayType.DETAILS ? (
148148
<Select
149149
name="sortBy"
150150
aria-label={t('Sort by')}

static/app/views/dashboards/widgetBuilder/components/typeSelector.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
44
import {Select} from 'sentry/components/core/select';
55
import {components} from 'sentry/components/forms/controls/reactSelectWrapper';
66
import FieldGroup from 'sentry/components/forms/fieldGroup';
7-
import {IconGraph, IconNumber, IconTable} from 'sentry/icons';
7+
import {IconGraph, IconNumber, IconSettings, IconTable} from 'sentry/icons';
88
import {t} from 'sentry/locale';
99
import {space} from 'sentry/styles/space';
1010
import {trackAnalytics} from 'sentry/utils/analytics';
@@ -24,15 +24,16 @@ const typeIcons = {
2424
[DisplayType.LINE]: <IconGraph key="line" type="line" />,
2525
[DisplayType.TABLE]: <IconTable key="table" />,
2626
[DisplayType.BIG_NUMBER]: <IconNumber key="number" />,
27+
[DisplayType.DETAILS]: <IconSettings key="details" />,
2728
};
2829

29-
const displayTypes = {
30+
const BASE_DISPLAY_TYPES: Partial<Record<DisplayType, string>> = {
3031
[DisplayType.AREA]: t('Area'),
3132
[DisplayType.BAR]: t('Bar'),
3233
[DisplayType.LINE]: t('Line'),
3334
[DisplayType.TABLE]: t('Table'),
3435
[DisplayType.BIG_NUMBER]: t('Big Number'),
35-
};
36+
} as const;
3637

3738
interface WidgetBuilderTypeSelectorProps {
3839
error?: Record<string, any>;
@@ -46,6 +47,11 @@ function WidgetBuilderTypeSelector({error, setError}: WidgetBuilderTypeSelectorP
4647
const isEditing = useIsEditingWidget();
4748
const organization = useOrganization();
4849

50+
const displayTypes = {...BASE_DISPLAY_TYPES};
51+
if (organization.features.includes('dashboards-details-widget')) {
52+
displayTypes[DisplayType.DETAILS] = t('Details');
53+
}
54+
4955
return (
5056
<Fragment>
5157
<SectionHeader

static/app/views/dashboards/widgetBuilder/components/visualize/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
WidgetType,
4242
type LinkedDashboard,
4343
} from 'sentry/views/dashboards/types';
44+
import {isChartDisplayType} from 'sentry/views/dashboards/utils';
4445
import {SectionHeader} from 'sentry/views/dashboards/widgetBuilder/components/common/sectionHeader';
4546
import SortableVisualizeFieldWrapper from 'sentry/views/dashboards/widgetBuilder/components/common/sortableFieldWrapper';
4647
import {ExploreArithmeticBuilder} from 'sentry/views/dashboards/widgetBuilder/components/exploreArithmeticBuilder';
@@ -277,9 +278,7 @@ function Visualize({error, setError}: VisualizeProps) {
277278
const isEditing = useIsEditingWidget();
278279
const disableTransactionWidget = useDisableTransactionWidget();
279280

280-
const isChartWidget =
281-
state.displayType !== DisplayType.TABLE &&
282-
state.displayType !== DisplayType.BIG_NUMBER;
281+
const isChartWidget = isChartDisplayType(state.displayType);
283282
const isBigNumberWidget = state.displayType === DisplayType.BIG_NUMBER;
284283
const isTableWidget = state.displayType === DisplayType.TABLE;
285284
const {tags: numericSpanTags} = useTraceItemTags('number');

static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {AggregationKey} from 'sentry/utils/fields';
2020
import useOrganization from 'sentry/utils/useOrganization';
2121
import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
2222
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
23+
import {isChartDisplayType} from 'sentry/views/dashboards/utils';
2324
import {
2425
AggregateCompactSelect,
2526
getAggregateValueKey,
@@ -116,9 +117,7 @@ export function SelectRow({
116117
const datasetConfig = getDatasetConfig(state.dataset);
117118
const columnSelectRef = useRef<HTMLDivElement>(null);
118119

119-
const isChartWidget =
120-
state.displayType !== DisplayType.TABLE &&
121-
state.displayType !== DisplayType.BIG_NUMBER;
120+
const isChartWidget = isChartDisplayType(state.displayType);
122121

123122
const updateAction = isChartWidget
124123
? BuilderStateAction.SET_Y_AXIS

static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
type DashboardFilters,
2929
type Widget,
3030
} from 'sentry/views/dashboards/types';
31+
import {isChartDisplayType} from 'sentry/views/dashboards/utils';
3132
import {animationTransitionSettings} from 'sentry/views/dashboards/widgetBuilder/components/common/animationSettings';
3233
import WidgetBuilderDatasetSelector from 'sentry/views/dashboards/widgetBuilder/components/datasetSelector';
3334
import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar';
@@ -122,9 +123,9 @@ function WidgetBuilderSlideout({
122123
: isEditing
123124
? t('Edit Widget')
124125
: t('Custom Widget Builder');
125-
const isChartWidget =
126-
state.displayType !== DisplayType.BIG_NUMBER &&
127-
state.displayType !== DisplayType.TABLE;
126+
const isChartWidget = isChartDisplayType(state.displayType);
127+
128+
const showVisualizeSection = state.displayType !== DisplayType.DETAILS;
128129

129130
const customPreviewRef = useRef<HTMLDivElement>(null);
130131
const templatesPreviewRef = useRef<HTMLDivElement>(null);
@@ -340,9 +341,11 @@ function WidgetBuilderSlideout({
340341
<WidgetBuilderFilterBar releases={dashboard.filters?.release ?? []} />
341342
</Section>
342343
)}
343-
<Section>
344-
<Visualize error={error} setError={setError} />
345-
</Section>
344+
{showVisualizeSection && (
345+
<Section>
346+
<Visualize error={error} setError={setError} />
347+
</Section>
348+
)}
346349
<Section>
347350
<WidgetBuilderQueryFilterBuilder
348351
onQueryConditionChange={onQueryConditionChange}

static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type DashboardDetails,
1515
type DashboardFilters,
1616
} from 'sentry/views/dashboards/types';
17+
import {isChartDisplayType} from 'sentry/views/dashboards/utils';
1718
import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext';
1819
import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
1920
import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';
@@ -63,9 +64,7 @@ function WidgetPreview({
6364
false,
6465
};
6566

66-
const isChart =
67-
widget.displayType !== DisplayType.TABLE &&
68-
widget.displayType !== DisplayType.BIG_NUMBER;
67+
const isChart = isChartDisplayType(widget.displayType);
6968

7069
// the spans dataset doesn't handle timeseries for duplicate yAxes/aggregates
7170
// automatically, so we need to dedupe them

static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
WidgetType,
2424
type LinkedDashboard,
2525
} from 'sentry/views/dashboards/types';
26+
import {isChartDisplayType} from 'sentry/views/dashboards/utils';
2627
import type {ThresholdsConfig} from 'sentry/views/dashboards/widgetBuilder/buildSteps/thresholdsStep/thresholds';
2728
import {
2829
DISABLED_SORT,
@@ -32,12 +33,22 @@ import {
3233
DEFAULT_RESULTS_LIMIT,
3334
getResultsLimit,
3435
} from 'sentry/views/dashboards/widgetBuilder/utils';
36+
import type {DefaultDetailWidgetFields} from 'sentry/views/dashboards/widgets/detailsWidget/types';
3537
import {FieldValueKind} from 'sentry/views/discover/table/types';
38+
import {SpanFields} from 'sentry/views/insights/types';
3639

3740
// For issues dataset, events and users are sorted descending and do not use '-'
3841
// All other issues fields are sorted ascending
3942
const REVERSED_ORDER_FIELD_SORT_LIST = ['freq', 'user'];
4043

44+
const DETAIL_WIDGET_FIELDS: DefaultDetailWidgetFields[] = [
45+
SpanFields.ID,
46+
SpanFields.SPAN_OP,
47+
SpanFields.SPAN_GROUP,
48+
SpanFields.SPAN_DESCRIPTION,
49+
SpanFields.SPAN_CATEGORY,
50+
] as const;
51+
4152
export const MAX_NUM_Y_AXES = 3;
4253

4354
export type WidgetBuilderStateQueryParams = {
@@ -296,6 +307,16 @@ function useWidgetBuilderState(): {
296307
// Columns are ignored for big number widgets because there is no grouping
297308
setFields([...aggregatesWithoutAlias, ...(yAxisWithoutAlias ?? [])], options);
298309
setQuery(query?.slice(0, 1), options);
310+
} else if (action.payload === DisplayType.DETAILS) {
311+
setLimit(undefined, options);
312+
setSort([], options);
313+
setYAxis([], options);
314+
setLegendAlias([], options);
315+
setFields(
316+
DETAIL_WIDGET_FIELDS.map(field => ({field, kind: FieldValueKind.FIELD})),
317+
options
318+
);
319+
setQuery(query?.slice(0, 1), options);
299320
} else {
300321
setFields(columnsWithoutAlias, options);
301322
const nextAggregates = [
@@ -344,10 +365,16 @@ function useWidgetBuilderState(): {
344365
config.defaultWidgetQuery.fields?.map(field => explodeField({field})),
345366
options
346367
);
347-
if (
348-
nextDisplayType === DisplayType.TABLE ||
349-
nextDisplayType === DisplayType.BIG_NUMBER
350-
) {
368+
if (isChartDisplayType(nextDisplayType)) {
369+
setFields([], options);
370+
setYAxis(
371+
config.defaultWidgetQuery.aggregates?.map(aggregate =>
372+
explodeField({field: aggregate})
373+
),
374+
options
375+
);
376+
setSort(decodeSorts(config.defaultWidgetQuery.orderby), options);
377+
} else {
351378
setYAxis([], options);
352379
setFields(
353380
config.defaultWidgetQuery.fields?.map(field => explodeField({field})),
@@ -359,15 +386,6 @@ function useWidgetBuilderState(): {
359386
: decodeSorts(config.defaultWidgetQuery.orderby),
360387
options
361388
);
362-
} else {
363-
setFields([], options);
364-
setYAxis(
365-
config.defaultWidgetQuery.aggregates?.map(aggregate =>
366-
explodeField({field: aggregate})
367-
),
368-
options
369-
);
370-
setSort(decodeSorts(config.defaultWidgetQuery.orderby), options);
371389
}
372390

373391
setThresholds(undefined, options);

0 commit comments

Comments
 (0)