mirror of
https://github.com/grafana/grafana.git
synced 2024-11-28 03:34:15 -06:00
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:
parent
fe11a31175
commit
2c3b35df64
@ -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 = {
|
||||
|
@ -85,6 +85,8 @@ const dummyProps: Props = {
|
||||
splitOpen: (() => {}) as any,
|
||||
logsVolumeData: undefined,
|
||||
loadLogsVolumeData: () => {},
|
||||
changeGraphStyle: () => {},
|
||||
graphStyle: 'lines',
|
||||
};
|
||||
|
||||
describe('Explore', () => {
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
31
public/app/features/explore/ExploreGraphLabel.tsx
Normal file
31
public/app/features/explore/ExploreGraphLabel.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -305,6 +305,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
|
||||
response.
|
||||
</div>
|
||||
<ExploreGraph
|
||||
graphStyle="lines"
|
||||
data={logsSeries}
|
||||
height={150}
|
||||
width={width}
|
||||
|
@ -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}
|
||||
|
56
public/app/features/explore/exploreGraphStyleUtils.ts
Normal file
56
public/app/features/explore/exploreGraphStyleUtils.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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 => ({
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user