mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Refactor ExploreGraph (#58660)
* WIP * revert collapse changes * use HorizontalGroup instead of custom styles * fix tests * use import aliases
This commit is contained in:
212
public/app/features/explore/Graph/ExploreGraph.tsx
Normal file
212
public/app/features/explore/Graph/ExploreGraph.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { identity } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useCounter } from 'react-use';
|
||||
|
||||
import {
|
||||
AbsoluteTimeRange,
|
||||
applyFieldOverrides,
|
||||
createFieldConfigRegistry,
|
||||
DataFrame,
|
||||
dateTime,
|
||||
FieldColorModeId,
|
||||
FieldConfigSource,
|
||||
getFrameDisplayName,
|
||||
GrafanaTheme2,
|
||||
LoadingState,
|
||||
SplitOpen,
|
||||
TimeZone,
|
||||
DashboardCursorSync,
|
||||
EventBus,
|
||||
} from '@grafana/data';
|
||||
import { PanelRenderer } from '@grafana/runtime';
|
||||
import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema';
|
||||
import {
|
||||
Icon,
|
||||
PanelContext,
|
||||
PanelContextProvider,
|
||||
SeriesVisibilityChangeMode,
|
||||
useStyles2,
|
||||
useTheme2,
|
||||
} from '@grafana/ui';
|
||||
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
|
||||
import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types';
|
||||
import { ExploreGraphStyle } from 'app/types';
|
||||
|
||||
import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory';
|
||||
|
||||
import { applyGraphStyle } from './exploreGraphStyleUtils';
|
||||
|
||||
const MAX_NUMBER_OF_TIME_SERIES = 20;
|
||||
|
||||
interface Props {
|
||||
data: DataFrame[];
|
||||
height: number;
|
||||
width: number;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
timeZone: TimeZone;
|
||||
loadingState: LoadingState;
|
||||
annotations?: DataFrame[];
|
||||
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
|
||||
tooltipDisplayMode?: TooltipDisplayMode;
|
||||
splitOpenFn: SplitOpen;
|
||||
onChangeTime: (timeRange: AbsoluteTimeRange) => void;
|
||||
graphStyle: ExploreGraphStyle;
|
||||
anchorToZero?: boolean;
|
||||
eventBus: EventBus;
|
||||
}
|
||||
|
||||
export function ExploreGraph({
|
||||
data,
|
||||
height,
|
||||
width,
|
||||
timeZone,
|
||||
absoluteRange,
|
||||
onChangeTime,
|
||||
loadingState,
|
||||
annotations,
|
||||
onHiddenSeriesChanged,
|
||||
splitOpenFn,
|
||||
graphStyle,
|
||||
tooltipDisplayMode = TooltipDisplayMode.Single,
|
||||
anchorToZero = false,
|
||||
eventBus,
|
||||
}: Props) {
|
||||
const theme = useTheme2();
|
||||
const style = useStyles2(getStyles);
|
||||
const [showAllTimeSeries, setShowAllTimeSeries] = useState(false);
|
||||
const [structureRev, { inc }] = useCounter(0);
|
||||
|
||||
const fieldConfigRegistry = useMemo(
|
||||
() => createFieldConfigRegistry(getGraphFieldConfig(defaultGraphConfig), 'Explore'),
|
||||
[]
|
||||
);
|
||||
|
||||
const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>({
|
||||
defaults: {
|
||||
min: anchorToZero ? 0 : undefined,
|
||||
color: {
|
||||
mode: FieldColorModeId.PaletteClassic,
|
||||
},
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
fillOpacity: 0,
|
||||
pointSize: 5,
|
||||
},
|
||||
},
|
||||
overrides: [],
|
||||
});
|
||||
|
||||
const timeRange = {
|
||||
from: dateTime(absoluteRange.from),
|
||||
to: dateTime(absoluteRange.to),
|
||||
raw: {
|
||||
from: dateTime(absoluteRange.from),
|
||||
to: dateTime(absoluteRange.to),
|
||||
},
|
||||
};
|
||||
|
||||
const styledFieldConfig = useMemo(() => applyGraphStyle(fieldConfig, graphStyle), [fieldConfig, graphStyle]);
|
||||
|
||||
const dataWithConfig = useMemo(() => {
|
||||
return applyFieldOverrides({
|
||||
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,
|
||||
});
|
||||
}, [fieldConfigRegistry, data, timeZone, theme, styledFieldConfig]);
|
||||
|
||||
// We need to increment structureRev when the number of series changes.
|
||||
// the function passed to useMemo runs during rendering, so when we get a different
|
||||
// amount of data, structureRev is incremented before we render it
|
||||
useMemo(inc, [dataWithConfig.length, styledFieldConfig, inc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onHiddenSeriesChanged) {
|
||||
const hiddenFrames: string[] = [];
|
||||
dataWithConfig.forEach((frame) => {
|
||||
const allFieldsHidden = frame.fields.map((field) => field.config?.custom?.hideFrom?.viz).every(identity);
|
||||
if (allFieldsHidden) {
|
||||
hiddenFrames.push(getFrameDisplayName(frame));
|
||||
}
|
||||
});
|
||||
onHiddenSeriesChanged(hiddenFrames);
|
||||
}
|
||||
}, [dataWithConfig, onHiddenSeriesChanged]);
|
||||
|
||||
const seriesToShow = showAllTimeSeries ? dataWithConfig : dataWithConfig.slice(0, MAX_NUMBER_OF_TIME_SERIES);
|
||||
|
||||
const panelContext: PanelContext = {
|
||||
eventBus,
|
||||
sync: () => DashboardCursorSync.Crosshair,
|
||||
onSplitOpen: splitOpenFn,
|
||||
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
|
||||
setFieldConfig(seriesVisibilityConfigFactory(label, mode, fieldConfig, data));
|
||||
},
|
||||
};
|
||||
|
||||
const panelOptions: TimeSeriesOptions = useMemo(
|
||||
() => ({
|
||||
tooltip: { mode: tooltipDisplayMode, sort: SortOrder.None },
|
||||
legend: {
|
||||
displayMode: LegendDisplayMode.List,
|
||||
showLegend: true,
|
||||
placement: 'bottom',
|
||||
calcs: [],
|
||||
},
|
||||
}),
|
||||
[tooltipDisplayMode]
|
||||
);
|
||||
|
||||
return (
|
||||
<PanelContextProvider value={panelContext}>
|
||||
{dataWithConfig.length > MAX_NUMBER_OF_TIME_SERIES && !showAllTimeSeries && (
|
||||
<div className={cx([style.timeSeriesDisclaimer])}>
|
||||
<Icon className={style.disclaimerIcon} name="exclamation-triangle" />
|
||||
{`Showing only ${MAX_NUMBER_OF_TIME_SERIES} time series. `}
|
||||
<span
|
||||
className={cx([style.showAllTimeSeries])}
|
||||
onClick={() => {
|
||||
setShowAllTimeSeries(true);
|
||||
}}
|
||||
>{`Show all ${dataWithConfig.length}`}</span>
|
||||
</div>
|
||||
)}
|
||||
<PanelRenderer
|
||||
data={{ series: seriesToShow, timeRange, state: loadingState, annotations, structureRev }}
|
||||
pluginId="timeseries"
|
||||
title=""
|
||||
width={width}
|
||||
height={height}
|
||||
onChangeTimeRange={onChangeTime}
|
||||
timeZone={timeZone}
|
||||
options={panelOptions}
|
||||
/>
|
||||
</PanelContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
timeSeriesDisclaimer: css`
|
||||
label: time-series-disclaimer;
|
||||
width: 300px;
|
||||
margin: ${theme.spacing(1)} auto;
|
||||
padding: 10px 0;
|
||||
border-radius: ${theme.spacing(2)};
|
||||
text-align: center;
|
||||
background-color: ${theme.colors.background.primary};
|
||||
`,
|
||||
disclaimerIcon: css`
|
||||
label: disclaimer-icon;
|
||||
color: ${theme.colors.warning.main};
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
showAllTimeSeries: css`
|
||||
label: show-all-time-series;
|
||||
cursor: pointer;
|
||||
color: ${theme.colors.text.link};
|
||||
`,
|
||||
});
|
||||
26
public/app/features/explore/Graph/ExploreGraphLabel.tsx
Normal file
26
public/app/features/explore/Graph/ExploreGraphLabel.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { RadioButtonGroup, HorizontalGroup } from '@grafana/ui';
|
||||
import { EXPLORE_GRAPH_STYLES, ExploreGraphStyle } from 'app/types';
|
||||
|
||||
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(/_/, ' '),
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
graphStyle: ExploreGraphStyle;
|
||||
onChangeGraphStyle: (style: ExploreGraphStyle) => void;
|
||||
};
|
||||
|
||||
export function ExploreGraphLabel(props: Props) {
|
||||
const { graphStyle, onChangeGraphStyle } = props;
|
||||
return (
|
||||
<HorizontalGroup justify="space-between" wrap>
|
||||
Graph
|
||||
<RadioButtonGroup size="sm" options={ALL_GRAPH_STYLE_OPTIONS} value={graphStyle} onChange={onChangeGraphStyle} />
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
70
public/app/features/explore/Graph/GraphContainer.tsx
Normal file
70
public/app/features/explore/Graph/GraphContainer.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { DataFrame, EventBus, AbsoluteTimeRange, TimeZone, SplitOpen, LoadingState } from '@grafana/data';
|
||||
import { Collapse, useTheme2 } from '@grafana/ui';
|
||||
import { ExploreGraphStyle } from 'app/types';
|
||||
|
||||
import { storeGraphStyle } from '../state/utils';
|
||||
|
||||
import { ExploreGraph } from './ExploreGraph';
|
||||
import { ExploreGraphLabel } from './ExploreGraphLabel';
|
||||
import { loadGraphStyle } from './utils';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
data: DataFrame[];
|
||||
annotations?: DataFrame[];
|
||||
eventBus: EventBus;
|
||||
height: number;
|
||||
width: number;
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
timeZone: TimeZone;
|
||||
onChangeTime: (absoluteRange: AbsoluteTimeRange) => void;
|
||||
splitOpenFn: SplitOpen;
|
||||
loadingState: LoadingState;
|
||||
}
|
||||
|
||||
export const GraphContainer = ({
|
||||
loading,
|
||||
data,
|
||||
eventBus,
|
||||
height,
|
||||
width,
|
||||
absoluteRange,
|
||||
timeZone,
|
||||
annotations,
|
||||
onChangeTime,
|
||||
splitOpenFn,
|
||||
loadingState,
|
||||
}: Props) => {
|
||||
const [graphStyle, setGraphStyle] = useState(loadGraphStyle);
|
||||
const theme = useTheme2();
|
||||
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10);
|
||||
|
||||
const onGraphStyleChange = useCallback((graphStyle: ExploreGraphStyle) => {
|
||||
storeGraphStyle(graphStyle);
|
||||
setGraphStyle(graphStyle);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
label={<ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={onGraphStyleChange} />}
|
||||
loading={loading}
|
||||
isOpen
|
||||
>
|
||||
<ExploreGraph
|
||||
graphStyle={graphStyle}
|
||||
data={data}
|
||||
height={height}
|
||||
width={width - spacing}
|
||||
absoluteRange={absoluteRange}
|
||||
onChangeTime={onChangeTime}
|
||||
timeZone={timeZone}
|
||||
annotations={annotations}
|
||||
splitOpenFn={splitOpenFn}
|
||||
loadingState={loadingState}
|
||||
eventBus={eventBus}
|
||||
/>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
57
public/app/features/explore/Graph/exploreGraphStyleUtils.ts
Normal file
57
public/app/features/explore/Graph/exploreGraphStyleUtils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import produce from 'immer';
|
||||
|
||||
import { FieldConfigSource } from '@grafana/data';
|
||||
import { GraphDrawStyle, GraphFieldConfig, StackingMode } from '@grafana/schema';
|
||||
import { ExploreGraphStyle } from 'app/types';
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
26
public/app/features/explore/Graph/utils.ts
Normal file
26
public/app/features/explore/Graph/utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import store from 'app/core/store';
|
||||
import { ExploreGraphStyle, EXPLORE_GRAPH_STYLES } from 'app/types';
|
||||
|
||||
const GRAPH_STYLE_KEY = 'grafana.explore.style.graph';
|
||||
export const storeGraphStyle = (graphStyle: string): void => {
|
||||
store.set(GRAPH_STYLE_KEY, graphStyle);
|
||||
};
|
||||
|
||||
export const loadGraphStyle = (): ExploreGraphStyle => {
|
||||
return toGraphStyle(store.get(GRAPH_STYLE_KEY));
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user