mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 05:04:29 +08:00
Compare commits
5 Commits
docs/add-t
...
logs-custo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b24a5c79f3 | ||
|
|
1756c45e33 | ||
|
|
9edf23ea68 | ||
|
|
3d15a07147 | ||
|
|
f0ef66eaa1 |
@@ -507,6 +507,7 @@ export type {
|
|||||||
ExploreCorrelationHelperData,
|
ExploreCorrelationHelperData,
|
||||||
ExploreTracePanelState,
|
ExploreTracePanelState,
|
||||||
ExploreLogsPanelState,
|
ExploreLogsPanelState,
|
||||||
|
CustomHighlight,
|
||||||
SplitOpenOptions,
|
SplitOpenOptions,
|
||||||
SplitOpen,
|
SplitOpen,
|
||||||
TraceSearchProps,
|
TraceSearchProps,
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export interface ExploreTracePanelState {
|
|||||||
spanFilters?: TraceSearchProps;
|
spanFilters?: TraceSearchProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomHighlight {
|
||||||
|
text: string;
|
||||||
|
colorIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExploreLogsPanelState {
|
export interface ExploreLogsPanelState {
|
||||||
id?: string;
|
id?: string;
|
||||||
columns?: Record<number, string>;
|
columns?: Record<number, string>;
|
||||||
@@ -85,6 +90,7 @@ export interface ExploreLogsPanelState {
|
|||||||
refId?: string;
|
refId?: string;
|
||||||
displayedFields?: string[];
|
displayedFields?: string[];
|
||||||
sortOrder?: LogsSortOrder;
|
sortOrder?: LogsSortOrder;
|
||||||
|
customHighlights?: CustomHighlight[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
|
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
|
||||||
|
|||||||
@@ -428,6 +428,13 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||||||
[sortOrderChanged]
|
[sortOrderChanged]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onPanelStateChange = useCallback(
|
||||||
|
(newState: ExploreLogsPanelState) => {
|
||||||
|
dispatch(changePanelState(exploreId, 'logs', newState));
|
||||||
|
},
|
||||||
|
[dispatch, exploreId]
|
||||||
|
);
|
||||||
|
|
||||||
const onChangeVisualisation = useCallback(
|
const onChangeVisualisation = useCallback(
|
||||||
(visualisation: LogsVisualisationType) => {
|
(visualisation: LogsVisualisationType) => {
|
||||||
setVisualisationType(visualisation);
|
setVisualisationType(visualisation);
|
||||||
@@ -1141,6 +1148,8 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||||||
onPermalinkClick={onPermalinkClick}
|
onPermalinkClick={onPermalinkClick}
|
||||||
onPinLine={onPinToContentOutlineClick}
|
onPinLine={onPinToContentOutlineClick}
|
||||||
onUnpinLine={onPinToContentOutlineClick}
|
onUnpinLine={onPinToContentOutlineClick}
|
||||||
|
panelState={panelState?.logs}
|
||||||
|
onPanelStateChange={onPanelStateChange}
|
||||||
permalinkedLogId={panelState?.logs?.id}
|
permalinkedLogId={panelState?.logs?.id}
|
||||||
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
||||||
pinnedLogs={pinnedLogs}
|
pinnedLogs={pinnedLogs}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface PopoverMenuProps {
|
|||||||
y: number;
|
y: number;
|
||||||
onClickFilterString?: (value: string, refId?: string) => void;
|
onClickFilterString?: (value: string, refId?: string) => void;
|
||||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||||
|
onClickHighlightText?: (text: string) => void;
|
||||||
onDisable: () => void;
|
onDisable: () => void;
|
||||||
row: LogRowModel;
|
row: LogRowModel;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
@@ -24,6 +25,7 @@ export const PopoverMenu = ({
|
|||||||
y,
|
y,
|
||||||
onClickFilterString,
|
onClickFilterString,
|
||||||
onClickFilterOutString,
|
onClickFilterOutString,
|
||||||
|
onClickHighlightText,
|
||||||
selection,
|
selection,
|
||||||
row,
|
row,
|
||||||
close,
|
close,
|
||||||
@@ -50,7 +52,7 @@ export const PopoverMenu = ({
|
|||||||
props.onDisable();
|
props.onDisable();
|
||||||
}, [props, row.datasourceType, selection.length]);
|
}, [props, row.datasourceType, selection.length]);
|
||||||
|
|
||||||
const supported = onClickFilterString || onClickFilterOutString;
|
const supported = onClickFilterString || onClickFilterOutString || onClickHighlightText;
|
||||||
|
|
||||||
if (!supported) {
|
if (!supported) {
|
||||||
return null;
|
return null;
|
||||||
@@ -88,6 +90,16 @@ export const PopoverMenu = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{onClickHighlightText && (
|
||||||
|
<Menu.Item
|
||||||
|
label={t('logs.popover-menu.highlight-occurrences', 'Highlight occurrences')}
|
||||||
|
onClick={() => {
|
||||||
|
onClickHighlightText(selection);
|
||||||
|
close();
|
||||||
|
track('highlight_occurrences', selection.length, row.datasourceType);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<Menu.Item label={t('logs.popover-menu.disable-menu', 'Disable menu')} onClick={onDisable} />
|
<Menu.Item label={t('logs.popover-menu.disable-menu', 'Disable menu')} onClick={onDisable} />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function pruneObject(obj: object): object | undefined {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
pruned = omitBy<typeof pruned>(pruned, isEmpty);
|
pruned = omitBy<typeof pruned>(pruned, (value) => isEmpty(value) && typeof value !== 'number');
|
||||||
if (isEmpty(pruned)) {
|
if (isEmpty(pruned)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -539,7 +539,20 @@ export const getStyles = (theme: GrafanaTheme2, virtualization?: LogLineVirtuali
|
|||||||
'.log-search-match': {
|
'.log-search-match': {
|
||||||
color: theme.components.textHighlight.text,
|
color: theme.components.textHighlight.text,
|
||||||
backgroundColor: theme.components.textHighlight.background,
|
backgroundColor: theme.components.textHighlight.background,
|
||||||
|
borderRadius: theme.shape.radius.default,
|
||||||
|
padding: '0 1px',
|
||||||
},
|
},
|
||||||
|
// Generate highlight classes from entire theme palette
|
||||||
|
...theme.visualization.palette
|
||||||
|
.map((_, index) => ({
|
||||||
|
[`.log-custom-highlight-${index}`]: {
|
||||||
|
backgroundColor: theme.visualization.getColorByName(theme.visualization.palette[index]),
|
||||||
|
color: theme.colors.getContrastText(
|
||||||
|
theme.visualization.getColorByName(theme.visualization.palette[index])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.reduce((acc, obj) => ({ ...acc, ...obj }), {}),
|
||||||
},
|
},
|
||||||
'& .no-highlighting': {
|
'& .no-highlighting': {
|
||||||
color: theme.colors.text.primary,
|
color: theme.colors.text.primary,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { Align, VariableSizeList } from 'react-window';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CoreApp,
|
CoreApp,
|
||||||
|
CustomHighlight,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
EventBus,
|
EventBus,
|
||||||
EventBusSrv,
|
EventBusSrv,
|
||||||
|
ExploreLogsPanelState,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
@@ -32,6 +34,7 @@ import { LogLineDetails, LogLineDetailsMode } from './LogLineDetails';
|
|||||||
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
import { GetRowContextQueryFn, LogLineMenuCustomItem } from './LogLineMenu';
|
||||||
import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext';
|
import { LogListContextProvider, LogListState, useLogListContext } from './LogListContext';
|
||||||
import { LogListControls } from './LogListControls';
|
import { LogListControls } from './LogListControls';
|
||||||
|
import { LogListHighlightContextProvider, useLogListHighlightContext } from './LogListHighlightContext';
|
||||||
import { LOG_LIST_SEARCH_HEIGHT, LogListSearch } from './LogListSearch';
|
import { LOG_LIST_SEARCH_HEIGHT, LogListSearch } from './LogListSearch';
|
||||||
import { LogListSearchContextProvider, useLogListSearchContext } from './LogListSearchContext';
|
import { LogListSearchContextProvider, useLogListSearchContext } from './LogListSearchContext';
|
||||||
import { preProcessLogs, LogListModel, getLevelsFromLogs } from './processing';
|
import { preProcessLogs, LogListModel, getLevelsFromLogs } from './processing';
|
||||||
@@ -76,6 +79,8 @@ export interface Props {
|
|||||||
onPinLine?: (row: LogRowModel) => void;
|
onPinLine?: (row: LogRowModel) => void;
|
||||||
onOpenContext?: (row: LogRowModel, onClose: () => void) => void;
|
onOpenContext?: (row: LogRowModel, onClose: () => void) => void;
|
||||||
onUnpinLine?: (row: LogRowModel) => void;
|
onUnpinLine?: (row: LogRowModel) => void;
|
||||||
|
panelState?: ExploreLogsPanelState;
|
||||||
|
onPanelStateChange?: (panelState: ExploreLogsPanelState) => void;
|
||||||
permalinkedLogId?: string;
|
permalinkedLogId?: string;
|
||||||
pinLineButtonTooltipTitle?: PopoverContent;
|
pinLineButtonTooltipTitle?: PopoverContent;
|
||||||
pinnedLogs?: string[];
|
pinnedLogs?: string[];
|
||||||
@@ -153,6 +158,8 @@ export const LogList = ({
|
|||||||
onPinLine,
|
onPinLine,
|
||||||
onOpenContext,
|
onOpenContext,
|
||||||
onUnpinLine,
|
onUnpinLine,
|
||||||
|
panelState,
|
||||||
|
onPanelStateChange,
|
||||||
permalinkedLogId,
|
permalinkedLogId,
|
||||||
pinLineButtonTooltipTitle,
|
pinLineButtonTooltipTitle,
|
||||||
pinnedLogs,
|
pinnedLogs,
|
||||||
@@ -170,6 +177,21 @@ export const LogList = ({
|
|||||||
timeZone,
|
timeZone,
|
||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const customHighlights = (panelState?.customHighlights ?? []).map((h, index) => ({
|
||||||
|
...h,
|
||||||
|
colorIndex: h.colorIndex ?? index % 8,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleHighlightsChange = useCallback(
|
||||||
|
(highlights: CustomHighlight[]) => {
|
||||||
|
onPanelStateChange?.({
|
||||||
|
...panelState,
|
||||||
|
customHighlights: highlights.length > 0 ? highlights : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[panelState, onPanelStateChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogListContextProvider
|
<LogListContextProvider
|
||||||
app={app}
|
app={app}
|
||||||
@@ -221,6 +243,10 @@ export const LogList = ({
|
|||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
>
|
>
|
||||||
<LogListSearchContextProvider>
|
<LogListSearchContextProvider>
|
||||||
|
<LogListHighlightContextProvider
|
||||||
|
customHighlights={customHighlights}
|
||||||
|
onHighlightsChange={handleHighlightsChange}
|
||||||
|
>
|
||||||
<LogListComponent
|
<LogListComponent
|
||||||
containerElement={containerElement}
|
containerElement={containerElement}
|
||||||
dataFrames={dataFrames}
|
dataFrames={dataFrames}
|
||||||
@@ -237,6 +263,7 @@ export const LogList = ({
|
|||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
/>
|
/>
|
||||||
|
</LogListHighlightContextProvider>
|
||||||
</LogListSearchContextProvider>
|
</LogListSearchContextProvider>
|
||||||
</LogDetailsContextProvider>
|
</LogDetailsContextProvider>
|
||||||
</LogListContextProvider>
|
</LogListContextProvider>
|
||||||
@@ -279,6 +306,7 @@ const LogListComponent = ({
|
|||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
} = useLogListContext();
|
} = useLogListContext();
|
||||||
const { detailsMode, showDetails, toggleDetails } = useLogDetailsContext();
|
const { detailsMode, showDetails, toggleDetails } = useLogDetailsContext();
|
||||||
|
const { customHighlights, addHighlight } = useLogListHighlightContext();
|
||||||
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
||||||
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
|
const [listHeight, setListHeight] = useState(getListHeight(containerElement, app));
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
@@ -354,6 +382,7 @@ const LogListComponent = ({
|
|||||||
preProcessLogs(
|
preProcessLogs(
|
||||||
logs,
|
logs,
|
||||||
{
|
{
|
||||||
|
customHighlights,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
escape: forceEscape ?? false,
|
escape: forceEscape ?? false,
|
||||||
prettifyJSON,
|
prettifyJSON,
|
||||||
@@ -367,7 +396,18 @@ const LogListComponent = ({
|
|||||||
);
|
);
|
||||||
virtualization.resetLogLineSizes();
|
virtualization.resetLogLineSizes();
|
||||||
listRef.current?.resetAfterIndex(0);
|
listRef.current?.resetAfterIndex(0);
|
||||||
}, [forceEscape, getFieldLinks, grammar, logs, prettifyJSON, sortOrder, timeZone, virtualization, wrapLogMessage]);
|
}, [
|
||||||
|
customHighlights,
|
||||||
|
forceEscape,
|
||||||
|
getFieldLinks,
|
||||||
|
grammar,
|
||||||
|
logs,
|
||||||
|
prettifyJSON,
|
||||||
|
sortOrder,
|
||||||
|
timeZone,
|
||||||
|
virtualization,
|
||||||
|
wrapLogMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listRef.current?.resetAfterIndex(0);
|
listRef.current?.resetAfterIndex(0);
|
||||||
@@ -469,6 +509,7 @@ const LogListComponent = ({
|
|||||||
{...popoverState.popoverMenuCoordinates}
|
{...popoverState.popoverMenuCoordinates}
|
||||||
onClickFilterString={onClickFilterString}
|
onClickFilterString={onClickFilterString}
|
||||||
onClickFilterOutString={onClickFilterOutString}
|
onClickFilterOutString={onClickFilterOutString}
|
||||||
|
onClickHighlightText={addHighlight}
|
||||||
onDisable={onDisablePopoverMenu}
|
onDisable={onDisablePopoverMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { DownloadFormat } from '../../utils';
|
|||||||
|
|
||||||
import { useLogListContext } from './LogListContext';
|
import { useLogListContext } from './LogListContext';
|
||||||
import { LogListControlsOption, LogListControlsSelectOption } from './LogListControlsOption';
|
import { LogListControlsOption, LogListControlsSelectOption } from './LogListControlsOption';
|
||||||
|
import { useLogListHighlightContext } from './LogListHighlightContext';
|
||||||
import { useLogListSearchContext } from './LogListSearchContext';
|
import { useLogListSearchContext } from './LogListSearchContext';
|
||||||
import { LOG_LIST_CONTROLS_WIDTH, ScrollToLogsEvent } from './virtualization';
|
import { LOG_LIST_CONTROLS_WIDTH, ScrollToLogsEvent } from './virtualization';
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ export const LogListControls = ({ eventBus, logLevels = FILTER_LEVELS, visualisa
|
|||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
} = useLogListContext();
|
} = useLogListContext();
|
||||||
const { hideSearch, searchVisible, showSearch } = useLogListSearchContext();
|
const { hideSearch, searchVisible, showSearch } = useLogListSearchContext();
|
||||||
|
const { hasHighlights, resetHighlights } = useLogListHighlightContext();
|
||||||
|
|
||||||
const styles = useStyles2(getStyles, controlsExpanded);
|
const styles = useStyles2(getStyles, controlsExpanded);
|
||||||
|
|
||||||
@@ -355,6 +357,23 @@ export const LogListControls = ({ eventBus, logLevels = FILTER_LEVELS, visualisa
|
|||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
{hasHighlights && (
|
||||||
|
<>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<LogListControlsOption
|
||||||
|
expanded={controlsExpanded}
|
||||||
|
name="times-circle"
|
||||||
|
className={styles.controlButtonActive}
|
||||||
|
onClick={() => {
|
||||||
|
resetHighlights();
|
||||||
|
reportInteraction('logs_log_list_controls_reset_highlights_clicked');
|
||||||
|
}}
|
||||||
|
label={t('logs.logs-controls.reset-highlights', 'Reset highlights')}
|
||||||
|
tooltip={t('logs.logs-controls.tooltip.reset-highlights', 'Clear all custom highlights')}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className={styles.divider} />
|
<div className={styles.divider} />
|
||||||
{config.featureToggles.newLogsPanel ? (
|
{config.featureToggles.newLogsPanel ? (
|
||||||
<TimestampResolutionButton expanded={controlsExpanded} />
|
<TimestampResolutionButton expanded={controlsExpanded} />
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { CustomHighlight } from '@grafana/data';
|
||||||
|
|
||||||
|
import { useLogListHighlightContext, LogListHighlightContext } from './LogListHighlightContext';
|
||||||
|
|
||||||
|
// Mock palette length to match typical theme palette size
|
||||||
|
const MOCK_PALETTE_LENGTH = 50;
|
||||||
|
|
||||||
|
describe('LogListHighlightContext', () => {
|
||||||
|
const createWrapper = (highlights: CustomHighlight[], onChange: (highlights: CustomHighlight[]) => void) => {
|
||||||
|
return ({ children }: { children: ReactNode }) => (
|
||||||
|
<LogListHighlightContext.Provider
|
||||||
|
value={{
|
||||||
|
customHighlights: highlights,
|
||||||
|
addHighlight: (text: string) => {
|
||||||
|
const filtered = highlights.filter((h) => h.text !== text);
|
||||||
|
const nextColorIndex = filtered.length % MOCK_PALETTE_LENGTH;
|
||||||
|
onChange([...filtered, { text, colorIndex: nextColorIndex }]);
|
||||||
|
},
|
||||||
|
resetHighlights: () => onChange([]),
|
||||||
|
hasHighlights: highlights.length > 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LogListHighlightContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('provides default context values', () => {
|
||||||
|
const value = {
|
||||||
|
customHighlights: [],
|
||||||
|
addHighlight: jest.fn(),
|
||||||
|
resetHighlights: jest.fn(),
|
||||||
|
hasHighlights: false,
|
||||||
|
};
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<LogListHighlightContext.Provider value={value}>{children}</LogListHighlightContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogListHighlightContext(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current).toEqual(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addHighlight adds a new highlight', () => {
|
||||||
|
let highlights: CustomHighlight[] = [];
|
||||||
|
const onChange = (newHighlights: CustomHighlight[]) => {
|
||||||
|
highlights = newHighlights;
|
||||||
|
};
|
||||||
|
const wrapper = createWrapper(highlights, onChange);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogListHighlightContext(), { wrapper });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addHighlight('test text');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(highlights).toEqual([{ text: 'test text', colorIndex: 0 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('addHighlight re-highlights existing text with next color', () => {
|
||||||
|
let highlights: CustomHighlight[] = [
|
||||||
|
{ text: 'first', colorIndex: 0 },
|
||||||
|
{ text: 'second', colorIndex: 1 },
|
||||||
|
];
|
||||||
|
const onChange = (newHighlights: CustomHighlight[]) => {
|
||||||
|
highlights = newHighlights;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(() => useLogListHighlightContext(), {
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => createWrapper(highlights, onChange)({ children }),
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.addHighlight('first');
|
||||||
|
rerender();
|
||||||
|
});
|
||||||
|
|
||||||
|
// First should be removed and re-added with color index 1 (since only 'second' remains before re-adding)
|
||||||
|
expect(highlights).toEqual([
|
||||||
|
{ text: 'second', colorIndex: 1 },
|
||||||
|
{ text: 'first', colorIndex: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resetHighlights clears all highlights', () => {
|
||||||
|
let highlights: CustomHighlight[] = [
|
||||||
|
{ text: 'first', colorIndex: 0 },
|
||||||
|
{ text: 'second', colorIndex: 1 },
|
||||||
|
];
|
||||||
|
const onChange = (newHighlights: CustomHighlight[]) => {
|
||||||
|
highlights = newHighlights;
|
||||||
|
};
|
||||||
|
const wrapper = createWrapper(highlights, onChange);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogListHighlightContext(), { wrapper });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.resetHighlights();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(highlights).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasHighlights returns true when highlights exist', () => {
|
||||||
|
const highlights: CustomHighlight[] = [{ text: 'test', colorIndex: 0 }];
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const wrapper = createWrapper(highlights, onChange);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogListHighlightContext(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.hasHighlights).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasHighlights returns false when no highlights', () => {
|
||||||
|
const highlights: CustomHighlight[] = [];
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const wrapper = createWrapper(highlights, onChange);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogListHighlightContext(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.hasHighlights).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('color indexes cycle through available colors', () => {
|
||||||
|
let highlights: CustomHighlight[] = [];
|
||||||
|
const onChange = (newHighlights: CustomHighlight[]) => {
|
||||||
|
highlights = newHighlights;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(() => useLogListHighlightContext(), {
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => createWrapper(highlights, onChange)({ children }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add 51 highlights to test cycling (50 colors available)
|
||||||
|
for (let i = 0; i < 51; i++) {
|
||||||
|
act(() => {
|
||||||
|
result.current.addHighlight(`text${i}`);
|
||||||
|
rerender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(highlights).toHaveLength(51);
|
||||||
|
// First highlight has color 0
|
||||||
|
expect(highlights[0].colorIndex).toBe(0);
|
||||||
|
// 50th highlight has color 49
|
||||||
|
expect(highlights[49].colorIndex).toBe(49);
|
||||||
|
// 51st highlight cycles back to color 0
|
||||||
|
expect(highlights[50].colorIndex).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { createContext, ReactNode, useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
import { CustomHighlight } from '@grafana/data';
|
||||||
|
import { useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface LogListHighlightContextData {
|
||||||
|
customHighlights: CustomHighlight[];
|
||||||
|
addHighlight: (text: string) => void;
|
||||||
|
resetHighlights: () => void;
|
||||||
|
hasHighlights: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogListHighlightContext = createContext<LogListHighlightContextData>({
|
||||||
|
customHighlights: [],
|
||||||
|
addHighlight: () => {},
|
||||||
|
resetHighlights: () => {},
|
||||||
|
hasHighlights: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useLogListHighlightContext = (): LogListHighlightContextData => {
|
||||||
|
return useContext(LogListHighlightContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LogListHighlightContextProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
customHighlights: CustomHighlight[];
|
||||||
|
onHighlightsChange: (highlights: CustomHighlight[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogListHighlightContextProvider = ({
|
||||||
|
children,
|
||||||
|
customHighlights,
|
||||||
|
onHighlightsChange,
|
||||||
|
}: LogListHighlightContextProviderProps) => {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const paletteLength = theme.visualization.palette.length;
|
||||||
|
|
||||||
|
const addHighlight = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
// If text already exists, remove it first (to re-highlight with next color)
|
||||||
|
const filtered = customHighlights.filter((h) => h.text !== text);
|
||||||
|
// Auto-assign next color index (cycling through available colors)
|
||||||
|
const nextColorIndex = filtered.length % paletteLength;
|
||||||
|
onHighlightsChange([...filtered, { text, colorIndex: nextColorIndex }]);
|
||||||
|
},
|
||||||
|
[customHighlights, onHighlightsChange, paletteLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetHighlights = useCallback(() => {
|
||||||
|
onHighlightsChange([]);
|
||||||
|
}, [onHighlightsChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogListHighlightContext.Provider
|
||||||
|
value={{
|
||||||
|
customHighlights,
|
||||||
|
addHighlight,
|
||||||
|
resetHighlights,
|
||||||
|
hasHighlights: customHighlights.length > 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LogListHighlightContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { CustomHighlight } from '@grafana/data';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LogListHighlightContext,
|
||||||
|
type LogListHighlightContextData,
|
||||||
|
useLogListHighlightContext,
|
||||||
|
} from '../LogListHighlightContext';
|
||||||
|
|
||||||
|
// Re-export for tests that import from the mock
|
||||||
|
export { LogListHighlightContext, useLogListHighlightContext };
|
||||||
|
export type { LogListHighlightContextData };
|
||||||
|
|
||||||
|
export const defaultValue: LogListHighlightContextData = {
|
||||||
|
customHighlights: [],
|
||||||
|
addHighlight: jest.fn(),
|
||||||
|
resetHighlights: jest.fn(),
|
||||||
|
hasHighlights: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LogListHighlightContextProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
customHighlights?: CustomHighlight[];
|
||||||
|
onHighlightsChange?: (highlights: CustomHighlight[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogListHighlightContextProvider = ({
|
||||||
|
children,
|
||||||
|
customHighlights = [],
|
||||||
|
onHighlightsChange,
|
||||||
|
}: LogListHighlightContextProviderProps) => {
|
||||||
|
return (
|
||||||
|
<LogListHighlightContext.Provider
|
||||||
|
value={{
|
||||||
|
customHighlights,
|
||||||
|
addHighlight: onHighlightsChange !== undefined ? jest.fn() : defaultValue.addHighlight,
|
||||||
|
resetHighlights: onHighlightsChange !== undefined ? jest.fn() : defaultValue.resetHighlights,
|
||||||
|
hasHighlights: customHighlights.length > 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LogListHighlightContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import Prism, { Token } from 'prismjs';
|
import Prism, { Token } from 'prismjs';
|
||||||
|
|
||||||
|
import { CustomHighlight } from '@grafana/data';
|
||||||
|
|
||||||
import { createLogLine } from '../mocks/logRow';
|
import { createLogLine } from '../mocks/logRow';
|
||||||
|
|
||||||
import { generateLogGrammar, generateTextMatchGrammar } from './grammar';
|
import { generateCustomHighlightGrammar, generateLogGrammar, generateTextMatchGrammar } from './grammar';
|
||||||
|
|
||||||
describe('generateLogGrammar', () => {
|
describe('generateLogGrammar', () => {
|
||||||
function generateScenario(entry: string) {
|
function generateScenario(entry: string) {
|
||||||
@@ -125,3 +127,81 @@ describe('generateTextMatchGrammar', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('generateCustomHighlightGrammar', () => {
|
||||||
|
const originalErr = console.error;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
console.error = originalErr;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty grammar for empty highlights array', () => {
|
||||||
|
expect(generateCustomHighlightGrammar([])).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates token for single highlight', () => {
|
||||||
|
const highlights: CustomHighlight[] = [{ text: 'error', colorIndex: 0 }];
|
||||||
|
const grammar = generateCustomHighlightGrammar(highlights) as Record<string, RegExp | RegExp[]>;
|
||||||
|
|
||||||
|
expect(grammar).toHaveProperty('log-search-match log-custom-highlight-0');
|
||||||
|
expect(grammar['log-search-match log-custom-highlight-0']).toEqual(/error/g);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates tokens for multiple highlights with different colors', () => {
|
||||||
|
const highlights: CustomHighlight[] = [
|
||||||
|
{ text: 'error', colorIndex: 0 },
|
||||||
|
{ text: 'warning', colorIndex: 1 },
|
||||||
|
{ text: 'info', colorIndex: 2 },
|
||||||
|
];
|
||||||
|
const grammar = generateCustomHighlightGrammar(highlights) as Record<string, RegExp | RegExp[]>;
|
||||||
|
|
||||||
|
expect(grammar).toHaveProperty('log-search-match log-custom-highlight-0');
|
||||||
|
expect(grammar).toHaveProperty('log-search-match log-custom-highlight-1');
|
||||||
|
expect(grammar).toHaveProperty('log-search-match log-custom-highlight-2');
|
||||||
|
expect(grammar['log-search-match log-custom-highlight-0']).toEqual(/error/g);
|
||||||
|
expect(grammar['log-search-match log-custom-highlight-1']).toEqual(/warning/g);
|
||||||
|
expect(grammar['log-search-match log-custom-highlight-2']).toEqual(/info/g);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapes regex special characters in highlight text', () => {
|
||||||
|
const highlights: CustomHighlight[] = [{ text: 'test.log[0]', colorIndex: 0 }];
|
||||||
|
const grammar = generateCustomHighlightGrammar(highlights) as Record<string, RegExp | RegExp[]>;
|
||||||
|
|
||||||
|
expect(grammar).toHaveProperty('log-search-match log-custom-highlight-0');
|
||||||
|
// Verify it escaped the special characters by trying to match the literal text
|
||||||
|
const regex = grammar['log-search-match log-custom-highlight-0'] as RegExp;
|
||||||
|
expect(regex.test('test.log[0]')).toBe(true);
|
||||||
|
expect(regex.test('testXlogY0Z')).toBe(false); // Should not match if . and [] were treated as regex
|
||||||
|
});
|
||||||
|
|
||||||
|
test('groups multiple highlights with same color index', () => {
|
||||||
|
const highlights: CustomHighlight[] = [
|
||||||
|
{ text: 'error', colorIndex: 0 },
|
||||||
|
{ text: 'failure', colorIndex: 0 },
|
||||||
|
];
|
||||||
|
const grammar = generateCustomHighlightGrammar(highlights) as Record<string, RegExp | RegExp[]>;
|
||||||
|
|
||||||
|
expect(grammar).toHaveProperty('log-search-match log-custom-highlight-0');
|
||||||
|
const tokenValue = grammar['log-search-match log-custom-highlight-0'];
|
||||||
|
expect(Array.isArray(tokenValue)).toBe(true);
|
||||||
|
expect(tokenValue).toHaveLength(2);
|
||||||
|
expect(tokenValue).toEqual([/error/g, /failure/g]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles invalid regex gracefully', () => {
|
||||||
|
const highlights: CustomHighlight[] = [{ text: '(?invalid', colorIndex: 0 }];
|
||||||
|
// Should not throw, but may log error to console (which we've mocked)
|
||||||
|
expect(() => generateCustomHighlightGrammar(highlights)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses case-sensitive matching', () => {
|
||||||
|
const highlights: CustomHighlight[] = [{ text: 'Error', colorIndex: 0 }];
|
||||||
|
const grammar = generateCustomHighlightGrammar(highlights) as Record<string, RegExp | RegExp[]>;
|
||||||
|
const regex = grammar['log-search-match log-custom-highlight-0'] as RegExp;
|
||||||
|
|
||||||
|
expect(regex.test('Error')).toBe(true);
|
||||||
|
expect(regex.test('error')).toBe(false); // Case-sensitive
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Grammar } from 'prismjs';
|
import { Grammar, GrammarValue } from 'prismjs';
|
||||||
|
|
||||||
import { escapeRegex, parseFlags } from '@grafana/data';
|
import { CustomHighlight, escapeRegex, parseFlags } from '@grafana/data';
|
||||||
|
|
||||||
import { LogListModel } from './processing';
|
import { LogListModel } from './processing';
|
||||||
|
|
||||||
@@ -86,3 +86,39 @@ export const generateTextMatchGrammar = (highlightWords: string[] | undefined =
|
|||||||
const cleanNeedle = (needle: string): string => {
|
const cleanNeedle = (needle: string): string => {
|
||||||
return needle.replace(/[[{(][\w,.\/:;<=>?:*+]+$/, '');
|
return needle.replace(/[[{(][\w,.\/:;<=>?:*+]+$/, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateCustomHighlightGrammar = (highlights: CustomHighlight[]): Grammar => {
|
||||||
|
if (!highlights.length) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const grammar: Record<string, GrammarValue> = {};
|
||||||
|
|
||||||
|
// Create separate token types for each color index
|
||||||
|
// This allows different CSS classes for different colors
|
||||||
|
highlights.forEach((highlight) => {
|
||||||
|
const tokenName = `log-search-match log-custom-highlight-${highlight.colorIndex}`;
|
||||||
|
const escapedText = escapeRegex(highlight.text);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Case-sensitive matching (using 'g' flag only, not 'gi')
|
||||||
|
const regex = new RegExp(escapedText, 'g');
|
||||||
|
|
||||||
|
// If token already exists for this color, add to array
|
||||||
|
if (grammar[tokenName]) {
|
||||||
|
const existing = grammar[tokenName];
|
||||||
|
if (Array.isArray(existing)) {
|
||||||
|
existing.push(regex);
|
||||||
|
} else {
|
||||||
|
grammar[tokenName] = [existing, regex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grammar[tokenName] = regex;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`generateCustomHighlightGrammar: cannot generate regular expression from /${escapedText}/g`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return grammar;
|
||||||
|
};
|
||||||
|
|||||||
@@ -278,6 +278,66 @@ Value"
|
|||||||
expect(logListModel.isJSON).toBe(false);
|
expect(logListModel.isJSON).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setCustomHighlights invalidates highlight tokens', () => {
|
||||||
|
const logListModel = createLogLine(
|
||||||
|
{ labels: { place: 'luna' }, entry: 'error message error' },
|
||||||
|
{
|
||||||
|
escape: false,
|
||||||
|
order: LogsSortOrder.Descending,
|
||||||
|
timeZone: 'browser',
|
||||||
|
wrapLogMessage: true,
|
||||||
|
customHighlights: [{ text: 'error', colorIndex: 0 }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access highlightedBodyTokens to populate the cache
|
||||||
|
const initialTokens = logListModel.highlightedBodyTokens;
|
||||||
|
expect(initialTokens).toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ type: 'log-search-match log-custom-highlight-0' })])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update custom highlights
|
||||||
|
logListModel.setCustomHighlights([{ text: 'message', colorIndex: 1 }]);
|
||||||
|
|
||||||
|
// After setCustomHighlights, tokens should be regenerated with new highlights
|
||||||
|
const updatedTokens = logListModel.highlightedBodyTokens;
|
||||||
|
expect(updatedTokens).not.toEqual(initialTokens);
|
||||||
|
expect(updatedTokens).toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ type: 'log-search-match log-custom-highlight-1' })])
|
||||||
|
);
|
||||||
|
expect(updatedTokens).not.toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ type: 'log-search-match log-custom-highlight-0' })])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setCustomHighlights with empty array removes custom highlights', () => {
|
||||||
|
const logListModel = createLogLine(
|
||||||
|
{ labels: { place: 'luna' }, entry: 'error message' },
|
||||||
|
{
|
||||||
|
escape: false,
|
||||||
|
order: LogsSortOrder.Descending,
|
||||||
|
timeZone: 'browser',
|
||||||
|
wrapLogMessage: true,
|
||||||
|
customHighlights: [{ text: 'error', colorIndex: 0 }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Access highlightedBodyTokens with custom highlight
|
||||||
|
const initialTokens = logListModel.highlightedBodyTokens;
|
||||||
|
expect(initialTokens).toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ type: 'log-search-match log-custom-highlight-0' })])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear custom highlights
|
||||||
|
logListModel.setCustomHighlights([]);
|
||||||
|
|
||||||
|
// After clearing, no custom highlight tokens should exist
|
||||||
|
const updatedTokens = logListModel.highlightedBodyTokens;
|
||||||
|
expect(updatedTokens).not.toEqual(
|
||||||
|
expect.arrayContaining([expect.objectContaining({ type: 'log-search-match log-custom-highlight-0' })])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('OTel logs', () => {
|
describe('OTel logs', () => {
|
||||||
const originalState = config.featureToggles.otelLogsFormatting;
|
const originalState = config.featureToggles.otelLogsFormatting;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { LosslessNumber, parse, stringify } from 'lossless-json';
|
|||||||
import Prism, { Grammar, Token } from 'prismjs';
|
import Prism, { Grammar, Token } from 'prismjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CustomHighlight,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
dateTimeFormat,
|
dateTimeFormat,
|
||||||
Labels,
|
Labels,
|
||||||
@@ -20,7 +21,7 @@ import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
|
|||||||
import { FieldDef, getAllFields } from '../logParser';
|
import { FieldDef, getAllFields } from '../logParser';
|
||||||
import { identifyOTelLanguage, getOtelAttributesField, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from '../otel/formats';
|
import { identifyOTelLanguage, getOtelAttributesField, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME } from '../otel/formats';
|
||||||
|
|
||||||
import { generateLogGrammar, generateTextMatchGrammar } from './grammar';
|
import { generateCustomHighlightGrammar, generateLogGrammar, generateTextMatchGrammar } from './grammar';
|
||||||
import { LogLineVirtualization } from './virtualization';
|
import { LogLineVirtualization } from './virtualization';
|
||||||
|
|
||||||
const TRUNCATION_DEFAULT_LENGTH = 50000;
|
const TRUNCATION_DEFAULT_LENGTH = 50000;
|
||||||
@@ -58,6 +59,7 @@ export class LogListModel implements LogRowModel {
|
|||||||
|
|
||||||
private _body: string | undefined = undefined;
|
private _body: string | undefined = undefined;
|
||||||
private _currentSearch: string | undefined = undefined;
|
private _currentSearch: string | undefined = undefined;
|
||||||
|
private _customHighlights: CustomHighlight[] = [];
|
||||||
private _grammar?: Grammar;
|
private _grammar?: Grammar;
|
||||||
private _highlightedBody: string | undefined = undefined;
|
private _highlightedBody: string | undefined = undefined;
|
||||||
private _highlightedLogAttributesTokens: Array<string | Token> | undefined = undefined;
|
private _highlightedLogAttributesTokens: Array<string | Token> | undefined = undefined;
|
||||||
@@ -72,7 +74,16 @@ export class LogListModel implements LogRowModel {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
log: LogRowModel,
|
log: LogRowModel,
|
||||||
{ escape, getFieldLinks, grammar, prettifyJSON, timeZone, virtualization, wrapLogMessage }: PreProcessLogOptions
|
{
|
||||||
|
customHighlights,
|
||||||
|
escape,
|
||||||
|
getFieldLinks,
|
||||||
|
grammar,
|
||||||
|
prettifyJSON,
|
||||||
|
timeZone,
|
||||||
|
virtualization,
|
||||||
|
wrapLogMessage,
|
||||||
|
}: PreProcessLogOptions
|
||||||
) {
|
) {
|
||||||
// LogRowModel
|
// LogRowModel
|
||||||
this.datasourceType = log.datasourceType;
|
this.datasourceType = log.datasourceType;
|
||||||
@@ -101,6 +112,7 @@ export class LogListModel implements LogRowModel {
|
|||||||
|
|
||||||
// LogListModel
|
// LogListModel
|
||||||
this.displayLevel = logLevelToDisplayLevel(log.logLevel);
|
this.displayLevel = logLevelToDisplayLevel(log.logLevel);
|
||||||
|
this._customHighlights = customHighlights ?? [];
|
||||||
this._getFieldLinks = getFieldLinks;
|
this._getFieldLinks = getFieldLinks;
|
||||||
this._grammar = grammar;
|
this._grammar = grammar;
|
||||||
this._prettifyJSON = Boolean(prettifyJSON);
|
this._prettifyJSON = Boolean(prettifyJSON);
|
||||||
@@ -185,8 +197,10 @@ export class LogListModel implements LogRowModel {
|
|||||||
// Body is accessed first to trigger the getter code before generateLogGrammar()
|
// Body is accessed first to trigger the getter code before generateLogGrammar()
|
||||||
const body = this.body;
|
const body = this.body;
|
||||||
this._grammar = this._grammar ?? generateLogGrammar(this);
|
this._grammar = this._grammar ?? generateLogGrammar(this);
|
||||||
|
const customHighlightGrammar = generateCustomHighlightGrammar(this._customHighlights);
|
||||||
const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch);
|
const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch);
|
||||||
this._highlightTokens = Prism.tokenize(body, { ...extraGrammar, ...this._grammar });
|
// Custom highlights first (higher priority), then search, then base grammar
|
||||||
|
this._highlightTokens = Prism.tokenize(body, { ...customHighlightGrammar, ...extraGrammar, ...this._grammar });
|
||||||
}
|
}
|
||||||
return this._highlightTokens;
|
return this._highlightTokens;
|
||||||
}
|
}
|
||||||
@@ -198,8 +212,13 @@ export class LogListModel implements LogRowModel {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
this._grammar = this._grammar ?? generateLogGrammar(this);
|
this._grammar = this._grammar ?? generateLogGrammar(this);
|
||||||
|
const customHighlightGrammar = generateCustomHighlightGrammar(this._customHighlights);
|
||||||
const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch);
|
const extraGrammar = generateTextMatchGrammar(this.searchWords, this._currentSearch);
|
||||||
this._highlightedLogAttributesTokens = Prism.tokenize(attributes, { ...extraGrammar, ...this._grammar });
|
this._highlightedLogAttributesTokens = Prism.tokenize(attributes, {
|
||||||
|
...customHighlightGrammar,
|
||||||
|
...extraGrammar,
|
||||||
|
...this._grammar,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this._highlightedLogAttributesTokens;
|
return this._highlightedLogAttributesTokens;
|
||||||
}
|
}
|
||||||
@@ -275,9 +294,16 @@ export class LogListModel implements LogRowModel {
|
|||||||
this._highlightTokens = undefined;
|
this._highlightTokens = undefined;
|
||||||
this._highlightedLogAttributesTokens = undefined;
|
this._highlightedLogAttributesTokens = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomHighlights(highlights: CustomHighlight[]) {
|
||||||
|
this._customHighlights = highlights;
|
||||||
|
this._highlightTokens = undefined;
|
||||||
|
this._highlightedLogAttributesTokens = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreProcessOptions {
|
export interface PreProcessOptions {
|
||||||
|
customHighlights?: CustomHighlight[];
|
||||||
escape: boolean;
|
escape: boolean;
|
||||||
getFieldLinks?: GetFieldLinksFn;
|
getFieldLinks?: GetFieldLinksFn;
|
||||||
order: LogsSortOrder;
|
order: LogsSortOrder;
|
||||||
@@ -289,12 +315,22 @@ export interface PreProcessOptions {
|
|||||||
|
|
||||||
export const preProcessLogs = (
|
export const preProcessLogs = (
|
||||||
logs: LogRowModel[],
|
logs: LogRowModel[],
|
||||||
{ escape, getFieldLinks, order, prettifyJSON, timeZone, virtualization, wrapLogMessage }: PreProcessOptions,
|
{
|
||||||
|
customHighlights,
|
||||||
|
escape,
|
||||||
|
getFieldLinks,
|
||||||
|
order,
|
||||||
|
prettifyJSON,
|
||||||
|
timeZone,
|
||||||
|
virtualization,
|
||||||
|
wrapLogMessage,
|
||||||
|
}: PreProcessOptions,
|
||||||
grammar?: Grammar
|
grammar?: Grammar
|
||||||
): LogListModel[] => {
|
): LogListModel[] => {
|
||||||
const orderedLogs = sortLogRows(logs, order);
|
const orderedLogs = sortLogRows(logs, order);
|
||||||
return orderedLogs.map((log) =>
|
return orderedLogs.map((log) =>
|
||||||
preProcessLog(log, {
|
preProcessLog(log, {
|
||||||
|
customHighlights,
|
||||||
escape,
|
escape,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
grammar,
|
grammar,
|
||||||
@@ -307,6 +343,7 @@ export const preProcessLogs = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface PreProcessLogOptions {
|
interface PreProcessLogOptions {
|
||||||
|
customHighlights?: CustomHighlight[];
|
||||||
escape: boolean;
|
escape: boolean;
|
||||||
getFieldLinks?: GetFieldLinksFn;
|
getFieldLinks?: GetFieldLinksFn;
|
||||||
grammar?: Grammar;
|
grammar?: Grammar;
|
||||||
@@ -336,7 +373,7 @@ function countNewLines(log: string, limit = Infinity) {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
for (let i = 0; i < log.length; ++i) {
|
for (let i = 0; i < log.length; ++i) {
|
||||||
// No need to iterate further
|
// No need to iterate further
|
||||||
if (count > Infinity) {
|
if (count > limit) {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
if (log[i] === '\n') {
|
if (log[i] === '\n') {
|
||||||
|
|||||||
@@ -10006,6 +10006,7 @@
|
|||||||
"oldest-first": "Sorted by oldest logs first - Click to show newest first",
|
"oldest-first": "Sorted by oldest logs first - Click to show newest first",
|
||||||
"prettify-json": "Expand JSON logs",
|
"prettify-json": "Expand JSON logs",
|
||||||
"remove-escaping": "Remove escaping",
|
"remove-escaping": "Remove escaping",
|
||||||
|
"reset-highlights": "Reset highlights",
|
||||||
"resolution-ms": "ms",
|
"resolution-ms": "ms",
|
||||||
"resolution-ns": "ns",
|
"resolution-ns": "ns",
|
||||||
"scroll-bottom": "Scroll to bottom",
|
"scroll-bottom": "Scroll to bottom",
|
||||||
@@ -10027,7 +10028,8 @@
|
|||||||
"disable-highlighting": "Disable highlighting",
|
"disable-highlighting": "Disable highlighting",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"enable-highlighting": "Enable highlighting",
|
"enable-highlighting": "Enable highlighting",
|
||||||
"filter-level": "Filter logs result by level"
|
"filter-level": "Filter logs result by level",
|
||||||
|
"reset-highlights": "Clear all custom highlights"
|
||||||
},
|
},
|
||||||
"unwrap-lines": "Unwrap lines",
|
"unwrap-lines": "Unwrap lines",
|
||||||
"wrap-lines": "Wrap lines"
|
"wrap-lines": "Wrap lines"
|
||||||
@@ -10067,6 +10069,7 @@
|
|||||||
"popover-menu": {
|
"popover-menu": {
|
||||||
"copy": "Copy selection",
|
"copy": "Copy selection",
|
||||||
"disable-menu": "Disable menu",
|
"disable-menu": "Disable menu",
|
||||||
|
"highlight-occurrences": "Highlight occurrences",
|
||||||
"line-contains": "Add as line contains filter",
|
"line-contains": "Add as line contains filter",
|
||||||
"line-contains-not": "Add as line does not contain filter"
|
"line-contains-not": "Add as line does not contain filter"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user