mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Element data links refactor (#90636)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
@@ -190,12 +190,12 @@ You can configure a canvas data link to open with a single click on the element.
|
|||||||
1. Click the element to which you want to add the data link.
|
1. Click the element to which you want to add the data link.
|
||||||
1. In either the inline editor or panel editor, expand the **Selected element** editor.
|
1. In either the inline editor or panel editor, expand the **Selected element** editor.
|
||||||
1. Scroll down to the **Data links** section and expand it.
|
1. Scroll down to the **Data links** section and expand it.
|
||||||
1. Toggle the **One-click** switch in the element's data links section.
|
1. In the **One-click** section, choose **Link**.
|
||||||
1. Disable inline editing.
|
1. Disable inline editing.
|
||||||
|
|
||||||
The first data link in the list will be configured as your one-click data link. If you want to change the one-click data link, simply drag the desired data link to the top of the list.
|
The first data link in the list will be configured as your one-click data link. If you want to change the one-click data link, simply drag the desired data link to the top of the list.
|
||||||
|
|
||||||
{{< video-embed src="/media/docs/grafana/panels-visualizations/canvas-one-click-data-link.mp4" >}}
|
{{< video-embed src="/media/docs/grafana/panels-visualizations/canvas-one-click-datalink-.mp4" >}}
|
||||||
|
|
||||||
## Panel options
|
## Panel options
|
||||||
|
|
||||||
|
|||||||
@@ -479,7 +479,6 @@ export const getLinksSupplier =
|
|||||||
href,
|
href,
|
||||||
title: replaceVariables(link.title || '', dataLinkScopedVars),
|
title: replaceVariables(link.title || '', dataLinkScopedVars),
|
||||||
target: link.targetBlank ? '_blank' : undefined,
|
target: link.targetBlank ? '_blank' : undefined,
|
||||||
sortIndex: link.sortIndex,
|
|
||||||
onClick: (evt: MouseEvent, origin: Field) => {
|
onClick: (evt: MouseEvent, origin: Field) => {
|
||||||
link.onClick!({
|
link.onClick!({
|
||||||
origin: origin ?? field,
|
origin: origin ?? field,
|
||||||
@@ -495,7 +494,6 @@ export const getLinksSupplier =
|
|||||||
title: replaceVariables(link.title || '', dataLinkScopedVars),
|
title: replaceVariables(link.title || '', dataLinkScopedVars),
|
||||||
target: link.targetBlank ? '_blank' : undefined,
|
target: link.targetBlank ? '_blank' : undefined,
|
||||||
origin: field,
|
origin: field,
|
||||||
sortIndex: link.sortIndex,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -793,6 +793,7 @@ export {
|
|||||||
VariableOrigin,
|
VariableOrigin,
|
||||||
type VariableSuggestion,
|
type VariableSuggestion,
|
||||||
VariableSuggestionsScope,
|
VariableSuggestionsScope,
|
||||||
|
OneClickMode,
|
||||||
} from './types/dataLink';
|
} from './types/dataLink';
|
||||||
export { DataFrameType } from './types/dataFrameTypes';
|
export { DataFrameType } from './types/dataFrameTypes';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export interface LinkModel<T = any> {
|
|||||||
|
|
||||||
// When a click callback exists, this is passed the raw mouse|react event
|
// When a click callback exists, this is passed the raw mouse|react event
|
||||||
onClick?: (e: any, origin?: any) => void;
|
onClick?: (e: any, origin?: any) => void;
|
||||||
sortIndex?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,3 +129,8 @@ export interface VariableSuggestion {
|
|||||||
export enum VariableSuggestionsScope {
|
export enum VariableSuggestionsScope {
|
||||||
Values = 'values',
|
Values = 'values',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OneClickMode {
|
||||||
|
Link = 'link',
|
||||||
|
Off = 'off',
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ export interface CanvasElementOptions {
|
|||||||
connections?: Array<CanvasConnection>;
|
connections?: Array<CanvasConnection>;
|
||||||
constraint?: Constraint;
|
constraint?: Constraint;
|
||||||
name: string;
|
name: string;
|
||||||
oneClickLinks?: boolean;
|
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface DataLinksInlineEditorProps {
|
|||||||
onChange: (links: DataLink[]) => void;
|
onChange: (links: DataLink[]) => void;
|
||||||
getSuggestions: () => VariableSuggestion[];
|
getSuggestions: () => VariableSuggestion[];
|
||||||
data: DataFrame[];
|
data: DataFrame[];
|
||||||
oneClickEnabled?: boolean;
|
showOneClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataLinksInlineEditor = ({
|
export const DataLinksInlineEditor = ({
|
||||||
@@ -25,13 +25,12 @@ export const DataLinksInlineEditor = ({
|
|||||||
onChange,
|
onChange,
|
||||||
getSuggestions,
|
getSuggestions,
|
||||||
data,
|
data,
|
||||||
oneClickEnabled = false,
|
showOneClick = false,
|
||||||
}: DataLinksInlineEditorProps) => {
|
}: DataLinksInlineEditorProps) => {
|
||||||
const [editIndex, setEditIndex] = useState<number | null>(null);
|
const [editIndex, setEditIndex] = useState<number | null>(null);
|
||||||
const [isNew, setIsNew] = useState(false);
|
const [isNew, setIsNew] = useState(false);
|
||||||
|
|
||||||
const [linksSafe, setLinksSafe] = useState<DataLink[]>([]);
|
const [linksSafe, setLinksSafe] = useState<DataLink[]>([]);
|
||||||
links?.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0));
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLinksSafe(links ?? []);
|
setLinksSafe(links ?? []);
|
||||||
@@ -81,24 +80,20 @@ export const DataLinksInlineEditor = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const copy = [...linksSafe];
|
const update = cloneDeep(linksSafe);
|
||||||
const link = copy[result.source.index];
|
const link = update[result.source.index];
|
||||||
link.sortIndex = result.destination.index;
|
|
||||||
|
|
||||||
const swapLink = copy[result.destination.index];
|
update.splice(result.source.index, 1);
|
||||||
swapLink.sortIndex = result.source.index;
|
update.splice(result.destination.index, 0, link);
|
||||||
|
|
||||||
copy.splice(result.source.index, 1);
|
setLinksSafe(update);
|
||||||
copy.splice(result.destination.index, 0, link);
|
onChange(update);
|
||||||
|
|
||||||
setLinksSafe(copy);
|
|
||||||
onChange(linksSafe);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFirstLink = (linkJSX: ReactNode) => {
|
const renderFirstLink = (linkJSX: ReactNode, key: string) => {
|
||||||
if (oneClickEnabled) {
|
if (showOneClick) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.oneClickOverlay}>
|
<div className={styles.oneClickOverlay} key={key}>
|
||||||
<span className={styles.oneClickSpan}>One-click</span>
|
<span className={styles.oneClickSpan}>One-click</span>
|
||||||
{linkJSX}
|
{linkJSX}
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +125,7 @@ export const DataLinksInlineEditor = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
return renderFirstLink(linkJSX);
|
return renderFirstLink(linkJSX, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return linkJSX;
|
return linkJSX;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
import { DataLink, RegistryItem } from '@grafana/data';
|
import { DataLink, RegistryItem, OneClickMode } from '@grafana/data';
|
||||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema';
|
import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
@@ -33,7 +33,7 @@ export interface CanvasElementOptions<TConfig = any> {
|
|||||||
border?: LineConfig;
|
border?: LineConfig;
|
||||||
connections?: CanvasConnection[];
|
connections?: CanvasConnection[];
|
||||||
links?: DataLink[];
|
links?: DataLink[];
|
||||||
oneClickLinks?: boolean;
|
oneClickMode?: OneClickMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unit is percentage from the middle of the element
|
// Unit is percentage from the middle of the element
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||||
@@ -99,6 +99,7 @@ export const cloudItem: CanvasElementItem = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
@@ -94,6 +94,8 @@ export const droneFrontItem: CanvasElementItem = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Called when data changes
|
// Called when data changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
@@ -93,6 +93,8 @@ export const droneSideItem: CanvasElementItem = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Called when data changes
|
// Called when data changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
@@ -98,6 +98,8 @@ export const droneTopItem: CanvasElementItem = {
|
|||||||
fixed: 'transparent',
|
fixed: 'transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Called when data changes
|
// Called when data changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { DimensionContext } from 'app/features/dimensions/context';
|
import { DimensionContext } from 'app/features/dimensions/context';
|
||||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||||
@@ -106,6 +106,7 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
import { LinkModel } from '@grafana/data';
|
import { LinkModel, OneClickMode } from '@grafana/data';
|
||||||
import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema';
|
import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema';
|
||||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||||
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
|
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
|
||||||
@@ -80,6 +80,7 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
|||||||
left: options?.placement?.left ?? 100,
|
left: options?.placement?.left ?? 100,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { useCallback } from 'react';
|
|||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
FieldNamePickerConfigSettings,
|
||||||
|
GrafanaTheme2,
|
||||||
|
OneClickMode,
|
||||||
|
StandardEditorsRegistryItem,
|
||||||
|
} from '@grafana/data';
|
||||||
import { TextDimensionMode } from '@grafana/schema';
|
import { TextDimensionMode } from '@grafana/schema';
|
||||||
import { usePanelContext, useStyles2 } from '@grafana/ui';
|
import { usePanelContext, useStyles2 } from '@grafana/ui';
|
||||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||||
@@ -175,6 +181,7 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
|
|||||||
left: options?.placement?.left ?? 100,
|
left: options?.placement?.left ?? 100,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||||
@@ -99,6 +99,7 @@ export const parallelogramItem: CanvasElementItem = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { stylesFactory } from '@grafana/ui';
|
import { stylesFactory } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { DimensionContext } from 'app/features/dimensions/context';
|
import { DimensionContext } from 'app/features/dimensions/context';
|
||||||
@@ -72,6 +72,7 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
|
|||||||
fixed: defaultBgColor,
|
fixed: defaultBgColor,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
import { GrafanaTheme2, LinkModel, OneClickMode } from '@grafana/data';
|
||||||
import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema';
|
import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
@@ -83,6 +83,7 @@ export const serverItem: CanvasElementItem<ServerConfig, ServerData> = {
|
|||||||
config: {
|
config: {
|
||||||
type: ServerType.Single,
|
type: ServerType.Single,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
import { DataFrame, GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
|
import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||||
import { DimensionContext } from 'app/features/dimensions/context';
|
import { DimensionContext } from 'app/features/dimensions/context';
|
||||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||||
@@ -148,6 +148,7 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||||
@@ -100,6 +100,7 @@ export const triangleItem: CanvasElementItem = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
import { GrafanaTheme2, LinkModel, OneClickMode } from '@grafana/data';
|
||||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
@@ -90,6 +90,7 @@ export const windTurbineItem: CanvasElementItem = {
|
|||||||
left: options?.placement?.left,
|
left: options?.placement?.left,
|
||||||
rotation: options?.placement?.rotation ?? 0,
|
rotation: options?.placement?.rotation ?? 0,
|
||||||
},
|
},
|
||||||
|
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
|
||||||
links: options?.links ?? [],
|
links: options?.links ?? [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CSSProperties } from 'react';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
import { OnDrag, OnResize, OnRotate } from 'react-moveable/declaration/types';
|
import { OnDrag, OnResize, OnRotate } from 'react-moveable/declaration/types';
|
||||||
|
|
||||||
import { FieldType, getLinksSupplier, LinkModel, ValueLinkConfig } from '@grafana/data';
|
import { FieldType, getLinksSupplier, LinkModel, OneClickMode, ValueLinkConfig } from '@grafana/data';
|
||||||
import { LayerElement } from 'app/core/components/Layers/types';
|
import { LayerElement } from 'app/core/components/Layers/types';
|
||||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
@@ -62,6 +62,7 @@ export class ElementState implements LayerElement {
|
|||||||
options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0, rotation: 0 };
|
options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0, rotation: 0 };
|
||||||
options.background = options.background ?? { color: { fixed: 'transparent' } };
|
options.background = options.background ?? { color: { fixed: 'transparent' } };
|
||||||
options.border = options.border ?? { color: { fixed: 'dark-green' } };
|
options.border = options.border ?? { color: { fixed: 'dark-green' } };
|
||||||
|
options.oneClickMode = options.oneClickMode ?? OneClickMode.Off;
|
||||||
const scene = this.getScene();
|
const scene = this.getScene();
|
||||||
if (!options.name) {
|
if (!options.name) {
|
||||||
const newName = scene?.getNextElementName();
|
const newName = scene?.getNextElementName();
|
||||||
@@ -584,13 +585,17 @@ export class ElementState implements LayerElement {
|
|||||||
handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => {
|
handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => {
|
||||||
const scene = this.getScene();
|
const scene = this.getScene();
|
||||||
|
|
||||||
if (!scene?.isEditingEnabled && !scene?.tooltip?.isOpen && !this.options.oneClickLinks) {
|
const shouldHandleTooltip =
|
||||||
|
!scene?.isEditingEnabled && !scene?.tooltip?.isOpen && this.options.oneClickMode === OneClickMode.Off;
|
||||||
|
if (shouldHandleTooltip) {
|
||||||
this.handleTooltip(event);
|
this.handleTooltip(event);
|
||||||
} else if (!isSelected) {
|
} else if (!isSelected) {
|
||||||
scene?.connections.handleMouseEnter(event);
|
scene?.connections.handleMouseEnter(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.oneClickLinks && this.div && this.options.links && this.options.links.length > 0) {
|
const shouldHandleOneClickLink =
|
||||||
|
this.options.oneClickMode === OneClickMode.Link && this.options.links && this.options.links.length > 0;
|
||||||
|
if (shouldHandleOneClickLink && this.div) {
|
||||||
const primaryDataLink = this.getPrimaryDataLink();
|
const primaryDataLink = this.getPrimaryDataLink();
|
||||||
if (primaryDataLink) {
|
if (primaryDataLink) {
|
||||||
this.div.style.cursor = 'pointer';
|
this.div.style.cursor = 'pointer';
|
||||||
@@ -602,8 +607,7 @@ export class ElementState implements LayerElement {
|
|||||||
getPrimaryDataLink = () => {
|
getPrimaryDataLink = () => {
|
||||||
if (this.getLinks) {
|
if (this.getLinks) {
|
||||||
const links = this.getLinks({ valueRowIndex: getRowIndex(this.data.field, this.getScene()!) });
|
const links = this.getLinks({ valueRowIndex: getRowIndex(this.data.field, this.getScene()!) });
|
||||||
const primaryDataLink = links.find((link: LinkModel) => link.sortIndex === 0);
|
return links[0];
|
||||||
return primaryDataLink ?? links[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -623,11 +627,11 @@ export class ElementState implements LayerElement {
|
|||||||
|
|
||||||
handleMouseLeave = (event: React.MouseEvent) => {
|
handleMouseLeave = (event: React.MouseEvent) => {
|
||||||
const scene = this.getScene();
|
const scene = this.getScene();
|
||||||
if (scene?.tooltipCallback && !scene?.tooltip?.isOpen && !this.options.oneClickLinks) {
|
if (scene?.tooltipCallback && !scene?.tooltip?.isOpen && this.options.oneClickMode === OneClickMode.Off) {
|
||||||
scene.tooltipCallback(undefined);
|
scene.tooltipCallback(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.oneClickLinks && this.div) {
|
if (this.options.oneClickMode !== OneClickMode.Off && this.div) {
|
||||||
this.div.style.cursor = 'auto';
|
this.div.style.cursor = 'auto';
|
||||||
this.div.title = '';
|
this.div.title = '';
|
||||||
}
|
}
|
||||||
@@ -635,7 +639,7 @@ export class ElementState implements LayerElement {
|
|||||||
|
|
||||||
onElementClick = (event: React.MouseEvent) => {
|
onElementClick = (event: React.MouseEvent) => {
|
||||||
// If one-click access is enabled, open the primary link
|
// If one-click access is enabled, open the primary link
|
||||||
if (this.options.oneClickLinks) {
|
if (this.options.oneClickMode === OneClickMode.Link) {
|
||||||
let primaryDataLink = this.getPrimaryDataLink();
|
let primaryDataLink = this.getPrimaryDataLink();
|
||||||
if (primaryDataLink) {
|
if (primaryDataLink) {
|
||||||
window.open(primaryDataLink.href, primaryDataLink.target);
|
window.open(primaryDataLink.href, primaryDataLink.target);
|
||||||
|
|||||||
@@ -83,9 +83,6 @@ export const CanvasTooltip = ({ scene }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort element data links
|
|
||||||
links.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{scene.tooltip?.element && scene.tooltip.anchorPoint && (
|
{scene.tooltip?.element && scene.tooltip.anchorPoint && (
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { StandardEditorProps, DataLink, VariableSuggestionsScope } from '@grafana/data';
|
import { StandardEditorProps, DataLink, VariableSuggestionsScope, OneClickMode } from '@grafana/data';
|
||||||
import { DataLinksInlineEditor } from '@grafana/ui';
|
import { DataLinksInlineEditor } from '@grafana/ui';
|
||||||
|
import { CanvasElementOptions } from 'app/features/canvas/element';
|
||||||
import { CanvasElementOptions } from '../../panelcfg.gen';
|
|
||||||
|
|
||||||
type Props = StandardEditorProps<DataLink[], CanvasElementOptions>;
|
type Props = StandardEditorProps<DataLink[], CanvasElementOptions>;
|
||||||
|
|
||||||
export function DataLinksEditor({ value, onChange, item, context }: Props) {
|
export function DataLinksEditor({ value, onChange, item, context }: Props) {
|
||||||
if (!value) {
|
const oneClickMode = item.settings?.oneClickMode;
|
||||||
value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = item.settings;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataLinksInlineEditor
|
<DataLinksInlineEditor
|
||||||
@@ -18,7 +13,7 @@ export function DataLinksEditor({ value, onChange, item, context }: Props) {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])}
|
getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])}
|
||||||
data={[]}
|
data={[]}
|
||||||
oneClickEnabled={settings?.oneClickLinks}
|
showOneClick={oneClickMode === OneClickMode.Link}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { get as lodashGet } from 'lodash';
|
import { capitalize, get as lodashGet } from 'lodash';
|
||||||
|
|
||||||
|
import { OneClickMode } from '@grafana/data';
|
||||||
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||||
import { CanvasElementOptions } from 'app/features/canvas/element';
|
import { CanvasElementOptions } from 'app/features/canvas/element';
|
||||||
import {
|
import {
|
||||||
@@ -119,6 +120,20 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
|||||||
optionBuilder.addBorder(builder, ctx);
|
optionBuilder.addBorder(builder, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.addRadio({
|
||||||
|
category: ['Data links'],
|
||||||
|
path: 'oneClickMode',
|
||||||
|
name: 'One-click',
|
||||||
|
description: 'When enabled, a single click opens the first link',
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ value: OneClickMode.Off, label: capitalize(OneClickMode.Off) },
|
||||||
|
{ value: OneClickMode.Link, label: capitalize(OneClickMode.Link) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: OneClickMode.Off,
|
||||||
|
});
|
||||||
|
|
||||||
optionBuilder.addDataLinks(builder, ctx);
|
optionBuilder.addDataLinks(builder, ctx);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { capitalize } from 'lodash';
|
import { capitalize } from 'lodash';
|
||||||
|
|
||||||
import { FieldType, standardEditorsRegistry } from '@grafana/data';
|
import { FieldType } from '@grafana/data';
|
||||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
import { ConnectionDirection } from 'app/features/canvas/element';
|
import { ConnectionDirection } from 'app/features/canvas/element';
|
||||||
import { SVGElements } from 'app/features/canvas/runtime/element';
|
import { SVGElements } from 'app/features/canvas/runtime/element';
|
||||||
@@ -209,23 +209,13 @@ export const optionBuilder: OptionSuppliers = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
addDataLinks: (builder, context) => {
|
addDataLinks: (builder, context) => {
|
||||||
const category = ['Data links'];
|
builder.addCustomEditor({
|
||||||
builder
|
category: ['Data links'],
|
||||||
.addCustomEditor({
|
id: 'dataLinks',
|
||||||
category,
|
path: 'links',
|
||||||
id: 'enableOneClick',
|
name: 'Links',
|
||||||
path: 'oneClickLinks',
|
editor: DataLinksEditor,
|
||||||
name: 'One-click',
|
settings: context.options,
|
||||||
description: 'When enabled, the top link in the list below works with a single click',
|
});
|
||||||
editor: standardEditorsRegistry.get('boolean').editor,
|
|
||||||
})
|
|
||||||
.addCustomEditor({
|
|
||||||
category,
|
|
||||||
id: 'dataLinks',
|
|
||||||
path: 'links',
|
|
||||||
name: '',
|
|
||||||
editor: DataLinksEditor,
|
|
||||||
settings: context.options,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DataLink, DynamicConfigValue, FieldMatcherID, PanelModel } from '@grafana/data';
|
import { DataLink, DynamicConfigValue, FieldMatcherID, PanelModel, OneClickMode } from '@grafana/data';
|
||||||
import { CanvasElementOptions } from 'app/features/canvas/element';
|
import { CanvasElementOptions } from 'app/features/canvas/element';
|
||||||
|
|
||||||
import { Options } from './panelcfg.gen';
|
import { Options } from './panelcfg.gen';
|
||||||
@@ -43,7 +43,8 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseFloat(pluginVersion) <= 11.2) {
|
if (parseFloat(pluginVersion) <= 11.3) {
|
||||||
|
// migrate links from field name overrides to elements
|
||||||
for (let idx = 0; idx < panel.fieldConfig.overrides.length; idx++) {
|
for (let idx = 0; idx < panel.fieldConfig.overrides.length; idx++) {
|
||||||
const override = panel.fieldConfig.overrides[idx];
|
const override = panel.fieldConfig.overrides[idx];
|
||||||
|
|
||||||
@@ -66,6 +67,17 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrate oneClickLinks to oneClickMode
|
||||||
|
const root = panel.options?.root;
|
||||||
|
if (root?.elements) {
|
||||||
|
for (const element of root.elements) {
|
||||||
|
if (element.oneClickLinks) {
|
||||||
|
element.oneClickMode = OneClickMode.Link;
|
||||||
|
delete element.oneClickLinks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return panel.options;
|
return panel.options;
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ composableKinds: PanelCfg: {
|
|||||||
background?: BackgroundConfig
|
background?: BackgroundConfig
|
||||||
border?: LineConfig
|
border?: LineConfig
|
||||||
connections?: [...CanvasConnection]
|
connections?: [...CanvasConnection]
|
||||||
oneClickLinks?: bool
|
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
Options: {
|
Options: {
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export interface CanvasElementOptions {
|
|||||||
connections?: Array<CanvasConnection>;
|
connections?: Array<CanvasConnection>;
|
||||||
constraint?: Constraint;
|
constraint?: Constraint;
|
||||||
name: string;
|
name: string;
|
||||||
oneClickLinks?: boolean;
|
|
||||||
placement?: Placement;
|
placement?: Placement;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user