diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index aba0fa97624..1c0e0279120 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -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', () => { diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 8e15f689d68..932d934801a 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -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!, diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index cbc987b798f..2d64eed4767 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -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[]; } diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 19e70a33f9f..2b3dc0657ab 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -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, diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index aade4903d82..ed62e779a2a 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -108,6 +108,8 @@ export interface LineConfig { */ export interface BarConfig { barAlignment?: BarAlignment; + barWidthFactor?: number; + barMaxWidth?: number; } /** diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index d6369fc03fb..bc6b98b0665 100755 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -41,6 +41,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { lineWidth, lineStyle, barAlignment, + barWidthFactor, + barMaxWidth, showPoints, pointColor, pointSize, @@ -68,7 +70,13 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { 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; - } - if (barAlignment === BarAlignment.Before) { - return builders.barsBefore; + // 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, + }); } - return builders.bars; - } - if (style === DrawStyle.Line) { + + return builders[barsCfgKey]; + } else if (style === DrawStyle.Line) { if (lineInterpolation === LineInterpolation.StepBefore) { return builders.stepBefore; } diff --git a/public/app/core/logs_model.test.ts b/public/app/core/logs_model.test.ts index 04aa3e0939a..a2e0f9da9c5 100644 --- a/public/app/core/logs_model.test.ts +++ b/public/app/core/logs_model.test.ts @@ -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([ { diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index b079ea6943b..9b36631eb86 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -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 { 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 }); - - 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.min = 0; + data.fields[valueField.index].config.decimals = 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 */ diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 0ea2a18abc2..d4ffa064a5d 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { ); } - renderGraphPanel(width: number) { + renderGraphPanel() { const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading } = this.props; return ( - + + + ); } @@ -250,11 +259,10 @@ export class Explore extends React.PureComponent { ); } - renderLogsPanel(width: number) { + renderLogsPanel() { const { exploreId, syncedTimes } = this.props; return ( { {showPanels && ( <> {showMetrics && graphResult && ( - {this.renderGraphPanel(width)} + {this.renderGraphPanel()} )} {showTable && {this.renderTablePanel(width)}} - {showLogs && {this.renderLogsPanel(width)}} + {showLogs && {this.renderLogsPanel()}} {showNodeGraph && {this.renderNodeGraphPanel()}} {showTrace && {this.renderTraceViewPanel()}} diff --git a/public/app/features/explore/ExploreGraphNGPanel.tsx b/public/app/features/explore/ExploreGraphNGPanel.tsx index c9ce1538fee..4350f629fb6 100644 --- a/public/app/features/explore/ExploreGraphNGPanel.tsx +++ b/public/app/features/explore/ExploreGraphNGPanel.tsx @@ -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 ( - <> + {dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
@@ -136,43 +156,43 @@ export function ExploreGraphNGPanel({ >{`Show all ${dataWithConfig.length}`}
)} - - - - {(config, alignedDataFrame) => { - return ( - <> - - - - {annotations && ( - + {({ width }) => ( + + {(config, alignedDataFrame) => { + return ( + <> + + - )} - - ); - }} - - - + + {annotations && ( + + )} + + ); + }} + + )} + +
); } diff --git a/public/app/features/explore/ExploreGraphPanel.tsx b/public/app/features/explore/ExploreGraphPanel.tsx deleted file mode 100644 index e7bbb82107b..00000000000 --- a/public/app/features/explore/ExploreGraphPanel.tsx +++ /dev/null @@ -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 { - 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 ( - - {({ onSeriesToggle, toggledSeries }: GraphSeriesTogglerAPI) => { - return ( - {}} - 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 */} - - - ); - }} - - ); - }; - - 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 && ( -
- - {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `} - {`Show all ${series.length}`} -
- )} - - {showPanel && ( - - {this.renderGraph()} - - )} - - {!showPanel && this.renderGraph()} - - ); - } -} - -export const ExploreGraphPanel = withTheme(UnThemedExploreGraphPanel); -ExploreGraphPanel.displayName = 'ExploreGraphPanel'; diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index b68b183dd7b..ce3437fa1d4 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -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 { scanning, scanRange, showContextToggle, - width, absoluteRange, onChangeTime, getFieldLinks, @@ -276,19 +275,17 @@ export class UnthemedLogs extends PureComponent {
This datasource does not support full-range histograms. The graph is based on the logs seen in the response.
- + {logsSeries && logsSeries.length ? ( + + ) : undefined}
diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index efc86135228..fe2a2ad5f54 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -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 { visibleRange, scanning, range, - width, isLive, exploreId, addResultsToCache, @@ -134,7 +132,6 @@ export class LogsContainer extends PureComponent { scanning={scanning} scanRange={range.raw} showContextToggle={this.showContextToggle} - width={width} getRowContext={this.getLogRowContext} getFieldLinks={this.getFieldLinks} addResultsToCache={() => addResultsToCache(exploreId)} diff --git a/public/app/features/explore/utils/decorators.test.ts b/public/app/features/explore/utils/decorators.test.ts index 088faa979da..186563994bf 100644 --- a/public/app/features/explore/utils/decorators.test.ts +++ b/public/app/features/explore/utils/decorators.test.ts @@ -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, diff --git a/public/app/features/explore/utils/decorators.ts b/public/app/features/explore/utils/decorators.ts index 60cd97d5da3..61fc0498cfc 100644 --- a/public/app/features/explore/utils/decorators.ts +++ b/public/app/features/explore/utils/decorators.ts @@ -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; diff --git a/public/app/features/inspector/InspectDataTab.tsx b/public/app/features/inspector/InspectDataTab.tsx index e3bb8328fe8..32121249543 100644 --- a/public/app/features/inspector/InspectDataTab.tsx +++ b/public/app/features/inspector/InspectDataTab.tsx @@ -100,7 +100,7 @@ export class InspectDataTab extends PureComponent { exportLogsAsTxt = () => { const { data, panel } = this.props; - const logsModel = dataFrameToLogsModel(data || [], undefined, 'utc'); + const logsModel = dataFrameToLogsModel(data || [], undefined); let textToDownload = ''; logsModel.meta?.forEach((metaItem) => { diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 8956fbbd22f..e8f3dd04299 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -20,7 +20,7 @@ export const LogsPanel: React.FunctionComponent = ({ ); } - 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);