From 4b3440325e3c0b4ead50696913dd4a6517c27ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 13 Aug 2019 07:32:43 +0200 Subject: [PATCH] Explore: Replaces TimeSeries with GraphSeriesXY (#18475) * Wip: Compiles and runs * WIP: Logs Graph partially working * Refactor: Adds GraphSeriesToggler * Refactor: Adds tickDecimals to YAxis * Refactor: Adds TimeZone and PlotSelection to Graph * Refactor: Makes the graphResult work in Explore * Refactor: Adds ExploreGraphPanel that is used by Logs and Explore * Fix: Fixes strange behaviour with ExploreMode not beeing changed * Fix: Adds onSelectionChanged to GraphWithLegend * Refactor: Cleans up unused comments * ExploreGraph: Disable colorpicker --- packages/grafana-data/src/types/graph.ts | 8 +- packages/grafana-data/src/types/logs.ts | 5 +- .../src/utils/processDataFrame.ts | 22 +- .../grafana-ui/src/components/Graph/Graph.tsx | 52 ++- .../Graph/GraphWithLegend.story.tsx | 2 +- .../src/components/Graph/GraphWithLegend.tsx | 15 +- .../Graph/mockGraphWithLegendData.ts | 16 +- packages/grafana-ui/src/types/panel.ts | 3 +- packages/grafana-ui/src/utils/fieldDisplay.ts | 2 +- .../grafana-ui/src/utils/flotPairs.test.ts | 10 +- packages/grafana-ui/src/utils/flotPairs.ts | 8 +- public/app/core/logs_model.ts | 49 ++- .../dashboard/dashgrid/PanelChrome.tsx | 3 +- public/app/features/explore/Explore.tsx | 22 +- .../features/explore/ExploreGraphPanel.tsx | 173 ++++++++++ public/app/features/explore/Graph.test.tsx | 50 --- public/app/features/explore/Graph.tsx | 280 ---------------- .../app/features/explore/GraphContainer.tsx | 100 ------ public/app/features/explore/Logs.tsx | 40 +-- public/app/features/explore/QueryRow.tsx | 4 +- .../explore/__snapshots__/Graph.test.tsx.snap | 315 ------------------ .../app/features/explore/state/actionTypes.ts | 4 +- .../explore/utils/ResultProcessor.test.ts | 125 +++---- .../features/explore/utils/ResultProcessor.ts | 72 ++-- .../app/plugins/panel/graph2/GraphPanel.tsx | 2 + .../panel/graph2/GraphPanelController.tsx | 67 ++-- .../panel/graph2/GraphSeriesToggler.tsx | 85 +++++ .../panel/graph2/getGraphSeriesModel.ts | 6 +- public/app/plugins/panel/graph2/types.ts | 4 +- public/app/types/explore.ts | 3 +- 30 files changed, 550 insertions(+), 997 deletions(-) create mode 100644 public/app/features/explore/ExploreGraphPanel.tsx delete mode 100644 public/app/features/explore/Graph.test.tsx delete mode 100644 public/app/features/explore/Graph.tsx delete mode 100644 public/app/features/explore/GraphContainer.tsx delete mode 100644 public/app/features/explore/__snapshots__/Graph.test.tsx.snap create mode 100644 public/app/plugins/panel/graph2/GraphSeriesToggler.tsx diff --git a/packages/grafana-data/src/types/graph.ts b/packages/grafana-data/src/types/graph.ts index 9ea6650e349..f098a24d05f 100644 --- a/packages/grafana-data/src/types/graph.ts +++ b/packages/grafana-data/src/types/graph.ts @@ -1,5 +1,11 @@ import { DisplayValue } from './displayValue'; +export interface YAxis { + index: number; + min?: number; + tickDecimals?: number; +} + export type GraphSeriesValue = number | null; /** View model projection of a series */ @@ -9,7 +15,7 @@ export interface GraphSeriesXY { data: GraphSeriesValue[][]; // [x,y][] info?: DisplayValue[]; // Legend info isVisible: boolean; - yAxis: number; + yAxis: YAxis; } export interface CreatePlotOverlay { diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index c0b295c55aa..641af88c502 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -1,4 +1,5 @@ -import { Labels, TimeSeries } from './data'; +import { Labels } from './data'; +import { GraphSeriesXY } from './graph'; /** * Mapping of log level abbreviation to canonical log level. @@ -54,7 +55,7 @@ export interface LogsModel { hasUniqueLabels: boolean; meta?: LogsMetaItem[]; rows: LogRowModel[]; - series?: TimeSeries[]; + series?: GraphSeriesXY[]; } export interface LogSearchMatch { diff --git a/packages/grafana-data/src/utils/processDataFrame.ts b/packages/grafana-data/src/utils/processDataFrame.ts index f4847f3e977..5c9a68cee9f 100644 --- a/packages/grafana-data/src/utils/processDataFrame.ts +++ b/packages/grafana-data/src/utils/processDataFrame.ts @@ -4,7 +4,7 @@ import isString from 'lodash/isString'; import isBoolean from 'lodash/isBoolean'; // Types -import { DataFrame, Field, TimeSeries, FieldType, TableData, Column } from '../types/index'; +import { DataFrame, Field, TimeSeries, FieldType, TableData, Column, GraphSeriesXY } from '../types/index'; import { isDateTime } from './moment_wrapper'; function convertTableToDataFrame(table: TableData): DataFrame { @@ -44,6 +44,23 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): DataFrame { }; } +function convertGraphSeriesToDataFrame(graphSeries: GraphSeriesXY): DataFrame { + return { + name: graphSeries.label, + fields: [ + { + name: graphSeries.label || 'Value', + }, + { + name: 'Time', + type: FieldType.time, + unit: 'dateTimeAsIso', + }, + ], + rows: graphSeries.data, + }; +} + // PapaParse Dynamic Typing regex: // https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998 const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i; @@ -145,6 +162,9 @@ export const toDataFrame = (data: any): DataFrame => { if (data.hasOwnProperty('datapoints')) { return convertTimeSeriesToDataFrame(data); } + if (data.hasOwnProperty('data')) { + return convertGraphSeriesToDataFrame(data); + } if (data.hasOwnProperty('columns')) { return convertTableToDataFrame(data); } diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index 6fd42a67fad..7db0aa699bc 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -4,16 +4,20 @@ import React, { PureComponent } from 'react'; import uniqBy from 'lodash/uniqBy'; // Types -import { TimeRange, GraphSeriesXY } from '@grafana/data'; +import { TimeRange, GraphSeriesXY, AbsoluteTimeRange, TimeZone, DefaultTimeZone } from '@grafana/data'; export interface GraphProps { series: GraphSeriesXY[]; timeRange: TimeRange; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs + timeZone: TimeZone; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs showLines?: boolean; showPoints?: boolean; showBars?: boolean; width: number; height: number; + isStacked?: boolean; + lineWidth?: number; + onSelectionChanged?: (range: AbsoluteTimeRange) => void; } export class Graph extends PureComponent { @@ -21,9 +25,12 @@ export class Graph extends PureComponent { showLines: true, showPoints: false, showBars: false, + isStacked: false, + lineWidth: 1, }; element: HTMLElement | null = null; + $element: any; componentDidUpdate() { this.draw(); @@ -31,14 +38,32 @@ export class Graph extends PureComponent { componentDidMount() { this.draw(); + if (this.element) { + this.$element = $(this.element); + this.$element.bind('plotselected', this.onPlotSelected); + } } + componentWillUnmount() { + this.$element.unbind('plotselected', this.onPlotSelected); + } + + onPlotSelected = (event: JQueryEventObject, ranges: { xaxis: { from: number; to: number } }) => { + const { onSelectionChanged } = this.props; + if (onSelectionChanged) { + onSelectionChanged({ + from: ranges.xaxis.from, + to: ranges.xaxis.to, + }); + } + }; + draw() { if (this.element === null) { return; } - const { width, series, timeRange, showLines, showBars, showPoints } = this.props; + const { width, series, timeRange, showLines, showBars, showPoints, isStacked, lineWidth, timeZone } = this.props; if (!width) { return; @@ -49,10 +74,16 @@ export class Graph extends PureComponent { const max = timeRange.to.valueOf(); const yaxes = uniqBy( series.map(s => { + const index = s.yAxis ? s.yAxis.index : 1; + const min = s.yAxis && !isNaN(s.yAxis.min as number) ? s.yAxis.min : null; + const tickDecimals = s.yAxis && !isNaN(s.yAxis.tickDecimals as number) ? s.yAxis.tickDecimals : null; + return { show: true, - index: s.yAxis, - position: s.yAxis === 1 ? 'left' : 'right', + index, + position: index === 1 ? 'left' : 'right', + min, + tickDecimals, }; }), yAxisConfig => yAxisConfig.index @@ -62,9 +93,10 @@ export class Graph extends PureComponent { show: false, }, series: { + stack: isStacked, lines: { show: showLines, - linewidth: 1, + linewidth: lineWidth, zero: false, }, points: { @@ -78,7 +110,7 @@ export class Graph extends PureComponent { fill: 1, barWidth: 1, zero: false, - lineWidth: 0, + lineWidth: lineWidth, }, shadowSize: 0, }, @@ -89,6 +121,7 @@ export class Graph extends PureComponent { label: 'Datetime', ticks: ticks, timeformat: timeFormat(ticks, min, max), + timezone: timeZone ? timeZone : DefaultTimeZone, }, yaxes, grid: { @@ -102,6 +135,10 @@ export class Graph extends PureComponent { margin: { left: 0, right: 0 }, labelMarginX: 0, }, + selection: { + mode: 'x', + color: '#666', + }, }; try { @@ -113,9 +150,10 @@ export class Graph extends PureComponent { } render() { + const { height } = this.props; return (
-
(this.element = e)} /> +
(this.element = e)} style={{ height }} />
); } diff --git a/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx b/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx index c0fbf52d38f..25d196d27be 100644 --- a/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphWithLegend.story.tsx @@ -74,7 +74,7 @@ GraphWithLegendStories.add('default', () => { .map(s => s.trim()) .indexOf(s.label.split('-')[0]) > -1 ) { - s.yAxis = 2; + s.yAxis = { index: 2 }; } return s; diff --git a/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx b/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx index a0567c90c29..d81b61b5b48 100644 --- a/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx @@ -2,7 +2,7 @@ import _ from 'lodash'; import React from 'react'; import { css } from 'emotion'; -import { GraphSeriesValue } from '@grafana/data'; +import { GraphSeriesValue, AbsoluteTimeRange } from '@grafana/data'; import { Graph, GraphProps } from './Graph'; import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend'; @@ -18,10 +18,11 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions { displayMode: LegendDisplayMode; sortLegendBy?: string; sortLegendDesc?: boolean; - onSeriesColorChange: SeriesColorChangeHandler; + onSeriesColorChange?: SeriesColorChangeHandler; onSeriesAxisToggle?: SeriesAxisToggleHandler; onSeriesToggle?: (label: string, event: React.MouseEvent) => void; onToggleSort: (sortBy: string) => void; + onSelectionChanged?: (range: AbsoluteTimeRange) => void; } const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({ @@ -67,6 +68,10 @@ export const GraphWithLegend: React.FunctionComponent = (p onToggleSort, hideEmpty, hideZero, + isStacked, + lineWidth, + onSelectionChanged, + timeZone, } = props; const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props); @@ -78,7 +83,7 @@ export const GraphWithLegend: React.FunctionComponent = (p label: s.label, color: s.color, isVisible: s.isVisible, - yAxis: s.yAxis, + yAxis: s.yAxis.index, displayValues: s.info || [], }, ]); @@ -90,12 +95,16 @@ export const GraphWithLegend: React.FunctionComponent = (p !!s.isVisible)} timeRange={timeRange} + timeZone={timeZone} showLines={showLines} showPoints={showPoints} showBars={showBars} width={width} height={height} key={isLegendVisible ? 'legend-visible' : 'legend-invisible'} + isStacked={isStacked} + lineWidth={lineWidth} + onSelectionChanged={onSelectionChanged} />
diff --git a/packages/grafana-ui/src/components/Graph/mockGraphWithLegendData.ts b/packages/grafana-ui/src/components/Graph/mockGraphWithLegendData.ts index 28899d90510..3e89a038005 100644 --- a/packages/grafana-ui/src/components/Graph/mockGraphWithLegendData.ts +++ b/packages/grafana-ui/src/components/Graph/mockGraphWithLegendData.ts @@ -1,7 +1,6 @@ import { GraphWithLegendProps } from './GraphWithLegend'; import { LegendDisplayMode } from '../Legend/Legend'; -import { dateTime } from '@grafana/data'; -// import { LegendList } from '../Legend/LegendList'; +import { dateTime, DefaultTimeZone } from '@grafana/data'; export const mockGraphWithLegendData = ({ displayMode, @@ -1099,7 +1098,9 @@ export const mockGraphWithLegendData = ({ { title: 'max', text: '18.42', numeric: 18.427101844163694 }, ], isVisible: true, - yAxis: 1, + yAxis: { + index: 1, + }, }, { label: 'B-series', @@ -2191,7 +2192,9 @@ export const mockGraphWithLegendData = ({ { title: 'max', text: '18.42', numeric: 18.427101844163694 }, ], isVisible: true, - yAxis: 1, + yAxis: { + index: 1, + }, }, { label: 'C-series', @@ -3283,7 +3286,9 @@ export const mockGraphWithLegendData = ({ { title: 'max', text: '18.42', numeric: 18.427101844163694 }, ], isVisible: true, - yAxis: 1, + yAxis: { + index: 1, + }, }, ], timeRange: { @@ -3313,4 +3318,5 @@ export const mockGraphWithLegendData = ({ }, onToggleSort: () => {}, displayMode: displayMode || LegendDisplayMode.List, + timeZone: DefaultTimeZone, }); diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index e50bfd77e76..c91df07019d 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,5 +1,5 @@ import { ComponentClass, ComponentType } from 'react'; -import { LoadingState, DataFrame, TimeRange } from '@grafana/data'; +import { LoadingState, DataFrame, TimeRange, TimeZone } from '@grafana/data'; import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource'; import { PluginMeta, GrafanaPlugin } from './plugin'; @@ -27,6 +27,7 @@ export interface PanelProps { // TODO: annotation?: PanelData; timeRange: TimeRange; + timeZone: TimeZone; options: T; onOptionsChange: (options: T) => void; renderCounter: number; diff --git a/packages/grafana-ui/src/utils/fieldDisplay.ts b/packages/grafana-ui/src/utils/fieldDisplay.ts index e77a04f816f..5813177c12c 100644 --- a/packages/grafana-ui/src/utils/fieldDisplay.ts +++ b/packages/grafana-ui/src/utils/fieldDisplay.ts @@ -169,7 +169,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi timeColumn < 0 ? undefined : getFlotPairs({ - series, + rows: series.rows, xIndex: timeColumn, yIndex: i, nullValueMode: NullValueMode.Null, diff --git a/packages/grafana-ui/src/utils/flotPairs.test.ts b/packages/grafana-ui/src/utils/flotPairs.test.ts index 525deda3ed7..a94f4c59853 100644 --- a/packages/grafana-ui/src/utils/flotPairs.test.ts +++ b/packages/grafana-ui/src/utils/flotPairs.test.ts @@ -1,12 +1,10 @@ import { getFlotPairs } from './flotPairs'; describe('getFlotPairs', () => { - const series = { - fields: [], - rows: [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']], - }; + const rows = [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']]; + it('should get X and y', () => { - const pairs = getFlotPairs({ series, xIndex: 0, yIndex: 1 }); + const pairs = getFlotPairs({ rows, xIndex: 0, yIndex: 1 }); expect(pairs.length).toEqual(3); expect(pairs[0].length).toEqual(2); @@ -15,7 +13,7 @@ describe('getFlotPairs', () => { }); it('should work with strings', () => { - const pairs = getFlotPairs({ series, xIndex: 0, yIndex: 2 }); + const pairs = getFlotPairs({ rows, xIndex: 0, yIndex: 2 }); expect(pairs.length).toEqual(3); expect(pairs[0].length).toEqual(2); diff --git a/packages/grafana-ui/src/utils/flotPairs.ts b/packages/grafana-ui/src/utils/flotPairs.ts index 71fb2d2b487..647b53a0a98 100644 --- a/packages/grafana-ui/src/utils/flotPairs.ts +++ b/packages/grafana-ui/src/utils/flotPairs.ts @@ -1,16 +1,14 @@ // Types -import { NullValueMode, DataFrame, GraphSeriesValue } from '@grafana/data'; +import { NullValueMode, GraphSeriesValue } from '@grafana/data'; export interface FlotPairsOptions { - series: DataFrame; + rows: any[][]; xIndex: number; yIndex: number; nullValueMode?: NullValueMode; } -export function getFlotPairs({ series, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] { - const rows = series.rows; - +export function getFlotPairs({ rows, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] { const ignoreNulls = nullValueMode === NullValueMode.Ignore; const nullAsZero = nullValueMode === NullValueMode.AsZero; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 3194015f5a8..f59b77e38f5 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,17 +1,15 @@ import _ from 'lodash'; import ansicolor from 'vendor/ansicolor/ansicolor'; -import { colors } from '@grafana/ui'; +import { colors, getFlotPairs } from '@grafana/ui'; import { - TimeSeries, Labels, LogLevel, DataFrame, findCommonLabels, findUniqueLabels, getLogLevel, - toLegacyResponseData, FieldCache, FieldType, getLogLevelFromKey, @@ -22,10 +20,15 @@ import { LogsParser, LogLabelStatsModel, LogsDedupStrategy, + GraphSeriesXY, + LoadingState, + dateTime, + toUtc, + NullValueMode, } from '@grafana/data'; import { getThemeColor } from 'app/core/utils/colors'; import { hasAnsiCodes } from 'app/core/utils/text'; -import { dateTime, toUtc } from '@grafana/data'; +import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel'; export const LogLevelColor = { [LogLevel.critical]: colors[7], @@ -192,7 +195,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set) }; } -export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] { +export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): GraphSeriesXY[] { // currently interval is rangeMs / resolution, which is too low for showing series as bars. // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain // when executing queries & interval calculated and not here but this is a temporary fix. @@ -242,12 +245,26 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time return a[1] - b[1]; }); - return { - datapoints: series.datapoints, - target: series.alias, - alias: series.alias, + const points = getFlotPairs({ + rows: series.datapoints, + xIndex: 1, + yIndex: 0, + nullValueMode: NullValueMode.Null, + }); + + const graphSeries: GraphSeriesXY = { color: series.color, + label: series.alias, + data: points, + isVisible: true, + yAxis: { + index: 1, + min: 0, + tickDecimals: 0, + }, }; + + return graphSeries; }); } @@ -273,10 +290,16 @@ export function dataFrameToLogsModel(dataFrame: DataFrame[], intervalMs: number) if (metricSeries.length === 0) { logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs); } else { - logsModel.series = []; - for (const series of metricSeries) { - logsModel.series.push(toLegacyResponseData(series) as TimeSeries); - } + logsModel.series = getGraphSeriesModel( + { series: metricSeries, state: LoadingState.Done }, + {}, + { showBars: true, showLines: false, showPoints: false }, + { + asTable: false, + isVisible: true, + placement: 'under', + } + ); } return logsModel; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index e4a13b0e1ce..dd4d4a1824e 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -178,7 +178,7 @@ export class PanelChrome extends PureComponent { queries: panel.targets, panelId: panel.id, dashboardId: this.props.dashboard.id, - timezone: this.props.dashboard.timezone, + timezone: this.props.dashboard.getTimezone(), timeRange: timeData.timeRange, timeInfo: timeData.timeInfo, widthPixels: width, @@ -251,6 +251,7 @@ export class PanelChrome extends PureComponent { id={panel.id} data={data} timeRange={data.request ? data.request.range : this.timeSrv.timeRange()} + timeZone={this.props.dashboard.getTimezone()} options={panel.getOptions()} transparent={panel.transparent} width={width - theme.panelPadding * 2} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 9ffb5b5ebbd..08d10efa919 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -12,7 +12,6 @@ import store from 'app/core/store'; // Components import { Alert } from './Error'; import ErrorBoundary from './ErrorBoundary'; -import GraphContainer from './GraphContainer'; import LogsContainer from './LogsContainer'; import QueryRows from './QueryRows'; import TableContainer from './TableContainer'; @@ -30,7 +29,7 @@ import { } from './state/actions'; // Types -import { RawTimeRange } from '@grafana/data'; +import { RawTimeRange, GraphSeriesXY } from '@grafana/data'; import { DataQuery, ExploreStartPageProps, DataSourceApi, DataQueryError } from '@grafana/ui'; import { @@ -56,6 +55,7 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn'; import { getTimeZone } from '../profile/state/selectors'; import { ErrorContainer } from './ErrorContainer'; import { scanStopAction } from './state/actionTypes'; +import ExploreGraphPanel from './ExploreGraphPanel'; interface ExploreProps { StartPage?: ComponentClass; @@ -87,6 +87,7 @@ interface ExploreProps { queryErrors: DataQueryError[]; isLive: boolean; updateTimeRange: typeof updateTimeRange; + graphResult?: GraphSeriesXY[]; } /** @@ -192,7 +193,7 @@ export class Explore extends React.PureComponent { refreshExplore = () => { const { exploreId, update } = this.props; - if (update.queries || update.ui || update.range || update.datasource) { + if (update.queries || update.ui || update.range || update.datasource || update.mode) { this.props.refreshExplore(exploreId); } }; @@ -225,6 +226,7 @@ export class Explore extends React.PureComponent { queryKeys, queryErrors, mode, + graphResult, } = this.props; const exploreClass = split ? 'explore explore-split' : 'explore'; @@ -259,7 +261,9 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {mode === ExploreMode.Metrics && } + {mode === ExploreMode.Metrics && ( + + )} {mode === ExploreMode.Metrics && ( )} @@ -306,6 +310,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { isLive, supportedModes, mode, + graphResult, } = item; const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState; @@ -318,15 +323,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { const urlModeIsValid = supportedModes.includes(urlMode); const modeStateIsValid = supportedModes.includes(mode); - if (urlModeIsValid) { - newMode = urlMode; - } else if (modeStateIsValid) { + if (modeStateIsValid) { newMode = mode; + } else if (urlModeIsValid) { + newMode = urlMode; } else { newMode = supportedModes[0]; } } else { - newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(urlMode) ? urlMode : ExploreMode.Metrics; + newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(mode) ? mode : ExploreMode.Metrics; } const initialUI = ui || DEFAULT_UI_STATE; @@ -349,6 +354,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { initialUI, queryErrors, isLive, + graphResult, }; } diff --git a/public/app/features/explore/ExploreGraphPanel.tsx b/public/app/features/explore/ExploreGraphPanel.tsx new file mode 100644 index 00000000000..affd7562fb8 --- /dev/null +++ b/public/app/features/explore/ExploreGraphPanel.tsx @@ -0,0 +1,173 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { LegendDisplayMode, GraphWithLegend } from '@grafana/ui'; +import { TimeZone, AbsoluteTimeRange, GraphSeriesXY, dateTimeForTimeZone, LoadingState } from '@grafana/data'; + +import { GraphSeriesToggler } from 'app/plugins/panel/graph2/GraphSeriesToggler'; +import Panel from './Panel'; +import { StoreState, ExploreId, ExploreMode } from 'app/types'; +import { getTimeZone } from '../profile/state/selectors'; +import { toggleGraph, updateTimeRange } from './state/actions'; + +const MAX_NUMBER_OF_TIME_SERIES = 20; + +interface Props { + exploreId: ExploreId; + series: GraphSeriesXY[]; + width: number; + absoluteRange?: AbsoluteTimeRange; + loading?: boolean; + mode?: ExploreMode; + showingGraph?: boolean; + showingTable?: boolean; + timeZone?: TimeZone; + onHiddenSeriesChanged?: (hiddenSeries: string[]) => void; + toggleGraph: typeof toggleGraph; + updateTimeRange: typeof updateTimeRange; +} + +interface State { + hiddenSeries: string[]; + showAllTimeSeries: boolean; +} + +export class ExploreGraphPanel extends PureComponent { + state: State = { + hiddenSeries: [], + showAllTimeSeries: false, + }; + + onShowAllTimeSeries = () => { + this.setState({ + showAllTimeSeries: true, + }); + }; + + onClickGraphButton = () => { + const { toggleGraph, exploreId, showingGraph } = this.props; + toggleGraph(exploreId, showingGraph); + }; + + onChangeTime = (absoluteRange: AbsoluteTimeRange) => { + const { exploreId, updateTimeRange } = this.props; + + updateTimeRange({ exploreId, absoluteRange }); + }; + + renderGraph = () => { + const { + width, + series, + onHiddenSeriesChanged, + timeZone, + absoluteRange, + mode, + showingGraph, + showingTable, + } = this.props; + const { showAllTimeSeries } = this.state; + + if (!series) { + return null; + } + + const timeRange = { + from: dateTimeForTimeZone(timeZone, absoluteRange.from), + to: dateTimeForTimeZone(timeZone, absoluteRange.to), + raw: { + from: dateTimeForTimeZone(timeZone, absoluteRange.from), + to: dateTimeForTimeZone(timeZone, absoluteRange.to), + }, + }; + const height = mode === ExploreMode.Logs ? 100 : showingGraph && showingTable ? 200 : 400; + const showBars = mode === ExploreMode.Logs ? true : false; + const showLines = mode === ExploreMode.Metrics ? true : false; + const isStacked = mode === ExploreMode.Logs ? true : false; + const lineWidth = mode === ExploreMode.Metrics ? 1 : 5; + const seriesToShow = showAllTimeSeries ? series : series.slice(0, MAX_NUMBER_OF_TIME_SERIES); + + return ( + + {({ onSeriesToggle, toggledSeries }) => { + return ( + {}} + series={toggledSeries} + isStacked={isStacked} + lineWidth={lineWidth} + onSeriesToggle={onSeriesToggle} + onSelectionChanged={this.onChangeTime} + /> + ); + }} + + ); + }; + + render() { + const { series, mode, showingGraph, loading } = this.props; + const { showAllTimeSeries } = this.state; + + return ( + <> + {series && series.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && ( +
+ + {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `} + {`Show all ${ + series.length + }`} +
+ )} + + {mode === ExploreMode.Metrics && ( + + {this.renderGraph()} + + )} + + {mode === ExploreMode.Logs && this.renderGraph()} + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) { + const explore = state.explore; + // @ts-ignore + const item: ExploreItemState = explore[exploreId]; + const { loadingState, showingGraph, showingTable, absoluteRange, mode } = item; + const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming; + + return { + loading, + showingGraph, + showingTable, + timeZone: getTimeZone(state.user), + absoluteRange, + mode, + }; +} + +const mapDispatchToProps = { + toggleGraph, + updateTimeRange, +}; + +export default hot(module)( + connect( + mapStateToProps, + mapDispatchToProps + )(ExploreGraphPanel) +); diff --git a/public/app/features/explore/Graph.test.tsx b/public/app/features/explore/Graph.test.tsx deleted file mode 100644 index 301b819717d..00000000000 --- a/public/app/features/explore/Graph.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { Graph } from './Graph'; -import { mockData } from './__mocks__/mockData'; -import { DefaultTimeZone } from '@grafana/data'; - -const setup = (propOverrides?: object) => { - const props = { - size: { width: 10, height: 20 }, - data: mockData().slice(0, 19), - range: { from: 0, to: 1 }, - timeZone: DefaultTimeZone, - ...propOverrides, - }; - - // Enzyme.shallow did not work well with jquery.flop. Mocking the draw function. - Graph.prototype.draw = jest.fn(); - - const wrapper = shallow(); - const instance = wrapper.instance() as Graph; - - return { - wrapper, - instance, - }; -}; - -describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render component with disclaimer', () => { - const { wrapper } = setup({ - data: mockData(), - }); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should show query return no time series', () => { - const { wrapper } = setup({ - data: [], - }); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx deleted file mode 100644 index 2d3695fc0ec..00000000000 --- a/public/app/features/explore/Graph.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import $ from 'jquery'; -import React, { PureComponent } from 'react'; -import difference from 'lodash/difference'; - -import 'vendor/flot/jquery.flot'; -import 'vendor/flot/jquery.flot.time'; -import 'vendor/flot/jquery.flot.selection'; -import 'vendor/flot/jquery.flot.stack'; - -import { GraphLegend, LegendItem, LegendDisplayMode } from '@grafana/ui'; -import { TimeZone, AbsoluteTimeRange } from '@grafana/data'; -import TimeSeries from 'app/core/time_series2'; - -const MAX_NUMBER_OF_TIME_SERIES = 20; - -// Copied from graph.ts -function time_format(ticks: number, min: number, max: number) { - if (min && max && ticks) { - const range = max - min; - const secPerTick = range / ticks / 1000; - const oneDay = 86400000; - const oneYear = 31536000000; - - if (secPerTick <= 45) { - return '%H:%M:%S'; - } - if (secPerTick <= 7200 || range <= oneDay) { - return '%H:%M'; - } - if (secPerTick <= 80000) { - return '%m/%d %H:%M'; - } - if (secPerTick <= 2419200 || range <= oneYear) { - return '%m/%d'; - } - return '%Y-%m'; - } - - return '%H:%M'; -} - -const FLOT_OPTIONS: any = { - legend: { - show: false, - }, - series: { - lines: { - linewidth: 1, - zero: false, - }, - shadowSize: 0, - }, - grid: { - minBorderMargin: 0, - markings: [], - backgroundColor: null, - borderWidth: 0, - // hoverable: true, - clickable: true, - color: '#a1a1a1', - margin: { left: 0, right: 0 }, - labelMarginX: 0, - }, - selection: { - mode: 'x', - color: '#666', - }, - // crosshair: { - // mode: 'x', - // }, -}; - -interface GraphProps { - data: any[]; - height?: number; - width?: number; - id?: string; - range: AbsoluteTimeRange; - timeZone: TimeZone; - split?: boolean; - userOptions?: any; - onChangeTime?: (range: AbsoluteTimeRange) => void; - onToggleSeries?: (alias: string, hiddenSeries: string[]) => void; -} - -interface GraphState { - /** - * Type parameter refers to the `alias` property of a `TimeSeries`. - * Consequently, all series sharing the same alias will share visibility state. - */ - hiddenSeries: string[]; - showAllTimeSeries: boolean; -} - -export class Graph extends PureComponent { - $el: any; - dynamicOptions: any = null; - - state: GraphState = { - hiddenSeries: [], - showAllTimeSeries: false, - }; - - getGraphData(): TimeSeries[] { - const { data } = this.props; - - return this.state.showAllTimeSeries ? data : data.slice(0, MAX_NUMBER_OF_TIME_SERIES); - } - - componentDidMount() { - this.draw(); - this.$el = $(`#${this.props.id}`); - this.$el.bind('plotselected', this.onPlotSelected); - } - - componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { - if ( - prevProps.data !== this.props.data || - prevProps.range !== this.props.range || - prevProps.split !== this.props.split || - prevProps.height !== this.props.height || - prevProps.width !== this.props.width || - prevState.hiddenSeries !== this.state.hiddenSeries - ) { - this.draw(); - } - } - - componentWillUnmount() { - this.$el.unbind('plotselected', this.onPlotSelected); - } - - onPlotSelected = (event: JQueryEventObject, ranges: { xaxis: { from: number; to: number } }) => { - const { onChangeTime } = this.props; - if (onChangeTime) { - this.props.onChangeTime({ - from: ranges.xaxis.from, - to: ranges.xaxis.to, - }); - } - }; - - getDynamicOptions() { - const { range, width, timeZone } = this.props; - const ticks = (width || 0) / 100; - const min = range.from; - const max = range.to; - return { - xaxis: { - mode: 'time', - min: min, - max: max, - label: 'Datetime', - ticks: ticks, - timezone: timeZone, - timeformat: time_format(ticks, min, max), - }, - }; - } - - onShowAllTimeSeries = () => { - this.setState( - { - showAllTimeSeries: true, - }, - this.draw - ); - }; - - draw() { - const { userOptions = {} } = this.props; - const { hiddenSeries } = this.state; - const data = this.getGraphData(); - - const $el = $(`#${this.props.id}`); - let series = [{ data: [[0, 0]] }]; - - if (data && data.length > 0) { - series = data - .filter((ts: TimeSeries) => hiddenSeries.indexOf(ts.alias) === -1) - .map((ts: TimeSeries) => ({ - color: ts.color, - label: ts.label, - data: ts.getFlotPairs('null'), - })); - } - - this.dynamicOptions = this.getDynamicOptions(); - - const options = { - ...FLOT_OPTIONS, - ...this.dynamicOptions, - ...userOptions, - }; - - $.plot($el, series, options); - } - - getLegendItems = (): LegendItem[] => { - const { hiddenSeries } = this.state; - const data = this.getGraphData(); - - return data.map(series => { - return { - label: series.alias, - color: series.color, - isVisible: hiddenSeries.indexOf(series.alias) === -1, - yAxis: 1, - }; - }); - }; - - onSeriesToggle(label: string, event: React.MouseEvent) { - // This implementation is more or less a copy of GraphPanel's logic. - // TODO: we need to use Graph's panel controller or split it into smaller - // controllers to remove code duplication. Right now we cant easily use that, since Explore - // is not using DataFrame for graph yet - - const exclusive = event.ctrlKey || event.metaKey || event.shiftKey; - - this.setState((state, props) => { - const { data, onToggleSeries } = props; - let nextHiddenSeries: string[] = []; - if (exclusive) { - // Toggling series with key makes the series itself to toggle - if (state.hiddenSeries.indexOf(label) > -1) { - nextHiddenSeries = state.hiddenSeries.filter(series => series !== label); - } else { - nextHiddenSeries = state.hiddenSeries.concat([label]); - } - } else { - // Toggling series with out key toggles all the series but the clicked one - const allSeriesLabels = data.map(series => series.label); - - if (state.hiddenSeries.length + 1 === allSeriesLabels.length) { - nextHiddenSeries = []; - } else { - nextHiddenSeries = difference(allSeriesLabels, [label]); - } - } - - if (onToggleSeries) { - onToggleSeries(label, nextHiddenSeries); - } - - return { - hiddenSeries: nextHiddenSeries, - }; - }); - } - - render() { - const { height = 100, id = 'graph' } = this.props; - return ( - <> - {this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && ( -
- - {`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `} - {`Show all ${ - this.props.data.length - }`} -
- )} -
- - { - this.onSeriesToggle(item.label, event); - }} - /> - - ); - } -} - -export default Graph; diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx deleted file mode 100644 index 1a08f08734d..00000000000 --- a/public/app/features/explore/GraphContainer.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { PureComponent } from 'react'; -import { hot } from 'react-hot-loader'; -import { connect } from 'react-redux'; -import { TimeZone, AbsoluteTimeRange, LoadingState } from '@grafana/data'; - -import { ExploreId, ExploreItemState } from 'app/types/explore'; -import { StoreState } from 'app/types'; - -import { toggleGraph, updateTimeRange } from './state/actions'; -import Graph from './Graph'; -import Panel from './Panel'; -import { getTimeZone } from '../profile/state/selectors'; - -interface GraphContainerProps { - exploreId: ExploreId; - graphResult?: any[]; - loading: boolean; - absoluteRange: AbsoluteTimeRange; - timeZone: TimeZone; - showingGraph: boolean; - showingTable: boolean; - split: boolean; - toggleGraph: typeof toggleGraph; - updateTimeRange: typeof updateTimeRange; - width: number; -} - -export class GraphContainer extends PureComponent { - onClickGraphButton = () => { - this.props.toggleGraph(this.props.exploreId, this.props.showingGraph); - }; - - onChangeTime = (absoluteRange: AbsoluteTimeRange) => { - const { exploreId, updateTimeRange } = this.props; - - updateTimeRange({ exploreId, absoluteRange }); - }; - - render() { - const { - exploreId, - graphResult, - loading, - showingGraph, - showingTable, - absoluteRange, - split, - width, - timeZone, - } = this.props; - const graphHeight = showingGraph && showingTable ? 200 : 400; - - return ( - - {graphResult && ( - - )} - - ); - } -} - -function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }) { - const explore = state.explore; - const { split } = explore; - // @ts-ignore - const item: ExploreItemState = explore[exploreId]; - const { graphResult, loadingState, showingGraph, showingTable, absoluteRange } = item; - const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming; - return { - graphResult, - loading, - showingGraph, - showingTable, - split, - timeZone: getTimeZone(state.user), - absoluteRange, - }; -} - -const mapDispatchToProps = { - toggleGraph, - updateTimeRange, -}; - -export default hot(module)( - connect( - mapStateToProps, - mapDispatchToProps - )(GraphContainer) -); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index f3637b8b039..b491be83882 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -13,32 +13,17 @@ import { LogsDedupStrategy, LogRowModel, } from '@grafana/data'; -import TimeSeries from 'app/core/time_series2'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; -import Graph from './Graph'; import { LogLabels } from './LogLabels'; import { LogRow } from './LogRow'; import { LogsDedupDescription } from 'app/core/logs_model'; +import ExploreGraphPanel from './ExploreGraphPanel'; +import { ExploreId } from 'app/types'; const PREVIEW_LIMIT = 100; -const graphOptions = { - series: { - stack: true, - bars: { - show: true, - lineWidth: 5, - // barWidth: 10, - }, - // stack: true, - }, - yaxis: { - tickDecimals: 0, - }, -}; - function renderMetaItem(value: any, kind: LogsMetaKind) { if (kind === LogsMetaKind.LabelsMap) { return ( @@ -54,7 +39,7 @@ interface Props { data?: LogsModel; dedupedData?: LogsModel; width: number; - exploreId: string; + exploreId: ExploreId; highlighterExpressions: string[]; loading: boolean; absoluteRange: AbsoluteTimeRange; @@ -135,7 +120,7 @@ export default class Logs extends PureComponent { }); }; - onToggleLogLevel = (rawLevel: string, hiddenRawLevels: string[]) => { + onToggleLogLevel = (hiddenRawLevels: string[]) => { const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map((level: LogLevel) => LogLevel[level]); this.props.onToggleLogLevel(hiddenLogLevels); }; @@ -157,7 +142,6 @@ export default class Logs extends PureComponent { highlighterExpressions, loading = false, onClickLabel, - absoluteRange, timeZone, scanning, scanRange, @@ -193,23 +177,15 @@ export default class Logs extends PureComponent { // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead const getRows = () => processedRows; - const timeSeries = data.series - ? data.series.map(series => new TimeSeries(series)) - : [new TimeSeries({ datapoints: [] })]; return (
-
diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index a7cd73c6fa9..34839620da2 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act // Types import { StoreState } from 'app/types'; -import { TimeRange, AbsoluteTimeRange } from '@grafana/data'; +import { TimeRange, AbsoluteTimeRange, toDataFrame, guessFieldTypes } from '@grafana/data'; import { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui'; import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; @@ -217,7 +217,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) const query = queries[index]; const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected; const error = queryErrors.filter(queryError => queryError.refId === query.refId)[0]; - const series = graphResult ? graphResult : []; // TODO: use DataFrame + const series = graphResult ? graphResult.map(serie => guessFieldTypes(toDataFrame(serie))) : []; // TODO: use DataFrame const queryResponse: PanelData = { series, state: loadingState, diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap deleted file mode 100644 index d43f9485605..00000000000 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ /dev/null @@ -1,315 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` - -
- - -`; - -exports[`Render should render component with disclaimer 1`] = ` - -
- - Showing only 20 time series. - - Show all 27 - -
-
- - -`; - -exports[`Render should show query return no time series 1`] = ` - -
- - -`; diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index ab8aaebd935..44c441f382f 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -15,9 +15,9 @@ import { TimeRange, DataFrame, LogsModel, - TimeSeries, LoadingState, AbsoluteTimeRange, + GraphSeriesXY, } from '@grafana/data'; import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; @@ -149,7 +149,7 @@ export interface QuerySuccessPayload { exploreId: ExploreId; latency: number; loadingState: LoadingState; - graphResult: TimeSeries[]; + graphResult: GraphSeriesXY[]; tableResult: TableModel; logsResult: LogsModel; } diff --git a/public/app/features/explore/utils/ResultProcessor.test.ts b/public/app/features/explore/utils/ResultProcessor.test.ts index dc80dffc60e..0a90c31fa6e 100644 --- a/public/app/features/explore/utils/ResultProcessor.test.ts +++ b/public/app/features/explore/utils/ResultProcessor.test.ts @@ -16,8 +16,7 @@ jest.mock('@grafana/data/src/utils/moment_wrapper', () => ({ import { ResultProcessor } from './ResultProcessor'; import { ExploreItemState, ExploreMode } from 'app/types/explore'; import TableModel from 'app/core/table_model'; -import { toFixed } from '@grafana/ui'; -import { TimeSeries, LogRowModel, LogsMetaItem } from '@grafana/data'; +import { TimeSeries, LogRowModel, LogsMetaItem, GraphSeriesXY } from '@grafana/data'; const testContext = (options: any = {}) => { const response = [ @@ -129,20 +128,14 @@ describe('ResultProcessor', () => { expect(theResult).toEqual([ { - alias: 'A-series', - aliasEscaped: 'A-series', - bars: { - fillColor: '#7EB26D', - }, - hasMsResolution: true, - id: 'A-series', label: 'A-series', - legend: true, - stats: {}, color: '#7EB26D', - datapoints: [[39.91264531864214, 1559038518831], [40.35179822906545, 1559038519831]], - unit: undefined, - valueFormater: toFixed, + data: [[1559038518831, 39.91264531864214], [1559038519831, 40.35179822906545]], + info: undefined, + isVisible: true, + yAxis: { + index: 1, + }, }, ]); }); @@ -205,12 +198,14 @@ describe('ResultProcessor', () => { ], series: [ { - alias: 'A-series', - datapoints: [[39.91264531864214, 1559038518831], [40.35179822906545, 1559038519831]], - meta: undefined, - refId: 'A', - target: 'A-series', - unit: undefined, + label: 'A-series', + color: '#7EB26D', + data: [[1559038518831, 39.91264531864214], [1559038519831, 40.35179822906545]], + info: undefined, + isVisible: true, + yAxis: { + index: 1, + }, }, ], }); @@ -234,20 +229,14 @@ describe('ResultProcessor', () => { replacePreviousResults: false, graphResult: [ { - alias: 'A-series', - aliasEscaped: 'A-series', - bars: { - fillColor: '#7EB26D', - }, - hasMsResolution: true, - id: 'A-series', label: 'A-series', - legend: true, - stats: {}, color: '#7EB26D', - datapoints: [[19.91264531864214, 1558038518831], [20.35179822906545, 1558038519831]], - unit: undefined, - valueFormater: toFixed, + data: [[1558038518831, 19.91264531864214], [1558038518831, 20.35179822906545]], + info: undefined, + isVisible: true, + yAxis: { + index: 1, + }, }, ], }); @@ -255,25 +244,19 @@ describe('ResultProcessor', () => { expect(theResult).toEqual([ { - alias: 'A-series', - aliasEscaped: 'A-series', - bars: { - fillColor: '#7EB26D', - }, - hasMsResolution: true, - id: 'A-series', label: 'A-series', - legend: true, - stats: {}, color: '#7EB26D', - datapoints: [ - [19.91264531864214, 1558038518831], - [20.35179822906545, 1558038519831], - [39.91264531864214, 1559038518831], - [40.35179822906545, 1559038519831], + data: [ + [1558038518831, 19.91264531864214], + [1558038518831, 20.35179822906545], + [1559038518831, 39.91264531864214], + [1559038519831, 40.35179822906545], ], - unit: undefined, - valueFormater: toFixed, + info: undefined, + isVisible: true, + yAxis: { + index: 1, + }, }, ]); }); @@ -351,20 +334,14 @@ describe('ResultProcessor', () => { ], series: [ { - alias: 'A-series', - aliasEscaped: 'A-series', - bars: { - fillColor: '#7EB26D', - }, - hasMsResolution: true, - id: 'A-series', label: 'A-series', - legend: true, - stats: {}, color: '#7EB26D', - datapoints: [[37.91264531864214, 1558038518831], [38.35179822906545, 1558038519831]], - unit: undefined, - valueFormater: toFixed, + data: [[1558038518831, 37.91264531864214], [1558038519831, 38.35179822906545]], + info: undefined, + isVisible: true, + yAxis: { + index: 1, + }, }, ], }, @@ -437,26 +414,20 @@ describe('ResultProcessor', () => { ], series: [ { - alias: 'A-series', - aliasEscaped: 'A-series', - bars: { - fillColor: '#7EB26D', - }, - hasMsResolution: true, - id: 'A-series', label: 'A-series', - legend: true, - stats: {}, color: '#7EB26D', - datapoints: [ - [37.91264531864214, 1558038518831], - [38.35179822906545, 1558038519831], - [39.91264531864214, 1559038518831], - [40.35179822906545, 1559038519831], + data: [ + [1558038518831, 37.91264531864214], + [1558038519831, 38.35179822906545], + [1559038518831, 39.91264531864214], + [1559038519831, 40.35179822906545], ], - unit: undefined as string, - valueFormater: toFixed, - }, + info: undefined, + isVisible: true, + yAxis: { + index: 1, + }, + } as GraphSeriesXY, ], }; diff --git a/public/app/features/explore/utils/ResultProcessor.ts b/public/app/features/explore/utils/ResultProcessor.ts index 4509fa811a0..2974c2e7ba0 100644 --- a/public/app/features/explore/utils/ResultProcessor.ts +++ b/public/app/features/explore/utils/ResultProcessor.ts @@ -1,14 +1,22 @@ import { DataQueryResponse, DataQueryResponseData } from '@grafana/ui'; -import { TableData, isTableData, LogsModel, toDataFrame, guessFieldTypes, TimeSeries } from '@grafana/data'; +import { + TableData, + isTableData, + LogsModel, + toDataFrame, + guessFieldTypes, + TimeSeries, + GraphSeriesXY, + LoadingState, +} from '@grafana/data'; import { ExploreItemState, ExploreMode } from 'app/types/explore'; import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import { sortLogsResult } from 'app/core/utils/explore'; import { dataFrameToLogsModel } from 'app/core/logs_model'; -import { default as TimeSeries2 } from 'app/core/time_series2'; -import { DataProcessor } from 'app/plugins/panel/graph/data_processor'; +import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel'; export class ResultProcessor { private rawData: DataQueryResponseData[] = []; @@ -45,12 +53,12 @@ export class ResultProcessor { return this.rawData; }; - getGraphResult = (): TimeSeries[] => { + getGraphResult = (): GraphSeriesXY[] => { if (this.state.mode !== ExploreMode.Metrics) { return []; } - const newResults = this.makeTimeSeriesList(this.metrics); + const newResults = this.createGraphSeries(this.metrics); return this.mergeGraphResults(newResults, this.state.graphResult); }; @@ -100,26 +108,26 @@ export class ResultProcessor { return { ...sortedNewResults, rows, series }; }; - private makeTimeSeriesList = (rawData: any[]) => { - const dataList = getProcessedDataFrames(rawData); - const dataProcessor = new DataProcessor({ xaxis: {}, aliasColors: [] }); // Hack before we use GraphSeriesXY instead - const timeSeries = dataProcessor.getSeriesList({ dataList }); + private createGraphSeries = (rawData: any[]) => { + const dataFrames = getProcessedDataFrames(rawData); + const graphSeries = getGraphSeriesModel( + { series: dataFrames, state: LoadingState.Done }, + {}, + { showBars: false, showLines: true, showPoints: false }, + { + asTable: false, + isVisible: true, + placement: 'under', + } + ); - return (timeSeries as any) as TimeSeries[]; // Hack before we use GraphSeriesXY instead + return graphSeries; }; - private isSameTimeSeries = (a: TimeSeries | TimeSeries2, b: TimeSeries | TimeSeries2) => { - if (a.hasOwnProperty('id') && b.hasOwnProperty('id')) { - const aValue = (a as TimeSeries2).id; - const bValue = (b as TimeSeries2).id; - if (aValue !== undefined && bValue !== undefined && aValue === bValue) { - return true; - } - } - - if (a.hasOwnProperty('alias') && b.hasOwnProperty('alias')) { - const aValue = (a as TimeSeries2).alias; - const bValue = (b as TimeSeries2).alias; + private isSameGraphSeries = (a: GraphSeriesXY, b: GraphSeriesXY) => { + if (a.hasOwnProperty('label') && b.hasOwnProperty('label')) { + const aValue = a.label; + const bValue = b.label; if (aValue !== undefined && bValue !== undefined && aValue === bValue) { return true; } @@ -128,24 +136,21 @@ export class ResultProcessor { return false; }; - private mergeGraphResults = ( - newResults: TimeSeries[] | TimeSeries2[], - prevResults: TimeSeries[] | TimeSeries2[] - ): TimeSeries[] => { + private mergeGraphResults = (newResults: GraphSeriesXY[], prevResults: GraphSeriesXY[]): GraphSeriesXY[] => { if (!prevResults || prevResults.length === 0 || this.replacePreviousResults) { - return (newResults as any) as TimeSeries[]; // Hack before we use GraphSeriesXY instead + return newResults; // Hack before we use GraphSeriesXY instead } - const results: TimeSeries[] = prevResults.slice() as TimeSeries[]; + const results: GraphSeriesXY[] = prevResults.slice() as GraphSeriesXY[]; // update existing results for (let index = 0; index < results.length; index++) { const prevResult = results[index]; for (const newResult of newResults) { - const isSame = this.isSameTimeSeries(prevResult, newResult); + const isSame = this.isSameGraphSeries(prevResult, newResult); if (isSame) { - prevResult.datapoints = prevResult.datapoints.concat(newResult.datapoints); + prevResult.data = prevResult.data.concat(newResult.data); break; } } @@ -155,7 +160,7 @@ export class ResultProcessor { for (const newResult of newResults) { let isNew = true; for (const prevResult of results) { - const isSame = this.isSameTimeSeries(prevResult, newResult); + const isSame = this.isSameGraphSeries(prevResult, newResult); if (isSame) { isNew = false; break; @@ -163,10 +168,7 @@ export class ResultProcessor { } if (isNew) { - const timeSeries2Result = new TimeSeries2({ ...newResult }); - - const result = (timeSeries2Result as any) as TimeSeries; // Hack before we use GraphSeriesXY instead - results.push(result); + results.push(newResult); } } return results; diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index 7b544d7e674..285a63de1f1 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -9,6 +9,7 @@ interface GraphPanelProps extends PanelProps {} export const GraphPanel: React.FunctionComponent = ({ data, timeRange, + timeZone, width, height, options, @@ -39,6 +40,7 @@ export const GraphPanel: React.FunctionComponent = ({ return ( { constructor(props: GraphPanelControllerProps) { super(props); - this.onSeriesToggle = this.onSeriesToggle.bind(this); this.onSeriesColorChange = this.onSeriesColorChange.bind(this); this.onSeriesAxisToggle = this.onSeriesAxisToggle.bind(this); this.onToggleSort = this.onToggleSort.bind(this); @@ -43,7 +41,6 @@ export class GraphPanelController extends React.Component) { - const { hiddenSeries, graphSeriesModel } = this.state; - - if (event.ctrlKey || event.metaKey || event.shiftKey) { - // Toggling series with key makes the series itself to toggle - if (hiddenSeries.indexOf(label) > -1) { - this.setState({ - hiddenSeries: hiddenSeries.filter(series => series !== label), - }); - } else { - this.setState({ - hiddenSeries: hiddenSeries.concat([label]), - }); - } - } else { - // Toggling series with out key toggles all the series but the clicked one - const allSeriesLabels = graphSeriesModel.map(series => series.label); - - if (hiddenSeries.length + 1 === allSeriesLabels.length) { - this.setState({ hiddenSeries: [] }); - } else { - this.setState({ - hiddenSeries: difference(allSeriesLabels, [label]), - }); - } - } - } - render() { const { children } = this.props; - const { graphSeriesModel, hiddenSeries } = this.state; + const { graphSeriesModel } = this.state; - return children({ - series: graphSeriesModel.map(series => ({ - ...series, - isVisible: hiddenSeries.indexOf(series.label) === -1, - })), - onSeriesToggle: this.onSeriesToggle, - onSeriesColorChange: this.onSeriesColorChange, - onSeriesAxisToggle: this.onSeriesAxisToggle, - onToggleSort: this.onToggleSort, - }); + return ( + + {({ onSeriesToggle, toggledSeries }) => { + return children({ + series: toggledSeries, + onSeriesColorChange: this.onSeriesColorChange, + onSeriesAxisToggle: this.onSeriesAxisToggle, + onToggleSort: this.onToggleSort, + onSeriesToggle: onSeriesToggle, + }); + }} + + ); } } diff --git a/public/app/plugins/panel/graph2/GraphSeriesToggler.tsx b/public/app/plugins/panel/graph2/GraphSeriesToggler.tsx new file mode 100644 index 00000000000..564f5b9efe6 --- /dev/null +++ b/public/app/plugins/panel/graph2/GraphSeriesToggler.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { GraphSeriesXY } from '@grafana/data'; +import difference from 'lodash/difference'; +import isEqual from 'lodash/isEqual'; + +interface GraphSeriesTogglerAPI { + onSeriesToggle: (label: string, event: React.MouseEvent) => void; + toggledSeries: GraphSeriesXY[]; +} + +interface GraphSeriesTogglerProps { + children: (api: GraphSeriesTogglerAPI) => JSX.Element; + series: GraphSeriesXY[]; + onHiddenSeriesChanged?: (hiddenSeries: string[]) => void; +} + +interface GraphSeriesTogglerState { + hiddenSeries: string[]; + toggledSeries: GraphSeriesXY[]; +} + +export class GraphSeriesToggler extends React.Component { + constructor(props: GraphSeriesTogglerProps) { + super(props); + + this.onSeriesToggle = this.onSeriesToggle.bind(this); + + this.state = { + hiddenSeries: [], + toggledSeries: props.series, + }; + } + + componentDidUpdate(prevProps: Readonly) { + const { series } = this.props; + if (!isEqual(prevProps.series, series)) { + this.setState({ hiddenSeries: [], toggledSeries: series }); + } + } + + onSeriesToggle(label: string, event: React.MouseEvent) { + const { series, onHiddenSeriesChanged } = this.props; + const { hiddenSeries } = this.state; + + if (event.ctrlKey || event.metaKey || event.shiftKey) { + // Toggling series with key makes the series itself to toggle + const newHiddenSeries = + hiddenSeries.indexOf(label) > -1 + ? hiddenSeries.filter(series => series !== label) + : hiddenSeries.concat([label]); + + const toggledSeries = series.map(series => ({ + ...series, + isVisible: newHiddenSeries.indexOf(series.label) === -1, + })); + this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () => + onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined + ); + return; + } + + // Toggling series with out key toggles all the series but the clicked one + const allSeriesLabels = series.map(series => series.label); + const newHiddenSeries = + hiddenSeries.length + 1 === allSeriesLabels.length ? [] : difference(allSeriesLabels, [label]); + const toggledSeries = series.map(series => ({ + ...series, + isVisible: newHiddenSeries.indexOf(series.label) === -1, + })); + + this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () => + onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined + ); + } + + render() { + const { children } = this.props; + const { toggledSeries } = this.state; + + return children({ + onSeriesToggle: this.onSeriesToggle, + toggledSeries, + }); + } +} diff --git a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts index 5216c8e5616..4e12632dd04 100644 --- a/public/app/plugins/panel/graph2/getGraphSeriesModel.ts +++ b/public/app/plugins/panel/graph2/getGraphSeriesModel.ts @@ -30,7 +30,7 @@ export const getGraphSeriesModel = ( const field = numberFields[i]; // Use external calculator just to make sure it works :) const points = getFlotPairs({ - series, + rows: series.rows, xIndex: timeColumn.index, yIndex: field.index, nullValueMode: NullValueMode.Null, @@ -67,7 +67,9 @@ export const getGraphSeriesModel = ( color: seriesColor, info: statsDisplayValues, isVisible: true, - yAxis: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1, + yAxis: { + index: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1, + }, }); } } diff --git a/public/app/plugins/panel/graph2/types.ts b/public/app/plugins/panel/graph2/types.ts index 7643f8e4ac2..a0872da75d5 100644 --- a/public/app/plugins/panel/graph2/types.ts +++ b/public/app/plugins/panel/graph2/types.ts @@ -1,9 +1,11 @@ import { LegendOptions } from '@grafana/ui'; +import { YAxis } from '@grafana/data'; + import { GraphLegendEditorLegendOptions } from './GraphLegendEditor'; export interface SeriesOptions { color?: string; - yAxis?: number; + yAxis?: YAxis; [key: string]: any; } export interface GraphOptions { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 5a2ddd1bc72..93f62f2f353 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -16,6 +16,7 @@ import { LogsDedupStrategy, LoadingState, AbsoluteTimeRange, + GraphSeriesXY, } from '@grafana/data'; import { Emitter } from 'app/core/core'; @@ -159,7 +160,7 @@ export interface ExploreItemState { /** * List of timeseries to be shown in the Explore graph result viewer. */ - graphResult?: any[]; + graphResult?: GraphSeriesXY[]; /** * History of recent queries. Datasource-specific and initialized via localStorage. */