Skip to content

Commit

Permalink
feat(explore) Making samples and aggregate tables resizable (#81899)
Browse files Browse the repository at this point in the history
  • Loading branch information
Abdkhan14 and Abdullah Khan authored Dec 13, 2024
1 parent 8b0b0ff commit d167211
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Fragment, useCallback, useMemo, useState} from 'react';
import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
import styled from '@emotion/styled';
import clamp from 'lodash/clamp';

Expand Down Expand Up @@ -110,9 +110,9 @@ export function SuspectFunctionsTable({
return sortedMetrics.slice(pagination.start, pagination.end);
}, [sortedMetrics, pagination]);

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

const baggage: RenderFunctionBaggage = {
location,
Expand All @@ -139,7 +139,7 @@ export function SuspectFunctionsTable({
/>
</ButtonBar>
</TableHeader>
<Table style={tableStyles}>
<Table ref={tableRef} styles={initialTableStyles}>
<TableHead>
<TableRow>
{COLUMNS.map((column, i) => {
Expand Down
120 changes: 87 additions & 33 deletions static/app/views/explore/components/table.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useMemo} from 'react';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import styled from '@emotion/styled';

import {COL_WIDTH_MINIMUM} from 'sentry/components/gridEditable';
Expand All @@ -16,17 +16,20 @@ import {
HeaderButtonContainer,
HeaderTitle,
} from 'sentry/components/gridEditable/styles';
import {space} from 'sentry/styles/space';
import {Actions} from 'sentry/views/discover/table/cellAction';

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

export function Table({children, style, ...props}: TableProps) {
return (
export const Table = React.forwardRef<HTMLTableElement, TableProps>(
({children, styles, ...props}, ref) => (
<_TableWrapper {...props}>
<_Table style={style}>{children}</_Table>
<_Table ref={ref} style={styles}>
{children}
</_Table>
</_TableWrapper>
);
}
)
);

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

const MINIMUM_COLUMN_WIDTH = COL_WIDTH_MINIMUM;

type Item = {
label: React.ReactNode;
value: string;
width?: number | 'min-content';
};
export function useTableStyles(
fields: string[],
tableRef: React.RefObject<HTMLDivElement>,
minimumColumnWidth = MINIMUM_COLUMN_WIDTH
) {
const resizingColumnIndex = useRef<number | null>(null);
const columnWidthsRef = useRef<(number | null)[]>(fields.map(() => null));

interface UseTableStylesOptions {
items: Item[];
minimumColumnWidth?: number;
}
useEffect(() => {
columnWidthsRef.current = fields.map(
(_, index) => columnWidthsRef.current[index] ?? null
);
}, [fields]);

const initialTableStyles = useMemo(
() => ({
gridTemplateColumns: fields
.map(() => `minmax(${minimumColumnWidth}px, auto)`)
.join(' '),
}),
[fields, minimumColumnWidth]
);

const onResizeMouseDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>, index: number) => {
event.preventDefault();

// <GridResizer> is expected to be nested 1 level down from <GridHeadCell>
const cell = event.currentTarget!.parentElement;
if (!cell) {
return;
}

resizingColumnIndex.current = index;

export function useTableStyles({
items,
minimumColumnWidth = MINIMUM_COLUMN_WIDTH,
}: UseTableStylesOptions) {
const tableStyles = useMemo(() => {
const columns = new Array(items.length);

for (let i = 0; i < items.length; i++) {
if (typeof items[i].width === 'number') {
columns[i] = `${items[i].width}px`;
} else {
columns[i] = items[i].width ?? `minmax(${minimumColumnWidth}px, auto)`;
const startX = event.clientX;
const initialWidth = cell.offsetWidth;

const gridElement = tableRef.current;

function onMouseMove(e: MouseEvent) {
if (resizingColumnIndex.current === null || !gridElement) {
return;
}

const newWidth = Math.max(
MINIMUM_COLUMN_WIDTH,
initialWidth + (e.clientX - startX)
);

columnWidthsRef.current[index] = newWidth;

// Updating the grid's `gridTemplateColumns` directly
gridElement.style.gridTemplateColumns = columnWidthsRef.current
.map(width => {
return typeof width === 'number'
? `${width}px`
: `minmax(${minimumColumnWidth}px, auto)`;
})
.join(' ');
}
}

return {
gridTemplateColumns: columns.join(' '),
};
}, [items, minimumColumnWidth]);
function onMouseUp() {
resizingColumnIndex.current = null;

return {tableStyles};
// Cleaning up event listeners
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}

window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
},
[tableRef, minimumColumnWidth]
);

return {initialTableStyles, onResizeMouseDown};
}

export const TableBody = GridBody;
Expand All @@ -94,3 +142,9 @@ export const TableHeaderTitle = HeaderTitle;
export const TableHeadCell = styled(GridHeadCell)<{align?: Alignments}>`
${p => p.align && `justify-content: ${p.align};`}
`;
export const TableHeadCellContent = styled('div')`
display: flex;
align-items: center;
gap: ${space(0.5)};
cursor: pointer;
`;
61 changes: 30 additions & 31 deletions static/app/views/explore/tables/aggregatesTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type {Dispatch, SetStateAction} from 'react';
import {Fragment, useEffect, useMemo} from 'react';
import {Fragment, useEffect, useMemo, useRef} from 'react';
import styled from '@emotion/styled';

import EmptyStateWarning from 'sentry/components/emptyStateWarning';
import {GridResizer} from 'sentry/components/gridEditable/styles';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import Pagination from 'sentry/components/pagination';
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
Expand All @@ -26,6 +27,7 @@ import {
TableBodyCell,
TableHead,
TableHeadCell,
TableHeadCellContent,
TableRow,
TableStatus,
useTableStyles,
Expand Down Expand Up @@ -132,14 +134,8 @@ export function AggregatesTable({confidence, setError}: AggregatesTableProps) {
confidence,
});

const {tableStyles} = useTableStyles({
items: fields.map(field => {
return {
label: field,
value: field,
};
}),
});
const tableRef = useRef<HTMLTableElement>(null);
const {initialTableStyles, onResizeMouseDown} = useTableStyles(fields, tableRef);

const meta = result.meta ?? {};

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

return (
<Fragment>
<Table style={tableStyles}>
<Table ref={tableRef} styles={initialTableStyles}>
<TableHead>
<TableRow>
{fields.map((field, i) => {
Expand Down Expand Up @@ -179,26 +175,33 @@ export function AggregatesTable({confidence, setError}: AggregatesTableProps) {
}

return (
<StyledTableHeadCell
align={align}
key={i}
isFirst={i === 0}
onClick={updateSort}
>
<span>{label}</span>
{defined(direction) && (
<IconArrow
size="xs"
direction={
direction === 'desc'
? 'down'
: direction === 'asc'
? 'up'
: undefined
<TableHeadCell align={align} key={i} isFirst={i === 0}>
<TableHeadCellContent onClick={updateSort}>
<span>{label}</span>
{defined(direction) && (
<IconArrow
size="xs"
direction={
direction === 'desc'
? 'down'
: direction === 'asc'
? 'up'
: undefined
}
/>
)}
</TableHeadCellContent>
{i !== fields.length - 1 && (
<GridResizer
dataRows={
!result.isError && !result.isPending && result.data
? result.data.length
: 0
}
onMouseDown={e => onResizeMouseDown(e, i)}
/>
)}
</StyledTableHeadCell>
</TableHeadCell>
);
})}
</TableRow>
Expand Down Expand Up @@ -258,7 +261,3 @@ const TopResultsIndicator = styled('div')<{index: number}>`
return CHART_PALETTE[TOP_EVENTS_LIMIT - 1][p.index];
}};
`;

const StyledTableHeadCell = styled(TableHeadCell)`
cursor: pointer;
`;
Loading

0 comments on commit d167211

Please sign in to comment.