Logs: Use GraphNG to plot log histograms (#34531)

* Switch to GraphNG for Logs Histogram

* Remove redundant timeZone

It was used just to format timestamp in the tooltip but it's not needed anymore.

* Add tests for creating logs histogram data

* Update decoractors tests

* Adjust bar width to be more like in the old graph

* Fix tooltip pointer color

* Test tooltip pointer color

* Decouple graph config from uPlot internals

* Ensure nested properties are not mutated when overrides are applied

* Add legend toggling for Explore graphs

* Remove unused component

ExploreGraphNGPanel is now used in Explore

* Code formatting

* allow multiple bars pathBuilders to be globally cached with different settings

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Piotr Jamróz
2021-06-01 09:28:25 +02:00
committed by GitHub
co-authored by Leon Sorokin
parent c6a78a6bd7
commit f4a40a4d85
17 changed files with 272 additions and 402 deletions
@@ -143,6 +143,7 @@ describe('applyFieldOverrides', () => {
// Hardcode the max value // Hardcode the max value
f0.fields[1].config.max = 0; f0.fields[1].config.max = 0;
f0.fields[1].config.decimals = 6; f0.fields[1].config.decimals = 6;
f0.fields[1].config.custom = { value: 1 };
const src: FieldConfigSource = { const src: FieldConfigSource = {
defaults: { defaults: {
@@ -315,6 +316,18 @@ describe('applyFieldOverrides', () => {
expect(data.fields[1].config.decimals).toEqual(1); expect(data.fields[1].config.decimals).toEqual(1);
expect(replaceVariablesCalls[0].__value.value.text).toEqual('100.0'); expect(replaceVariablesCalls[0].__value.value.text).toEqual('100.0');
}); });
it('creates a deep clone of field config', () => {
const data = applyFieldOverrides({
data: [f0], // the frame
fieldConfig: src as FieldConfigSource, // defaults + overrides
replaceVariables: (undefined as any) as InterpolateFunction,
theme: createTheme(),
})[0];
expect(data.fields[1].config).not.toBe(f0.fields[1].config);
expect(data.fields[1].config.custom).not.toBe(f0.fields[1].config.custom);
});
}); });
describe('setFieldConfigDefaults', () => { describe('setFieldConfigDefaults', () => {
@@ -18,7 +18,7 @@ import {
} from '../types'; } from '../types';
import { fieldMatchers, reduceField, ReducerID } from '../transformations'; import { fieldMatchers, reduceField, ReducerID } from '../transformations';
import { FieldMatcher } from '../types/transformations'; import { FieldMatcher } from '../types/transformations';
import { isNumber, set, unset, get } from 'lodash'; import { isNumber, set, unset, get, cloneDeep } from 'lodash';
import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor'; import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor';
import { guessFieldTypeForField } from '../dataframe'; import { guessFieldTypeForField } from '../dataframe';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
@@ -119,7 +119,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
displayName, displayName,
}; };
const config: FieldConfig = { ...field.config }; const config: FieldConfig = { ...cloneDeep(field.config) };
const context = { const context = {
field, field,
data: options.data!, data: options.data!,
+1 -2
View File
@@ -1,5 +1,4 @@
import { Labels } from './data'; import { Labels } from './data';
import { GraphSeriesXY } from './graph';
import { DataFrame } from './dataFrame'; import { DataFrame } from './dataFrame';
import { AbsoluteTimeRange } from './time'; import { AbsoluteTimeRange } from './time';
import { DataQuery } from './datasource'; import { DataQuery } from './datasource';
@@ -84,7 +83,7 @@ export interface LogsModel {
hasUniqueLabels: boolean; hasUniqueLabels: boolean;
meta?: LogsMetaItem[]; meta?: LogsMetaItem[];
rows: LogRowModel[]; rows: LogRowModel[];
series?: GraphSeriesXY[]; series?: DataFrame[];
visibleRange?: AbsoluteTimeRange; visibleRange?: AbsoluteTimeRange;
queries?: DataQuery[]; queries?: DataQuery[];
} }
@@ -179,6 +179,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
lineInterpolation: customConfig.lineInterpolation, lineInterpolation: customConfig.lineInterpolation,
lineStyle: customConfig.lineStyle, lineStyle: customConfig.lineStyle,
barAlignment: customConfig.barAlignment, barAlignment: customConfig.barAlignment,
barWidthFactor: customConfig.barWidthFactor,
barMaxWidth: customConfig.barMaxWidth,
pointSize: customConfig.pointSize, pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor, pointColor: customConfig.pointColor ?? seriesColor,
spanNulls: customConfig.spanNulls || false, spanNulls: customConfig.spanNulls || false,
@@ -108,6 +108,8 @@ export interface LineConfig {
*/ */
export interface BarConfig { export interface BarConfig {
barAlignment?: BarAlignment; barAlignment?: BarAlignment;
barWidthFactor?: number;
barMaxWidth?: number;
} }
/** /**
@@ -41,6 +41,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
lineWidth, lineWidth,
lineStyle, lineStyle,
barAlignment, barAlignment,
barWidthFactor,
barMaxWidth,
showPoints, showPoints,
pointColor, pointColor,
pointSize, pointSize,
@@ -68,7 +70,13 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
lineConfig.dash = lineStyle.dash ?? [10, 10]; lineConfig.dash = lineStyle.dash ?? [10, 10];
} }
lineConfig.paths = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => { lineConfig.paths = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
let pathsBuilder = mapDrawStyleToPathBuilder(drawStyle, lineInterpolation, barAlignment); let pathsBuilder = mapDrawStyleToPathBuilder(
drawStyle,
lineInterpolation,
barAlignment,
barWidthFactor,
barMaxWidth
);
return pathsBuilder(self, seriesIdx, idx0, idx1); return pathsBuilder(self, seriesIdx, idx0, idx1);
}; };
} }
@@ -153,9 +161,7 @@ interface PathBuilders {
smooth: Series.PathBuilder; smooth: Series.PathBuilder;
stepBefore: Series.PathBuilder; stepBefore: Series.PathBuilder;
stepAfter: Series.PathBuilder; stepAfter: Series.PathBuilder;
bars: Series.PathBuilder; [key: string]: Series.PathBuilder;
barsAfter: Series.PathBuilder;
barsBefore: Series.PathBuilder;
} }
let builders: PathBuilders | undefined = undefined; let builders: PathBuilders | undefined = undefined;
@@ -163,35 +169,35 @@ let builders: PathBuilders | undefined = undefined;
function mapDrawStyleToPathBuilder( function mapDrawStyleToPathBuilder(
style: DrawStyle, style: DrawStyle,
lineInterpolation?: LineInterpolation, lineInterpolation?: LineInterpolation,
barAlignment?: BarAlignment barAlignment = 0,
barWidthFactor = 0.6,
barMaxWidth = Infinity
): Series.PathBuilder { ): Series.PathBuilder {
const pathBuilders = uPlot.paths;
if (!builders) { if (!builders) {
// This should be global static, but Jest initalization was failing so we lazy load to avoid the issue // This should be global static, but Jest initalization was failing so we lazy load to avoid the issue
const pathBuilders = uPlot.paths;
const barWidthFactor = 0.6;
const barMaxWidth = Infinity;
builders = { builders = {
linear: pathBuilders.linear!(), linear: pathBuilders.linear!(),
smooth: pathBuilders.spline!(), smooth: pathBuilders.spline!(),
stepBefore: pathBuilders.stepped!({ align: -1 }), stepBefore: pathBuilders.stepped!({ align: -1 }),
stepAfter: pathBuilders.stepped!({ align: 1 }), stepAfter: pathBuilders.stepped!({ align: 1 }),
bars: pathBuilders.bars!({ size: [barWidthFactor, barMaxWidth] }),
barsBefore: pathBuilders.bars!({ size: [barWidthFactor, barMaxWidth], align: -1 }),
barsAfter: pathBuilders.bars!({ size: [barWidthFactor, barMaxWidth], align: 1 }),
}; };
} }
if (style === DrawStyle.Bars) { if (style === DrawStyle.Bars) {
if (barAlignment === BarAlignment.After) { // each bars pathBuilder is lazy-initialized and globally cached by a key composed of its options
return builders.barsAfter; let barsCfgKey = `bars|${barAlignment}|${barWidthFactor}|${barMaxWidth}`;
if (!builders[barsCfgKey]) {
builders[barsCfgKey] = pathBuilders.bars!({
size: [barWidthFactor, barMaxWidth],
align: barAlignment as BarAlignment,
});
} }
if (barAlignment === BarAlignment.Before) {
return builders.barsBefore; return builders[barsCfgKey];
} } else if (style === DrawStyle.Line) {
return builders.bars;
}
if (style === DrawStyle.Line) {
if (lineInterpolation === LineInterpolation.StepBefore) { if (lineInterpolation === LineInterpolation.StepBefore) {
return builders.stepBefore; return builders.stepBefore;
} }
+46 -13
View File
@@ -1,4 +1,5 @@
import { import {
ArrayVector,
DataFrame, DataFrame,
FieldType, FieldType,
LogLevel, LogLevel,
@@ -208,7 +209,7 @@ const emptyLogsModel: any = {
describe('dataFrameToLogsModel', () => { describe('dataFrameToLogsModel', () => {
it('given empty series should return empty logs model', () => { it('given empty series should return empty logs model', () => {
expect(dataFrameToLogsModel([] as DataFrame[], 0, 'utc')).toMatchObject(emptyLogsModel); expect(dataFrameToLogsModel([] as DataFrame[], 0)).toMatchObject(emptyLogsModel);
}); });
it('given series without correct series name should return empty logs model', () => { it('given series without correct series name should return empty logs model', () => {
@@ -217,7 +218,7 @@ describe('dataFrameToLogsModel', () => {
fields: [], fields: [],
}), }),
]; ];
expect(dataFrameToLogsModel(series, 0, 'utc')).toMatchObject(emptyLogsModel); expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
}); });
it('given series without a time field should return empty logs model', () => { it('given series without a time field should return empty logs model', () => {
@@ -232,7 +233,7 @@ describe('dataFrameToLogsModel', () => {
], ],
}), }),
]; ];
expect(dataFrameToLogsModel(series, 0, 'utc')).toMatchObject(emptyLogsModel); expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
}); });
it('given series without a string field should return empty logs model', () => { it('given series without a string field should return empty logs model', () => {
@@ -247,7 +248,7 @@ describe('dataFrameToLogsModel', () => {
], ],
}), }),
]; ];
expect(dataFrameToLogsModel(series, 0, 'utc')).toMatchObject(emptyLogsModel); expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
}); });
it('given one series should return expected logs model', () => { it('given one series should return expected logs model', () => {
@@ -282,7 +283,7 @@ describe('dataFrameToLogsModel', () => {
}, },
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc'); const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.hasUniqueLabels).toBeFalsy(); expect(logsModel.hasUniqueLabels).toBeFalsy();
expect(logsModel.rows).toHaveLength(2); expect(logsModel.rows).toHaveLength(2);
expect(logsModel.rows).toMatchObject([ expect(logsModel.rows).toMatchObject([
@@ -303,6 +304,22 @@ describe('dataFrameToLogsModel', () => {
]); ]);
expect(logsModel.series).toHaveLength(2); expect(logsModel.series).toHaveLength(2);
expect(logsModel.series).toMatchObject([
{
name: 'info',
fields: [
{ type: 'time', values: new ArrayVector([1556270891000, 1556289770000]) },
{ type: 'number', values: new ArrayVector([1, 0]) },
],
},
{
name: 'error',
fields: [
{ type: 'time', values: new ArrayVector([1556289770000]) },
{ type: 'number', values: new ArrayVector([1]) },
],
},
]);
expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: 'Common labels',
@@ -352,7 +369,7 @@ describe('dataFrameToLogsModel', () => {
}, },
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc'); const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.hasUniqueLabels).toBeFalsy(); expect(logsModel.hasUniqueLabels).toBeFalsy();
expect(logsModel.rows).toHaveLength(2); expect(logsModel.rows).toHaveLength(2);
expect(logsModel.rows).toMatchObject([ expect(logsModel.rows).toMatchObject([
@@ -413,7 +430,7 @@ describe('dataFrameToLogsModel', () => {
], ],
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc'); const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.rows).toHaveLength(1); expect(logsModel.rows).toHaveLength(1);
expect(logsModel.rows).toMatchObject([ expect(logsModel.rows).toMatchObject([
{ {
@@ -477,7 +494,7 @@ describe('dataFrameToLogsModel', () => {
], ],
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc'); const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.hasUniqueLabels).toBeTruthy(); expect(logsModel.hasUniqueLabels).toBeTruthy();
expect(logsModel.rows).toHaveLength(3); expect(logsModel.rows).toHaveLength(3);
expect(logsModel.rows).toMatchObject([ expect(logsModel.rows).toMatchObject([
@@ -502,6 +519,22 @@ describe('dataFrameToLogsModel', () => {
]); ]);
expect(logsModel.series).toHaveLength(2); expect(logsModel.series).toHaveLength(2);
expect(logsModel.series).toMatchObject([
{
name: 'error',
fields: [
{ type: 'time', values: new ArrayVector([0, 1000, 2000]) },
{ type: 'number', values: new ArrayVector([1, 0, 1]) },
],
},
{
name: 'debug',
fields: [
{ type: 'time', values: new ArrayVector([1000, 2000]) },
{ type: 'number', values: new ArrayVector([1, 0]) },
],
},
]);
expect(logsModel.meta).toHaveLength(1); expect(logsModel.meta).toHaveLength(1);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: 'Common labels',
@@ -587,7 +620,7 @@ describe('dataFrameToLogsModel', () => {
], ],
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc'); const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.hasUniqueLabels).toBeTruthy(); expect(logsModel.hasUniqueLabels).toBeTruthy();
expect(logsModel.rows).toHaveLength(4); expect(logsModel.rows).toHaveLength(4);
expect(logsModel.rows).toMatchObject([ expect(logsModel.rows).toMatchObject([
@@ -650,7 +683,7 @@ describe('dataFrameToLogsModel', () => {
}, },
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc', { from: 1556270591353, to: 1556289770991 }); const logsModel = dataFrameToLogsModel(series, 1, { from: 1556270591353, to: 1556289770991 });
expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: 'Common labels',
@@ -682,7 +715,7 @@ describe('dataFrameToLogsModel', () => {
], ],
}), }),
]; ];
const logsModel = dataFrameToLogsModel(series, 1, 'utc'); const logsModel = dataFrameToLogsModel(series, 1);
expect(logsModel.rows[0].uid).toBe('0'); expect(logsModel.rows[0].uid).toBe('0');
}); });
}); });
@@ -763,7 +796,7 @@ describe('logSeriesToLogsModel', () => {
}), }),
]; ];
const logsModel = dataFrameToLogsModel(logSeries, 0, 'utc'); const logsModel = dataFrameToLogsModel(logSeries, 0);
expect(logsModel.meta).toMatchObject([ expect(logsModel.meta).toMatchObject([
{ kind: 2, label: 'Common labels', value: { foo: 'bar', level: 'dbug' } }, { kind: 2, label: 'Common labels', value: { foo: 'bar', level: 'dbug' } },
{ kind: 0, label: LIMIT_LABEL, value: 2000 }, { kind: 0, label: LIMIT_LABEL, value: 2000 },
@@ -818,7 +851,7 @@ describe('logSeriesToLogsModel', () => {
}), }),
]; ];
const logsModel = dataFrameToLogsModel(logSeries, 0, 'utc'); const logsModel = dataFrameToLogsModel(logSeries, 0);
expect(logsModel.rows).toHaveLength(3); expect(logsModel.rows).toHaveLength(3);
expect(logsModel.rows).toMatchObject([ expect(logsModel.rows).toMatchObject([
{ {
+35 -64
View File
@@ -1,40 +1,34 @@
import { size } from 'lodash'; import { size } from 'lodash';
import { colors, ansicolor } from '@grafana/ui'; import { ansicolor, BarAlignment, colors, DrawStyle, StackingMode } from '@grafana/ui';
import { import {
Labels, AbsoluteTimeRange,
LogLevel,
DataFrame, DataFrame,
DataQuery,
dateTime,
dateTimeFormat,
dateTimeFormatTimeAgo,
FieldCache,
FieldType,
FieldWithIndex,
findCommonLabels, findCommonLabels,
findUniqueLabels, findUniqueLabels,
getLogLevel, getLogLevel,
FieldType,
getLogLevelFromKey, getLogLevelFromKey,
Labels,
LogLevel,
LogRowModel, LogRowModel,
LogsModel, LogsDedupStrategy,
LogsMetaItem, LogsMetaItem,
LogsMetaKind, LogsMetaKind,
LogsDedupStrategy, LogsModel,
GraphSeriesXY,
dateTimeFormat,
dateTimeFormatTimeAgo,
NullValueMode,
toDataFrame,
FieldCache,
FieldWithIndex,
getFlotPairs,
TimeZone,
getDisplayProcessor,
textUtil,
dateTime,
AbsoluteTimeRange,
sortInAscendingOrder,
rangeUtil, rangeUtil,
DataQuery, sortInAscendingOrder,
textUtil,
toDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { getThemeColor } from 'app/core/utils/colors'; import { getThemeColor } from 'app/core/utils/colors';
import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters'; import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
import { config } from '@grafana/runtime';
export const LIMIT_LABEL = 'Line limit'; export const LIMIT_LABEL = 'Line limit';
@@ -94,7 +88,7 @@ export function filterLogLevels(logRows: LogRowModel[], hiddenLogLevels: Set<Log
}); });
} }
export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number, timeZone: TimeZone): GraphSeriesXY[] { export function makeDataFramesForLogs(sortedRows: LogRowModel[], bucketSize: number): DataFrame[] {
// currently interval is rangeMs / resolution, which is too low for showing series as bars. // currently interval is rangeMs / resolution, which is too low for showing series as bars.
// Should be solved higher up the chain when executing queries & interval calculated and not here but this is a temporary fix. // Should be solved higher up the chain when executing queries & interval calculated and not here but this is a temporary fix.
@@ -109,7 +103,6 @@ export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number,
seriesByLevel[row.logLevel] = series = { seriesByLevel[row.logLevel] = series = {
lastTs: null, lastTs: null,
datapoints: [], datapoints: [],
alias: row.logLevel,
target: row.logLevel, target: row.logLevel,
color: LogLevelColor[row.logLevel], color: LogLevelColor[row.logLevel],
}; };
@@ -141,52 +134,31 @@ export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number,
return seriesList.map((series, i) => { return seriesList.map((series, i) => {
series.datapoints.sort((a: number[], b: number[]) => a[1] - b[1]); series.datapoints.sort((a: number[], b: number[]) => a[1] - b[1]);
// EEEP: converts GraphSeriesXY to DataFrame and back again!
const data = toDataFrame(series); const data = toDataFrame(series);
const fieldCache = new FieldCache(data); const fieldCache = new FieldCache(data);
const timeField = fieldCache.getFirstFieldOfType(FieldType.time)!;
timeField.display = getDisplayProcessor({
field: timeField,
timeZone,
theme: config.theme2,
});
const valueField = fieldCache.getFirstFieldOfType(FieldType.number)!; const valueField = fieldCache.getFirstFieldOfType(FieldType.number)!;
valueField.config = {
...valueField.config,
color: series.color,
};
valueField.name = series.alias; data.fields[valueField.index].config.min = 0;
const fieldDisplayProcessor = getDisplayProcessor({ field: valueField, timeZone, theme: config.theme2 }); data.fields[valueField.index].config.decimals = 0;
valueField.display = (value: any) => ({ ...fieldDisplayProcessor(value), color: series.color });
const points = getFlotPairs({ data.fields[valueField.index].config.custom = {
xField: timeField, drawStyle: DrawStyle.Bars,
yField: valueField, barAlignment: BarAlignment.Center,
nullValueMode: NullValueMode.Null, barWidthFactor: 0.9,
}); barMaxWidth: 5,
lineColor: series.color,
const graphSeries: GraphSeriesXY = { pointColor: series.color,
color: series.color, fillColor: series.color,
label: series.alias, lineWidth: 0,
data: points, fillOpacity: 100,
isVisible: true, stacking: {
yAxis: { mode: StackingMode.Normal,
index: 1, group: 'A',
min: 0,
tickDecimals: 0,
}, },
seriesIndex: i,
timeField,
valueField,
// for now setting the time step to be 0,
// and handle the bar width by setting lineWidth instead of barWidth in flot options
timeStep: 0,
}; };
return graphSeries; return data;
}); });
} }
@@ -203,7 +175,6 @@ function isLogsData(series: DataFrame) {
export function dataFrameToLogsModel( export function dataFrameToLogsModel(
dataFrame: DataFrame[], dataFrame: DataFrame[],
intervalMs: number | undefined, intervalMs: number | undefined,
timeZone: TimeZone,
absoluteRange?: AbsoluteTimeRange, absoluteRange?: AbsoluteTimeRange,
queries?: DataQuery[] queries?: DataQuery[]
): LogsModel { ): LogsModel {
@@ -220,7 +191,7 @@ export function dataFrameToLogsModel(
absoluteRange absoluteRange
); );
logsModel.visibleRange = visibleRange; logsModel.visibleRange = visibleRange;
logsModel.series = makeSeriesForLogs(sortedRows, bucketSize, timeZone); logsModel.series = makeDataFramesForLogs(sortedRows, bucketSize);
if (logsModel.meta) { if (logsModel.meta) {
logsModel.meta = adjustMetaInfo(logsModel, visibleRangeMs, requestedRangeMs); logsModel.meta = adjustMetaInfo(logsModel, visibleRangeMs, requestedRangeMs);
@@ -245,7 +216,7 @@ export function dataFrameToLogsModel(
* Returns a clamped time range and interval based on the visible logs and the given range. * Returns a clamped time range and interval based on the visible logs and the given range.
* *
* @param sortedRows Log rows from the query response * @param sortedRows Log rows from the query response
* @param intervalMs Dynamnic data interval based on available pixel width * @param intervalMs Dynamic data interval based on available pixel width
* @param absoluteRange Requested time range * @param absoluteRange Requested time range
* @param pxPerBar Default: 20, buckets will be rendered as bars, assuming 10px per histogram bar plus some free space around it * @param pxPerBar Default: 20, buckets will be rendered as bars, assuming 10px per histogram bar plus some free space around it
*/ */
+24 -16
View File
@@ -6,7 +6,14 @@ import { connect } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { ErrorBoundaryAlert, stylesFactory, withTheme, CustomScrollbar } from '@grafana/ui'; import {
ErrorBoundaryAlert,
stylesFactory,
withTheme,
CustomScrollbar,
Collapse,
TooltipDisplayMode,
} from '@grafana/ui';
import { import {
AbsoluteTimeRange, AbsoluteTimeRange,
DataQuery, DataQuery,
@@ -222,19 +229,21 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
); );
} }
renderGraphPanel(width: number) { renderGraphPanel() {
const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading } = this.props; const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading } = this.props;
return ( return (
<ExploreGraphNGPanel <Collapse label="Graph" loading={loading} isOpen>
data={graphResult!} <ExploreGraphNGPanel
width={width} data={graphResult!}
absoluteRange={absoluteRange} height={400}
timeZone={timeZone} tooltipDisplayMode={TooltipDisplayMode.Single}
onUpdateTimeRange={this.onUpdateTimeRange} absoluteRange={absoluteRange}
annotations={queryResponse.annotations} timeZone={timeZone}
splitOpenFn={splitOpen} onUpdateTimeRange={this.onUpdateTimeRange}
isLoading={loading} annotations={queryResponse.annotations}
/> splitOpenFn={splitOpen}
/>
</Collapse>
); );
} }
@@ -250,11 +259,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
); );
} }
renderLogsPanel(width: number) { renderLogsPanel() {
const { exploreId, syncedTimes } = this.props; const { exploreId, syncedTimes } = this.props;
return ( return (
<LogsContainer <LogsContainer
width={width}
exploreId={exploreId} exploreId={exploreId}
syncedTimes={syncedTimes} syncedTimes={syncedTimes}
onClickFilterLabel={this.onClickFilterLabel} onClickFilterLabel={this.onClickFilterLabel}
@@ -349,10 +357,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
{showPanels && ( {showPanels && (
<> <>
{showMetrics && graphResult && ( {showMetrics && graphResult && (
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert> <ErrorBoundaryAlert>{this.renderGraphPanel()}</ErrorBoundaryAlert>
)} )}
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>} {showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>} {showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel()}</ErrorBoundaryAlert>}
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>} {showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
{showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>} {showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
</> </>
@@ -9,54 +9,61 @@ import {
Field, Field,
FieldColorModeId, FieldColorModeId,
FieldConfigSource, FieldConfigSource,
getFrameDisplayName,
GrafanaTheme, GrafanaTheme,
TimeZone, TimeZone,
} from '@grafana/data'; } from '@grafana/data';
import { import {
Collapse,
DrawStyle, DrawStyle,
GraphNGLegendEvent,
Icon, Icon,
LegendDisplayMode, LegendDisplayMode,
PanelContext,
PanelContextProvider,
SeriesVisibilityChangeMode,
TimeSeries,
TooltipDisplayMode,
TooltipPlugin, TooltipPlugin,
useStyles, useStyles,
useTheme2, useTheme2,
ZoomPlugin, ZoomPlugin,
TooltipDisplayMode,
TimeSeries,
} from '@grafana/ui'; } from '@grafana/ui';
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config'; import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
import { hideSeriesConfigFactory } from 'app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory';
import { ContextMenuPlugin } from 'app/plugins/panel/timeseries/plugins/ContextMenuPlugin'; import { ContextMenuPlugin } from 'app/plugins/panel/timeseries/plugins/ContextMenuPlugin';
import { ExemplarsPlugin } from 'app/plugins/panel/timeseries/plugins/ExemplarsPlugin'; import { ExemplarsPlugin } from 'app/plugins/panel/timeseries/plugins/ExemplarsPlugin';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useCallback, useMemo, useState, useRef } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { splitOpen } from './state/main'; import { splitOpen } from './state/main';
import { getFieldLinksForExplore } from './utils/links'; import { getFieldLinksForExplore } from './utils/links';
import { usePrevious } from 'react-use'; import { usePrevious } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import appEvents from 'app/core/app_events';
import { seriesVisibilityConfigFactory } from '../dashboard/dashgrid/SeriesVisibilityConfigFactory';
import { identity } from 'lodash';
const MAX_NUMBER_OF_TIME_SERIES = 20; const MAX_NUMBER_OF_TIME_SERIES = 20;
interface Props { interface Props {
data: DataFrame[]; data: DataFrame[];
height: number;
annotations?: DataFrame[]; annotations?: DataFrame[];
isLoading: boolean;
width: number;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
timeZone: TimeZone; timeZone: TimeZone;
onUpdateTimeRange: (absoluteRange: AbsoluteTimeRange) => void; onUpdateTimeRange: (absoluteRange: AbsoluteTimeRange) => void;
splitOpenFn: typeof splitOpen; onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
tooltipDisplayMode: TooltipDisplayMode;
splitOpenFn?: typeof splitOpen;
} }
export function ExploreGraphNGPanel({ export function ExploreGraphNGPanel({
width,
data, data,
height,
timeZone, timeZone,
absoluteRange, absoluteRange,
onUpdateTimeRange, onUpdateTimeRange,
isLoading,
annotations, annotations,
tooltipDisplayMode,
splitOpenFn, splitOpenFn,
onHiddenSeriesChanged,
}: Props) { }: Props) {
const theme = useTheme2(); const theme = useTheme2();
const [showAllTimeSeries, setShowAllTimeSeries] = useState(false); const [showAllTimeSeries, setShowAllTimeSeries] = useState(false);
@@ -107,13 +114,18 @@ export function ExploreGraphNGPanel({
}); });
}, [fieldConfig, data, timeZone, theme]); }, [fieldConfig, data, timeZone, theme]);
const onLegendClick = useCallback( useEffect(() => {
(event: GraphNGLegendEvent) => { if (onHiddenSeriesChanged) {
setBaseStructureRev((r) => r + 1); const hiddenFrames: string[] = [];
setFieldConfig(hideSeriesConfigFactory(event, fieldConfig, data)); dataWithConfig.forEach((frame) => {
}, const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity);
[fieldConfig, data] if (allFieldsHidden) {
); hiddenFrames.push(getFrameDisplayName(frame));
}
});
onHiddenSeriesChanged(hiddenFrames);
}
}, [dataWithConfig, onHiddenSeriesChanged]);
const seriesToShow = showAllTimeSeries ? dataWithConfig : dataWithConfig.slice(0, MAX_NUMBER_OF_TIME_SERIES); const seriesToShow = showAllTimeSeries ? dataWithConfig : dataWithConfig.slice(0, MAX_NUMBER_OF_TIME_SERIES);
@@ -121,8 +133,16 @@ export function ExploreGraphNGPanel({
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn, range: timeRange }); return getFieldLinksForExplore({ field, rowIndex, splitOpenFn, range: timeRange });
}; };
const panelContext: PanelContext = {
eventBus: appEvents,
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
setBaseStructureRev((r) => r + 1);
setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
},
};
return ( return (
<> <PanelContextProvider value={panelContext}>
{dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && ( {dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
<div className={cx([style.timeSeriesDisclaimer])}> <div className={cx([style.timeSeriesDisclaimer])}>
<Icon className={style.disclaimerIcon} name="exclamation-triangle" /> <Icon className={style.disclaimerIcon} name="exclamation-triangle" />
@@ -136,43 +156,43 @@ export function ExploreGraphNGPanel({
>{`Show all ${dataWithConfig.length}`}</span> >{`Show all ${dataWithConfig.length}`}</span>
</div> </div>
)} )}
<AutoSizer disableHeight>
<Collapse label="Graph" loading={isLoading} isOpen> {({ width }) => (
<TimeSeries <TimeSeries
frames={seriesToShow} frames={seriesToShow}
structureRev={structureRev} structureRev={structureRev}
width={width} width={width}
height={400} height={height}
timeRange={timeRange} timeRange={timeRange}
onLegendClick={onLegendClick} legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }} timeZone={timeZone}
timeZone={timeZone} >
> {(config, alignedDataFrame) => {
{(config, alignedDataFrame) => { return (
return ( <>
<> <ZoomPlugin config={config} onZoom={onUpdateTimeRange} />
<ZoomPlugin config={config} onZoom={onUpdateTimeRange} /> <TooltipPlugin
<TooltipPlugin
config={config}
data={alignedDataFrame}
mode={TooltipDisplayMode.Single}
timeZone={timeZone}
/>
<ContextMenuPlugin config={config} data={alignedDataFrame} timeZone={timeZone} />
{annotations && (
<ExemplarsPlugin
config={config} config={config}
exemplars={annotations} data={alignedDataFrame}
mode={tooltipDisplayMode}
timeZone={timeZone} timeZone={timeZone}
getFieldLinks={getFieldLinks}
/> />
)} <ContextMenuPlugin config={config} data={alignedDataFrame} timeZone={timeZone} />
</> {annotations && (
); <ExemplarsPlugin
}} config={config}
</TimeSeries> exemplars={annotations}
</Collapse> timeZone={timeZone}
</> getFieldLinks={getFieldLinks}
/>
)}
</>
);
}}
</TimeSeries>
)}
</AutoSizer>
</PanelContextProvider>
); );
} }
@@ -1,172 +0,0 @@
import React, { PureComponent } from 'react';
import { css, cx } from '@emotion/css';
import { GrafanaTheme, TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTime } from '@grafana/data';
import {
Themeable,
GraphWithLegend,
LegendDisplayMode,
withTheme,
Collapse,
GraphSeriesToggler,
GraphSeriesTogglerAPI,
VizTooltip,
Icon,
TooltipDisplayMode,
} from '@grafana/ui';
const MAX_NUMBER_OF_TIME_SERIES = 20;
const getStyles = (theme: GrafanaTheme) => ({
timeSeriesDisclaimer: css`
label: time-series-disclaimer;
width: 300px;
margin: ${theme.spacing.sm} auto;
padding: 10px 0;
border-radius: ${theme.border.radius.md};
text-align: center;
background-color: ${theme.isLight ? theme.palette.white : theme.palette.dark4};
`,
disclaimerIcon: css`
label: disclaimer-icon;
color: ${theme.palette.yellow};
margin-right: ${theme.spacing.xs};
`,
showAllTimeSeries: css`
label: show-all-time-series;
cursor: pointer;
color: ${theme.colors.linkExternal};
`,
});
interface Props extends Themeable {
ariaLabel?: string;
series?: GraphSeriesXY[] | null;
width: number;
absoluteRange: AbsoluteTimeRange;
loading?: boolean;
showPanel: boolean;
showBars: boolean;
showLines: boolean;
isStacked: boolean;
timeZone?: TimeZone;
onUpdateTimeRange: (absoluteRange: AbsoluteTimeRange) => void;
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
}
interface State {
hiddenSeries: string[];
showAllTimeSeries: boolean;
}
class UnThemedExploreGraphPanel extends PureComponent<Props, State> {
state: State = {
hiddenSeries: [],
showAllTimeSeries: false,
};
onShowAllTimeSeries = () => {
this.setState({
showAllTimeSeries: true,
});
};
onChangeTime = (from: number, to: number) => {
const { onUpdateTimeRange } = this.props;
onUpdateTimeRange({ from, to });
};
renderGraph = () => {
const {
ariaLabel,
width,
series,
onHiddenSeriesChanged,
timeZone,
absoluteRange,
showPanel,
showBars,
showLines,
isStacked,
} = this.props;
const { showAllTimeSeries } = this.state;
if (!series) {
return null;
}
const timeRange = {
from: dateTime(absoluteRange.from),
to: dateTime(absoluteRange.to),
raw: {
from: dateTime(absoluteRange.from),
to: dateTime(absoluteRange.to),
},
};
const height = showPanel ? 200 : 100;
const lineWidth = showLines ? 1 : 5;
const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES);
return (
<GraphSeriesToggler series={seriesToShow} onHiddenSeriesChanged={onHiddenSeriesChanged}>
{({ onSeriesToggle, toggledSeries }: GraphSeriesTogglerAPI) => {
return (
<GraphWithLegend
ariaLabel={ariaLabel}
legendDisplayMode={LegendDisplayMode.List}
height={height}
placement={'bottom'}
width={width}
timeRange={timeRange}
timeZone={timeZone}
showBars={showBars}
showLines={showLines}
showPoints={false}
onToggleSort={() => {}}
series={toggledSeries}
isStacked={isStacked}
lineWidth={lineWidth}
onSeriesToggle={onSeriesToggle}
onHorizontalRegionSelected={this.onChangeTime}
>
{/* For logs we are using mulit mode until we refactor logs histogram to use barWidth instead of lineWidth to render bars */}
<VizTooltip mode={showBars ? TooltipDisplayMode.Multi : TooltipDisplayMode.Single} />
</GraphWithLegend>
);
}}
</GraphSeriesToggler>
);
};
render() {
const { series, showPanel, loading, theme } = this.props;
const { showAllTimeSeries } = this.state;
const style = getStyles(theme);
return (
<>
{series && series.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
<div className={cx([style.timeSeriesDisclaimer])}>
<Icon className={style.disclaimerIcon} name="exclamation-triangle" />
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
<span
className={cx([style.showAllTimeSeries])}
onClick={this.onShowAllTimeSeries}
>{`Show all ${series.length}`}</span>
</div>
)}
{showPanel && (
<Collapse label="Graph" loading={loading} isOpen>
{this.renderGraph()}
</Collapse>
)}
{!showPanel && this.renderGraph()}
</>
);
}
}
export const ExploreGraphPanel = withTheme(UnThemedExploreGraphPanel);
ExploreGraphPanel.displayName = 'ExploreGraphPanel';
+15 -18
View File
@@ -14,11 +14,11 @@ import {
LogsDedupDescription, LogsDedupDescription,
LogsMetaItem, LogsMetaItem,
LogsSortOrder, LogsSortOrder,
GraphSeriesXY,
LinkModel, LinkModel,
Field, Field,
GrafanaTheme, GrafanaTheme,
DataQuery, DataQuery,
DataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { import {
RadioButtonGroup, RadioButtonGroup,
@@ -29,13 +29,14 @@ import {
InlineSwitch, InlineSwitch,
withTheme, withTheme,
stylesFactory, stylesFactory,
TooltipDisplayMode,
} from '@grafana/ui'; } from '@grafana/ui';
import store from 'app/core/store'; import store from 'app/core/store';
import { dedupLogRows, filterLogLevels } from 'app/core/logs_model'; import { dedupLogRows, filterLogLevels } from 'app/core/logs_model';
import { ExploreGraphPanel } from './ExploreGraphPanel';
import { LogsMetaRow } from './LogsMetaRow'; import { LogsMetaRow } from './LogsMetaRow';
import LogsNavigation from './LogsNavigation'; import LogsNavigation from './LogsNavigation';
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
import { ExploreGraphNGPanel } from './ExploreGraphNGPanel';
const SETTINGS_KEYS = { const SETTINGS_KEYS = {
showLabels: 'grafana.explore.logs.showLabels', showLabels: 'grafana.explore.logs.showLabels',
@@ -46,10 +47,9 @@ const SETTINGS_KEYS = {
interface Props { interface Props {
logRows: LogRowModel[]; logRows: LogRowModel[];
logsMeta?: LogsMetaItem[]; logsMeta?: LogsMetaItem[];
logsSeries?: GraphSeriesXY[]; logsSeries?: DataFrame[];
logsQueries?: DataQuery[]; logsQueries?: DataQuery[];
visibleRange?: AbsoluteTimeRange; visibleRange?: AbsoluteTimeRange;
width: number;
theme: GrafanaTheme; theme: GrafanaTheme;
highlighterExpressions?: string[]; highlighterExpressions?: string[];
loading: boolean; loading: boolean;
@@ -240,7 +240,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
scanning, scanning,
scanRange, scanRange,
showContextToggle, showContextToggle,
width,
absoluteRange, absoluteRange,
onChangeTime, onChangeTime,
getFieldLinks, getFieldLinks,
@@ -276,19 +275,17 @@ export class UnthemedLogs extends PureComponent<Props, State> {
<div className={styles.infoText}> <div className={styles.infoText}>
This datasource does not support full-range histograms. The graph is based on the logs seen in the response. This datasource does not support full-range histograms. The graph is based on the logs seen in the response.
</div> </div>
<ExploreGraphPanel {logsSeries && logsSeries.length ? (
series={logsSeries || []} <ExploreGraphNGPanel
width={width} data={logsSeries}
onHiddenSeriesChanged={this.onToggleLogLevel} height={150}
loading={loading} tooltipDisplayMode={TooltipDisplayMode.Multi}
absoluteRange={visibleRange || absoluteRange} absoluteRange={visibleRange || absoluteRange}
isStacked={true} timeZone={timeZone}
showPanel={false} onUpdateTimeRange={onChangeTime}
timeZone={timeZone} onHiddenSeriesChanged={this.onToggleLogLevel}
showBars={true} />
showLines={false} ) : undefined}
onUpdateTimeRange={onChangeTime}
/>
<div className={styles.logOptions} ref={this.topLogsRef}> <div className={styles.logOptions} ref={this.topLogsRef}>
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Time" transparent> <InlineField label="Time" transparent>
@@ -19,7 +19,6 @@ import { getFieldLinksForExplore } from './utils/links';
interface LogsContainerProps extends PropsFromRedux { interface LogsContainerProps extends PropsFromRedux {
exploreId: ExploreId; exploreId: ExploreId;
scanRange?: RawTimeRange; scanRange?: RawTimeRange;
width: number;
syncedTimes: boolean; syncedTimes: boolean;
onClickFilterLabel?: (key: string, value: string) => void; onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
@@ -75,7 +74,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
visibleRange, visibleRange,
scanning, scanning,
range, range,
width,
isLive, isLive,
exploreId, exploreId,
addResultsToCache, addResultsToCache,
@@ -134,7 +132,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
scanning={scanning} scanning={scanning}
scanRange={range.raw} scanRange={range.raw}
showContextToggle={this.showContextToggle} showContextToggle={this.showContextToggle}
width={width}
getRowContext={this.getLogRowContext} getRowContext={this.getLogRowContext}
getFieldLinks={this.getFieldLinks} getFieldLinks={this.getFieldLinks}
addResultsToCache={() => addResultsToCache(exploreId)} addResultsToCache={() => addResultsToCache(exploreId)}
@@ -1,3 +1,5 @@
import { DrawStyle, StackingMode } from '@grafana/ui';
jest.mock('@grafana/data/src/datetime/formatter', () => ({ jest.mock('@grafana/data/src/datetime/formatter', () => ({
dateTimeFormat: () => 'format() jest mocked', dateTimeFormat: () => 'format() jest mocked',
dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked', dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked',
@@ -349,35 +351,34 @@ describe('decorateWithLogsResult', () => {
], ],
series: [ series: [
{ {
label: 'unknown', name: 'unknown',
color: '#8e8e8e', length: 1,
data: [[0, 3]], fields: [
isVisible: true, { name: 'Time', type: 'time', values: new ArrayVector([0]), config: {} },
yAxis: { {
index: 1, name: 'Value',
min: 0, type: 'number',
tickDecimals: 0, labels: undefined,
}, values: new ArrayVector([3]),
seriesIndex: 0, config: {
timeField: { min: 0,
name: 'Time', decimals: 0,
type: 'time', unit: undefined,
config: {}, custom: {
values: new ArrayVector([0]), drawStyle: DrawStyle.Bars,
index: 0, barAlignment: 0,
display: expect.anything(), barMaxWidth: 5,
}, barWidthFactor: 0.9,
valueField: { lineColor: '#8e8e8e',
name: 'unknown', fillColor: '#8e8e8e',
type: 'number', pointColor: '#8e8e8e',
config: { unit: undefined, color: '#8e8e8e' }, lineWidth: 0,
values: new ArrayVector([3]), fillOpacity: 100,
labels: undefined, stacking: { mode: StackingMode.Normal, group: 'A' },
index: 1, },
display: expect.anything(), },
state: expect.anything(), },
}, ],
timeStep: 0,
}, },
], ],
visibleRange: undefined, visibleRange: undefined,
@@ -137,15 +137,8 @@ export const decorateWithLogsResult = (
return { ...data, logsResult: null }; return { ...data, logsResult: null };
} }
const timeZone = data.request?.timezone ?? 'browser';
const intervalMs = data.request?.intervalMs; const intervalMs = data.request?.intervalMs;
const newResults = dataFrameToLogsModel( const newResults = dataFrameToLogsModel(data.logsFrames, intervalMs, options.absoluteRange, options.queries);
data.logsFrames,
intervalMs,
timeZone,
options.absoluteRange,
options.queries
);
const sortOrder = refreshIntervalToSortOrder(options.refreshInterval); const sortOrder = refreshIntervalToSortOrder(options.refreshInterval);
const sortedNewResults = sortLogsResult(newResults, sortOrder); const sortedNewResults = sortLogsResult(newResults, sortOrder);
const rows = sortedNewResults.rows; const rows = sortedNewResults.rows;
@@ -100,7 +100,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
exportLogsAsTxt = () => { exportLogsAsTxt = () => {
const { data, panel } = this.props; const { data, panel } = this.props;
const logsModel = dataFrameToLogsModel(data || [], undefined, 'utc'); const logsModel = dataFrameToLogsModel(data || [], undefined);
let textToDownload = ''; let textToDownload = '';
logsModel.meta?.forEach((metaItem) => { logsModel.meta?.forEach((metaItem) => {
+1 -1
View File
@@ -20,7 +20,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
); );
} }
const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs, timeZone) : null; const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null;
const logRows = newResults?.rows || []; const logRows = newResults?.rows || [];
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy); const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);