From 9b565e8459a4b85842330d9ef6f4728527bee732 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Mon, 9 Jun 2025 14:38:38 -0400 Subject: [PATCH 1/9] ref(dashboards): use gridEditable table for all widgets --- .../widgetViewerTableCell.tsx | 89 +++++---- .../app/views/dashboards/widgetCard/chart.tsx | 99 +++++----- .../dashboards/widgetCard/issueWidgetCard.tsx | 84 ++++----- .../widgetCard/widgetCardChartContainer.tsx | 12 +- static/app/views/dashboards/widgetTable.tsx | 174 ++++++++++++++++++ 5 files changed, 325 insertions(+), 133 deletions(-) create mode 100644 static/app/views/dashboards/widgetTable.tsx diff --git a/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx b/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx index 25e693b13f174c..895727e9b79375 100644 --- a/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx +++ b/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx @@ -51,9 +51,10 @@ type Props = { eventView?: EventView; isFirstPage?: boolean; isMetricsData?: boolean; - onHeaderClick?: () => void; + onHeaderClick?: (s?: string) => void; projects?: Project[]; tableData?: TableDataWithTitle; + usesLocationQuery?: boolean; }; export const renderIssueGridHeaderCell = ({ @@ -62,6 +63,7 @@ export const renderIssueGridHeaderCell = ({ tableData, organization, onHeaderClick, + usesLocationQuery = true, }: Props) => function ( column: TableColumn, @@ -77,24 +79,30 @@ export const renderIssueGridHeaderCell = ({ title={{column.name}} direction={widget.queries[0]!.orderby === sortField ? 'desc' : undefined} canSort={!!sortField} - generateSortLink={() => ({ - ...location, - query: { - ...location.query, - [WidgetViewerQueryField.SORT]: sortField, - [WidgetViewerQueryField.PAGE]: undefined, - [WidgetViewerQueryField.CURSOR]: undefined, - }, - })} + generateSortLink={ + usesLocationQuery + ? () => ({ + ...location, + query: { + ...location.query, + [WidgetViewerQueryField.SORT]: sortField, + [WidgetViewerQueryField.PAGE]: undefined, + [WidgetViewerQueryField.CURSOR]: undefined, + }, + }) + : () => ({...location}) + } onClick={() => { - onHeaderClick?.(); - trackAnalytics('dashboards_views.widget_viewer.sort', { - organization, - widget_type: WidgetType.ISSUE, - display_type: widget.displayType, - column: column.name, - order: 'desc', - }); + onHeaderClick?.(sortField ? sortField : undefined); + if (usesLocationQuery) { + trackAnalytics('dashboards_views.widget_viewer.sort', { + organization, + widget_type: WidgetType.ISSUE, + display_type: widget.displayType, + column: column.name, + order: 'desc', + }); + } }} preventScrollReset /> @@ -109,6 +117,7 @@ export const renderDiscoverGridHeaderCell = ({ organization, onHeaderClick, isMetricsData, + usesLocationQuery = true, }: Props) => function ( column: TableColumn, @@ -132,6 +141,10 @@ export const renderDiscoverGridHeaderCell = ({ return undefined; } + if (!usesLocationQuery) { + return {...location}; + } + const nextEventView = eventView.sortOnField(field, tableMeta, undefined, true); const queryStringObject = nextEventView.generateQueryStringObject(); @@ -161,14 +174,16 @@ export const renderDiscoverGridHeaderCell = ({ canSort={canSort} generateSortLink={generateSortLink} onClick={() => { - onHeaderClick?.(); - trackAnalytics('dashboards_views.widget_viewer.sort', { - organization, - widget_type: WidgetType.DISCOVER, - display_type: widget.displayType, - column: column.name, - order: currentSort?.kind === 'desc' ? 'asc' : 'desc', - }); + onHeaderClick?.(currentSort?.kind === 'asc' ? '-' + column.name : column.name); + if (usesLocationQuery) { + trackAnalytics('dashboards_views.widget_viewer.sort', { + organization, + widget_type: WidgetType.DISCOVER, + display_type: widget.displayType, + column: column.name, + order: currentSort?.kind === 'desc' ? 'asc' : 'desc', + }); + } }} preventScrollReset /> @@ -275,6 +290,7 @@ export const renderReleaseGridHeaderCell = ({ tableData, organization, onHeaderClick, + usesLocationQuery = true, }: Props) => function ( column: TableColumn, @@ -291,6 +307,9 @@ export const renderReleaseGridHeaderCell = ({ const titleText = column.name; function generateSortLink(): LocationDescriptorObject { + if (!usesLocationQuery) { + return {...location}; + } const columnSort = column.name === sort.field ? {...sort, kind: sort.kind === 'desc' ? 'asc' : 'desc'} @@ -316,14 +335,16 @@ export const renderReleaseGridHeaderCell = ({ canSort={canSort} generateSortLink={generateSortLink} onClick={() => { - onHeaderClick?.(); - trackAnalytics('dashboards_views.widget_viewer.sort', { - organization, - widget_type: WidgetType.RELEASE, - display_type: widget.displayType, - column: column.name, - order: sort?.kind === 'desc' ? 'asc' : 'desc', - }); + onHeaderClick?.(sort.kind === 'asc' ? '-' + column.name : column.name); + if (usesLocationQuery) { + trackAnalytics('dashboards_views.widget_viewer.sort', { + organization, + widget_type: WidgetType.RELEASE, + display_type: widget.displayType, + column: column.name, + order: sort?.kind === 'desc' ? 'asc' : 'desc', + }); + } }} preventScrollReset /> diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index 27b2924137330c..0bdb9ceed03caa 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -1,4 +1,5 @@ import type React from 'react'; +import type {Dispatch, SetStateAction} from 'react'; import {Component} from 'react'; import type {Theme} from '@emotion/react'; import {withTheme} from '@emotion/react'; @@ -15,11 +16,14 @@ import {getFormatter} from 'sentry/components/charts/components/tooltip'; import ErrorPanel from 'sentry/components/charts/errorPanel'; import {LineChart} from 'sentry/components/charts/lineChart'; import ReleaseSeries from 'sentry/components/charts/releaseSeries'; -import SimpleTableChart from 'sentry/components/charts/simpleTableChart'; import TransitionChart from 'sentry/components/charts/transitionChart'; import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask'; import {getSeriesSelection, isChartHovered} from 'sentry/components/charts/utils'; import LoadingIndicator from 'sentry/components/loadingIndicator'; +import { + renderDiscoverGridHeaderCell, + renderReleaseGridHeaderCell, +} from 'sentry/components/modals/widgetViewerModal/widgetViewerTableCell'; import type {PlaceholderProps} from 'sentry/components/placeholder'; import Placeholder from 'sentry/components/placeholder'; import {IconWarning} from 'sentry/icons'; @@ -39,7 +43,7 @@ import { getDurationUnit, tooltipFormatter, } from 'sentry/utils/discover/charts'; -import type {EventsMetaType, MetaType} from 'sentry/utils/discover/eventView'; +import type {EventsMetaType} from 'sentry/utils/discover/eventView'; import type {AggregationOutputType, DataUnit} from 'sentry/utils/discover/fields'; import { aggregateOutputType, @@ -48,18 +52,16 @@ import { getMeasurementSlug, isEquation, maybeEquationAlias, - stripDerivedMetricsPrefix, stripEquationPrefix, } from 'sentry/utils/discover/fields'; import getDynamicText from 'sentry/utils/getDynamicText'; -import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; import type {Widget} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; -import {eventViewFromWidget} from 'sentry/views/dashboards/utils'; import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize'; import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization'; +import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter'; import type {GenericWidgetQueriesChildrenProps} from './genericWidgetQueries'; @@ -98,14 +100,20 @@ type WidgetCardChartProps = Pick< }>; onZoom?: EChartDataZoomHandler; sampleCount?: number; + setCurrentWidget?: Dispatch>; shouldResize?: boolean; showConfidenceWarning?: boolean; showLoadingText?: boolean; timeseriesResultsTypes?: Record; windowWidth?: number; }; - class WidgetCardChart extends Component { + // Used for the table widget to maintain column widths between table sorts and column resizing. + // Eventually this will live in Widget to allow users to save custom widths for tables + state = { + widths: [], + }; + shouldComponentUpdate(nextProps: WidgetCardChartProps): boolean { if ( this.props.widget.displayType === DisplayType.BIG_NUMBER && @@ -136,52 +144,47 @@ class WidgetCardChart extends Component { return !isEqual(currentProps, nextProps); } + setWidths(newWidths: string[]) { + this.setState({widths: newWidths}); + } + tableResultComponent({loading, tableResults}: TableResultProps): React.ReactNode { - const {location, widget, selection, minTableColumnWidth} = this.props; + const {widget, selection, organization, setCurrentWidget} = this.props; if (typeof tableResults === 'undefined') { // Align height to other charts. return ; } + const sort = widget.queries[0]?.orderby; - const datasetConfig = getDatasetConfig(widget.widgetType); - - const getCustomFieldRenderer = ( - field: string, - meta: MetaType, - organization?: Organization - ) => { - return ( - datasetConfig.getCustomFieldRenderer?.(field, meta, widget, organization) || null - ); - }; - - return tableResults.map((result, i) => { - const fields = widget.queries[i]?.fields?.map(stripDerivedMetricsPrefix) ?? []; - const fieldAliases = widget.queries[i]?.fieldAliases ?? []; - const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection); - - return ( - - 1 ? result.title : ''} - // Bypass the loading state for span widgets because this renders the loading placeholder - // and we want to show the underlying data during preflight instead - loading={widget.widgetType === WidgetType.SPANS ? false : loading} - loader={} - metadata={result.meta} - data={result.data} - stickyHeaders - fieldHeaderMap={datasetConfig.getFieldHeaderMap?.(widget.queries[i])} - getCustomFieldRenderer={getCustomFieldRenderer} - minColumnWidth={minTableColumnWidth} - /> - - ); - }); + return ( + + this.setWidths(w)} + usesLocationQuery={false} + /> + + ); } bigNumberComponent({loading, tableResults}: TableResultProps): React.ReactNode { @@ -656,11 +659,7 @@ const TableWrapper = styled('div')` min-height: 0; border-bottom-left-radius: ${p => p.theme.borderRadius}; border-bottom-right-radius: ${p => p.theme.borderRadius}; -`; - -const StyledSimpleTableChart = styled(SimpleTableChart)` overflow: auto; - height: 100%; `; const StyledErrorPanel = styled(ErrorPanel)` diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx index dc46937e924cfa..847df151fcc292 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx @@ -1,29 +1,27 @@ +import type {Dispatch, SetStateAction} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import ErrorPanel from 'sentry/components/charts/errorPanel'; -import SimpleTableChart from 'sentry/components/charts/simpleTableChart'; +import {renderIssueGridHeaderCell} from 'sentry/components/modals/widgetViewerModal/widgetViewerTableCell'; import Placeholder from 'sentry/components/placeholder'; import {IconWarning} from 'sentry/icons'; -import {space} from 'sentry/styles/space'; import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; -import {defined} from 'sentry/utils'; -import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; -import type {MetaType} from 'sentry/utils/discover/eventView'; -import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; +import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; import type {Widget} from 'sentry/views/dashboards/types'; -import {WidgetType} from 'sentry/views/dashboards/types'; -import {eventViewFromWidget} from 'sentry/views/dashboards/utils'; -import {ISSUE_FIELDS} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields'; +import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; type Props = { loading: boolean; location: Location; + organization: Organization; selection: PageFilters; - transformedResults: TableDataRow[]; + tableResults: TableDataWithTitle[] | undefined; widget: Widget; errorMessage?: string; + setCurrentWidget?: Dispatch>; }; export function IssueWidgetCard({ @@ -31,10 +29,11 @@ export function IssueWidgetCard({ widget, errorMessage, loading, - transformedResults, - location, + tableResults, + setCurrentWidget, + organization, }: Props) { - const datasetConfig = getDatasetConfig(WidgetType.ISSUE); + const [widths, setWidths] = useState([]); if (errorMessage) { return ( @@ -44,42 +43,37 @@ export function IssueWidgetCard({ ); } - if (loading) { + if (loading || !tableResults) { // Align height to other charts. return ; } - const query = widget.queries[0]!; - const queryFields = defined(query.fields) - ? query.fields - : [...query.columns, ...query.aggregates]; - const fieldAliases = query.fieldAliases ?? []; - const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection); - - const getCustomFieldRenderer = ( - field: string, - meta: MetaType, - organization?: Organization - ) => { - return ( - datasetConfig.getCustomFieldRenderer?.(field, meta, widget, organization) || null - ); - }; + const sort = widget.queries[0]?.orderby; return ( - +
+ setWidths(w)} + usesLocationQuery={false} + /> +
); } @@ -87,10 +81,10 @@ const LoadingPlaceholder = styled(Placeholder)` background-color: ${p => p.theme.surface300}; `; -const StyledSimpleTableChart = styled(SimpleTableChart)` - margin-top: ${space(1.5)}; +const StyledWidgetTable = styled(WidgetTable)` border-bottom-left-radius: ${p => p.theme.borderRadius}; border-bottom-right-radius: ${p => p.theme.borderRadius}; font-size: ${p => p.theme.fontSizeMedium}; box-shadow: none; + overflow: auto; `; diff --git a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx index 7919ad3e6c4f36..402dc9a1e40759 100644 --- a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx +++ b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx @@ -1,4 +1,4 @@ -import {Fragment} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import type {LegendComponentOption} from 'echarts'; import type {Location} from 'history'; @@ -91,6 +91,7 @@ export function WidgetCardChartContainer({ showLoadingText, }: Props) { const location = useLocation(); + const [currentWidget, setCurrentWidget] = useState(widget); function keepLegendState({ selected, @@ -122,7 +123,7 @@ export function WidgetCardChartContainer({ return ( ); @@ -184,7 +187,7 @@ export function WidgetCardChartContainer({ errorMessage={errorOrEmptyMessage} loading={loading} location={location} - widget={widget} + widget={currentWidget} selection={selection} organization={organization} isMobile={isMobile} @@ -210,6 +213,7 @@ export function WidgetCardChartContainer({ minTableColumnWidth={minTableColumnWidth} isSampled={isSampled} showLoadingText={showLoadingText} + setCurrentWidget={setCurrentWidget} /> ); diff --git a/static/app/views/dashboards/widgetTable.tsx b/static/app/views/dashboards/widgetTable.tsx new file mode 100644 index 00000000000000..1d8d7c50b00d08 --- /dev/null +++ b/static/app/views/dashboards/widgetTable.tsx @@ -0,0 +1,174 @@ +import type {Dispatch, SetStateAction} from 'react'; +import {Fragment} from 'react'; +import {useTheme} from '@emotion/react'; +import cloneDeep from 'lodash/cloneDeep'; + +import type {GridColumnOrder} from 'sentry/components/gridEditable'; +import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; +import {WidgetViewerQueryField} from 'sentry/components/modals/widgetViewerModal/utils'; +import {renderGridBodyCell} from 'sentry/components/modals/widgetViewerModal/widgetViewerTableCell'; +import type {PageFilters} from 'sentry/types/core'; +import type {Organization} from 'sentry/types/organization'; +import {defined} from 'sentry/utils'; +import type {TableDataRow, TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; +import {useLocation} from 'sentry/utils/useLocation'; +import {useNavigate} from 'sentry/utils/useNavigate'; +import useProjects from 'sentry/utils/useProjects'; +import {type Widget} from 'sentry/views/dashboards/types'; +import {eventViewFromWidget} from 'sentry/views/dashboards/utils'; +import {useDashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext'; +import type {TableColumn, TableColumnSort} from 'sentry/views/discover/table/types'; +import {decodeColumnOrder} from 'sentry/views/discover/utils'; + +interface Props { + loading: boolean; + organization: Organization; + renderHeaderGridCell: ( + props: any + ) => (column: TableColumn, _columnIndex: number) => React.ReactNode; + selection: PageFilters; + sort: string; + style: any; + tableResults: TableDataWithTitle[]; + widget: Widget; + widths: string[]; + customHeaderClick?: () => void; + setCurrentWidget?: Dispatch>; + setWidths?: (w: string[]) => void; + stickyHeader?: boolean; + usesLocationQuery?: boolean; +} + +export const getColumnSortFromString = ( + sort: string +): Array> => { + if (sort.length < 1) return []; + if (sort.startsWith('-')) + return [ + { + key: sort.substring(1), + order: 'desc', + }, + ]; + return [ + { + key: sort, + order: 'asc', + }, + ]; +}; + +export function WidgetTable(props: Props) { + const { + tableResults, + loading, + customHeaderClick, + renderHeaderGridCell, + selection, + sort, + widget, + style, + usesLocationQuery, + stickyHeader, + widths, + setCurrentWidget, + setWidths, + } = props; + const theme = useTheme(); + const location = useLocation(); + const {projects} = useProjects(); + const navigate = useNavigate(); + const {isMetricsData} = useDashboardsMEPContext(); + + const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection); + + const columnSortBy = usesLocationQuery + ? eventView.getSorts() + : getColumnSortFromString(sort); + + const {aggregates, columns} = widget.queries[0]!; + + const fields = defined(widget.queries[0]!.fields) + ? widget.queries[0]!.fields + : [...columns, ...aggregates]; + + let columnOrder = decodeColumnOrder( + fields.map(field => ({ + field, + })) + ); + columnOrder = columnOrder.map((column, index) => ({ + ...column, + width: parseInt(widths[index] || '-1', 10), + })); + + const onResizeColumn = (columnIndex: number, nextColumn: GridColumnOrder) => { + const newWidth = nextColumn.width ? Number(nextColumn.width) : COL_WIDTH_UNDEFINED; + const newWidths: number[] = new Array(Math.max(columnIndex, widths.length)).fill( + COL_WIDTH_UNDEFINED + ); + widths.forEach((width, index) => (newWidths[index] = parseInt(width, 10))); + newWidths[columnIndex] = newWidth; + setWidths?.(newWidths.map(String)); + // Editing a widget relies on location query, while state is used in dashboard + if (usesLocationQuery) { + navigate( + { + pathname: location.pathname, + query: { + ...location.query, + [WidgetViewerQueryField.WIDTH]: newWidths, + }, + }, + {replace: true} + ); + } + }; + + const onHeaderClick = (newSort?: string) => { + customHeaderClick?.(); + if (widget.queries[0]) { + const newWidget = cloneDeep(widget); + // @ts-expect-error: Object is possibly 'undefined'. + newWidget.queries[0].orderby = newSort || sort; + setCurrentWidget?.(newWidget); + } + }; + + return ( + + React.ReactNode, + renderBodyCell: renderGridBodyCell({ + ...props, + location, + tableData: tableResults?.[0], + isFirstPage: false, + projects, + eventView, + theme, + isMetricsData, + }), + onResizeColumn, + }} + bodyStyle={style} + stickyHeader={stickyHeader} + scrollable + /> + + ); +} From 8fcc7c3723e21dd10103f71fd1740d0f93965b97 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Mon, 9 Jun 2025 15:35:43 -0400 Subject: [PATCH 2/9] pull up sorting and column widths --- static/app/views/dashboards/dashboard.tsx | 16 ++++++++++ .../app/views/dashboards/sortableWidget.tsx | 3 ++ .../app/views/dashboards/widgetCard/chart.tsx | 18 +++++------ .../app/views/dashboards/widgetCard/index.tsx | 3 ++ .../dashboards/widgetCard/issueWidgetCard.tsx | 31 ++++++++++--------- .../widgetCard/widgetCardChartContainer.tsx | 18 ++++++++--- static/app/views/dashboards/widgetTable.tsx | 13 ++------ 7 files changed, 62 insertions(+), 40 deletions(-) diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 412beb2d09815a..2e780d14ba0b9d 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -351,6 +351,21 @@ class Dashboard extends Component { ]; } + handleWidgetSort(index: number, newSort: string) { + const {dashboard, onUpdate} = this.props; + const widget = dashboard.widgets[index]!; + const widgetCopy = cloneDeep({ + ...widget, + id: undefined, + }); + if (widgetCopy.queries[0]) widgetCopy.queries[0].orderby = newSort; + + const nextList = [...dashboard.widgets]; + nextList[index] = widgetCopy; + + onUpdate(nextList); + } + renderWidget(widget: Widget, index: number) { const {isMobile, windowWidth} = this.state; const { @@ -391,6 +406,7 @@ class Dashboard extends Component { index={String(index)} newlyAddedWidget={newlyAddedWidget} onNewWidgetScrollComplete={onNewWidgetScrollComplete} + handleWidgetSort={(ns: string) => this.handleWidgetSort(index, ns)} /> ); diff --git a/static/app/views/dashboards/sortableWidget.tsx b/static/app/views/dashboards/sortableWidget.tsx index 6a997a8fb46559..855dc66a808b6b 100644 --- a/static/app/views/dashboards/sortableWidget.tsx +++ b/static/app/views/dashboards/sortableWidget.tsx @@ -30,6 +30,7 @@ type Props = { dashboardCreator?: User; dashboardFilters?: DashboardFilters; dashboardPermissions?: DashboardPermissions; + handleWidgetSort?: (ns: string) => void; isMobile?: boolean; isPreview?: boolean; newlyAddedWidget?: Widget; @@ -57,6 +58,7 @@ function SortableWidget(props: Props) { dashboardCreator, newlyAddedWidget, onNewWidgetScrollComplete, + handleWidgetSort, } = props; const organization = useOrganization(); @@ -104,6 +106,7 @@ function SortableWidget(props: Props) { isMobile, windowWidth, tableItemLimit: TABLE_ITEM_LIMIT, + handleWidgetSort, }; return ( diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index 0bdb9ceed03caa..8d5487f6a99deb 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -101,19 +101,16 @@ type WidgetCardChartProps = Pick< onZoom?: EChartDataZoomHandler; sampleCount?: number; setCurrentWidget?: Dispatch>; + setTableWidths?: (tableWidths: string[]) => void; + setWidgetSort?: (ns: string) => void; shouldResize?: boolean; showConfidenceWarning?: boolean; showLoadingText?: boolean; + tableWidths?: string[]; timeseriesResultsTypes?: Record; windowWidth?: number; }; class WidgetCardChart extends Component { - // Used for the table widget to maintain column widths between table sorts and column resizing. - // Eventually this will live in Widget to allow users to save custom widths for tables - state = { - widths: [], - }; - shouldComponentUpdate(nextProps: WidgetCardChartProps): boolean { if ( this.props.widget.displayType === DisplayType.BIG_NUMBER && @@ -149,7 +146,8 @@ class WidgetCardChart extends Component { } tableResultComponent({loading, tableResults}: TableResultProps): React.ReactNode { - const {widget, selection, organization, setCurrentWidget} = this.props; + const {widget, selection, organization, setWidgetSort, tableWidths, setTableWidths} = + this.props; if (typeof tableResults === 'undefined') { // Align height to other charts. return ; @@ -176,11 +174,11 @@ class WidgetCardChart extends Component { : renderDiscoverGridHeaderCell } sort={sort || ''} - widths={this.state.widths} + widths={tableWidths || []} organization={organization} stickyHeader - setCurrentWidget={setCurrentWidget} - setWidths={(w: string[]) => this.setWidths(w)} + setWidgetSort={setWidgetSort} + setWidths={(w: string[]) => setTableWidths?.(w)} usesLocationQuery={false} /> diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 4d851c434a51c1..0b8a454f0daeed 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -73,6 +73,7 @@ type Props = WithRouterProps & { disableFullscreen?: boolean; disableZoom?: boolean; forceDescriptionTooltip?: boolean; + handleWidgetSort?: (ns: string) => void; hasEditAccess?: boolean; index?: string; isEditingWidget?: boolean; @@ -151,6 +152,7 @@ function WidgetCard(props: Props) { minTableColumnWidth, disableZoom, showLoadingText, + handleWidgetSort, } = props; if (widget.displayType === DisplayType.TOP_N) { @@ -315,6 +317,7 @@ function WidgetCard(props: Props) { disableZoom={disableZoom} onDataFetchStart={onDataFetchStart} showLoadingText={showLoadingText && isLoadingTextVisible} + handleWidgetSort={handleWidgetSort} /> diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx index 847df151fcc292..13ad464107d10e 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx @@ -1,5 +1,3 @@ -import type {Dispatch, SetStateAction} from 'react'; -import {useState} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; @@ -7,6 +5,7 @@ import ErrorPanel from 'sentry/components/charts/errorPanel'; import {renderIssueGridHeaderCell} from 'sentry/components/modals/widgetViewerModal/widgetViewerTableCell'; import Placeholder from 'sentry/components/placeholder'; import {IconWarning} from 'sentry/icons'; +import {space} from 'sentry/styles/space'; import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; @@ -21,7 +20,9 @@ type Props = { tableResults: TableDataWithTitle[] | undefined; widget: Widget; errorMessage?: string; - setCurrentWidget?: Dispatch>; + setTableWidths?: (tableWidths: string[]) => void; + setWidgetSort?: (ns: string) => void; + tableWidths?: string[]; }; export function IssueWidgetCard({ @@ -30,11 +31,11 @@ export function IssueWidgetCard({ errorMessage, loading, tableResults, - setCurrentWidget, + setWidgetSort, organization, + tableWidths, + setTableWidths, }: Props) { - const [widths, setWidths] = useState([]); - if (errorMessage) { return ( @@ -51,8 +52,8 @@ export function IssueWidgetCard({ const sort = widget.queries[0]?.orderby; return ( -
- + setWidths(w)} + setWidgetSort={setWidgetSort} + setWidths={(w: string[]) => setTableWidths?.(w)} usesLocationQuery={false} /> -
+ ); } @@ -81,10 +82,10 @@ const LoadingPlaceholder = styled(Placeholder)` background-color: ${p => p.theme.surface300}; `; -const StyledWidgetTable = styled(WidgetTable)` +const TableWrapper = styled('div')` + margin-top: ${space(1.5)}; + min-height: 0; border-bottom-left-radius: ${p => p.theme.borderRadius}; border-bottom-right-radius: ${p => p.theme.borderRadius}; - font-size: ${p => p.theme.fontSizeMedium}; - box-shadow: none; overflow: auto; `; diff --git a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx index 402dc9a1e40759..56503b4dcb78bd 100644 --- a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx +++ b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx @@ -38,6 +38,7 @@ type Props = { dashboardFilters?: DashboardFilters; disableZoom?: boolean; expandNumbers?: boolean; + handleWidgetSort?: (ns: string) => void; isMobile?: boolean; legendOptions?: LegendComponentOption; minTableColumnWidth?: string; @@ -89,9 +90,12 @@ export function WidgetCardChartContainer({ onDataFetchStart, disableZoom, showLoadingText, + handleWidgetSort, }: Props) { const location = useLocation(); - const [currentWidget, setCurrentWidget] = useState(widget); + // Used to maintain correct widths when sorting/column resizing the table widget + // Eventually this will be placed in Widget, to enable users to save column widths + const [tableWidths, setTableWidths] = useState([]); function keepLegendState({ selected, @@ -123,7 +127,7 @@ export function WidgetCardChartContainer({ return ( ); @@ -187,7 +193,7 @@ export function WidgetCardChartContainer({ errorMessage={errorOrEmptyMessage} loading={loading} location={location} - widget={currentWidget} + widget={widget} selection={selection} organization={organization} isMobile={isMobile} @@ -213,7 +219,9 @@ export function WidgetCardChartContainer({ minTableColumnWidth={minTableColumnWidth} isSampled={isSampled} showLoadingText={showLoadingText} - setCurrentWidget={setCurrentWidget} + setWidgetSort={handleWidgetSort} + tableWidths={tableWidths} + setTableWidths={setTableWidths} /> ); diff --git a/static/app/views/dashboards/widgetTable.tsx b/static/app/views/dashboards/widgetTable.tsx index 1d8d7c50b00d08..0a907f5873cc88 100644 --- a/static/app/views/dashboards/widgetTable.tsx +++ b/static/app/views/dashboards/widgetTable.tsx @@ -1,7 +1,5 @@ -import type {Dispatch, SetStateAction} from 'react'; import {Fragment} from 'react'; import {useTheme} from '@emotion/react'; -import cloneDeep from 'lodash/cloneDeep'; import type {GridColumnOrder} from 'sentry/components/gridEditable'; import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; @@ -33,7 +31,7 @@ interface Props { widget: Widget; widths: string[]; customHeaderClick?: () => void; - setCurrentWidget?: Dispatch>; + setWidgetSort?: (ns: string) => void; setWidths?: (w: string[]) => void; stickyHeader?: boolean; usesLocationQuery?: boolean; @@ -71,7 +69,7 @@ export function WidgetTable(props: Props) { usesLocationQuery, stickyHeader, widths, - setCurrentWidget, + setWidgetSort, setWidths, } = props; const theme = useTheme(); @@ -127,12 +125,7 @@ export function WidgetTable(props: Props) { const onHeaderClick = (newSort?: string) => { customHeaderClick?.(); - if (widget.queries[0]) { - const newWidget = cloneDeep(widget); - // @ts-expect-error: Object is possibly 'undefined'. - newWidget.queries[0].orderby = newSort || sort; - setCurrentWidget?.(newWidget); - } + setWidgetSort?.(newSort || ''); }; return ( From 6fff2ad3dd057310b93cf4a412ea0125e238bee4 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Tue, 10 Jun 2025 12:15:03 -0400 Subject: [PATCH 3/9] fix sorting for preview --- static/app/views/dashboards/dashboard.tsx | 1 - .../dashboards/manage/dashboardTable.tsx | 2 + .../components/widgetPreview.tsx | 18 ++++- .../app/views/dashboards/widgetCard/chart.tsx | 78 ++++++++++--------- .../dashboards/widgetCard/issueWidgetCard.tsx | 4 +- .../widgetCard/widgetCardChartContainer.tsx | 4 + static/app/views/dashboards/widgetTable.tsx | 4 +- 7 files changed, 70 insertions(+), 41 deletions(-) diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 2e780d14ba0b9d..37d409e41a58a0 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -356,7 +356,6 @@ class Dashboard extends Component { const widget = dashboard.widgets[index]!; const widgetCopy = cloneDeep({ ...widget, - id: undefined, }); if (widgetCopy.queries[0]) widgetCopy.queries[0].orderby = newSort; diff --git a/static/app/views/dashboards/manage/dashboardTable.tsx b/static/app/views/dashboards/manage/dashboardTable.tsx index 747aef08ea4413..2d3b2201d91be1 100644 --- a/static/app/views/dashboards/manage/dashboardTable.tsx +++ b/static/app/views/dashboards/manage/dashboardTable.tsx @@ -379,6 +379,8 @@ function DashboardTable({

{t('Sorry, no Dashboards match your filters.')}

} + stickyHeader + scrollable /> ); } diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx index 7e94cb625998d4..3826015f0d6862 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx @@ -12,6 +12,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,10 +40,23 @@ function WidgetPreview({ const navigate = useNavigate(); const pageFilters = usePageFilters(); - const {state} = useWidgetBuilderContext(); + const {state, dispatch} = useWidgetBuilderContext(); const widget = convertBuilderStateToWidget(state); + const updateWidgetSort = (newSort: string) => { + if (newSort.startsWith('-')) + dispatch({ + type: BuilderStateAction.SET_SORT, + payload: [{field: newSort.substring(1), kind: 'desc'}], + }); + else + dispatch({ + type: BuilderStateAction.SET_SORT, + payload: [{field: newSort, kind: 'asc'}], + }); + }; + const widgetLegendState = new WidgetLegendSelectionState({ location, organization, @@ -117,6 +131,8 @@ function WidgetPreview({ minTableColumnWidth={MIN_TABLE_COLUMN_WIDTH} disableZoom showLoadingText + isPreview + handleWidgetSort={updateWidgetSort} /> ); } diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index 8d5487f6a99deb..9b41d415995dea 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -1,5 +1,4 @@ import type React from 'react'; -import type {Dispatch, SetStateAction} from 'react'; import {Component} from 'react'; import type {Theme} from '@emotion/react'; import {withTheme} from '@emotion/react'; @@ -89,6 +88,7 @@ type WidgetCardChartProps = Pick< disableZoom?: boolean; expandNumbers?: boolean; isMobile?: boolean; + isPreview?: boolean; isSampled?: boolean | null; legendOptions?: LegendComponentOption; minTableColumnWidth?: string; @@ -100,7 +100,6 @@ type WidgetCardChartProps = Pick< }>; onZoom?: EChartDataZoomHandler; sampleCount?: number; - setCurrentWidget?: Dispatch>; setTableWidths?: (tableWidths: string[]) => void; setWidgetSort?: (ns: string) => void; shouldResize?: boolean; @@ -146,43 +145,52 @@ class WidgetCardChart extends Component { } tableResultComponent({loading, tableResults}: TableResultProps): React.ReactNode { - const {widget, selection, organization, setWidgetSort, tableWidths, setTableWidths} = - this.props; - if (typeof tableResults === 'undefined') { + const { + widget, + selection, + organization, + setWidgetSort, + tableWidths, + setTableWidths, + isPreview, + } = this.props; + if (typeof tableResults === 'undefined' || loading) { // Align height to other charts. return ; } - const sort = widget.queries[0]?.orderby; - return ( - - setTableWidths?.(w)} - usesLocationQuery={false} - /> - - ); + return tableResults.map((result, _) => { + const sort = widget.queries[0]?.orderby; + return ( + + setTableWidths?.(w)} + usesLocationQuery={isPreview} + /> + + ); + }); } bigNumberComponent({loading, tableResults}: TableResultProps): React.ReactNode { diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx index 13ad464107d10e..2f6d3678981230 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx @@ -20,6 +20,7 @@ type Props = { tableResults: TableDataWithTitle[] | undefined; widget: Widget; errorMessage?: string; + isPreview?: boolean; setTableWidths?: (tableWidths: string[]) => void; setWidgetSort?: (ns: string) => void; tableWidths?: string[]; @@ -35,6 +36,7 @@ export function IssueWidgetCard({ organization, tableWidths, setTableWidths, + isPreview, }: Props) { if (errorMessage) { return ( @@ -72,7 +74,7 @@ export function IssueWidgetCard({ stickyHeader setWidgetSort={setWidgetSort} setWidths={(w: string[]) => setTableWidths?.(w)} - usesLocationQuery={false} + usesLocationQuery={isPreview} /> ); diff --git a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx index 56503b4dcb78bd..14cb49cb53c4c0 100644 --- a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx +++ b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx @@ -40,6 +40,7 @@ type Props = { expandNumbers?: boolean; handleWidgetSort?: (ns: string) => void; isMobile?: boolean; + isPreview?: boolean; legendOptions?: LegendComponentOption; minTableColumnWidth?: string; noPadding?: boolean; @@ -91,6 +92,7 @@ export function WidgetCardChartContainer({ disableZoom, showLoadingText, handleWidgetSort, + isPreview, }: Props) { const location = useLocation(); // Used to maintain correct widths when sorting/column resizing the table widget @@ -176,6 +178,7 @@ export function WidgetCardChartContainer({ organization={organization} tableWidths={tableWidths} setTableWidths={setTableWidths} + isPreview={isPreview} /> ); @@ -222,6 +225,7 @@ export function WidgetCardChartContainer({ setWidgetSort={handleWidgetSort} tableWidths={tableWidths} setTableWidths={setTableWidths} + isPreview={isPreview} /> ); diff --git a/static/app/views/dashboards/widgetTable.tsx b/static/app/views/dashboards/widgetTable.tsx index 0a907f5873cc88..1e02db254e9ab3 100644 --- a/static/app/views/dashboards/widgetTable.tsx +++ b/static/app/views/dashboards/widgetTable.tsx @@ -3,7 +3,6 @@ import {useTheme} from '@emotion/react'; import type {GridColumnOrder} from 'sentry/components/gridEditable'; import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; -import {WidgetViewerQueryField} from 'sentry/components/modals/widgetViewerModal/utils'; import {renderGridBodyCell} from 'sentry/components/modals/widgetViewerModal/widgetViewerTableCell'; import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; @@ -115,7 +114,7 @@ export function WidgetTable(props: Props) { pathname: location.pathname, query: { ...location.query, - [WidgetViewerQueryField.WIDTH]: newWidths, + width: newWidths, }, }, {replace: true} @@ -160,7 +159,6 @@ export function WidgetTable(props: Props) { }} bodyStyle={style} stickyHeader={stickyHeader} - scrollable /> ); From 2479dbfdd37837b27b7dcd70fa19dda87348e92e Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Tue, 10 Jun 2025 13:13:25 -0400 Subject: [PATCH 4/9] bring back min width --- .../widgetBuilder/components/widgetPreview.tsx | 11 ++++------- static/app/views/dashboards/widgetCard/chart.tsx | 2 ++ static/app/views/dashboards/widgetTable.tsx | 7 ++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx index 3826015f0d6862..d434acf908daa3 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx @@ -17,6 +17,7 @@ import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder import WidgetCard from 'sentry/views/dashboards/widgetCard'; import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; +import {getColumnSortFromString} from 'sentry/views/dashboards/widgetTable'; interface WidgetPreviewProps { dashboard: DashboardDetails; @@ -45,15 +46,11 @@ function WidgetPreview({ const widget = convertBuilderStateToWidget(state); const updateWidgetSort = (newSort: string) => { - if (newSort.startsWith('-')) + const sortFields = getColumnSortFromString(newSort); + if (sortFields.length > 0 && sortFields[0]) dispatch({ type: BuilderStateAction.SET_SORT, - payload: [{field: newSort.substring(1), kind: 'desc'}], - }); - else - dispatch({ - type: BuilderStateAction.SET_SORT, - payload: [{field: newSort, kind: 'asc'}], + payload: [{field: sortFields[0].key, kind: sortFields[0].order}], }); }; diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index 9b41d415995dea..2b8cbd31cc546d 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -153,6 +153,7 @@ class WidgetCardChart extends Component { tableWidths, setTableWidths, isPreview, + minTableColumnWidth, } = this.props; if (typeof tableResults === 'undefined' || loading) { // Align height to other charts. @@ -187,6 +188,7 @@ class WidgetCardChart extends Component { setWidgetSort={setWidgetSort} setWidths={(w: string[]) => setTableWidths?.(w)} usesLocationQuery={isPreview} + minColumnWidth={minTableColumnWidth} /> ); diff --git a/static/app/views/dashboards/widgetTable.tsx b/static/app/views/dashboards/widgetTable.tsx index 1e02db254e9ab3..d1eaa7fed44f23 100644 --- a/static/app/views/dashboards/widgetTable.tsx +++ b/static/app/views/dashboards/widgetTable.tsx @@ -30,15 +30,14 @@ interface Props { widget: Widget; widths: string[]; customHeaderClick?: () => void; + minColumnWidth?: string; setWidgetSort?: (ns: string) => void; setWidths?: (w: string[]) => void; stickyHeader?: boolean; usesLocationQuery?: boolean; } -export const getColumnSortFromString = ( - sort: string -): Array> => { +export const getColumnSortFromString = (sort: string): Array> => { if (sort.length < 1) return []; if (sort.startsWith('-')) return [ @@ -70,6 +69,7 @@ export function WidgetTable(props: Props) { widths, setWidgetSort, setWidths, + minColumnWidth, } = props; const theme = useTheme(); const location = useLocation(); @@ -159,6 +159,7 @@ export function WidgetTable(props: Props) { }} bodyStyle={style} stickyHeader={stickyHeader} + minimumColWidth={minColumnWidth ? parseInt(minColumnWidth, 10) : undefined} /> ); From 4739674bbf3a7f61f245eb56091678b8760671eb Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 12 Jun 2025 11:25:27 -0400 Subject: [PATCH 5/9] fix styles, refactor viewer modal --- static/app/components/gridEditable/index.tsx | 12 +- static/app/components/gridEditable/styles.tsx | 17 +- .../components/modals/widgetViewerModal.tsx | 174 +++++------------- .../components/widgetPreview.tsx | 4 +- .../app/views/dashboards/widgetCard/chart.tsx | 12 +- .../app/views/dashboards/widgetCard/index.tsx | 18 +- .../dashboards/widgetCard/issueWidgetCard.tsx | 12 +- .../widgetCard/widgetCardChartContainer.tsx | 2 +- static/app/views/dashboards/widgetTable.tsx | 28 ++- 9 files changed, 119 insertions(+), 160 deletions(-) diff --git a/static/app/components/gridEditable/index.tsx b/static/app/components/gridEditable/index.tsx index 27147487e19f00..c7592cbfe5442f 100644 --- a/static/app/components/gridEditable/index.tsx +++ b/static/app/components/gridEditable/index.tsx @@ -98,23 +98,24 @@ type GridEditableProps = { bodyStyle?: React.CSSProperties; emptyMessage?: React.ReactNode; error?: unknown | null; + fitMaxContent?: boolean; + /** * Inject a set of buttons into the top of the grid table. * The controlling component is responsible for handling any actions * in these buttons and updating props to the GridEditable instance. */ headerButtons?: () => React.ReactNode; - height?: string | number; + highlightedRowKey?: number; isLoading?: boolean; minimumColWidth?: number; - onRowMouseOut?: (row: DataRow, key: number, event: React.MouseEvent) => void; - onRowMouseOver?: (row: DataRow, key: number, event: React.MouseEvent) => void; + onRowMouseOver?: (row: DataRow, key: number, event: React.MouseEvent) => void; /** * Whether columns in the grid can be resized. * @@ -465,6 +466,8 @@ class GridEditable< height, 'aria-label': ariaLabel, bodyStyle, + stickyHeader, + fitMaxContent, } = this.props; const showHeader = title || headerButtons; return ( @@ -485,8 +488,9 @@ class GridEditable< scrollable={scrollable} height={height} ref={this.refGrid} + fitMaxContent={fitMaxContent} > - {this.renderGridHead()} + {this.renderGridHead()} {this.renderGridBody()} diff --git a/static/app/components/gridEditable/styles.tsx b/static/app/components/gridEditable/styles.tsx index 8e08528221d6e6..ba27106ebf1c66 100644 --- a/static/app/components/gridEditable/styles.tsx +++ b/static/app/components/gridEditable/styles.tsx @@ -57,7 +57,7 @@ export const Body = styled( showVerticalScrollbar?: boolean; }) => ( - {children} + {children} ) )` @@ -79,7 +79,11 @@ export const Body = styled( * , , are ignored by CSS Grid. * The entire layout is determined by the usage of and . */ -export const Grid = styled('table')<{height?: string | number; scrollable?: boolean}>` +export const Grid = styled('table')<{ + fitMaxContent?: boolean; + height?: string | number; + scrollable?: boolean; +}>` position: inherit; display: grid; @@ -103,6 +107,11 @@ export const Grid = styled('table')<{height?: string | number; scrollable?: bool max-height: ${typeof p.height === 'number' ? p.height + 'px' : p.height}; ` : ''} + ${p => + p.fitMaxContent && + css` + min-width: max-content; + `} `; /** @@ -110,7 +119,7 @@ export const Grid = styled('table')<{height?: string | number; scrollable?: bool * Grid. As the entirety of the add/remove/resize actions are performed on the * header, most of the elements behave different for each stage. */ -export const GridHead = styled('thead')` +export const GridHead = styled('thead')<{sticky?: boolean}>` display: grid; grid-template-columns: subgrid; grid-column: 1/-1; @@ -126,6 +135,8 @@ export const GridHead = styled('thead')` border-top-left-radius: ${p => p.theme.borderRadius}; border-top-right-radius: ${p => p.theme.borderRadius}; + + ${p => (p.sticky ? `position: sticky; top: 0; z-index: ${Z_INDEX_GRID + 1}` : '')} `; export const GridHeadCell = styled('th')<{isFirst: boolean; sticky?: boolean}>` diff --git a/static/app/components/modals/widgetViewerModal.tsx b/static/app/components/modals/widgetViewerModal.tsx index 4e69f0fef05009..550b3ee57f2a42 100644 --- a/static/app/components/modals/widgetViewerModal.tsx +++ b/static/app/components/modals/widgetViewerModal.tsx @@ -1,5 +1,5 @@ import {Fragment, memo, useEffect, useMemo, useState} from 'react'; -import {css, useTheme} from '@emotion/react'; +import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {truncate} from '@sentry/core'; import * as Sentry from '@sentry/react'; @@ -20,8 +20,6 @@ import {Select} from 'sentry/components/core/select'; import {SelectOption} from 'sentry/components/core/select/option'; import {Tooltip} from 'sentry/components/core/tooltip'; import {components} from 'sentry/components/forms/controls/reactSelectWrapper'; -import type {GridColumnOrder} from 'sentry/components/gridEditable'; -import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; import Pagination from 'sentry/components/pagination'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {parseSearch} from 'sentry/components/searchSyntax/parser'; @@ -55,7 +53,6 @@ import {decodeInteger, decodeList, decodeScalar} from 'sentry/utils/queryString' import useApi from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; -import useProjects from 'sentry/utils/useProjects'; import {useUser} from 'sentry/utils/useUser'; import {useUserTeams} from 'sentry/utils/useUserTeams'; import withPageFilters from 'sentry/utils/withPageFilters'; @@ -95,13 +92,12 @@ import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidg import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer'; import WidgetQueries from 'sentry/views/dashboards/widgetCard/widgetQueries'; import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; -import {decodeColumnOrder} from 'sentry/views/discover/utils'; +import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; import {MetricsDataSwitcher} from 'sentry/views/performance/landing/metricsDataSwitcher'; import {WidgetViewerQueryField} from './widgetViewerModal/utils'; import { renderDiscoverGridHeaderCell, - renderGridBodyCell, renderIssueGridHeaderCell, renderReleaseGridHeaderCell, } from './widgetViewerModal/widgetViewerTableCell'; @@ -200,9 +196,7 @@ function WidgetViewerModal(props: Props) { confidence, sampleCount, } = props; - const theme = useTheme(); const location = useLocation(); - const {projects} = useProjects(); const navigate = useNavigate(); // TODO(Tele-Team): Re-enable this when we have a better way to determine if the data is transaction only // let widgetContentLoadingStatus: boolean | undefined = undefined; @@ -378,17 +372,6 @@ function WidgetViewerModal(props: Props) { modalSelection ); - let columnOrder = decodeColumnOrder( - fields.map(field => ({ - field, - })) - ); - const columnSortBy = eventView.getSorts(); - columnOrder = columnOrder.map((column, index) => ({ - ...column, - width: parseInt(widths[index]!, 10) || -1, - })); - const getOnDemandFilterWarning = createOnDemandFilterWarning( t( 'We don’t routinely collect metrics from this property. As such, historical data may be limited.' @@ -432,25 +415,6 @@ function WidgetViewerModal(props: Props) { }; }); - const onResizeColumn = (columnIndex: number, nextColumn: GridColumnOrder) => { - const newWidth = nextColumn.width ? Number(nextColumn.width) : COL_WIDTH_UNDEFINED; - const newWidths: number[] = new Array(Math.max(columnIndex, widths.length)).fill( - COL_WIDTH_UNDEFINED - ); - widths.forEach((width, index) => (newWidths[index] = parseInt(width, 10))); - newWidths[columnIndex] = newWidth; - navigate( - { - pathname: location.pathname, - query: { - ...location.query, - [WidgetViewerQueryField.WIDTH]: newWidths, - }, - }, - {replace: true} - ); - }; - // Get discover result totals useEffect(() => { const getDiscoverTotals = async () => { @@ -478,44 +442,29 @@ function WidgetViewerModal(props: Props) { loading, pageLinks, }: GenericWidgetQueriesChildrenProps) { - const {isMetricsData} = useDashboardsMEPContext(); const links = parseLinkHeader(pageLinks ?? null); const isFirstPage = links.previous?.results === false; return ( - { - if ( - [DisplayType.TOP_N, DisplayType.TABLE].includes(widget.displayType) || - defined(widget.limit) - ) { - setChartUnmodified(false); - } - }, - isMetricsData, - }) as (column: GridColumnOrder, columnIndex: number) => React.ReactNode, - renderBodyCell: renderGridBodyCell({ - ...props, - location, - tableData: tableResults?.[0], - isFirstPage, - projects, - eventView, - theme, - }), - onResizeColumn, + { + if ( + [DisplayType.TOP_N, DisplayType.TABLE].includes(widget.displayType) || + defined(widget.limit) + ) { + setChartUnmodified(false); + } }} + widget={tableWidget} + organization={organization} + sort={sort || ''} + widths={widths} + usesLocationQuery + selection={selection} + isFirstPage={isFirstPage} /> {(links?.previous?.results || links?.next?.results) && ( - { - setChartUnmodified(false); - }, - }) as (column: GridColumnOrder, columnIndex: number) => React.ReactNode, - renderBodyCell: renderGridBodyCell({ - location, - theme, - organization, - selection, - widget: tableWidget, - }), - onResizeColumn, + { + setChartUnmodified(false); }} + widget={tableWidget} + organization={organization} + sort={sort || ''} + widths={widths} + usesLocationQuery + selection={selection} /> {(links?.previous?.results || links?.next?.results) && ( - { - if ( - [DisplayType.TOP_N, DisplayType.TABLE].includes(widget.displayType) || - defined(widget.limit) - ) { - setChartUnmodified(false); - } - }, - }) as (column: GridColumnOrder, columnIndex: number) => React.ReactNode, - renderBodyCell: renderGridBodyCell({ - ...props, - location, - theme, - tableData: tableResults?.[0], - isFirstPage, - }), - onResizeColumn, + { + if ( + [DisplayType.TOP_N, DisplayType.TABLE].includes(widget.displayType) || + defined(widget.limit) + ) { + setChartUnmodified(false); + } }} /> {!tableWidget.queries[0]!.orderby.match(/^-?release$/) && diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx index d434acf908daa3..992bd12540dfef 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx @@ -27,7 +27,7 @@ interface WidgetPreviewProps { shouldForceDescriptionTooltip?: boolean; } -const MIN_TABLE_COLUMN_WIDTH = '125px'; +const MIN_TABLE_COLUMN_WIDTH_PX = 125; function WidgetPreview({ dashboard, @@ -125,7 +125,7 @@ function WidgetPreview({ showConfidenceWarning={widget.widgetType === WidgetType.SPANS} // ensure table columns are at least a certain width (helps with lack of truncation on large fields) - minTableColumnWidth={MIN_TABLE_COLUMN_WIDTH} + minTableColumnWidth={MIN_TABLE_COLUMN_WIDTH_PX} disableZoom showLoadingText isPreview diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index 2b8cbd31cc546d..6a1efdf7d1487a 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -57,6 +57,7 @@ import getDynamicText from 'sentry/utils/getDynamicText'; import type {Widget} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize'; +import {TABLE_WIDGET_STYLES} from 'sentry/views/dashboards/widgetCard'; import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization'; @@ -91,7 +92,7 @@ type WidgetCardChartProps = Pick< isPreview?: boolean; isSampled?: boolean | null; legendOptions?: LegendComponentOption; - minTableColumnWidth?: string; + minTableColumnWidth?: number; noPadding?: boolean; onLegendSelectChanged?: EChartEventHandler<{ name: string; @@ -165,13 +166,7 @@ class WidgetCardChart extends Component { return ( { setWidths={(w: string[]) => setTableWidths?.(w)} usesLocationQuery={isPreview} minColumnWidth={minTableColumnWidth} + fitMaxContent={!isPreview} /> ); diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 0b8a454f0daeed..a651788e489fa4 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -59,6 +59,20 @@ export const SESSION_DURATION_ALERT = ( {SESSION_DURATION_ALERT_TEXT} ); +// Used in widget preview and on the dashboard +export const TABLE_WIDGET_STYLES = { + // Makes the top edges of the table sharp + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + // Removes extra bordering from the table + marginBottom: 0, + borderLeft: 0, + borderRight: 0, + borderBottom: 0, + // Get sticky headers to work + height: '100%', +}; + type Props = WithRouterProps & { api: Client; isEditingDashboard: boolean; @@ -81,7 +95,7 @@ type Props = WithRouterProps & { isPreview?: boolean; isWidgetInvalid?: boolean; legendOptions?: LegendComponentOption; - minTableColumnWidth?: string; + minTableColumnWidth?: number; onDataFetched?: (results: TableDataWithTitle[]) => void; onDelete?: () => void; onDuplicate?: () => void; @@ -153,6 +167,7 @@ function WidgetCard(props: Props) { disableZoom, showLoadingText, handleWidgetSort, + isPreview, } = props; if (widget.displayType === DisplayType.TOP_N) { @@ -318,6 +333,7 @@ function WidgetCard(props: Props) { onDataFetchStart={onDataFetchStart} showLoadingText={showLoadingText && isLoadingTextVisible} handleWidgetSort={handleWidgetSort} + isPreview={isPreview} /> diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx index 2f6d3678981230..c8e0beadf6c919 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx @@ -10,6 +10,7 @@ import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; import type {Widget} from 'sentry/views/dashboards/types'; +import {TABLE_WIDGET_STYLES} from 'sentry/views/dashboards/widgetCard'; import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; type Props = { @@ -56,13 +57,7 @@ export function IssueWidgetCard({ return ( setTableWidths?.(w)} usesLocationQuery={isPreview} + fitMaxContent={!isPreview} /> ); @@ -89,5 +85,5 @@ const TableWrapper = styled('div')` min-height: 0; border-bottom-left-radius: ${p => p.theme.borderRadius}; border-bottom-right-radius: ${p => p.theme.borderRadius}; - overflow: auto; + display: flex; `; diff --git a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx index 14cb49cb53c4c0..fa5b3badf5e12e 100644 --- a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx +++ b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx @@ -42,7 +42,7 @@ type Props = { isMobile?: boolean; isPreview?: boolean; legendOptions?: LegendComponentOption; - minTableColumnWidth?: string; + minTableColumnWidth?: number; noPadding?: boolean; onDataFetchStart?: () => void; onDataFetched?: (results: { diff --git a/static/app/views/dashboards/widgetTable.tsx b/static/app/views/dashboards/widgetTable.tsx index d1eaa7fed44f23..bfdccc1a669e80 100644 --- a/static/app/views/dashboards/widgetTable.tsx +++ b/static/app/views/dashboards/widgetTable.tsx @@ -17,7 +17,7 @@ import {useDashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashbo import type {TableColumn, TableColumnSort} from 'sentry/views/discover/table/types'; import {decodeColumnOrder} from 'sentry/views/discover/utils'; -interface Props { +interface WidgetTableProps { loading: boolean; organization: Organization; renderHeaderGridCell: ( @@ -25,15 +25,17 @@ interface Props { ) => (column: TableColumn, _columnIndex: number) => React.ReactNode; selection: PageFilters; sort: string; - style: any; tableResults: TableDataWithTitle[]; widget: Widget; widths: string[]; customHeaderClick?: () => void; - minColumnWidth?: string; + fitMaxContent?: boolean; + isFirstPage?: boolean; + minColumnWidth?: number; setWidgetSort?: (ns: string) => void; setWidths?: (w: string[]) => void; stickyHeader?: boolean; + style?: any; usesLocationQuery?: boolean; } @@ -54,7 +56,7 @@ export const getColumnSortFromString = (sort: string): Array (newWidths[index] = parseInt(width, 10))); newWidths[columnIndex] = newWidth; setWidths?.(newWidths.map(String)); - // Editing a widget relies on location query, while state is used in dashboard + // Some use cases rely on state. + // Ex. modal viewer widget table uses location query, while state is used when + // there are multiple widgets on the dashboard if (usesLocationQuery) { navigate( { @@ -124,6 +130,7 @@ export function WidgetTable(props: Props) { const onHeaderClick = (newSort?: string) => { customHeaderClick?.(); + // To trigger a rerender when relying on widget state setWidgetSort?.(newSort || ''); }; @@ -149,7 +156,7 @@ export function WidgetTable(props: Props) { ...props, location, tableData: tableResults?.[0], - isFirstPage: false, + isFirstPage, projects, eventView, theme, @@ -159,7 +166,10 @@ export function WidgetTable(props: Props) { }} bodyStyle={style} stickyHeader={stickyHeader} - minimumColWidth={minColumnWidth ? parseInt(minColumnWidth, 10) : undefined} + scrollable + height={'100%'} + minimumColWidth={minColumnWidth} + fitMaxContent={fitMaxContent} /> ); From b7db9fd0c1903256df2a91b5729669d4f5d1494c Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 12 Jun 2025 12:47:17 -0400 Subject: [PATCH 6/9] fix unit tests --- .../components/modals/widgetViewerModal.tsx | 3 +++ .../widgetViewerTableCell.tsx | 22 +++++++++++++++++-- .../app/views/dashboards/widgetCard/chart.tsx | 2 +- .../dashboards/widgetCard/index.spec.tsx | 8 +++---- .../dashboards/widgetCard/issueWidgetCard.tsx | 2 +- static/app/views/dashboards/widgetTable.tsx | 13 +++++++---- 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/static/app/components/modals/widgetViewerModal.tsx b/static/app/components/modals/widgetViewerModal.tsx index 550b3ee57f2a42..07010208c5598b 100644 --- a/static/app/components/modals/widgetViewerModal.tsx +++ b/static/app/components/modals/widgetViewerModal.tsx @@ -465,6 +465,7 @@ function WidgetViewerModal(props: Props) { usesLocationQuery selection={selection} isFirstPage={isFirstPage} + combineColumnsAndAggregates /> {(links?.previous?.results || links?.next?.results) && ( {(links?.previous?.results || links?.next?.results) && ( {!tableWidget.queries[0]!.orderby.match(/^-?release$/) && (links?.previous?.results || links?.next?.results) && ( diff --git a/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx b/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx index 895727e9b79375..2d56ba305af47b 100644 --- a/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx +++ b/static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx @@ -32,7 +32,11 @@ import {getCustomEventsFieldRenderer} from 'sentry/views/dashboards/datasetConfi import type {Widget} from 'sentry/views/dashboards/types'; import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import {eventViewFromWidget} from 'sentry/views/dashboards/utils'; -import {ISSUE_FIELDS} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields'; +import { + FieldKey, + ISSUE_FIELD_TO_HEADER_MAP, + ISSUE_FIELDS, +} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields'; import {TransactionLink} from 'sentry/views/discover/table/tableView'; import {TopResultsIndicator} from 'sentry/views/discover/table/topResultsIndicator'; import type {TableColumn} from 'sentry/views/discover/table/types'; @@ -73,10 +77,24 @@ export const renderIssueGridHeaderCell = ({ const align = fieldAlignment(column.name, column.type, tableMeta); const sortField = getSortField(String(column.key)); + const getHumanReadableName = (columnName: string) => { + if ( + columnName === FieldKey.LIFETIME_EVENTS || + columnName === FieldKey.LIFETIME_USERS + ) { + return ISSUE_FIELD_TO_HEADER_MAP[columnName]; + } + return columnName; + }; + return ( {column.name}} + title={ + + {getHumanReadableName(column.name)} + + } direction={widget.queries[0]!.orderby === sortField ? 'desc' : undefined} canSort={!!sortField} generateSortLink={ diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx index 6a1efdf7d1487a..ef5742643d96dd 100644 --- a/static/app/views/dashboards/widgetCard/chart.tsx +++ b/static/app/views/dashboards/widgetCard/chart.tsx @@ -61,7 +61,7 @@ import {TABLE_WIDGET_STYLES} from 'sentry/views/dashboards/widgetCard'; import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization'; -import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; +import WidgetTable from 'sentry/views/dashboards/widgetTable'; import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter'; import type {GenericWidgetQueriesChildrenProps} from './genericWidgetQueries'; diff --git a/static/app/views/dashboards/widgetCard/index.spec.tsx b/static/app/views/dashboards/widgetCard/index.spec.tsx index f06149082f2109..affc24b206e336 100644 --- a/static/app/views/dashboards/widgetCard/index.spec.tsx +++ b/static/app/views/dashboards/widgetCard/index.spec.tsx @@ -14,7 +14,6 @@ import { import * as modal from 'sentry/actionCreators/modal'; import * as LineChart from 'sentry/components/charts/lineChart'; -import SimpleTableChart from 'sentry/components/charts/simpleTableChart'; import {DatasetSource} from 'sentry/utils/discover/types'; import {MINUTE, SECOND} from 'sentry/utils/formatters'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; @@ -23,10 +22,11 @@ import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; import WidgetCard from 'sentry/views/dashboards/widgetCard'; import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidgetQueries'; import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; +import WidgetTable from 'sentry/views/dashboards/widgetTable'; import {DashboardsMEPProvider} from './dashboardsMEPContext'; -jest.mock('sentry/components/charts/simpleTableChart', () => jest.fn(() =>
)); +jest.mock('sentry/views/dashboards/widgetTable', () => jest.fn(() =>
)); jest.mock('sentry/views/dashboards/widgetCard/releaseWidgetQueries'); describe('Dashboards > WidgetCard', function () { @@ -520,8 +520,8 @@ describe('Dashboards > WidgetCard', function () { await waitFor(() => expect(eventsMock).toHaveBeenCalled()); await waitFor(() => - expect((SimpleTableChart as jest.Mock).mock.calls[0][0]).toEqual( - expect.objectContaining({stickyHeaders: true}) + expect((WidgetTable as jest.Mock).mock.calls[0][0]).toEqual( + expect.objectContaining({stickyHeader: true}) ) ); }); diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx index c8e0beadf6c919..2b519f5bffcfa0 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx @@ -11,7 +11,7 @@ import type {Organization} from 'sentry/types/organization'; import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery'; import type {Widget} from 'sentry/views/dashboards/types'; import {TABLE_WIDGET_STYLES} from 'sentry/views/dashboards/widgetCard'; -import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; +import WidgetTable from 'sentry/views/dashboards/widgetTable'; type Props = { loading: boolean; diff --git a/static/app/views/dashboards/widgetTable.tsx b/static/app/views/dashboards/widgetTable.tsx index bfdccc1a669e80..607e76c76d75aa 100644 --- a/static/app/views/dashboards/widgetTable.tsx +++ b/static/app/views/dashboards/widgetTable.tsx @@ -28,6 +28,7 @@ interface WidgetTableProps { tableResults: TableDataWithTitle[]; widget: Widget; widths: string[]; + combineColumnsAndAggregates?: boolean; customHeaderClick?: () => void; fitMaxContent?: boolean; isFirstPage?: boolean; @@ -56,7 +57,7 @@ export const getColumnSortFromString = (sort: string): Array ({ @@ -174,3 +177,5 @@ export function WidgetTable(props: WidgetTableProps) { ); } + +export default WidgetTable; From 7bb0da4aa2d82eb6fc1bdfec12f0988a8424f105 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 12 Jun 2025 13:11:40 -0400 Subject: [PATCH 7/9] fix import error --- static/app/components/modals/widgetViewerModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/modals/widgetViewerModal.tsx b/static/app/components/modals/widgetViewerModal.tsx index 07010208c5598b..d912f50cf87c96 100644 --- a/static/app/components/modals/widgetViewerModal.tsx +++ b/static/app/components/modals/widgetViewerModal.tsx @@ -92,7 +92,7 @@ import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidg import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer'; import WidgetQueries from 'sentry/views/dashboards/widgetCard/widgetQueries'; import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState'; -import {WidgetTable} from 'sentry/views/dashboards/widgetTable'; +import WidgetTable from 'sentry/views/dashboards/widgetTable'; import {MetricsDataSwitcher} from 'sentry/views/performance/landing/metricsDataSwitcher'; import {WidgetViewerQueryField} from './widgetViewerModal/utils'; From 0a7f05c813a086753fd8b3b4784e2374d982b120 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 12 Jun 2025 13:35:07 -0400 Subject: [PATCH 8/9] delete unused table --- .../components/charts/simpleTableChart.tsx | 177 ------------------ .../buildSteps/visualizationStep.tsx | 5 - 2 files changed, 182 deletions(-) delete mode 100644 static/app/components/charts/simpleTableChart.tsx diff --git a/static/app/components/charts/simpleTableChart.tsx b/static/app/components/charts/simpleTableChart.tsx deleted file mode 100644 index b6f38c9d349cc7..00000000000000 --- a/static/app/components/charts/simpleTableChart.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import {Fragment} from 'react'; -import {css, useTheme} from '@emotion/react'; -import styled from '@emotion/styled'; -import type {Location} from 'history'; - -import {Tooltip} from 'sentry/components/core/tooltip'; -import type {PanelTableProps} from 'sentry/components/panels/panelTable'; -import {PanelTable, PanelTableHeader} from 'sentry/components/panels/panelTable'; -import Truncate from 'sentry/components/truncate'; -import {space} from 'sentry/styles/space'; -import type {Organization} from 'sentry/types/organization'; -import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery'; -import type {MetaType} from 'sentry/utils/discover/eventView'; -import type EventView from 'sentry/utils/discover/eventView'; -import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; -import {fieldAlignment} from 'sentry/utils/discover/fields'; -import useOrganization from 'sentry/utils/useOrganization'; -import useProjects from 'sentry/utils/useProjects'; -import {TransactionLink} from 'sentry/views/discover/table/tableView'; -import {TopResultsIndicator} from 'sentry/views/discover/table/topResultsIndicator'; -import { - decodeColumnOrder, - getTargetForTransactionSummaryLink, -} from 'sentry/views/discover/utils'; - -type Props = { - eventView: EventView; - fieldAliases: string[]; - fields: string[]; - loading: boolean; - location: Location; - title: string; - className?: string; - data?: TableData['data']; - fieldHeaderMap?: Record; - getCustomFieldRenderer?: ( - field: string, - meta: MetaType, - organization?: Organization - ) => ReturnType | null; - loader?: PanelTableProps['loader']; - metadata?: TableData['meta']; - minColumnWidth?: string; - stickyHeaders?: boolean; - topResultsIndicators?: number; -}; - -function SimpleTableChart({ - className, - loading, - eventView, - fields, - metadata, - data, - title, - fieldHeaderMap, - stickyHeaders, - getCustomFieldRenderer, - topResultsIndicators, - location, - fieldAliases, - loader, - minColumnWidth, -}: Props) { - const organization = useOrganization(); - const {projects} = useProjects(); - const theme = useTheme(); - function renderRow( - index: number, - row: TableDataRow, - tableMeta: NonNullable, - columns: ReturnType - ) { - return columns.map((column, columnIndex) => { - const fieldRenderer = - getCustomFieldRenderer?.(column.key, tableMeta, organization) ?? - getFieldRenderer(column.key, tableMeta); - - const unit = tableMeta.units?.[column.key]; - let cell = fieldRenderer(row, {organization, location, eventView, unit, theme}); - - if (column.key === 'transaction' && row.transaction) { - cell = ( - - {cell} - - ); - } - - return ( - - {topResultsIndicators && columnIndex === 0 && ( - - )} - {cell} - - ); - }); - } - - const meta = metadata ?? {}; - const columns = decodeColumnOrder( - fields.map((field, index) => ({field, alias: fieldAliases[index]})) - ); - - return ( - - {title &&

{title}

} - { - const align = fieldAlignment(column.name, column.type, meta); - const header = - column.column.alias || (fieldHeaderMap?.[column.key] ?? column.name); - return ( - - - - - - ); - })} - isEmpty={!data?.length} - stickyHeaders={stickyHeaders} - disablePadding - > - {data?.map((row, index) => renderRow(index, row, meta, columns))} - -
- ); -} - -const StyledTruncate = styled(Truncate)` - white-space: nowrap; -`; - -const StyledPanelTable = styled(PanelTable)` - border-radius: 0; - border-left: 0; - border-right: 0; - border-bottom: 0; - - margin: 0; - ${PanelTableHeader} { - height: min-content; - } -`; - -type HeadCellProps = { - align: string | undefined; -}; -const HeadCell = styled('div')` - ${(p: HeadCellProps) => (p.align ? `text-align: ${p.align};` : '')} - padding: ${space(1)} ${space(3)}; -`; - -export const TableCell = styled('div')` - padding: ${space(1)} ${space(3)}; -`; - -export default SimpleTableChart; diff --git a/static/app/views/dashboards/widgetBuilder/buildSteps/visualizationStep.tsx b/static/app/views/dashboards/widgetBuilder/buildSteps/visualizationStep.tsx index 949d0738211737..79cc334ccc019c 100644 --- a/static/app/views/dashboards/widgetBuilder/buildSteps/visualizationStep.tsx +++ b/static/app/views/dashboards/widgetBuilder/buildSteps/visualizationStep.tsx @@ -6,7 +6,6 @@ import type {Location} from 'history'; import debounce from 'lodash/debounce'; import isEqual from 'lodash/isEqual'; -import {TableCell} from 'sentry/components/charts/simpleTableChart'; import {Select} from 'sentry/components/core/select'; import FieldGroup from 'sentry/components/forms/fieldGroup'; import PanelAlert from 'sentry/components/panels/panelAlert'; @@ -177,10 +176,6 @@ const VisualizationWrapper = styled('div')<{displayType: DisplayType}>` p.displayType === DisplayType.TABLE && css` overflow: hidden; - ${TableCell} { - /* 24px ActorContainer height + 16px top and bottom padding + 1px border = 41px */ - height: 41px; - } ${WidgetCardPanel} { /* total size of a table, if it would display 5 rows of content */ height: 301px; From 97189defc9885445ee0491edc132b5f9f8cf91ec Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 12 Jun 2025 15:09:18 -0400 Subject: [PATCH 9/9] remove unnecessary display flex --- static/app/views/dashboards/widgetCard/issueWidgetCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx index 2b519f5bffcfa0..95284bb02e52d7 100644 --- a/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx +++ b/static/app/views/dashboards/widgetCard/issueWidgetCard.tsx @@ -85,5 +85,4 @@ const TableWrapper = styled('div')` min-height: 0; border-bottom-left-radius: ${p => p.theme.borderRadius}; border-bottom-right-radius: ${p => p.theme.borderRadius}; - display: flex; `;