Commit 4b344032 by Hugo Häggmark Committed by GitHub

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
parent 8fd153ed
import { DisplayValue } from './displayValue'; import { DisplayValue } from './displayValue';
export interface YAxis {
index: number;
min?: number;
tickDecimals?: number;
}
export type GraphSeriesValue = number | null; export type GraphSeriesValue = number | null;
/** View model projection of a series */ /** View model projection of a series */
...@@ -9,7 +15,7 @@ export interface GraphSeriesXY { ...@@ -9,7 +15,7 @@ export interface GraphSeriesXY {
data: GraphSeriesValue[][]; // [x,y][] data: GraphSeriesValue[][]; // [x,y][]
info?: DisplayValue[]; // Legend info info?: DisplayValue[]; // Legend info
isVisible: boolean; isVisible: boolean;
yAxis: number; yAxis: YAxis;
} }
export interface CreatePlotOverlay { export interface CreatePlotOverlay {
......
import { Labels, TimeSeries } from './data'; import { Labels } from './data';
import { GraphSeriesXY } from './graph';
/** /**
* Mapping of log level abbreviation to canonical log level. * Mapping of log level abbreviation to canonical log level.
...@@ -54,7 +55,7 @@ export interface LogsModel { ...@@ -54,7 +55,7 @@ export interface LogsModel {
hasUniqueLabels: boolean; hasUniqueLabels: boolean;
meta?: LogsMetaItem[]; meta?: LogsMetaItem[];
rows: LogRowModel[]; rows: LogRowModel[];
series?: TimeSeries[]; series?: GraphSeriesXY[];
} }
export interface LogSearchMatch { export interface LogSearchMatch {
......
...@@ -4,7 +4,7 @@ import isString from 'lodash/isString'; ...@@ -4,7 +4,7 @@ import isString from 'lodash/isString';
import isBoolean from 'lodash/isBoolean'; import isBoolean from 'lodash/isBoolean';
// Types // 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'; import { isDateTime } from './moment_wrapper';
function convertTableToDataFrame(table: TableData): DataFrame { function convertTableToDataFrame(table: TableData): DataFrame {
...@@ -44,6 +44,23 @@ function convertTimeSeriesToDataFrame(timeSeries: TimeSeries): 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: // PapaParse Dynamic Typing regex:
// https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998 // https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998
const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i; const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i;
...@@ -145,6 +162,9 @@ export const toDataFrame = (data: any): DataFrame => { ...@@ -145,6 +162,9 @@ export const toDataFrame = (data: any): DataFrame => {
if (data.hasOwnProperty('datapoints')) { if (data.hasOwnProperty('datapoints')) {
return convertTimeSeriesToDataFrame(data); return convertTimeSeriesToDataFrame(data);
} }
if (data.hasOwnProperty('data')) {
return convertGraphSeriesToDataFrame(data);
}
if (data.hasOwnProperty('columns')) { if (data.hasOwnProperty('columns')) {
return convertTableToDataFrame(data); return convertTableToDataFrame(data);
} }
......
...@@ -4,16 +4,20 @@ import React, { PureComponent } from 'react'; ...@@ -4,16 +4,20 @@ import React, { PureComponent } from 'react';
import uniqBy from 'lodash/uniqBy'; import uniqBy from 'lodash/uniqBy';
// Types // Types
import { TimeRange, GraphSeriesXY } from '@grafana/data'; import { TimeRange, GraphSeriesXY, AbsoluteTimeRange, TimeZone, DefaultTimeZone } from '@grafana/data';
export interface GraphProps { export interface GraphProps {
series: GraphSeriesXY[]; series: GraphSeriesXY[];
timeRange: TimeRange; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs 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; showLines?: boolean;
showPoints?: boolean; showPoints?: boolean;
showBars?: boolean; showBars?: boolean;
width: number; width: number;
height: number; height: number;
isStacked?: boolean;
lineWidth?: number;
onSelectionChanged?: (range: AbsoluteTimeRange) => void;
} }
export class Graph extends PureComponent<GraphProps> { export class Graph extends PureComponent<GraphProps> {
...@@ -21,9 +25,12 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -21,9 +25,12 @@ export class Graph extends PureComponent<GraphProps> {
showLines: true, showLines: true,
showPoints: false, showPoints: false,
showBars: false, showBars: false,
isStacked: false,
lineWidth: 1,
}; };
element: HTMLElement | null = null; element: HTMLElement | null = null;
$element: any;
componentDidUpdate() { componentDidUpdate() {
this.draw(); this.draw();
...@@ -31,14 +38,32 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -31,14 +38,32 @@ export class Graph extends PureComponent<GraphProps> {
componentDidMount() { componentDidMount() {
this.draw(); 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() { draw() {
if (this.element === null) { if (this.element === null) {
return; return;
} }
const { width, series, timeRange, showLines, showBars, showPoints } = this.props; const { width, series, timeRange, showLines, showBars, showPoints, isStacked, lineWidth, timeZone } = this.props;
if (!width) { if (!width) {
return; return;
...@@ -49,10 +74,16 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -49,10 +74,16 @@ export class Graph extends PureComponent<GraphProps> {
const max = timeRange.to.valueOf(); const max = timeRange.to.valueOf();
const yaxes = uniqBy( const yaxes = uniqBy(
series.map(s => { 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 { return {
show: true, show: true,
index: s.yAxis, index,
position: s.yAxis === 1 ? 'left' : 'right', position: index === 1 ? 'left' : 'right',
min,
tickDecimals,
}; };
}), }),
yAxisConfig => yAxisConfig.index yAxisConfig => yAxisConfig.index
...@@ -62,9 +93,10 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -62,9 +93,10 @@ export class Graph extends PureComponent<GraphProps> {
show: false, show: false,
}, },
series: { series: {
stack: isStacked,
lines: { lines: {
show: showLines, show: showLines,
linewidth: 1, linewidth: lineWidth,
zero: false, zero: false,
}, },
points: { points: {
...@@ -78,7 +110,7 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -78,7 +110,7 @@ export class Graph extends PureComponent<GraphProps> {
fill: 1, fill: 1,
barWidth: 1, barWidth: 1,
zero: false, zero: false,
lineWidth: 0, lineWidth: lineWidth,
}, },
shadowSize: 0, shadowSize: 0,
}, },
...@@ -89,6 +121,7 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -89,6 +121,7 @@ export class Graph extends PureComponent<GraphProps> {
label: 'Datetime', label: 'Datetime',
ticks: ticks, ticks: ticks,
timeformat: timeFormat(ticks, min, max), timeformat: timeFormat(ticks, min, max),
timezone: timeZone ? timeZone : DefaultTimeZone,
}, },
yaxes, yaxes,
grid: { grid: {
...@@ -102,6 +135,10 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -102,6 +135,10 @@ export class Graph extends PureComponent<GraphProps> {
margin: { left: 0, right: 0 }, margin: { left: 0, right: 0 },
labelMarginX: 0, labelMarginX: 0,
}, },
selection: {
mode: 'x',
color: '#666',
},
}; };
try { try {
...@@ -113,9 +150,10 @@ export class Graph extends PureComponent<GraphProps> { ...@@ -113,9 +150,10 @@ export class Graph extends PureComponent<GraphProps> {
} }
render() { render() {
const { height } = this.props;
return ( return (
<div className="graph-panel"> <div className="graph-panel">
<div className="graph-panel__chart" ref={e => (this.element = e)} /> <div className="graph-panel__chart" ref={e => (this.element = e)} style={{ height }} />
</div> </div>
); );
} }
......
...@@ -74,7 +74,7 @@ GraphWithLegendStories.add('default', () => { ...@@ -74,7 +74,7 @@ GraphWithLegendStories.add('default', () => {
.map(s => s.trim()) .map(s => s.trim())
.indexOf(s.label.split('-')[0]) > -1 .indexOf(s.label.split('-')[0]) > -1
) { ) {
s.yAxis = 2; s.yAxis = { index: 2 };
} }
return s; return s;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { GraphSeriesValue } from '@grafana/data'; import { GraphSeriesValue, AbsoluteTimeRange } from '@grafana/data';
import { Graph, GraphProps } from './Graph'; import { Graph, GraphProps } from './Graph';
import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend'; import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend';
...@@ -18,10 +18,11 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions { ...@@ -18,10 +18,11 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
displayMode: LegendDisplayMode; displayMode: LegendDisplayMode;
sortLegendBy?: string; sortLegendBy?: string;
sortLegendDesc?: boolean; sortLegendDesc?: boolean;
onSeriesColorChange: SeriesColorChangeHandler; onSeriesColorChange?: SeriesColorChangeHandler;
onSeriesAxisToggle?: SeriesAxisToggleHandler; onSeriesAxisToggle?: SeriesAxisToggleHandler;
onSeriesToggle?: (label: string, event: React.MouseEvent<HTMLElement>) => void; onSeriesToggle?: (label: string, event: React.MouseEvent<HTMLElement>) => void;
onToggleSort: (sortBy: string) => void; onToggleSort: (sortBy: string) => void;
onSelectionChanged?: (range: AbsoluteTimeRange) => void;
} }
const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({ const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
...@@ -67,6 +68,10 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p ...@@ -67,6 +68,10 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
onToggleSort, onToggleSort,
hideEmpty, hideEmpty,
hideZero, hideZero,
isStacked,
lineWidth,
onSelectionChanged,
timeZone,
} = props; } = props;
const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props); const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props);
...@@ -78,7 +83,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p ...@@ -78,7 +83,7 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
label: s.label, label: s.label,
color: s.color, color: s.color,
isVisible: s.isVisible, isVisible: s.isVisible,
yAxis: s.yAxis, yAxis: s.yAxis.index,
displayValues: s.info || [], displayValues: s.info || [],
}, },
]); ]);
...@@ -90,12 +95,16 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p ...@@ -90,12 +95,16 @@ export const GraphWithLegend: React.FunctionComponent<GraphWithLegendProps> = (p
<Graph <Graph
series={series.filter(s => !!s.isVisible)} series={series.filter(s => !!s.isVisible)}
timeRange={timeRange} timeRange={timeRange}
timeZone={timeZone}
showLines={showLines} showLines={showLines}
showPoints={showPoints} showPoints={showPoints}
showBars={showBars} showBars={showBars}
width={width} width={width}
height={height} height={height}
key={isLegendVisible ? 'legend-visible' : 'legend-invisible'} key={isLegendVisible ? 'legend-visible' : 'legend-invisible'}
isStacked={isStacked}
lineWidth={lineWidth}
onSelectionChanged={onSelectionChanged}
/> />
</div> </div>
......
import { GraphWithLegendProps } from './GraphWithLegend'; import { GraphWithLegendProps } from './GraphWithLegend';
import { LegendDisplayMode } from '../Legend/Legend'; import { LegendDisplayMode } from '../Legend/Legend';
import { dateTime } from '@grafana/data'; import { dateTime, DefaultTimeZone } from '@grafana/data';
// import { LegendList } from '../Legend/LegendList';
export const mockGraphWithLegendData = ({ export const mockGraphWithLegendData = ({
displayMode, displayMode,
...@@ -1099,7 +1098,9 @@ export const mockGraphWithLegendData = ({ ...@@ -1099,7 +1098,9 @@ export const mockGraphWithLegendData = ({
{ title: 'max', text: '18.42', numeric: 18.427101844163694 }, { title: 'max', text: '18.42', numeric: 18.427101844163694 },
], ],
isVisible: true, isVisible: true,
yAxis: 1, yAxis: {
index: 1,
},
}, },
{ {
label: 'B-series', label: 'B-series',
...@@ -2191,7 +2192,9 @@ export const mockGraphWithLegendData = ({ ...@@ -2191,7 +2192,9 @@ export const mockGraphWithLegendData = ({
{ title: 'max', text: '18.42', numeric: 18.427101844163694 }, { title: 'max', text: '18.42', numeric: 18.427101844163694 },
], ],
isVisible: true, isVisible: true,
yAxis: 1, yAxis: {
index: 1,
},
}, },
{ {
label: 'C-series', label: 'C-series',
...@@ -3283,7 +3286,9 @@ export const mockGraphWithLegendData = ({ ...@@ -3283,7 +3286,9 @@ export const mockGraphWithLegendData = ({
{ title: 'max', text: '18.42', numeric: 18.427101844163694 }, { title: 'max', text: '18.42', numeric: 18.427101844163694 },
], ],
isVisible: true, isVisible: true,
yAxis: 1, yAxis: {
index: 1,
},
}, },
], ],
timeRange: { timeRange: {
...@@ -3313,4 +3318,5 @@ export const mockGraphWithLegendData = ({ ...@@ -3313,4 +3318,5 @@ export const mockGraphWithLegendData = ({
}, },
onToggleSort: () => {}, onToggleSort: () => {},
displayMode: displayMode || LegendDisplayMode.List, displayMode: displayMode || LegendDisplayMode.List,
timeZone: DefaultTimeZone,
}); });
import { ComponentClass, ComponentType } from 'react'; 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 { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource';
import { PluginMeta, GrafanaPlugin } from './plugin'; import { PluginMeta, GrafanaPlugin } from './plugin';
...@@ -27,6 +27,7 @@ export interface PanelProps<T = any> { ...@@ -27,6 +27,7 @@ export interface PanelProps<T = any> {
// TODO: annotation?: PanelData; // TODO: annotation?: PanelData;
timeRange: TimeRange; timeRange: TimeRange;
timeZone: TimeZone;
options: T; options: T;
onOptionsChange: (options: T) => void; onOptionsChange: (options: T) => void;
renderCounter: number; renderCounter: number;
......
...@@ -169,7 +169,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi ...@@ -169,7 +169,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
timeColumn < 0 timeColumn < 0
? undefined ? undefined
: getFlotPairs({ : getFlotPairs({
series, rows: series.rows,
xIndex: timeColumn, xIndex: timeColumn,
yIndex: i, yIndex: i,
nullValueMode: NullValueMode.Null, nullValueMode: NullValueMode.Null,
......
import { getFlotPairs } from './flotPairs'; import { getFlotPairs } from './flotPairs';
describe('getFlotPairs', () => { describe('getFlotPairs', () => {
const series = { const rows = [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']];
fields: [],
rows: [[1, 100, 'a'], [2, 200, 'b'], [3, 300, 'c']],
};
it('should get X and y', () => { 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.length).toEqual(3);
expect(pairs[0].length).toEqual(2); expect(pairs[0].length).toEqual(2);
...@@ -15,7 +13,7 @@ describe('getFlotPairs', () => { ...@@ -15,7 +13,7 @@ describe('getFlotPairs', () => {
}); });
it('should work with strings', () => { 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.length).toEqual(3);
expect(pairs[0].length).toEqual(2); expect(pairs[0].length).toEqual(2);
......
// Types // Types
import { NullValueMode, DataFrame, GraphSeriesValue } from '@grafana/data'; import { NullValueMode, GraphSeriesValue } from '@grafana/data';
export interface FlotPairsOptions { export interface FlotPairsOptions {
series: DataFrame; rows: any[][];
xIndex: number; xIndex: number;
yIndex: number; yIndex: number;
nullValueMode?: NullValueMode; nullValueMode?: NullValueMode;
} }
export function getFlotPairs({ series, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] { export function getFlotPairs({ rows, xIndex, yIndex, nullValueMode }: FlotPairsOptions): GraphSeriesValue[][] {
const rows = series.rows;
const ignoreNulls = nullValueMode === NullValueMode.Ignore; const ignoreNulls = nullValueMode === NullValueMode.Ignore;
const nullAsZero = nullValueMode === NullValueMode.AsZero; const nullAsZero = nullValueMode === NullValueMode.AsZero;
......
import _ from 'lodash'; import _ from 'lodash';
import ansicolor from 'vendor/ansicolor/ansicolor'; import ansicolor from 'vendor/ansicolor/ansicolor';
import { colors } from '@grafana/ui'; import { colors, getFlotPairs } from '@grafana/ui';
import { import {
TimeSeries,
Labels, Labels,
LogLevel, LogLevel,
DataFrame, DataFrame,
findCommonLabels, findCommonLabels,
findUniqueLabels, findUniqueLabels,
getLogLevel, getLogLevel,
toLegacyResponseData,
FieldCache, FieldCache,
FieldType, FieldType,
getLogLevelFromKey, getLogLevelFromKey,
...@@ -22,10 +20,15 @@ import { ...@@ -22,10 +20,15 @@ import {
LogsParser, LogsParser,
LogLabelStatsModel, LogLabelStatsModel,
LogsDedupStrategy, LogsDedupStrategy,
GraphSeriesXY,
LoadingState,
dateTime,
toUtc,
NullValueMode,
} from '@grafana/data'; } from '@grafana/data';
import { getThemeColor } from 'app/core/utils/colors'; import { getThemeColor } from 'app/core/utils/colors';
import { hasAnsiCodes } from 'app/core/utils/text'; import { hasAnsiCodes } from 'app/core/utils/text';
import { dateTime, toUtc } from '@grafana/data'; import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
export const LogLevelColor = { export const LogLevelColor = {
[LogLevel.critical]: colors[7], [LogLevel.critical]: colors[7],
...@@ -192,7 +195,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>) ...@@ -192,7 +195,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set<LogLevel>)
}; };
} }
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. // 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 // 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. // 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 ...@@ -242,12 +245,26 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
return a[1] - b[1]; return a[1] - b[1];
}); });
return { const points = getFlotPairs({
datapoints: series.datapoints, rows: series.datapoints,
target: series.alias, xIndex: 1,
alias: series.alias, yIndex: 0,
nullValueMode: NullValueMode.Null,
});
const graphSeries: GraphSeriesXY = {
color: series.color, 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) ...@@ -273,10 +290,16 @@ export function dataFrameToLogsModel(dataFrame: DataFrame[], intervalMs: number)
if (metricSeries.length === 0) { if (metricSeries.length === 0) {
logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs); logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs);
} else { } else {
logsModel.series = []; logsModel.series = getGraphSeriesModel(
for (const series of metricSeries) { { series: metricSeries, state: LoadingState.Done },
logsModel.series.push(toLegacyResponseData(series) as TimeSeries); {},
{ showBars: true, showLines: false, showPoints: false },
{
asTable: false,
isVisible: true,
placement: 'under',
} }
);
} }
return logsModel; return logsModel;
......
...@@ -178,7 +178,7 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -178,7 +178,7 @@ export class PanelChrome extends PureComponent<Props, State> {
queries: panel.targets, queries: panel.targets,
panelId: panel.id, panelId: panel.id,
dashboardId: this.props.dashboard.id, dashboardId: this.props.dashboard.id,
timezone: this.props.dashboard.timezone, timezone: this.props.dashboard.getTimezone(),
timeRange: timeData.timeRange, timeRange: timeData.timeRange,
timeInfo: timeData.timeInfo, timeInfo: timeData.timeInfo,
widthPixels: width, widthPixels: width,
...@@ -251,6 +251,7 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -251,6 +251,7 @@ export class PanelChrome extends PureComponent<Props, State> {
id={panel.id} id={panel.id}
data={data} data={data}
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()} timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
timeZone={this.props.dashboard.getTimezone()}
options={panel.getOptions()} options={panel.getOptions()}
transparent={panel.transparent} transparent={panel.transparent}
width={width - theme.panelPadding * 2} width={width - theme.panelPadding * 2}
......
...@@ -12,7 +12,6 @@ import store from 'app/core/store'; ...@@ -12,7 +12,6 @@ import store from 'app/core/store';
// Components // Components
import { Alert } from './Error'; import { Alert } from './Error';
import ErrorBoundary from './ErrorBoundary'; import ErrorBoundary from './ErrorBoundary';
import GraphContainer from './GraphContainer';
import LogsContainer from './LogsContainer'; import LogsContainer from './LogsContainer';
import QueryRows from './QueryRows'; import QueryRows from './QueryRows';
import TableContainer from './TableContainer'; import TableContainer from './TableContainer';
...@@ -30,7 +29,7 @@ import { ...@@ -30,7 +29,7 @@ import {
} from './state/actions'; } from './state/actions';
// Types // Types
import { RawTimeRange } from '@grafana/data'; import { RawTimeRange, GraphSeriesXY } from '@grafana/data';
import { DataQuery, ExploreStartPageProps, DataSourceApi, DataQueryError } from '@grafana/ui'; import { DataQuery, ExploreStartPageProps, DataSourceApi, DataQueryError } from '@grafana/ui';
import { import {
...@@ -56,6 +55,7 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn'; ...@@ -56,6 +55,7 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn';
import { getTimeZone } from '../profile/state/selectors'; import { getTimeZone } from '../profile/state/selectors';
import { ErrorContainer } from './ErrorContainer'; import { ErrorContainer } from './ErrorContainer';
import { scanStopAction } from './state/actionTypes'; import { scanStopAction } from './state/actionTypes';
import ExploreGraphPanel from './ExploreGraphPanel';
interface ExploreProps { interface ExploreProps {
StartPage?: ComponentClass<ExploreStartPageProps>; StartPage?: ComponentClass<ExploreStartPageProps>;
...@@ -87,6 +87,7 @@ interface ExploreProps { ...@@ -87,6 +87,7 @@ interface ExploreProps {
queryErrors: DataQueryError[]; queryErrors: DataQueryError[];
isLive: boolean; isLive: boolean;
updateTimeRange: typeof updateTimeRange; updateTimeRange: typeof updateTimeRange;
graphResult?: GraphSeriesXY[];
} }
/** /**
...@@ -192,7 +193,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -192,7 +193,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
refreshExplore = () => { refreshExplore = () => {
const { exploreId, update } = this.props; 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); this.props.refreshExplore(exploreId);
} }
}; };
...@@ -225,6 +226,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -225,6 +226,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
queryKeys, queryKeys,
queryErrors, queryErrors,
mode, mode,
graphResult,
} = this.props; } = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore'; const exploreClass = split ? 'explore explore-split' : 'explore';
...@@ -259,7 +261,9 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -259,7 +261,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
{showingStartPage && <StartPage onClickExample={this.onClickExample} />} {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && ( {!showingStartPage && (
<> <>
{mode === ExploreMode.Metrics && <GraphContainer width={width} exploreId={exploreId} />} {mode === ExploreMode.Metrics && (
<ExploreGraphPanel exploreId={exploreId} series={graphResult} width={width} />
)}
{mode === ExploreMode.Metrics && ( {mode === ExploreMode.Metrics && (
<TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} /> <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />
)} )}
...@@ -306,6 +310,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { ...@@ -306,6 +310,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
isLive, isLive,
supportedModes, supportedModes,
mode, mode,
graphResult,
} = item; } = item;
const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState; const { datasource, queries, range: urlRange, mode: urlMode, ui } = (urlState || {}) as ExploreUrlState;
...@@ -318,15 +323,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { ...@@ -318,15 +323,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
const urlModeIsValid = supportedModes.includes(urlMode); const urlModeIsValid = supportedModes.includes(urlMode);
const modeStateIsValid = supportedModes.includes(mode); const modeStateIsValid = supportedModes.includes(mode);
if (urlModeIsValid) { if (modeStateIsValid) {
newMode = urlMode;
} else if (modeStateIsValid) {
newMode = mode; newMode = mode;
} else if (urlModeIsValid) {
newMode = urlMode;
} else { } else {
newMode = supportedModes[0]; newMode = supportedModes[0];
} }
} else { } 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; const initialUI = ui || DEFAULT_UI_STATE;
...@@ -349,6 +354,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { ...@@ -349,6 +354,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
initialUI, initialUI,
queryErrors, queryErrors,
isLive, isLive,
graphResult,
}; };
} }
......
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<Props, State> {
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 (
<GraphSeriesToggler series={seriesToShow} onHiddenSeriesChanged={onHiddenSeriesChanged}>
{({ onSeriesToggle, toggledSeries }) => {
return (
<GraphWithLegend
displayMode={LegendDisplayMode.List}
height={height}
isLegendVisible={true}
placement={'under'}
width={width}
timeRange={timeRange}
timeZone={timeZone}
showBars={showBars}
showLines={showLines}
showPoints={false}
onToggleSort={() => {}}
series={toggledSeries}
isStacked={isStacked}
lineWidth={lineWidth}
onSeriesToggle={onSeriesToggle}
onSelectionChanged={this.onChangeTime}
/>
);
}}
</GraphSeriesToggler>
);
};
render() {
const { series, mode, showingGraph, loading } = this.props;
const { showAllTimeSeries } = this.state;
return (
<>
{series && series.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
<div className="time-series-disclaimer">
<i className="fa fa-fw fa-warning disclaimer-icon" />
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
<span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
series.length
}`}</span>
</div>
)}
{mode === ExploreMode.Metrics && (
<Panel label="Graph" collapsible isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
{this.renderGraph()}
</Panel>
)}
{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)
);
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(<Graph {...props} />);
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();
});
});
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<GraphProps, GraphState> {
$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<HTMLElement>) {
// 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 && (
<div className="time-series-disclaimer">
<i className="fa fa-fw fa-warning disclaimer-icon" />
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
<span className="show-all-time-series" onClick={this.onShowAllTimeSeries}>{`Show all ${
this.props.data.length
}`}</span>
</div>
)}
<div id={id} className="explore-graph" style={{ height }} />
<GraphLegend
items={this.getLegendItems()}
displayMode={LegendDisplayMode.List}
placement="under"
onLabelClick={(item, event) => {
this.onSeriesToggle(item.label, event);
}}
/>
</>
);
}
}
export default Graph;
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<GraphContainerProps> {
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 (
<Panel label="Graph" collapsible isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
{graphResult && (
<Graph
data={graphResult}
height={graphHeight}
id={`explore-graph-${exploreId}`}
onChangeTime={this.onChangeTime}
range={absoluteRange}
timeZone={timeZone}
split={split}
width={width}
/>
)}
</Panel>
);
}
}
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)
);
...@@ -13,32 +13,17 @@ import { ...@@ -13,32 +13,17 @@ import {
LogsDedupStrategy, LogsDedupStrategy,
LogRowModel, LogRowModel,
} from '@grafana/data'; } from '@grafana/data';
import TimeSeries from 'app/core/time_series2';
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
import Graph from './Graph';
import { LogLabels } from './LogLabels'; import { LogLabels } from './LogLabels';
import { LogRow } from './LogRow'; import { LogRow } from './LogRow';
import { LogsDedupDescription } from 'app/core/logs_model'; import { LogsDedupDescription } from 'app/core/logs_model';
import ExploreGraphPanel from './ExploreGraphPanel';
import { ExploreId } from 'app/types';
const PREVIEW_LIMIT = 100; 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) { function renderMetaItem(value: any, kind: LogsMetaKind) {
if (kind === LogsMetaKind.LabelsMap) { if (kind === LogsMetaKind.LabelsMap) {
return ( return (
...@@ -54,7 +39,7 @@ interface Props { ...@@ -54,7 +39,7 @@ interface Props {
data?: LogsModel; data?: LogsModel;
dedupedData?: LogsModel; dedupedData?: LogsModel;
width: number; width: number;
exploreId: string; exploreId: ExploreId;
highlighterExpressions: string[]; highlighterExpressions: string[];
loading: boolean; loading: boolean;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
...@@ -135,7 +120,7 @@ export default class Logs extends PureComponent<Props, State> { ...@@ -135,7 +120,7 @@ export default class Logs extends PureComponent<Props, State> {
}); });
}; };
onToggleLogLevel = (rawLevel: string, hiddenRawLevels: string[]) => { onToggleLogLevel = (hiddenRawLevels: string[]) => {
const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map((level: LogLevel) => LogLevel[level]); const hiddenLogLevels: LogLevel[] = hiddenRawLevels.map((level: LogLevel) => LogLevel[level]);
this.props.onToggleLogLevel(hiddenLogLevels); this.props.onToggleLogLevel(hiddenLogLevels);
}; };
...@@ -157,7 +142,6 @@ export default class Logs extends PureComponent<Props, State> { ...@@ -157,7 +142,6 @@ export default class Logs extends PureComponent<Props, State> {
highlighterExpressions, highlighterExpressions,
loading = false, loading = false,
onClickLabel, onClickLabel,
absoluteRange,
timeZone, timeZone,
scanning, scanning,
scanRange, scanRange,
...@@ -193,23 +177,15 @@ export default class Logs extends PureComponent<Props, State> { ...@@ -193,23 +177,15 @@ export default class Logs extends PureComponent<Props, State> {
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
const getRows = () => processedRows; const getRows = () => processedRows;
const timeSeries = data.series
? data.series.map(series => new TimeSeries(series))
: [new TimeSeries({ datapoints: [] })];
return ( return (
<div className="logs-panel"> <div className="logs-panel">
<div className="logs-panel-graph"> <div className="logs-panel-graph">
<Graph <ExploreGraphPanel
data={timeSeries} exploreId={exploreId}
height={100} series={data.series}
width={width} width={width}
range={absoluteRange} onHiddenSeriesChanged={this.onToggleLogLevel}
timeZone={timeZone}
id={`explore-logs-graph-${exploreId}`}
onChangeTime={this.props.onChangeTime}
onToggleSeries={this.onToggleLogLevel}
userOptions={graphOptions}
/> />
</div> </div>
<div className="logs-panel-options"> <div className="logs-panel-options">
......
...@@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act ...@@ -13,7 +13,7 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
// Types // Types
import { StoreState } from 'app/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 { DataQuery, DataSourceApi, QueryFixAction, DataSourceStatus, PanelData, DataQueryError } from '@grafana/ui';
import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore'; import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter'; import { Emitter } from 'app/core/utils/emitter';
...@@ -217,7 +217,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) ...@@ -217,7 +217,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
const query = queries[index]; const query = queries[index];
const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected; const datasourceStatus = datasourceError ? DataSourceStatus.Disconnected : DataSourceStatus.Connected;
const error = queryErrors.filter(queryError => queryError.refId === query.refId)[0]; 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 = { const queryResponse: PanelData = {
series, series,
state: loadingState, state: loadingState,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<Fragment>
<div
className="explore-graph"
id="graph"
style={
Object {
"height": 100,
}
}
/>
<GraphLegend
displayMode="list"
items={
Array [
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
]
}
onLabelClick={[Function]}
placement="under"
/>
</Fragment>
`;
exports[`Render should render component with disclaimer 1`] = `
<Fragment>
<div
className="time-series-disclaimer"
>
<i
className="fa fa-fw fa-warning disclaimer-icon"
/>
Showing only 20 time series.
<span
className="show-all-time-series"
onClick={[Function]}
>
Show all 27
</span>
</div>
<div
className="explore-graph"
id="graph"
style={
Object {
"height": 100,
}
}
/>
<GraphLegend
displayMode="list"
items={
Array [
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
Object {
"color": undefined,
"isVisible": true,
"label": undefined,
"yAxis": 1,
},
]
}
onLabelClick={[Function]}
placement="under"
/>
</Fragment>
`;
exports[`Render should show query return no time series 1`] = `
<Fragment>
<div
className="explore-graph"
id="graph"
style={
Object {
"height": 100,
}
}
/>
<GraphLegend
displayMode="list"
items={Array []}
onLabelClick={[Function]}
placement="under"
/>
</Fragment>
`;
...@@ -15,9 +15,9 @@ import { ...@@ -15,9 +15,9 @@ import {
TimeRange, TimeRange,
DataFrame, DataFrame,
LogsModel, LogsModel,
TimeSeries,
LoadingState, LoadingState,
AbsoluteTimeRange, AbsoluteTimeRange,
GraphSeriesXY,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore'; import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore';
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
...@@ -149,7 +149,7 @@ export interface QuerySuccessPayload { ...@@ -149,7 +149,7 @@ export interface QuerySuccessPayload {
exploreId: ExploreId; exploreId: ExploreId;
latency: number; latency: number;
loadingState: LoadingState; loadingState: LoadingState;
graphResult: TimeSeries[]; graphResult: GraphSeriesXY[];
tableResult: TableModel; tableResult: TableModel;
logsResult: LogsModel; logsResult: LogsModel;
} }
......
...@@ -16,8 +16,7 @@ jest.mock('@grafana/data/src/utils/moment_wrapper', () => ({ ...@@ -16,8 +16,7 @@ jest.mock('@grafana/data/src/utils/moment_wrapper', () => ({
import { ResultProcessor } from './ResultProcessor'; import { ResultProcessor } from './ResultProcessor';
import { ExploreItemState, ExploreMode } from 'app/types/explore'; import { ExploreItemState, ExploreMode } from 'app/types/explore';
import TableModel from 'app/core/table_model'; import TableModel from 'app/core/table_model';
import { toFixed } from '@grafana/ui'; import { TimeSeries, LogRowModel, LogsMetaItem, GraphSeriesXY } from '@grafana/data';
import { TimeSeries, LogRowModel, LogsMetaItem } from '@grafana/data';
const testContext = (options: any = {}) => { const testContext = (options: any = {}) => {
const response = [ const response = [
...@@ -129,20 +128,14 @@ describe('ResultProcessor', () => { ...@@ -129,20 +128,14 @@ describe('ResultProcessor', () => {
expect(theResult).toEqual([ expect(theResult).toEqual([
{ {
alias: 'A-series',
aliasEscaped: 'A-series',
bars: {
fillColor: '#7EB26D',
},
hasMsResolution: true,
id: 'A-series',
label: 'A-series', label: 'A-series',
legend: true,
stats: {},
color: '#7EB26D', color: '#7EB26D',
datapoints: [[39.91264531864214, 1559038518831], [40.35179822906545, 1559038519831]], data: [[1559038518831, 39.91264531864214], [1559038519831, 40.35179822906545]],
unit: undefined, info: undefined,
valueFormater: toFixed, isVisible: true,
yAxis: {
index: 1,
},
}, },
]); ]);
}); });
...@@ -205,12 +198,14 @@ describe('ResultProcessor', () => { ...@@ -205,12 +198,14 @@ describe('ResultProcessor', () => {
], ],
series: [ series: [
{ {
alias: 'A-series', label: 'A-series',
datapoints: [[39.91264531864214, 1559038518831], [40.35179822906545, 1559038519831]], color: '#7EB26D',
meta: undefined, data: [[1559038518831, 39.91264531864214], [1559038519831, 40.35179822906545]],
refId: 'A', info: undefined,
target: 'A-series', isVisible: true,
unit: undefined, yAxis: {
index: 1,
},
}, },
], ],
}); });
...@@ -234,20 +229,14 @@ describe('ResultProcessor', () => { ...@@ -234,20 +229,14 @@ describe('ResultProcessor', () => {
replacePreviousResults: false, replacePreviousResults: false,
graphResult: [ graphResult: [
{ {
alias: 'A-series',
aliasEscaped: 'A-series',
bars: {
fillColor: '#7EB26D',
},
hasMsResolution: true,
id: 'A-series',
label: 'A-series', label: 'A-series',
legend: true,
stats: {},
color: '#7EB26D', color: '#7EB26D',
datapoints: [[19.91264531864214, 1558038518831], [20.35179822906545, 1558038519831]], data: [[1558038518831, 19.91264531864214], [1558038518831, 20.35179822906545]],
unit: undefined, info: undefined,
valueFormater: toFixed, isVisible: true,
yAxis: {
index: 1,
},
}, },
], ],
}); });
...@@ -255,25 +244,19 @@ describe('ResultProcessor', () => { ...@@ -255,25 +244,19 @@ describe('ResultProcessor', () => {
expect(theResult).toEqual([ expect(theResult).toEqual([
{ {
alias: 'A-series',
aliasEscaped: 'A-series',
bars: {
fillColor: '#7EB26D',
},
hasMsResolution: true,
id: 'A-series',
label: 'A-series', label: 'A-series',
legend: true,
stats: {},
color: '#7EB26D', color: '#7EB26D',
datapoints: [ data: [
[19.91264531864214, 1558038518831], [1558038518831, 19.91264531864214],
[20.35179822906545, 1558038519831], [1558038518831, 20.35179822906545],
[39.91264531864214, 1559038518831], [1559038518831, 39.91264531864214],
[40.35179822906545, 1559038519831], [1559038519831, 40.35179822906545],
], ],
unit: undefined, info: undefined,
valueFormater: toFixed, isVisible: true,
yAxis: {
index: 1,
},
}, },
]); ]);
}); });
...@@ -351,20 +334,14 @@ describe('ResultProcessor', () => { ...@@ -351,20 +334,14 @@ describe('ResultProcessor', () => {
], ],
series: [ series: [
{ {
alias: 'A-series',
aliasEscaped: 'A-series',
bars: {
fillColor: '#7EB26D',
},
hasMsResolution: true,
id: 'A-series',
label: 'A-series', label: 'A-series',
legend: true,
stats: {},
color: '#7EB26D', color: '#7EB26D',
datapoints: [[37.91264531864214, 1558038518831], [38.35179822906545, 1558038519831]], data: [[1558038518831, 37.91264531864214], [1558038519831, 38.35179822906545]],
unit: undefined, info: undefined,
valueFormater: toFixed, isVisible: true,
yAxis: {
index: 1,
},
}, },
], ],
}, },
...@@ -437,26 +414,20 @@ describe('ResultProcessor', () => { ...@@ -437,26 +414,20 @@ describe('ResultProcessor', () => {
], ],
series: [ series: [
{ {
alias: 'A-series',
aliasEscaped: 'A-series',
bars: {
fillColor: '#7EB26D',
},
hasMsResolution: true,
id: 'A-series',
label: 'A-series', label: 'A-series',
legend: true,
stats: {},
color: '#7EB26D', color: '#7EB26D',
datapoints: [ data: [
[37.91264531864214, 1558038518831], [1558038518831, 37.91264531864214],
[38.35179822906545, 1558038519831], [1558038519831, 38.35179822906545],
[39.91264531864214, 1559038518831], [1559038518831, 39.91264531864214],
[40.35179822906545, 1559038519831], [1559038519831, 40.35179822906545],
], ],
unit: undefined as string, info: undefined,
valueFormater: toFixed, isVisible: true,
yAxis: {
index: 1,
}, },
} as GraphSeriesXY,
], ],
}; };
......
import { DataQueryResponse, DataQueryResponseData } from '@grafana/ui'; 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 { ExploreItemState, ExploreMode } from 'app/types/explore';
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState'; import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import { sortLogsResult } from 'app/core/utils/explore'; import { sortLogsResult } from 'app/core/utils/explore';
import { dataFrameToLogsModel } from 'app/core/logs_model'; import { dataFrameToLogsModel } from 'app/core/logs_model';
import { default as TimeSeries2 } from 'app/core/time_series2'; import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel';
import { DataProcessor } from 'app/plugins/panel/graph/data_processor';
export class ResultProcessor { export class ResultProcessor {
private rawData: DataQueryResponseData[] = []; private rawData: DataQueryResponseData[] = [];
...@@ -45,12 +53,12 @@ export class ResultProcessor { ...@@ -45,12 +53,12 @@ export class ResultProcessor {
return this.rawData; return this.rawData;
}; };
getGraphResult = (): TimeSeries[] => { getGraphResult = (): GraphSeriesXY[] => {
if (this.state.mode !== ExploreMode.Metrics) { if (this.state.mode !== ExploreMode.Metrics) {
return []; return [];
} }
const newResults = this.makeTimeSeriesList(this.metrics); const newResults = this.createGraphSeries(this.metrics);
return this.mergeGraphResults(newResults, this.state.graphResult); return this.mergeGraphResults(newResults, this.state.graphResult);
}; };
...@@ -100,26 +108,26 @@ export class ResultProcessor { ...@@ -100,26 +108,26 @@ export class ResultProcessor {
return { ...sortedNewResults, rows, series }; return { ...sortedNewResults, rows, series };
}; };
private makeTimeSeriesList = (rawData: any[]) => { private createGraphSeries = (rawData: any[]) => {
const dataList = getProcessedDataFrames(rawData); const dataFrames = getProcessedDataFrames(rawData);
const dataProcessor = new DataProcessor({ xaxis: {}, aliasColors: [] }); // Hack before we use GraphSeriesXY instead const graphSeries = getGraphSeriesModel(
const timeSeries = dataProcessor.getSeriesList({ dataList }); { 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) => { private isSameGraphSeries = (a: GraphSeriesXY, b: GraphSeriesXY) => {
if (a.hasOwnProperty('id') && b.hasOwnProperty('id')) { if (a.hasOwnProperty('label') && b.hasOwnProperty('label')) {
const aValue = (a as TimeSeries2).id; const aValue = a.label;
const bValue = (b as TimeSeries2).id; const bValue = b.label;
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;
if (aValue !== undefined && bValue !== undefined && aValue === bValue) { if (aValue !== undefined && bValue !== undefined && aValue === bValue) {
return true; return true;
} }
...@@ -128,24 +136,21 @@ export class ResultProcessor { ...@@ -128,24 +136,21 @@ export class ResultProcessor {
return false; return false;
}; };
private mergeGraphResults = ( private mergeGraphResults = (newResults: GraphSeriesXY[], prevResults: GraphSeriesXY[]): GraphSeriesXY[] => {
newResults: TimeSeries[] | TimeSeries2[],
prevResults: TimeSeries[] | TimeSeries2[]
): TimeSeries[] => {
if (!prevResults || prevResults.length === 0 || this.replacePreviousResults) { 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 // update existing results
for (let index = 0; index < results.length; index++) { for (let index = 0; index < results.length; index++) {
const prevResult = results[index]; const prevResult = results[index];
for (const newResult of newResults) { for (const newResult of newResults) {
const isSame = this.isSameTimeSeries(prevResult, newResult); const isSame = this.isSameGraphSeries(prevResult, newResult);
if (isSame) { if (isSame) {
prevResult.datapoints = prevResult.datapoints.concat(newResult.datapoints); prevResult.data = prevResult.data.concat(newResult.data);
break; break;
} }
} }
...@@ -155,7 +160,7 @@ export class ResultProcessor { ...@@ -155,7 +160,7 @@ export class ResultProcessor {
for (const newResult of newResults) { for (const newResult of newResults) {
let isNew = true; let isNew = true;
for (const prevResult of results) { for (const prevResult of results) {
const isSame = this.isSameTimeSeries(prevResult, newResult); const isSame = this.isSameGraphSeries(prevResult, newResult);
if (isSame) { if (isSame) {
isNew = false; isNew = false;
break; break;
...@@ -163,10 +168,7 @@ export class ResultProcessor { ...@@ -163,10 +168,7 @@ export class ResultProcessor {
} }
if (isNew) { if (isNew) {
const timeSeries2Result = new TimeSeries2({ ...newResult }); results.push(newResult);
const result = (timeSeries2Result as any) as TimeSeries; // Hack before we use GraphSeriesXY instead
results.push(result);
} }
} }
return results; return results;
......
...@@ -9,6 +9,7 @@ interface GraphPanelProps extends PanelProps<Options> {} ...@@ -9,6 +9,7 @@ interface GraphPanelProps extends PanelProps<Options> {}
export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
data, data,
timeRange, timeRange,
timeZone,
width, width,
height, height,
options, options,
...@@ -39,6 +40,7 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({ ...@@ -39,6 +40,7 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
return ( return (
<GraphWithLegend <GraphWithLegend
timeRange={timeRange} timeRange={timeRange}
timeZone={timeZone}
width={width} width={width}
height={height} height={height}
displayMode={asTable ? LegendDisplayMode.Table : LegendDisplayMode.List} displayMode={asTable ? LegendDisplayMode.Table : LegendDisplayMode.List}
......
import React from 'react'; import React from 'react';
import { PanelData } from '@grafana/ui'; import { PanelData } from '@grafana/ui';
import { GraphSeriesXY } from '@grafana/data'; import { GraphSeriesXY } from '@grafana/data';
import difference from 'lodash/difference';
import { getGraphSeriesModel } from './getGraphSeriesModel'; import { getGraphSeriesModel } from './getGraphSeriesModel';
import { Options, SeriesOptions } from './types'; import { Options, SeriesOptions } from './types';
import { SeriesColorChangeHandler, SeriesAxisToggleHandler } from '@grafana/ui/src/components/Graph/GraphWithLegend'; import { SeriesColorChangeHandler, SeriesAxisToggleHandler } from '@grafana/ui/src/components/Graph/GraphWithLegend';
import { GraphSeriesToggler } from './GraphSeriesToggler';
interface GraphPanelControllerAPI { interface GraphPanelControllerAPI {
series: GraphSeriesXY[]; series: GraphSeriesXY[];
...@@ -24,14 +24,12 @@ interface GraphPanelControllerProps { ...@@ -24,14 +24,12 @@ interface GraphPanelControllerProps {
interface GraphPanelControllerState { interface GraphPanelControllerState {
graphSeriesModel: GraphSeriesXY[]; graphSeriesModel: GraphSeriesXY[];
hiddenSeries: string[];
} }
export class GraphPanelController extends React.Component<GraphPanelControllerProps, GraphPanelControllerState> { export class GraphPanelController extends React.Component<GraphPanelControllerProps, GraphPanelControllerState> {
constructor(props: GraphPanelControllerProps) { constructor(props: GraphPanelControllerProps) {
super(props); super(props);
this.onSeriesToggle = this.onSeriesToggle.bind(this);
this.onSeriesColorChange = this.onSeriesColorChange.bind(this); this.onSeriesColorChange = this.onSeriesColorChange.bind(this);
this.onSeriesAxisToggle = this.onSeriesAxisToggle.bind(this); this.onSeriesAxisToggle = this.onSeriesAxisToggle.bind(this);
this.onToggleSort = this.onToggleSort.bind(this); this.onToggleSort = this.onToggleSort.bind(this);
...@@ -43,7 +41,6 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr ...@@ -43,7 +41,6 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
props.options.graph, props.options.graph,
props.options.legend props.options.legend
), ),
hiddenSeries: [],
}; };
} }
...@@ -76,10 +73,15 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr ...@@ -76,10 +73,15 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
const seriesOptionsUpdate: SeriesOptions = series[label] const seriesOptionsUpdate: SeriesOptions = series[label]
? { ? {
...series[label], ...series[label],
yAxis, yAxis: {
...series[label].yAxis,
index: yAxis,
},
} }
: { : {
yAxis, yAxis: {
index: yAxis,
},
}; };
this.onSeriesOptionsUpdate(label, seriesOptionsUpdate); this.onSeriesOptionsUpdate(label, seriesOptionsUpdate);
} }
...@@ -112,47 +114,22 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr ...@@ -112,47 +114,22 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
}); });
} }
onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
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() { render() {
const { children } = this.props; const { children } = this.props;
const { graphSeriesModel, hiddenSeries } = this.state; const { graphSeriesModel } = this.state;
return (
<GraphSeriesToggler series={graphSeriesModel}>
{({ onSeriesToggle, toggledSeries }) => {
return children({ return children({
series: graphSeriesModel.map(series => ({ series: toggledSeries,
...series,
isVisible: hiddenSeries.indexOf(series.label) === -1,
})),
onSeriesToggle: this.onSeriesToggle,
onSeriesColorChange: this.onSeriesColorChange, onSeriesColorChange: this.onSeriesColorChange,
onSeriesAxisToggle: this.onSeriesAxisToggle, onSeriesAxisToggle: this.onSeriesAxisToggle,
onToggleSort: this.onToggleSort, onToggleSort: this.onToggleSort,
onSeriesToggle: onSeriesToggle,
}); });
}}
</GraphSeriesToggler>
);
} }
} }
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<HTMLElement>) => 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<GraphSeriesTogglerProps, GraphSeriesTogglerState> {
constructor(props: GraphSeriesTogglerProps) {
super(props);
this.onSeriesToggle = this.onSeriesToggle.bind(this);
this.state = {
hiddenSeries: [],
toggledSeries: props.series,
};
}
componentDidUpdate(prevProps: Readonly<GraphSeriesTogglerProps>) {
const { series } = this.props;
if (!isEqual(prevProps.series, series)) {
this.setState({ hiddenSeries: [], toggledSeries: series });
}
}
onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
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,
});
}
}
...@@ -30,7 +30,7 @@ export const getGraphSeriesModel = ( ...@@ -30,7 +30,7 @@ export const getGraphSeriesModel = (
const field = numberFields[i]; const field = numberFields[i];
// Use external calculator just to make sure it works :) // Use external calculator just to make sure it works :)
const points = getFlotPairs({ const points = getFlotPairs({
series, rows: series.rows,
xIndex: timeColumn.index, xIndex: timeColumn.index,
yIndex: field.index, yIndex: field.index,
nullValueMode: NullValueMode.Null, nullValueMode: NullValueMode.Null,
...@@ -67,7 +67,9 @@ export const getGraphSeriesModel = ( ...@@ -67,7 +67,9 @@ export const getGraphSeriesModel = (
color: seriesColor, color: seriesColor,
info: statsDisplayValues, info: statsDisplayValues,
isVisible: true, isVisible: true,
yAxis: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1, yAxis: {
index: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1,
},
}); });
} }
} }
......
import { LegendOptions } from '@grafana/ui'; import { LegendOptions } from '@grafana/ui';
import { YAxis } from '@grafana/data';
import { GraphLegendEditorLegendOptions } from './GraphLegendEditor'; import { GraphLegendEditorLegendOptions } from './GraphLegendEditor';
export interface SeriesOptions { export interface SeriesOptions {
color?: string; color?: string;
yAxis?: number; yAxis?: YAxis;
[key: string]: any; [key: string]: any;
} }
export interface GraphOptions { export interface GraphOptions {
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
LogsDedupStrategy, LogsDedupStrategy,
LoadingState, LoadingState,
AbsoluteTimeRange, AbsoluteTimeRange,
GraphSeriesXY,
} from '@grafana/data'; } from '@grafana/data';
import { Emitter } from 'app/core/core'; import { Emitter } from 'app/core/core';
...@@ -159,7 +160,7 @@ export interface ExploreItemState { ...@@ -159,7 +160,7 @@ export interface ExploreItemState {
/** /**
* List of timeseries to be shown in the Explore graph result viewer. * 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. * History of recent queries. Datasource-specific and initialized via localStorage.
*/ */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment