Compare commits

...

2 Commits

Author SHA1 Message Date
Ashwin Bhatkal
c26aa6ed8e chore: fix tests 2026-03-06 10:53:44 +05:30
Ashwin Bhatkal
e4f99ffee7 chore(frontend): separate out columnWidths from ResizeTable 2026-03-06 10:53:44 +05:30
15 changed files with 347 additions and 52 deletions

View File

@@ -14,8 +14,6 @@ import cx from 'classnames';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { getColumnWidth, RowData } from 'lib/query/createTableColumnsFromQuery';
import { debounce, set } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { Widgets } from 'types/api/dashboard/getAll';
import ResizableHeader from './ResizableHeader';
import { DragSpanStyle } from './styles';
@@ -26,28 +24,18 @@ function ResizeTable({
columns,
onDragColumn,
pagination,
widgetId,
shouldPersistColumnWidths = false,
columnWidths,
onColumnWidthsChange,
...restProps
}: ResizeTableProps): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>([]);
const { setColumnWidths, selectedDashboard } = useDashboard();
const columnWidths = shouldPersistColumnWidths
? (selectedDashboard?.data?.widgets?.find(
(widget) => widget.id === widgetId,
) as Widgets)?.columnWidths
: undefined;
const updateAllColumnWidths = useRef(
debounce((widthsConfig: Record<string, number>) => {
if (!widgetId || !shouldPersistColumnWidths) {
if (!onColumnWidthsChange) {
return;
}
setColumnWidths?.((prev) => ({
...prev,
[widgetId]: widthsConfig,
}));
onColumnWidthsChange(widthsConfig);
}, 1000),
).current;
@@ -75,7 +63,7 @@ function ResizeTable({
...col,
...(onDragColumn && {
title: (
<DragSpanStyle className="dragHandler">
<DragSpanStyle className="dragHandler" data-testid="drag-column-title">
{col?.title?.toString() || ''}
</DragSpanStyle>
),
@@ -106,31 +94,31 @@ function ResizeTable({
}, [mergedColumns, pagination, restProps]);
useEffect(() => {
if (columns) {
// Apply stored column widths from widget configuration
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths) {
const width = getColumnWidth(dataIndex, columnWidths);
if (width) {
return {
...col,
width, // Apply stored width
};
}
}
return col;
});
setColumns(columnsWithStoredWidths);
}
}, [columns, columnWidths]);
useEffect(() => {
if (!shouldPersistColumnWidths) {
if (!columns) {
return;
}
// Collect all column widths in a single object
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths) {
const width = getColumnWidth(dataIndex, columnWidths);
if (width) {
return { ...col, width };
}
}
return col;
});
setColumns(columnsWithStoredWidths);
}, [columns, columnWidths]);
const lastReportedWidthsRef = useRef<Record<string, number>>({});
useEffect(() => {
if (!onColumnWidthsChange) {
return;
}
const newColumnWidths: Record<string, number> = {};
mergedColumns.forEach((col) => {
@@ -140,11 +128,20 @@ function ResizeTable({
}
});
// Only update if there are actual widths to set
if (Object.keys(newColumnWidths).length > 0) {
if (Object.keys(newColumnWidths).length === 0) {
return;
}
const last = lastReportedWidthsRef.current;
const hasChange =
Object.keys(newColumnWidths).length !== Object.keys(last).length ||
Object.keys(newColumnWidths).some((k) => newColumnWidths[k] !== last[k]);
if (hasChange) {
lastReportedWidthsRef.current = newColumnWidths;
updateAllColumnWidths(newColumnWidths);
}
}, [mergedColumns, updateAllColumnWidths, shouldPersistColumnWidths]);
}, [mergedColumns, updateAllColumnWidths, onColumnWidthsChange]);
return onDragColumn ? (
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>

View File

@@ -0,0 +1,244 @@
import { act } from '@testing-library/react';
import { render, screen, userEvent } from 'tests/test-utils';
import ResizeTable from '../ResizeTable';
jest.mock('react-resizable', () => ({
Resizable: ({
children,
onResize,
width,
}: {
children: React.ReactNode;
onResize: (
e: React.SyntheticEvent,
data: { size: { width: number } },
) => void;
width: number;
}): JSX.Element => (
<div>
{children}
<button
data-testid="resize-trigger"
type="button"
onClick={(e): void => onResize(e, { size: { width: width + 50 } })}
/>
</div>
),
}));
// Make debounce synchronous so onColumnWidthsChange fires immediately
jest.mock('lodash-es', () => ({
...jest.requireActual('lodash-es'),
debounce: (fn: (...args: any[]) => any): ((...args: any[]) => any) => fn,
}));
const baseColumns = [
{ dataIndex: 'name', title: 'Name', width: 100 },
{ dataIndex: 'value', title: 'Value', width: 100 },
];
const baseDataSource = [
{ key: '1', name: 'Alice', value: 42 },
{ key: '2', name: 'Bob', value: 99 },
];
describe('ResizeTable', () => {
it('renders column headers and data rows', () => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
/>,
);
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Value')).toBeInTheDocument();
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
it('overrides column widths from columnWidths prop and reports them via onColumnWidthsChange', () => {
const onColumnWidthsChange = jest.fn();
act(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
columnWidths={{ name: 250, value: 180 }}
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
});
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 250, value: 180 }),
);
});
it('reports original column widths via onColumnWidthsChange when columnWidths prop is not provided', () => {
const onColumnWidthsChange = jest.fn();
act(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
});
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 100, value: 100 }),
);
});
it('does not call onColumnWidthsChange when it is not provided', () => {
// Should render without errors and without attempting to call an undefined callback
expect(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
/>,
);
}).not.toThrow();
});
it('only overrides the column that has a stored width, leaving others at their original width', () => {
const onColumnWidthsChange = jest.fn();
act(() => {
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
columnWidths={{ name: 250 }}
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
});
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 250, value: 100 }),
);
});
it('does not call onColumnWidthsChange on re-render when widths have not changed', () => {
const onColumnWidthsChange = jest.fn();
const { rerender } = render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
expect(onColumnWidthsChange).toHaveBeenCalledTimes(1);
onColumnWidthsChange.mockClear();
rerender(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
expect(onColumnWidthsChange).not.toHaveBeenCalled();
});
it('does not call onColumnWidthsChange when no column has a defined width', () => {
const onColumnWidthsChange = jest.fn();
render(
<ResizeTable
columns={[
{ dataIndex: 'name', title: 'Name' },
{ dataIndex: 'value', title: 'Value' },
]}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
expect(onColumnWidthsChange).not.toHaveBeenCalled();
});
it('calls onColumnWidthsChange with the new width after a column is resized', async () => {
const user = userEvent.setup();
const onColumnWidthsChange = jest.fn();
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
onColumnWidthsChange.mockClear();
// Click the first column's resize trigger — mock adds 50px to the current width (100 → 150)
const [firstResizeTrigger] = screen.getAllByTestId('resize-trigger');
await user.click(firstResizeTrigger);
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 150, value: 100 }),
);
});
it('does not affect other columns when only one column is resized', async () => {
const user = userEvent.setup();
const onColumnWidthsChange = jest.fn();
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onColumnWidthsChange={onColumnWidthsChange}
/>,
);
onColumnWidthsChange.mockClear();
// Resize only the second column (value: 100 → 150), name should stay at 100
const resizeTriggers = screen.getAllByTestId('resize-trigger');
await user.click(resizeTriggers[1]);
expect(onColumnWidthsChange).toHaveBeenCalledWith(
expect.objectContaining({ name: 100, value: 150 }),
);
});
it('wraps column titles in drag handler spans when onDragColumn is provided', () => {
const onDragColumn = jest.fn();
render(
<ResizeTable
columns={baseColumns}
dataSource={baseDataSource}
rowKey="key"
onDragColumn={onDragColumn}
/>,
);
const dragTitles = screen.getAllByTestId('drag-column-title');
expect(dragTitles).toHaveLength(baseColumns.length);
expect(dragTitles[0]).toHaveTextContent('Name');
expect(dragTitles[1]).toHaveTextContent('Value');
});
});

