grafana/public/app/features/explore/Logs.tsx
Marcus Efraimsson 02cb7ff436
Explore: Support user timezone (#16469)
Explore now uses the timezone of the user to decide if local browser 
time or UTC should be used. 
- Now uses TimeRange instead of RawTimeRange in explore item
state tree and only parsing actual time in a few action
handlers.
- Time picker should now properly handle moving back/forward and
apply time range when both utc and non utc time zone.
- URL range representation is changed from YYYY-MM-DD HH:mm:ss
to epoch ms.
- Now uses AbsoluteTimeRange in graph component instead of moment.
- Makes a copy of the time range passed to timeSrv to make sure immutability
of explore time range when for example elasticsearch test datasources uses
timeSrv and sets a time range of last 1 min.
- Various refactorings and cleanup.

Closes #12812
2019-04-29 18:28:41 +02:00

302 lines
9.1 KiB
TypeScript

import _ from 'lodash';
import React, { PureComponent } from 'react';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch, LogLevel, TimeZone, TimeRange, AbsoluteTimeRange } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind } from 'app/core/logs_model';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph';
import { LogLabels } from './LogLabels';
import { LogRow } from './LogRow';
const PREVIEW_LIMIT = 100;
const graphOptions = {
series: {
stack: true,
bars: {
show: true,
lineWidth: 5,
// barWidth: 10,
},
// stack: true,
},
yaxis: {
tickDecimals: 0,
},
};
function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) {
return (
<span className="logs-meta-item__labels">
<LogLabels labels={value} plain />
</span>
);
}
return value;
}
interface Props {
data?: LogsModel;
dedupedData?: LogsModel;
width: number;
exploreId: string;
highlighterExpressions: string[];
loading: boolean;
range: TimeRange;
timeZone: TimeZone;
scanning?: boolean;
scanRange?: RawTimeRange;
dedupStrategy: LogsDedupStrategy;
hiddenLogLevels: Set<LogLevel>;
onChangeTime?: (range: AbsoluteTimeRange) => void;
onClickLabel?: (label: string, value: string) => void;
onStartScanning?: () => void;
onStopScanning?: () => void;
onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
onToggleLogLevel: (hiddenLogLevels: Set<LogLevel>) => void;
}
interface State {
deferLogs: boolean;
renderAll: boolean;
showLabels: boolean;
showLocalTime: boolean;
showUtc: boolean;
}
export default class Logs extends PureComponent<Props, State> {
deferLogsTimer: NodeJS.Timer;
renderAllTimer: NodeJS.Timer;
state = {
deferLogs: true,
renderAll: false,
showLabels: false,
showLocalTime: true,
showUtc: false,
};
componentDidMount() {
// Staged rendering
if (this.state.deferLogs) {
const { data } = this.props;
const rowCount = data && data.rows ? data.rows.length : 0;
// Render all right away if not too far over the limit
const renderAll = rowCount <= PREVIEW_LIMIT * 2;
this.deferLogsTimer = setTimeout(() => this.setState({ deferLogs: false, renderAll }), rowCount);
}
}
componentDidUpdate(prevProps, prevState) {
// Staged rendering
if (prevState.deferLogs && !this.state.deferLogs && !this.state.renderAll) {
this.renderAllTimer = setTimeout(() => this.setState({ renderAll: true }), 2000);
}
}
componentWillUnmount() {
clearTimeout(this.deferLogsTimer);
clearTimeout(this.renderAllTimer);
}
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.target as HTMLInputElement;
this.setState({
showLabels: target.checked,
});
};
onChangeLocalTime = (event: React.SyntheticEvent) => {
const target = event.target as HTMLInputElement;
this.setState({
showLocalTime: target.checked,
});
};
onChangeUtc = (event: React.SyntheticEvent) => {
const target = event.target as HTMLInputElement;
this.setState({
showUtc: target.checked,
});
};
onToggleLogLevel = (rawLevel: string, hiddenRawLevels: Set<string>) => {
const hiddenLogLevels: Set<LogLevel> = new Set(Array.from(hiddenRawLevels).map(level => LogLevel[level]));
this.props.onToggleLogLevel(hiddenLogLevels);
};
onClickScan = (event: React.SyntheticEvent) => {
event.preventDefault();
this.props.onStartScanning();
};
onClickStopScan = (event: React.SyntheticEvent) => {
event.preventDefault();
this.props.onStopScanning();
};
render() {
const {
data,
exploreId,
highlighterExpressions,
loading = false,
onClickLabel,
range,
timeZone,
scanning,
scanRange,
width,
dedupedData,
} = this.props;
if (!data) {
return null;
}
const { deferLogs, renderAll, showLabels, showLocalTime, showUtc } = this.state;
const { dedupStrategy } = this.props;
const hasData = data && data.rows && data.rows.length > 0;
const hasLabel = hasData && dedupedData.hasUniqueLabels;
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
const showDuplicates = dedupStrategy !== LogsDedupStrategy.none && dedupCount > 0;
const meta = [...data.meta];
if (dedupStrategy !== LogsDedupStrategy.none) {
meta.push({
label: 'Dedup count',
value: dedupCount,
kind: LogsMetaKind.Number,
});
}
// Staged rendering
const processedRows = dedupedData.rows;
const firstRows = processedRows.slice(0, PREVIEW_LIMIT);
const lastRows = processedRows.slice(PREVIEW_LIMIT);
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
const getRows = () => processedRows;
const timeSeries = data.series.map(series => new TimeSeries(series));
const absRange = {
from: range.from.valueOf(),
to: range.to.valueOf(),
};
return (
<div className="logs-panel">
<div className="logs-panel-graph">
<Graph
data={timeSeries}
height={100}
width={width}
range={absRange}
timeZone={timeZone}
id={`explore-logs-graph-${exploreId}`}
onChangeTime={this.props.onChangeTime}
onToggleSeries={this.onToggleLogLevel}
userOptions={graphOptions}
/>
</div>
<div className="logs-panel-options">
<div className="logs-panel-controls">
<Switch label="Timestamp" checked={showUtc} onChange={this.onChangeUtc} transparent />
<Switch label="Local time" checked={showLocalTime} onChange={this.onChangeLocalTime} transparent />
<Switch label="Labels" checked={showLabels} onChange={this.onChangeLabels} transparent />
<ToggleButtonGroup label="Dedup" transparent={true}>
{Object.keys(LogsDedupStrategy).map((dedupType, i) => (
<ToggleButton
key={i}
value={dedupType}
onChange={this.onChangeDedup}
selected={dedupStrategy === dedupType}
tooltip={LogsDedupDescription[dedupType]}
>
{dedupType}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
</div>
{hasData && meta && (
<div className="logs-panel-meta">
{meta.map(item => (
<div className="logs-panel-meta__item" key={item.label}>
<span className="logs-panel-meta__label">{item.label}:</span>
<span className="logs-panel-meta__value">{renderMetaItem(item.value, item.kind)}</span>
</div>
))}
</div>
)}
<div className="logs-rows">
{hasData &&
!deferLogs && // Only inject highlighterExpression in the first set for performance reasons
firstRows.map(row => (
<LogRow
key={row.key + row.duplicates}
getRows={getRows}
highlighterExpressions={highlighterExpressions}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels && hasLabel}
showLocalTime={showLocalTime}
showUtc={showUtc}
onClickLabel={onClickLabel}
/>
))}
{hasData &&
!deferLogs &&
renderAll &&
lastRows.map(row => (
<LogRow
key={row.key + row.duplicates}
getRows={getRows}
row={row}
showDuplicates={showDuplicates}
showLabels={showLabels && hasLabel}
showLocalTime={showLocalTime}
showUtc={showUtc}
onClickLabel={onClickLabel}
/>
))}
{hasData && deferLogs && <span>Rendering {dedupedData.rows.length} rows...</span>}
</div>
{!loading && !hasData && !scanning && (
<div className="logs-panel-nodata">
No logs found.
<a className="link" onClick={this.onClickScan}>
Scan for older logs
</a>
</div>
)}
{scanning && (
<div className="logs-panel-nodata">
<span>{scanText}</span>
<a className="link" onClick={this.onClickStopScan}>
Stop scan
</a>
</div>
)}
</div>
);
}
}