Logs: refactor unescaped content (#108187)

This commit is contained in:
Matias Chomicki
2025-07-17 10:59:03 +02:00
committed by GitHub
parent 6c69ae244e
commit 433a5fd464
7 changed files with 34 additions and 30 deletions
@@ -689,7 +689,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
[getPinnedLogsCount, onOpenContext, onPinLineCallback, outlineItems, pinnedLogs, register, unregister, updateItem]
);
const hasUnescapedContent = useMemo(() => checkUnescapedContent(logRows), [logRows]);
const { dedupedRows, dedupCount } = useMemo(() => dedupRows(logRows, dedupStrategy), [dedupStrategy, logRows]);
const navigationRange = useMemo(() => createNavigationRange(logRows), [logRows]);
const infiniteScrollAvailable = useMemo(
@@ -1035,7 +1034,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
logsMeta={logsMeta}
logOptionsStorageKey={SETTING_KEY_ROOT}
onLogOptionsChange={onLogOptionsChange}
hasUnescapedContent={hasUnescapedContent}
filterLevels={filterLevels}
/>
</div>
@@ -1260,10 +1258,6 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n
};
};
const checkUnescapedContent = (logRows: LogRowModel[]) => {
return logRows.some((r) => r.hasUnescapedContent);
};
const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) => {
const dedupedRows = dedupLogRows(logRows, dedupStrategy);
const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0);
@@ -26,7 +26,6 @@ import { LogListControls } from './panel/LogListControls';
import { ScrollToLogsEvent } from './panel/virtualization';
export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
hasUnescapedContent?: boolean;
loading: boolean;
logsMeta?: LogsMetaItem[];
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
@@ -63,7 +62,6 @@ export const ControlledLogRows = forwardRef<HTMLDivElement | null, ControlledLog
deduplicatedRows,
dedupStrategy,
filterLevels,
hasUnescapedContent,
showLabels,
showTime,
logsMeta,
@@ -84,7 +82,6 @@ export const ControlledLogRows = forwardRef<HTMLDivElement | null, ControlledLog
enableLogDetails={false}
filterLevels={filterLevels}
fontSize="default"
hasUnescapedContent={hasUnescapedContent}
logOptionsStorageKey={logOptionsStorageKey}
logs={deduplicatedRows ?? []}
logsMeta={logsMeta}
@@ -148,7 +148,6 @@ export const LogList = ({
timeZone,
wrapLogMessage,
}: Props) => {
const hasUnescapedContent = useMemo(() => logs.some((log) => log.hasUnescapedContent), [logs]);
return (
<LogListContextProvider
app={app}
@@ -160,7 +159,6 @@ export const LogList = ({
filterLevels={filterLevels}
fontSize={fontSize}
getRowContextQuery={getRowContextQuery}
hasUnescapedContent={hasUnescapedContent}
isLabelFilterActive={isLabelFilterActive}
logs={logs}
logsMeta={logsMeta}
@@ -42,9 +42,10 @@ export interface LogListContextData extends Omit<Props, 'containerElement' | 'lo
downloadLogs: (format: DownloadFormat) => void;
enableLogDetails: boolean;
filterLevels: LogLevel[];
forceEscape: boolean;
hasLogsWithErrors?: boolean;
hasSampledLogs?: boolean;
hasUnescapedContent?: boolean;
hasUnescapedContent: boolean;
logLineMenuCustomItems?: LogLineMenuCustomItem[];
setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void;
setDetailsMode: (mode: LogLineDetailsMode) => void;
@@ -75,6 +76,7 @@ export const LogListContext = createContext<LogListContextData>({
downloadLogs: () => {},
enableLogDetails: false,
filterLevels: [],
forceEscape: false,
fontSize: 'default',
hasUnescapedContent: false,
setDedupStrategy: () => {},
@@ -124,7 +126,6 @@ export type LogListState = Pick<
| 'fontSize'
| 'forceEscape'
| 'filterLevels'
| 'hasUnescapedContent'
| 'pinnedLogs'
| 'prettifyJSON'
| 'showUniqueLabels'
@@ -145,8 +146,6 @@ export interface Props {
enableLogDetails: boolean;
filterLevels?: LogLevel[];
fontSize: LogListFontSize;
forceEscape?: boolean;
hasUnescapedContent?: boolean;
getRowContextQuery?: GetRowContextQueryFn;
isLabelFilterActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
logs: LogRowModel[];
@@ -189,8 +188,6 @@ export const LogListContextProvider = ({
displayedFields,
filterLevels,
fontSize,
forceEscape = false,
hasUnescapedContent,
isLabelFilterActive,
getRowContextQuery,
logs,
@@ -227,8 +224,7 @@ export const LogListContextProvider = ({
filterLevels:
filterLevels ?? (logOptionsStorageKey ? store.getObject(`${logOptionsStorageKey}.filterLevels`, []) : []),
fontSize,
forceEscape,
hasUnescapedContent,
forceEscape: logOptionsStorageKey ? store.getBool(`${logOptionsStorageKey}.forceEscape`, false) : false,
pinnedLogs,
prettifyJSON,
showTime,
@@ -295,12 +291,6 @@ export const LogListContextProvider = ({
setLogListState((logListState) => ({ ...logListState, fontSize }));
}, [fontSize]);
useEffect(() => {
if (logListState.hasUnescapedContent !== hasUnescapedContent) {
setLogListState({ ...logListState, hasUnescapedContent });
}
}, [hasUnescapedContent, logListState]);
useEffect(() => {
if (!shallowCompare(logListState.pinnedLogs ?? [], pinnedLogs ?? [])) {
setLogListState({ ...logListState, pinnedLogs });
@@ -495,6 +485,7 @@ export const LogListContextProvider = ({
const hasLogsWithErrors = useMemo(() => logs.some((log) => !!checkLogsError(log)), [logs]);
const hasSampledLogs = useMemo(() => logs.some((log) => !!checkLogsSampled(log)), [logs]);
const hasUnescapedContent = useMemo(() => logs.some((r) => r.hasUnescapedContent), [logs]);
return (
<LogListContext.Provider
@@ -513,7 +504,7 @@ export const LogListContextProvider = ({
forceEscape: logListState.forceEscape,
hasLogsWithErrors,
hasSampledLogs,
hasUnescapedContent: logListState.hasUnescapedContent,
hasUnescapedContent,
isLabelFilterActive,
getRowContextQuery,
logSupportsContext,
@@ -5,14 +5,17 @@ import { CoreApp, EventBusSrv, LogLevel, LogsDedupStrategy, LogsSortOrder } from
import { config } from '@grafana/runtime';
import { downloadLogs } from '../../utils';
import { createLogRow } from '../mocks/logRow';
import { createLogLine, createLogRow } from '../mocks/logRow';
import { LogListFontSize } from './LogList';
import { LogListContextProvider } from './LogListContext';
import { LogListControls } from './LogListControls';
import { ScrollToLogsEvent } from './virtualization';
jest.mock('../../utils');
jest.mock('../../utils', () => ({
...jest.requireActual('../../utils'),
downloadLogs: jest.fn(),
}));
const fontSize: LogListFontSize = 'default';
const contextProps = {
@@ -294,14 +297,15 @@ describe('LogListControls', () => {
});
test('Controls new lines', async () => {
const log = createLogLine({ entry: 'the\\r\\nentry', hasUnescapedContent: true });
const { rerender } = render(
<LogListContextProvider {...contextProps} hasUnescapedContent>
<LogListContextProvider {...contextProps} logs={[log]}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
await userEvent.click(screen.getByLabelText('Fix incorrectly escaped newline and tab sequences in log lines'));
rerender(
<LogListContextProvider {...contextProps} hasUnescapedContent>
<LogListContextProvider {...contextProps} logs={[log]}>
<LogListControls eventBus={new EventBusSrv()} />
</LogListContextProvider>
);
@@ -417,6 +417,23 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
size="lg"
/>
</Dropdown>
{visualisationType === 'logs' && hasUnescapedContent && (
<IconButton
name="enter"
aria-pressed={forceEscape}
className={forceEscape ? styles.controlButtonActive : styles.controlButton}
onClick={onForceEscapeClick}
tooltip={
forceEscape
? t('logs.logs-controls.remove-escaping', 'Remove escaping')
: t(
'logs.logs-controls.escape-newlines',
'Fix incorrectly escaped newline and tab sequences in log lines'
)
}
size="lg"
/>
)}
</>
)}
{visualisationType === 'logs' && (
@@ -18,6 +18,7 @@ export const LogListContext = createContext<LogListContextData>({
enableLogDetails: false,
filterLevels: [],
fontSize: 'default',
forceEscape: false,
hasUnescapedContent: false,
setDedupStrategy: () => {},
setDetailsWidth: () => {},
@@ -85,6 +86,8 @@ export const defaultValue: LogListContextData = {
enableLogDetails: false,
filterLevels: [],
fontSize: 'default',
forceEscape: false,
hasUnescapedContent: false,
setDetailsWidth: jest.fn(),
showDetails: [],
toggleDetails: jest.fn(),