diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 15b80380254dff..62731139449c68 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -25,6 +25,7 @@ import type {PageFilters} from 'sentry/types/core'; import type {InjectedRouter} from 'sentry/types/legacyReactRouter'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; +import type {Sort} from 'sentry/utils/discover/fields'; import {DatasetSource} from 'sentry/utils/discover/types'; import withApi from 'sentry/utils/withApi'; import withPageFilters from 'sentry/utils/withPageFilters'; @@ -351,6 +352,23 @@ class Dashboard extends Component { ]; } + handleWidgetTableSort(index: number) { + const {dashboard, onUpdate} = this.props; + return function (sort: Sort) { + const widget = dashboard.widgets[index]!; + const widgetCopy = cloneDeep(widget); + if (widgetCopy.queries[0]) { + const direction = sort.kind === 'desc' ? '-' : ''; + widgetCopy.queries[0].orderby = `${direction}${sort.field}`; + } + + const nextList = [...dashboard.widgets]; + nextList[index] = widgetCopy; + + onUpdate(nextList); + }; + } + renderWidget(widget: Widget, index: number) { const {isMobile, windowWidth} = this.state; const { @@ -391,6 +409,7 @@ class Dashboard extends Component { index={String(index)} newlyAddedWidget={newlyAddedWidget} onNewWidgetScrollComplete={onNewWidgetScrollComplete} + onWidgetTableSort={this.handleWidgetTableSort(index)} /> ); diff --git a/static/app/views/dashboards/sortableWidget.tsx b/static/app/views/dashboards/sortableWidget.tsx index 6a997a8fb46559..7ce71cd259b07b 100644 --- a/static/app/views/dashboards/sortableWidget.tsx +++ b/static/app/views/dashboards/sortableWidget.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import {LazyRender} from 'sentry/components/lazyRender'; import PanelAlert from 'sentry/components/panels/panelAlert'; import type {User} from 'sentry/types/user'; +import type {Sort} from 'sentry/utils/discover/fields'; import useOrganization from 'sentry/utils/useOrganization'; import {useUser} from 'sentry/utils/useUser'; import {useUserTeams} from 'sentry/utils/useUserTeams'; @@ -34,6 +35,7 @@ type Props = { isPreview?: boolean; newlyAddedWidget?: Widget; onNewWidgetScrollComplete?: () => void; + onWidgetTableSort?: (sort: Sort) => void; windowWidth?: number; }; @@ -57,6 +59,7 @@ function SortableWidget(props: Props) { dashboardCreator, newlyAddedWidget, onNewWidgetScrollComplete, + onWidgetTableSort, } = props; const organization = useOrganization(); @@ -104,6 +107,7 @@ function SortableWidget(props: Props) { isMobile, windowWidth, tableItemLimit: TABLE_ITEM_LIMIT, + onWidgetTableSort, }; return ( diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx index 741f32b615856c..18dc97a0ae196f 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx @@ -1,6 +1,7 @@ import PanelAlert from 'sentry/components/panels/panelAlert'; import {dedupeArray} from 'sentry/utils/dedupeArray'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; +import type {Sort} from 'sentry/utils/discover/fields'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; @@ -12,6 +13,7 @@ import { WidgetType, } from 'sentry/views/dashboards/types'; import {useWidgetBuilderContext} from 'sentry/views/dashboards/widgetBuilder/contexts/widgetBuilderContext'; +import {BuilderStateAction} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState'; import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget'; import WidgetCard from 'sentry/views/dashboards/widgetCard'; import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; @@ -39,7 +41,7 @@ function WidgetPreview({ const navigate = useNavigate(); const pageFilters = usePageFilters(); - const {state} = useWidgetBuilderContext(); + const {state, dispatch} = useWidgetBuilderContext(); const widget = convertBuilderStateToWidget(state); @@ -75,6 +77,13 @@ function WidgetPreview({ }), }; + function handleWidgetTableSort(sort: Sort) { + dispatch({ + payload: [sort], + type: BuilderStateAction.SET_SORT, + }); + } + return ( ); } diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index b30466e5a03e74..25aaa725527413 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -41,18 +41,20 @@ import { } from 'sentry/utils/discover/charts'; import type {EventsMetaType, MetaType} from 'sentry/utils/discover/eventView'; import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers'; -import type {AggregationOutputType, DataUnit} from 'sentry/utils/discover/fields'; +import type {AggregationOutputType, DataUnit, Sort} from 'sentry/utils/discover/fields'; import { aggregateOutputType, getAggregateArg, getEquation, getMeasurementSlug, + isAggregateField, isEquation, maybeEquationAlias, stripDerivedMetricsPrefix, stripEquationPrefix, } from 'sentry/utils/discover/fields'; import getDynamicText from 'sentry/utils/getDynamicText'; +import {decodeSorts} from 'sentry/utils/queryString'; import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; import type {Widget} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; @@ -103,6 +105,7 @@ type WidgetCardChartProps = Pick< selected: Record; type: 'legendselectchanged'; }>; + onWidgetTableSort?: (sort: Sort) => void; onZoom?: EChartDataZoomHandler; sampleCount?: number; shouldResize?: boolean; @@ -144,8 +147,15 @@ class WidgetCardChart extends Component { } tableResultComponent({loading, tableResults}: TableResultProps): React.ReactNode { - const {widget, selection, minTableColumnWidth, location, organization, theme} = - this.props; + const { + widget, + selection, + minTableColumnWidth, + location, + organization, + theme, + onWidgetTableSort, + } = this.props; if (loading || !tableResults?.[0]) { // Align height to other charts. return ; @@ -176,9 +186,12 @@ class WidgetCardChart extends Component { name: column.name, width: minTableColumnWidth ?? column.width, type: column.type === 'never' ? null : column.type, + sortable: + widget.widgetType === WidgetType.RELEASE ? isAggregateField(column.key) : true, })); const aliases = decodeColumnAliases(columns, fieldAliases, fieldHeaderMap); const tableData = convertTableDataToTabularData(tableResults?.[i]); + const sort = decodeSorts(widget.queries[0]?.orderby)?.[0]; return ( @@ -190,6 +203,8 @@ class WidgetCardChart extends Component { scrollable fit="max-content" aliases={aliases} + onChangeSort={onWidgetTableSort} + sort={sort} getRenderer={(field, _dataRow, meta) => { const customRenderer = datasetConfig.getCustomFieldRenderer?.( field, diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 048589bf6f63e5..6983429c5286bc 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -16,7 +16,7 @@ import type {Series} from 'sentry/types/echarts'; import type {WithRouterProps} from 'sentry/types/legacyReactRouter'; import type {Confidence, Organization} from 'sentry/types/organization'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; -import type {AggregationOutputType} from 'sentry/utils/discover/fields'; +import type {AggregationOutputType, Sort} from 'sentry/utils/discover/fields'; import {statsPeriodToDays} from 'sentry/utils/duration/statsPeriodToDays'; import {hasOnDemandMetricWidgetFeature} from 'sentry/utils/onDemandMetrics/features'; import {useExtractionStatus} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext'; @@ -89,6 +89,7 @@ type Props = WithRouterProps & { onSetTransactionsDataset?: () => void; onUpdate?: (widget: Widget | null) => void; onWidgetSplitDecision?: (splitDecision: WidgetType) => void; + onWidgetTableSort?: (sort: Sort) => void; renderErrorMessage?: (errorMessage?: string) => React.ReactNode; shouldResize?: boolean; showConfidenceWarning?: boolean; @@ -156,6 +157,7 @@ function WidgetCard(props: Props) { disableZoom, showLoadingText, router, + onWidgetTableSort, } = props; if (widget.displayType === DisplayType.TOP_N) { @@ -322,6 +324,7 @@ function WidgetCard(props: Props) { disableZoom={disableZoom} onDataFetchStart={onDataFetchStart} showLoadingText={showLoadingText && isLoadingTextVisible} + onWidgetTableSort={onWidgetTableSort} /> diff --git a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx index 4c2b8be125b915..e9aaf8966b41b5 100644 --- a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx +++ b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx @@ -17,7 +17,7 @@ import type { } from 'sentry/types/echarts'; import type {Organization} from 'sentry/types/organization'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; -import type {AggregationOutputType} from 'sentry/utils/discover/fields'; +import type {AggregationOutputType, Sort} from 'sentry/utils/discover/fields'; import {useLocation} from 'sentry/utils/useLocation'; import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; @@ -57,6 +57,7 @@ type Props = { type: 'legendselectchanged'; }>; onWidgetSplitDecision?: (splitDecision: WidgetType) => void; + onWidgetTableSort?: (sort: Sort) => void; onZoom?: EChartDataZoomHandler; renderErrorMessage?: (errorMessage?: string) => React.ReactNode; shouldResize?: boolean; @@ -90,6 +91,7 @@ export function WidgetCardChartContainer({ onDataFetchStart, disableZoom, showLoadingText, + onWidgetTableSort, }: Props) { const location = useLocation(); const theme = useTheme(); @@ -215,6 +217,7 @@ export function WidgetCardChartContainer({ isSampled={isSampled} showLoadingText={showLoadingText} theme={theme} + onWidgetTableSort={onWidgetTableSort} /> );