mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c6a78a6bd7
commit
f4a40a4d85
@ -143,6 +143,7 @@ describe('applyFieldOverrides', () => {
|
||||
// Hardcode the max value
|
||||
f0.fields[1].config.max = 0;
|
||||
f0.fields[1].config.decimals = 6;
|
||||
f0.fields[1].config.custom = { value: 1 };
|
||||
|
||||
const src: FieldConfigSource = {
|
||||
defaults: {
|
||||
@ -315,6 +316,18 @@ describe('applyFieldOverrides', () => {
|
||||
expect(data.fields[1].config.decimals).toEqual(1);
|
||||
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', () => {
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
} from '../types';
|
||||
import { fieldMatchers, reduceField, ReducerID } from '../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 { guessFieldTypeForField } from '../dataframe';
|
||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||
@ -119,7 +119,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
displayName,
|
||||
};
|
||||
|
||||
const config: FieldConfig = { ...field.config };
|
||||
const config: FieldConfig = { ...cloneDeep(field.config) };
|
||||
const context = {
|
||||
field,
|
||||
data: options.data!,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Labels } from './data';
|
||||
import { GraphSeriesXY } from './graph';
|
||||
import { DataFrame } from './dataFrame';
|
||||
import { AbsoluteTimeRange } from './time';
|
||||
import { DataQuery } from './datasource';
|
||||
@ -84,7 +83,7 @@ export interface LogsModel {
|
||||
hasUniqueLabels: boolean;
|
||||
meta?: LogsMetaItem[];
|
||||
rows: LogRowModel[];
|
||||
series?: GraphSeriesXY[];
|
||||
series?: DataFrame[];
|
||||
visibleRange?: AbsoluteTimeRange;
|
||||
queries?: DataQuery[];
|
||||
}
|
||||
|
@ -179,6 +179,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
lineInterpolation: customConfig.lineInterpolation,
|
||||
lineStyle: customConfig.lineStyle,
|
||||
barAlignment: customConfig.barAlignment,
|
||||
barWidthFactor: customConfig.barWidthFactor,
|
||||
barMaxWidth: customConfig.barMaxWidth,
|
||||
pointSize: customConfig.pointSize,
|
||||
pointColor: customConfig.pointColor ?? seriesColor,
|
||||
spanNulls: customConfig.spanNulls || false,
|
||||
|
@ -108,6 +108,8 @@ export interface LineConfig {
|
||||
*/
|
||||
export interface BarConfig {
|
||||
barAlignment?: BarAlignment;
|
||||
barWidthFactor?: number;
|
||||
barMaxWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,6 +41,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
lineWidth,
|
||||
lineStyle,
|
||||
barAlignment,
|
||||
barWidthFactor,
|
||||
barMaxWidth,
|
||||
showPoints,
|
||||
pointColor,
|
||||
pointSize,
|
||||
@ -68,7 +70,13 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
lineConfig.dash = lineStyle.dash ?? [10, 10];
|
||||
}
|
||||
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);
|
||||
};
|
||||
}
|
||||
@ -153,9 +161,7 @@ interface PathBuilders {
|
||||
smooth: Series.PathBuilder;
|
||||
stepBefore: Series.PathBuilder;
|
||||
stepAfter: Series.PathBuilder;
|
||||
bars: Series.PathBuilder;
|
||||
barsAfter: Series.PathBuilder;
|
||||
barsBefore: Series.PathBuilder;
|
||||
[key: string]: Series.PathBuilder;
|
||||
}
|
||||
|
||||
let builders: PathBuilders | undefined = undefined;
|
||||
@ -163,35 +169,35 @@ let builders: PathBuilders | undefined = undefined;
|
||||
function mapDrawStyleToPathBuilder(
|
||||
style: DrawStyle,
|
||||
lineInterpolation?: LineInterpolation,
|
||||
barAlignment?: BarAlignment
|
||||
barAlignment = 0,
|
||||
barWidthFactor = 0.6,
|
||||
barMaxWidth = Infinity
|
||||
): Series.PathBuilder {
|
||||
const pathBuilders = uPlot.paths;
|
||||
|
||||
if (!builders) {
|
||||
// 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 = {
|
||||
linear: pathBuilders.linear!(),
|
||||
smooth: pathBuilders.spline!(),
|
||||
stepBefore: 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 (barAlignment === BarAlignment.After) {
|
||||
return builders.barsAfter;
|
||||
// each bars pathBuilder is lazy-initialized and globally cached by a key composed of its options
|
||||
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.bars;
|
||||
}
|
||||
if (style === DrawStyle.Line) {
|
||||
|
||||
return builders[barsCfgKey];
|
||||
} else if (style === DrawStyle.Line) {
|
||||
if (lineInterpolation === LineInterpolation.StepBefore) {
|
||||
return builders.stepBefore;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
FieldType,
|
||||
LogLevel,
|
||||
@ -208,7 +209,7 @@ const emptyLogsModel: any = {
|
||||
|
||||
describe('dataFrameToLogsModel', () => {
|
||||
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', () => {
|
||||
@ -217,7 +218,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
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', () => {
|
||||
@ -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', () => {
|
||||
@ -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', () => {
|
||||
@ -282,7 +283,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const logsModel = dataFrameToLogsModel(series, 1, 'utc');
|
||||
const logsModel = dataFrameToLogsModel(series, 1);
|
||||
expect(logsModel.hasUniqueLabels).toBeFalsy();
|
||||
expect(logsModel.rows).toHaveLength(2);
|
||||
expect(logsModel.rows).toMatchObject([
|
||||
@ -303,6 +304,22 @@ describe('dataFrameToLogsModel', () => {
|
||||
]);
|
||||
|
||||
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![0]).toMatchObject({
|
||||
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.rows).toHaveLength(2);
|
||||
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).toMatchObject([
|
||||
{
|
||||
@ -477,7 +494,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
],
|
||||
}),
|
||||
];
|
||||
const logsModel = dataFrameToLogsModel(series, 1, 'utc');
|
||||
const logsModel = dataFrameToLogsModel(series, 1);
|
||||
expect(logsModel.hasUniqueLabels).toBeTruthy();
|
||||
expect(logsModel.rows).toHaveLength(3);
|
||||
expect(logsModel.rows).toMatchObject([
|
||||
@ -502,6 +519,22 @@ describe('dataFrameToLogsModel', () => {
|
||||
]);
|
||||
|
||||
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![0]).toMatchObject({
|
||||
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.rows).toHaveLength(4);
|
||||
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![0]).toMatchObject({
|
||||
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');
|
||||
});
|
||||
});
|
||||
@ -763,7 +796,7 @@ describe('logSeriesToLogsModel', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const logsModel = dataFrameToLogsModel(logSeries, 0, 'utc');
|
||||
const logsModel = dataFrameToLogsModel(logSeries, 0);
|
||||
expect(logsModel.meta).toMatchObject([
|
||||
{ kind: 2, label: 'Common labels', value: { foo: 'bar', level: 'dbug' } },
|
||||
{ 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).toMatchObject([
|
||||
{
|
||||
|
@ -1,40 +1,34 @@
|
||||
import { size } from 'lodash';
|
||||
import { colors, ansicolor } from '@grafana/ui';
|
||||
import { ansicolor, BarAlignment, colors, DrawStyle, StackingMode } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
Labels,
|
||||
LogLevel,
|
||||
AbsoluteTimeRange,
|
||||
DataFrame,
|
||||
DataQuery,
|
||||
dateTime,
|
||||
dateTimeFormat,
|
||||
dateTimeFormatTimeAgo,
|
||||
FieldCache,
|
||||
FieldType,
|
||||
FieldWithIndex,
|
||||
findCommonLabels,
|
||||
findUniqueLabels,
|
||||
getLogLevel,
|
||||
FieldType,
|
||||
getLogLevelFromKey,
|
||||
Labels,
|
||||
LogLevel,
|
||||
LogRowModel,
|
||||
LogsModel,
|
||||
LogsDedupStrategy,
|
||||
LogsMetaItem,
|
||||
LogsMetaKind,
|
||||
LogsDedupStrategy,
|
||||
GraphSeriesXY,
|
||||
dateTimeFormat,
|
||||
dateTimeFormatTimeAgo,
|
||||
NullValueMode,
|
||||
toDataFrame,
|
||||
FieldCache,
|
||||
FieldWithIndex,
|
||||
getFlotPairs,
|
||||
TimeZone,
|
||||
getDisplayProcessor,
|
||||
textUtil,
|
||||
dateTime,
|
||||
AbsoluteTimeRange,
|
||||
sortInAscendingOrder,
|
||||
LogsModel,
|
||||
rangeUtil,
|
||||
DataQuery,
|
||||
sortInAscendingOrder,
|
||||
textUtil,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
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.
|
||||
// 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 = {
|
||||
lastTs: null,
|
||||
datapoints: [],
|
||||
alias: row.logLevel,
|
||||
target: row.logLevel,
|
||||
color: LogLevelColor[row.logLevel],
|
||||
};
|
||||
@ -141,52 +134,31 @@ export function makeSeriesForLogs(sortedRows: LogRowModel[], bucketSize: number,
|
||||
return seriesList.map((series, i) => {
|
||||
series.datapoints.sort((a: number[], b: number[]) => a[1] - b[1]);
|
||||
|
||||
// EEEP: converts GraphSeriesXY to DataFrame and back again!
|
||||
const data = toDataFrame(series);
|
||||
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)!;
|
||||
valueField.config = {
|
||||
...valueField.config,
|
||||
color: series.color,
|
||||
};
|
||||
|
||||
valueField.name = series.alias;
|
||||
const fieldDisplayProcessor = getDisplayProcessor({ field: valueField, timeZone, theme: config.theme2 });
|
||||
valueField.display = (value: any) => ({ ...fieldDisplayProcessor(value), color: series.color });
|
||||
data.fields[valueField.index].config.min = 0;
|
||||
data.fields[valueField.index].config.decimals = 0;
|
||||
|
||||
const points = getFlotPairs({
|
||||
xField: timeField,
|
||||
yField: valueField,
|
||||
nullValueMode: NullValueMode.Null,
|
||||
});
|
||||
|
||||
const graphSeries: GraphSeriesXY = {
|
||||
color: series.color,
|
||||
label: series.alias,
|
||||
data: points,
|
||||
isVisible: true,
|
||||
yAxis: {
|
||||
index: 1,
|
||||
min: 0,
|
||||
tickDecimals: 0,
|
||||
data.fields[valueField.index].config.custom = {
|
||||
drawStyle: DrawStyle.Bars,
|
||||
barAlignment: BarAlignment.Center,
|
||||
barWidthFactor: 0.9,
|
||||
barMaxWidth: 5,
|
||||
lineColor: series.color,
|
||||
pointColor: series.color,
|
||||
fillColor: series.color,
|
||||
lineWidth: 0,
|
||||
fillOpacity: 100,
|
||||
stacking: {
|
||||
mode: StackingMode.Normal,
|
||||
group: 'A',
|
||||
},
|
||||
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(
|
||||
dataFrame: DataFrame[],
|
||||
intervalMs: number | undefined,
|
||||
timeZone: TimeZone,
|
||||
absoluteRange?: AbsoluteTimeRange,
|
||||
queries?: DataQuery[]
|
||||
): LogsModel {
|
||||
@ -220,7 +191,7 @@ export function dataFrameToLogsModel(
|
||||
absoluteRange
|
||||
);
|
||||
logsModel.visibleRange = visibleRange;
|
||||
logsModel.series = makeSeriesForLogs(sortedRows, bucketSize, timeZone);
|
||||
logsModel.series = makeDataFramesForLogs(sortedRows, bucketSize);
|
||||
|
||||
if (logsModel.meta) {
|
||||
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.
|
||||
*
|
||||
* @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 pxPerBar Default: 20, buckets will be rendered as bars, assuming 10px per histogram bar plus some free space around it
|
||||
*/
|
||||
|
@ -6,7 +6,14 @@ import { connect } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import memoizeOne from 'memoize-one';
|
||||
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 {
|
||||
AbsoluteTimeRange,
|
||||
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;
|
||||
return (
|
||||
<ExploreGraphNGPanel
|
||||
data={graphResult!}
|
||||
width={width}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onUpdateTimeRange={this.onUpdateTimeRange}
|
||||
annotations={queryResponse.annotations}
|
||||
splitOpenFn={splitOpen}
|
||||
isLoading={loading}
|
||||
/>
|
||||
<Collapse label="Graph" loading={loading} isOpen>
|
||||
<ExploreGraphNGPanel
|
||||
data={graphResult!}
|
||||
height={400}
|
||||
tooltipDisplayMode={TooltipDisplayMode.Single}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onUpdateTimeRange={this.onUpdateTimeRange}
|
||||
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;
|
||||
return (
|
||||
<LogsContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
syncedTimes={syncedTimes}
|
||||
onClickFilterLabel={this.onClickFilterLabel}
|
||||
@ -349,10 +357,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
{showPanels && (
|
||||
<>
|
||||
{showMetrics && graphResult && (
|
||||
<ErrorBoundaryAlert>{this.renderGraphPanel(width)}</ErrorBoundaryAlert>
|
||||
<ErrorBoundaryAlert>{this.renderGraphPanel()}</ErrorBoundaryAlert>
|
||||
)}
|
||||
{showTable && <ErrorBoundaryAlert>{this.renderTablePanel(width)}</ErrorBoundaryAlert>}
|
||||
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel(width)}</ErrorBoundaryAlert>}
|
||||
{showLogs && <ErrorBoundaryAlert>{this.renderLogsPanel()}</ErrorBoundaryAlert>}
|
||||
{showNodeGraph && <ErrorBoundaryAlert>{this.renderNodeGraphPanel()}</ErrorBoundaryAlert>}
|
||||
{showTrace && <ErrorBoundaryAlert>{this.renderTraceViewPanel()}</ErrorBoundaryAlert>}
|
||||
</>
|
||||
|
@ -9,54 +9,61 @@ import {
|
||||
Field,
|
||||
FieldColorModeId,
|
||||
FieldConfigSource,
|
||||
getFrameDisplayName,
|
||||
GrafanaTheme,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
Collapse,
|
||||
DrawStyle,
|
||||
GraphNGLegendEvent,
|
||||
Icon,
|
||||
LegendDisplayMode,
|
||||
PanelContext,
|
||||
PanelContextProvider,
|
||||
SeriesVisibilityChangeMode,
|
||||
TimeSeries,
|
||||
TooltipDisplayMode,
|
||||
TooltipPlugin,
|
||||
useStyles,
|
||||
useTheme2,
|
||||
ZoomPlugin,
|
||||
TooltipDisplayMode,
|
||||
TimeSeries,
|
||||
} from '@grafana/ui';
|
||||
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 { ExemplarsPlugin } from 'app/plugins/panel/timeseries/plugins/ExemplarsPlugin';
|
||||
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 { getFieldLinksForExplore } from './utils/links';
|
||||
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;
|
||||
|
||||
interface Props {
|
||||
data: DataFrame[];
|
||||
height: number;
|
||||
annotations?: DataFrame[];
|
||||
isLoading: boolean;
|
||||
width: number;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
timeZone: TimeZone;
|
||||
onUpdateTimeRange: (absoluteRange: AbsoluteTimeRange) => void;
|
||||
splitOpenFn: typeof splitOpen;
|
||||
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
|
||||
tooltipDisplayMode: TooltipDisplayMode;
|
||||
splitOpenFn?: typeof splitOpen;
|
||||
}
|
||||
|
||||
export function ExploreGraphNGPanel({
|
||||
width,
|
||||
data,
|
||||
height,
|
||||
timeZone,
|
||||
absoluteRange,
|
||||
onUpdateTimeRange,
|
||||
isLoading,
|
||||
annotations,
|
||||
tooltipDisplayMode,
|
||||
splitOpenFn,
|
||||
onHiddenSeriesChanged,
|
||||
}: Props) {
|
||||
const theme = useTheme2();
|
||||
const [showAllTimeSeries, setShowAllTimeSeries] = useState(false);
|
||||
@ -107,13 +114,18 @@ export function ExploreGraphNGPanel({
|
||||
});
|
||||
}, [fieldConfig, data, timeZone, theme]);
|
||||
|
||||
const onLegendClick = useCallback(
|
||||
(event: GraphNGLegendEvent) => {
|
||||
setBaseStructureRev((r) => r + 1);
|
||||
setFieldConfig(hideSeriesConfigFactory(event, fieldConfig, data));
|
||||
},
|
||||
[fieldConfig, data]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (onHiddenSeriesChanged) {
|
||||
const hiddenFrames: string[] = [];
|
||||
dataWithConfig.forEach((frame) => {
|
||||
const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity);
|
||||
if (allFieldsHidden) {
|
||||
hiddenFrames.push(getFrameDisplayName(frame));
|
||||
}
|
||||
});
|
||||
onHiddenSeriesChanged(hiddenFrames);
|
||||
}
|
||||
}, [dataWithConfig, onHiddenSeriesChanged]);
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
const panelContext: PanelContext = {
|
||||
eventBus: appEvents,
|
||||
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
|
||||
setBaseStructureRev((r) => r + 1);
|
||||
setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelContextProvider value={panelContext}>
|
||||
{dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
|
||||
<div className={cx([style.timeSeriesDisclaimer])}>
|
||||
<Icon className={style.disclaimerIcon} name="exclamation-triangle" />
|
||||
@ -136,43 +156,43 @@ export function ExploreGraphNGPanel({
|
||||
>{`Show all ${dataWithConfig.length}`}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Collapse label="Graph" loading={isLoading} isOpen>
|
||||
<TimeSeries
|
||||
frames={seriesToShow}
|
||||
structureRev={structureRev}
|
||||
width={width}
|
||||
height={400}
|
||||
timeRange={timeRange}
|
||||
onLegendClick={onLegendClick}
|
||||
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
|
||||
timeZone={timeZone}
|
||||
>
|
||||
{(config, alignedDataFrame) => {
|
||||
return (
|
||||
<>
|
||||
<ZoomPlugin config={config} onZoom={onUpdateTimeRange} />
|
||||
<TooltipPlugin
|
||||
config={config}
|
||||
data={alignedDataFrame}
|
||||
mode={TooltipDisplayMode.Single}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
<ContextMenuPlugin config={config} data={alignedDataFrame} timeZone={timeZone} />
|
||||
{annotations && (
|
||||
<ExemplarsPlugin
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<TimeSeries
|
||||
frames={seriesToShow}
|
||||
structureRev={structureRev}
|
||||
width={width}
|
||||
height={height}
|
||||
timeRange={timeRange}
|
||||
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
|
||||
timeZone={timeZone}
|
||||
>
|
||||
{(config, alignedDataFrame) => {
|
||||
return (
|
||||
<>
|
||||
<ZoomPlugin config={config} onZoom={onUpdateTimeRange} />
|
||||
<TooltipPlugin
|
||||
config={config}
|
||||
exemplars={annotations}
|
||||
data={alignedDataFrame}
|
||||
mode={tooltipDisplayMode}
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</TimeSeries>
|
||||
</Collapse>
|
||||
</>
|
||||
<ContextMenuPlugin config={config} data={alignedDataFrame} timeZone={timeZone} />
|
||||
{annotations && (
|
||||
<ExemplarsPlugin
|
||||
config={config}
|
||||
exemplars={annotations}
|
||||
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';
|
@ -14,11 +14,11 @@ import {
|
||||
LogsDedupDescription,
|
||||
LogsMetaItem,
|
||||
LogsSortOrder,
|
||||
GraphSeriesXY,
|
||||
LinkModel,
|
||||
Field,
|
||||
GrafanaTheme,
|
||||
DataQuery,
|
||||
DataFrame,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
RadioButtonGroup,
|
||||
@ -29,13 +29,14 @@ import {
|
||||
InlineSwitch,
|
||||
withTheme,
|
||||
stylesFactory,
|
||||
TooltipDisplayMode,
|
||||
} from '@grafana/ui';
|
||||
import store from 'app/core/store';
|
||||
import { dedupLogRows, filterLogLevels } from 'app/core/logs_model';
|
||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||
import { LogsMetaRow } from './LogsMetaRow';
|
||||
import LogsNavigation from './LogsNavigation';
|
||||
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider';
|
||||
import { ExploreGraphNGPanel } from './ExploreGraphNGPanel';
|
||||
|
||||
const SETTINGS_KEYS = {
|
||||
showLabels: 'grafana.explore.logs.showLabels',
|
||||
@ -46,10 +47,9 @@ const SETTINGS_KEYS = {
|
||||
interface Props {
|
||||
logRows: LogRowModel[];
|
||||
logsMeta?: LogsMetaItem[];
|
||||
logsSeries?: GraphSeriesXY[];
|
||||
logsSeries?: DataFrame[];
|
||||
logsQueries?: DataQuery[];
|
||||
visibleRange?: AbsoluteTimeRange;
|
||||
width: number;
|
||||
theme: GrafanaTheme;
|
||||
highlighterExpressions?: string[];
|
||||
loading: boolean;
|
||||
@ -240,7 +240,6 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
||||
scanning,
|
||||
scanRange,
|
||||
showContextToggle,
|
||||
width,
|
||||
absoluteRange,
|
||||
onChangeTime,
|
||||
getFieldLinks,
|
||||
@ -276,19 +275,17 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
||||
<div className={styles.infoText}>
|
||||
This datasource does not support full-range histograms. The graph is based on the logs seen in the response.
|
||||
</div>
|
||||
<ExploreGraphPanel
|
||||
series={logsSeries || []}
|
||||
width={width}
|
||||
onHiddenSeriesChanged={this.onToggleLogLevel}
|
||||
loading={loading}
|
||||
absoluteRange={visibleRange || absoluteRange}
|
||||
isStacked={true}
|
||||
showPanel={false}
|
||||
timeZone={timeZone}
|
||||
showBars={true}
|
||||
showLines={false}
|
||||
onUpdateTimeRange={onChangeTime}
|
||||
/>
|
||||
{logsSeries && logsSeries.length ? (
|
||||
<ExploreGraphNGPanel
|
||||
data={logsSeries}
|
||||
height={150}
|
||||
tooltipDisplayMode={TooltipDisplayMode.Multi}
|
||||
absoluteRange={visibleRange || absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onUpdateTimeRange={onChangeTime}
|
||||
onHiddenSeriesChanged={this.onToggleLogLevel}
|
||||
/>
|
||||
) : undefined}
|
||||
<div className={styles.logOptions} ref={this.topLogsRef}>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Time" transparent>
|
||||
|
@ -19,7 +19,6 @@ import { getFieldLinksForExplore } from './utils/links';
|
||||
interface LogsContainerProps extends PropsFromRedux {
|
||||
exploreId: ExploreId;
|
||||
scanRange?: RawTimeRange;
|
||||
width: number;
|
||||
syncedTimes: boolean;
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
@ -75,7 +74,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
visibleRange,
|
||||
scanning,
|
||||
range,
|
||||
width,
|
||||
isLive,
|
||||
exploreId,
|
||||
addResultsToCache,
|
||||
@ -134,7 +132,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
scanning={scanning}
|
||||
scanRange={range.raw}
|
||||
showContextToggle={this.showContextToggle}
|
||||
width={width}
|
||||
getRowContext={this.getLogRowContext}
|
||||
getFieldLinks={this.getFieldLinks}
|
||||
addResultsToCache={() => addResultsToCache(exploreId)}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { DrawStyle, StackingMode } from '@grafana/ui';
|
||||
|
||||
jest.mock('@grafana/data/src/datetime/formatter', () => ({
|
||||
dateTimeFormat: () => 'format() jest mocked',
|
||||
dateTimeFormatTimeAgo: (ts: any) => 'fromNow() jest mocked',
|
||||
@ -349,35 +351,34 @@ describe('decorateWithLogsResult', () => {
|
||||
],
|
||||
series: [
|
||||
{
|
||||
label: 'unknown',
|
||||
color: '#8e8e8e',
|
||||
data: [[0, 3]],
|
||||
isVisible: true,
|
||||
yAxis: {
|
||||
index: 1,
|
||||
min: 0,
|
||||
tickDecimals: 0,
|
||||
},
|
||||
seriesIndex: 0,
|
||||
timeField: {
|
||||
name: 'Time',
|
||||
type: 'time',
|
||||
config: {},
|
||||
values: new ArrayVector([0]),
|
||||
index: 0,
|
||||
display: expect.anything(),
|
||||
},
|
||||
valueField: {
|
||||
name: 'unknown',
|
||||
type: 'number',
|
||||
config: { unit: undefined, color: '#8e8e8e' },
|
||||
values: new ArrayVector([3]),
|
||||
labels: undefined,
|
||||
index: 1,
|
||||
display: expect.anything(),
|
||||
state: expect.anything(),
|
||||
},
|
||||
timeStep: 0,
|
||||
name: 'unknown',
|
||||
length: 1,
|
||||
fields: [
|
||||
{ name: 'Time', type: 'time', values: new ArrayVector([0]), config: {} },
|
||||
{
|
||||
name: 'Value',
|
||||
type: 'number',
|
||||
labels: undefined,
|
||||
values: new ArrayVector([3]),
|
||||
config: {
|
||||
min: 0,
|
||||
decimals: 0,
|
||||
unit: undefined,
|
||||
custom: {
|
||||
drawStyle: DrawStyle.Bars,
|
||||
barAlignment: 0,
|
||||
barMaxWidth: 5,
|
||||
barWidthFactor: 0.9,
|
||||
lineColor: '#8e8e8e',
|
||||
fillColor: '#8e8e8e',
|
||||
pointColor: '#8e8e8e',
|
||||
lineWidth: 0,
|
||||
fillOpacity: 100,
|
||||
stacking: { mode: StackingMode.Normal, group: 'A' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
visibleRange: undefined,
|
||||
|
@ -137,15 +137,8 @@ export const decorateWithLogsResult = (
|
||||
return { ...data, logsResult: null };
|
||||
}
|
||||
|
||||
const timeZone = data.request?.timezone ?? 'browser';
|
||||
const intervalMs = data.request?.intervalMs;
|
||||
const newResults = dataFrameToLogsModel(
|
||||
data.logsFrames,
|
||||
intervalMs,
|
||||
timeZone,
|
||||
options.absoluteRange,
|
||||
options.queries
|
||||
);
|
||||
const newResults = dataFrameToLogsModel(data.logsFrames, intervalMs, options.absoluteRange, options.queries);
|
||||
const sortOrder = refreshIntervalToSortOrder(options.refreshInterval);
|
||||
const sortedNewResults = sortLogsResult(newResults, sortOrder);
|
||||
const rows = sortedNewResults.rows;
|
||||
|
@ -100,7 +100,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
|
||||
exportLogsAsTxt = () => {
|
||||
const { data, panel } = this.props;
|
||||
const logsModel = dataFrameToLogsModel(data || [], undefined, 'utc');
|
||||
const logsModel = dataFrameToLogsModel(data || [], undefined);
|
||||
let textToDownload = '';
|
||||
|
||||
logsModel.meta?.forEach((metaItem) => {
|
||||
|
@ -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 deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user