The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/features/logs/components/LogDetailsRow.tsx

370 lines
10 KiB

import { css, cx } from '@emotion/css';
import memoizeOne from 'memoize-one';
import React, { PureComponent } from 'react';
import { CoreApp, Field, GrafanaTheme2, LinkModel, LogLabelStatsModel, LogRowModel } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { ClipboardButton, DataLinkButton, Themeable2, ToolbarButton, ToolbarButtonRow, withTheme2 } from '@grafana/ui';
import { LogLabelStats } from './LogLabelStats';
import { getLogRowStyles } from './getLogRowStyles';
//Components
export interface Props extends Themeable2 {
parsedValue: string;
parsedKey: string;
wrapLogMessage?: boolean;
isLabel?: boolean;
onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void;
links?: Array<LinkModel<Field>>;
getStats: () => LogLabelStatsModel[] | null;
displayedFields?: string[];
onClickShowField?: (key: string) => void;
onClickHideField?: (key: string) => void;
row: LogRowModel;
app?: CoreApp;
}
interface State {
showFieldsStats: boolean;
fieldCount: number;
fieldStats: LogLabelStatsModel[] | null;
}
const getStyles = memoizeOne((theme: GrafanaTheme2, activeButton: boolean) => {
// those styles come from ToolbarButton. Unfortunately this is needed because we can not control the variant of the menu-button in a ToolbarButtonRow.
const defaultOld = css`
color: ${theme.colors.text.secondary};
background-color: ${theme.colors.background.primary};
&:hover {
color: ${theme.colors.text.primary};
background: ${theme.colors.background.secondary};
}
`;
const defaultTopNav = css`
color: ${theme.colors.text.secondary};
background-color: transparent;
border-color: transparent;
&:hover {
color: ${theme.colors.text.primary};
background: ${theme.colors.background.secondary};
}
`;
const active = css`
color: ${theme.v1.palette.orangeDark};
border-color: ${theme.v1.palette.orangeDark};
background-color: transparent;
&:hover {
color: ${theme.colors.text.primary};
background: ${theme.colors.emphasize(theme.colors.background.canvas, 0.03)};
}
`;
const defaultToolbarButtonStyle = theme.flags.topnav ? defaultTopNav : defaultOld;
return {
noHoverBackground: css`
label: noHoverBackground;
:hover {
background-color: transparent;
}
`,
hoverCursor: css`
label: hoverCursor;
cursor: pointer;
`,
wordBreakAll: css`
label: wordBreakAll;
word-break: break-all;
`,
showingField: css`
color: ${theme.colors.primary.text};
`,
copyButton: css`
& > button {
color: ${theme.colors.text.secondary};
padding: 0;
justify-content: center;
border-radius: 50%;
height: ${theme.spacing(theme.components.height.sm)};
width: ${theme.spacing(theme.components.height.sm)};
svg {
margin: 0;
}
span > div {
top: -5px;
& button {
color: ${theme.colors.success.main};
}
}
}
`,
wrapLine: css`
label: wrapLine;
white-space: pre-wrap;
`,
toolbarButtonRow: css`
label: toolbarButtonRow;
gap: ${theme.spacing(0.5)};
max-width: calc(3 * ${theme.spacing(theme.components.height.sm)});
& > div {
height: ${theme.spacing(theme.components.height.sm)};
width: ${theme.spacing(theme.components.height.sm)};
& > button {
border: 0;
background-color: transparent;
height: inherit;
&:hover {
box-shadow: none;
border-radius: 50%;
}
}
}
& div:last-child > button:not(.stats-button) {
${activeButton ? active : defaultToolbarButtonStyle};
}
`,
logDetailsStats: css`
padding: 0 ${theme.spacing(1)};
`,
logDetailsValue: css`
display: table-cell;
vertical-align: middle;
line-height: 22px;
.show-on-hover {
display: inline;
visibility: hidden;
}
&:hover {
.show-on-hover {
visibility: visible;
}
}
`,
};
});
class UnThemedLogDetailsRow extends PureComponent<Props, State> {
state: State = {
showFieldsStats: false,
fieldCount: 0,
fieldStats: null,
};
showField = () => {
const { onClickShowField: onClickShowDetectedField, parsedKey, row } = this.props;
if (onClickShowDetectedField) {
onClickShowDetectedField(parsedKey);
}
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
datasourceType: row.datasourceType,
logRowUid: row.uid,
type: 'enable',
});
};
hideField = () => {
const { onClickHideField: onClickHideDetectedField, parsedKey, row } = this.props;
if (onClickHideDetectedField) {
onClickHideDetectedField(parsedKey);
}
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
datasourceType: row.datasourceType,
logRowUid: row.uid,
type: 'disable',
});
};
filterLabel = () => {
const { onClickFilterLabel, parsedKey, parsedValue, row } = this.props;
if (onClickFilterLabel) {
onClickFilterLabel(parsedKey, parsedValue);
}
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
datasourceType: row.datasourceType,
filterType: 'include',
logRowUid: row.uid,
});
};
filterOutLabel = () => {
const { onClickFilterOutLabel, parsedKey, parsedValue, row } = this.props;
if (onClickFilterOutLabel) {
onClickFilterOutLabel(parsedKey, parsedValue);
}
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
datasourceType: row.datasourceType,
filterType: 'exclude',
logRowUid: row.uid,
});
};
showStats = () => {
const { getStats, isLabel, row, app } = this.props;
const { showFieldsStats } = this.state;
if (!showFieldsStats) {
const fieldStats = getStats();
const fieldCount = fieldStats ? fieldStats.reduce((sum, stat) => sum + stat.count, 0) : 0;
this.setState({ fieldStats, fieldCount });
}
this.toggleFieldsStats();
reportInteraction('grafana_explore_logs_log_details_stats_clicked', {
dataSourceType: row.datasourceType,
fieldType: isLabel ? 'label' : 'detectedField',
type: showFieldsStats ? 'close' : 'open',
logRowUid: row.uid,
app,
});
};
toggleFieldsStats() {
this.setState((state) => {
return {
showFieldsStats: !state.showFieldsStats,
};
});
}
render() {
const {
theme,
parsedKey,
parsedValue,
isLabel,
links,
displayedFields,
wrapLogMessage,
onClickFilterLabel,
onClickFilterOutLabel,
} = this.props;
const { showFieldsStats, fieldStats, fieldCount } = this.state;
const activeButton = displayedFields?.includes(parsedKey) || showFieldsStats;
const styles = getStyles(theme, activeButton);
const style = getLogRowStyles(theme);
const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel;
const toggleFieldButton =
displayedFields && displayedFields.includes(parsedKey) ? (
<ToolbarButton variant="active" tooltip="Hide this field" iconOnly narrow icon="eye" onClick={this.hideField} />
) : (
<ToolbarButton
tooltip="Show this field instead of the message"
iconOnly
narrow
icon="eye"
onClick={this.showField}
/>
);
return (
<>
<tr className={cx(style.logDetailsValue)}>
<td className={style.logsDetailsIcon}>
<ToolbarButtonRow alignment="left" className={styles.toolbarButtonRow}>
{hasFilteringFunctionality && (
<ToolbarButton
iconOnly
narrow
icon="search-plus"
tooltip="Filter for value"
onClick={this.filterLabel}
/>
)}
{hasFilteringFunctionality && (
<ToolbarButton
iconOnly
narrow
icon="search-minus"
tooltip="Filter out value"
onClick={this.filterOutLabel}
/>
)}
{displayedFields && toggleFieldButton}
<ToolbarButton
iconOnly
variant={showFieldsStats ? 'active' : 'default'}
narrow
icon="signal"
tooltip="Ad-hoc statistics"
className="stats-button"
onClick={this.showStats}
/>
</ToolbarButtonRow>
</td>
{/* Key - value columns */}
<td className={style.logDetailsLabel}>{parsedKey}</td>
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
<div className={styles.logDetailsValue}>
{parsedValue}
<div className={cx('show-on-hover', styles.copyButton)}>
<ClipboardButton
getText={() => parsedValue}
title="Copy value to clipboard"
fill="text"
variant="secondary"
icon="copy"
size="md"
/>
</div>
{links?.map((link) => (
<span key={link.title}>
&nbsp;
<DataLinkButton link={link} />
</span>
))}
</div>
</td>
</tr>
{showFieldsStats && (
<tr>
<td>
<ToolbarButtonRow alignment="left" className={styles.toolbarButtonRow}>
<ToolbarButton
iconOnly
variant={showFieldsStats ? 'active' : 'default'}
narrow
icon="signal"
tooltip="Hide ad-hoc statistics"
onClick={this.showStats}
/>
</ToolbarButtonRow>
</td>
<td colSpan={2}>
<div className={styles.logDetailsStats}>
<LogLabelStats
stats={fieldStats!}
label={parsedKey}
value={parsedValue}
rowCount={fieldCount}
isLabel={isLabel}
/>
</div>
</td>
</tr>
)}
</>
);
}
}
export const LogDetailsRow = withTheme2(UnThemedLogDetailsRow);
LogDetailsRow.displayName = 'LogDetailsRow';