import React, { PureComponent } from 'react'; import { css } from 'emotion'; import { capitalize } from 'lodash'; import memoizeOne from 'memoize-one'; import { rangeUtil, RawTimeRange, LogLevel, TimeZone, AbsoluteTimeRange, LogsMetaKind, LogsDedupStrategy, LogRowModel, LogsDedupDescription, LogsMetaItem, LogsSortOrder, GraphSeriesXY, LinkModel, Field, GrafanaTheme, } from '@grafana/data'; import { LogLabels, RadioButtonGroup, LogRows, Button, InlineField, InlineFieldRow, InlineSwitch, withTheme, stylesFactory, Icon, Tooltip, } from '@grafana/ui'; import store from 'app/core/store'; import { ExploreGraphPanel } from './ExploreGraphPanel'; import { MetaInfoText } from './MetaInfoText'; import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { MAX_CHARACTERS } from '@grafana/ui/src/components/Logs/LogRowMessage'; const SETTINGS_KEYS = { showLabels: 'grafana.explore.logs.showLabels', showTime: 'grafana.explore.logs.showTime', wrapLogMessage: 'grafana.explore.logs.wrapLogMessage', }; function renderMetaItem(value: any, kind: LogsMetaKind) { if (kind === LogsMetaKind.LabelsMap) { return ( ); } else if (kind === LogsMetaKind.Error) { return {value}; } return value; } interface Props { logRows: LogRowModel[]; logsMeta?: LogsMetaItem[]; logsSeries?: GraphSeriesXY[]; dedupedRows?: LogRowModel[]; visibleRange?: AbsoluteTimeRange; width: number; theme: GrafanaTheme; highlighterExpressions?: string[]; loading: boolean; absoluteRange: AbsoluteTimeRange; timeZone: TimeZone; scanning?: boolean; scanRange?: RawTimeRange; dedupStrategy: LogsDedupStrategy; showContextToggle?: (row?: LogRowModel) => boolean; onChangeTime: (range: AbsoluteTimeRange) => void; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; onStartScanning?: () => void; onStopScanning?: () => void; onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void; onToggleLogLevel: (hiddenLogLevels: LogLevel[]) => void; getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks: (field: Field, rowIndex: number) => Array>; } interface State { showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; logsSortOrder: LogsSortOrder | null; isFlipping: boolean; showDetectedFields: string[]; forceEscape: boolean; } export class UnthemedLogs extends PureComponent { flipOrderTimer: NodeJS.Timeout; cancelFlippingTimer: NodeJS.Timeout; state: State = { showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), showTime: store.getBool(SETTINGS_KEYS.showTime, true), wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true), logsSortOrder: null, isFlipping: false, showDetectedFields: [], forceEscape: false, }; componentWillUnmount() { clearTimeout(this.flipOrderTimer); clearTimeout(this.cancelFlippingTimer); } onChangeLogsSortOrder = () => { this.setState({ isFlipping: true }); // we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs this.flipOrderTimer = setTimeout(() => { this.setState((prevState) => { if (prevState.logsSortOrder === null || prevState.logsSortOrder === LogsSortOrder.Descending) { return { logsSortOrder: LogsSortOrder.Ascending }; } return { logsSortOrder: LogsSortOrder.Descending }; }); }, 0); this.cancelFlippingTimer = setTimeout(() => this.setState({ isFlipping: false }), 1000); }; onEscapeNewlines = () => { this.setState((prevState) => ({ forceEscape: !prevState.forceEscape, })); }; onChangeDedup = (dedup: LogsDedupStrategy) => { const { onDedupStrategyChange } = this.props; if (this.props.dedupStrategy === dedup) { return onDedupStrategyChange(LogsDedupStrategy.none); } return onDedupStrategyChange(dedup); }; onChangeLabels = (event?: React.SyntheticEvent) => { const target = event && (event.target as HTMLInputElement); if (target) { const showLabels = target.checked; this.setState({ showLabels, }); store.set(SETTINGS_KEYS.showLabels, showLabels); } }; onChangeTime = (event?: React.SyntheticEvent) => { const target = event && (event.target as HTMLInputElement); if (target) { const showTime = target.checked; this.setState({ showTime, }); store.set(SETTINGS_KEYS.showTime, showTime); } }; onChangewrapLogMessage = (event?: React.SyntheticEvent) => { const target = event && (event.target as HTMLInputElement); if (target) { const wrapLogMessage = target.checked; this.setState({ wrapLogMessage, }); store.set(SETTINGS_KEYS.wrapLogMessage, wrapLogMessage); } }; onToggleLogLevel = (hiddenRawLevels: string[]) => { const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map((level) => LogLevel[level as LogLevel]); this.props.onToggleLogLevel(hiddenLogLevels); }; onClickScan = (event: React.SyntheticEvent) => { event.preventDefault(); if (this.props.onStartScanning) { this.props.onStartScanning(); } }; onClickStopScan = (event: React.SyntheticEvent) => { event.preventDefault(); if (this.props.onStopScanning) { this.props.onStopScanning(); } }; showDetectedField = (key: string) => { const index = this.state.showDetectedFields.indexOf(key); if (index === -1) { this.setState((state) => { return { showDetectedFields: state.showDetectedFields.concat(key), }; }); } }; hideDetectedField = (key: string) => { const index = this.state.showDetectedFields.indexOf(key); if (index > -1) { this.setState((state) => { return { showDetectedFields: state.showDetectedFields.filter((k) => key !== k), }; }); } }; clearDetectedFields = () => { this.setState((state) => { return { showDetectedFields: [], }; }); }; checkUnescapedContent = memoizeOne((logRows: LogRowModel[]) => { return !!logRows.some((r) => r.hasUnescapedContent); }); render() { const { logRows, logsMeta, logsSeries, visibleRange, highlighterExpressions, loading = false, onClickFilterLabel, onClickFilterOutLabel, timeZone, scanning, scanRange, showContextToggle, width, dedupedRows, absoluteRange, onChangeTime, getFieldLinks, dedupStrategy, theme, } = this.props; const { showLabels, showTime, wrapLogMessage, logsSortOrder, isFlipping, showDetectedFields, forceEscape, } = this.state; const hasData = logRows && logRows.length > 0; const dedupCount = dedupedRows ? dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0) : 0; const meta = logsMeta ? [...logsMeta] : []; if (dedupStrategy !== LogsDedupStrategy.none) { meta.push({ label: 'Dedup count', value: dedupCount, kind: LogsMetaKind.Number, }); } if (logRows.some((r) => r.entry.length > MAX_CHARACTERS)) { meta.push({ label: 'Info', value: 'Logs with more than 100,000 characters could not be parsed and highlighted', kind: LogsMetaKind.String, }); } const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; const series = logsSeries ? logsSeries : []; const styles = getStyles(theme); const hasUnescapedContent = this.checkUnescapedContent(logRows); return ( <>
({ label: capitalize(dedupType), value: dedupType, description: LogsDedupDescription[dedupType], }))} value={dedupStrategy} onChange={this.onChangeDedup} className={styles.radioButtons} />
{meta && ( { return { label: item.label, value: renderMetaItem(item.value, item.kind), }; })} /> )} {showDetectedFields?.length > 0 && ( Show all detected fields ), }, ]} /> )} {hasUnescapedContent && ( ), }, ]} /> )} {!loading && !hasData && !scanning && (
No logs found.
)} {scanning && (
{scanText}
)} ); } } export const Logs = withTheme(UnthemedLogs); const getStyles = stylesFactory((theme: GrafanaTheme) => { return { noData: css` > * { margin-left: 0.5em; } `, logOptions: css` display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; background-color: ${theme.colors.bg1}; padding: ${theme.spacing.sm} ${theme.spacing.md}; border-radius: ${theme.border.radius.md}; margin: ${theme.spacing.md} 0 ${theme.spacing.sm}; border: 1px solid ${theme.colors.panelBorder}; `, flipButton: css` margin: ${theme.spacing.xs} 0 0 ${theme.spacing.sm}; `, radioButtons: css` margin: 0 ${theme.spacing.sm}; `, }; });