mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 17:48:59 +08:00
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:
parent
a876c0a744
commit
d8d8191a32
@ -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 } => ({
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
53
frontend/src/components/ResizeTable/ResizeTable.styles.scss
Normal file
53
frontend/src/components/ResizeTable/ResizeTable.styles.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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}>
|
||||||
|
@ -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`
|
||||||
|
@ -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];
|
||||||
|
@ -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 />);
|
||||||
|
@ -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 />);
|
||||||
|
@ -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');
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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> => ({
|
||||||
|
@ -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'>;
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 => {
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>>;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user