Explore: allow changing the graph type (#40522)

* explore: allow switching graph-styles

* refactor: simplify code

* adjust test to test a case that can really happen

* better generate-options approach

* explore: graph styles: remove url functionality

* not-stacked-bars should be filled
This commit is contained in:
Gábor Farkas 2021-10-26 15:51:59 +02:00 committed by GitHub
parent fe11a31175
commit 2c3b35df64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 174 additions and 6 deletions

View File

@ -201,6 +201,25 @@ export const safeStringifyValue = (value: any, space?: number) => {
return '';
};
export const EXPLORE_GRAPH_STYLES = ['lines', 'bars', 'points', 'stacked_lines', 'stacked_bars'] as const;
export type ExploreGraphStyle = typeof EXPLORE_GRAPH_STYLES[number];
const DEFAULT_GRAPH_STYLE: ExploreGraphStyle = 'lines';
// we use this function to take any kind of data we loaded
// from an external source (URL, localStorage, whatever),
// and extract the graph-style from it, or return the default
// graph-style if we are not able to do that.
// it is important that this function is able to take any form of data,
// (be it objects, or arrays, or booleans or whatever),
// and produce a best-effort graphStyle.
// note that typescript makes sure we make no mistake in this function.
// we do not rely on ` as ` or ` any `.
export const toGraphStyle = (data: unknown): ExploreGraphStyle => {
const found = EXPLORE_GRAPH_STYLES.find((v) => v === data);
return found ?? DEFAULT_GRAPH_STYLE;
};
export function parseUrlState(initial: string | undefined): ExploreUrlState {
const parsed = safeParseJson(initial);
const errorResult: any = {

View File

@ -85,6 +85,8 @@ const dummyProps: Props = {
splitOpen: (() => {}) as any,
logsVolumeData: undefined,
loadLogsVolumeData: () => {},
changeGraphStyle: () => {},
graphStyle: 'lines',
};
describe('Explore', () => {

View File

@ -14,7 +14,7 @@ import TableContainer from './TableContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector';
import { splitOpen } from './state/main';
import { changeSize } from './state/explorePane';
import { changeSize, changeGraphStyle } from './state/explorePane';
import { updateTimeRange } from './state/time';
import { addQueryRow, loadLogsVolumeData, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query';
import { ExploreId, ExploreItemState } from 'app/types/explore';
@ -29,6 +29,8 @@ import { ResponseErrorContainer } from './ResponseErrorContainer';
import { TraceViewContainer } from './TraceView/TraceViewContainer';
import { ExploreGraph } from './ExploreGraph';
import { LogsVolumePanel } from './LogsVolumePanel';
import { ExploreGraphLabel } from './ExploreGraphLabel';
import { ExploreGraphStyle } from 'app/core/utils/explore';
const getStyles = (theme: GrafanaTheme2) => {
return {
@ -162,6 +164,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
updateTimeRange({ exploreId, absoluteRange });
};
onChangeGraphStyle = (graphStyle: ExploreGraphStyle) => {
const { exploreId, changeGraphStyle } = this.props;
changeGraphStyle(exploreId, graphStyle);
};
toggleShowRichHistory = () => {
this.setState((state) => {
return {
@ -187,11 +194,13 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
}
renderGraphPanel(width: number) {
const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading, theme } = this.props;
const { graphResult, absoluteRange, timeZone, splitOpen, queryResponse, loading, theme, graphStyle } = this.props;
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
const label = <ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={this.onChangeGraphStyle} />;
return (
<Collapse label="Graph" loading={loading} isOpen>
<Collapse label={label} loading={loading} isOpen>
<ExploreGraph
graphStyle={graphStyle}
data={graphResult!}
height={400}
width={width - spacing}
@ -390,6 +399,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
queryResponse,
showNodeGraph,
loading,
graphStyle,
} = item;
return {
@ -410,11 +420,13 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
showTrace,
showNodeGraph,
loading,
graphStyle,
};
}
const mapDispatchToProps = {
changeSize,
changeGraphStyle,
modifyQueries,
scanStart,
scanStopAction,

View File

@ -26,12 +26,14 @@ import {
useTheme2,
} from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { ExploreGraphStyle } from 'app/core/utils/explore';
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types';
import { identity } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { usePrevious } from 'react-use';
import { seriesVisibilityConfigFactory } from '../dashboard/dashgrid/SeriesVisibilityConfigFactory';
import { applyGraphStyle } from './exploreGraphStyleUtils';
const MAX_NUMBER_OF_TIME_SERIES = 20;
@ -47,6 +49,7 @@ interface Props {
tooltipDisplayMode?: TooltipDisplayMode;
splitOpenFn?: SplitOpen;
onChangeTime: (timeRange: AbsoluteTimeRange) => void;
graphStyle: ExploreGraphStyle;
}
export function ExploreGraph({
@ -60,6 +63,7 @@ export function ExploreGraph({
annotations,
onHiddenSeriesChanged,
splitOpenFn,
graphStyle,
tooltipDisplayMode = TooltipDisplayMode.Single,
}: Props) {
const theme = useTheme2();
@ -101,15 +105,16 @@ export function ExploreGraph({
const dataWithConfig = useMemo(() => {
const registry = createFieldConfigRegistry(getGraphFieldConfig(defaultGraphConfig), 'Explore');
const styledFieldConfig = applyGraphStyle(fieldConfig, graphStyle);
return applyFieldOverrides({
fieldConfig,
fieldConfig: styledFieldConfig,
data,
timeZone,
replaceVariables: (value) => value, // We don't need proper replace here as it is only used in getLinks and we use getFieldLinks
theme,
fieldConfigRegistry: registry,
});
}, [fieldConfig, data, timeZone, theme]);
}, [fieldConfig, graphStyle, data, timeZone, theme]);
useEffect(() => {
if (onHiddenSeriesChanged) {

View File

@ -0,0 +1,31 @@
import React from 'react';
import { css } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { RadioButtonGroup } from '@grafana/ui';
import { ExploreGraphStyle, EXPLORE_GRAPH_STYLES } from 'app/core/utils/explore';
const ALL_GRAPH_STYLE_OPTIONS: Array<SelectableValue<ExploreGraphStyle>> = EXPLORE_GRAPH_STYLES.map((style) => ({
value: style,
// capital-case it and switch `_` to ` `
label: style[0].toUpperCase() + style.slice(1).replace(/_/, ' '),
}));
const spacing = css({
display: 'flex',
justifyContent: 'space-between',
});
type Props = {
graphStyle: ExploreGraphStyle;
onChangeGraphStyle: (style: ExploreGraphStyle) => void;
};
export function ExploreGraphLabel(props: Props) {
const { graphStyle, onChangeGraphStyle } = props;
return (
<div className={spacing}>
Graph
<RadioButtonGroup size="sm" options={ALL_GRAPH_STYLE_OPTIONS} value={graphStyle} onChange={onChangeGraphStyle} />
</div>
);
}

View File

@ -305,6 +305,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
response.
</div>
<ExploreGraph
graphStyle="lines"
data={logsSeries}
height={150}
width={width}

View File

@ -37,6 +37,7 @@ export function LogsVolumePanel(props: Props) {
if (logsVolumeData.data.length > 0) {
LogsVolumePanelContent = (
<ExploreGraph
graphStyle="lines"
loadingState={LoadingState.Done}
data={logsVolumeData.data}
height={height}

View File

@ -0,0 +1,56 @@
import produce from 'immer';
import { FieldConfigSource } from '@grafana/data';
import { GraphDrawStyle, GraphFieldConfig, StackingMode } from '@grafana/schema';
import { ExploreGraphStyle } from 'app/core/utils/explore';
export type FieldConfig = FieldConfigSource<GraphFieldConfig>;
export function applyGraphStyle(config: FieldConfig, style: ExploreGraphStyle): FieldConfig {
return produce(config, (draft) => {
if (draft.defaults.custom === undefined) {
draft.defaults.custom = {};
}
const { custom } = draft.defaults;
if (custom.stacking === undefined) {
custom.stacking = { group: 'A' };
}
switch (style) {
case 'lines':
custom.drawStyle = GraphDrawStyle.Line;
custom.stacking.mode = StackingMode.None;
custom.fillOpacity = 0;
break;
case 'bars':
custom.drawStyle = GraphDrawStyle.Bars;
custom.stacking.mode = StackingMode.None;
custom.fillOpacity = 100;
break;
case 'points':
custom.drawStyle = GraphDrawStyle.Points;
custom.stacking.mode = StackingMode.None;
custom.fillOpacity = 0;
break;
case 'stacked_lines':
custom.drawStyle = GraphDrawStyle.Line;
custom.stacking.mode = StackingMode.Normal;
custom.fillOpacity = 100;
break;
case 'stacked_bars':
custom.drawStyle = GraphDrawStyle.Bars;
custom.stacking.mode = StackingMode.Normal;
custom.fillOpacity = 100;
break;
default: {
// should never happen
// NOTE: casting to `never` will cause typescript
// to verify that the switch statement checks every possible
// enum-value
const invalidValue: never = style;
throw new Error(`Invalid graph-style: ${invalidValue}`);
}
}
});
}

View File

@ -8,6 +8,7 @@ import {
ensureQueries,
generateNewKeyAndAddRefIdIfMissing,
getTimeRangeFromUrl,
ExploreGraphStyle,
} from 'app/core/utils/explore';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { queryReducer, runQueries, setQueriesAction } from './query';
@ -19,6 +20,7 @@ import {
loadAndInitDatasource,
createEmptyQueryResponse,
getUrlStateFromPaneState,
storeGraphStyle,
} from './utils';
import { createAction, PayloadAction } from '@reduxjs/toolkit';
import { EventBusExtended, DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi } from '@grafana/data';
@ -76,6 +78,20 @@ export function changeSize(
return changeSizeAction({ exploreId, height, width });
}
interface ChangeGraphStylePayload {
exploreId: ExploreId;
graphStyle: ExploreGraphStyle;
}
const changeGraphStyleAction = createAction<ChangeGraphStylePayload>('explore/changeGraphStyle');
export function changeGraphStyle(exploreId: ExploreId, graphStyle: ExploreGraphStyle): ThunkResult<void> {
return async (dispatch, getState) => {
storeGraphStyle(graphStyle);
dispatch(changeGraphStyleAction({ exploreId, graphStyle }));
};
}
/**
* Initialize Explore state with state from the URL and the React component.
* Call this only on components for with the Explore state has not been initialized.
@ -200,6 +216,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
return { ...state, containerWidth };
}
if (changeGraphStyleAction.match(action)) {
const { graphStyle } = action.payload;
return { ...state, graphStyle };
}
if (initializeExploreAction.match(action)) {
const { containerWidth, eventBridge, queries, range, originPanelId, datasourceInstance, history } = action.payload;

View File

@ -12,7 +12,12 @@ import {
import { ExploreItemState } from 'app/types/explore';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import store from '../../../core/store';
import { clearQueryKeys, lastUsedDatasourceKeyForOrgId } from '../../../core/utils/explore';
import {
clearQueryKeys,
ExploreGraphStyle,
lastUsedDatasourceKeyForOrgId,
toGraphStyle,
} from '../../../core/utils/explore';
import { toRawTimeRange } from '../utils/time';
export const DEFAULT_RANGE = {
@ -20,6 +25,16 @@ export const DEFAULT_RANGE = {
to: 'now',
};
const GRAPH_STYLE_KEY = 'grafana.explore.style.graph';
export const storeGraphStyle = (graphStyle: string): void => {
store.set(GRAPH_STYLE_KEY, graphStyle);
};
const loadGraphStyle = (): ExploreGraphStyle => {
const data = store.get(GRAPH_STYLE_KEY);
return toGraphStyle(data);
};
/**
* Returns a fresh Explore area state
*/
@ -52,6 +67,7 @@ export const makeExplorePaneState = (): ExploreItemState => ({
cache: [],
logsVolumeDataProvider: undefined,
logsVolumeData: undefined,
graphStyle: loadGraphStyle(),
});
export const createEmptyQueryResponse = (): PanelData => ({

View File

@ -14,6 +14,7 @@ import {
EventBusExtended,
DataQueryResponse,
} from '@grafana/data';
import { ExploreGraphStyle } from 'app/core/utils/explore';
export enum ExploreId {
left = 'left',
@ -167,6 +168,9 @@ export interface ExploreItemState {
logsVolumeDataProvider?: Observable<DataQueryResponse>;
logsVolumeDataSubscription?: SubscriptionLike;
logsVolumeData?: DataQueryResponse;
/* explore graph style */
graphStyle: ExploreGraphStyle;
}
export interface ExploreUpdateState {