2022-09-19 16:51:46 +08:00
|
|
|
import { css, cx } from '@emotion/css';
|
2023-01-12 02:20:11 +08:00
|
|
|
import memoizeOne from 'memoize-one';
|
2022-09-19 16:51:46 +08:00
|
|
|
import React, { PureComponent } from 'react';
|
|
|
|
|
|
2023-01-12 02:20:11 +08:00
|
|
|
import { CoreApp, Field, GrafanaTheme2, LinkModel, LogLabelStatsModel, LogRowModel } from '@grafana/data';
|
2022-10-12 21:37:24 +08:00
|
|
|
import { reportInteraction } from '@grafana/runtime';
|
2023-01-12 02:20:11 +08:00
|
|
|
import { ClipboardButton, DataLinkButton, Themeable2, ToolbarButton, ToolbarButtonRow, withTheme2 } from '@grafana/ui';
|
2022-09-19 16:51:46 +08:00
|
|
|
|
|
|
|
|
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;
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields?: string[];
|
|
|
|
|
onClickShowField?: (key: string) => void;
|
|
|
|
|
onClickHideField?: (key: string) => void;
|
2022-10-12 21:37:24 +08:00
|
|
|
row: LogRowModel;
|
2022-10-13 21:26:59 +08:00
|
|
|
app?: CoreApp;
|
2022-09-19 16:51:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
showFieldsStats: boolean;
|
|
|
|
|
fieldCount: number;
|
|
|
|
|
fieldStats: LogLabelStatsModel[] | null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 02:20:11 +08:00
|
|
|
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;
|
2022-09-19 16:51:46 +08:00
|
|
|
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};
|
|
|
|
|
`,
|
2023-01-12 02:20:11 +08:00
|
|
|
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};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-09-19 16:51:46 +08:00
|
|
|
`,
|
|
|
|
|
wrapLine: css`
|
|
|
|
|
label: wrapLine;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
`,
|
2023-01-12 02:20:11 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
2023-01-12 02:20:11 +08:00
|
|
|
});
|
2022-11-04 23:18:55 +08:00
|
|
|
|
2022-09-19 16:51:46 +08:00
|
|
|
class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|
|
|
|
state: State = {
|
|
|
|
|
showFieldsStats: false,
|
|
|
|
|
fieldCount: 0,
|
|
|
|
|
fieldStats: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
showField = () => {
|
2023-01-12 02:20:11 +08:00
|
|
|
const { onClickShowField: onClickShowDetectedField, parsedKey, row } = this.props;
|
2022-09-19 16:51:46 +08:00
|
|
|
if (onClickShowDetectedField) {
|
|
|
|
|
onClickShowDetectedField(parsedKey);
|
|
|
|
|
}
|
2022-10-14 18:10:53 +08:00
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
|
|
|
|
|
datasourceType: row.datasourceType,
|
|
|
|
|
logRowUid: row.uid,
|
|
|
|
|
type: 'enable',
|
|
|
|
|
});
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
hideField = () => {
|
2023-01-12 02:20:11 +08:00
|
|
|
const { onClickHideField: onClickHideDetectedField, parsedKey, row } = this.props;
|
2022-09-19 16:51:46 +08:00
|
|
|
if (onClickHideDetectedField) {
|
|
|
|
|
onClickHideDetectedField(parsedKey);
|
|
|
|
|
}
|
2022-10-14 18:10:53 +08:00
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_replace_line_clicked', {
|
|
|
|
|
datasourceType: row.datasourceType,
|
|
|
|
|
logRowUid: row.uid,
|
|
|
|
|
type: 'disable',
|
|
|
|
|
});
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
filterLabel = () => {
|
2022-10-13 18:21:17 +08:00
|
|
|
const { onClickFilterLabel, parsedKey, parsedValue, row } = this.props;
|
2022-09-19 16:51:46 +08:00
|
|
|
if (onClickFilterLabel) {
|
|
|
|
|
onClickFilterLabel(parsedKey, parsedValue);
|
|
|
|
|
}
|
2022-10-13 18:21:17 +08:00
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
|
|
|
|
|
datasourceType: row.datasourceType,
|
|
|
|
|
filterType: 'include',
|
|
|
|
|
logRowUid: row.uid,
|
|
|
|
|
});
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
filterOutLabel = () => {
|
2022-10-13 18:21:17 +08:00
|
|
|
const { onClickFilterOutLabel, parsedKey, parsedValue, row } = this.props;
|
2022-09-19 16:51:46 +08:00
|
|
|
if (onClickFilterOutLabel) {
|
|
|
|
|
onClickFilterOutLabel(parsedKey, parsedValue);
|
|
|
|
|
}
|
2022-10-13 18:21:17 +08:00
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_filter_clicked', {
|
|
|
|
|
datasourceType: row.datasourceType,
|
|
|
|
|
filterType: 'exclude',
|
|
|
|
|
logRowUid: row.uid,
|
|
|
|
|
});
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
showStats = () => {
|
2022-10-13 21:26:59 +08:00
|
|
|
const { getStats, isLabel, row, app } = this.props;
|
2022-09-19 16:51:46 +08:00
|
|
|
const { showFieldsStats } = this.state;
|
|
|
|
|
if (!showFieldsStats) {
|
2022-10-13 18:21:17 +08:00
|
|
|
const fieldStats = getStats();
|
2022-09-19 16:51:46 +08:00
|
|
|
const fieldCount = fieldStats ? fieldStats.reduce((sum, stat) => sum + stat.count, 0) : 0;
|
|
|
|
|
this.setState({ fieldStats, fieldCount });
|
|
|
|
|
}
|
|
|
|
|
this.toggleFieldsStats();
|
2022-10-12 21:37:24 +08:00
|
|
|
|
|
|
|
|
reportInteraction('grafana_explore_logs_log_details_stats_clicked', {
|
2022-10-13 18:21:17 +08:00
|
|
|
dataSourceType: row.datasourceType,
|
|
|
|
|
fieldType: isLabel ? 'label' : 'detectedField',
|
2022-10-12 21:37:24 +08:00
|
|
|
type: showFieldsStats ? 'close' : 'open',
|
2022-10-13 18:21:17 +08:00
|
|
|
logRowUid: row.uid,
|
2022-10-13 21:26:59 +08:00
|
|
|
app,
|
2022-10-12 21:37:24 +08:00
|
|
|
});
|
2022-09-19 16:51:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
toggleFieldsStats() {
|
|
|
|
|
this.setState((state) => {
|
|
|
|
|
return {
|
|
|
|
|
showFieldsStats: !state.showFieldsStats,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const {
|
|
|
|
|
theme,
|
|
|
|
|
parsedKey,
|
|
|
|
|
parsedValue,
|
|
|
|
|
isLabel,
|
|
|
|
|
links,
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields,
|
2022-09-19 16:51:46 +08:00
|
|
|
wrapLogMessage,
|
|
|
|
|
onClickFilterLabel,
|
|
|
|
|
onClickFilterOutLabel,
|
|
|
|
|
} = this.props;
|
2023-01-12 02:20:11 +08:00
|
|
|
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
|
|
|
|
const activeButton = displayedFields?.includes(parsedKey) || showFieldsStats;
|
|
|
|
|
const styles = getStyles(theme, activeButton);
|
2022-09-19 16:51:46 +08:00
|
|
|
const style = getLogRowStyles(theme);
|
|
|
|
|
const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel;
|
|
|
|
|
|
|
|
|
|
const toggleFieldButton =
|
2023-01-12 02:20:11 +08:00
|
|
|
displayedFields && displayedFields.includes(parsedKey) ? (
|
|
|
|
|
<ToolbarButton variant="active" tooltip="Hide this field" iconOnly narrow icon="eye" onClick={this.hideField} />
|
2022-09-19 16:51:46 +08:00
|
|
|
) : (
|
2023-01-12 02:20:11 +08:00
|
|
|
<ToolbarButton
|
|
|
|
|
tooltip="Show this field instead of the message"
|
|
|
|
|
iconOnly
|
|
|
|
|
narrow
|
|
|
|
|
icon="eye"
|
|
|
|
|
onClick={this.showField}
|
|
|
|
|
/>
|
2022-09-19 16:51:46 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2023-01-12 02:20:11 +08:00
|
|
|
<>
|
|
|
|
|
<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}
|
2022-09-19 16:51:46 +08:00
|
|
|
|
2023-01-12 02:20:11 +08:00
|
|
|
<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}>
|
|
|
|
|
|
|
|
|
|
<DataLinkButton link={link} />
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2022-09-19 16:51:46 +08:00
|
|
|
</td>
|
2023-01-12 02:20:11 +08:00
|
|
|
</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>
|
2022-09-19 16:51:46 +08:00
|
|
|
)}
|
2023-01-12 02:20:11 +08:00
|
|
|
</>
|
2022-09-19 16:51:46 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const LogDetailsRow = withTheme2(UnThemedLogDetailsRow);
|
|
|
|
|
LogDetailsRow.displayName = 'LogDetailsRow';
|