Skip to content

Commit eb42cac

Browse files
authored
feat(dashboards): add column resizing to dashboard tables (#94997)
### Changes Add column resizing to dashboard widgets and widget preview. The columns do not persist between page reload right now. Only visible to perf team (feature flag). ### Video https://github.com/user-attachments/assets/eb452926-8b0d-47cb-a896-d896da7c0362
1 parent 6327368 commit eb42cac

File tree

10 files changed

+63
-10
lines changed

10 files changed

+63
-10
lines changed

static/app/views/dashboards/dashboard.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {DatasetSource} from 'sentry/utils/discover/types';
3030
import withApi from 'sentry/utils/withApi';
3131
import withPageFilters from 'sentry/utils/withPageFilters';
3232
import type {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
33+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
3334

3435
import AddWidget, {ADD_WIDGET_BUTTON_DRAG_ID} from './addWidget';
3536
import type {Position} from './layoutUtils';
@@ -369,6 +370,21 @@ class Dashboard extends Component<Props, State> {
369370
};
370371
}
371372

373+
handleWidgetColumnTableResize(index: number) {
374+
const {dashboard, onUpdate} = this.props;
375+
return function (columns: TabularColumn[]) {
376+
const widths = columns.map(column => column.width as number);
377+
const widget = dashboard.widgets[index]!;
378+
const widgetCopy = cloneDeep(widget);
379+
widgetCopy.tableWidths = widths;
380+
381+
const nextList = [...dashboard.widgets];
382+
nextList[index] = widgetCopy;
383+
384+
onUpdate(nextList);
385+
};
386+
}
387+
372388
renderWidget(widget: Widget, index: number) {
373389
const {isMobile, windowWidth} = this.state;
374390
const {
@@ -410,6 +426,7 @@ class Dashboard extends Component<Props, State> {
410426
newlyAddedWidget={newlyAddedWidget}
411427
onNewWidgetScrollComplete={onNewWidgetScrollComplete}
412428
onWidgetTableSort={this.handleWidgetTableSort(index)}
429+
onWidgetTableResizeColumn={this.handleWidgetColumnTableResize(index)}
413430
/>
414431
</div>
415432
);

static/app/views/dashboards/sortableWidget.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {useUser} from 'sentry/utils/useUser';
1010
import {useUserTeams} from 'sentry/utils/useUserTeams';
1111
import {checkUserHasEditAccess} from 'sentry/views/dashboards/detail';
1212
import WidgetCard from 'sentry/views/dashboards/widgetCard';
13+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
1314

1415
import {DashboardsMEPProvider} from './widgetCard/dashboardsMEPContext';
1516
import {Toolbar} from './widgetCard/toolbar';
@@ -35,6 +36,7 @@ type Props = {
3536
isPreview?: boolean;
3637
newlyAddedWidget?: Widget;
3738
onNewWidgetScrollComplete?: () => void;
39+
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
3840
onWidgetTableSort?: (sort: Sort) => void;
3941
windowWidth?: number;
4042
};
@@ -60,6 +62,7 @@ function SortableWidget(props: Props) {
6062
newlyAddedWidget,
6163
onNewWidgetScrollComplete,
6264
onWidgetTableSort,
65+
onWidgetTableResizeColumn,
6366
} = props;
6467

6568
const organization = useOrganization();
@@ -108,6 +111,7 @@ function SortableWidget(props: Props) {
108111
windowWidth,
109112
tableItemLimit: TABLE_ITEM_LIMIT,
110113
onWidgetTableSort,
114+
onWidgetTableResizeColumn,
111115
};
112116

113117
return (

static/app/views/dashboards/types.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export type Widget = {
105105
layout?: WidgetLayout | null;
106106
// Used to define 'topEvents' when fetching time-series data for a widget
107107
limit?: number;
108+
// Used for table widget column widths, currently is not saved
109+
tableWidths?: number[];
108110
tempId?: string;
109111
thresholds?: ThresholdsConfig | null;
110112
widgetType?: WidgetType;

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {useState} from 'react';
2+
13
import PanelAlert from 'sentry/components/panels/panelAlert';
24
import {dedupeArray} from 'sentry/utils/dedupeArray';
35
import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
@@ -18,6 +20,7 @@ import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder
1820
import WidgetCard from 'sentry/views/dashboards/widgetCard';
1921
import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
2022
import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
23+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
2124

2225
interface WidgetPreviewProps {
2326
dashboard: DashboardDetails;
@@ -42,8 +45,9 @@ function WidgetPreview({
4245
const pageFilters = usePageFilters();
4346

4447
const {state, dispatch} = useWidgetBuilderContext();
48+
const [tableWidths, setTableWidths] = useState<number[]>();
4549

46-
const widget = convertBuilderStateToWidget(state);
50+
const widget = {...convertBuilderStateToWidget(state), tableWidths};
4751

4852
const widgetLegendState = new WidgetLegendSelectionState({
4953
location,
@@ -84,6 +88,11 @@ function WidgetPreview({
8488
});
8589
}
8690

91+
function handleWidgetTableResizeColumn(columns: TabularColumn[]) {
92+
const widths = columns.map(column => column.width as number);
93+
setTableWidths(widths);
94+
}
95+
8796
return (
8897
<WidgetCard
8998
disableFullscreen
@@ -127,6 +136,7 @@ function WidgetPreview({
127136
disableZoom
128137
showLoadingText
129138
onWidgetTableSort={handleWidgetTableSort}
139+
onWidgetTableResizeColumn={handleWidgetTableResizeColumn}
130140
/>
131141
);
132142
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize';
6363
import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
6464
import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
6565
import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization';
66+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
6667
import {TableWidgetVisualization} from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';
6768
import {
6869
convertTableDataToTabularData,
@@ -105,6 +106,7 @@ type WidgetCardChartProps = Pick<
105106
selected: Record<string, boolean>;
106107
type: 'legendselectchanged';
107108
}>;
109+
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
108110
onWidgetTableSort?: (sort: Sort) => void;
109111
onZoom?: EChartDataZoomHandler;
110112
sampleCount?: number;
@@ -155,6 +157,7 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
155157
organization,
156158
theme,
157159
onWidgetTableSort,
160+
onWidgetTableResizeColumn,
158161
} = this.props;
159162
if (loading || !tableResults?.[0]) {
160163
// Align height to other charts.
@@ -181,10 +184,10 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
181184
field,
182185
})),
183186
tableResults[i]?.meta
184-
).map(column => ({
187+
).map((column, index) => ({
185188
key: column.key,
186189
name: column.name,
187-
width: minTableColumnWidth ?? column.width,
190+
width: widget.tableWidths?.[index] ?? minTableColumnWidth ?? column.width,
188191
type: column.type === 'never' ? null : column.type,
189192
sortable:
190193
widget.widgetType === WidgetType.RELEASE ? isAggregateField(column.key) : true,
@@ -226,6 +229,7 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
226229
eventView,
227230
} satisfies RenderFunctionBaggage;
228231
}}
232+
onResizeColumn={onWidgetTableResizeColumn}
229233
/>
230234
) : (
231235
<StyledSimpleTableChart

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import {DEFAULT_RESULTS_LIMIT} from 'sentry/views/dashboards/widgetBuilder/utils';
4141
import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer';
4242
import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
43+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
4344
import {WidgetViewerContext} from 'sentry/views/dashboards/widgetViewer/widgetViewerContext';
4445

4546
import {useDashboardsMEPContext} from './dashboardsMEPContext';
@@ -89,6 +90,7 @@ type Props = WithRouterProps & {
8990
onSetTransactionsDataset?: () => void;
9091
onUpdate?: (widget: Widget | null) => void;
9192
onWidgetSplitDecision?: (splitDecision: WidgetType) => void;
93+
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
9294
onWidgetTableSort?: (sort: Sort) => void;
9395
renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
9496
shouldResize?: boolean;
@@ -158,6 +160,7 @@ function WidgetCard(props: Props) {
158160
showLoadingText,
159161
router,
160162
onWidgetTableSort,
163+
onWidgetTableResizeColumn,
161164
} = props;
162165

163166
if (widget.displayType === DisplayType.TOP_N) {
@@ -325,6 +328,7 @@ function WidgetCard(props: Props) {
325328
onDataFetchStart={onDataFetchStart}
326329
showLoadingText={showLoadingText && isLoadingTextVisible}
327330
onWidgetTableSort={onWidgetTableSort}
331+
onWidgetTableResizeColumn={onWidgetTableResizeColumn}
328332
/>
329333
</WidgetFrame>
330334
</VisuallyCompleteWithData>

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
1616
import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
1717
import {type Widget, WidgetType} from 'sentry/views/dashboards/types';
1818
import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
19+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
1920
import {TableWidgetVisualization} from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';
2021
import {
2122
convertTableDataToTabularData,
@@ -31,6 +32,7 @@ type Props = {
3132
theme: Theme;
3233
widget: Widget;
3334
errorMessage?: string;
35+
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
3436
tableResults?: TableData[];
3537
};
3638

@@ -43,6 +45,7 @@ export function IssueWidgetCard({
4345
organization,
4446
location,
4547
theme,
48+
onWidgetTableResizeColumn,
4649
}: Props) {
4750
const datasetConfig = getDatasetConfig(WidgetType.ISSUE);
4851

@@ -65,12 +68,14 @@ export function IssueWidgetCard({
6568
: [...query.columns, ...query.aggregates];
6669
const fieldAliases = query.fieldAliases ?? [];
6770
const fieldHeaderMap = datasetConfig.getFieldHeaderMap?.();
68-
const columns = decodeColumnOrder(queryFields.map(field => ({field}))).map(column => ({
69-
key: column.key,
70-
name: column.name,
71-
width: column.width,
72-
type: column.type === 'never' ? null : column.type,
73-
}));
71+
const columns = decodeColumnOrder(queryFields.map(field => ({field}))).map(
72+
(column, index) => ({
73+
key: column.key,
74+
name: column.name,
75+
width: widget.tableWidths?.[index] ?? column.width,
76+
type: column.type === 'never' ? null : column.type,
77+
})
78+
);
7479
const aliases = decodeColumnAliases(columns, fieldAliases, fieldHeaderMap);
7580
const tableData = convertTableDataToTabularData(tableResults?.[0]);
7681
const eventView = eventViewFromWidget(widget.title, widget.queries[0]!, selection);
@@ -108,6 +113,7 @@ export function IssueWidgetCard({
108113
unit,
109114
} satisfies RenderFunctionBaggage;
110115
}}
116+
onResizeColumn={onWidgetTableResizeColumn}
111117
/>
112118
</TableContainer>
113119
) : (

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ class ReleaseWidgetQueries extends Component<ReleaseWidgetQueriesProps, State> {
246246
'layout',
247247
'tempId',
248248
'widgetType',
249+
'tableWidths',
249250
];
250251
const ignoredQueryProps = ['name', 'fields', 'aggregates', 'columns'];
251252
return (

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
2323
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
2424
import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
2525
import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
26+
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
2627

2728
import WidgetCardChart from './chart';
2829
import {IssueWidgetCard} from './issueWidgetCard';
@@ -57,6 +58,7 @@ type Props = {
5758
type: 'legendselectchanged';
5859
}>;
5960
onWidgetSplitDecision?: (splitDecision: WidgetType) => void;
61+
onWidgetTableResizeColumn?: (columns: TabularColumn[]) => void;
6062
onWidgetTableSort?: (sort: Sort) => void;
6163
onZoom?: EChartDataZoomHandler;
6264
renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
@@ -92,6 +94,7 @@ export function WidgetCardChartContainer({
9294
disableZoom,
9395
showLoadingText,
9496
onWidgetTableSort,
97+
onWidgetTableResizeColumn,
9598
}: Props) {
9699
const location = useLocation();
97100
const theme = useTheme();
@@ -173,6 +176,7 @@ export function WidgetCardChartContainer({
173176
selection={selection}
174177
theme={theme}
175178
organization={organization}
179+
onWidgetTableResizeColumn={onWidgetTableResizeColumn}
176180
/>
177181
</Fragment>
178182
);
@@ -218,6 +222,7 @@ export function WidgetCardChartContainer({
218222
showLoadingText={showLoadingText}
219223
theme={theme}
220224
onWidgetTableSort={onWidgetTableSort}
225+
onWidgetTableResizeColumn={onWidgetTableResizeColumn}
221226
/>
222227
</Fragment>
223228
);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ interface TableWidgetVisualizationProps {
8484
onChangeSort?: (sort: Sort) => void;
8585

8686
/**
87-
* A callback function that is invoked after a user resizes a column. If omitted, resizing will update the width parameters in the URL
87+
* A callback function that is invoked after a user resizes a column. If omitted, resizing will update the width parameters in the URL. This function always guarantees width field is supplied, meaning it will fallback to -1
8888
* @param columns an array of columns with the updated widths
8989
*/
9090
onResizeColumn?: (columns: TabularColumn[]) => void;

0 commit comments

Comments
 (0)