Skip to content

Commit 661a162

Browse files
Abdkhan14Abdullah Khan
authored andcommitted
feat(explore) Making samples and aggregate tables resizable (#81899)
https://github.com/user-attachments/assets/964924fb-0f14-46b9-9e8c-b669a27f1f44 --------- Co-authored-by: Abdullah Khan <[email protected]>
1 parent 94862ef commit 661a162

File tree

4 files changed

+154
-103
lines changed

4 files changed

+154
-103
lines changed

static/app/components/profiling/suspectFunctions/suspectFunctionsTable.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Fragment, useCallback, useMemo, useState} from 'react';
1+
import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
22
import styled from '@emotion/styled';
33
import clamp from 'lodash/clamp';
44

@@ -110,9 +110,9 @@ export function SuspectFunctionsTable({
110110
return sortedMetrics.slice(pagination.start, pagination.end);
111111
}, [sortedMetrics, pagination]);
112112

113-
const {tableStyles} = useTableStyles({
114-
items: COLUMNS,
115-
});
113+
const fields = COLUMNS.map(column => column.value);
114+
const tableRef = useRef<HTMLTableElement>(null);
115+
const {initialTableStyles} = useTableStyles(fields, tableRef);
116116

117117
const baggage: RenderFunctionBaggage = {
118118
location,
@@ -139,7 +139,7 @@ export function SuspectFunctionsTable({
139139
/>
140140
</ButtonBar>
141141
</TableHeader>
142-
<Table style={tableStyles}>
142+
<Table ref={tableRef} styles={initialTableStyles}>
143143
<TableHead>
144144
<TableRow>
145145
{COLUMNS.map((column, i) => {
+87-33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useMemo} from 'react';
1+
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
22
import styled from '@emotion/styled';
33

44
import {COL_WIDTH_MINIMUM} from 'sentry/components/gridEditable';
@@ -16,17 +16,20 @@ import {
1616
HeaderButtonContainer,
1717
HeaderTitle,
1818
} from 'sentry/components/gridEditable/styles';
19+
import {space} from 'sentry/styles/space';
1920
import {Actions} from 'sentry/views/discover/table/cellAction';
2021

2122
interface TableProps extends React.ComponentProps<typeof _TableWrapper> {}
2223

23-
export function Table({children, style, ...props}: TableProps) {
24-
return (
24+
export const Table = React.forwardRef<HTMLTableElement, TableProps>(
25+
({children, styles, ...props}, ref) => (
2526
<_TableWrapper {...props}>
26-
<_Table style={style}>{children}</_Table>
27+
<_Table ref={ref} style={styles}>
28+
{children}
29+
</_Table>
2730
</_TableWrapper>
28-
);
29-
}
31+
)
32+
);
3033

3134
interface TableStatusProps {
3235
children: React.ReactNode;
@@ -49,38 +52,83 @@ export const ALLOWED_CELL_ACTIONS: Actions[] = [
4952

5053
const MINIMUM_COLUMN_WIDTH = COL_WIDTH_MINIMUM;
5154

52-
type Item = {
53-
label: React.ReactNode;
54-
value: string;
55-
width?: number | 'min-content';
56-
};
55+
export function useTableStyles(
56+
fields: string[],
57+
tableRef: React.RefObject<HTMLDivElement>,
58+
minimumColumnWidth = MINIMUM_COLUMN_WIDTH
59+
) {
60+
const resizingColumnIndex = useRef<number | null>(null);
61+
const columnWidthsRef = useRef<(number | null)[]>(fields.map(() => null));
5762

58-
interface UseTableStylesOptions {
59-
items: Item[];
60-
minimumColumnWidth?: number;
61-
}
63+
useEffect(() => {
64+
columnWidthsRef.current = fields.map(
65+
(_, index) => columnWidthsRef.current[index] ?? null
66+
);
67+
}, [fields]);
68+
69+
const initialTableStyles = useMemo(
70+
() => ({
71+
gridTemplateColumns: fields
72+
.map(() => `minmax(${minimumColumnWidth}px, auto)`)
73+
.join(' '),
74+
}),
75+
[fields, minimumColumnWidth]
76+
);
77+
78+
const onResizeMouseDown = useCallback(
79+
(event: React.MouseEvent<HTMLDivElement>, index: number) => {
80+
event.preventDefault();
81+
82+
// <GridResizer> is expected to be nested 1 level down from <GridHeadCell>
83+
const cell = event.currentTarget!.parentElement;
84+
if (!cell) {
85+
return;
86+
}
87+
88+
resizingColumnIndex.current = index;
6289

63-
export function useTableStyles({
64-
items,
65-
minimumColumnWidth = MINIMUM_COLUMN_WIDTH,
66-
}: UseTableStylesOptions) {
67-
const tableStyles = useMemo(() => {
68-
const columns = new Array(items.length);
69-
70-
for (let i = 0; i < items.length; i++) {
71-
if (typeof items[i].width === 'number') {
72-
columns[i] = `${items[i].width}px`;
73-
} else {
74-
columns[i] = items[i].width ?? `minmax(${minimumColumnWidth}px, auto)`;
90+
const startX = event.clientX;
91+
const initialWidth = cell.offsetWidth;
92+
93+
const gridElement = tableRef.current;
94+
95+
function onMouseMove(e: MouseEvent) {
96+
if (resizingColumnIndex.current === null || !gridElement) {
97+
return;
98+
}
99+
100+
const newWidth = Math.max(
101+
MINIMUM_COLUMN_WIDTH,
102+
initialWidth + (e.clientX - startX)
103+
);
104+
105+
columnWidthsRef.current[index] = newWidth;
106+
107+
// Updating the grid's `gridTemplateColumns` directly
108+
gridElement.style.gridTemplateColumns = columnWidthsRef.current
109+
.map(width => {
110+
return typeof width === 'number'
111+
? `${width}px`
112+
: `minmax(${minimumColumnWidth}px, auto)`;
113+
})
114+
.join(' ');
75115
}
76-
}
77116

78-
return {
79-
gridTemplateColumns: columns.join(' '),
80-
};
81-
}, [items, minimumColumnWidth]);
117+
function onMouseUp() {
118+
resizingColumnIndex.current = null;
82119

83-
return {tableStyles};
120+
// Cleaning up event listeners
121+
window.removeEventListener('mousemove', onMouseMove);
122+
window.removeEventListener('mouseup', onMouseUp);
123+
}
124+
125+
window.addEventListener('mousemove', onMouseMove);
126+
window.addEventListener('mouseup', onMouseUp);
127+
},
128+
[tableRef, minimumColumnWidth]
129+
);
130+
131+
return {initialTableStyles, onResizeMouseDown};
84132
}
85133

86134
export const TableBody = GridBody;
@@ -94,3 +142,9 @@ export const TableHeaderTitle = HeaderTitle;
94142
export const TableHeadCell = styled(GridHeadCell)<{align?: Alignments}>`
95143
${p => p.align && `justify-content: ${p.align};`}
96144
`;
145+
export const TableHeadCellContent = styled('div')`
146+
display: flex;
147+
align-items: center;
148+
gap: ${space(0.5)};
149+
cursor: pointer;
150+
`;

static/app/views/explore/tables/aggregatesTable.tsx

+30-31
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type {Dispatch, SetStateAction} from 'react';
2-
import {Fragment, useEffect, useMemo} from 'react';
2+
import {Fragment, useEffect, useMemo, useRef} from 'react';
33
import styled from '@emotion/styled';
44

55
import EmptyStateWarning from 'sentry/components/emptyStateWarning';
6+
import {GridResizer} from 'sentry/components/gridEditable/styles';
67
import LoadingIndicator from 'sentry/components/loadingIndicator';
78
import Pagination from 'sentry/components/pagination';
89
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
@@ -26,6 +27,7 @@ import {
2627
TableBodyCell,
2728
TableHead,
2829
TableHeadCell,
30+
TableHeadCellContent,
2931
TableRow,
3032
TableStatus,
3133
useTableStyles,
@@ -132,14 +134,8 @@ export function AggregatesTable({confidence, setError}: AggregatesTableProps) {
132134
confidence,
133135
});
134136

135-
const {tableStyles} = useTableStyles({
136-
items: fields.map(field => {
137-
return {
138-
label: field,
139-
value: field,
140-
};
141-
}),
142-
});
137+
const tableRef = useRef<HTMLTableElement>(null);
138+
const {initialTableStyles, onResizeMouseDown} = useTableStyles(fields, tableRef);
143139

144140
const meta = result.meta ?? {};
145141

@@ -148,7 +144,7 @@ export function AggregatesTable({confidence, setError}: AggregatesTableProps) {
148144

149145
return (
150146
<Fragment>
151-
<Table style={tableStyles}>
147+
<Table ref={tableRef} styles={initialTableStyles}>
152148
<TableHead>
153149
<TableRow>
154150
{fields.map((field, i) => {
@@ -179,26 +175,33 @@ export function AggregatesTable({confidence, setError}: AggregatesTableProps) {
179175
}
180176

181177
return (
182-
<StyledTableHeadCell
183-
align={align}
184-
key={i}
185-
isFirst={i === 0}
186-
onClick={updateSort}
187-
>
188-
<span>{label}</span>
189-
{defined(direction) && (
190-
<IconArrow
191-
size="xs"
192-
direction={
193-
direction === 'desc'
194-
? 'down'
195-
: direction === 'asc'
196-
? 'up'
197-
: undefined
178+
<TableHeadCell align={align} key={i} isFirst={i === 0}>
179+
<TableHeadCellContent onClick={updateSort}>
180+
<span>{label}</span>
181+
{defined(direction) && (
182+
<IconArrow
183+
size="xs"
184+
direction={
185+
direction === 'desc'
186+
? 'down'
187+
: direction === 'asc'
188+
? 'up'
189+
: undefined
190+
}
191+
/>
192+
)}
193+
</TableHeadCellContent>
194+
{i !== fields.length - 1 && (
195+
<GridResizer
196+
dataRows={
197+
!result.isError && !result.isPending && result.data
198+
? result.data.length
199+
: 0
198200
}
201+
onMouseDown={e => onResizeMouseDown(e, i)}
199202
/>
200203
)}
201-
</StyledTableHeadCell>
204+
</TableHeadCell>
202205
);
203206
})}
204207
</TableRow>
@@ -258,7 +261,3 @@ const TopResultsIndicator = styled('div')<{index: number}>`
258261
return CHART_PALETTE[TOP_EVENTS_LIMIT - 1][p.index];
259262
}};
260263
`;
261-
262-
const StyledTableHeadCell = styled(TableHeadCell)`
263-
cursor: pointer;
264-
`;

0 commit comments

Comments
 (0)