diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 3b04394faf8..f2950a31887 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -1011,6 +1011,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { hasData && (
{ - const { sortOrder } = useLogListContext(); + const { sortOrder, controlsExpanded } = useLogListContext(); const eventBus = useMemo(() => new EventBusSrv(), []); + const ref = useRef(null); const theme = useTheme2(); const styles = getStyles(theme); @@ -37,8 +38,11 @@ export const ControlledLogsTable = ({ return; } + const tableWidthExpandedControls = width - (CONTROLS_WIDTH_EXPANDED + 12); + const tableWidth = width - (CONTROLS_WIDTH + 12); + return ( -
+
{/* Width should be full width minus logs navigation and padding */} @@ -47,7 +51,7 @@ export const ControlledLogsTable = ({ range={range} splitOpen={splitOpen} timeZone={rest.timeZone} - width={width - 45} + width={controlsExpanded ? tableWidthExpandedControls : tableWidth} logsFrames={logsTableFrames ?? []} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index dda8fed0c4b..244ce97fcf0 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -40,6 +40,7 @@ import { getScrollbarWidth, LOG_LIST_CONTROLS_WIDTH, LOG_LIST_MIN_WIDTH } from ' export interface LogListContextData extends Omit { closeDetails: () => void; + controlsExpanded: boolean; detailsDisplayed: (log: LogListModel) => boolean; detailsMode: LogLineDetailsMode; detailsWidth: number; @@ -51,6 +52,7 @@ export interface LogListContextData extends Omit void; setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void; setDetailsMode: (mode: LogLineDetailsMode) => void; setDetailsWidth: (width: number) => void; @@ -76,6 +78,7 @@ export interface LogListContextData extends Omit({ app: CoreApp.Unknown, closeDetails: () => {}, + controlsExpanded: false, dedupStrategy: LogsDedupStrategy.none, detailsDisplayed: () => false, detailsMode: 'sidebar', @@ -88,6 +91,7 @@ export const LogListContext = createContext({ fontSize: 'default', hasUnescapedContent: false, noInteractions: false, + setControlsExpanded: () => {}, setDedupStrategy: () => {}, setDetailsMode: () => {}, setDetailsWidth: () => {}, @@ -393,6 +397,13 @@ export const LogListContextProvider = ({ })); }, [timestampResolution]); + const controlsExpandedFromStore = store.getBool( + `${logOptionsStorageKey}.controlsExpanded`, + getDefaultControlsExpandedMode(containerElement ?? null) + ); + // If the user has a large viewport, show the expanded state by default + const [controlsExpanded, setControlsExpanded] = useState(controlsExpandedFromStore); + const detailsDisplayed = useCallback( (log: LogListModel) => !!showDetails.find((shownLog) => shownLog.uid === log.uid), [showDetails] @@ -594,6 +605,7 @@ export const LogListContextProvider = ({ value={{ app, closeDetails, + controlsExpanded, detailsDisplayed, dedupStrategy: logListState.dedupStrategy, detailsMode, @@ -628,6 +640,7 @@ export const LogListContextProvider = ({ pinLineButtonTooltipTitle, pinnedLogs: logListState.pinnedLogs, prettifyJSON, + setControlsExpanded, setDedupStrategy, setDetailsMode, setDetailsWidth, @@ -754,3 +767,8 @@ export function getDefaultDetailsMode(container: HTMLDivElement | undefined): Lo const width = container?.clientWidth ?? window.innerWidth; return width > 1440 ? 'sidebar' : 'inline'; } + +export function getDefaultControlsExpandedMode(container: HTMLDivElement | null): boolean { + const width = container?.clientWidth ?? window.innerWidth; + return width > 1200; +} diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx index d61bfbde5bb..a0e32c09ccf 100644 --- a/public/app/features/logs/components/panel/LogListControls.test.tsx +++ b/public/app/features/logs/components/panel/LogListControls.test.tsx @@ -12,6 +12,35 @@ import { LogListContextProvider } from './LogListContext'; import { LogListControls } from './LogListControls'; import { ScrollToLogsEvent } from './virtualization'; +const FILTER_LEVELS_LABEL_COPY = 'Filter levels'; +const SCROLL_BOTTOM_LABEL_COPY = 'Scroll to bottom'; +const SCROLL_TOP_LABEL_COPY = 'Scroll to top'; +const OLDEST_LOGS_LABEL_COPY = 'Oldest logs first'; +const DEDUPE_LABEL_COPY = 'Deduplication'; +const SHOW_TIMESTAMP_LABEL_COPY = 'Show timestamps'; +const WRAP_LINES_LABEL_COPY = 'Wrap lines'; +const WRAP_JSON_TOOLTIP_COPY = 'Enable line wrapping and prettify JSON'; +const WRAP_JSON_LABEL_COPY = 'Wrap JSON'; +const WRAP_DISABLE_LABEL_COPY = 'Disable line wrapping'; +const ENABLE_HIGHLIGHTING_LABEL_COPY = 'Enable highlighting'; +const EXPANDED_LABEL_COPY = 'Expanded'; +const COLLAPSED_LABEL_COPY = 'Collapsed'; +const SHOW_UNIQUE_LABELS_LABEL_COPY = 'Show unique labels'; +const HIDE_UNIQUE_LABELS_LABEL_COPY = 'Hide unique labels'; +const EXPAND_JSON_LOGS_LABEL_COPY = 'Expand JSON logs'; +const COLLAPSE_JSON_LOGS_LABEL_COPY = 'Collapse JSON logs'; +const ESCAPE_NEWLINES_TOOLTIP_COPY = 'Fix incorrectly escaped newline and tab sequences in log lines'; +const REMOVE_ESCAPE_NEWLINES_LABEL_COPY = 'Remove escaping'; +const TIMESTAMP_LABEL_COPY = 'Log timestamps'; +const TIMESTAMP_HIDE_LABEL_COPY = 'Hide timestamps'; +const FONT_SIZE_LARGE_LABEL_COPY = 'Large font'; +const FONT_SIZE_LARGE_TOOLTIP_COPY = 'Set large font'; +const FONT_SIZE_SMALL_LABEL_COPY = 'Small font'; +const FONT_SIZE_SMALL_TOOLTIP_COPY = 'Set small font'; +const DOWNLOAD_LOGS_LABEL_COPY = 'Download logs'; + +const OLDEST_LOGS_LABEL_REGEX = /oldest logs first/; + jest.mock('../../utils', () => ({ ...jest.requireActual('../../utils'), downloadLogs: jest.fn(), @@ -42,6 +71,13 @@ const contextProps = { openAssistantByLog: () => {}, }; +const assertExpandedOptionsCopyVisible = () => { + expect(screen.getByText(EXPANDED_LABEL_COPY)).toBeVisible(); + expect(screen.getByText(SCROLL_BOTTOM_LABEL_COPY)).toBeVisible(); + expect(screen.getByText(OLDEST_LOGS_LABEL_COPY)).toBeVisible(); + expect(screen.getByText(DEDUPE_LABEL_COPY)).toBeVisible(); + expect(screen.getByText(SCROLL_TOP_LABEL_COPY)).toBeVisible(); +}; describe('LogListControls', () => { test('Renders without errors', () => { render( @@ -49,20 +85,18 @@ describe('LogListControls', () => { ); - expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument(); - expect(screen.getByLabelText(/oldest logs first/)).toBeInTheDocument(); - expect(screen.getByLabelText('Deduplication')).toBeInTheDocument(); - expect(screen.getByLabelText('Display levels')).toBeInTheDocument(); - expect(screen.getByLabelText('Show timestamps')).toBeInTheDocument(); - expect(screen.getByLabelText('Wrap lines')).toBeInTheDocument(); - expect(screen.getByLabelText('Enable highlighting')).toBeInTheDocument(); - expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument(); - expect(screen.queryByLabelText('Show unique labels')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Expand JSON logs')).not.toBeInTheDocument(); - expect( - screen.queryByLabelText('Fix incorrectly escaped newline and tab sequences in log lines') - ).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Remove escaping')).not.toBeInTheDocument(); + expect(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX)).toBeInTheDocument(); + expect(screen.getByLabelText(DEDUPE_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(WRAP_LINES_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)).toBeInTheDocument(); + expect(screen.queryByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(ESCAPE_NEWLINES_TOOLTIP_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(REMOVE_ESCAPE_NEWLINES_LABEL_COPY)).not.toBeInTheDocument(); }); test('Renders legacy controls', () => { @@ -71,8 +105,8 @@ describe('LogListControls', () => { ); - expect(screen.getByLabelText('Show unique labels')).toBeInTheDocument(); - expect(screen.getByLabelText('Expand JSON logs')).toBeInTheDocument(); + expect(screen.getByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)).toBeInTheDocument(); }); test.each([CoreApp.Dashboard, CoreApp.PanelEditor, CoreApp.PanelViewer])( @@ -83,14 +117,14 @@ describe('LogListControls', () => { ); - expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument(); - expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument(); - expect(screen.getByLabelText('Display levels')).toBeInTheDocument(); - expect(screen.queryByLabelText(/oldest logs first/)).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Deduplication')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Show timestamps')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Wrap lines')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Enable highlighting')).not.toBeInTheDocument(); + expect(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)).toBeInTheDocument(); + expect(screen.queryByLabelText(OLDEST_LOGS_LABEL_REGEX)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(DEDUPE_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(SHOW_TIMESTAMP_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(WRAP_LINES_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)).not.toBeInTheDocument(); } ); @@ -100,20 +134,18 @@ describe('LogListControls', () => { ); - expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument(); - expect(screen.getByLabelText(/oldest logs first/)).toBeInTheDocument(); - expect(screen.getByLabelText('Deduplication')).toBeInTheDocument(); - expect(screen.getByLabelText('Display levels')).toBeInTheDocument(); - expect(screen.getByLabelText('Show timestamps')).toBeInTheDocument(); - expect(screen.getByLabelText('Wrap lines')).toBeInTheDocument(); - expect(screen.getByLabelText('Enable highlighting')).toBeInTheDocument(); - expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument(); - expect(screen.queryByLabelText('Show unique labels')).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Expand JSON logs')).not.toBeInTheDocument(); - expect( - screen.queryByLabelText('Fix incorrectly escaped newline and tab sequences in log lines') - ).not.toBeInTheDocument(); - expect(screen.queryByLabelText('Remove escaping')).not.toBeInTheDocument(); + expect(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX)).toBeInTheDocument(); + expect(screen.getByLabelText(DEDUPE_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(WRAP_LINES_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)).toBeInTheDocument(); + expect(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)).toBeInTheDocument(); + expect(screen.queryByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(ESCAPE_NEWLINES_TOOLTIP_COPY)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(REMOVE_ESCAPE_NEWLINES_LABEL_COPY)).not.toBeInTheDocument(); }); test('Allows to scroll', async () => { @@ -124,8 +156,8 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Scroll to bottom')); - await userEvent.click(screen.getByLabelText('Scroll to top')); + await userEvent.click(screen.getByLabelText(SCROLL_BOTTOM_LABEL_COPY)); + await userEvent.click(screen.getByLabelText(SCROLL_TOP_LABEL_COPY)); expect(eventBus.publish).toHaveBeenCalledTimes(2); expect(eventBus.publish).toHaveBeenCalledWith( new ScrollToLogsEvent({ @@ -139,6 +171,45 @@ describe('LogListControls', () => { ); }); + test('Expands options', async () => { + render( + + + + ); + // Initial state should be collapsed + expect(screen.getByLabelText(COLLAPSED_LABEL_COPY)).toBeVisible(); + // Expanded label should not be visible + expect(screen.queryByText(EXPANDED_LABEL_COPY)).not.toBeInTheDocument(); + // Expand options + await userEvent.click(screen.getByLabelText(COLLAPSED_LABEL_COPY)); + // Verify that the label (state) is not collapsed + expect(screen.queryByLabelText(COLLAPSED_LABEL_COPY)).not.toBeInTheDocument(); + expect(screen.getByLabelText(EXPANDED_LABEL_COPY)).toBeVisible(); + // Verify the expanded labels are rendered + assertExpandedOptionsCopyVisible(); + }); + + test('Expands options shown by default with container width > 1200', async () => { + const div = document.createElement('div'); + const divSpy = jest.spyOn(div, 'clientWidth', 'get'); + //@ts-expect-error + divSpy['clientWidth'] = 1201; + render( + //@ts-expect-error + + + + ); + + // Verify the expanded labels are rendered + assertExpandedOptionsCopyVisible(); + // Collapse options + await userEvent.click(screen.getByLabelText(EXPANDED_LABEL_COPY)); + // State should be collapsed + expect(screen.getByLabelText(COLLAPSED_LABEL_COPY)).toBeVisible(); + }); + test('Controls sort order', async () => { const onLogOptionsChange = jest.fn(); render( @@ -150,7 +221,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText(/oldest logs first/)); + await userEvent.click(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('sortOrder', LogsSortOrder.Descending); }); @@ -162,7 +233,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Deduplication')); + await userEvent.click(screen.getByLabelText(DEDUPE_LABEL_COPY)); await userEvent.click(screen.getByText('Numbers')); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('dedupStrategy', LogsDedupStrategy.numbers); @@ -175,7 +246,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Display levels')); + await userEvent.click(screen.getByLabelText(FILTER_LEVELS_LABEL_COPY)); expect(await screen.findByText('All levels')).toBeVisible(); expect(screen.getByText('Info')).toBeVisible(); expect(screen.getByText('Debug')).toBeVisible(); @@ -194,7 +265,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Show timestamps')); + await userEvent.click(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', true); }); @@ -206,7 +277,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Wrap lines')); + await userEvent.click(screen.getByLabelText(WRAP_LINES_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', true); }); @@ -227,21 +298,21 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Log line wrapping')); + await userEvent.click(screen.getByLabelText('Wrap disabled')); 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('Log line wrapping')); - await userEvent.click(screen.getByText('Enable line wrapping and prettify JSON')); + await userEvent.click(screen.getByLabelText(WRAP_LINES_LABEL_COPY)); + await userEvent.click(screen.getByText(WRAP_JSON_TOOLTIP_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(4); expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', true); - await userEvent.click(screen.getByLabelText('Log line wrapping')); - await userEvent.click(screen.getByText('Disable line wrapping')); + await userEvent.click(screen.getByLabelText(WRAP_JSON_LABEL_COPY)); + await userEvent.click(screen.getByText(WRAP_DISABLE_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', false); expect(onLogOptionsChange).toHaveBeenCalledWith('prettifyJSON', false); @@ -262,19 +333,19 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Log timestamps')); + await userEvent.click(screen.getByLabelText(TIMESTAMP_LABEL_COPY)); 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.getByLabelText(TIMESTAMP_LABEL_COPY)); 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')); + await userEvent.click(screen.getByLabelText(TIMESTAMP_LABEL_COPY)); + await userEvent.click(screen.getByText(TIMESTAMP_HIDE_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(3); expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', false); @@ -289,7 +360,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Enable highlighting')); + await userEvent.click(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('syntaxHighlighting', true); }); @@ -300,13 +371,13 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Show unique labels')); + await userEvent.click(screen.getByLabelText(SHOW_UNIQUE_LABELS_LABEL_COPY)); rerender( ); - expect(screen.getByLabelText('Hide unique labels')); + expect(screen.getByLabelText(HIDE_UNIQUE_LABELS_LABEL_COPY)); }); test('Controls Expand JSON logs', async () => { @@ -315,13 +386,13 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Expand JSON logs')); + await userEvent.click(screen.getByLabelText(EXPAND_JSON_LOGS_LABEL_COPY)); rerender( ); - expect(screen.getByLabelText('Collapse JSON logs')); + expect(screen.getByLabelText(COLLAPSE_JSON_LOGS_LABEL_COPY)); }); test('Controls font size', async () => { @@ -333,11 +404,11 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Use small font size')); - await screen.findByLabelText('Use default font size'); + await userEvent.click(screen.getByLabelText(FONT_SIZE_LARGE_LABEL_COPY)); + await screen.findByLabelText(FONT_SIZE_LARGE_TOOLTIP_COPY); - await userEvent.click(screen.getByLabelText('Use default font size')); - await screen.findByLabelText('Use small font size'); + await userEvent.click(screen.getByLabelText(FONT_SIZE_SMALL_LABEL_COPY)); + await screen.findByLabelText(FONT_SIZE_SMALL_TOOLTIP_COPY); config.featureToggles.newLogsPanel = originalValue; }); @@ -353,7 +424,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Download logs')); + await userEvent.click(screen.getByLabelText(DOWNLOAD_LOGS_LABEL_COPY)); await userEvent.click(await screen.findByText(label)); expect(downloadLogs).toHaveBeenCalledTimes(1); expect(downloadLogs).toHaveBeenCalledWith(format, [], undefined); @@ -371,7 +442,7 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Download logs')); + await userEvent.click(screen.getByLabelText(DOWNLOAD_LOGS_LABEL_COPY)); await userEvent.click(await screen.findByText('txt')); expect(downloadLogs).toHaveBeenCalledWith('text', filteredLogs, undefined); }); @@ -383,12 +454,12 @@ describe('LogListControls', () => { ); - await userEvent.click(screen.getByLabelText('Fix incorrectly escaped newline and tab sequences in log lines')); + await userEvent.click(screen.getByLabelText(ESCAPE_NEWLINES_TOOLTIP_COPY)); rerender( ); - await userEvent.click(screen.getByLabelText('Remove escaping')); + await userEvent.click(screen.getByLabelText(REMOVE_ESCAPE_NEWLINES_LABEL_COPY)); }); }); diff --git a/public/app/features/logs/components/panel/LogListControls.tsx b/public/app/features/logs/components/panel/LogListControls.tsx index bb8ebd398f8..8446fd13ed8 100644 --- a/public/app/features/logs/components/panel/LogListControls.tsx +++ b/public/app/features/logs/components/panel/LogListControls.tsx @@ -1,17 +1,26 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { capitalize } from 'lodash'; import { MouseEvent, useCallback, useMemo } from 'react'; -import { CoreApp, EventBus, LogLevel, LogsDedupDescription, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; +import { + CoreApp, + EventBus, + LogLevel, + LogsDedupDescription, + LogsDedupStrategy, + LogsSortOrder, + store, +} from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data/'; import { t } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; -import { Dropdown, Icon, IconButton, Menu, Tooltip, useStyles2 } from '@grafana/ui'; +import { Dropdown, Menu, useStyles2 } from '@grafana/ui'; import { LogsVisualisationType } from '../../../explore/Logs/Logs'; import { DownloadFormat } from '../../utils'; import { useLogListContext } from './LogListContext'; +import { LogListControlsOption, LogListControlsSelectOption } from './LogListControlsOption'; import { useLogListSearchContext } from './LogListSearchContext'; import { ScrollToLogsEvent } from './virtualization'; @@ -38,9 +47,9 @@ const FILTER_LEVELS: LogLevel[] = [ ]; export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) => { - const styles = useStyles2(getStyles); const { app, + controlsExpanded, dedupStrategy, downloadLogs, filterLevels, @@ -48,6 +57,7 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) forceEscape, hasUnescapedContent, prettifyJSON, + setControlsExpanded, setDedupStrategy, setFilterLevels, setFontSize, @@ -63,9 +73,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) sortOrder, syntaxHighlighting, wrapLogMessage, + logOptionsStorageKey, } = useLogListContext(); const { hideSearch, searchVisible, showSearch } = useLogListSearchContext(); + const styles = useStyles2(getStyles, controlsExpanded); + const onScrollToTopClick = useCallback(() => { reportInteraction('logs_log_list_controls_scroll_top_clicked'); eventBus.publish( @@ -84,6 +97,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) ); }, [eventBus]); + const onExpandControlsClick = useCallback(() => { + reportInteraction('logs_log_list_controls_expand_controls_clicked'); + setControlsExpanded(!controlsExpanded); + store.set(`${logOptionsStorageKey}.controlsExpanded`, !controlsExpanded); + }, [controlsExpanded, logOptionsStorageKey, setControlsExpanded]); + const onForceEscapeClick = useCallback(() => { reportInteraction('logs_log_list_controls_force_escape_clicked'); setForceEscape(!forceEscape); @@ -242,22 +261,47 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) return (
- {visualisationType === 'logs' && ( - + - )} + {visualisationType === 'logs' && ( + + )} + {!inDashboard ? ( <> -
{config.featureToggles.newLogsPanel && ( - )} - - 0 ? styles.controlButtonActive : styles.controlButton } - tooltip={t('logs.logs-controls.display-level', 'Display levels')} + label={t('logs.logs-controls.filter-levels', 'Filter levels')} + tooltip={t('logs.logs-controls.tooltip.filter-level', 'Filter logs result by level')} size="lg" />
{config.featureToggles.newLogsPanel ? ( - + ) : ( - )} {config.featureToggles.newLogsPanel ? ( - + ) : ( - )} {prettifyJSON !== undefined && !config.featureToggles.newLogsPanel && ( - )} {syntaxHighlighting !== undefined && ( - )} {config.featureToggles.newLogsPanel && ( - )} {hasUnescapedContent && ( -
- @@ -427,10 +504,16 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) ) : ( <> {config.featureToggles.newLogsPanel && ( - )} - 0 ? styles.controlButtonActive : styles.controlButton} - tooltip={t('logs.logs-controls.display-level', 'Display levels')} + label={t('logs.logs-controls.filter-levels', 'Filter levels')} + tooltip={t('logs.logs-controls.tooltip.filter-level', 'Filter logs result by level')} size="lg" /> {visualisationType === 'logs' && hasUnescapedContent && ( - )} {visualisationType === 'logs' && ( - { - const styles = useStyles2(getStyles); +interface LogSelectOptionProps { + expanded: boolean; +} + +const TimestampResolutionButton = ({ expanded }: LogSelectOptionProps) => { + const styles = useStyles2(getWrapButtonStyles, expanded); const { setTimestampResolution, setShowTime, showTime, timestampResolution } = useLogListContext(); const hide = useCallback(() => { @@ -533,33 +630,32 @@ const TimestampResolutionButton = () => { [hide, showMs, showNs, showTime, styles.menuItemActive, timestampResolution] ); + const labelText = !showTime + ? t('logs.logs-controls.timestamp.label-hide', 'Hide timestamps') + : timestampResolution === 'ms' + ? t('logs.logs-controls.timestamp.label-ms', 'Display ms') + : t('logs.logs-controls.timestamp.label-ns', 'Display ns'); + + const customTagText = + timestampResolution === 'ms' + ? t('logs.logs-controls.resolution-ms', 'ms') + : t('logs.logs-controls.resolution-ns', 'ns'); + return ( - -
- - - -
-
+ ); }; - -const WrapLogMessageButton = () => { - const styles = useStyles2(getStyles); +const WrapLogMessageButton = ({ expanded }: LogSelectOptionProps) => { + const styles = useStyles2(getWrapButtonStyles, expanded); const { prettifyJSON, setPrettifyJSON, setWrapLogMessage, wrapLogMessage } = useLogListContext(); /** @@ -622,39 +718,62 @@ const WrapLogMessageButton = () => { [disable, prettifyJSON, styles.menuItemActive, wrap, wrapAndPrettify, wrapLogMessage] ); + const wrapStateText = !wrapLogMessage + ? t('logs.logs-controls.line-wrapping.state.hide', 'Wrap disabled') + : wrapLogMessage && !prettifyJSON + ? t('logs.logs-controls.line-wrapping.state.wrap', 'Wrap lines') + : t('logs.logs-controls.line-wrapping.state.json', 'Wrap JSON'); + + const tooltip = t('logs.logs-controls.line-wrapping.tooltip', 'Set line wrap'); + return ( - -
- - - -
-
+ ); }; -const getStyles = (theme: GrafanaTheme2) => { +const getWrapButtonStyles = (theme: GrafanaTheme2, expanded: boolean) => { + return { + menuItemActive: css({ + '&:before': { + content: '""', + position: 'absolute', + left: 0, + top: theme.spacing(0.5), + height: `calc(100% - ${theme.spacing(1)})`, + width: '2px', + backgroundColor: theme.colors.warning.main, + }, + }), + }; +}; + +export const CONTROLS_WIDTH = 35; +export const CONTROLS_WIDTH_EXPANDED = 176; + +const getStyles = (theme: GrafanaTheme2, controlsExpanded: boolean) => { return { navContainer: css({ maxHeight: '100%', display: 'flex', + flex: '1 0 auto', gap: theme.spacing(3), flexDirection: 'column', justifyContent: 'flex-start', - width: theme.spacing(4), + width: controlsExpanded ? CONTROLS_WIDTH_EXPANDED : CONTROLS_WIDTH, paddingTop: theme.spacing(0.75), paddingLeft: theme.spacing(1), borderLeft: `solid 1px ${theme.colors.border.medium}`, - overflow: 'hidden', minWidth: theme.spacing(4), + backgroundColor: theme.colors.background.primary, }), scrollToTopButton: css({ margin: 0, @@ -662,6 +781,9 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.text.secondary, height: theme.spacing(2), }), + controlsExpandedButton: css({ + transform: !controlsExpanded ? 'rotate(180deg)' : '', + }), controlButton: css({ margin: 0, color: theme.colors.text.secondary, @@ -685,7 +807,7 @@ const getStyles = (theme: GrafanaTheme2) => { borderRadius: theme.shape.radius.default, bottom: theme.spacing(-1), backgroundImage: theme.colors.gradients.brandHorizontal, - width: '95%', + width: theme.spacing(2.25), opacity: 1, }, }), @@ -700,32 +822,5 @@ const getStyles = (theme: GrafanaTheme2) => { backgroundColor: theme.colors.warning.main, }, }), - customControlButton: css({ - position: 'relative', - zIndex: 0, - margin: 0, - boxShadow: 'none', - border: 'none', - display: 'flex', - background: 'transparent', - justifyContent: 'center', - alignItems: 'center', - padding: 0, - overflow: 'visible', - width: '100%', - }), - customControlIcon: css({ - verticalAlign: 'baseline', - }), - customControlTag: css({ - color: theme.colors.primary.text, - fontSize: 10, - position: 'absolute', - bottom: -4, - right: 1, - lineHeight: '10px', - backgroundColor: theme.colors.background.primary, - paddingLeft: 2, - }), }; }; diff --git a/public/app/features/logs/components/panel/LogListControlsOption.tsx b/public/app/features/logs/components/panel/LogListControlsOption.tsx new file mode 100644 index 00000000000..2c3e6738c1e --- /dev/null +++ b/public/app/features/logs/components/panel/LogListControlsOption.tsx @@ -0,0 +1,211 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Dropdown, Icon, IconButton, Tooltip, useStyles2 } from '@grafana/ui'; + +interface LogControlOptionProps { + label?: string; + expanded: boolean; + tooltip: string; + stickToBottom?: boolean; +} + +export type Props = React.ComponentProps & LogControlOptionProps; + +export const LogListControlsOption = React.forwardRef( + ( + { + stickToBottom, + expanded, + label, + tooltip, + className: iconButtonClassName, + name: iconButtonName, + ...iconButtonProps + }: Props, + ref + ) => { + const styles = useStyles2(getStyles, expanded); + + return ( +
+ +
+ ); + } +); + +interface LogControlSelectOptionProps { + label?: string; + expanded: boolean; + tooltip: string; + stickToBottom?: boolean; + dropdown: JSX.Element; + isActive: boolean; + customTagText: string; + buttonAriaLabel: string; +} +export type SelectProps = React.ComponentProps & LogControlSelectOptionProps; + +export const LogListControlsSelectOption = React.forwardRef( + ( + { + stickToBottom, + expanded, + label, + tooltip, + className: iconButtonClassName, + name: iconButtonName, + dropdown, + isActive: isActive, + customTagText, + buttonAriaLabel, + ...iconButtonProps + }: SelectProps, + ref + ) => { + const styles = useStyles2(getStyles, expanded); + + return ( +
+ +
+ ); + } +); + +LogListControlsSelectOption.displayName = 'LogListControlsSelectOption'; +const getStyles = (theme: GrafanaTheme2, expanded: boolean) => { + const hoverSize = '26'; + return { + customControlTag: css({ + color: theme.colors.primary.text, + fontSize: 10, + position: 'absolute', + bottom: -4, + right: 1, + lineHeight: '10px', + backgroundColor: theme.colors.background.primary, + paddingLeft: 2, + }), + customControlIcon: css({ + verticalAlign: 'baseline', + }), + customControlButton: css({ + position: 'relative', + zIndex: 0, + margin: 0, + boxShadow: 'none', + border: 'none', + display: 'flex', + background: 'transparent', + justifyContent: 'center', + alignItems: 'center', + padding: 0, + overflow: 'visible', + width: '100%', + }), + controlButtonActive: css({ + margin: 0, + color: theme.colors.text.secondary, + height: theme.spacing(2), + '&:hover': { + '&:before': { + backgroundColor: theme.colors.action.hover, + opacity: 1, + }, + }, + '&:before': { + zIndex: -1, + position: 'absolute', + opacity: 0, + width: `${hoverSize}px`, + height: `${hoverSize}px`, + borderRadius: theme.shape.radius.default, + content: '""', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transitionDuration: '0.2s', + transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)', + transitionProperty: 'opacity', + }, + }, + '&:after': { + display: 'block', + content: '" "', + position: 'absolute', + height: 2, + borderRadius: theme.shape.radius.default, + bottom: theme.spacing(-1), + backgroundImage: theme.colors.gradients.brandHorizontal, + width: theme.spacing(2.25), + opacity: 1, + }, + }), + controlButton: css({ + margin: 0, + color: theme.colors.text.secondary, + height: theme.spacing(2), + }), + marginTopAuto: css({ + marginTop: 'auto', + marginBottom: theme.spacing(1), + }), + labelText: css({ + display: expanded ? 'block' : 'none', + }), + iconContainer: css({ + display: 'flex', + alignItems: 'center', + height: '16px', + }), + container: css({ + fontSize: theme.typography.pxToRem(12), + height: theme.spacing(2), + width: 'auto', + }), + label: css({ + display: 'flex', + justifyContent: expanded ? 'space-between' : 'center', + marginRight: expanded ? '2.5px' : 0, + }), + }; +}; + +LogListControlsOption.displayName = 'LogListControlsOption'; diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index 007ac591dca..03e5e394870 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -3,7 +3,6 @@ import { createContext, useContext } from 'react'; import { CoreApp, LogsDedupStrategy, LogsSortOrder } from '@grafana/data'; import { checkLogsError, checkLogsSampled } from 'app/features/logs/utils'; -import { LogLineDetailsMode } from '../LogLineDetails'; import { LogListContextData, Props } from '../LogListContext'; import { LogListModel } from '../processing'; @@ -49,11 +48,11 @@ export const LogListContext = createContext({ toggleDetails: () => {}, wrapLogMessage: false, detailsMode: 'sidebar', - setDetailsMode: function (mode: LogLineDetailsMode): void { - throw new Error('Function not implemented.'); - }, + setDetailsMode: () => {}, isAssistantAvailable: false, openAssistantByLog: () => {}, + controlsExpanded: false, + setControlsExpanded: () => {}, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -110,8 +109,10 @@ export const defaultValue: LogListContextData = { sortOrder: LogsSortOrder.Ascending, wrapLogMessage: false, isAssistantAvailable: false, - openAssistantByLog: () => {}, + openAssistantByLog: jest.fn(), timestampResolution: 'ns', + controlsExpanded: false, + setControlsExpanded: jest.fn(), }; export const defaultProps: Props = { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index dbd9423b48d..2434af01c23 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9633,10 +9633,9 @@ } }, "logs-controls": { + "collapse": "Collapse", "deduplication": "Deduplication", - "disable-highlighting": "Disable highlighting", "disable-prettify-json": "Collapse JSON logs", - "display-level": "Display levels", "display-level-all": "All levels", "download": "Download logs", "download-logs": { @@ -9644,18 +9643,39 @@ "json": "json", "txt": "txt" }, - "enable-highlighting": "Enable highlighting", "escape-newlines": "Fix incorrectly escaped newline and tab sequences in log lines", - "font-size-default": "Use small font size", - "font-size-small": "Use default font size", + "expand": "Expand", + "filter-levels": "Filter levels", + "font-large": "Set large font", + "font-small": "Set small font", "hide-search": "Close search", "hide-timestamps": "Hide timestamps", "hide-unique-labels": "Hide unique labels", + "label": { + "collapse": "Expanded", + "disable-highlighting": "Highlight text", + "enable-highlighting": "Plain text", + "escape-newlines": "Escape newlines", + "expand": "Collapsed" + }, + "labels": { + "font-large": "Large font", + "font-small": "Small font", + "hide-search": "Close search", + "newest-first": "Newest logs first", + "oldest-first": "Oldest logs first", + "show-search": "Search logs" + }, "line-wrapping": { "enable": "Enable line wrapping", "enable-prettify": "Enable line wrapping and prettify JSON", "hide": "Disable line wrapping", - "label": "Log line wrapping" + "state": { + "hide": "Wrap disabled", + "json": "Wrap JSON", + "wrap": "Wrap lines" + }, + "tooltip": "Set line wrap" }, "newest-first": "Sorted by newest logs first - Click to show oldest first", "oldest-first": "Sorted by oldest logs first - Click to show newest first", @@ -9671,8 +9691,18 @@ "timestamp": { "hide": "Hide timestamps", "label": "Log timestamps", + "label-hide": "Hide timestamps", + "label-ms": "Display ms", + "label-ns": "Display ns", "milliseconds": "Show millisecond timestamps", - "nanoseconds": "Show nanosecond timestamps" + "nanoseconds": "Show nanosecond timestamps", + "tooltip": "Set timestamp format" + }, + "tooltip": { + "disable-highlighting": "Disable highlighting", + "download": "Download", + "enable-highlighting": "Enable highlighting", + "filter-level": "Filter logs result by level" }, "unwrap-lines": "Unwrap lines", "wrap-lines": "Wrap lines"