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": {