grafana/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx

229 lines
8.2 KiB
TypeScript

import { useMemo, useState } from 'react';
import {
PanelProps,
DataFrameType,
DashboardCursorSync,
DataFrame,
alignTimeRangeCompareData,
shouldAlignTimeCompare,
useDataLinksContext,
FieldType,
} from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { TooltipDisplayMode, VizOrientation } from '@grafana/schema';
import { EventBusPlugin, KeyboardPlugin, TooltipPlugin2, usePanelContext } from '@grafana/ui';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/internal';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
import { TimeSeriesTooltip } from './TimeSeriesTooltip';
import { Options } from './panelcfg.gen';
import { AnnotationsPlugin2 } from './plugins/AnnotationsPlugin2';
import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin';
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
import { getAnnotationFrames } from './plugins/utils';
import { getPrepareTimeseriesSuggestion } from './suggestions';
import { getTimezones, prepareGraphableFields } from './utils';
interface TimeSeriesPanelProps extends PanelProps<Options> {}
export const TimeSeriesPanel = ({
data,
timeRange,
timeZone,
width,
height,
options,
fieldConfig,
onChangeTimeRange,
replaceVariables,
id,
}: TimeSeriesPanelProps) => {
const {
sync,
eventsScope,
canAddAnnotations,
onThresholdsChange,
canEditThresholds,
showThresholds,
eventBus,
canExecuteActions,
} = usePanelContext();
const { dataLinkPostProcessor } = useDataLinksContext();
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
// Vertical orientation is not available for users through config.
// It is simplified version of horizontal time series panel and it does not support all plugins.
const isVerticallyOriented = options.orientation === VizOrientation.Vertical;
const { frames, compareDiffMs } = useMemo(() => {
let frames = prepareGraphableFields(data.series, config.theme2, timeRange);
if (frames != null) {
let compareDiffMs: number[] = [0];
frames.forEach((frame: DataFrame) => {
const diffMs = frame.meta?.timeCompare?.diffMs ?? 0;
frame.fields.forEach((field) => {
if (field.type !== FieldType.time) {
compareDiffMs.push(diffMs);
}
});
if (diffMs !== 0) {
// Check if the compared frame needs time alignment
// Apply alignment when time ranges match (no shift applied yet)
const needsAlignment = shouldAlignTimeCompare(frame, frames, timeRange);
if (needsAlignment) {
alignTimeRangeCompareData(frame, diffMs, config.theme2);
}
}
});
return { frames, compareDiffMs };
}
return { frames };
}, [data.series, timeRange]);
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
const suggestions = useMemo(() => {
if (frames?.length && frames.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) {
const s = getPrepareTimeseriesSuggestion(id);
return {
message: 'Long data must be converted to wide',
suggestions: s ? [s] : undefined,
};
}
return undefined;
}, [frames, id]);
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
const cursorSync = sync?.() ?? DashboardCursorSync.Off;
if (!frames || suggestions) {
return (
<PanelDataErrorView
panelId={id}
message={suggestions?.message}
fieldConfig={fieldConfig}
data={data}
needsTimeField={true}
needsNumberField={true}
suggestions={suggestions?.suggestions}
/>
);
}
return (
<TimeSeries
frames={frames}
structureRev={data.structureRev}
timeRange={timeRange}
timeZone={timezones}
width={width}
height={height}
legend={options.legend}
options={options}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
cursorSync={cursorSync}
annotationLanes={getAnnotationFrames(data.annotations).length}
>
{(uplotConfig, alignedFrame) => {
return (
<>
<KeyboardPlugin config={uplotConfig} />
{cursorSync !== DashboardCursorSync.Off && (
<EventBusPlugin config={uplotConfig} eventBus={eventBus} frame={alignedFrame} />
)}
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={uplotConfig}
hoverMode={
options.tooltip.mode === TooltipDisplayMode.Single ? TooltipHoverMode.xOne : TooltipHoverMode.xAll
}
queryZoom={onChangeTimeRange}
clientZoom={true}
syncMode={cursorSync}
syncScope={eventsScope}
getDataLinks={(seriesIdx, dataIdx) =>
alignedFrame.fields[seriesIdx].getLinks?.({ valueRowIndex: dataIdx }) ?? []
}
render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync, dataLinks) => {
if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
// not sure it header time here works for annotations, since it's taken from nearest datapoint index
<TimeSeriesTooltip
series={alignedFrame}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
sortOrder={options.tooltip.sort}
hideZeros={options.tooltip.hideZeros}
isPinned={isPinned}
annotate={enableAnnotationCreation ? annotate : undefined}
maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables}
dataLinks={dataLinks}
canExecuteActions={userCanExecuteActions}
compareDiffMs={compareDiffMs}
/>
);
}}
maxWidth={options.tooltip.maxWidth}
/>
)}
{!isVerticallyOriented && (
<>
<AnnotationsPlugin2
annotationsConfig={options.annotations}
annotations={data.annotations ?? []}
config={uplotConfig}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
/>
<OutsideRangePlugin config={uplotConfig} onChangeTimeRange={onChangeTimeRange} />
{data.annotations && (
<ExemplarsPlugin
visibleSeries={getVisibleLabels(uplotConfig, frames)}
config={uplotConfig}
exemplars={data.annotations}
timeZone={timeZone}
maxHeight={options.tooltip.maxHeight}
maxWidth={options.tooltip.maxWidth}
/>
)}
{((canEditThresholds && onThresholdsChange) || showThresholds) && (
<ThresholdControlsPlugin
config={uplotConfig}
fieldConfig={fieldConfig}
onThresholdsChange={canEditThresholds ? onThresholdsChange : undefined}
/>
)}
</>
)}
</>
);
}}
</TimeSeries>
);
};