View File

@@ -6,10 +6,25 @@ import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatS
import { TableDataSource } from './contants';
type ColumnWidths = Record<string, number>;
export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void;
widgetId?: string;
shouldPersistColumnWidths?: boolean;
/**
* Pre-resolved column widths for this table, keyed by column dataIndex.
* Use this to apply persisted widths on mount (e.g. from widget.columnWidths).
* Do NOT pass a value that updates reactively on every resize — that creates a
* feedback loop. Pass only stable / persisted values.
*/
columnWidths?: ColumnWidths;
/**
* Called (debounced) whenever the user finishes resizing a column.
* The widths object contains all current column widths keyed by dataIndex.
* Intended for persisting widths to an external store (e.g. dashboard context
* staging buffer). The caller owns the storage; ResizeTable does not read back
* whatever is written here.
*/
onColumnWidthsChange?: (widths: ColumnWidths) => void;
}
export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];

View File

@@ -168,6 +168,9 @@ jest.mock('providers/Dashboard/Dashboard', () => ({
variables: [],
},
},
setLayouts: jest.fn(),
setSelectedDashboard: jest.fn(),
setColumnWidths: jest.fn(),
}),
}));

View File

@@ -101,7 +101,19 @@ function WidgetGraphComponent({
const navigateToExplorerPages = useNavigateToExplorerPages();
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
const {
setLayouts,
selectedDashboard,
setSelectedDashboard,
setColumnWidths,
} = useDashboard();
const onColumnWidthsChange = useCallback(
(widths: Record<string, number>) => {
setColumnWidths((prev) => ({ ...prev, [widget.id]: widths }));
},
[setColumnWidths, widget.id],
);
const onToggleModal = useCallback(
(func: Dispatch<SetStateAction<boolean>>) => {
@@ -424,6 +436,7 @@ function WidgetGraphComponent({
customSeries={customSeries}
customOnRowClick={customOnRowClick}
enableDrillDown={enableDrillDown}
onColumnWidthsChange={onColumnWidthsChange}
/>
</div>
)}

View File

@@ -45,6 +45,8 @@ function GridTableComponent({
onOpenTraceBtnClick,
customOnRowClick,
widgetId,
columnWidths,
onColumnWidthsChange,
panelType,
queryRangeRequest,
decimalPrecision,
@@ -284,6 +286,8 @@ function GridTableComponent({
dataSource={dataSource}
sticky={sticky}
widgetId={widgetId}
columnWidths={columnWidths}
onColumnWidthsChange={onColumnWidthsChange}
panelType={panelType}
queryRangeRequest={queryRangeRequest}
onRow={

View File

@@ -24,6 +24,8 @@ export type GridTableComponentProps = {
onOpenTraceBtnClick?: (record: RowData) => void;
customOnRowClick?: (record: RowData) => void;
widgetId?: string;
columnWidths?: Record<string, number>;
onColumnWidthsChange?: (widths: Record<string, number>) => void;
renderColumnCell?: QueryTableProps['renderColumnCell'];
customColTitles?: Record<string, string>;
enableDrillDown?: boolean;

View File

@@ -33,6 +33,7 @@ function LogsPanelComponent({
widget,
setRequestData,
queryResponse,
onColumnWidthsChange,
}: LogsPanelComponentProps): JSX.Element {
const [pageSize, setPageSize] = useState<number>(10);
const [offset, setOffset] = useState<number>(0);
@@ -145,8 +146,8 @@ function LogsPanelComponent({
columns={columns}
onRow={handleRow}
rowKey={(record): string => record.id}
widgetId={widget.id}
shouldPersistColumnWidths
columnWidths={widget.columnWidths}
onColumnWidthsChange={onColumnWidthsChange}
/>
</OverlayScrollbar>
</div>
@@ -189,6 +190,7 @@ export type LogsPanelComponentProps = {
Error
>;
widget: Widgets;
onColumnWidthsChange?: (widths: Record<string, number>) => void;
};
export default LogsPanelComponent;

View File

@@ -8,6 +8,7 @@ function ListPanelWrapper({
widget,
queryResponse,
setRequestData,
onColumnWidthsChange,
}: PanelWrapperProps): JSX.Element {
const dataSource = widget.query.builder?.queryData[0]?.dataSource;
@@ -21,6 +22,7 @@ function ListPanelWrapper({
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}
onColumnWidthsChange={onColumnWidthsChange}
/>
);
}
@@ -29,6 +31,7 @@ function ListPanelWrapper({
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}
onColumnWidthsChange={onColumnWidthsChange}
/>
);
}

View File

@@ -24,6 +24,7 @@ function PanelWrapper({
customOnRowClick,
panelMode,
enableDrillDown = false,
onColumnWidthsChange,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@@ -58,6 +59,7 @@ function PanelWrapper({
customOnRowClick={customOnRowClick}
customSeries={customSeries}
enableDrillDown={enableDrillDown}
onColumnWidthsChange={onColumnWidthsChange}
/>
);
}

View File

@@ -14,6 +14,7 @@ function TablePanelWrapper({
onOpenTraceBtnClick,
customOnRowClick,
enableDrillDown = false,
onColumnWidthsChange,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
@@ -34,6 +35,8 @@ function TablePanelWrapper({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customOnRowClick={customOnRowClick}
widgetId={widget.id}
columnWidths={widget.columnWidths}
onColumnWidthsChange={onColumnWidthsChange}
renderColumnCell={widget.renderColumnCell}
customColTitles={widget.customColTitles}
contextLinks={widget.contextLinks}

View File

@@ -29,6 +29,7 @@ export type PanelWrapperProps = {
customSeries?: (data: QueryData[]) => uPlot.Series[];
enableDrillDown?: boolean;
panelMode: PanelMode;
onColumnWidthsChange?: (widths: Record<string, number>) => void;
};
export type TooltipData = {

View File

@@ -24,6 +24,8 @@ export type QueryTableProps = Omit<
sticky?: TableProps<RowData>['sticky'];
searchTerm?: string;
widgetId?: string;
columnWidths?: Record<string, number>;
onColumnWidthsChange?: (widths: Record<string, number>) => void;
enableDrillDown?: boolean;
contextLinks?: ContextLinksData;
panelType?: PANEL_TYPES;

View File

@@ -28,6 +28,8 @@ export function QueryTable({
sticky,
searchTerm,
widgetId,
columnWidths,
onColumnWidthsChange,
panelType,
...props
}: QueryTableProps): JSX.Element {
@@ -175,8 +177,8 @@ export function QueryTable({
dataSource={filterTable === null ? newDataSource : filterTable}
scroll={{ x: 'max-content' }}
pagination={paginationConfig}
widgetId={widgetId}
shouldPersistColumnWidths
columnWidths={columnWidths}
onColumnWidthsChange={onColumnWidthsChange}
sticky={sticky}
{...props}
/>

View File

@@ -35,6 +35,7 @@ function TracesTableComponent({
widget,
queryResponse,
setRequestData,
onColumnWidthsChange,
}: TracesTableComponentProps): JSX.Element {
const [pagination, setPagination] = useState<Pagination>({
offset: 0,
@@ -131,8 +132,8 @@ function TracesTableComponent({
columns={columns}
onRow={handleRow}
sticky
widgetId={widget.id}
shouldPersistColumnWidths
columnWidths={widget.columnWidths}
onColumnWidthsChange={onColumnWidthsChange}
/>
</OverlayScrollbar>
</div>
@@ -175,6 +176,7 @@ export type TracesTableComponentProps = {
>;
widget: Widgets;
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
onColumnWidthsChange?: (widths: Record<string, number>) => void;
};
export default TracesTableComponent;