Logs: Persist sort order in the Explore URL (#114350)

* Logs: store sort order in the URL

* ToolbarExtensionPoint: pass sort order to extension

* Logs: send sort order in links

* ToolbarExtensionPoint: pass panelState instead of sortOrder

* Update test

* Remove condition

* Logs: initialize sort order and remove unnecessary check
This commit is contained in:
Matias Chomicki
2025-11-26 12:00:15 +01:00
committed by GitHub
parent 5538dfe73d
commit 291e3ea9cf
4 changed files with 77 additions and 31 deletions
+2 -1
View File
@@ -1,4 +1,4 @@
import { DataQuery } from '@grafana/schema';
import { DataQuery, LogsSortOrder } from '@grafana/schema';
import { PreferredVisualisationType } from './data';
import { SelectableValue } from './select';
@@ -84,6 +84,7 @@ export interface ExploreLogsPanelState {
// Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized
refId?: string;
displayedFields?: string[];
sortOrder?: LogsSortOrder;
}
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
+38 -21
View File
@@ -197,7 +197,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
);
const [dedupStrategy, setDedupStrategy] = useState<LogsDedupStrategy>(LogsDedupStrategy.none);
const [logsSortOrder, setLogsSortOrder] = useState<LogsSortOrder>(
store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending
panelState?.logs?.sortOrder ?? store.get(SETTINGS_KEYS.logsSortOrder) ?? LogsSortOrder.Descending
);
const [isFlipping, setIsFlipping] = useState<boolean>(false);
const [displayedFields, setDisplayedFields] = useState<string[]>(panelState?.logs?.displayedFields ?? []);
@@ -269,6 +269,18 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
}
}, [dispatch, exploreId, loading, panelState, previousLoading]);
useEffect(() => {
// Initialize URL sort order
if (!panelState?.logs?.sortOrder) {
dispatch(
changePanelState(exploreId, 'logs', {
...panelState,
sortOrder: logsSortOrder,
})
);
}
}, [dispatch, exploreId, logsSortOrder, panelState]);
useEffect(() => {
const visualisationType = panelState?.logs?.visualisationType ?? getDefaultVisualisationType();
setVisualisationType(visualisationType);
@@ -287,23 +299,17 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
useUnmount(() => {
// If we're unmounting logs (e.g. switching to another datasource), we need to remove the logs specific panel state, otherwise it will persist in the explore url
if (
panelState?.logs?.columns ||
panelState?.logs?.refId ||
panelState?.logs?.labelFieldName ||
panelState?.logs?.displayedFields
) {
dispatch(
changePanelState(exploreId, 'logs', {
...panelState?.logs,
columns: undefined,
visualisationType: visualisationType,
labelFieldName: undefined,
refId: undefined,
displayedFields: undefined,
})
);
}
dispatch(
changePanelState(exploreId, 'logs', {
...panelState?.logs,
columns: undefined,
visualisationType: visualisationType,
labelFieldName: undefined,
refId: undefined,
displayedFields: undefined,
sortOrder: undefined,
})
);
});
const updatePanelState = useCallback(
@@ -398,8 +404,14 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
dispatch(changeQueries({ exploreId, queries: newQueries }));
dispatch(runQueries({ exploreId }));
}
dispatch(
changePanelState(exploreId, 'logs', {
...panelState?.logs,
sortOrder: newSortOrder,
})
);
},
[dispatch, exploreId, logsQueries]
[dispatch, exploreId, logsQueries, panelState?.logs]
);
const onChangeLogsSortOrder = useCallback(
@@ -588,7 +600,12 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
const urlState = getUrlStateFromPaneState(getState().explore.panes[exploreId]!);
urlState.panelsState = {
...panelState,
logs: { id: row.uid, visualisationType: visualisationType ?? getDefaultVisualisationType(), displayedFields },
logs: {
id: row.uid,
visualisationType: visualisationType ?? getDefaultVisualisationType(),
displayedFields,
sortOrder: logsSortOrder,
},
};
urlState.range = getLogsPermalinkRange(row, logRows, absoluteRange);
@@ -604,7 +621,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
logRowLevel: row.logLevel,
});
},
[absoluteRange, displayedFields, exploreId, logRows, panelState, visualisationType]
[absoluteRange, displayedFields, exploreId, logRows, logsSortOrder, panelState, visualisationType]
);
const scrollToTopLogs = useCallback(() => {
@@ -3,9 +3,9 @@ import userEvent from '@testing-library/user-event';
import { ReactNode } from 'react';
import { Provider } from 'react-redux';
import { PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data';
import { ExplorePanelsState, PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data';
import { usePluginLinks } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { DataQuery, LogsSortOrder } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore';
import { ExplorePanelData, ExploreState } from 'app/types/explore';
@@ -27,13 +27,14 @@ const usePluginLinksMock = jest.mocked(usePluginLinks);
type storeOptions = {
targets: DataQuery[];
data: ExplorePanelData;
panelsState?: ExplorePanelsState;
};
function renderWithExploreStore(
children: ReactNode,
options: storeOptions = { targets: [{ refId: 'A' }], data: createEmptyQueryResponse() }
) {
const { targets, data } = options;
const { targets, data, panelsState } = options;
const store = configureStore({
explore: {
panes: {
@@ -43,6 +44,7 @@ function renderWithExploreStore(
range: {
raw: { from: 'now-1h', to: 'now' },
},
panelsState,
},
},
} as unknown as ExploreState,
@@ -90,6 +92,9 @@ describe('ToolbarExtensionPoint', () => {
isLoading: false,
});
});
beforeEach(() => {
jest.mocked(usePluginLinksMock).mockClear();
});
it('should render "Add" extension point menu button', () => {
renderWithExploreStore(setupToolbarExtensionPoint());
@@ -180,6 +185,20 @@ describe('ToolbarExtensionPoint', () => {
expect(extensionPointId).toBe(PluginExtensionPoints.ExploreToolbarAction);
});
it('should pass panelsState to the extensions', async () => {
const panelsState: ExplorePanelsState = {
logs: { sortOrder: LogsSortOrder.Ascending, displayedFields: ['time', 'body'] },
};
const targets = [{ refId: 'A' }];
const data = createEmptyQueryResponse();
renderWithExploreStore(setupToolbarExtensionPoint(), { targets, data, panelsState });
const [options] = usePluginLinksMock.mock.calls[0];
const { context } = options;
expect(context).toHaveProperty('panelsState', panelsState);
});
});
describe('with extension points without categories', () => {
@@ -1,6 +1,12 @@
import { ReactElement, useMemo, useState } from 'react';
import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange, getTimeZone } from '@grafana/data';
import {
type ExplorePanelsState,
type PluginExtensionLink,
PluginExtensionPoints,
RawTimeRange,
getTimeZone,
} from '@grafana/data';
import { reportInteraction, usePluginLinks } from '@grafana/runtime';
import { DataQuery, TimeZone } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
@@ -89,13 +95,14 @@ export type PluginExtensionExploreContext = {
timeRange: RawTimeRange;
timeZone: TimeZone;
shouldShowAddCorrelation: boolean;
panelsSate?: ExplorePanelsState;
};
function useExtensionPointContext(props: Props): PluginExtensionExploreContext {
const { exploreId, timeZone } = props;
const isCorrelationDetails = useSelector(selectCorrelationDetails);
const isCorrelationsEditorMode = isCorrelationDetails?.editorMode || false;
const { queries, queryResponse, range } = useSelector(getExploreItemSelector(exploreId))!;
const { queries, queryResponse, range, panelsState } = useSelector(getExploreItemSelector(exploreId))!;
const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
const datasourceUids = queries.map((query) => query?.datasource?.uid).filter((uid) => uid !== undefined);
@@ -110,16 +117,18 @@ function useExtensionPointContext(props: Props): PluginExtensionExploreContext {
timeRange: range.raw,
timeZone: getTimeZone({ timeZone }),
shouldShowAddCorrelation: canWriteCorrelations && !isCorrelationsEditorMode && isLeftPane && numUniqueIds === 1,
panelsState,
};
}, [
canWriteCorrelations,
exploreId,
isCorrelationsEditorMode,
isLeftPane,
numUniqueIds,
panelsState,
queries,
queryResponse,
range.raw,
timeZone,
canWriteCorrelations,
isCorrelationsEditorMode,
isLeftPane,
numUniqueIds,
]);
}