1import React, { useCallback, useMemo } from 'react';
2import { DataFrame, FieldType, PanelProps } from '@grafana/data';
3import { TooltipPlugin, useTheme2, ZoomPlugin, usePanelContext } from '@grafana/ui';
4import { TimelineMode, TimelineOptions } from './types';
5import { TimelineChart } from './TimelineChart';
6import { prepareTimelineFields, prepareTimelineLegendItems } from './utils';
7import { StateTimelineTooltip } from './StateTimelineTooltip';
8import { getLastStreamingDataFramePacket } from '@grafana/data/src/dataframe/StreamingDataFrame';
9
10interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
11
12/**
13 * @alpha
14 */
15export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
16  data,
17  timeRange,
18  timeZone,
19  options,
20  width,
21  height,
22  onChangeTimeRange,
23}) => {
24  const theme = useTheme2();
25  const { sync } = usePanelContext();
26
27  const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, options.mergeValues ?? true, theme), [
28    data,
29    options.mergeValues,
30    theme,
31  ]);
32
33  const legendItems = useMemo(() => prepareTimelineLegendItems(frames, options.legend, theme), [
34    frames,
35    options.legend,
36    theme,
37  ]);
38
39  const renderCustomTooltip = useCallback(
40    (alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => {
41      const data = frames ?? [];
42      // Count value fields in the state-timeline-ready frame
43      const valueFieldsCount = data.reduce(
44        (acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length,
45        0
46      );
47
48      // Not caring about multi mode in StateTimeline
49      if (seriesIdx === null || datapointIdx === null) {
50        return null;
51      }
52
53      /**
54       * There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first
55       * from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count.
56       * Render nothing in this case to prevent error.
57       * See https://github.com/grafana/support-escalations/issues/932
58       */
59      if (
60        (!alignedData.meta?.transformations?.length && alignedData.fields.length - 1 !== valueFieldsCount) ||
61        !alignedData.fields[seriesIdx]
62      ) {
63        return null;
64      }
65
66      return (
67        <StateTimelineTooltip
68          data={data}
69          alignedData={alignedData}
70          seriesIdx={seriesIdx}
71          datapointIdx={datapointIdx}
72          timeZone={timeZone}
73        />
74      );
75    },
76    [timeZone, frames]
77  );
78
79  if (!frames || warn) {
80    return (
81      <div className="panel-empty">
82        <p>{warn ?? 'No data found in response'}</p>
83      </div>
84    );
85  }
86
87  if (frames.length === 1) {
88    const packet = getLastStreamingDataFramePacket(frames[0]);
89    if (packet) {
90      // console.log('STREAM Packet', packet);
91    }
92  }
93
94  return (
95    <TimelineChart
96      theme={theme}
97      frames={frames}
98      structureRev={data.structureRev}
99      timeRange={timeRange}
100      timeZone={timeZone}
101      width={width}
102      height={height}
103      legendItems={legendItems}
104      {...options}
105      mode={TimelineMode.Changes}
106    >
107      {(config, alignedFrame) => {
108        return (
109          <>
110            <ZoomPlugin config={config} onZoom={onChangeTimeRange} />
111            <TooltipPlugin
112              data={alignedFrame}
113              sync={sync}
114              config={config}
115              mode={options.tooltip.mode}
116              timeZone={timeZone}
117              renderTooltip={renderCustomTooltip}
118            />
119          </>
120        );
121      }}
122    </TimelineChart>
123  );
124};
125