From 19be779274a2a6b838e2272878ad48232656df90 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 19 Sep 2025 11:29:26 +0200 Subject: [PATCH] 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 fda27101efd92b9aa5c57e8b46557e12e8e1d7d2. * Update tests * Prettier --- .../logs/components/panel/LogList.test.tsx | 30 ++++++++++++++++++- .../logs/components/panel/LogList.tsx | 6 ++-- .../components/panel/LogListControls.test.tsx | 21 ++++++++++++- .../logs/components/panel/LogListControls.tsx | 9 +++--- .../logs/components/panel/processing.ts | 8 +++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 1416b7cf7d0..572ff363097 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -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(); + + 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 = {}) { return render( diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index d2e43f0512a..1986a9d2e57 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -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 (
- {showControls && } + {showControls && } {detailsMode === 'sidebar' && showDetails.length > 0 && ( { 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( @@ -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( + + + + ); + 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( diff --git a/public/app/features/logs/components/panel/LogListControls.tsx b/public/app/features/logs/components/panel/LogListControls.tsx index 18c905a02c9..0f7074c7972 100644 --- a/public/app/features/logs/components/panel/LogListControls.tsx +++ b/public/app/features/logs/components/panel/LogListControls.tsx @@ -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) => ( ), - [filterLevels, onFilterLevelClick, styles.menuItemActive] + [filterLevels, logLevels, onFilterLevelClick, styles.menuItemActive] ); const downloadMenu = useMemo( diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 0c8559d5f47..98a0cf6a00c 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -327,3 +327,11 @@ function countNewLines(log: string, limit = Infinity) { } return count; } + +export function getLevelsFromLogs(logs: LogListModel[]) { + const levels = new Set(); + for (const log of logs) { + levels.add(log.logLevel); + } + return Array.from(levels).filter((level) => level != null); +}