Logs in Explore: Persist table sorting in the url (#114060)
This commit is contained in:
@@ -85,6 +85,9 @@ export interface ExploreLogsPanelState {
|
||||
refId?: string;
|
||||
displayedFields?: string[];
|
||||
sortOrder?: LogsSortOrder;
|
||||
// Column sort state for table view. Persists between query changes.
|
||||
tableSortBy?: string;
|
||||
tableSortDir?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
|
||||
|
||||
@@ -308,6 +308,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
refId: undefined,
|
||||
displayedFields: undefined,
|
||||
sortOrder: undefined,
|
||||
tableSortBy: undefined,
|
||||
tableSortDir: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -324,6 +326,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
labelFieldName: logsPanelState.labelFieldName,
|
||||
refId: logsPanelState.refId ?? panelState?.logs?.refId,
|
||||
displayedFields: logsPanelState.displayedFields ?? panelState?.logs?.displayedFields,
|
||||
tableSortBy: logsPanelState.tableSortBy ?? panelState?.logs?.tableSortBy,
|
||||
tableSortDir: logsPanelState.tableSortDir ?? panelState?.logs?.tableSortDir,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -334,6 +338,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
panelState?.logs?.columns,
|
||||
panelState?.logs?.displayedFields,
|
||||
panelState?.logs?.refId,
|
||||
panelState?.logs?.tableSortBy,
|
||||
panelState?.logs?.tableSortDir,
|
||||
visualisationType,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { DataFrame, FieldType, LogsSortOrder, toUtc } from '@grafana/data';
|
||||
@@ -301,4 +301,65 @@ describe('LogsTable', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sort persistence', () => {
|
||||
it('should update URL with sort parameters when sort changes', async () => {
|
||||
// Mock onSortByChange to update URL (simulating parent Explore component behavior)
|
||||
const onSortByChange = jest.fn((sortBy) => {
|
||||
const mockUrl = new URL(window.location.href);
|
||||
if (sortBy && sortBy.length > 0) {
|
||||
mockUrl.searchParams.set('tableSortBy', sortBy[0].displayName);
|
||||
mockUrl.searchParams.set('tableSortDir', sortBy[0].desc ? 'desc' : 'asc');
|
||||
} else {
|
||||
// Remove sort params if no sort is applied
|
||||
mockUrl.searchParams.delete('tableSortBy');
|
||||
mockUrl.searchParams.delete('tableSortDir');
|
||||
}
|
||||
window.history.replaceState({}, '', mockUrl.toString());
|
||||
});
|
||||
|
||||
setup({
|
||||
tableSortBy: 'Time',
|
||||
tableSortDir: 'desc',
|
||||
onSortByChange,
|
||||
columnsWithMeta: {
|
||||
Time: { active: true, percentOfLinesWithLabel: 3, index: 0 },
|
||||
line: { active: true, percentOfLinesWithLabel: 3, index: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const rows = screen.getAllByRole('row');
|
||||
expect(rows.length).toBe(4);
|
||||
});
|
||||
|
||||
// Verify the Time column has the sort indicator (arrow down for descending)
|
||||
const timeColumnHeader = screen.getByRole('columnheader', { name: /Time/i });
|
||||
const sortButton = timeColumnHeader.querySelector('button[title="Toggle SortBy"]');
|
||||
expect(sortButton).toBeTruthy();
|
||||
|
||||
// Click to toggle sort (desc -> asc)
|
||||
if (sortButton) {
|
||||
fireEvent.click(sortButton);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSortByChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Verify URL was updated (callback was called and URL reflects the new sort state)
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const tableSortBy = currentUrl.searchParams.get('tableSortBy');
|
||||
const tableSortDir = currentUrl.searchParams.get('tableSortDir');
|
||||
|
||||
expect(onSortByChange).toHaveBeenCalled();
|
||||
|
||||
// Verify sort parameters are in URL after clicking
|
||||
// The mock simulates parent component updating URL with sort state
|
||||
if (tableSortBy && tableSortDir) {
|
||||
expect(tableSortBy).toBe('Time');
|
||||
expect(tableSortDir).toBe('desc');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
ValueLinkConfig,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AdHocFilterItem, Table } from '@grafana/ui';
|
||||
import { AdHocFilterItem, Table, TableSortByFieldState } from '@grafana/ui';
|
||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/internal';
|
||||
import { LogsFrame } from 'app/features/logs/logsFrame';
|
||||
|
||||
@@ -38,10 +38,25 @@ interface Props {
|
||||
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
||||
logsFrame: LogsFrame | null;
|
||||
tableSortBy?: string;
|
||||
tableSortDir?: 'asc' | 'desc';
|
||||
onSortByChange?: (sortBy: TableSortByFieldState[]) => void;
|
||||
}
|
||||
|
||||
export function LogsTable(props: Props) {
|
||||
const { timeZone, splitOpen, range, logsSortOrder, width, dataFrame, columnsWithMeta, logsFrame } = props;
|
||||
const {
|
||||
timeZone,
|
||||
splitOpen,
|
||||
range,
|
||||
logsSortOrder,
|
||||
width,
|
||||
dataFrame,
|
||||
columnsWithMeta,
|
||||
logsFrame,
|
||||
tableSortBy,
|
||||
tableSortDir,
|
||||
onSortByChange,
|
||||
} = props;
|
||||
const [tableFrame, setTableFrame] = useState<DataFrame | undefined>(undefined);
|
||||
const timeIndex = logsFrame?.timeField.index;
|
||||
|
||||
@@ -167,6 +182,13 @@ export function LogsTable(props: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
// Use persisted sortBy if available, otherwise default to time field based on logsSortOrder
|
||||
const defaultSortBy: TableSortByFieldState[] = [
|
||||
{ displayName: logsFrame?.timeField.name || '', desc: logsSortOrder === LogsSortOrder.Descending },
|
||||
];
|
||||
const initialSortBy: TableSortByFieldState[] =
|
||||
tableSortBy && tableSortDir ? [{ displayName: tableSortBy, desc: tableSortDir === 'desc' }] : defaultSortBy;
|
||||
|
||||
return (
|
||||
<Table
|
||||
data={tableFrame}
|
||||
@@ -174,9 +196,8 @@ export function LogsTable(props: Props) {
|
||||
onCellFilterAdded={props.onClickFilterLabel && props.onClickFilterOutLabel ? onCellFilterAdded : undefined}
|
||||
height={props.height}
|
||||
footerOptions={{ show: true, reducer: ['count'], countRows: true }}
|
||||
initialSortBy={[
|
||||
{ displayName: logsFrame?.timeField.name || '', desc: logsSortOrder === LogsSortOrder.Descending },
|
||||
]}
|
||||
initialSortBy={initialSortBy}
|
||||
onSortByChange={onSortByChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -278,6 +278,25 @@ export function LogsTableWrap(props: Props) {
|
||||
|
||||
const styles = useStyles2(getStyles, height, sidebarWidth);
|
||||
|
||||
const onSortByChange = useCallback(
|
||||
(sortBy: Array<{ displayName: string; desc?: boolean }>) => {
|
||||
// Transform from Table format to URL format - only store the first sort column
|
||||
if (sortBy.length > 0) {
|
||||
// Defer the Redux dispatch to avoid updating ExploreActions during Table's render cycle
|
||||
// Even though this is called from an event handler, the synchronous Redux dispatch
|
||||
// can cause ExploreActions (which subscribes to panes state) to re-render while
|
||||
// Table is still rendering, triggering the React warning
|
||||
setTimeout(() => {
|
||||
updatePanelState({
|
||||
tableSortBy: sortBy[0].displayName,
|
||||
tableSortDir: sortBy[0].desc ? 'desc' : 'asc',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[updatePanelState]
|
||||
);
|
||||
|
||||
if (!columnsWithMeta) {
|
||||
return null;
|
||||
}
|
||||
@@ -512,6 +531,9 @@ export function LogsTableWrap(props: Props) {
|
||||
dataFrame={currentDataFrame}
|
||||
columnsWithMeta={columnsWithMeta}
|
||||
height={height}
|
||||
tableSortBy={panelState?.tableSortBy}
|
||||
tableSortDir={panelState?.tableSortDir}
|
||||
onSortByChange={onSortByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user