mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DataLinks: enable data links in Gauge, BarGauge and SingleStat2 panel (#18605)
* datalink on field * add dataFrame to view * Use scoped variables to pass series name and value time to data links interpolation * Use scoped variables to pass series name and value time to data links interpolation * Enable value specific variable suggestions when Gauge is displaying values * Fix prettier * Add basic context menu with data links to GaugePanel * Fix incorrect import in grafana/ui * Add custom cursor indicating datalinks available via context menu (in Gauge only now) * Add data links to SingleStat2 * Minor refactor * Retrieve data links in a lazy way * Update test to respect links retrieval being lazy * delay link creation * cleanup * Add origin to LinkModel and introduce field & panel links suppliers * Add value time and series name field link supplier * Remove links prop from visualization and implement common UI for data links context menu * Update snapshot * Rename className prop to clickTargetClassName * Simplify condition * Updated drilldown dashboard and minor changes * Use class name an onClick handler on the top level dom element in visualization * Enable series name interpolation when presented value is a calculation
This commit is contained in:
parent
e1924608a2
commit
ff6b8c5adc
@ -15,14 +15,15 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"gnetId": null,
|
"gnetId": null,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"iteration": 1565097360786,
|
"id": 13844,
|
||||||
|
"iteration": 1566896059256,
|
||||||
"links": [],
|
"links": [],
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
|
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 6,
|
"h": 9,
|
||||||
"w": 14,
|
"w": 12,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
@ -55,9 +56,9 @@
|
|||||||
"thresholdMarkers": true
|
"thresholdMarkers": true
|
||||||
},
|
},
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 6,
|
"h": 9,
|
||||||
"w": 10,
|
"w": 4,
|
||||||
"x": 14,
|
"x": 12,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"id": 6,
|
"id": 6,
|
||||||
@ -116,6 +117,117 @@
|
|||||||
],
|
],
|
||||||
"valueName": "avg"
|
"valueName": "avg"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cacheTimeout": null,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 4,
|
||||||
|
"x": 16,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"links": [],
|
||||||
|
"options": {
|
||||||
|
"fieldOptions": {
|
||||||
|
"calcs": ["mean"],
|
||||||
|
"defaults": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"targetBlank": true,
|
||||||
|
"title": "Go to drilldown",
|
||||||
|
"url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1&${__all_variables}&${__url_time_range}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mappings": [],
|
||||||
|
"max": 100,
|
||||||
|
"min": 0,
|
||||||
|
"nullValueMode": "connected",
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unit": "none"
|
||||||
|
},
|
||||||
|
"override": {},
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"showThresholdLabels": false,
|
||||||
|
"showThresholdMarkers": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "6.4.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "React gauge datalink",
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cacheTimeout": null,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 4,
|
||||||
|
"x": 20,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"links": [],
|
||||||
|
"options": {
|
||||||
|
"displayMode": "basic",
|
||||||
|
"fieldOptions": {
|
||||||
|
"calcs": ["mean"],
|
||||||
|
"defaults": {
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"targetBlank": true,
|
||||||
|
"title": "Go to drilldown",
|
||||||
|
"url": "/d/O6GmNPvWk/dashboard-tests-nested-template-variables-drilldown?orgId=1&${__all_variables}&${__url_time_range}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mappings": [],
|
||||||
|
"max": 100,
|
||||||
|
"min": 0,
|
||||||
|
"nullValueMode": "connected",
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unit": "none"
|
||||||
|
},
|
||||||
|
"override": {},
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"orientation": "vertical"
|
||||||
|
},
|
||||||
|
"pluginVersion": "6.4.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "React gauge datalink",
|
||||||
|
"type": "bargauge"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"aliasColors": {},
|
"aliasColors": {},
|
||||||
"bars": false,
|
"bars": false,
|
||||||
@ -128,7 +240,7 @@
|
|||||||
"h": 13,
|
"h": 13,
|
||||||
"w": 24,
|
"w": 24,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 6
|
"y": 9
|
||||||
},
|
},
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"legend": {
|
"legend": {
|
||||||
@ -296,5 +408,5 @@
|
|||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Templating - Nested Template Variables",
|
"title": "Templating - Nested Template Variables",
|
||||||
"uid": "-Y-tnEDWk",
|
"uid": "-Y-tnEDWk",
|
||||||
"version": 11
|
"version": 2
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { ValueMapping } from './valueMapping';
|
|||||||
import { QueryResultBase, Labels, NullValueMode } from './data';
|
import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||||
import { FieldCalcs } from '../utils/index';
|
import { FieldCalcs } from '../utils/index';
|
||||||
import { DisplayProcessor } from './displayValue';
|
import { DisplayProcessor } from './displayValue';
|
||||||
|
import { DataLink } from './dataLink';
|
||||||
|
|
||||||
export enum FieldType {
|
export enum FieldType {
|
||||||
time = 'time', // or date
|
time = 'time', // or date
|
||||||
@ -36,6 +37,9 @@ export interface FieldConfig {
|
|||||||
// Used when reducing field values
|
// Used when reducing field values
|
||||||
nullValueMode?: NullValueMode;
|
nullValueMode?: NullValueMode;
|
||||||
|
|
||||||
|
// The behavior when clicking on a result
|
||||||
|
links?: DataLink[];
|
||||||
|
|
||||||
// Alternative to empty string
|
// Alternative to empty string
|
||||||
noValue?: string;
|
noValue?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Link configuration. The values may contain variables that need to be
|
||||||
|
* processed before running
|
||||||
|
*/
|
||||||
export interface DataLink {
|
export interface DataLink {
|
||||||
url: string;
|
url: string;
|
||||||
title: string;
|
title: string;
|
||||||
targetBlank?: boolean;
|
targetBlank?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LinkTarget = '_blank' | '_self';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processed Link Model. The values are ready to use
|
||||||
|
*/
|
||||||
|
export interface LinkModel<T> {
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
target: LinkTarget;
|
||||||
|
origin: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a way to produce links on demand
|
||||||
|
*
|
||||||
|
* TODO: ScopedVars in in GrafanaUI package!
|
||||||
|
*/
|
||||||
|
export interface LinkModelSupplier<T extends object> {
|
||||||
|
getLinks(scopedVars?: any): Array<LinkModel<T>>;
|
||||||
|
}
|
||||||
|
@ -44,6 +44,10 @@ export class DataFrameView<T = any> implements Vector<T> {
|
|||||||
this.obj = obj;
|
this.obj = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get dataFrame() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
get length() {
|
get length() {
|
||||||
return this.data.length;
|
return this.data.length;
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ export interface Props extends Themeable {
|
|||||||
orientation: VizOrientation;
|
orientation: VizOrientation;
|
||||||
itemSpacing?: number;
|
itemSpacing?: number;
|
||||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
displayMode: 'basic' | 'lcd' | 'gradient';
|
||||||
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BarGauge extends PureComponent<Props> {
|
export class BarGauge extends PureComponent<Props> {
|
||||||
@ -43,16 +45,20 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { onClick, className } = this.props;
|
||||||
const { title } = this.props.value;
|
const { title } = this.props.value;
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
return this.renderBarAndValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = getTitleStyles(this.props);
|
const styles = getTitleStyles(this.props);
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return (
|
||||||
|
<div style={styles.wrapper} onClick={onClick} className={className}>
|
||||||
|
{this.renderBarAndValue()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.wrapper}>
|
<div style={styles.wrapper} onClick={onClick} className={className}>
|
||||||
<div style={styles.title}>{title}</div>
|
<div style={styles.title}>{title}</div>
|
||||||
{this.renderBarAndValue()}
|
{this.renderBarAndValue()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,41 +4,51 @@ exports[`BarGauge Render with basic options should render 1`] = `
|
|||||||
<div
|
<div
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"alignItems": "center",
|
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"flexDirection": "row-reverse",
|
"flexDirection": "column",
|
||||||
"justifyContent": "flex-end",
|
"overflow": "hidden",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="bar-gauge__value"
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"color": "#73BF69",
|
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"fontSize": "27.2727px",
|
"flexDirection": "row-reverse",
|
||||||
"height": "300px",
|
"justifyContent": "flex-end",
|
||||||
"lineHeight": 1,
|
|
||||||
"paddingLeft": "10px",
|
|
||||||
"width": "60px",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
25
|
<div
|
||||||
</div>
|
className="bar-gauge__value"
|
||||||
<div
|
style={
|
||||||
style={
|
Object {
|
||||||
Object {
|
"alignItems": "center",
|
||||||
"background": "rgba(115, 191, 105, 0.25)",
|
"color": "#73BF69",
|
||||||
"borderRadius": "3px",
|
"display": "flex",
|
||||||
"borderRight": "2px solid #73BF69",
|
"fontSize": "27.2727px",
|
||||||
"height": "300px",
|
"height": "300px",
|
||||||
"transition": "width 1s",
|
"lineHeight": 1,
|
||||||
"width": "60px",
|
"paddingLeft": "10px",
|
||||||
|
"width": "60px",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
>
|
||||||
/>
|
25
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "rgba(115, 191, 105, 0.25)",
|
||||||
|
"borderRadius": "3px",
|
||||||
|
"borderRight": "2px solid #73BF69",
|
||||||
|
"height": "300px",
|
||||||
|
"transition": "width 1s",
|
||||||
|
"width": "60px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Library
|
// Library
|
||||||
import React, { PureComponent, ReactNode, CSSProperties } from 'react';
|
import React, { PureComponent, ReactNode, CSSProperties } from 'react';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { css } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { DisplayValue } from '@grafana/data';
|
import { DisplayValue } from '@grafana/data';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
@ -27,6 +27,8 @@ export interface Props extends Themeable {
|
|||||||
suffix?: DisplayValue;
|
suffix?: DisplayValue;
|
||||||
sparkline?: BigValueSparkline;
|
sparkline?: BigValueSparkline;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -119,15 +121,19 @@ export class BigValue extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { height, width, value, prefix, suffix, sparkline, backgroundColor } = this.props;
|
const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={cx(
|
||||||
position: 'relative',
|
css({
|
||||||
display: 'table',
|
position: 'relative',
|
||||||
})}
|
display: 'table',
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
style={{ width, height, backgroundColor }}
|
style={{ width, height, backgroundColor }}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{value.title && (
|
{value.title && (
|
||||||
<div
|
<div
|
||||||
@ -143,6 +149,7 @@ export class BigValue extends PureComponent<Props> {
|
|||||||
{value.title}
|
{value.title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={css({
|
className={css({
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
|
@ -3,10 +3,11 @@ import { css, cx } from 'emotion';
|
|||||||
import useClickAway from 'react-use/lib/useClickAway';
|
import useClickAway from 'react-use/lib/useClickAway';
|
||||||
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||||
import { Portal, List } from '../index';
|
import { Portal, List } from '../index';
|
||||||
|
import { LinkTarget } from '@grafana/data';
|
||||||
|
|
||||||
export interface ContextMenuItem {
|
export interface ContextMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
target?: string;
|
target?: LinkTarget;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
onClick?: (event?: React.SyntheticEvent<HTMLElement>) => void;
|
onClick?: (event?: React.SyntheticEvent<HTMLElement>) => void;
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ContextMenu, ContextMenuGroup } from '../ContextMenu/ContextMenu';
|
||||||
|
|
||||||
|
interface WithContextMenuProps {
|
||||||
|
children: (props: { openMenu: React.MouseEventHandler<HTMLElement> }) => JSX.Element;
|
||||||
|
getContextMenuItems: () => ContextMenuGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithContextMenu: React.FC<WithContextMenuProps> = ({ children, getContextMenuItems }) => {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const [menuPosition, setMenuPositon] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children({
|
||||||
|
openMenu: e => {
|
||||||
|
setIsMenuOpen(true);
|
||||||
|
setMenuPositon({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isMenuOpen && (
|
||||||
|
<ContextMenu
|
||||||
|
onClose={() => setIsMenuOpen(false)}
|
||||||
|
x={menuPosition.x}
|
||||||
|
y={menuPosition.y}
|
||||||
|
items={getContextMenuItems()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -59,6 +59,7 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
|
|||||||
onBlur={onTitleBlur}
|
onBlur={onTitleBlur}
|
||||||
inputWidth={15}
|
inputWidth={15}
|
||||||
labelWidth={5}
|
labelWidth={5}
|
||||||
|
placeholder="Show details"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { WithContextMenu } from '../ContextMenu/WithContextMenu';
|
||||||
|
import { LinkModelSupplier } from '@grafana/data';
|
||||||
|
import { linkModelToContextMenuItems } from '../../utils/dataLinks';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
|
interface DataLinksContextMenuProps {
|
||||||
|
children: (props: { openMenu?: React.MouseEventHandler<HTMLElement>; targetClassName?: string }) => JSX.Element;
|
||||||
|
links?: LinkModelSupplier<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ children, links }) => {
|
||||||
|
if (!links) {
|
||||||
|
return children({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDataLinksContextMenuItems = () => {
|
||||||
|
return [{ items: linkModelToContextMenuItems(links), label: 'Data links' }];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use this class name (exposed via render prop) to add context menu indicator to the click target of the visualization
|
||||||
|
const targetClassName = css`
|
||||||
|
cursor: context-menu;
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WithContextMenu getContextMenuItems={getDataLinksContextMenuItems}>
|
||||||
|
{({ openMenu }) => {
|
||||||
|
return children({ openMenu, targetClassName });
|
||||||
|
}}
|
||||||
|
</WithContextMenu>
|
||||||
|
);
|
||||||
|
};
|
@ -68,7 +68,7 @@ export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, on
|
|||||||
|
|
||||||
{(!value || (value && value.length < (maxLinks || Infinity))) && (
|
{(!value || (value && value.length < (maxLinks || Infinity))) && (
|
||||||
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
|
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
|
||||||
Create link
|
Add link
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
margin-bottom: $space-xxs;
|
margin-bottom: $space-xxs;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ export interface Props extends Themeable {
|
|||||||
showThresholdLabels: boolean;
|
showThresholdLabels: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
value: DisplayValue;
|
value: DisplayValue;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FONT_SCALE = 1;
|
const FONT_SCALE = 1;
|
||||||
@ -133,24 +135,16 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderVisualization = () => {
|
||||||
const { width, value, height } = this.props;
|
const { width, value, height, onClick } = this.props;
|
||||||
const autoProps = calculateGaugeAutoProps(width, height, value.title);
|
const autoProps = calculateGaugeAutoProps(width, height, value.title);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{ height: `${autoProps.gaugeHeight}px`, width: '100%' }}
|
style={{ height: `${autoProps.gaugeHeight}px`, width: '100%' }}
|
||||||
ref={element => (this.canvasElement = element)}
|
ref={element => (this.canvasElement = element)}
|
||||||
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
{autoProps.showLabel && (
|
{autoProps.showLabel && (
|
||||||
<div
|
<div
|
||||||
@ -163,11 +157,30 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
top: '-4px',
|
top: '-4px',
|
||||||
|
cursor: 'default',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value.title}
|
{value.title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
className={this.props.className}
|
||||||
|
>
|
||||||
|
{this.renderVisualization()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ describe('Next id to add', () => {
|
|||||||
it('should be 4', () => {
|
it('should be 4', () => {
|
||||||
const { instance } = setup();
|
const { instance } = setup();
|
||||||
|
|
||||||
instance.addMapping();
|
instance.onAddMapping();
|
||||||
|
|
||||||
expect(instance.state.nextIdToAdd).toEqual(4);
|
expect(instance.state.nextIdToAdd).toEqual(4);
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
|||||||
|
|
||||||
import MappingRow from './MappingRow';
|
import MappingRow from './MappingRow';
|
||||||
import { MappingType, ValueMapping } from '@grafana/data';
|
import { MappingType, ValueMapping } from '@grafana/data';
|
||||||
|
import { Button } from '../index';
|
||||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -30,7 +31,7 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
|||||||
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
|
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
addMapping = () =>
|
onAddMapping = () =>
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
valueMappings: [
|
valueMappings: [
|
||||||
...prevState.valueMappings,
|
...prevState.valueMappings,
|
||||||
@ -81,16 +82,21 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
|||||||
const { valueMappings } = this.state;
|
const { valueMappings } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
|
<PanelOptionsGroup title="Value mappings">
|
||||||
{valueMappings.length > 0 &&
|
<div>
|
||||||
valueMappings.map((valueMapping, index) => (
|
{valueMappings.length > 0 &&
|
||||||
<MappingRow
|
valueMappings.map((valueMapping, index) => (
|
||||||
key={`${valueMapping.text}-${index}`}
|
<MappingRow
|
||||||
valueMapping={valueMapping}
|
key={`${valueMapping.text}-${index}`}
|
||||||
updateValueMapping={this.updateGauge}
|
valueMapping={valueMapping}
|
||||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
updateValueMapping={this.updateGauge}
|
||||||
/>
|
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
<Button variant="inverse" icon="fa fa-plus" onClick={this.onAddMapping}>
|
||||||
|
Add mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</PanelOptionsGroup>
|
</PanelOptionsGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,37 +2,45 @@
|
|||||||
|
|
||||||
exports[`Render should render component 1`] = `
|
exports[`Render should render component 1`] = `
|
||||||
<Component
|
<Component
|
||||||
onAdd={[Function]}
|
title="Value mappings"
|
||||||
title="Add value mapping"
|
|
||||||
>
|
>
|
||||||
<MappingRow
|
<div>
|
||||||
key="Ok-0"
|
<MappingRow
|
||||||
removeValueMapping={[Function]}
|
key="Ok-0"
|
||||||
updateValueMapping={[Function]}
|
removeValueMapping={[Function]}
|
||||||
valueMapping={
|
updateValueMapping={[Function]}
|
||||||
Object {
|
valueMapping={
|
||||||
"id": 1,
|
Object {
|
||||||
"operator": "",
|
"id": 1,
|
||||||
"text": "Ok",
|
"operator": "",
|
||||||
"type": 1,
|
"text": "Ok",
|
||||||
"value": "20",
|
"type": 1,
|
||||||
|
"value": "20",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<MappingRow
|
||||||
<MappingRow
|
key="Meh-1"
|
||||||
key="Meh-1"
|
removeValueMapping={[Function]}
|
||||||
removeValueMapping={[Function]}
|
updateValueMapping={[Function]}
|
||||||
updateValueMapping={[Function]}
|
valueMapping={
|
||||||
valueMapping={
|
Object {
|
||||||
Object {
|
"from": "21",
|
||||||
"from": "21",
|
"id": 2,
|
||||||
"id": 2,
|
"operator": "",
|
||||||
"operator": "",
|
"text": "Meh",
|
||||||
"text": "Meh",
|
"to": "30",
|
||||||
"to": "30",
|
"type": 2,
|
||||||
"type": 2,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Button
|
||||||
|
icon="fa fa-plus"
|
||||||
|
onClick={[Function]}
|
||||||
|
variant="inverse"
|
||||||
|
>
|
||||||
|
Add mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Component>
|
</Component>
|
||||||
`;
|
`;
|
||||||
|
@ -76,4 +76,5 @@ export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
|||||||
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
|
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
|
||||||
export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggestions';
|
export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggestions';
|
||||||
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
|
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
|
||||||
|
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||||
export { SeriesIcon } from './Legend/SeriesIcon';
|
export { SeriesIcon } from './Legend/SeriesIcon';
|
||||||
|
24
packages/grafana-ui/src/utils/dataLinks.ts
Normal file
24
packages/grafana-ui/src/utils/dataLinks.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ContextMenuItem } from '../components/ContextMenu/ContextMenu';
|
||||||
|
import { LinkModelSupplier } from '@grafana/data';
|
||||||
|
|
||||||
|
export const DataLinkBuiltInVars = {
|
||||||
|
keepTime: '__url_time_range',
|
||||||
|
includeVars: '__all_variables',
|
||||||
|
seriesName: '__series_name',
|
||||||
|
valueTime: '__value_time',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delays creating links until we need to open the ContextMenu
|
||||||
|
*/
|
||||||
|
export const linkModelToContextMenuItems: (links: LinkModelSupplier<any>) => ContextMenuItem[] = links => {
|
||||||
|
return links.getLinks().map(link => {
|
||||||
|
return {
|
||||||
|
label: link.title,
|
||||||
|
// TODO: rename to href
|
||||||
|
url: link.href,
|
||||||
|
target: link.target,
|
||||||
|
icon: `fa ${link.target === '_self' ? 'fa-link' : 'fa-external-link'}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
@ -6,6 +6,7 @@ import {
|
|||||||
FieldConfig,
|
FieldConfig,
|
||||||
DisplayValue,
|
DisplayValue,
|
||||||
GraphSeriesValue,
|
GraphSeriesValue,
|
||||||
|
DataFrameView,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import toNumber from 'lodash/toNumber';
|
import toNumber from 'lodash/toNumber';
|
||||||
@ -14,6 +15,7 @@ import toString from 'lodash/toString';
|
|||||||
import { GrafanaTheme, InterpolateFunction, ScopedVars } from '../types/index';
|
import { GrafanaTheme, InterpolateFunction, ScopedVars } from '../types/index';
|
||||||
import { getDisplayProcessor } from './displayProcessor';
|
import { getDisplayProcessor } from './displayProcessor';
|
||||||
import { getFlotPairs } from './flotPairs';
|
import { getFlotPairs } from './flotPairs';
|
||||||
|
import { DataLinkBuiltInVars } from '../utils/dataLinks';
|
||||||
|
|
||||||
export interface FieldDisplayOptions {
|
export interface FieldDisplayOptions {
|
||||||
values?: boolean; // If true show each row value
|
values?: boolean; // If true show each row value
|
||||||
@ -23,7 +25,7 @@ export interface FieldDisplayOptions {
|
|||||||
defaults: FieldConfig; // Use these values unless otherwise stated
|
defaults: FieldConfig; // Use these values unless otherwise stated
|
||||||
override: FieldConfig; // Set these values regardless of the source
|
override: FieldConfig; // Set these values regardless of the source
|
||||||
}
|
}
|
||||||
|
// TODO: use built in variables, same as for data links?
|
||||||
export const VAR_SERIES_NAME = '__series_name';
|
export const VAR_SERIES_NAME = '__series_name';
|
||||||
export const VAR_FIELD_NAME = '__field_name';
|
export const VAR_FIELD_NAME = '__field_name';
|
||||||
export const VAR_CALC = '__calc';
|
export const VAR_CALC = '__calc';
|
||||||
@ -59,10 +61,15 @@ function getTitleTemplate(title: string | undefined, stats: string[], data?: Dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FieldDisplay {
|
export interface FieldDisplay {
|
||||||
name: string; // NOT title!
|
name: string; // The field name (title is in display)
|
||||||
field: FieldConfig;
|
field: FieldConfig;
|
||||||
display: DisplayValue;
|
display: DisplayValue;
|
||||||
sparkline?: GraphSeriesValue[][];
|
sparkline?: GraphSeriesValue[][];
|
||||||
|
|
||||||
|
// Expose to the original values for delayed inspection (DataLinks etc)
|
||||||
|
view?: DataFrameView;
|
||||||
|
column?: number; // The field column index
|
||||||
|
row?: number; // only filled in when the value is from a row (ie, not a reduction)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetFieldDisplayValuesOptions {
|
export interface GetFieldDisplayValuesOptions {
|
||||||
@ -75,8 +82,19 @@ export interface GetFieldDisplayValuesOptions {
|
|||||||
|
|
||||||
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
|
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
|
||||||
|
|
||||||
|
const getTimeColumnIdx = (series: DataFrame) => {
|
||||||
|
let timeColumn = -1;
|
||||||
|
for (let i = 0; i < series.fields.length; i++) {
|
||||||
|
if (series.fields[i].type === FieldType.time) {
|
||||||
|
timeColumn = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return timeColumn;
|
||||||
|
};
|
||||||
|
|
||||||
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
|
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
|
||||||
const { data, replaceVariables, fieldOptions, sparkline } = options;
|
const { data, replaceVariables, fieldOptions } = options;
|
||||||
const { defaults, override } = fieldOptions;
|
const { defaults, override } = fieldOptions;
|
||||||
const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
|
const calcs = fieldOptions.calcs.length ? fieldOptions.calcs : [ReducerID.last];
|
||||||
|
|
||||||
@ -96,17 +114,11 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
name: series.refId ? series.refId : `Series[${s}]`,
|
name: series.refId ? series.refId : `Series[${s}]`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
scopedVars[VAR_SERIES_NAME] = { text: 'Series', value: series.name };
|
|
||||||
|
|
||||||
let timeColumn = -1;
|
scopedVars[DataLinkBuiltInVars.seriesName] = { text: 'Series', value: series.name };
|
||||||
if (sparkline) {
|
|
||||||
for (let i = 0; i < series.fields.length; i++) {
|
const timeColumn = getTimeColumnIdx(series);
|
||||||
if (series.fields[i].type === FieldType.time) {
|
const view = new DataFrameView(series);
|
||||||
timeColumn = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
||||||
const field = series.fields[i];
|
const field = series.fields[i];
|
||||||
@ -131,7 +143,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
|
|
||||||
const title = config.title ? config.title : defaultTitle;
|
const title = config.title ? config.title : defaultTitle;
|
||||||
|
|
||||||
// Show all number fields
|
// Show all rows
|
||||||
if (fieldOptions.values) {
|
if (fieldOptions.values) {
|
||||||
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||||
|
|
||||||
@ -154,6 +166,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
name,
|
name,
|
||||||
field: config,
|
field: config,
|
||||||
display: displayValue,
|
display: displayValue,
|
||||||
|
view,
|
||||||
|
column: i,
|
||||||
|
row: j,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (values.length >= limit) {
|
if (values.length >= limit) {
|
||||||
@ -166,15 +181,15 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
field,
|
field,
|
||||||
reducers: calcs, // The stats to calculate
|
reducers: calcs, // The stats to calculate
|
||||||
});
|
});
|
||||||
|
let sparkline: GraphSeriesValue[][] | undefined = undefined;
|
||||||
|
|
||||||
// Single sparkline for a field
|
// Single sparkline for every reducer
|
||||||
const points =
|
if (options.sparkline && timeColumn >= 0) {
|
||||||
timeColumn < 0
|
sparkline = getFlotPairs({
|
||||||
? undefined
|
xField: series.fields[timeColumn],
|
||||||
: getFlotPairs({
|
yField: series.fields[i],
|
||||||
xField: series.fields[timeColumn],
|
});
|
||||||
yField: series.fields[i],
|
}
|
||||||
});
|
|
||||||
|
|
||||||
for (const calc of calcs) {
|
for (const calc of calcs) {
|
||||||
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
||||||
@ -184,7 +199,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
|||||||
name,
|
name,
|
||||||
field: config,
|
field: config,
|
||||||
display: displayValue,
|
display: displayValue,
|
||||||
sparkline: points,
|
sparkline,
|
||||||
|
view,
|
||||||
|
column: i,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ export * from './fieldDisplay';
|
|||||||
export * from './validate';
|
export * from './validate';
|
||||||
export { getFlotPairs } from './flotPairs';
|
export { getFlotPairs } from './flotPairs';
|
||||||
export * from './slate';
|
export * from './slate';
|
||||||
|
export * from './dataLinks';
|
||||||
export { default as ansicolor } from './ansicolor';
|
export { default as ansicolor } from './ansicolor';
|
||||||
|
|
||||||
// Export with a namespace
|
// Export with a namespace
|
||||||
|
@ -11,6 +11,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
|||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||||
import { DataLink } from '@grafana/data';
|
import { DataLink } from '@grafana/data';
|
||||||
|
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -88,7 +89,7 @@ export class PanelHeader extends Component<Props, State> {
|
|||||||
title={panel.title}
|
title={panel.title}
|
||||||
description={panel.description}
|
description={panel.description}
|
||||||
scopedVars={panel.scopedVars}
|
scopedVars={panel.scopedVars}
|
||||||
links={panel.links}
|
links={getPanelLinksSupplier(panel)}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
@ -6,14 +6,7 @@ import { PanelModel } from '../../state';
|
|||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
it('should render component', () => {
|
it('should render component', () => {
|
||||||
const panel = new PanelModel({});
|
const panel = new PanelModel({});
|
||||||
const links: any[] = [
|
const wrapper = shallow(<PanelHeaderCorner panel={panel} />);
|
||||||
{
|
|
||||||
url: 'asd',
|
|
||||||
title: 'asd',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const wrapper = shallow(<PanelHeaderCorner panel={panel} links={links} />);
|
|
||||||
const instance = wrapper.instance() as PanelHeaderCorner;
|
const instance = wrapper.instance() as PanelHeaderCorner;
|
||||||
|
|
||||||
expect(instance.getInfoContent()).toBeDefined();
|
expect(instance.getInfoContent()).toBeDefined();
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { renderMarkdown } from '@grafana/data';
|
import { renderMarkdown, LinkModelSupplier } from '@grafana/data';
|
||||||
import { Tooltip, ScopedVars, PopoverContent } from '@grafana/ui';
|
import { Tooltip, ScopedVars, PopoverContent } from '@grafana/ui';
|
||||||
import { DataLink } from '@grafana/data';
|
|
||||||
|
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
|
|
||||||
enum InfoMode {
|
enum InfoMode {
|
||||||
@ -20,7 +18,7 @@ interface Props {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
scopedVars?: ScopedVars;
|
scopedVars?: ScopedVars;
|
||||||
links?: DataLink[];
|
links?: LinkModelSupplier<PanelModel>;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,22 +43,21 @@ export class PanelHeaderCorner extends Component<Props> {
|
|||||||
getInfoContent = (): JSX.Element => {
|
getInfoContent = (): JSX.Element => {
|
||||||
const { panel } = this.props;
|
const { panel } = this.props;
|
||||||
const markdown = panel.description || '';
|
const markdown = panel.description || '';
|
||||||
const linkSrv = new LinkSrv(templateSrv, this.timeSrv);
|
|
||||||
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
|
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
|
||||||
const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown);
|
const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown);
|
||||||
|
const links = this.props.links && this.props.links.getLinks(panel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-info-content markdown-html">
|
<div className="panel-info-content markdown-html">
|
||||||
<div dangerouslySetInnerHTML={{ __html: markedInterpolatedMarkdown }} />
|
<div dangerouslySetInnerHTML={{ __html: markedInterpolatedMarkdown }} />
|
||||||
|
|
||||||
{panel.links && panel.links.length > 0 && (
|
{links && links.length > 0 && (
|
||||||
<ul className="panel-info-corner-links">
|
<ul className="panel-info-corner-links">
|
||||||
{panel.links.map((link, idx) => {
|
{links.map((link, idx) => {
|
||||||
const info = linkSrv.getDataLinkUIModel(link, panel.scopedVars);
|
|
||||||
return (
|
return (
|
||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<a className="panel-info-corner-links__item" href={info.href} target={info.target}>
|
<a className="panel-info-corner-links__item" href={link.href} target={link.target}>
|
||||||
{info.title}
|
{link.title}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import { DashboardModel } from '../state/DashboardModel';
|
|||||||
import { PanelModel } from '../state/PanelModel';
|
import { PanelModel } from '../state/PanelModel';
|
||||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||||
import { expect } from 'test/lib/common';
|
import { expect } from 'test/lib/common';
|
||||||
import { DataLinkBuiltInVars } from 'app/features/panel/panellinks/link_srv';
|
import { DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
|
|
||||||
jest.mock('app/core/services/context_srv', () => ({}));
|
jest.mock('app/core/services/context_srv', () => ({}));
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
MIN_PANEL_HEIGHT,
|
MIN_PANEL_HEIGHT,
|
||||||
DEFAULT_PANEL_SPAN,
|
DEFAULT_PANEL_SPAN,
|
||||||
} from 'app/core/constants';
|
} from 'app/core/constants';
|
||||||
import { DataLinkBuiltInVars } from 'app/features/panel/panellinks/link_srv';
|
import { DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
|
|
||||||
export class DashboardMigrator {
|
export class DashboardMigrator {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
|
@ -18,8 +18,8 @@ import {
|
|||||||
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { TemplateSrv } from '../templating/template_srv';
|
import { TemplateSrv } from '../templating/template_srv';
|
||||||
import { LinkSrv } from './panellinks/link_srv';
|
|
||||||
import { PanelPluginMeta } from '@grafana/ui/src/types/panel';
|
import { PanelPluginMeta } from '@grafana/ui/src/types/panel';
|
||||||
|
import { getPanelLinksSupplier } from './panellinks/linkSuppliers';
|
||||||
|
|
||||||
export class PanelCtrl {
|
export class PanelCtrl {
|
||||||
panel: any;
|
panel: any;
|
||||||
@ -255,31 +255,31 @@ export class PanelCtrl {
|
|||||||
markdown = this.error || this.panel.description || '';
|
markdown = this.error || this.panel.description || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkSrv: LinkSrv = this.$injector.get('linkSrv');
|
|
||||||
const templateSrv: TemplateSrv = this.$injector.get('templateSrv');
|
const templateSrv: TemplateSrv = this.$injector.get('templateSrv');
|
||||||
const interpolatedMarkdown = templateSrv.replace(markdown, this.panel.scopedVars);
|
const interpolatedMarkdown = templateSrv.replace(markdown, this.panel.scopedVars);
|
||||||
let html = '<div class="markdown-html panel-info-content">';
|
let html = '<div class="markdown-html panel-info-content">';
|
||||||
|
|
||||||
const md = renderMarkdown(interpolatedMarkdown);
|
const md = renderMarkdown(interpolatedMarkdown);
|
||||||
html += config.disableSanitizeHtml ? md : sanitize(md);
|
html += config.disableSanitizeHtml ? md : sanitize(md);
|
||||||
|
const links = this.panel.links && getPanelLinksSupplier(this.panel).getLinks();
|
||||||
|
|
||||||
if (this.panel.links && this.panel.links.length > 0) {
|
if (links && links.length > 0) {
|
||||||
html += '<ul class="panel-info-corner-links">';
|
html += '<ul class="panel-info-corner-links">';
|
||||||
for (const link of this.panel.links) {
|
for (const link of links) {
|
||||||
const info = linkSrv.getDataLinkUIModel(link, this.panel.scopedVars);
|
|
||||||
html +=
|
html +=
|
||||||
'<li><a class="panel-menu-link" href="' +
|
'<li><a class="panel-menu-link" href="' +
|
||||||
escapeHtml(info.href) +
|
escapeHtml(link.href) +
|
||||||
'" target="' +
|
'" target="' +
|
||||||
escapeHtml(info.target) +
|
escapeHtml(link.target) +
|
||||||
'">' +
|
'">' +
|
||||||
escapeHtml(info.title) +
|
escapeHtml(link.title) +
|
||||||
'</a></li>';
|
'</a></li>';
|
||||||
}
|
}
|
||||||
html += '</ul>';
|
html += '</ul>';
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
66
public/app/features/panel/panellinks/linkSuppliers.ts
Normal file
66
public/app/features/panel/panellinks/linkSuppliers.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
|
import { FieldDisplay, ScopedVars, DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
|
import { LinkModelSupplier, DataFrameHelper, FieldType } from '@grafana/data';
|
||||||
|
import { getLinkSrv } from './link_srv';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link suppliers creates link models based on a link origin
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<FieldDisplay> | undefined => {
|
||||||
|
const links = value.field.links;
|
||||||
|
if (!links || links.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getLinks: (_scopedVars?: any) => {
|
||||||
|
const scopedVars: ScopedVars = {};
|
||||||
|
// TODO, add values to scopedVars and/or pass objects to event listeners
|
||||||
|
if (value.view) {
|
||||||
|
scopedVars[DataLinkBuiltInVars.seriesName] = {
|
||||||
|
text: 'Series',
|
||||||
|
value: value.view.dataFrame.name,
|
||||||
|
};
|
||||||
|
const field = value.column ? value.view.dataFrame.fields[value.column] : undefined;
|
||||||
|
if (field) {
|
||||||
|
console.log('Full Field Info:', field);
|
||||||
|
}
|
||||||
|
if (value.row) {
|
||||||
|
const row = value.view.get(value.row);
|
||||||
|
console.log('ROW:', row);
|
||||||
|
const dataFrame = new DataFrameHelper(value.view.dataFrame);
|
||||||
|
|
||||||
|
const timeField = dataFrame.getFirstFieldOfType(FieldType.time);
|
||||||
|
if (timeField) {
|
||||||
|
scopedVars[DataLinkBuiltInVars.valueTime] = {
|
||||||
|
text: 'Value time',
|
||||||
|
value: timeField.values.get(value.row),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('VALUE', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return links.map(link => {
|
||||||
|
return getLinkSrv().getDataLinkUIModel(link, scopedVars, value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<PanelModel> => {
|
||||||
|
const links = value.links;
|
||||||
|
|
||||||
|
if (!links || links.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getLinks: () => {
|
||||||
|
return links.map(link => {
|
||||||
|
return getLinkSrv().getDataLinkUIModel(link, value.scopedVars, value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -3,15 +3,8 @@ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
|||||||
import templateSrv, { TemplateSrv } from 'app/features/templating/template_srv';
|
import templateSrv, { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
||||||
import { VariableSuggestion, ScopedVars, VariableOrigin } from '@grafana/ui';
|
import { VariableSuggestion, ScopedVars, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
import { TimeSeriesValue, DateTime, dateTime, DataLink, KeyValue, deprecationWarning } from '@grafana/data';
|
import { DataLink, KeyValue, deprecationWarning, LinkModel } from '@grafana/data';
|
||||||
|
|
||||||
export const DataLinkBuiltInVars = {
|
|
||||||
keepTime: '__url_time_range',
|
|
||||||
includeVars: '__all_variables',
|
|
||||||
seriesName: '__series_name',
|
|
||||||
valueTime: '__value_time',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||||
...templateSrv.variables.map(variable => ({
|
...templateSrv.variables.map(variable => ({
|
||||||
@ -44,22 +37,17 @@ export const getDataLinksVariableSuggestions = (): VariableSuggestion[] => [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
type LinkTarget = '_blank' | '_self';
|
export const getCalculationValueDataLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||||
|
...getPanelLinksVariableSuggestions(),
|
||||||
|
{
|
||||||
|
value: `${DataLinkBuiltInVars.seriesName}`,
|
||||||
|
documentation: 'Adds series name',
|
||||||
|
origin: VariableOrigin.BuiltIn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export interface LinkModel {
|
|
||||||
href: string;
|
|
||||||
title: string;
|
|
||||||
target: LinkTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LinkDataPoint {
|
|
||||||
datapoint: TimeSeriesValue[];
|
|
||||||
seriesName: string;
|
|
||||||
[key: number]: any;
|
|
||||||
}
|
|
||||||
export interface LinkService {
|
export interface LinkService {
|
||||||
getDataLinkUIModel: (link: DataLink, scopedVars: ScopedVars, dataPoint?: LinkDataPoint) => LinkModel;
|
getDataLinkUIModel: <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => LinkModel<T>;
|
||||||
getDataPointVars: (seriesName: string, dataPointTs: DateTime) => ScopedVars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LinkSrv implements LinkService {
|
export class LinkSrv implements LinkService {
|
||||||
@ -90,33 +78,20 @@ export class LinkSrv implements LinkService {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataPointVars = (seriesName: string, valueTime: DateTime) => {
|
getDataLinkUIModel = <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => {
|
||||||
return {
|
|
||||||
[DataLinkBuiltInVars.valueTime]: {
|
|
||||||
text: valueTime.valueOf(),
|
|
||||||
value: valueTime.valueOf(),
|
|
||||||
},
|
|
||||||
[DataLinkBuiltInVars.seriesName]: {
|
|
||||||
text: seriesName,
|
|
||||||
value: seriesName,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
getDataLinkUIModel = (link: DataLink, scopedVars: ScopedVars, dataPoint?: LinkDataPoint) => {
|
|
||||||
const params: KeyValue = {};
|
const params: KeyValue = {};
|
||||||
const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl());
|
const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl());
|
||||||
|
|
||||||
const info: LinkModel = {
|
const info: LinkModel<T> = {
|
||||||
href: link.url,
|
href: link.url,
|
||||||
title: this.templateSrv.replace(link.title || '', scopedVars),
|
title: this.templateSrv.replace(link.title || '', scopedVars),
|
||||||
target: link.targetBlank ? '_blank' : '_self',
|
target: link.targetBlank ? '_blank' : '_self',
|
||||||
|
origin,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
|
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
|
||||||
|
|
||||||
const variablesQuery = toUrlParams(params);
|
const variablesQuery = toUrlParams(params);
|
||||||
|
|
||||||
info.href = this.templateSrv.replace(link.url, {
|
info.href = this.templateSrv.replace(link.url, {
|
||||||
...scopedVars,
|
...scopedVars,
|
||||||
[DataLinkBuiltInVars.keepTime]: {
|
[DataLinkBuiltInVars.keepTime]: {
|
||||||
@ -129,13 +104,6 @@ export class LinkSrv implements LinkService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dataPoint) {
|
|
||||||
info.href = this.templateSrv.replace(
|
|
||||||
info.href,
|
|
||||||
this.getDataPointVars(dataPoint.seriesName, dateTime(dataPoint.datapoint[0]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -146,7 +114,7 @@ export class LinkSrv implements LinkService {
|
|||||||
*/
|
*/
|
||||||
getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) {
|
getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) {
|
||||||
deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel');
|
deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel');
|
||||||
return this.getDataLinkUIModel(link, scopedVars);
|
return this.getDataLinkUIModel(link, scopedVars, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { LinkSrv, DataLinkBuiltInVars } from '../link_srv';
|
import { LinkSrv } from '../link_srv';
|
||||||
|
import { DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
@ -80,6 +81,7 @@ describe('linkSrv', () => {
|
|||||||
title: 'Any title',
|
title: 'Any title',
|
||||||
url: `/d/1?$${DataLinkBuiltInVars.keepTime}`,
|
url: `/d/1?$${DataLinkBuiltInVars.keepTime}`,
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
{}
|
{}
|
||||||
).href
|
).href
|
||||||
).toEqual('/d/1?from=now-1h&to=now');
|
).toEqual('/d/1?from=now-1h&to=now');
|
||||||
@ -92,32 +94,43 @@ describe('linkSrv', () => {
|
|||||||
title: 'Any title',
|
title: 'Any title',
|
||||||
url: `/d/1?$${DataLinkBuiltInVars.includeVars}`,
|
url: `/d/1?$${DataLinkBuiltInVars.includeVars}`,
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
{}
|
{}
|
||||||
).href
|
).href
|
||||||
).toEqual('/d/1?var-test1=val1&var-test2=val2');
|
).toEqual('/d/1?var-test1=val1&var-test2=val2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should interpolate series name from datapoint', () => {
|
it('should interpolate series name', () => {
|
||||||
expect(
|
expect(
|
||||||
linkSrv.getDataLinkUIModel(
|
linkSrv.getDataLinkUIModel(
|
||||||
{
|
{
|
||||||
title: 'Any title',
|
title: 'Any title',
|
||||||
url: `/d/1?var-test=$${DataLinkBuiltInVars.seriesName}`,
|
url: `/d/1?var-test=$${DataLinkBuiltInVars.seriesName}`,
|
||||||
},
|
},
|
||||||
{},
|
{
|
||||||
dataPointMock
|
[DataLinkBuiltInVars.seriesName]: {
|
||||||
|
value: 'A-series',
|
||||||
|
text: 'A-series',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}
|
||||||
).href
|
).href
|
||||||
).toEqual('/d/1?var-test=A-series');
|
).toEqual('/d/1?var-test=A-series');
|
||||||
});
|
});
|
||||||
it('should interpolate time range based on datapoint timestamp', () => {
|
it('should interpolate value time', () => {
|
||||||
expect(
|
expect(
|
||||||
linkSrv.getDataLinkUIModel(
|
linkSrv.getDataLinkUIModel(
|
||||||
{
|
{
|
||||||
title: 'Any title',
|
title: 'Any title',
|
||||||
url: `/d/1?time=$${DataLinkBuiltInVars.valueTime}`,
|
url: `/d/1?time=$${DataLinkBuiltInVars.valueTime}`,
|
||||||
},
|
},
|
||||||
{},
|
{
|
||||||
dataPointMock
|
[DataLinkBuiltInVars.valueTime]: {
|
||||||
|
value: dataPointMock.datapoint[0],
|
||||||
|
text: dataPointMock.datapoint[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{}
|
||||||
).href
|
).href
|
||||||
).toEqual('/d/1?time=1000000001');
|
).toEqual('/d/1?time=1000000001');
|
||||||
});
|
});
|
||||||
|
@ -5,11 +5,12 @@ import React, { PureComponent } from 'react';
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay } from '@grafana/ui';
|
import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay, DataLinksContextMenu } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { BarGaugeOptions } from './types';
|
import { BarGaugeOptions } from './types';
|
||||||
import { PanelProps } from '@grafana/ui';
|
import { PanelProps } from '@grafana/ui';
|
||||||
|
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
|
|
||||||
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||||
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
||||||
@ -17,18 +18,26 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
|||||||
const { field, display } = value;
|
const { field, display } = value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BarGauge
|
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
|
||||||
value={display}
|
{({ openMenu, targetClassName }) => {
|
||||||
width={width}
|
return (
|
||||||
height={height}
|
<BarGauge
|
||||||
orientation={options.orientation}
|
value={display}
|
||||||
thresholds={field.thresholds}
|
width={width}
|
||||||
theme={config.theme}
|
height={height}
|
||||||
itemSpacing={this.getItemSpacing()}
|
orientation={options.orientation}
|
||||||
displayMode={options.displayMode}
|
thresholds={field.thresholds}
|
||||||
minValue={field.min}
|
theme={config.theme}
|
||||||
maxValue={field.max}
|
itemSpacing={this.getItemSpacing()}
|
||||||
/>
|
displayMode={options.displayMode}
|
||||||
|
minValue={field.min}
|
||||||
|
maxValue={field.max}
|
||||||
|
onClick={openMenu}
|
||||||
|
className={targetClassName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataLinksContextMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,11 +12,16 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
PanelEditorProps,
|
PanelEditorProps,
|
||||||
Select,
|
Select,
|
||||||
|
DataLinksEditor,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { FieldConfig } from '@grafana/data';
|
import { FieldConfig, DataLink } from '@grafana/data';
|
||||||
|
|
||||||
import { Threshold, ValueMapping } from '@grafana/data';
|
import { Threshold, ValueMapping } from '@grafana/data';
|
||||||
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
||||||
|
import {
|
||||||
|
getDataLinksVariableSuggestions,
|
||||||
|
getCalculationValueDataLinksVariableSuggestions,
|
||||||
|
} from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||||
onThresholdsChanged = (thresholds: Threshold[]) => {
|
onThresholdsChanged = (thresholds: Threshold[]) => {
|
||||||
@ -51,11 +56,20 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
|||||||
onOrientationChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
|
onOrientationChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, orientation: value });
|
||||||
onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
|
onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
|
||||||
|
|
||||||
|
onDataLinksChanged = (links: DataLink[]) => {
|
||||||
|
this.onDefaultsChange({
|
||||||
|
...this.props.options.fieldOptions.defaults,
|
||||||
|
links,
|
||||||
|
});
|
||||||
|
};
|
||||||
render() {
|
render() {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
const { fieldOptions } = options;
|
const { fieldOptions } = options;
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
|
||||||
|
const suggestions = fieldOptions.values
|
||||||
|
? getDataLinksVariableSuggestions()
|
||||||
|
: getCalculationValueDataLinksVariableSuggestions();
|
||||||
const labelWidth = 6;
|
const labelWidth = 6;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -92,6 +106,15 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
|||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
|
||||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||||
|
|
||||||
|
<PanelOptionsGroup title="Data links">
|
||||||
|
<DataLinksEditor
|
||||||
|
value={defaults.links}
|
||||||
|
onChange={this.onDataLinksChanged}
|
||||||
|
suggestions={suggestions}
|
||||||
|
maxLinks={10}
|
||||||
|
/>
|
||||||
|
</PanelOptionsGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,12 @@ import React, { PureComponent } from 'react';
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation } from '@grafana/ui';
|
import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation, DataLinksContextMenu } from '@grafana/ui';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
import { PanelProps, VizRepeater } from '@grafana/ui';
|
import { PanelProps, VizRepeater } from '@grafana/ui';
|
||||||
|
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
|
|
||||||
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||||
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
||||||
@ -17,17 +18,25 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
|||||||
const { field, display } = value;
|
const { field, display } = value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gauge
|
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
|
||||||
value={display}
|
{({ openMenu, targetClassName }) => {
|
||||||
width={width}
|
return (
|
||||||
height={height}
|
<Gauge
|
||||||
thresholds={field.thresholds}
|
value={display}
|
||||||
showThresholdLabels={options.showThresholdLabels}
|
width={width}
|
||||||
showThresholdMarkers={options.showThresholdMarkers}
|
height={height}
|
||||||
minValue={field.min}
|
thresholds={field.thresholds}
|
||||||
maxValue={field.max}
|
showThresholdLabels={options.showThresholdLabels}
|
||||||
theme={config.theme}
|
showThresholdMarkers={options.showThresholdMarkers}
|
||||||
/>
|
minValue={field.min}
|
||||||
|
maxValue={field.max}
|
||||||
|
theme={config.theme}
|
||||||
|
onClick={openMenu}
|
||||||
|
className={targetClassName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataLinksContextMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,10 +10,15 @@ import {
|
|||||||
FieldPropertiesEditor,
|
FieldPropertiesEditor,
|
||||||
Switch,
|
Switch,
|
||||||
PanelOptionsGroup,
|
PanelOptionsGroup,
|
||||||
|
DataLinksEditor,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { Threshold, ValueMapping, FieldConfig } from '@grafana/data';
|
import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
|
||||||
|
|
||||||
import { GaugeOptions } from './types';
|
import { GaugeOptions } from './types';
|
||||||
|
import {
|
||||||
|
getCalculationValueDataLinksVariableSuggestions,
|
||||||
|
getDataLinksVariableSuggestions,
|
||||||
|
} from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||||
labelWidth = 6;
|
labelWidth = 6;
|
||||||
@ -56,10 +61,20 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onDataLinksChanged = (links: DataLink[]) => {
|
||||||
|
this.onDefaultsChange({
|
||||||
|
...this.props.options.fieldOptions.defaults,
|
||||||
|
links,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
|
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
const suggestions = fieldOptions.values
|
||||||
|
? getDataLinksVariableSuggestions()
|
||||||
|
: getCalculationValueDataLinksVariableSuggestions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -92,6 +107,15 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
|||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
|
||||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||||
|
|
||||||
|
<PanelOptionsGroup title="Data links">
|
||||||
|
<DataLinksEditor
|
||||||
|
value={defaults.links}
|
||||||
|
onChange={this.onDataLinksChanged}
|
||||||
|
suggestions={suggestions}
|
||||||
|
maxLinks={10}
|
||||||
|
/>
|
||||||
|
</PanelOptionsGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import { GraphLegendProps, Legend } from './Legend/Legend';
|
import { GraphLegendProps, Legend } from './Legend/Legend';
|
||||||
|
|
||||||
import { GraphCtrl } from './module';
|
import { GraphCtrl } from './module';
|
||||||
import { getValueFormat, ContextMenuItem, ContextMenuGroup } from '@grafana/ui';
|
import { getValueFormat, ContextMenuItem, ContextMenuGroup, DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
import { provideTheme } from 'app/core/utils/ConfigProvider';
|
import { provideTheme } from 'app/core/utils/ConfigProvider';
|
||||||
import { DataLink, toUtc } from '@grafana/data';
|
import { DataLink, toUtc } from '@grafana/data';
|
||||||
import { GraphContextMenuCtrl, FlotDataPoint } from './GraphContextMenuCtrl';
|
import { GraphContextMenuCtrl, FlotDataPoint } from './GraphContextMenuCtrl';
|
||||||
@ -196,10 +196,15 @@ class GraphElement {
|
|||||||
{
|
{
|
||||||
items: [
|
items: [
|
||||||
...dataLinks.map<ContextMenuItem>(link => {
|
...dataLinks.map<ContextMenuItem>(link => {
|
||||||
const linkUiModel = this.linkSrv.getDataLinkUIModel(link, this.panel.scopedVars, {
|
const linkUiModel = this.linkSrv.getDataLinkUIModel(
|
||||||
seriesName: item.series.alias,
|
link,
|
||||||
datapoint: item.datapoint,
|
{
|
||||||
});
|
...this.panel.scopedVars,
|
||||||
|
[DataLinkBuiltInVars.seriesName]: { value: item.series.alias, text: item.series.alias },
|
||||||
|
[DataLinkBuiltInVars.valueTime]: { value: item.datapoint[0], text: item.datapoint[0] },
|
||||||
|
},
|
||||||
|
item
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
label: linkUiModel.title,
|
label: linkUiModel.title,
|
||||||
url: linkUiModel.href,
|
url: linkUiModel.href,
|
||||||
|
@ -24,9 +24,10 @@ import {
|
|||||||
DisplayValue,
|
DisplayValue,
|
||||||
fieldReducers,
|
fieldReducers,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
|
LinkModel,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { LinkSrv, LinkModel } from 'app/features/panel/panellinks/link_srv';
|
import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||||
import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
|
import { PanelQueryRunnerFormat } from 'app/features/dashboard/state/PanelQueryRunner';
|
||||||
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
|
import { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
|
||||||
|
|
||||||
@ -328,7 +329,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
const $sanitize = this.$sanitize;
|
const $sanitize = this.$sanitize;
|
||||||
const panel = ctrl.panel;
|
const panel = ctrl.panel;
|
||||||
const templateSrv = this.templateSrv;
|
const templateSrv = this.templateSrv;
|
||||||
let linkInfo: LinkModel | null = null;
|
let linkInfo: LinkModel<any> | null = null;
|
||||||
const $panelContainer = elem.find('.panel-container');
|
const $panelContainer = elem.find('.panel-container');
|
||||||
elem = elem.find('.singlestat-panel');
|
elem = elem.find('.singlestat-panel');
|
||||||
|
|
||||||
@ -592,7 +593,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
|||||||
elem.toggleClass('pointer', panel.links.length > 0);
|
elem.toggleClass('pointer', panel.links.length > 0);
|
||||||
|
|
||||||
if (panel.links.length > 0) {
|
if (panel.links.length > 0) {
|
||||||
linkInfo = linkSrv.getDataLinkUIModel(panel.links[0], data.scopedVars);
|
linkInfo = linkSrv.getDataLinkUIModel(panel.links[0], data.scopedVars, {});
|
||||||
} else {
|
} else {
|
||||||
linkInfo = null;
|
linkInfo = null;
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,18 @@ import {
|
|||||||
FieldDisplayEditor,
|
FieldDisplayEditor,
|
||||||
FieldPropertiesEditor,
|
FieldPropertiesEditor,
|
||||||
PanelOptionsGroup,
|
PanelOptionsGroup,
|
||||||
|
DataLinksEditor,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { Threshold, ValueMapping, FieldConfig } from '@grafana/data';
|
import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
|
||||||
|
|
||||||
import { SingleStatOptions, SparklineOptions } from './types';
|
import { SingleStatOptions, SparklineOptions } from './types';
|
||||||
import { ColoringEditor } from './ColoringEditor';
|
import { ColoringEditor } from './ColoringEditor';
|
||||||
import { FontSizeEditor } from './FontSizeEditor';
|
import { FontSizeEditor } from './FontSizeEditor';
|
||||||
import { SparklineEditor } from './SparklineEditor';
|
import { SparklineEditor } from './SparklineEditor';
|
||||||
|
import {
|
||||||
|
getDataLinksVariableSuggestions,
|
||||||
|
getCalculationValueDataLinksVariableSuggestions,
|
||||||
|
} from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
|
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
|
||||||
onThresholdsChanged = (thresholds: Threshold[]) => {
|
onThresholdsChanged = (thresholds: Threshold[]) => {
|
||||||
@ -53,10 +58,20 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onDataLinksChanged = (links: DataLink[]) => {
|
||||||
|
this.onDefaultsChange({
|
||||||
|
...this.props.options.fieldOptions.defaults,
|
||||||
|
links,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { options } = this.props;
|
const { options } = this.props;
|
||||||
const { fieldOptions } = options;
|
const { fieldOptions } = options;
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
|
const suggestions = fieldOptions.values
|
||||||
|
? getDataLinksVariableSuggestions()
|
||||||
|
: getCalculationValueDataLinksVariableSuggestions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -77,6 +92,15 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
|||||||
</PanelOptionsGrid>
|
</PanelOptionsGrid>
|
||||||
|
|
||||||
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={defaults.mappings} />
|
||||||
|
|
||||||
|
<PanelOptionsGroup title="Data links">
|
||||||
|
<DataLinksEditor
|
||||||
|
value={defaults.links}
|
||||||
|
onChange={this.onDataLinksChanged}
|
||||||
|
suggestions={suggestions}
|
||||||
|
maxLinks={10}
|
||||||
|
/>
|
||||||
|
</PanelOptionsGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,16 @@ import { config } from 'app/core/config';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { SingleStatOptions } from './types';
|
import { SingleStatOptions } from './types';
|
||||||
import { PanelProps, getFieldDisplayValues, VizRepeater, FieldDisplay, BigValue } from '@grafana/ui';
|
import {
|
||||||
|
PanelProps,
|
||||||
|
getFieldDisplayValues,
|
||||||
|
VizRepeater,
|
||||||
|
FieldDisplay,
|
||||||
|
BigValue,
|
||||||
|
DataLinksContextMenu,
|
||||||
|
} from '@grafana/ui';
|
||||||
import { BigValueSparkline } from '@grafana/ui/src/components/BigValue/BigValue';
|
import { BigValueSparkline } from '@grafana/ui/src/components/BigValue/BigValue';
|
||||||
|
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||||
|
|
||||||
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
|
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
|
||||||
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
||||||
@ -23,7 +31,23 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BigValue value={value.display} sparkline={sparkline} width={width} height={height} theme={config.theme} />;
|
return (
|
||||||
|
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
|
||||||
|
{({ openMenu, targetClassName }) => {
|
||||||
|
return (
|
||||||
|
<BigValue
|
||||||
|
value={value.display}
|
||||||
|
sparkline={sparkline}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
theme={config.theme}
|
||||||
|
onClick={openMenu}
|
||||||
|
className={targetClassName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataLinksContextMenu>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getValues = (): FieldDisplay[] => {
|
getValues = (): FieldDisplay[] => {
|
||||||
|
Loading…
Reference in New Issue
Block a user