feat: allow width customisation and persist it across users and view (#7273)

* feat: removed ellipsis prop

* feat: prevent unnecessary save calls

* feat: fix dashboard detail resize icon

* feat: adjusted resizable header - set minConstraint

* feat: fixed dashboard vanishing issue

* feat: removed dependency causing maximum callstack warning

* feat: corrected the list edit view render issue and resize handler fix

* feat: style fix

* feat: removed comments

* fix: updated test cases

* feat: updated the test cases

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
SagarRajput-7 2025-03-28 11:36:40 +05:30 committed by GitHub
parent a876c0a744
commit d8d8191a32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 270 additions and 37 deletions

View File

@ -521,7 +521,7 @@ export default function CeleryOverviewTable({
locale={{ locale={{
emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>, emptyText: isLoading ? null : <Typography.Text>No data</Typography.Text>,
}} }}
scroll={{ x: true }} scroll={{ x: 'max-content' }}
showSorterTooltip showSorterTooltip
onDragColumn={handleDragColumn} onDragColumn={handleDragColumn}
onRow={(record): { onClick: () => void; className: string } => ({ onRow={(record): { onClick: () => void; className: string } => ({

View File

@ -1,3 +1,5 @@
import './ResizeTable.styles.scss';
import { SyntheticEvent, useMemo } from 'react'; import { SyntheticEvent, useMemo } from 'react';
import { Resizable, ResizeCallbackData } from 'react-resizable'; import { Resizable, ResizeCallbackData } from 'react-resizable';
@ -10,8 +12,8 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
const handle = useMemo( const handle = useMemo(
() => ( () => (
<SpanStyle <SpanStyle
className="react-resizable-handle"
onClick={(e): void => e.stopPropagation()} onClick={(e): void => e.stopPropagation()}
className="resize-handle"
/> />
), ),
[], [],
@ -19,7 +21,7 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
if (!width) { if (!width) {
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return <th {...restProps} />; return <th {...restProps} className="resizable-header" />;
} }
return ( return (
@ -29,9 +31,10 @@ function ResizableHeader(props: ResizableHeaderProps): JSX.Element {
handle={handle} handle={handle}
onResize={onResize} onResize={onResize}
draggableOpts={enableUserSelectHack} draggableOpts={enableUserSelectHack}
minConstraints={[150, 0]}
> >
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {/* eslint-disable-next-line react/jsx-props-no-spreading */}
<th {...restProps} /> <th {...restProps} className="resizable-header" />
</Resizable> </Resizable>
); );
} }

View File

@ -0,0 +1,53 @@
.resizable-header {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
position: relative;
.ant-table-column-title {
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
}
}
.resize-main-table {
.ant-table-body {
.ant-table-tbody {
.ant-table-row {
.ant-table-cell {
.ant-typography {
white-space: unset;
}
}
}
}
}
}
.logs-table,
.traces-table {
.resize-table {
.resize-handle {
position: absolute;
top: 0;
bottom: 0;
inset-inline-end: -5px;
width: 10px;
cursor: col-resize;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1px;
height: 1.6em;
background-color: var(--bg-slate-200);
transition: background-color 0.2s;
}
}
}
}

View File

@ -2,35 +2,63 @@
import { Table } from 'antd'; import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import cx from 'classnames';
import { dragColumnParams } from 'hooks/useDragColumns/configs'; import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { set } from 'lodash-es'; import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { debounce, set } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { import {
SyntheticEvent, SyntheticEvent,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from 'react'; } from 'react';
import ReactDragListView from 'react-drag-listview'; import ReactDragListView from 'react-drag-listview';
import { ResizeCallbackData } from 'react-resizable'; import { ResizeCallbackData } from 'react-resizable';
import { Widgets } from 'types/api/dashboard/getAll';
import ResizableHeader from './ResizableHeader'; import ResizableHeader from './ResizableHeader';
import { DragSpanStyle } from './styles'; import { DragSpanStyle } from './styles';
import { ResizeTableProps } from './types'; import { ResizeTableProps } from './types';
// eslint-disable-next-line sonarjs/cognitive-complexity
function ResizeTable({ function ResizeTable({
columns, columns,
onDragColumn, onDragColumn,
pagination, pagination,
widgetId,
shouldPersistColumnWidths = false,
...restProps ...restProps
}: ResizeTableProps): JSX.Element { }: ResizeTableProps): JSX.Element {
const [columnsData, setColumns] = useState<ColumnsType>([]); 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) return;
setColumnWidths?.((prev) => ({
...prev,
[widgetId]: widthsConfig,
}));
}, 1000),
).current;
const handleResize = useCallback( const handleResize = useCallback(
(index: number) => ( (index: number) => (
_e: SyntheticEvent<Element>, e: SyntheticEvent<Element>,
{ size }: ResizeCallbackData, { size }: ResizeCallbackData,
): void => { ): void => {
e.preventDefault();
e.stopPropagation();
const newColumns = [...columnsData]; const newColumns = [...columnsData];
newColumns[index] = { newColumns[index] = {
...newColumns[index], ...newColumns[index],
@ -65,6 +93,7 @@ function ResizeTable({
...restProps, ...restProps,
components: { header: { cell: ResizableHeader } }, components: { header: { cell: ResizableHeader } },
columns: mergedColumns, columns: mergedColumns,
className: cx('resize-main-table', restProps.className),
}; };
set( set(
@ -78,9 +107,39 @@ function ResizeTable({
useEffect(() => { useEffect(() => {
if (columns) { if (columns) {
setColumns(columns); // Apply stored column widths from widget configuration
const columnsWithStoredWidths = columns.map((col) => {
const dataIndex = (col as RowData).dataIndex as string;
if (dataIndex && columnWidths && columnWidths[dataIndex]) {
return {
...col,
width: columnWidths[dataIndex], // Apply stored width
};
}
return col;
});
setColumns(columnsWithStoredWidths);
} }
}, [columns]); }, [columns, columnWidths]);
useEffect(() => {
if (!shouldPersistColumnWidths) return;
// Collect all column widths in a single object
const newColumnWidths: Record<string, number> = {};
mergedColumns.forEach((col) => {
if (col.width && (col as RowData).dataIndex) {
const dataIndex = (col as RowData).dataIndex as string;
newColumnWidths[dataIndex] = col.width as number;
}
});
// Only update if there are actual widths to set
if (Object.keys(newColumnWidths).length > 0) {
updateAllColumnWidths(newColumnWidths);
}
}, [mergedColumns, updateAllColumnWidths, shouldPersistColumnWidths]);
return onDragColumn ? ( return onDragColumn ? (
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}> <ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>

View File

@ -8,6 +8,8 @@ export const SpanStyle = styled.span`
width: 0.625rem; width: 0.625rem;
height: 100%; height: 100%;
cursor: col-resize; cursor: col-resize;
margin-left: 4px;
margin-right: 4px;
`; `;
export const DragSpanStyle = styled.span` export const DragSpanStyle = styled.span`

View File

@ -9,6 +9,8 @@ import { TableDataSource } from './contants';
export interface ResizeTableProps extends TableProps<any> { export interface ResizeTableProps extends TableProps<any> {
onDragColumn?: (fromIndex: number, toIndex: number) => void; onDragColumn?: (fromIndex: number, toIndex: number) => void;
widgetId?: string;
shouldPersistColumnWidths?: boolean;
} }
export interface DynamicColumnTableProps extends TableProps<any> { export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource]; tablesource: typeof TableDataSource[keyof typeof TableDataSource];

View File

@ -1,3 +1,4 @@
import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels'; import AlertChannels from 'container/AllAlertChannels';
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts'; import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
@ -20,6 +21,13 @@ jest.mock('hooks/useNotifications', () => ({
})), })),
})); }));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
}),
}));
describe('Alert Channels Settings List page', () => { describe('Alert Channels Settings List page', () => {
beforeEach(() => { beforeEach(() => {
render(<AlertChannels />); render(<AlertChannels />);

View File

@ -1,3 +1,4 @@
import ROUTES from 'constants/routes';
import AlertChannels from 'container/AllAlertChannels'; import AlertChannels from 'container/AllAlertChannels';
import { allAlertChannels } from 'mocks-server/__mockdata__/alerts'; import { allAlertChannels } from 'mocks-server/__mockdata__/alerts';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
@ -25,6 +26,13 @@ jest.mock('hooks/useComponentPermission', () => ({
default: jest.fn().mockImplementation(() => [false]), default: jest.fn().mockImplementation(() => [false]),
})); }));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALL_CHANNELS}`,
}),
}));
describe('Alert Channels Settings List page (Normal User)', () => { describe('Alert Channels Settings List page (Normal User)', () => {
beforeEach(() => { beforeEach(() => {
render(<AlertChannels />); render(<AlertChannels />);

View File

@ -44,7 +44,10 @@ import { EditMenuAction, ViewMenuAction } from './config';
import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState'; import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
import GridCard from './GridCard'; import GridCard from './GridCard';
import { Card, CardContainer, ReactGridLayout } from './styles'; import { Card, CardContainer, ReactGridLayout } from './styles';
import { removeUndefinedValuesFromLayout } from './utils'; import {
hasColumnWidthsChanged,
removeUndefinedValuesFromLayout,
} from './utils';
import { MenuItemKeys } from './WidgetHeader/contants'; import { MenuItemKeys } from './WidgetHeader/contants';
import { WidgetRowHeader } from './WidgetRow'; import { WidgetRowHeader } from './WidgetRow';
@ -68,6 +71,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setDashboardQueryRangeCalled, setDashboardQueryRangeCalled,
setSelectedRowWidgetId, setSelectedRowWidgetId,
isDashboardFetching, isDashboardFetching,
columnWidths,
} = useDashboard(); } = useDashboard();
const { data } = selectedDashboard || {}; const { data } = selectedDashboard || {};
const { pathname } = useLocation(); const { pathname } = useLocation();
@ -162,6 +166,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
logEventCalledRef.current = true; logEventCalledRef.current = true;
} }
}, [data]); }, [data]);
const onSaveHandler = (): void => { const onSaveHandler = (): void => {
if (!selectedDashboard) return; if (!selectedDashboard) return;
@ -171,6 +176,15 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
...selectedDashboard.data, ...selectedDashboard.data,
panelMap: { ...currentPanelMap }, panelMap: { ...currentPanelMap },
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET), layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
widgets: selectedDashboard?.data?.widgets?.map((widget) => {
if (columnWidths?.[widget.id]) {
return {
...widget,
columnWidths: columnWidths[widget.id],
};
}
return widget;
}),
}, },
uuid: selectedDashboard.uuid, uuid: selectedDashboard.uuid,
}; };
@ -227,20 +241,31 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useEffect(() => { useEffect(() => {
if ( if (
isDashboardLocked ||
!saveLayoutPermission ||
updateDashboardMutation.isLoading ||
isDashboardFetching
) {
return;
}
const shouldSaveLayout =
dashboardLayout && dashboardLayout &&
Array.isArray(dashboardLayout) && Array.isArray(dashboardLayout) &&
dashboardLayout.length > 0 && dashboardLayout.length > 0 &&
!isEqual(layouts, dashboardLayout) && !isEqual(layouts, dashboardLayout);
!isDashboardLocked &&
saveLayoutPermission && const shouldSaveColumnWidths =
!updateDashboardMutation.isLoading && dashboardLayout &&
!isDashboardFetching Array.isArray(dashboardLayout) &&
) { dashboardLayout.length > 0 &&
hasColumnWidthsChanged(columnWidths, selectedDashboard);
if (shouldSaveLayout || shouldSaveColumnWidths) {
onSaveHandler(); onSaveHandler();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardLayout]); }, [dashboardLayout, columnWidths]);
const onSettingsModalSubmit = (): void => { const onSettingsModalSubmit = (): void => {
const newTitle = form.getFieldValue('title'); const newTitle = form.getFieldValue('title');

View File

@ -1,5 +1,7 @@
import { FORMULA_REGEXP } from 'constants/regExp'; import { FORMULA_REGEXP } from 'constants/regExp';
import { isEmpty, isEqual } from 'lodash-es';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] => export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
layout.map((obj) => layout.map((obj) =>
@ -25,3 +27,27 @@ export function extractQueryNamesFromExpression(expression: string): string[] {
// Extract matches and deduplicate // Extract matches and deduplicate
return [...new Set(expression.match(queryNameRegex) || [])]; return [...new Set(expression.match(queryNameRegex) || [])];
} }
export const hasColumnWidthsChanged = (
columnWidths: Record<string, Record<string, number>>,
selectedDashboard?: Dashboard,
): boolean => {
// If no column widths stored, no changes
if (isEmpty(columnWidths) || !selectedDashboard) return false;
// Check each widget's column widths
return Object.keys(columnWidths).some((widgetId) => {
const dashboardWidget = selectedDashboard?.data?.widgets?.find(
(widget) => widget.id === widgetId,
) as Widgets;
const newWidths = columnWidths[widgetId];
const existingWidths = dashboardWidget?.columnWidths;
// If both are empty/undefined, no change
if (isEmpty(newWidths) || isEmpty(existingWidths)) return false;
// Compare stored column widths with dashboard widget's column widths
return !isEqual(newWidths, existingWidths);
});
};

View File

@ -43,6 +43,7 @@ function GridTableComponent({
sticky, sticky,
openTracesButton, openTracesButton,
onOpenTraceBtnClick, onOpenTraceBtnClick,
widgetId,
...props ...props
}: GridTableComponentProps): JSX.Element { }: GridTableComponentProps): JSX.Element {
const { t } = useTranslation(['valueGraph']); const { t } = useTranslation(['valueGraph']);
@ -229,6 +230,7 @@ function GridTableComponent({
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData} columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
dataSource={dataSource} dataSource={dataSource}
sticky={sticky} sticky={sticky}
widgetId={widgetId}
onRow={ onRow={
openTracesButton openTracesButton
? (record): React.HTMLAttributes<HTMLElement> => ({ ? (record): React.HTMLAttributes<HTMLElement> => ({

View File

@ -17,6 +17,7 @@ export type GridTableComponentProps = {
searchTerm?: string; searchTerm?: string;
openTracesButton?: boolean; openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void; onOpenTraceBtnClick?: (record: RowData) => void;
widgetId?: string;
} & Pick<LogsExplorerTableProps, 'data'> & } & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>; Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@ -1,9 +1,9 @@
import './LogsPanelComponent.styles.scss'; import './LogsPanelComponent.styles.scss';
import { Table } from 'antd';
import LogDetail from 'components/LogDetail'; import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants'; import { VIEW_TYPES } from 'components/LogDetail/constants';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { ResizeTable } from 'components/ResizeTable';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls'; import Controls from 'container/Controls';
@ -79,9 +79,14 @@ function LogsPanelComponent({
const { formatTimezoneAdjustedTimestamp } = useTimezone(); const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns = getLogPanelColumnsList( const columns = useMemo(
widget.selectedLogFields, () =>
formatTimezoneAdjustedTimestamp, getLogPanelColumnsList(
widget.selectedLogFields,
formatTimezoneAdjustedTimestamp,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[widget.selectedLogFields],
); );
const dataLength = const dataLength =
@ -216,16 +221,18 @@ function LogsPanelComponent({
<div className="logs-table"> <div className="logs-table">
<div className="resize-table"> <div className="resize-table">
<OverlayScrollbar> <OverlayScrollbar>
<Table <ResizeTable
pagination={false} pagination={false}
tableLayout="fixed" tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }} scroll={{ x: `max-content` }}
sticky sticky
loading={queryResponse.isFetching} loading={queryResponse.isFetching}
style={tableStyles} style={tableStyles}
dataSource={flattenLogData} dataSource={flattenLogData}
columns={columns} columns={columns}
onRow={handleRow} onRow={handleRow}
widgetId={widget.id}
shouldPersistColumnWidths
/> />
</OverlayScrollbar> </OverlayScrollbar>
</div> </div>

View File

@ -74,6 +74,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
setToScrollWidgetId, setToScrollWidgetId,
selectedRowWidgetId, selectedRowWidgetId,
setSelectedRowWidgetId, setSelectedRowWidgetId,
columnWidths,
} = useDashboard(); } = useDashboard();
const { t } = useTranslation(['dashboard']); const { t } = useTranslation(['dashboard']);
@ -238,8 +239,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedLogFields, selectedLogFields,
selectedTracesFields, selectedTracesFields,
isLogScale, isLogScale,
columnWidths: columnWidths?.[selectedWidget?.id],
}; };
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
columnUnits, columnUnits,
currentQuery, currentQuery,
@ -260,6 +263,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
combineHistogram, combineHistogram,
stackedBarChart, stackedBarChart,
isLogScale, isLogScale,
columnWidths,
]); ]);
const closeModal = (): void => { const closeModal = (): void => {

View File

@ -26,6 +26,7 @@ function TablePanelWrapper({
searchTerm={searchTerm} searchTerm={searchTerm}
openTracesButton={openTracesButton} openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick} onOpenTraceBtnClick={onOpenTraceBtnClick}
widgetId={widget.id}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG} {...GRID_TABLE_CONFIG}
/> />

View File

@ -9,6 +9,8 @@ exports[`Table panel wrappper tests table should render fine with the query resp
width: 0.625rem; width: 0.625rem;
height: 100%; height: 100%;
cursor: col-resize; cursor: col-resize;
margin-left: 4px;
margin-right: 4px;
} }
.c0 { .c0 {
@ -54,7 +56,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
class="query-table" class="query-table"
> >
<div <div
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap" class="ant-table-wrapper resize-main-table css-dev-only-do-not-override-2i2tap"
> >
<div <div
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap" class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
@ -82,7 +84,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
<tr> <tr>
<th <th
aria-label="service_name" aria-label="service_name"
class="ant-table-cell ant-table-column-has-sorters react-resizable" class="resizable-header react-resizable"
scope="col" scope="col"
tabindex="0" tabindex="0"
> >
@ -143,12 +145,12 @@ exports[`Table panel wrappper tests table should render fine with the query resp
</span> </span>
</div> </div>
<span <span
class="c1 react-resizable-handle" class="c1 resize-handle"
/> />
</th> </th>
<th <th
aria-label="latency-per-service" aria-label="latency-per-service"
class="ant-table-cell ant-table-column-has-sorters react-resizable" class="resizable-header react-resizable"
scope="col" scope="col"
tabindex="0" tabindex="0"
> >
@ -209,7 +211,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
</span> </span>
</div> </div>
<span <span
class="c1 react-resizable-handle" class="c1 resize-handle"
/> />
</th> </th>
</tr> </tr>
@ -221,7 +223,7 @@ exports[`Table panel wrappper tests table should render fine with the query resp
style="overflow-x: auto; overflow-y: hidden;" style="overflow-x: auto; overflow-y: hidden;"
> >
<table <table
style="width: auto; min-width: 100%; table-layout: fixed;" style="min-width: 100%; table-layout: fixed;"
> >
<colgroup> <colgroup>
<col <col

View File

@ -20,4 +20,5 @@ export type QueryTableProps = Omit<
dataSource?: RowData[]; dataSource?: RowData[];
sticky?: TableProps<RowData>['sticky']; sticky?: TableProps<RowData>['sticky'];
searchTerm?: string; searchTerm?: string;
widgetId?: string;
}; };

View File

@ -24,6 +24,7 @@ export function QueryTable({
dataSource, dataSource,
sticky, sticky,
searchTerm, searchTerm,
widgetId,
...props ...props
}: QueryTableProps): JSX.Element { }: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {}; const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
@ -95,8 +96,10 @@ export function QueryTable({
columns={tableColumns} columns={tableColumns}
tableLayout="fixed" tableLayout="fixed"
dataSource={filterTable === null ? newDataSource : filterTable} dataSource={filterTable === null ? newDataSource : filterTable}
scroll={{ x: true }} scroll={{ x: 'max-content' }}
pagination={paginationConfig} pagination={paginationConfig}
widgetId={widgetId}
shouldPersistColumnWidths
sticky={sticky} sticky={sticky}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}

View File

@ -1,7 +1,7 @@
import './TracesTableComponent.styles.scss'; import './TracesTableComponent.styles.scss';
import { Table } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { ResizeTable } from 'components/ResizeTable';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import Controls from 'container/Controls'; import Controls from 'container/Controls';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs'; import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
@ -54,9 +54,14 @@ function TracesTableComponent({
const { formatTimezoneAdjustedTimestamp } = useTimezone(); const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns = getListColumns( const columns = useMemo(
widget.selectedTracesFields || [], () =>
formatTimezoneAdjustedTimestamp, getListColumns(
widget.selectedTracesFields || [],
formatTimezoneAdjustedTimestamp,
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[widget.selectedTracesFields],
); );
const dataLength = const dataLength =
@ -116,16 +121,18 @@ function TracesTableComponent({
<div className="traces-table"> <div className="traces-table">
<div className="resize-table"> <div className="resize-table">
<OverlayScrollbar> <OverlayScrollbar>
<Table <ResizeTable
pagination={false} pagination={false}
tableLayout="fixed" tableLayout="fixed"
scroll={{ x: true }} scroll={{ x: 'max-content' }}
loading={queryResponse.isFetching} loading={queryResponse.isFetching}
style={tableStyles} style={tableStyles}
dataSource={transformedQueryTableData} dataSource={transformedQueryTableData}
columns={columns} columns={columns}
onRow={handleRow} onRow={handleRow}
sticky sticky
widgetId={widget.id}
shouldPersistColumnWidths
/> />
</OverlayScrollbar> </OverlayScrollbar>
</div> </div>

View File

@ -40,7 +40,11 @@ import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { DashboardSortOrder, IDashboardContext } from './types'; import {
DashboardSortOrder,
IDashboardContext,
WidgetColumnWidths,
} from './types';
import { sortLayout } from './util'; import { sortLayout } from './util';
const DashboardContext = createContext<IDashboardContext>({ const DashboardContext = createContext<IDashboardContext>({
@ -74,6 +78,8 @@ const DashboardContext = createContext<IDashboardContext>({
selectedRowWidgetId: '', selectedRowWidgetId: '',
setSelectedRowWidgetId: () => {}, setSelectedRowWidgetId: () => {},
isDashboardFetching: false, isDashboardFetching: false,
columnWidths: {},
setColumnWidths: () => {},
}); });
interface Props { interface Props {
@ -408,6 +414,8 @@ export function DashboardProvider({
} }
}; };
const [columnWidths, setColumnWidths] = useState<WidgetColumnWidths>({});
const value: IDashboardContext = useMemo( const value: IDashboardContext = useMemo(
() => ({ () => ({
toScrollWidgetId, toScrollWidgetId,
@ -435,6 +443,8 @@ export function DashboardProvider({
selectedRowWidgetId, selectedRowWidgetId,
setSelectedRowWidgetId, setSelectedRowWidgetId,
isDashboardFetching, isDashboardFetching,
columnWidths,
setColumnWidths,
}), }),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[ [
@ -457,6 +467,8 @@ export function DashboardProvider({
selectedRowWidgetId, selectedRowWidgetId,
setSelectedRowWidgetId, setSelectedRowWidgetId,
isDashboardFetching, isDashboardFetching,
columnWidths,
setColumnWidths,
], ],
); );

View File

@ -10,6 +10,10 @@ export interface DashboardSortOrder {
search: string; search: string;
} }
export type WidgetColumnWidths = {
[widgetId: string]: Record<string, number>;
};
export interface IDashboardContext { export interface IDashboardContext {
isDashboardSliderOpen: boolean; isDashboardSliderOpen: boolean;
isDashboardLocked: boolean; isDashboardLocked: boolean;
@ -48,4 +52,6 @@ export interface IDashboardContext {
selectedRowWidgetId: string | null; selectedRowWidgetId: string | null;
setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>; setSelectedRowWidgetId: React.Dispatch<React.SetStateAction<string | null>>;
isDashboardFetching: boolean; isDashboardFetching: boolean;
columnWidths: WidgetColumnWidths;
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;
} }

View File

@ -109,6 +109,7 @@ export interface IBaseWidget {
selectedLogFields: IField[] | null; selectedLogFields: IField[] | null;
selectedTracesFields: BaseAutocompleteData[] | null; selectedTracesFields: BaseAutocompleteData[] | null;
isLogScale?: boolean; isLogScale?: boolean;
columnWidths?: Record<string, number>;
} }
export interface Widgets extends IBaseWidget { export interface Widgets extends IBaseWidget {
query: Query; query: Query;