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:
co-authored by
Leon Sorokin
parent
c6a78a6bd7
commit
f4a40a4d85
@@ -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,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user