From 46258ac2c11d179583a032831230074613fb79aa Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 5 Sep 2025 19:43:34 +0200 Subject: [PATCH] Log Controls: Add dropdown menu for timestamp and line wrapping (#110698) * LogListControls: add dropdown menu for timestamps * LogListControls: add dropdown menu for line wrapping * Update styles * Translations * Update tests * Update test * LogListControls: all events --- .../logs/components/panel/LogList.test.tsx | 9 +- .../components/panel/LogListControls.test.tsx | 45 +++- .../logs/components/panel/LogListControls.tsx | 241 ++++++++++++------ public/locales/en-US/grafana.json | 15 +- 4 files changed, 215 insertions(+), 95 deletions(-) diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 4032aa2b9e4..1416b7cf7d0 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -359,17 +359,16 @@ describe('LogList', () => { render(); expect(screen.getByText('2025-08-06 03:35:19.504')).toBeInTheDocument(); - expect(screen.getByLabelText('Show nanosecond timestamps')).toBeInTheDocument(); - await userEvent.click(screen.getByLabelText('Show nanosecond timestamps')); + await userEvent.click(screen.getByLabelText('Log timestamps')); + await userEvent.click(screen.getByText('Show nanosecond timestamps')); expect(screen.getByText('2025-08-06 03:35:19.504133766')).toBeInTheDocument(); - expect(screen.getByLabelText('Hide timestamps')).toBeInTheDocument(); - await userEvent.click(screen.getByLabelText('Hide timestamps')); + await userEvent.click(screen.getByLabelText('Log timestamps')); + await userEvent.click(screen.getByText('Hide timestamps')); expect(screen.queryByText(/2025-08-06 03:35:19/)).not.toBeInTheDocument(); - expect(screen.getByLabelText('Show millisecond timestamps')).toBeInTheDocument(); }); }); describe('Interactions', () => { diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx index 40782778f79..d61bfbde5bb 100644 --- a/public/app/features/logs/components/panel/LogListControls.test.tsx +++ b/public/app/features/logs/components/panel/LogListControls.test.tsx @@ -227,22 +227,57 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Wrap lines')); + await userEvent.click(screen.getByLabelText('Log line wrapping')); + await userEvent.click(screen.getByText('Enable line wrapping')); expect(onLogOptionsChange).toHaveBeenCalledTimes(2); expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', true); expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', false); - await userEvent.click(screen.getByLabelText('Wrap lines and expand JSON')); - expect(onLogOptionsChange).toHaveBeenCalledTimes(3); + await userEvent.click(screen.getByLabelText('Log line wrapping')); + await userEvent.click(screen.getByText('Enable line wrapping and prettify JSON')); + + expect(onLogOptionsChange).toHaveBeenCalledTimes(4); expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', true); - await userEvent.click(screen.getByLabelText('Unwrap lines')); + await userEvent.click(screen.getByLabelText('Log line wrapping')); + await userEvent.click(screen.getByText('Disable line wrapping')); expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', false); expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', false); - expect(onLogOptionsChange).toHaveBeenCalledTimes(5); + expect(onLogOptionsChange).toHaveBeenCalledTimes(6); + + config.featureToggles.newLogsPanel = originalFlagState; + }); + + test('Controls line wrapping and prettify JSON', async () => { + const originalFlagState = config.featureToggles.newLogsPanel; + config.featureToggles.newLogsPanel = true; + + const onLogOptionsChange = jest.fn(); + render( + + + + ); + + await userEvent.click(screen.getByLabelText('Log timestamps')); + await userEvent.click(screen.getByText('Show millisecond timestamps')); + + expect(onLogOptionsChange).toHaveBeenCalledTimes(1); + expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', true); + + await userEvent.click(screen.getByLabelText('Log timestamps')); + await userEvent.click(screen.getByText('Show nanosecond timestamps')); + + expect(onLogOptionsChange).toHaveBeenCalledTimes(2); + + await userEvent.click(screen.getByLabelText('Log timestamps')); + await userEvent.click(screen.getByText('Hide timestamps')); + + expect(onLogOptionsChange).toHaveBeenCalledTimes(3); + expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', false); config.featureToggles.newLogsPanel = originalFlagState; }); diff --git a/public/app/features/logs/components/panel/LogListControls.tsx b/public/app/features/logs/components/panel/LogListControls.tsx index 372b135e48f..bb8ebd398f8 100644 --- a/public/app/features/logs/components/panel/LogListControls.tsx +++ b/public/app/features/logs/components/panel/LogListControls.tsx @@ -11,7 +11,6 @@ import { Dropdown, Icon, IconButton, Menu, Tooltip, useStyles2 } from '@grafana/ import { LogsVisualisationType } from '../../../explore/Logs/Logs'; import { DownloadFormat } from '../../utils'; -import { LogLineTimestampResolution } from './LogLine'; import { useLogListContext } from './LogListContext'; import { useLogListSearchContext } from './LogListSearchContext'; import { ScrollToLogsEvent } from './virtualization'; @@ -92,7 +91,9 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) const onFilterLevelClick = useCallback( (level?: LogLevel) => { - reportInteraction('logs_log_list_controls_level_clicked'); + reportInteraction('logs_log_list_controls_level_clicked', { + level, + }); if (level === undefined) { setFilterLevels([]); } else if (!filterLevels.includes(level)) { @@ -167,7 +168,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) className={dedupStrategy === option ? styles.menuItemActive : undefined} description={LogsDedupDescription[option]} label={capitalize(option)} - onClick={() => setDedupStrategy(option)} + onClick={() => { + setDedupStrategy(option); + reportInteraction('logs_log_list_controls_deduplication_clicked', { + option, + }); + }} /> ))} @@ -202,15 +208,30 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) downloadLogs(DownloadFormat.Text)} + onClick={() => { + downloadLogs(DownloadFormat.Text); + reportInteraction('logs_log_list_controls_downloaded_logs', { + format: DownloadFormat.Text, + }); + }} /> downloadLogs(DownloadFormat.Json)} + onClick={() => { + downloadLogs(DownloadFormat.Json); + reportInteraction('logs_log_list_controls_downloaded_logs', { + format: DownloadFormat.Json, + }); + }} /> downloadLogs(DownloadFormat.CSV)} + onClick={() => { + downloadLogs(DownloadFormat.CSV); + reportInteraction('logs_log_list_controls_downloaded_logs', { + format: DownloadFormat.CSV, + }); + }} /> ), @@ -464,40 +485,76 @@ const TimestampResolutionButton = () => { const styles = useStyles2(getStyles); const { setTimestampResolution, setShowTime, showTime, timestampResolution } = useLogListContext(); - const onShowTimestampsClick = useCallback(() => { - if (!config.featureToggles.newLogsPanel) { - reportInteraction('logs_log_list_controls_show_time_clicked', { - show_time: !showTime, - }); - setShowTime(!showTime); - return; - } - if (!showTime || timestampResolution === 'ns') { - setShowTime(!showTime); - } else if (timestampResolution === 'ms') { - setTimestampResolution('ns'); - } - }, [setShowTime, setTimestampResolution, showTime, timestampResolution]); + const hide = useCallback(() => { + setShowTime(false); + reportInteraction('logs_log_list_controls_show_time_clicked', { + show_time: false, + }); + }, [setShowTime]); + + const showMs = useCallback(() => { + setShowTime(true); + setTimestampResolution('ms'); + reportInteraction('logs_log_list_controls_show_time_clicked', { + show_time: false, + resolution: 'ms', + }); + }, [setShowTime, setTimestampResolution]); + + const showNs = useCallback(() => { + setShowTime(true); + setTimestampResolution('ns'); + reportInteraction('logs_log_list_controls_show_time_clicked', { + show_time: false, + resolution: 'ns', + }); + }, [setShowTime, setTimestampResolution]); + + const timestampMenu = useMemo( + () => ( + + + + + + ), + [hide, showMs, showNs, showTime, styles.menuItemActive, timestampResolution] + ); return ( - - - + +
+ + + +
+
); }; @@ -515,34 +572,72 @@ const WrapLogMessageButton = () => { * Line wrapping also controls JSON formatting, because with line wrapping disabled, * JSON formatting has no effect, so one is related with the other. */ - const onWrapLogMessageClick = useCallback(() => { - if (!wrapLogMessage) { - setWrapLogMessage(true); - setPrettifyJSON(false); - } else if (!prettifyJSON) { - setPrettifyJSON(true); - } else { - setWrapLogMessage(false); - setPrettifyJSON(false); - } + const disable = useCallback(() => { + setWrapLogMessage(false); + setPrettifyJSON(false); reportInteraction('logs_log_list_controls_wrap_clicked', { - state: !wrapLogMessage, + state: false, + prettify: false, }); - }, [prettifyJSON, setPrettifyJSON, setWrapLogMessage, wrapLogMessage]); + }, [setPrettifyJSON, setWrapLogMessage]); + + const wrap = useCallback(() => { + setWrapLogMessage(true); + setPrettifyJSON(false); + reportInteraction('logs_log_list_controls_wrap_clicked', { + state: true, + prettify: false, + }); + }, [setPrettifyJSON, setWrapLogMessage]); + + const wrapAndPrettify = useCallback(() => { + setWrapLogMessage(true); + setPrettifyJSON(true); + reportInteraction('logs_log_list_controls_wrap_clicked', { + state: true, + prettify: true, + }); + }, [setPrettifyJSON, setWrapLogMessage]); + + const wrappingMenu = useMemo( + () => ( + + + + + + ), + [disable, prettifyJSON, styles.menuItemActive, wrap, wrapAndPrettify, wrapLogMessage] + ); return ( - - - + +
+ + + +
+
); }; @@ -611,12 +706,13 @@ const getStyles = (theme: GrafanaTheme2) => { margin: 0, boxShadow: 'none', border: 'none', - display: 'inline-flex', + display: 'flex', background: 'transparent', justifyContent: 'center', alignItems: 'center', padding: 0, overflow: 'visible', + width: '100%', }), customControlIcon: css({ verticalAlign: 'baseline', @@ -633,22 +729,3 @@ const getStyles = (theme: GrafanaTheme2) => { }), }; }; - -function getTimestampTooltip(showTime: boolean, timestampResolution: LogLineTimestampResolution) { - if (!showTime) { - return t('logs.logs-controls.show-ms-timestamps', 'Show millisecond timestamps'); - } - if (timestampResolution === 'ms') { - return t('logs.logs-controls.show-ns-timestamps', 'Show nanosecond timestamps'); - } - return t('logs.logs-controls.hide-timestamps', 'Hide timestamps'); -} - -function getWrapLogMessageTooltip(wrapLogMessage: boolean, prettifyJSON: boolean | undefined) { - if (!wrapLogMessage) { - return t('logs.logs-controls.wrap-lines', 'Wrap lines'); - } - return prettifyJSON - ? t('logs.logs-controls.unwrap-lines', 'Unwrap lines') - : t('logs.logs-controls.wrap-json-lines', 'Wrap lines and expand JSON'); -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 013c0255105..46459e68f27 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9644,6 +9644,12 @@ "hide-search": "Close search", "hide-timestamps": "Hide timestamps", "hide-unique-labels": "Hide unique labels", + "line-wrapping": { + "enable": "Enable line wrapping", + "enable-prettify": "Enable line wrapping and prettify JSON", + "hide": "Disable line wrapping", + "label": "Log line wrapping" + }, "newest-first": "Sorted by newest logs first - Click to show oldest first", "oldest-first": "Sorted by oldest logs first - Click to show newest first", "prettify-json": "Expand JSON logs", @@ -9652,13 +9658,16 @@ "resolution-ns": "ns", "scroll-bottom": "Scroll to bottom", "scroll-top": "Scroll to top", - "show-ms-timestamps": "Show millisecond timestamps", - "show-ns-timestamps": "Show nanosecond timestamps", "show-search": "Search in logs result", "show-timestamps": "Show timestamps", "show-unique-labels": "Show unique labels", + "timestamp": { + "hide": "Hide timestamps", + "label": "Log timestamps", + "milliseconds": "Show millisecond timestamps", + "nanoseconds": "Show nanosecond timestamps" + }, "unwrap-lines": "Unwrap lines", - "wrap-json-lines": "Wrap lines and expand JSON", "wrap-lines": "Wrap lines" }, "logs-navigation": {