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,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"iteration": 1565097360786,
|
||||
"id": 13844,
|
||||
"iteration": 1566896059256,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod",
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 14,
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
@ -55,9 +56,9 @@
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 6,
|
||||
"w": 10,
|
||||
"x": 14,
|
||||
"h": 9,
|
||||
"w": 4,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
@ -116,6 +117,117 @@
|
||||
],
|
||||
"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": {},
|
||||
"bars": false,
|
||||
@ -128,7 +240,7 @@
|
||||
"h": 13,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 6
|
||||
"y": 9
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
@ -296,5 +408,5 @@
|
||||
"timezone": "",
|
||||
"title": "Templating - Nested Template Variables",
|
||||
"uid": "-Y-tnEDWk",
|
||||
"version": 11
|
||||
"version": 2
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { ValueMapping } from './valueMapping';
|
||||
import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||
import { FieldCalcs } from '../utils/index';
|
||||
import { DisplayProcessor } from './displayValue';
|
||||
import { DataLink } from './dataLink';
|
||||
|
||||
export enum FieldType {
|
||||
time = 'time', // or date
|
||||
@ -36,6 +37,9 @@ export interface FieldConfig {
|
||||
// Used when reducing field values
|
||||
nullValueMode?: NullValueMode;
|
||||
|
||||
// The behavior when clicking on a result
|
||||
links?: DataLink[];
|
||||
|
||||
// Alternative to empty string
|
||||
noValue?: string;
|
||||
}
|
||||
|
@ -1,5 +1,30 @@
|
||||
/**
|
||||
* Link configuration. The values may contain variables that need to be
|
||||
* processed before running
|
||||
*/
|
||||
export interface DataLink {
|
||||
url: string;
|
||||
title: string;
|
||||
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;
|
||||
}
|
||||
|
||||
get dataFrame() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.data.length;
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ export interface Props extends Themeable {
|
||||
orientation: VizOrientation;
|
||||
itemSpacing?: number;
|
||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export class BarGauge extends PureComponent<Props> {
|
||||
@ -43,16 +45,20 @@ export class BarGauge extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onClick, className } = this.props;
|
||||
const { title } = this.props.value;
|
||||
|
||||
if (!title) {
|
||||
return this.renderBarAndValue();
|
||||
}
|
||||
|
||||
const styles = getTitleStyles(this.props);
|
||||
|
||||
if (!title) {
|
||||
return (
|
||||
<div style={styles.wrapper} onClick={onClick} className={className}>
|
||||
{this.renderBarAndValue()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.wrapper}>
|
||||
<div style={styles.wrapper} onClick={onClick} className={className}>
|
||||
<div style={styles.title}>{title}</div>
|
||||
{this.renderBarAndValue()}
|
||||
</div>
|
||||
|
@ -4,41 +4,51 @@ exports[`BarGauge Render with basic options should render 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row-reverse",
|
||||
"justifyContent": "flex-end",
|
||||
"flexDirection": "column",
|
||||
"overflow": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="bar-gauge__value"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"color": "#73BF69",
|
||||
"display": "flex",
|
||||
"fontSize": "27.2727px",
|
||||
"height": "300px",
|
||||
"lineHeight": 1,
|
||||
"paddingLeft": "10px",
|
||||
"width": "60px",
|
||||
"flexDirection": "row-reverse",
|
||||
"justifyContent": "flex-end",
|
||||
}
|
||||
}
|
||||
>
|
||||
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
|
||||
className="bar-gauge__value"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"color": "#73BF69",
|
||||
"display": "flex",
|
||||
"fontSize": "27.2727px",
|
||||
"height": "300px",
|
||||
"lineHeight": 1,
|
||||
"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>
|
||||
`;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Library
|
||||
import React, { PureComponent, ReactNode, CSSProperties } from 'react';
|
||||
import $ from 'jquery';
|
||||
import { css } from 'emotion';
|
||||
import { css, cx } from 'emotion';
|
||||
import { DisplayValue } from '@grafana/data';
|
||||
|
||||
// Utils
|
||||
@ -27,6 +27,8 @@ export interface Props extends Themeable {
|
||||
suffix?: DisplayValue;
|
||||
sparkline?: BigValueSparkline;
|
||||
backgroundColor?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -119,15 +121,19 @@ export class BigValue extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { height, width, value, prefix, suffix, sparkline, backgroundColor } = this.props;
|
||||
const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
display: 'table',
|
||||
})}
|
||||
className={cx(
|
||||
css({
|
||||
position: 'relative',
|
||||
display: 'table',
|
||||
}),
|
||||
className
|
||||
)}
|
||||
style={{ width, height, backgroundColor }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{value.title && (
|
||||
<div
|
||||
@ -143,6 +149,7 @@ export class BigValue extends PureComponent<Props> {
|
||||
{value.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={css({
|
||||
lineHeight: 1,
|
||||
|
@ -3,10 +3,11 @@ import { css, cx } from 'emotion';
|
||||
import useClickAway from 'react-use/lib/useClickAway';
|
||||
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||
import { Portal, List } from '../index';
|
||||
import { LinkTarget } from '@grafana/data';
|
||||
|
||||
export interface ContextMenuItem {
|
||||
label: string;
|
||||
target?: string;
|
||||
target?: LinkTarget;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
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}
|
||||
inputWidth={15}
|
||||
labelWidth={5}
|
||||
placeholder="Show details"
|
||||
/>
|
||||
|
||||
<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))) && (
|
||||
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
|
||||
Create link
|
||||
Add link
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
@ -2,7 +2,7 @@
|
||||
margin-bottom: $space-xxs;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
|
||||
|
@ -15,6 +15,8 @@ export interface Props extends Themeable {
|
||||
showThresholdLabels: boolean;
|
||||
width: number;
|
||||
value: DisplayValue;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const FONT_SCALE = 1;
|
||||
@ -133,24 +135,16 @@ export class Gauge extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, value, height } = this.props;
|
||||
renderVisualization = () => {
|
||||
const { width, value, height, onClick } = this.props;
|
||||
const autoProps = calculateGaugeAutoProps(width, height, value.title);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
style={{ height: `${autoProps.gaugeHeight}px`, width: '100%' }}
|
||||
ref={element => (this.canvasElement = element)}
|
||||
onClick={onClick}
|
||||
/>
|
||||
{autoProps.showLabel && (
|
||||
<div
|
||||
@ -163,11 +157,30 @@ export class Gauge extends PureComponent<Props> {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
top: '-4px',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
{value.title}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
className={this.props.className}
|
||||
>
|
||||
{this.renderVisualization()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ describe('Next id to add', () => {
|
||||
it('should be 4', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
instance.addMapping();
|
||||
instance.onAddMapping();
|
||||
|
||||
expect(instance.state.nextIdToAdd).toEqual(4);
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import MappingRow from './MappingRow';
|
||||
import { MappingType, ValueMapping } from '@grafana/data';
|
||||
import { Button } from '../index';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
addMapping = () =>
|
||||
onAddMapping = () =>
|
||||
this.setState(prevState => ({
|
||||
valueMappings: [
|
||||
...prevState.valueMappings,
|
||||
@ -81,16 +82,21 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
||||
const { valueMappings } = this.state;
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
<PanelOptionsGroup title="Value mappings">
|
||||
<div>
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
<Button variant="inverse" icon="fa fa-plus" onClick={this.onAddMapping}>
|
||||
Add mapping
|
||||
</Button>
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
|
@ -2,37 +2,45 @@
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<Component
|
||||
onAdd={[Function]}
|
||||
title="Add value mapping"
|
||||
title="Value mappings"
|
||||
>
|
||||
<MappingRow
|
||||
key="Ok-0"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
"text": "Ok",
|
||||
"type": 1,
|
||||
"value": "20",
|
||||
<div>
|
||||
<MappingRow
|
||||
key="Ok-0"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"id": 1,
|
||||
"operator": "",
|
||||
"text": "Ok",
|
||||
"type": 1,
|
||||
"value": "20",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
<MappingRow
|
||||
key="Meh-1"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"from": "21",
|
||||
"id": 2,
|
||||
"operator": "",
|
||||
"text": "Meh",
|
||||
"to": "30",
|
||||
"type": 2,
|
||||
/>
|
||||
<MappingRow
|
||||
key="Meh-1"
|
||||
removeValueMapping={[Function]}
|
||||
updateValueMapping={[Function]}
|
||||
valueMapping={
|
||||
Object {
|
||||
"from": "21",
|
||||
"id": 2,
|
||||
"operator": "",
|
||||
"text": "Meh",
|
||||
"to": "30",
|
||||
"type": 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
<Button
|
||||
icon="fa fa-plus"
|
||||
onClick={[Function]}
|
||||
variant="inverse"
|
||||
>
|
||||
Add mapping
|
||||
</Button>
|
||||
</div>
|
||||
</Component>
|
||||
`;
|
||||
|
@ -76,4 +76,5 @@ export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
|
||||
export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggestions';
|
||||
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
|
||||
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||
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,
|
||||
DisplayValue,
|
||||
GraphSeriesValue,
|
||||
DataFrameView,
|
||||
} from '@grafana/data';
|
||||
|
||||
import toNumber from 'lodash/toNumber';
|
||||
@ -14,6 +15,7 @@ import toString from 'lodash/toString';
|
||||
import { GrafanaTheme, InterpolateFunction, ScopedVars } from '../types/index';
|
||||
import { getDisplayProcessor } from './displayProcessor';
|
||||
import { getFlotPairs } from './flotPairs';
|
||||
import { DataLinkBuiltInVars } from '../utils/dataLinks';
|
||||
|
||||
export interface FieldDisplayOptions {
|
||||
values?: boolean; // If true show each row value
|
||||
@ -23,7 +25,7 @@ export interface FieldDisplayOptions {
|
||||
defaults: FieldConfig; // Use these values unless otherwise stated
|
||||
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_FIELD_NAME = '__field_name';
|
||||
export const VAR_CALC = '__calc';
|
||||
@ -59,10 +61,15 @@ function getTitleTemplate(title: string | undefined, stats: string[], data?: Dat
|
||||
}
|
||||
|
||||
export interface FieldDisplay {
|
||||
name: string; // NOT title!
|
||||
name: string; // The field name (title is in display)
|
||||
field: FieldConfig;
|
||||
display: DisplayValue;
|
||||
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 {
|
||||
@ -75,8 +82,19 @@ export interface GetFieldDisplayValuesOptions {
|
||||
|
||||
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[] => {
|
||||
const { data, replaceVariables, fieldOptions, sparkline } = options;
|
||||
const { data, replaceVariables, fieldOptions } = options;
|
||||
const { defaults, override } = fieldOptions;
|
||||
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}]`,
|
||||
};
|
||||
}
|
||||
scopedVars[VAR_SERIES_NAME] = { text: 'Series', value: series.name };
|
||||
|
||||
let timeColumn = -1;
|
||||
if (sparkline) {
|
||||
for (let i = 0; i < series.fields.length; i++) {
|
||||
if (series.fields[i].type === FieldType.time) {
|
||||
timeColumn = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
scopedVars[DataLinkBuiltInVars.seriesName] = { text: 'Series', value: series.name };
|
||||
|
||||
const timeColumn = getTimeColumnIdx(series);
|
||||
const view = new DataFrameView(series);
|
||||
|
||||
for (let i = 0; i < series.fields.length && !hitLimit; i++) {
|
||||
const field = series.fields[i];
|
||||
@ -131,7 +143,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
|
||||
const title = config.title ? config.title : defaultTitle;
|
||||
|
||||
// Show all number fields
|
||||
// Show all rows
|
||||
if (fieldOptions.values) {
|
||||
const usesCellValues = title.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||
|
||||
@ -154,6 +166,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
name,
|
||||
field: config,
|
||||
display: displayValue,
|
||||
view,
|
||||
column: i,
|
||||
row: j,
|
||||
});
|
||||
|
||||
if (values.length >= limit) {
|
||||
@ -166,15 +181,15 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
field,
|
||||
reducers: calcs, // The stats to calculate
|
||||
});
|
||||
let sparkline: GraphSeriesValue[][] | undefined = undefined;
|
||||
|
||||
// Single sparkline for a field
|
||||
const points =
|
||||
timeColumn < 0
|
||||
? undefined
|
||||
: getFlotPairs({
|
||||
xField: series.fields[timeColumn],
|
||||
yField: series.fields[i],
|
||||
});
|
||||
// Single sparkline for every reducer
|
||||
if (options.sparkline && timeColumn >= 0) {
|
||||
sparkline = getFlotPairs({
|
||||
xField: series.fields[timeColumn],
|
||||
yField: series.fields[i],
|
||||
});
|
||||
}
|
||||
|
||||
for (const calc of calcs) {
|
||||
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
||||
@ -184,7 +199,9 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
name,
|
||||
field: config,
|
||||
display: displayValue,
|
||||
sparkline: points,
|
||||
sparkline,
|
||||
view,
|
||||
column: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ export * from './fieldDisplay';
|
||||
export * from './validate';
|
||||
export { getFlotPairs } from './flotPairs';
|
||||
export * from './slate';
|
||||
export * from './dataLinks';
|
||||
export { default as ansicolor } from './ansicolor';
|
||||
|
||||
// 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 { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { DataLink } from '@grafana/data';
|
||||
import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@ -88,7 +89,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
links={getPanelLinksSupplier(panel)}
|
||||
error={error}
|
||||
/>
|
||||
<div
|
||||
|
@ -6,14 +6,7 @@ import { PanelModel } from '../../state';
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const panel = new PanelModel({});
|
||||
const links: any[] = [
|
||||
{
|
||||
url: 'asd',
|
||||
title: 'asd',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = shallow(<PanelHeaderCorner panel={panel} links={links} />);
|
||||
const wrapper = shallow(<PanelHeaderCorner panel={panel} />);
|
||||
const instance = wrapper.instance() as PanelHeaderCorner;
|
||||
|
||||
expect(instance.getInfoContent()).toBeDefined();
|
||||
|
@ -1,12 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { renderMarkdown } from '@grafana/data';
|
||||
import { renderMarkdown, LinkModelSupplier } from '@grafana/data';
|
||||
import { Tooltip, ScopedVars, PopoverContent } from '@grafana/ui';
|
||||
import { DataLink } from '@grafana/data';
|
||||
|
||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||
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';
|
||||
|
||||
enum InfoMode {
|
||||
@ -20,7 +18,7 @@ interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
scopedVars?: ScopedVars;
|
||||
links?: DataLink[];
|
||||
links?: LinkModelSupplier<PanelModel>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -45,22 +43,21 @@ export class PanelHeaderCorner extends Component<Props> {
|
||||
getInfoContent = (): JSX.Element => {
|
||||
const { panel } = this.props;
|
||||
const markdown = panel.description || '';
|
||||
const linkSrv = new LinkSrv(templateSrv, this.timeSrv);
|
||||
const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars);
|
||||
const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown);
|
||||
const links = this.props.links && this.props.links.getLinks(panel);
|
||||
|
||||
return (
|
||||
<div className="panel-info-content markdown-html">
|
||||
<div dangerouslySetInnerHTML={{ __html: markedInterpolatedMarkdown }} />
|
||||
|
||||
{panel.links && panel.links.length > 0 && (
|
||||
{links && links.length > 0 && (
|
||||
<ul className="panel-info-corner-links">
|
||||
{panel.links.map((link, idx) => {
|
||||
const info = linkSrv.getDataLinkUIModel(link, panel.scopedVars);
|
||||
{links.map((link, idx) => {
|
||||
return (
|
||||
<li key={idx}>
|
||||
<a className="panel-info-corner-links__item" href={info.href} target={info.target}>
|
||||
{info.title}
|
||||
<a className="panel-info-corner-links__item" href={link.href} target={link.target}>
|
||||
{link.title}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { DashboardModel } from '../state/DashboardModel';
|
||||
import { PanelModel } from '../state/PanelModel';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
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', () => ({}));
|
||||
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
MIN_PANEL_HEIGHT,
|
||||
DEFAULT_PANEL_SPAN,
|
||||
} from 'app/core/constants';
|
||||
import { DataLinkBuiltInVars } from 'app/features/panel/panellinks/link_srv';
|
||||
import { DataLinkBuiltInVars } from '@grafana/ui';
|
||||
|
||||
export class DashboardMigrator {
|
||||
dashboard: DashboardModel;
|
||||
|
@ -18,8 +18,8 @@ import {
|
||||
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { auto } from 'angular';
|
||||
import { TemplateSrv } from '../templating/template_srv';
|
||||
import { LinkSrv } from './panellinks/link_srv';
|
||||
import { PanelPluginMeta } from '@grafana/ui/src/types/panel';
|
||||
import { getPanelLinksSupplier } from './panellinks/linkSuppliers';
|
||||
|
||||
export class PanelCtrl {
|
||||
panel: any;
|
||||
@ -255,31 +255,31 @@ export class PanelCtrl {
|
||||
markdown = this.error || this.panel.description || '';
|
||||
}
|
||||
|
||||
const linkSrv: LinkSrv = this.$injector.get('linkSrv');
|
||||
const templateSrv: TemplateSrv = this.$injector.get('templateSrv');
|
||||
const interpolatedMarkdown = templateSrv.replace(markdown, this.panel.scopedVars);
|
||||
let html = '<div class="markdown-html panel-info-content">';
|
||||
|
||||
const md = renderMarkdown(interpolatedMarkdown);
|
||||
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">';
|
||||
for (const link of this.panel.links) {
|
||||
const info = linkSrv.getDataLinkUIModel(link, this.panel.scopedVars);
|
||||
for (const link of links) {
|
||||
html +=
|
||||
'<li><a class="panel-menu-link" href="' +
|
||||
escapeHtml(info.href) +
|
||||
escapeHtml(link.href) +
|
||||
'" target="' +
|
||||
escapeHtml(info.target) +
|
||||
escapeHtml(link.target) +
|
||||
'">' +
|
||||
escapeHtml(info.title) +
|
||||
escapeHtml(link.title) +
|
||||
'</a></li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
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 coreModule from 'app/core/core_module';
|
||||
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
||||
import { VariableSuggestion, ScopedVars, VariableOrigin } from '@grafana/ui';
|
||||
import { TimeSeriesValue, DateTime, dateTime, DataLink, KeyValue, deprecationWarning } from '@grafana/data';
|
||||
|
||||
export const DataLinkBuiltInVars = {
|
||||
keepTime: '__url_time_range',
|
||||
includeVars: '__all_variables',
|
||||
seriesName: '__series_name',
|
||||
valueTime: '__value_time',
|
||||
};
|
||||
import { VariableSuggestion, ScopedVars, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
|
||||
import { DataLink, KeyValue, deprecationWarning, LinkModel } from '@grafana/data';
|
||||
|
||||
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
|
||||
...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 {
|
||||
getDataLinkUIModel: (link: DataLink, scopedVars: ScopedVars, dataPoint?: LinkDataPoint) => LinkModel;
|
||||
getDataPointVars: (seriesName: string, dataPointTs: DateTime) => ScopedVars;
|
||||
getDataLinkUIModel: <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => LinkModel<T>;
|
||||
}
|
||||
|
||||
export class LinkSrv implements LinkService {
|
||||
@ -90,33 +78,20 @@ export class LinkSrv implements LinkService {
|
||||
return info;
|
||||
}
|
||||
|
||||
getDataPointVars = (seriesName: string, valueTime: DateTime) => {
|
||||
return {
|
||||
[DataLinkBuiltInVars.valueTime]: {
|
||||
text: valueTime.valueOf(),
|
||||
value: valueTime.valueOf(),
|
||||
},
|
||||
[DataLinkBuiltInVars.seriesName]: {
|
||||
text: seriesName,
|
||||
value: seriesName,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
getDataLinkUIModel = (link: DataLink, scopedVars: ScopedVars, dataPoint?: LinkDataPoint) => {
|
||||
getDataLinkUIModel = <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => {
|
||||
const params: KeyValue = {};
|
||||
const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl());
|
||||
|
||||
const info: LinkModel = {
|
||||
const info: LinkModel<T> = {
|
||||
href: link.url,
|
||||
title: this.templateSrv.replace(link.title || '', scopedVars),
|
||||
target: link.targetBlank ? '_blank' : '_self',
|
||||
origin,
|
||||
};
|
||||
|
||||
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
|
||||
|
||||
const variablesQuery = toUrlParams(params);
|
||||
|
||||
info.href = this.templateSrv.replace(link.url, {
|
||||
...scopedVars,
|
||||
[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;
|
||||
};
|
||||
|
||||
@ -146,7 +114,7 @@ export class LinkSrv implements LinkService {
|
||||
*/
|
||||
getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) {
|
||||
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 { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
@ -80,6 +81,7 @@ describe('linkSrv', () => {
|
||||
title: 'Any title',
|
||||
url: `/d/1?$${DataLinkBuiltInVars.keepTime}`,
|
||||
},
|
||||
{},
|
||||
{}
|
||||
).href
|
||||
).toEqual('/d/1?from=now-1h&to=now');
|
||||
@ -92,32 +94,43 @@ describe('linkSrv', () => {
|
||||
title: 'Any title',
|
||||
url: `/d/1?$${DataLinkBuiltInVars.includeVars}`,
|
||||
},
|
||||
{},
|
||||
{}
|
||||
).href
|
||||
).toEqual('/d/1?var-test1=val1&var-test2=val2');
|
||||
});
|
||||
|
||||
it('should interpolate series name from datapoint', () => {
|
||||
it('should interpolate series name', () => {
|
||||
expect(
|
||||
linkSrv.getDataLinkUIModel(
|
||||
{
|
||||
title: 'Any title',
|
||||
url: `/d/1?var-test=$${DataLinkBuiltInVars.seriesName}`,
|
||||
},
|
||||
{},
|
||||
dataPointMock
|
||||
{
|
||||
[DataLinkBuiltInVars.seriesName]: {
|
||||
value: 'A-series',
|
||||
text: 'A-series',
|
||||
},
|
||||
},
|
||||
{}
|
||||
).href
|
||||
).toEqual('/d/1?var-test=A-series');
|
||||
});
|
||||
it('should interpolate time range based on datapoint timestamp', () => {
|
||||
it('should interpolate value time', () => {
|
||||
expect(
|
||||
linkSrv.getDataLinkUIModel(
|
||||
{
|
||||
title: 'Any title',
|
||||
url: `/d/1?time=$${DataLinkBuiltInVars.valueTime}`,
|
||||
},
|
||||
{},
|
||||
dataPointMock
|
||||
{
|
||||
[DataLinkBuiltInVars.valueTime]: {
|
||||
value: dataPointMock.datapoint[0],
|
||||
text: dataPointMock.datapoint[0],
|
||||
},
|
||||
},
|
||||
{}
|
||||
).href
|
||||
).toEqual('/d/1?time=1000000001');
|
||||
});
|
||||
|
@ -5,11 +5,12 @@ import React, { PureComponent } from 'react';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay } from '@grafana/ui';
|
||||
import { BarGauge, VizRepeater, getFieldDisplayValues, FieldDisplay, DataLinksContextMenu } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { BarGaugeOptions } from './types';
|
||||
import { PanelProps } from '@grafana/ui';
|
||||
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
|
||||
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
||||
@ -17,18 +18,26 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
|
||||
const { field, display } = value;
|
||||
|
||||
return (
|
||||
<BarGauge
|
||||
value={display}
|
||||
width={width}
|
||||
height={height}
|
||||
orientation={options.orientation}
|
||||
thresholds={field.thresholds}
|
||||
theme={config.theme}
|
||||
itemSpacing={this.getItemSpacing()}
|
||||
displayMode={options.displayMode}
|
||||
minValue={field.min}
|
||||
maxValue={field.max}
|
||||
/>
|
||||
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
|
||||
{({ openMenu, targetClassName }) => {
|
||||
return (
|
||||
<BarGauge
|
||||
value={display}
|
||||
width={width}
|
||||
height={height}
|
||||
orientation={options.orientation}
|
||||
thresholds={field.thresholds}
|
||||
theme={config.theme}
|
||||
itemSpacing={this.getItemSpacing()}
|
||||
displayMode={options.displayMode}
|
||||
minValue={field.min}
|
||||
maxValue={field.max}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -12,11 +12,16 @@ import {
|
||||
FormLabel,
|
||||
PanelEditorProps,
|
||||
Select,
|
||||
DataLinksEditor,
|
||||
} from '@grafana/ui';
|
||||
import { FieldConfig } from '@grafana/data';
|
||||
import { FieldConfig, DataLink } from '@grafana/data';
|
||||
|
||||
import { Threshold, ValueMapping } from '@grafana/data';
|
||||
import { BarGaugeOptions, orientationOptions, displayModes } from './types';
|
||||
import {
|
||||
getDataLinksVariableSuggestions,
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGaugeOptions>> {
|
||||
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 });
|
||||
onDisplayModeChange = ({ value }: any) => this.props.onOptionsChange({ ...this.props.options, displayMode: value });
|
||||
|
||||
onDataLinksChanged = (links: DataLink[]) => {
|
||||
this.onDefaultsChange({
|
||||
...this.props.options.fieldOptions.defaults,
|
||||
links,
|
||||
});
|
||||
};
|
||||
render() {
|
||||
const { options } = this.props;
|
||||
const { fieldOptions } = options;
|
||||
const { defaults } = fieldOptions;
|
||||
|
||||
const suggestions = fieldOptions.values
|
||||
? getDataLinksVariableSuggestions()
|
||||
: getCalculationValueDataLinksVariableSuggestions();
|
||||
const labelWidth = 6;
|
||||
|
||||
return (
|
||||
@ -92,6 +106,15 @@ export class BarGaugePanelEditor extends PureComponent<PanelEditorProps<BarGauge
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<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';
|
||||
|
||||
// Components
|
||||
import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation } from '@grafana/ui';
|
||||
import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation, DataLinksContextMenu } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { GaugeOptions } from './types';
|
||||
import { PanelProps, VizRepeater } from '@grafana/ui';
|
||||
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
|
||||
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
renderValue = (value: FieldDisplay, width: number, height: number): JSX.Element => {
|
||||
@ -17,17 +18,25 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
const { field, display } = value;
|
||||
|
||||
return (
|
||||
<Gauge
|
||||
value={display}
|
||||
width={width}
|
||||
height={height}
|
||||
thresholds={field.thresholds}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={field.min}
|
||||
maxValue={field.max}
|
||||
theme={config.theme}
|
||||
/>
|
||||
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
|
||||
{({ openMenu, targetClassName }) => {
|
||||
return (
|
||||
<Gauge
|
||||
value={display}
|
||||
width={width}
|
||||
height={height}
|
||||
thresholds={field.thresholds}
|
||||
showThresholdLabels={options.showThresholdLabels}
|
||||
showThresholdMarkers={options.showThresholdMarkers}
|
||||
minValue={field.min}
|
||||
maxValue={field.max}
|
||||
theme={config.theme}
|
||||
onClick={openMenu}
|
||||
className={targetClassName}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -10,10 +10,15 @@ import {
|
||||
FieldPropertiesEditor,
|
||||
Switch,
|
||||
PanelOptionsGroup,
|
||||
DataLinksEditor,
|
||||
} from '@grafana/ui';
|
||||
import { Threshold, ValueMapping, FieldConfig } from '@grafana/data';
|
||||
import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
|
||||
|
||||
import { GaugeOptions } from './types';
|
||||
import {
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
getDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
|
||||
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() {
|
||||
const { options } = this.props;
|
||||
const { fieldOptions, showThresholdLabels, showThresholdMarkers } = options;
|
||||
const { defaults } = fieldOptions;
|
||||
const suggestions = fieldOptions.values
|
||||
? getDataLinksVariableSuggestions()
|
||||
: getCalculationValueDataLinksVariableSuggestions();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -92,6 +107,15 @@ export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOption
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<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 { 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 { DataLink, toUtc } from '@grafana/data';
|
||||
import { GraphContextMenuCtrl, FlotDataPoint } from './GraphContextMenuCtrl';
|
||||
@ -196,10 +196,15 @@ class GraphElement {
|
||||
{
|
||||
items: [
|
||||
...dataLinks.map<ContextMenuItem>(link => {
|
||||
const linkUiModel = this.linkSrv.getDataLinkUIModel(link, this.panel.scopedVars, {
|
||||
seriesName: item.series.alias,
|
||||
datapoint: item.datapoint,
|
||||
});
|
||||
const linkUiModel = this.linkSrv.getDataLinkUIModel(
|
||||
link,
|
||||
{
|
||||
...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 {
|
||||
label: linkUiModel.title,
|
||||
url: linkUiModel.href,
|
||||
|
@ -24,9 +24,10 @@ import {
|
||||
DisplayValue,
|
||||
fieldReducers,
|
||||
KeyValue,
|
||||
LinkModel,
|
||||
} from '@grafana/data';
|
||||
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 { getProcessedDataFrames } from 'app/features/dashboard/state/PanelQueryState';
|
||||
|
||||
@ -328,7 +329,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
const $sanitize = this.$sanitize;
|
||||
const panel = ctrl.panel;
|
||||
const templateSrv = this.templateSrv;
|
||||
let linkInfo: LinkModel | null = null;
|
||||
let linkInfo: LinkModel<any> | null = null;
|
||||
const $panelContainer = elem.find('.panel-container');
|
||||
elem = elem.find('.singlestat-panel');
|
||||
|
||||
@ -592,7 +593,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
|
||||
elem.toggleClass('pointer', 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 {
|
||||
linkInfo = null;
|
||||
}
|
||||
|
@ -9,13 +9,18 @@ import {
|
||||
FieldDisplayEditor,
|
||||
FieldPropertiesEditor,
|
||||
PanelOptionsGroup,
|
||||
DataLinksEditor,
|
||||
} from '@grafana/ui';
|
||||
import { Threshold, ValueMapping, FieldConfig } from '@grafana/data';
|
||||
import { Threshold, ValueMapping, FieldConfig, DataLink } from '@grafana/data';
|
||||
|
||||
import { SingleStatOptions, SparklineOptions } from './types';
|
||||
import { ColoringEditor } from './ColoringEditor';
|
||||
import { FontSizeEditor } from './FontSizeEditor';
|
||||
import { SparklineEditor } from './SparklineEditor';
|
||||
import {
|
||||
getDataLinksVariableSuggestions,
|
||||
getCalculationValueDataLinksVariableSuggestions,
|
||||
} from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatOptions>> {
|
||||
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() {
|
||||
const { options } = this.props;
|
||||
const { fieldOptions } = options;
|
||||
const { defaults } = fieldOptions;
|
||||
const suggestions = fieldOptions.values
|
||||
? getDataLinksVariableSuggestions()
|
||||
: getCalculationValueDataLinksVariableSuggestions();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -77,6 +92,15 @@ export class SingleStatEditor extends PureComponent<PanelEditorProps<SingleStatO
|
||||
</PanelOptionsGrid>
|
||||
|
||||
<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
|
||||
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 { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
|
||||
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
|
||||
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[] => {
|
||||
|
Loading…
Reference in New Issue
Block a user