diff --git a/public/app/features/logs/components/LogRowMessage.test.tsx b/public/app/features/logs/components/LogRowMessage.test.tsx index a54eacfd727..a64602fa07d 100644 --- a/public/app/features/logs/components/LogRowMessage.test.tsx +++ b/public/app/features/logs/components/LogRowMessage.test.tsx @@ -5,7 +5,7 @@ import { ComponentProps } from 'react'; import { CoreApp, createTheme, LogLevel, LogRowModel } from '@grafana/data'; import { IconButton } from '@grafana/ui'; -import { LogRowMessage } from './LogRowMessage'; +import { LogRowMessage, MAX_CHARACTERS } from './LogRowMessage'; import { createLogRow } from './__mocks__/logRow'; import { getLogRowStyles } from './getLogRowStyles'; @@ -219,4 +219,19 @@ line3`; expect(onAfter).toHaveBeenCalledWith(expect.anything(), row); }); }); + + describe('Extremely long log lines', () => { + let entry = ''; + beforeEach(() => { + entry = new Array(MAX_CHARACTERS).fill('a').join('') + 'b'; + }); + it('Displays an ellipsis for log lines above the character limit', async () => { + setup({ + row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000 }), + }); + expect(screen.getByText(/1 more/)).toBeInTheDocument(); + await userEvent.click(screen.getByText(/1 more/)); + expect(screen.queryByText(/1 more/)).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index 6202c4af3ed..77f240caaa1 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -1,9 +1,11 @@ -import { memo, ReactNode, useMemo } from 'react'; +import { css } from '@emotion/css'; +import { memo, ReactNode, SyntheticEvent, useMemo, useState } from 'react'; import Highlighter from 'react-highlight-words'; -import { CoreApp, findHighlightChunksInText, LogRowContextOptions, LogRowModel } from '@grafana/data'; +import { CoreApp, findHighlightChunksInText, GrafanaTheme2, LogRowContextOptions, LogRowModel } from '@grafana/data'; import { DataQuery } from '@grafana/schema'; -import { PopoverContent } from '@grafana/ui'; +import { PopoverContent, useTheme2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; import { LogMessageAnsi } from './LogMessageAnsi'; import { LogRowMenuCell } from './LogRowMenuCell'; @@ -44,25 +46,73 @@ interface LogMessageProps { } const LogMessage = ({ hasAnsi, entry, highlights, styles }: LogMessageProps) => { + const excessCharacters = useMemo(() => entry.length - MAX_CHARACTERS, [entry]); const needsHighlighter = - highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0 && entry.length < MAX_CHARACTERS; + highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0 && excessCharacters <= 0; const searchWords = highlights ?? []; + const [showFull, setShowFull] = useState(excessCharacters < 0); + const truncatedEntry = useMemo(() => (showFull ? entry : entry.substring(0, MAX_CHARACTERS)), [entry, showFull]); + if (hasAnsi) { const highlight = needsHighlighter ? { searchWords, highlightClassName: styles.logsRowMatchHighLight } : undefined; - return ; + return ; } else if (needsHighlighter) { return ( ); } - return <>{entry}; + return ( + <> + {truncatedEntry} + {!showFull && } + + ); }; +interface EllipsisProps { + showFull: boolean; + toggle(state: boolean): void; + diff: number; +} +const Ellipsis = ({ toggle, diff }: EllipsisProps) => { + const styles = getEllipsisStyles(useTheme2()); + const handleClick = (e: SyntheticEvent) => { + e.stopPropagation(); + toggle(true); + }; + return ( + <> + + + {diff} more + + + ); +}; + +const getEllipsisStyles = (theme: GrafanaTheme2) => ({ + showMore: css({ + display: 'inline-flex', + fontWeight: theme.typography.fontWeightMedium, + fontSize: theme.typography.size.sm, + fontFamily: theme.typography.fontFamily, + height: theme.spacing(3), + padding: theme.spacing(0.25, 1), + color: theme.colors.secondary.text, + border: `1px solid ${theme.colors.border.strong}`, + '&:hover': { + background: theme.colors.secondary.transparent, + borderColor: theme.colors.emphasize(theme.colors.border.strong, 0.25), + color: theme.colors.secondary.text, + }, + }), +}); + const restructureLog = ( line: string, prettifyLogMessage: boolean, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f1e10e74643..f9d43626348 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1742,6 +1742,10 @@ "logs": { "infinite-scroll": { "older-logs": "Older logs" + }, + "log-row-message": { + "ellipsis": "… ", + "more": "more" } }, "migrate-to-cloud": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 2b8582ed407..ef7d1e95cf8 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1742,6 +1742,10 @@ "logs": { "infinite-scroll": { "older-logs": "Øľđęř ľőģş" + }, + "log-row-message": { + "ellipsis": "… ", + "more": "mőřę" } }, "migrate-to-cloud": {