mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -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<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.
|
||||
// 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;
|
||||
|
||||
@@ -178,7 +178,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
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<Props, State> {
|
||||
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}
|
||||
|
||||
@@ -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<ExploreStartPageProps>;
|
||||
@@ -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<ExploreProps> {
|
||||
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<ExploreProps> {
|
||||
queryKeys,
|
||||
queryErrors,
|
||||
mode,
|
||||
graphResult,
|
||||
} = this.props;
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
|
||||
@@ -259,7 +261,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
|
||||
{!showingStartPage && (
|
||||
<>
|
||||
{mode === ExploreMode.Metrics && <GraphContainer width={width} exploreId={exploreId} />}
|
||||
{mode === ExploreMode.Metrics && (
|
||||
<ExploreGraphPanel exploreId={exploreId} series={graphResult} width={width} />
|
||||
)}
|
||||
{mode === ExploreMode.Metrics && (
|
||||
<TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />
|
||||
)}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
173
public/app/features/explore/ExploreGraphPanel.tsx
Normal file
173
public/app/features/explore/ExploreGraphPanel.tsx
Normal file
@@ -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<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)
|
||||
);
|
||||
@@ -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(<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();
|
||||
});
|
||||
});
|
||||
@@ -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<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;
|
||||
@@ -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<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 {
|
||||
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<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
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<Props, State> {
|
||||
highlighterExpressions,
|
||||
loading = false,
|
||||
onClickLabel,
|
||||
absoluteRange,
|
||||
timeZone,
|
||||
scanning,
|
||||
scanRange,
|
||||
@@ -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
|
||||
const getRows = () => processedRows;
|
||||
const timeSeries = data.series
|
||||
? data.series.map(series => new TimeSeries(series))
|
||||
: [new TimeSeries({ datapoints: [] })];
|
||||
|
||||
return (
|
||||
<div className="logs-panel">
|
||||
<div className="logs-panel-graph">
|
||||
<Graph
|
||||
data={timeSeries}
|
||||
height={100}
|
||||
<ExploreGraphPanel
|
||||
exploreId={exploreId}
|
||||
series={data.series}
|
||||
width={width}
|
||||
range={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
id={`explore-logs-graph-${exploreId}`}
|
||||
onChangeTime={this.props.onChangeTime}
|
||||
onToggleSeries={this.onToggleLogLevel}
|
||||
userOptions={graphOptions}
|
||||
onHiddenSeriesChanged={this.onToggleLogLevel}
|
||||
/>
|
||||
</div>
|
||||
<div className="logs-panel-options">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
// 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,7 @@ interface GraphPanelProps extends PanelProps<Options> {}
|
||||
export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
|
||||
data,
|
||||
timeRange,
|
||||
timeZone,
|
||||
width,
|
||||
height,
|
||||
options,
|
||||
@@ -39,6 +40,7 @@ export const GraphPanel: React.FunctionComponent<GraphPanelProps> = ({
|
||||
return (
|
||||
<GraphWithLegend
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
width={width}
|
||||
height={height}
|
||||
displayMode={asTable ? LegendDisplayMode.Table : LegendDisplayMode.List}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { PanelData } from '@grafana/ui';
|
||||
import { GraphSeriesXY } from '@grafana/data';
|
||||
import difference from 'lodash/difference';
|
||||
|
||||
import { getGraphSeriesModel } from './getGraphSeriesModel';
|
||||
import { Options, SeriesOptions } from './types';
|
||||
import { SeriesColorChangeHandler, SeriesAxisToggleHandler } from '@grafana/ui/src/components/Graph/GraphWithLegend';
|
||||
import { GraphSeriesToggler } from './GraphSeriesToggler';
|
||||
|
||||
interface GraphPanelControllerAPI {
|
||||
series: GraphSeriesXY[];
|
||||
@@ -24,14 +24,12 @@ interface GraphPanelControllerProps {
|
||||
|
||||
interface GraphPanelControllerState {
|
||||
graphSeriesModel: GraphSeriesXY[];
|
||||
hiddenSeries: string[];
|
||||
}
|
||||
|
||||
export class GraphPanelController extends React.Component<GraphPanelControllerProps, GraphPanelControllerState> {
|
||||
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<GraphPanelControllerPr
|
||||
props.options.graph,
|
||||
props.options.legend
|
||||
),
|
||||
hiddenSeries: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,10 +73,15 @@ export class GraphPanelController extends React.Component<GraphPanelControllerPr
|
||||
const seriesOptionsUpdate: SeriesOptions = series[label]
|
||||
? {
|
||||
...series[label],
|
||||
yAxis,
|
||||
yAxis: {
|
||||
...series[label].yAxis,
|
||||
index: yAxis,
|
||||
},
|
||||
}
|
||||
: {
|
||||
yAxis,
|
||||
yAxis: {
|
||||
index: yAxis,
|
||||
},
|
||||
};
|
||||
this.onSeriesOptionsUpdate(label, seriesOptionsUpdate);
|
||||
}
|
||||
@@ -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() {
|
||||
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 (
|
||||
<GraphSeriesToggler series={graphSeriesModel}>
|
||||
{({ onSeriesToggle, toggledSeries }) => {
|
||||
return children({
|
||||
series: toggledSeries,
|
||||
onSeriesColorChange: this.onSeriesColorChange,
|
||||
onSeriesAxisToggle: this.onSeriesAxisToggle,
|
||||
onToggleSort: this.onToggleSort,
|
||||
onSeriesToggle: onSeriesToggle,
|
||||
});
|
||||
}}
|
||||
</GraphSeriesToggler>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
85
public/app/plugins/panel/graph2/GraphSeriesToggler.tsx
Normal file
85
public/app/plugins/panel/graph2/GraphSeriesToggler.tsx
Normal file
@@ -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<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 = (
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user