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:
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user