mirror of https://github.com/grafana/grafana.git
Logs: Add log line to content outline when clicking on datalinks (#90207)
* feat: add bg color to pinned logs, pin logs when opening datalinks
This commit is contained in:
parent
2d35b11323
commit
1367d5d721
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { LogLevel } from '@grafana/data';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export function contentOutlineTrackPinAdded() {
|
||||||
|
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||||
|
item: 'section',
|
||||||
|
type: 'Logs:pinned:pinned-log-added',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentOutlineTrackPinRemoved() {
|
||||||
|
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||||
|
item: 'section',
|
||||||
|
type: 'Logs:pinned:pinned-log-deleted',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentOutlineTrackPinLimitReached() {
|
||||||
|
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||||
|
item: 'section',
|
||||||
|
type: 'Logs:pinned:pinned-log-limit-reached',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentOutlineTrackPinClicked() {
|
||||||
|
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||||
|
item: 'section',
|
||||||
|
type: 'Logs:pinned:pinned-log-clicked',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentOutlineTrackUnpinClicked() {
|
||||||
|
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||||
|
item: 'section',
|
||||||
|
type: 'Logs:pinned:pinned-log-deleted',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contentOutlineTrackLevelFilter(level: { levelStr: string; logLevel: LogLevel }) {
|
||||||
|
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
||||||
|
item: 'section',
|
||||||
|
type: `Logs:filter:${level.levelStr}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import { useState, useContext, createContext, ReactNode, useCallback, useRef, useEffect } from 'react';
|
import { useState, useContext, createContext, ReactNode, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { SetOptional } from 'type-fest';
|
||||||
|
|
||||||
import { ContentOutlineItemBaseProps, ITEM_TYPES } from './ContentOutlineItem';
|
import { ContentOutlineItemBaseProps, ITEM_TYPES } from './ContentOutlineItem';
|
||||||
|
|
||||||
|
|
@ -10,7 +11,7 @@ export interface ContentOutlineItemContextProps extends ContentOutlineItemBasePr
|
||||||
children?: ContentOutlineItemContextProps[];
|
children?: ContentOutlineItemContextProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegisterFunction = (outlineItem: Omit<ContentOutlineItemContextProps, 'id'>) => string;
|
type RegisterFunction = (outlineItem: SetOptional<ContentOutlineItemContextProps, 'id'>) => string;
|
||||||
|
|
||||||
export interface ContentOutlineContextProps {
|
export interface ContentOutlineContextProps {
|
||||||
outlineItems: ContentOutlineItemContextProps[];
|
outlineItems: ContentOutlineItemContextProps[];
|
||||||
|
|
@ -44,7 +45,10 @@ export function ContentOutlineContextProvider({ children, refreshDependencies }:
|
||||||
const parentlessItemsRef = useRef<ParentlessItems>({});
|
const parentlessItemsRef = useRef<ParentlessItems>({});
|
||||||
|
|
||||||
const register: RegisterFunction = useCallback((outlineItem) => {
|
const register: RegisterFunction = useCallback((outlineItem) => {
|
||||||
const id = uniqueId(`${outlineItem.panelId}-${outlineItem.title}-${outlineItem.icon}_`);
|
// Allow the caller to define unique ID so the outlineItem can be differentiated
|
||||||
|
const id = outlineItem.id
|
||||||
|
? outlineItem.id
|
||||||
|
: uniqueId(`${outlineItem.panelId}-${outlineItem.title}-${outlineItem.icon}_`);
|
||||||
|
|
||||||
setOutlineItems((prevItems) => {
|
setOutlineItems((prevItems) => {
|
||||||
if (outlineItem.level === 'root') {
|
if (outlineItem.level === 'root') {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,14 @@ import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/l
|
||||||
import { getState } from 'app/store/store';
|
import { getState } from 'app/store/store';
|
||||||
import { ExploreItemState, useDispatch } from 'app/types';
|
import { ExploreItemState, useDispatch } from 'app/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
contentOutlineTrackLevelFilter,
|
||||||
|
contentOutlineTrackPinAdded,
|
||||||
|
contentOutlineTrackPinClicked,
|
||||||
|
contentOutlineTrackPinLimitReached,
|
||||||
|
contentOutlineTrackPinRemoved,
|
||||||
|
contentOutlineTrackUnpinClicked,
|
||||||
|
} from '../ContentOutline/ContentOutlineAnalyticEvents';
|
||||||
import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext';
|
import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext';
|
||||||
import { getUrlStateFromPaneState } from '../hooks/useStateSync';
|
import { getUrlStateFromPaneState } from '../hooks/useStateSync';
|
||||||
import { changePanelState } from '../state/explorePane';
|
import { changePanelState } from '../state/explorePane';
|
||||||
|
|
@ -145,7 +153,10 @@ const getDefaultVisualisationType = (): LogsVisualisationType => {
|
||||||
return 'logs';
|
return 'logs';
|
||||||
};
|
};
|
||||||
|
|
||||||
const PINNED_LOGS_LIMIT = 3;
|
const PINNED_LOGS_LIMIT = 10;
|
||||||
|
const PINNED_LOGS_TITLE = 'Pinned log';
|
||||||
|
const PINNED_LOGS_MESSAGE = 'Pin to content outline';
|
||||||
|
const PINNED_LOGS_PANELID = 'Logs';
|
||||||
|
|
||||||
const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -194,7 +205,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
const [forceEscape, setForceEscape] = useState<boolean>(false);
|
const [forceEscape, setForceEscape] = useState<boolean>(false);
|
||||||
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||||
const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
|
const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
|
||||||
const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>('Pin to content outline');
|
const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE);
|
||||||
const [visualisationType, setVisualisationType] = useState<LogsVisualisationType | undefined>(
|
const [visualisationType, setVisualisationType] = useState<LogsVisualisationType | undefined>(
|
||||||
panelState?.logs?.visualisationType ?? getDefaultVisualisationType()
|
panelState?.logs?.visualisationType ?? getDefaultVisualisationType()
|
||||||
);
|
);
|
||||||
|
|
@ -215,6 +226,17 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
const hasData = logRows && logRows.length > 0;
|
const hasData = logRows && logRows.length > 0;
|
||||||
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
||||||
|
|
||||||
|
// Get pinned log lines
|
||||||
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');
|
||||||
|
const pinnedLogs = logsParent?.children
|
||||||
|
?.filter((outlines) => outlines.title === PINNED_LOGS_TITLE)
|
||||||
|
.map((pinnedLogs) => pinnedLogs.id);
|
||||||
|
|
||||||
|
const getPinnedLogsCount = useCallback(() => {
|
||||||
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');
|
||||||
|
return logsParent?.children?.filter((child) => child.title === PINNED_LOGS_TITLE).length ?? 0;
|
||||||
|
}, [outlineItems]);
|
||||||
|
|
||||||
const registerLogLevelsWithContentOutline = useCallback(() => {
|
const registerLogLevelsWithContentOutline = useCallback(() => {
|
||||||
const levelsArr = Object.keys(LogLevelColor);
|
const levelsArr = Object.keys(LogLevelColor);
|
||||||
const logVolumeDataFrames = new Set(logsVolumeData?.data);
|
const logVolumeDataFrames = new Set(logsVolumeData?.data);
|
||||||
|
|
@ -228,7 +250,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
// clean up all current log levels
|
// clean up all current log levels
|
||||||
if (unregisterAllChildren) {
|
if (unregisterAllChildren) {
|
||||||
unregisterAllChildren((items) => {
|
unregisterAllChildren((items) => {
|
||||||
const logsParent = items?.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
const logsParent = items?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');
|
||||||
return logsParent?.id;
|
return logsParent?.id;
|
||||||
}, 'filter');
|
}, 'filter');
|
||||||
}
|
}
|
||||||
|
|
@ -256,16 +278,13 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
register({
|
register({
|
||||||
title: level.levelStr,
|
title: level.levelStr,
|
||||||
icon: 'gf-logs',
|
icon: 'gf-logs',
|
||||||
panelId: 'Logs',
|
panelId: PINNED_LOGS_PANELID,
|
||||||
level: 'child',
|
level: 'child',
|
||||||
type: 'filter',
|
type: 'filter',
|
||||||
highlight: currentLevelSelected && !allLevelsSelected,
|
highlight: currentLevelSelected && !allLevelsSelected,
|
||||||
onClick: (e: React.MouseEvent) => {
|
onClick: (e: React.MouseEvent) => {
|
||||||
toggleLegendRef.current?.(level.levelStr, mapMouseEventToMode(e));
|
toggleLegendRef.current?.(level.levelStr, mapMouseEventToMode(e));
|
||||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
contentOutlineTrackLevelFilter(level);
|
||||||
item: 'section',
|
|
||||||
type: `Logs:filter:${level.levelStr}`,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
ref: null,
|
ref: null,
|
||||||
color: LogLevelColor[level.logLevel],
|
color: LogLevelColor[level.logLevel],
|
||||||
|
|
@ -275,6 +294,21 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
}
|
}
|
||||||
}, [logsVolumeData?.data, unregisterAllChildren, logsVolumeEnabled, hiddenLogLevels, register, toggleLegendRef]);
|
}, [logsVolumeData?.data, unregisterAllChildren, logsVolumeEnabled, hiddenLogLevels, register, toggleLegendRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) {
|
||||||
|
setPinLineButtonTooltipTitle(
|
||||||
|
<span style={{ display: 'flex', textAlign: 'center' }}>
|
||||||
|
❗️
|
||||||
|
<Trans i18nKey="explore.logs.maximum-pinned-logs">
|
||||||
|
Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another.
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setPinLineButtonTooltipTitle(PINNED_LOGS_MESSAGE);
|
||||||
|
}
|
||||||
|
}, [outlineItems, getPinnedLogsCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading && !previousLoading && panelState?.logs?.id) {
|
if (loading && !previousLoading && panelState?.logs?.id) {
|
||||||
// loading stopped, so we need to remove any permalinked log lines
|
// loading stopped, so we need to remove any permalinked log lines
|
||||||
|
|
@ -652,70 +686,47 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
topLogsRef.current?.scrollIntoView();
|
topLogsRef.current?.scrollIntoView();
|
||||||
}, [logsContainerRef, topLogsRef]);
|
}, [logsContainerRef, topLogsRef]);
|
||||||
|
|
||||||
const onPinToContentOutlineClick = (row: LogRowModel) => {
|
const onPinToContentOutlineClick = (row: LogRowModel, allowUnPin = true) => {
|
||||||
if (getPinnedLogsCount() === PINNED_LOGS_LIMIT) {
|
if (getPinnedLogsCount() === PINNED_LOGS_LIMIT && !allowUnPin) {
|
||||||
setPinLineButtonTooltipTitle(
|
contentOutlineTrackPinLimitReached();
|
||||||
<span style={{ display: 'flex', textAlign: 'center' }}>
|
|
||||||
❗️
|
|
||||||
<Trans i18nKey="explore.logs.maximum-pinned-logs">
|
|
||||||
Maximum of {{ PINNED_LOGS_LIMIT }} pinned logs reached. Unpin a log to add another.
|
|
||||||
</Trans>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
|
||||||
item: 'section',
|
|
||||||
type: 'Logs:pinned:pinned-log-limit-reached',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the Logs parent item
|
// find the Logs parent item
|
||||||
const logsParent = outlineItems?.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
const logsParent = outlineItems?.find((item) => item.panelId === PINNED_LOGS_PANELID && item.level === 'root');
|
||||||
|
|
||||||
//update the parent's expanded state
|
//update the parent's expanded state
|
||||||
if (logsParent && updateItem) {
|
if (logsParent && updateItem) {
|
||||||
updateItem(logsParent.id, { expanded: true });
|
updateItem(logsParent.id, { expanded: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
register?.({
|
const alreadyPinned = pinnedLogs?.find((pin) => pin === row.rowId);
|
||||||
icon: 'gf-logs',
|
if (alreadyPinned && row.rowId && allowUnPin) {
|
||||||
title: 'Pinned log',
|
unregister?.(row.rowId);
|
||||||
panelId: 'Logs',
|
contentOutlineTrackPinRemoved();
|
||||||
level: 'child',
|
} else if (getPinnedLogsCount() !== PINNED_LOGS_LIMIT && !alreadyPinned) {
|
||||||
ref: null,
|
register?.({
|
||||||
color: LogLevelColor[row.logLevel],
|
id: row.rowId,
|
||||||
childOnTop: true,
|
icon: 'gf-logs',
|
||||||
onClick: () => {
|
title: PINNED_LOGS_TITLE,
|
||||||
onOpenContext(row, () => {});
|
panelId: PINNED_LOGS_PANELID,
|
||||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
level: 'child',
|
||||||
item: 'section',
|
ref: null,
|
||||||
type: 'Logs:pinned:pinned-log-clicked',
|
color: LogLevelColor[row.logLevel],
|
||||||
});
|
childOnTop: true,
|
||||||
},
|
onClick: () => {
|
||||||
onRemove: (id: string) => {
|
onOpenContext(row, () => {});
|
||||||
unregister?.(id);
|
contentOutlineTrackPinClicked();
|
||||||
if (getPinnedLogsCount() < PINNED_LOGS_LIMIT) {
|
},
|
||||||
setPinLineButtonTooltipTitle('Pin to content outline');
|
onRemove: (id: string) => {
|
||||||
}
|
unregister?.(id);
|
||||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
contentOutlineTrackUnpinClicked();
|
||||||
item: 'section',
|
},
|
||||||
type: 'Logs:pinned:pinned-log-deleted',
|
});
|
||||||
});
|
contentOutlineTrackPinAdded();
|
||||||
},
|
}
|
||||||
});
|
|
||||||
|
|
||||||
props.onPinLineCallback?.();
|
props.onPinLineCallback?.();
|
||||||
|
|
||||||
reportInteraction('explore_toolbar_contentoutline_clicked', {
|
|
||||||
item: 'section',
|
|
||||||
type: 'Logs:pinned:pinned-log-added',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPinnedLogsCount = () => {
|
|
||||||
const logsParent = outlineItems?.find((item) => item.panelId === 'Logs' && item.level === 'root');
|
|
||||||
return logsParent?.children?.filter((child) => child.title === 'Pinned log').length ?? 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasUnescapedContent = checkUnescapedContent(logRows);
|
const hasUnescapedContent = checkUnescapedContent(logRows);
|
||||||
|
|
@ -929,6 +940,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
sortOrder={logsSortOrder}
|
sortOrder={logsSortOrder}
|
||||||
>
|
>
|
||||||
<LogRows
|
<LogRows
|
||||||
|
pinnedLogs={pinnedLogs}
|
||||||
logRows={logRows}
|
logRows={logRows}
|
||||||
deduplicatedRows={dedupedRows}
|
deduplicatedRows={dedupedRows}
|
||||||
dedupStrategy={dedupStrategy}
|
dedupStrategy={dedupStrategy}
|
||||||
|
|
@ -958,6 +970,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||||
containerRendered={!!logsContainerRef}
|
containerRendered={!!logsContainerRef}
|
||||||
onClickFilterString={props.onClickFilterString}
|
onClickFilterString={props.onClickFilterString}
|
||||||
onClickFilterOutString={props.onClickFilterOutString}
|
onClickFilterOutString={props.onClickFilterOutString}
|
||||||
|
onUnpinLine={onPinToContentOutlineClick}
|
||||||
onPinLine={onPinToContentOutlineClick}
|
onPinLine={onPinToContentOutlineClick}
|
||||||
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { cx } from '@emotion/css';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { CoreApp, DataFrame, DataFrameType, Field, LinkModel, LogRowModel } from '@grafana/data';
|
import { CoreApp, DataFrame, DataFrameType, Field, LinkModel, LogRowModel } from '@grafana/data';
|
||||||
import { Themeable2, withTheme2 } from '@grafana/ui';
|
import { PopoverContent, Themeable2, withTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { calculateLogsLabelStats, calculateStats } from '../utils';
|
import { calculateLogsLabelStats, calculateStats } from '../utils';
|
||||||
|
|
||||||
|
|
@ -27,6 +27,9 @@ export interface Props extends Themeable2 {
|
||||||
onClickShowField?: (key: string) => void;
|
onClickShowField?: (key: string) => void;
|
||||||
onClickHideField?: (key: string) => void;
|
onClickHideField?: (key: string) => void;
|
||||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||||
|
|
||||||
|
onPinLine?: (row: LogRowModel) => void;
|
||||||
|
pinLineButtonTooltipTitle?: PopoverContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnThemedLogDetails extends PureComponent<Props> {
|
class UnThemedLogDetails extends PureComponent<Props> {
|
||||||
|
|
@ -46,7 +49,9 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||||
displayedFields,
|
displayedFields,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
wrapLogMessage,
|
wrapLogMessage,
|
||||||
|
onPinLine,
|
||||||
styles,
|
styles,
|
||||||
|
pinLineButtonTooltipTitle,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const levelStyles = getLogLevelStyles(theme, row.logLevel);
|
const levelStyles = getLogLevelStyles(theme, row.logLevel);
|
||||||
const labels = row.labels ? row.labels : {};
|
const labels = row.labels ? row.labels : {};
|
||||||
|
|
@ -151,6 +156,8 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||||
links={links}
|
links={links}
|
||||||
onClickShowField={onClickShowField}
|
onClickShowField={onClickShowField}
|
||||||
onClickHideField={onClickHideField}
|
onClickHideField={onClickHideField}
|
||||||
|
onPinLine={onPinLine}
|
||||||
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
||||||
getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)}
|
getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)}
|
||||||
displayedFields={displayedFields}
|
displayedFields={displayedFields}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
|
|
@ -170,6 +177,8 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||||
links={links}
|
links={links}
|
||||||
onClickShowField={onClickShowField}
|
onClickShowField={onClickShowField}
|
||||||
onClickHideField={onClickHideField}
|
onClickHideField={onClickHideField}
|
||||||
|
onPinLine={onPinLine}
|
||||||
|
pinLineButtonTooltipTitle={pinLineButtonTooltipTitle}
|
||||||
getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)}
|
getStats={() => calculateStats(row.dataFrame.fields[fieldIndex].values)}
|
||||||
displayedFields={displayedFields}
|
displayedFields={displayedFields}
|
||||||
wrapLogMessage={wrapLogMessage}
|
wrapLogMessage={wrapLogMessage}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
import { CoreApp } from '@grafana/data';
|
import { CoreApp, FieldType, LinkModel } from '@grafana/data';
|
||||||
|
import { Field } from '@grafana/data/';
|
||||||
|
|
||||||
import { LogDetailsRow } from './LogDetailsRow';
|
import { LogDetailsRow } from './LogDetailsRow';
|
||||||
import { createLogRow } from './__mocks__/logRow';
|
import { createLogRow } from './__mocks__/logRow';
|
||||||
|
|
@ -147,4 +148,34 @@ describe('LogDetailsRow', () => {
|
||||||
// Asserting visibility on mouse-over is currently not possible.
|
// Asserting visibility on mouse-over is currently not possible.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('datalinks', () => {
|
||||||
|
it('datalinks should pin and call the original link click', () => {
|
||||||
|
const onLinkClick = jest.fn();
|
||||||
|
const onPinLine = jest.fn();
|
||||||
|
const links: Array<LinkModel<Field>> = [
|
||||||
|
{
|
||||||
|
onClick: onLinkClick,
|
||||||
|
href: '#',
|
||||||
|
title: 'Hello link',
|
||||||
|
target: '_self',
|
||||||
|
origin: {
|
||||||
|
name: 'name',
|
||||||
|
type: FieldType.string,
|
||||||
|
config: {},
|
||||||
|
values: ['string'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setup({ links, onPinLine });
|
||||||
|
|
||||||
|
expect(onLinkClick).not.toHaveBeenCalled();
|
||||||
|
expect(onPinLine).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Hello link' }));
|
||||||
|
|
||||||
|
expect(onLinkClick).toHaveBeenCalled();
|
||||||
|
expect(onPinLine).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
LogRowModel,
|
LogRowModel,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { ClipboardButton, DataLinkButton, IconButton, Themeable2, withTheme2 } from '@grafana/ui';
|
import { ClipboardButton, DataLinkButton, IconButton, PopoverContent, Themeable2, withTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { logRowToSingleRowDataFrame } from '../logsModel';
|
import { logRowToSingleRowDataFrame } from '../logsModel';
|
||||||
|
|
||||||
|
|
@ -38,6 +38,8 @@ export interface Props extends Themeable2 {
|
||||||
row: LogRowModel;
|
row: LogRowModel;
|
||||||
app?: CoreApp;
|
app?: CoreApp;
|
||||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||||
|
onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void;
|
||||||
|
pinLineButtonTooltipTitle?: PopoverContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
|
@ -263,6 +265,8 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||||
disableActions,
|
disableActions,
|
||||||
row,
|
row,
|
||||||
app,
|
app,
|
||||||
|
onPinLine,
|
||||||
|
pinLineButtonTooltipTitle,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
@ -324,11 +328,32 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||||
{singleVal ? parsedValues[0] : this.generateMultiVal(parsedValues, true)}
|
{singleVal ? parsedValues[0] : this.generateMultiVal(parsedValues, true)}
|
||||||
{singleVal && this.generateClipboardButton(parsedValues[0])}
|
{singleVal && this.generateClipboardButton(parsedValues[0])}
|
||||||
<div className={cx((singleVal || isMultiParsedValueWithNoContent) && styles.adjoiningLinkButton)}>
|
<div className={cx((singleVal || isMultiParsedValueWithNoContent) && styles.adjoiningLinkButton)}>
|
||||||
{links?.map((link, i) => (
|
{links?.map((link, i) => {
|
||||||
<span key={`${link.title}-${i}`}>
|
if (link.onClick && onPinLine) {
|
||||||
<DataLinkButton link={link} />
|
const originalOnClick = link.onClick;
|
||||||
</span>
|
link.onClick = (e, origin) => {
|
||||||
))}
|
// Pin the line
|
||||||
|
onPinLine(row, false);
|
||||||
|
|
||||||
|
// Execute the link onClick function
|
||||||
|
originalOnClick(e, origin);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span key={`${link.title}-${i}`}>
|
||||||
|
<DataLinkButton
|
||||||
|
buttonProps={{
|
||||||
|
// Show tooltip message if max number of pinned lines has been reached
|
||||||
|
tooltip:
|
||||||
|
typeof pinLineButtonTooltipTitle === 'object' && link.onClick
|
||||||
|
? pinLineButtonTooltipTitle
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
link={link}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import { PureComponent, MouseEvent } from 'react';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { MouseEvent, PureComponent } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Field,
|
|
||||||
LinkModel,
|
|
||||||
LogRowModel,
|
|
||||||
LogsSortOrder,
|
|
||||||
dateTimeFormat,
|
|
||||||
CoreApp,
|
CoreApp,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
|
dateTimeFormat,
|
||||||
|
Field,
|
||||||
|
LinkModel,
|
||||||
LogRowContextOptions,
|
LogRowContextOptions,
|
||||||
|
LogRowModel,
|
||||||
|
LogsSortOrder,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||||
import { withTheme2, Themeable2, Icon, Tooltip, PopoverContent } from '@grafana/ui';
|
import { Icon, PopoverContent, Themeable2, Tooltip, withTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { checkLogsError, escapeUnescapedString, checkLogsSampled } from '../utils';
|
import { checkLogsError, checkLogsSampled, escapeUnescapedString } from '../utils';
|
||||||
|
|
||||||
import { LogDetails } from './LogDetails';
|
import { LogDetails } from './LogDetails';
|
||||||
import { LogLabels } from './LogLabels';
|
import { LogLabels } from './LogLabels';
|
||||||
|
|
@ -59,7 +59,7 @@ interface Props extends Themeable2 {
|
||||||
permalinkedRowId?: string;
|
permalinkedRowId?: string;
|
||||||
scrollIntoView?: (element: HTMLElement) => void;
|
scrollIntoView?: (element: HTMLElement) => void;
|
||||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||||
onPinLine?: (row: LogRowModel) => void;
|
onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void;
|
||||||
onUnpinLine?: (row: LogRowModel) => void;
|
onUnpinLine?: (row: LogRowModel) => void;
|
||||||
pinLineButtonTooltipTitle?: PopoverContent;
|
pinLineButtonTooltipTitle?: PopoverContent;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
|
|
@ -219,14 +219,16 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||||
app,
|
app,
|
||||||
styles,
|
styles,
|
||||||
getRowContextQuery,
|
getRowContextQuery,
|
||||||
|
pinned,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { showDetails, showingContext, permalinked } = this.state;
|
const { showDetails, showingContext, permalinked } = this.state;
|
||||||
const levelStyles = getLogLevelStyles(theme, row.logLevel);
|
const levelStyles = getLogLevelStyles(theme, row.logLevel);
|
||||||
const { errorMessage, hasError } = checkLogsError(row);
|
const { errorMessage, hasError } = checkLogsError(row);
|
||||||
const { sampleMessage, isSampled } = checkLogsSampled(row);
|
const { sampleMessage, isSampled } = checkLogsSampled(row);
|
||||||
const logRowBackground = cx(styles.logsRow, {
|
const logRowBackground = cx(styles.logsRow, {
|
||||||
[styles.errorLogRow]: hasError,
|
[styles.errorLogRow]: hasError,
|
||||||
[styles.highlightBackground]: showingContext || permalinked,
|
[styles.highlightBackground]: showingContext || permalinked || pinned,
|
||||||
});
|
});
|
||||||
const logRowDetailsBackground = cx(styles.logsRow, {
|
const logRowDetailsBackground = cx(styles.logsRow, {
|
||||||
[styles.errorLogRow]: hasError,
|
[styles.errorLogRow]: hasError,
|
||||||
|
|
@ -327,6 +329,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||||
</tr>
|
</tr>
|
||||||
{this.state.showDetails && (
|
{this.state.showDetails && (
|
||||||
<LogDetails
|
<LogDetails
|
||||||
|
onPinLine={this.props.onPinLine}
|
||||||
className={logRowDetailsBackground}
|
className={logRowDetailsBackground}
|
||||||
showDuplicates={showDuplicates}
|
showDuplicates={showDuplicates}
|
||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
|
|
@ -342,6 +345,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||||
app={app}
|
app={app}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||||
|
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export interface Props extends Themeable2 {
|
||||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||||
onClickShowField?: (key: string) => void;
|
onClickShowField?: (key: string) => void;
|
||||||
onClickHideField?: (key: string) => void;
|
onClickHideField?: (key: string) => void;
|
||||||
onPinLine?: (row: LogRowModel) => void;
|
onPinLine?: (row: LogRowModel, allowUnPin?: boolean) => void;
|
||||||
onUnpinLine?: (row: LogRowModel) => void;
|
onUnpinLine?: (row: LogRowModel) => void;
|
||||||
pinLineButtonTooltipTitle?: PopoverContent;
|
pinLineButtonTooltipTitle?: PopoverContent;
|
||||||
onLogRowHover?: (row?: LogRowModel) => void;
|
onLogRowHover?: (row?: LogRowModel) => void;
|
||||||
|
|
@ -63,6 +63,7 @@ export interface Props extends Themeable2 {
|
||||||
scrollIntoView?: (element: HTMLElement) => void;
|
scrollIntoView?: (element: HTMLElement) => void;
|
||||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||||
pinnedRowId?: string;
|
pinnedRowId?: string;
|
||||||
|
pinnedLogs?: string[];
|
||||||
containerRendered?: boolean;
|
containerRendered?: boolean;
|
||||||
/**
|
/**
|
||||||
* If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons.
|
* If false or undefined, the `contain:strict` css property will be added to the wrapping `<table>` for performance reasons.
|
||||||
|
|
@ -191,7 +192,8 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { deduplicatedRows, logRows, dedupStrategy, theme, logsSortOrder, previewLimit, ...rest } = this.props;
|
const { deduplicatedRows, logRows, dedupStrategy, theme, logsSortOrder, previewLimit, pinnedLogs, ...rest } =
|
||||||
|
this.props;
|
||||||
const { renderAll } = this.state;
|
const { renderAll } = this.state;
|
||||||
const styles = getLogRowStyles(theme);
|
const styles = getLogRowStyles(theme);
|
||||||
const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
|
const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows;
|
||||||
|
|
@ -241,7 +243,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||||
onPinLine={this.props.onPinLine}
|
onPinLine={this.props.onPinLine}
|
||||||
onUnpinLine={this.props.onUnpinLine}
|
onUnpinLine={this.props.onUnpinLine}
|
||||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||||
pinned={this.props.pinnedRowId === row.uid}
|
pinned={this.props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)}
|
||||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||||
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|
@ -264,7 +266,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||||
onPinLine={this.props.onPinLine}
|
onPinLine={this.props.onPinLine}
|
||||||
onUnpinLine={this.props.onUnpinLine}
|
onUnpinLine={this.props.onUnpinLine}
|
||||||
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
pinLineButtonTooltipTitle={this.props.pinLineButtonTooltipTitle}
|
||||||
pinned={this.props.pinnedRowId === row.uid}
|
pinned={this.props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)}
|
||||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||||
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
handleTextSelection={this.popoverMenuSupported() ? this.handleSelection : undefined}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue