From 2c3b35df646a714019bc95191fcd0ea6e9fd0df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Tue, 26 Oct 2021 15:51:59 +0200 Subject: [PATCH] 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 --- public/app/core/utils/explore.ts | 19 +++++++ public/app/features/explore/Explore.test.tsx | 2 + public/app/features/explore/Explore.tsx | 18 +++++- public/app/features/explore/ExploreGraph.tsx | 9 ++- .../features/explore/ExploreGraphLabel.tsx | 31 ++++++++++ public/app/features/explore/Logs.tsx | 1 + .../app/features/explore/LogsVolumePanel.tsx | 1 + .../explore/exploreGraphStyleUtils.ts | 56 +++++++++++++++++++ .../app/features/explore/state/explorePane.ts | 21 +++++++ public/app/features/explore/state/utils.ts | 18 +++++- public/app/types/explore.ts | 4 ++ 11 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 public/app/features/explore/ExploreGraphLabel.tsx create mode 100644 public/app/features/explore/exploreGraphStyleUtils.ts diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index ce4e1d59318..11f8c522f18 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -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 = { diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 173b4fba7fd..b243e5f1739 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -85,6 +85,8 @@ const dummyProps: Props = { splitOpen: (() => {}) as any, logsVolumeData: undefined, loadLogsVolumeData: () => {}, + changeGraphStyle: () => {}, + graphStyle: 'lines', }; describe('Explore', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 72bbb6d628a..ff1ee759646 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { 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 { } 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 = ; return ( - + 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) { diff --git a/public/app/features/explore/ExploreGraphLabel.tsx b/public/app/features/explore/ExploreGraphLabel.tsx new file mode 100644 index 00000000000..b3378d2e1af --- /dev/null +++ b/public/app/features/explore/ExploreGraphLabel.tsx @@ -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> = 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 ( +
+ Graph + +
+ ); +} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index a17ca23d537..8b8d804fb6c 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -305,6 +305,7 @@ export class UnthemedLogs extends PureComponent { response. 0) { LogsVolumePanelContent = ( ; + +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}`); + } + } + }); +} diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index d1da8319c0d..160f5e5e949 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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('explore/changeGraphStyle'); + +export function changeGraphStyle(exploreId: ExploreId, graphStyle: ExploreGraphStyle): ThunkResult { + 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; diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index f9b0aab0ae2..5fa76e41950 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -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 => ({ diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 0544a8d0255..0414bec869a 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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; logsVolumeDataSubscription?: SubscriptionLike; logsVolumeData?: DataQueryResponse; + + /* explore graph style */ + graphStyle: ExploreGraphStyle; } export interface ExploreUpdateState {