Log Controls: Show level filter based on levels from the logs results (#111264)

* LogListControls: add logLevels prop

* LogListControls: show levels from displayed logs

* LogListSearch: fix tooltips

* Revert "LogListSearch: fix tooltips"

This reverts commit fda27101ef.

* Update tests

* Prettier
This commit is contained in:
Matias Chomicki
2025-09-19 11:29:26 +02:00
committed by GitHub
parent c012a95fa1
commit 19be779274
5 changed files with 66 additions and 8 deletions
@@ -1,7 +1,15 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data';
import {
CoreApp,
getDefaultTimeRange,
LogLevel,
LogRowModel,
LogsDedupStrategy,
LogsSortOrder,
store,
} from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils';
@@ -195,6 +203,26 @@ describe('LogList', () => {
spy.mockRestore();
});
test('Shows controls with level filters based on the displayed logs', async () => {
logs = [createLogRow({ uid: '1', logLevel: LogLevel.info }), createLogRow({ uid: '2', logLevel: LogLevel.debug })];
render(<LogList {...defaultProps} showControls logs={logs} />);
expect(screen.getByText('info')).toBeInTheDocument();
expect(screen.getByText('debug')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Filter levels'));
expect(await screen.findByText('All levels')).toBeVisible();
expect(screen.getByText('Info')).toBeVisible();
expect(screen.getByText('Debug')).toBeVisible();
await userEvent.click(screen.getByText('Debug'));
expect(screen.queryByText('info')).not.toBeInTheDocument();
expect(screen.getByText('debug')).toBeInTheDocument();
});
describe('Popover menu', () => {
function setup(overrides: Partial<Props> = {}) {
return render(
@@ -31,7 +31,7 @@ import { LogListContextProvider, LogListState, useLogListContext } from './LogLi
import { LogListControls } from './LogListControls';
import { LOG_LIST_SEARCH_HEIGHT, LogListSearch } from './LogListSearch';
import { LogListSearchContextProvider, useLogListSearchContext } from './LogListSearchContext';
import { preProcessLogs, LogListModel } from './processing';
import { preProcessLogs, LogListModel, getLevelsFromLogs } from './processing';
import { useKeyBindings } from './useKeyBindings';
import { usePopoverMenu } from './usePopoverMenu';
import { LogLineVirtualization, getLogLineSize, LogFieldDimension, ScrollToLogsEvent } from './virtualization';
@@ -417,6 +417,8 @@ const LogListComponent = ({
[debouncedScrollToItem, filteredLogs]
);
const logLevels = useMemo(() => getLevelsFromLogs(processedLogs), [processedLogs]);
if (!containerElement || listHeight == null) {
// Wait for container to be rendered
return null;
@@ -424,7 +426,7 @@ const LogListComponent = ({
return (
<div className={styles.logListContainer}>
{showControls && <LogListControls eventBus={eventBus} />}
{showControls && <LogListControls logLevels={logLevels} eventBus={eventBus} />}
{detailsMode === 'sidebar' && showDetails.length > 0 && (
<LogLineDetails
containerElement={containerElement}
@@ -239,7 +239,7 @@ describe('LogListControls', () => {
expect(onLogOptionsChange).toHaveBeenCalledWith('dedupStrategy', LogsDedupStrategy.numbers);
});
test('Sets level filters', async () => {
test('Sets all level filters if not provided', async () => {
const onLogOptionsChange = jest.fn();
render(
<LogListContextProvider {...contextProps} onLogOptionsChange={onLogOptionsChange}>
@@ -258,6 +258,25 @@ describe('LogListControls', () => {
expect(onLogOptionsChange).toHaveBeenCalledWith('filterLevels', ['error']);
});
test('Sets all level filters from provided levels', async () => {
const onLogOptionsChange = jest.fn();
render(
<LogListContextProvider {...contextProps} onLogOptionsChange={onLogOptionsChange}>
<LogListControls eventBus={new EventBusSrv()} logLevels={[LogLevel.critical, LogLevel.information]} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY));
expect(await screen.findByText('All levels')).toBeVisible();
expect(screen.getByText('Info')).toBeVisible();
expect(screen.queryByText('Debug')).not.toBeInTheDocument();
expect(screen.queryByText('Trace')).not.toBeInTheDocument();
expect(screen.queryByText('Warning')).not.toBeInTheDocument();
expect(screen.queryByText('Error')).not.toBeInTheDocument();
expect(screen.getByText('Critical')).toBeVisible();
await userEvent.click(screen.getByText('Critical'));
expect(onLogOptionsChange).toHaveBeenCalledWith('filterLevels', ['critical']);
});
test('Controls timestamp visibility', async () => {
const onLogOptionsChange = jest.fn();
render(
@@ -27,6 +27,7 @@ import { ScrollToLogsEvent } from './virtualization';
type Props = {
eventBus: EventBus;
visualisationType?: LogsVisualisationType;
logLevels?: LogLevel[];
};
const DEDUP_OPTIONS = [
@@ -46,7 +47,7 @@ const FILTER_LEVELS: LogLevel[] = [
LogLevel.unknown,
];
export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) => {
export const LogListControls = ({ eventBus, logLevels = FILTER_LEVELS, visualisationType = 'logs' }: Props) => {
const {
app,
controlsExpanded,
@@ -56,6 +57,7 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
fontSize,
forceEscape,
hasUnescapedContent,
logOptionsStorageKey,
prettifyJSON,
setControlsExpanded,
setDedupStrategy,
@@ -73,7 +75,6 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
sortOrder,
syntaxHighlighting,
wrapLogMessage,
logOptionsStorageKey,
} = useLogListContext();
const { hideSearch, searchVisible, showSearch } = useLogListSearchContext();
@@ -209,7 +210,7 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
label={t('logs.logs-controls.display-level-all', 'All levels')}
onClick={() => onFilterLevelClick()}
/>
{FILTER_LEVELS.map((level) => (
{logLevels.map((level) => (
<Menu.Item
key={level}
className={filterLevels.includes(level) ? styles.menuItemActive : undefined}
@@ -219,7 +220,7 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
))}
</Menu>
),
[filterLevels, onFilterLevelClick, styles.menuItemActive]
[filterLevels, logLevels, onFilterLevelClick, styles.menuItemActive]
);
const downloadMenu = useMemo(
@@ -327,3 +327,11 @@ function countNewLines(log: string, limit = Infinity) {
}
return count;
}
export function getLevelsFromLogs(logs: LogListModel[]) {
const levels = new Set<LogLevel>();
for (const log of logs) {
levels.add(log.logLevel);
}
return Array.from(levels).filter((level) => level != null);
}