diff --git a/packages/grafana-data/src/types/flot.ts b/packages/grafana-data/src/types/flot.ts new file mode 100644 index 00000000000..d9b858c44c3 --- /dev/null +++ b/packages/grafana-data/src/types/flot.ts @@ -0,0 +1,8 @@ +export interface FlotDataPoint { + dataIndex: number; + datapoint: number[]; + pageX: number; + pageY: number; + series: any; + seriesIndex: number; +} diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index f6b7e29503b..33d9ca94a34 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -22,6 +22,7 @@ export * from './thresholds'; export * from './fieldColor'; export * from './theme'; export * from './orgs'; +export * from './flot'; import * as AppEvents from './appEvents'; import { AppEvent } from './appEvents'; diff --git a/packages/grafana-data/src/utils/flotPairs.ts b/packages/grafana-data/src/utils/flotPairs.ts index 0a4f6a8a41d..7f99dd11fdb 100644 --- a/packages/grafana-data/src/utils/flotPairs.ts +++ b/packages/grafana-data/src/utils/flotPairs.ts @@ -5,7 +5,6 @@ import { TimeRange } from '../types/time'; // Types // import { NullValueMode, GraphSeriesValue, Field, TimeRange } from '@grafana/data'; - export interface FlotPairsOptions { xField: Field; yField: Field; diff --git a/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx b/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx index 1452d47ed4c..252cbea10df 100644 --- a/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx +++ b/packages/grafana-ui/src/components/Chart/TooltipContainer.tsx @@ -22,6 +22,7 @@ const getTooltipContainerStyles = stylesFactory((theme: GrafanaTheme) => { max-width: 800px; padding: ${theme.spacing.sm}; border-radius: ${theme.border.radius.sm}; + z-index: ${theme.zIndex.tooltip}; `, }; }); diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index 6efd2322239..285cd9ee0ff 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -25,7 +25,7 @@ export interface ContextMenuProps { y: number; onClose: () => void; items?: ContextMenuGroup[]; - renderHeader?: () => JSX.Element; + renderHeader?: () => React.ReactNode; } const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => { @@ -181,10 +181,11 @@ export const ContextMenu: React.FC = React.memo(({ x, y, onClo }); const styles = getContextMenuStyles(theme); + const header = renderHeader && renderHeader(); return (
- {renderHeader &&
{renderHeader()}
} + {header &&
{header}
} { diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index 846c959c8a5..1558378c6f9 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -3,11 +3,20 @@ import $ from 'jquery'; import React, { PureComponent } from 'react'; import uniqBy from 'lodash/uniqBy'; // Types -import { TimeRange, GraphSeriesXY, TimeZone, DefaultTimeZone, createDimension } from '@grafana/data'; +import { + TimeRange, + GraphSeriesXY, + TimeZone, + DefaultTimeZone, + createDimension, + DateTimeInput, + dateTime, +} from '@grafana/data'; import _ from 'lodash'; import { FlotPosition, FlotItem } from './types'; import { TooltipProps, TooltipContentProps, ActiveDimensions, Tooltip } from '../Chart/Tooltip'; import { GraphTooltip } from './GraphTooltip/GraphTooltip'; +import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu'; import { GraphDimensions } from './GraphTooltip/types'; export interface GraphProps { @@ -27,8 +36,11 @@ export interface GraphProps { interface GraphState { pos?: FlotPosition; + contextPos?: FlotPosition; isTooltipVisible: boolean; + isContextVisible: boolean; activeItem?: FlotItem; + contextItem?: FlotItem; } export class Graph extends PureComponent { @@ -42,6 +54,7 @@ export class Graph extends PureComponent { state: GraphState = { isTooltipVisible: false, + isContextVisible: false, }; element: HTMLElement | null = null; @@ -59,6 +72,7 @@ export class Graph extends PureComponent { this.$element = $(this.element); this.$element.bind('plotselected', this.onPlotSelected); this.$element.bind('plothover', this.onPlotHover); + this.$element.bind('plotclick', this.onPlotClick); } } @@ -81,6 +95,15 @@ export class Graph extends PureComponent { }); }; + onPlotClick = (event: JQueryEventObject, contextPos: FlotPosition, item?: FlotItem) => { + this.setState({ + isContextVisible: true, + isTooltipVisible: false, + contextItem: item, + contextPos, + }); + }; + getYAxes(series: GraphSeriesXY[]) { if (series.length === 0) { return [{ show: true, min: -1, max: 1 }]; @@ -179,6 +202,68 @@ export class Graph extends PureComponent { }); }; + renderContextMenu = () => { + const { series } = this.props; + const { contextPos, contextItem, isContextVisible } = this.state; + + if (!isContextVisible || !contextPos || !contextItem || series.length === 0) { + return null; + } + + // Indicates column(field) index in y-axis dimension + const seriesIndex = contextItem ? contextItem.series.seriesIndex : 0; + // Indicates row index in context field values + const rowIndex = contextItem ? contextItem.dataIndex : undefined; + + const contextDimensions: ContextDimensions = { + // Described x-axis context item + xAxis: [seriesIndex, rowIndex], + // Describes y-axis context item + yAxis: contextItem ? [contextItem.series.seriesIndex, contextItem.dataIndex] : null, + }; + + const dimensions: GraphDimensions = { + // time/value dimension columns are index-aligned - see getGraphSeriesModel + xAxis: createDimension( + 'xAxis', + series.map(s => s.timeField) + ), + yAxis: createDimension( + 'yAxis', + series.map(s => s.valueField) + ), + }; + + const formatDate = (date: DateTimeInput, format?: string) => { + return dateTime(date)?.format(format); + }; + + const closeContext = () => this.setState({ isContextVisible: false }); + + const getContextMenuSource = () => { + return { + datapoint: contextItem.datapoint, + dataIndex: contextItem.dataIndex, + series: contextItem.series, + seriesIndex: contextItem.series.seriesIndex, + pageX: contextPos.pageX, + pageY: contextPos.pageY, + }; + }; + + const contextContentProps: GraphContextMenuProps = { + x: contextPos.pageX, + y: contextPos.pageY, + onClose: closeContext, + getContextMenuSource: getContextMenuSource, + formatSourceDate: formatDate, + dimensions, + contextDimensions, + }; + + return ; + }; + getBarWidth = () => { const { series } = this.props; return Math.min(...series.map(s => s.timeStep)); @@ -285,6 +370,8 @@ export class Graph extends PureComponent { render() { const { height, width, series } = this.props; const noDataToBeDisplayed = series.length === 0; + const tooltip = this.renderTooltip(); + const context = this.renderContextMenu(); return (
{ this.setState({ isTooltipVisible: false }); }} /> - {noDataToBeDisplayed &&
No data
} - {this.renderTooltip()} + {tooltip} + {context}
); } diff --git a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx new file mode 100644 index 00000000000..76df5d60785 --- /dev/null +++ b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx @@ -0,0 +1,99 @@ +import React, { useContext } from 'react'; +import { ContextMenu, ContextMenuProps } from '../ContextMenu/ContextMenu'; +import { ThemeContext } from '../../themes'; +import { SeriesIcon } from '../Legend/SeriesIcon'; +import { GraphDimensions } from './GraphTooltip/types'; +import { + DateTimeInput, + FlotDataPoint, + getValueFromDimension, + getDisplayProcessor, + formattedValueToString, + Dimensions, + MS_DATE_TIME_FORMAT, + DEFAULT_DATE_TIME_FORMAT, +} from '@grafana/data'; +import { css } from 'emotion'; + +export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; + +export type GraphContextMenuProps = ContextMenuProps & { + getContextMenuSource: () => FlotDataPoint | null; + formatSourceDate: (date: DateTimeInput, format?: string) => string; + dimensions?: GraphDimensions; + contextDimensions?: ContextDimensions; +}; + +export const GraphContextMenu: React.FC = ({ + getContextMenuSource, + formatSourceDate, + items, + dimensions, + contextDimensions, + ...otherProps +}) => { + const theme = useContext(ThemeContext); + const source = getContextMenuSource(); + + // Do not render items that do not have label specified + const itemsToRender = items + ? items.map(group => ({ + ...group, + items: group.items.filter(item => item.label), + })) + : []; + + const renderHeader = () => { + if (!source) { + return null; + } + + // If dimensions supplied, we can calculate and display value + let value; + if (dimensions?.yAxis && contextDimensions?.yAxis?.[1]) { + const valueFromDimensions = getValueFromDimension( + dimensions.yAxis, + contextDimensions.yAxis[0], + contextDimensions.yAxis[1] + ); + const display = source.series.valueField.display ?? getDisplayProcessor({ field: source.series.valueField }); + value = display(valueFromDimensions); + } + + const timeFormat = source.series.hasMsResolution ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT; + return ( +
+ {formatSourceDate(source.datapoint[0], timeFormat)} +
+ + + {source.series.alias || source.series.label} + + {value && ( + + {formattedValueToString(value)} + + )} +
+
+ ); + }; + + return ; +}; diff --git a/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx b/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx index efa6ec2dc94..b328be4ad6a 100644 --- a/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphTooltip/SeriesTable.tsx @@ -22,6 +22,7 @@ const getSeriesTableRowStyles = stylesFactory((theme: GrafanaTheme) => { `, seriesTableRow: css` display: table-row; + font-size: ${theme.typography.size.sm}; `, seriesTableCell: css` display: table-cell; @@ -35,6 +36,10 @@ const getSeriesTableRowStyles = stylesFactory((theme: GrafanaTheme) => { activeSeries: css` font-weight: ${theme.typography.weight.bold}; `, + timestamp: css` + font-weight: ${theme.typography.weight.bold}; + font-size: ${theme.typography.size.sm}; + `, }; }); @@ -60,9 +65,15 @@ interface SeriesTableProps { } export const SeriesTable: React.FC = ({ timestamp, series }) => { + const theme = useTheme(); + const styles = getSeriesTableRowStyles(theme); return ( <> - {timestamp &&
{timestamp}
} + {timestamp && ( +
+ {timestamp} +
+ )} {series.map(s => { return ; })} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 7140e883dd2..995fb5d3ce2 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -66,6 +66,7 @@ export { Gauge } from './Gauge/Gauge'; export { Graph } from './Graph/Graph'; export { GraphLegend } from './Graph/GraphLegend'; export { GraphWithLegend } from './Graph/GraphWithLegend'; +export { GraphContextMenu } from './Graph/GraphContextMenu'; export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge'; export { GraphTooltipOptions } from './Graph/GraphTooltip/types'; export { VizRepeater } from './VizRepeater/VizRepeater'; diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 36bf0cd9aeb..ef9514c9550 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -15,10 +15,10 @@ import { UnitPicker, DataLinksEditor, DataSourceHttpSettings, + GraphContextMenu, } from '@grafana/ui'; import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor'; import { SearchField } from './components/search/SearchField'; -import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu'; import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper'; import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor'; import { HelpModal } from './components/help/HelpModal'; diff --git a/public/app/plugins/panel/graph/GraphContextMenu.tsx b/public/app/plugins/panel/graph/GraphContextMenu.tsx deleted file mode 100644 index fd0e3ef50b9..00000000000 --- a/public/app/plugins/panel/graph/GraphContextMenu.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useContext } from 'react'; -import { FlotDataPoint } from './GraphContextMenuCtrl'; -import { ContextMenu, ContextMenuProps, SeriesIcon, ThemeContext } from '@grafana/ui'; -import { DateTimeInput } from '@grafana/data'; -import { css } from 'emotion'; - -type GraphContextMenuProps = ContextMenuProps & { - getContextMenuSource: () => FlotDataPoint | null; - formatSourceDate: (date: DateTimeInput, format?: string) => string; -}; - -export const GraphContextMenu: React.FC = ({ - getContextMenuSource, - formatSourceDate, - items, - ...otherProps -}) => { - const theme = useContext(ThemeContext); - const source = getContextMenuSource(); - - // Do not render items that do not have label specified - const itemsToRender = items - ? items.map(group => ({ - ...group, - items: group.items.filter(item => item.label), - })) - : []; - - const renderHeader = source - ? () => { - if (!source) { - return null; - } - - const timeFormat = source.series.hasMsResolution ? 'YYYY-MM-DD HH:mm:ss.SSS' : 'YYYY-MM-DD HH:mm:ss'; - - return ( -
- {formatSourceDate(source.datapoint[0], timeFormat)} -
- - - {source.series.alias} - -
-
- ); - } - : null; - - return ; -}; diff --git a/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts b/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts index 84f44aa1e57..519487f707c 100644 --- a/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts +++ b/public/app/plugins/panel/graph/GraphContextMenuCtrl.ts @@ -1,13 +1,5 @@ import { ContextMenuItem } from '@grafana/ui'; - -export interface FlotDataPoint { - dataIndex: number; - datapoint: number[]; - pageX: number; - pageY: number; - series: any; - seriesIndex: number; -} +import { FlotDataPoint } from '@grafana/data'; export class GraphContextMenuCtrl { private source?: FlotDataPoint | null;