New Logs Panel: Read default font size from storage + add missing unescaped content check + fix mutation (#106755)

* Logs Panel: remove default value to it's taken from local storage code

* processing: do not remove newlines, let truncation handle

* LogList: check and pass unescaped content to context

* LogLine: prevent mutation of searchWords

* grammar: use parseFlags

* grammar: escape regex
This commit is contained in:
Matias Chomicki
2025-06-17 23:10:34 +02:00
committed by GitHub
parent 286a6638b8
commit 693bc51693
6 changed files with 50 additions and 11 deletions

View File

@@ -236,7 +236,7 @@ const DisplayedFields = ({
const { matchingUids, search } = useLogListSearchContext();
const searchWords = useMemo(() => {
const searchWords = log.searchWords && log.searchWords[0] ? log.searchWords : [];
const searchWords = log.searchWords && log.searchWords[0] ? log.searchWords.slice() : [];
if (search && matchingUids?.includes(log.uid)) {
searchWords.push(search);
}
@@ -271,7 +271,7 @@ const LogLineBody = ({ log, styles }: { log: LogListModel; styles: LogLineStyles
const { matchingUids, search } = useLogListSearchContext();
const highlight = useMemo(() => {
const searchWords = log.searchWords && log.searchWords[0] ? log.searchWords : [];
const searchWords = log.searchWords && log.searchWords[0] ? log.searchWords.slice() : [];
if (search && matchingUids?.includes(log.uid)) {
searchWords.push(search);
}

View File

@@ -259,5 +259,33 @@ describe('LogList', () => {
expect(screen.queryByPlaceholderText('Search in logs')).not.toBeInTheDocument();
});
test('Does not conflict with search words', async () => {
logs = [
createLogRow({ uid: '1' }),
createLogRow({ uid: '2', entry: '(?i)some text', searchWords: ['some text'] }),
];
render(<LogList {...defaultProps} logs={logs} />);
expect(screen.queryByPlaceholderText('Search in logs')).not.toBeInTheDocument();
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('some text')).toBeInTheDocument();
await userEvent.keyboard('{Control>}{f}{/Control}');
expect(screen.getByPlaceholderText('Search in logs')).toBeInTheDocument();
await userEvent.type(screen.getByPlaceholderText('Search in logs'), '(?i)');
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('(?i)')).toBeInTheDocument();
expect(screen.getByText('some text')).toBeInTheDocument();
await userEvent.clear(screen.getByPlaceholderText('Search in logs'));
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('some text')).toBeInTheDocument();
});
});
});

View File

@@ -151,6 +151,7 @@ export const LogList = ({
timeZone,
wrapLogMessage,
}: Props) => {
const hasUnescapedContent = useMemo(() => logs.some((log) => log.hasUnescapedContent), [logs]);
return (
<LogListContextProvider
app={app}
@@ -161,6 +162,7 @@ export const LogList = ({
filterLevels={filterLevels}
fontSize={fontSize}
getRowContextQuery={getRowContextQuery}
hasUnescapedContent={hasUnescapedContent}
isLabelFilterActive={isLabelFilterActive}
logs={logs}
logsMeta={logsMeta}

View File

@@ -1,6 +1,6 @@
import { Grammar } from 'prismjs';
import { escapeRegex } from '@grafana/data';
import { escapeRegex, parseFlags } from '@grafana/data';
import { LogListModel } from './processing';
@@ -30,14 +30,26 @@ export const generateTextMatchGrammar = (
highlightWords: string[] | undefined = [],
search: string | undefined
): Grammar => {
const textMatches = [...highlightWords];
/**
* See:
* - https://github.com/grafana/grafana/blob/96f1582c36f94cf4ac7621b7af86bc9e2ad626fb/public/app/features/logs/components/LogRowMessage.tsx#L67
* - https://github.com/grafana/grafana/blob/96f1582c36f94cf4ac7621b7af86bc9e2ad626fb/packages/grafana-data/src/text/text.ts#L12
*/
const expressions = highlightWords.map((word) => {
const { cleaned, flags } = parseFlags(cleanNeedle(word));
return new RegExp(`(?:${cleaned})`, flags);
});
if (search) {
textMatches.push(escapeRegex(search));
expressions.push(new RegExp(escapeRegex(search), 'gi'));
}
if (!textMatches.length) {
if (!expressions.length) {
return {};
}
return {
'log-search-match': new RegExp(textMatches.join('|'), 'g'),
'log-search-match': expressions,
};
};
const cleanNeedle = (needle: string): string => {
return needle.replace(/[[{(][\w,.\/:;<=>?:*+]+$/, '');
};

View File

@@ -88,9 +88,7 @@ export class LogListModel implements LogRowModel {
get body(): string {
if (this._body === undefined) {
let body = this.collapsed ? this.raw.substring(0, getTruncationLength(null)) : this.raw;
// Turn it into a single-line log entry for the list
this._body = body.replace(/(\r\n|\n|\r)/g, '');
this._body = this.collapsed ? this.raw.substring(0, getTruncationLength(null)) : this.raw;
}
return this._body;
}

View File

@@ -87,7 +87,6 @@ export const plugin = new PanelPlugin<Options>(LogsPanel)
},
],
},
defaultValue: 'default',
});
}