Canvas: One click links and actions (#99616)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2025-02-13 11:46:29 -06:00 committed by GitHub
parent 02118cc6aa
commit 5aeaa18ac2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 218 additions and 157 deletions

View File

@ -1,11 +1,13 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data';
import { ActionModel, Field, GrafanaTheme2, LinkModel, ThemeSpacingTokens } from '@grafana/data';
import { Button, DataLinkButton, Icon, Stack } from '..';
import { useStyles2 } from '../../themes';
import { Trans } from '../../utils/i18n';
import { ActionButton } from '../Actions/ActionButton';
import { ResponsiveProp } from '../Layout/utils/responsiveness';
interface VizTooltipFooterProps {
dataLinks: Array<LinkModel<Field>>;
@ -15,50 +17,75 @@ interface VizTooltipFooterProps {
export const ADD_ANNOTATION_ID = 'add-annotation-button';
const renderDataLinks = (dataLinks: LinkModel[], styles: ReturnType<typeof getStyles>) => {
const oneClickLink = dataLinks.find((link) => link.oneClick === true);
type RenderOneClickTrans = (title: string) => React.ReactNode;
type RenderItem<T extends LinkModel | ActionModel> = (
item: T,
idx: number,
styles: ReturnType<typeof getStyles>
) => React.ReactNode;
function makeRenderLinksOrActions<T extends LinkModel | ActionModel>(
renderOneClickTrans: RenderOneClickTrans,
renderItem: RenderItem<T>,
itemGap?: ResponsiveProp<ThemeSpacingTokens>
) {
const renderLinksOrActions = (items: T[], styles: ReturnType<typeof getStyles>) => {
if (items.length === 0) {
return;
}
const oneClickItem = items.find((item) => item.oneClick === true);
if (oneClickItem != null) {
return (
<div className={styles.dataLinks}>
<Stack direction="column" justifyContent="flex-start" gap={0.5}>
<span className={styles.oneClickWrapper}>
<Icon name="info-circle" size="lg" className={styles.infoIcon} />
{renderOneClickTrans(oneClickItem.title)}
</span>
</Stack>
</div>
);
}
if (oneClickLink != null) {
return (
<Stack direction="column" justifyContent="flex-start" gap={0.5}>
<span className={styles.oneClickWrapper}>
<Icon name="info-circle" size="lg" className={styles.infoIcon} />
<Trans i18nKey="grafana-ui.viz-tooltip.footer-click-to-navigate">
Click to open {{ linkTitle: oneClickLink.title }}
</Trans>
</span>
</Stack>
<div className={styles.dataLinks}>
<Stack direction="column" justifyContent="flex-start" gap={itemGap}>
{items.map((item, i) => renderItem(item, i, styles))}
</Stack>
</div>
);
}
};
return (
<Stack direction="column" justifyContent="flex-start" gap={0.5}>
{dataLinks.map((link, i) => (
<DataLinkButton link={link} key={i} buttonProps={{ className: styles.dataLinkButton, fill: 'text' }} />
))}
</Stack>
);
};
return renderLinksOrActions;
}
const renderActions = (actions: ActionModel[]) => {
return (
<Stack direction="column" justifyContent="flex-start">
{actions.map((action, i) => (
<ActionButton key={i} action={action} variant="secondary" />
))}
</Stack>
);
};
const renderDataLinks = makeRenderLinksOrActions<LinkModel>(
(title) => (
<Trans i18nKey="grafana-ui.viz-tooltip.footer-click-to-navigate">Click to open {{ linkTitle: title }}</Trans>
),
(item, i, styles) => (
<DataLinkButton link={item} key={i} buttonProps={{ className: styles.dataLinkButton, fill: 'text' }} />
),
0.5
);
const renderActions = makeRenderLinksOrActions<ActionModel>(
(title) => <Trans i18nKey="grafana-ui.viz-tooltip.footer-click-to-action">Click to {{ actionTitle: title }}</Trans>,
(item, i, styles) => <ActionButton key={i} action={item} variant="secondary" />
);
export const VizTooltipFooter = ({ dataLinks, actions = [], annotate }: VizTooltipFooterProps) => {
const styles = useStyles2(getStyles);
const hasOneClickLink = dataLinks.some((link) => link.oneClick === true);
const hasOneClickLink = useMemo(() => dataLinks.some((link) => link.oneClick === true), [dataLinks]);
const hasOneClickAction = useMemo(() => actions.some((action) => action.oneClick === true), [actions]);
return (
<div className={styles.wrapper}>
{dataLinks.length > 0 && <div className={styles.dataLinks}>{renderDataLinks(dataLinks, styles)}</div>}
{!hasOneClickLink && actions.length > 0 && <div className={styles.dataLinks}>{renderActions(actions)}</div>}
{!hasOneClickLink && annotate != null && (
{!hasOneClickAction && renderDataLinks(dataLinks, styles)}
{!hasOneClickLink && renderActions(actions, styles)}
{!hasOneClickLink && !hasOneClickAction && annotate != null && (
<div className={styles.addAnnotations}>
<Button icon="comment-alt" variant="secondary" size="sm" id={ADD_ANNOTATION_ID} onClick={annotate}>
<Trans i18nKey="grafana-ui.viz-tooltip.footer-add-annotation">Add annotation</Trans>

View File

@ -62,6 +62,7 @@ export const getActions = (
onClick: (evt: MouseEvent, origin: Field) => {
buildActionOnClick(action, boundReplaceVariables);
},
oneClick: action.oneClick ?? false,
};
return actionModel;

View File

@ -1,6 +1,6 @@
import { ComponentType } from 'react';
import { DataLink, RegistryItem, OneClickMode, Action } from '@grafana/data';
import { DataLink, RegistryItem, Action } from '@grafana/data';
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
import { ColorDimensionConfig, ScaleDimensionConfig } from '@grafana/schema';
import { config } from 'app/core/config';
@ -34,7 +34,6 @@ export interface CanvasElementOptions<TConfig = any> {
connections?: CanvasConnection[];
links?: DataLink[];
actions?: Action[];
oneClickMode?: OneClickMode;
}
// Unit is percentage from the middle of the element

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -99,7 +99,6 @@ export const cloudItem: CanvasElementItem = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
@ -94,7 +94,6 @@ export const droneFrontItem: CanvasElementItem = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
@ -93,7 +93,6 @@ export const droneSideItem: CanvasElementItem = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
@ -98,7 +98,6 @@ export const droneTopItem: CanvasElementItem = {
fixed: 'transparent',
},
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -106,7 +106,6 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { isString } from 'lodash';
import { CSSProperties } from 'react';
import { LinkModel, OneClickMode } from '@grafana/data';
import { LinkModel } from '@grafana/data';
import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
import { getPublicOrAbsoluteUrl } from 'app/features/dimensions';
@ -80,7 +80,6 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
left: options?.placement?.left ?? 100,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -3,13 +3,7 @@ import { useCallback } from 'react';
import { useObservable } from 'react-use';
import { of } from 'rxjs';
import {
DataFrame,
FieldNamePickerConfigSettings,
GrafanaTheme2,
OneClickMode,
StandardEditorsRegistryItem,
} from '@grafana/data';
import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data';
import { TextDimensionMode } from '@grafana/schema';
import { usePanelContext, useStyles2 } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
@ -181,7 +175,6 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
left: options?.placement?.left ?? 100,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -99,7 +99,6 @@ export const parallelogramItem: CanvasElementItem = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { PureComponent } from 'react';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context';
@ -72,7 +72,6 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
fixed: defaultBgColor,
},
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, LinkModel, OneClickMode } from '@grafana/data';
import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema';
import config from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
@ -83,7 +83,6 @@ export const serverItem: CanvasElementItem<ServerConfig, ServerData> = {
config: {
type: ServerType.Single,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import { useObservable } from 'react-use';
import { of } from 'rxjs';
import { DataFrame, GrafanaTheme2, OneClickMode } from '@grafana/data';
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -148,7 +148,6 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2, OneClickMode } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -100,7 +100,6 @@ export const triangleItem: CanvasElementItem = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, LinkModel, OneClickMode } from '@grafana/data';
import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { ScalarDimensionConfig } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions';
@ -90,7 +90,6 @@ export const windTurbineItem: CanvasElementItem = {
left: options?.placement?.left,
rotation: options?.placement?.rotation ?? 0,
},
oneClickMode: options?.oneClickMode ?? OneClickMode.Off,
links: options?.links ?? [],
}),

View File

@ -2,8 +2,18 @@ import * as React from 'react';
import { CSSProperties } from 'react';
import { OnDrag, OnResize, OnRotate } from 'react-moveable/declaration/types';
import { FieldType, getLinksSupplier, LinkModel, OneClickMode, ScopedVars, ValueLinkConfig } from '@grafana/data';
import {
FieldType,
getLinksSupplier,
LinkModel,
ScopedVars,
ValueLinkConfig,
OneClickMode,
ActionModel,
} from '@grafana/data';
import { ConfirmModal } from '@grafana/ui';
import { LayerElement } from 'app/core/components/Layers/types';
import { t } from 'app/core/internationalization';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { DimensionContext } from 'app/features/dimensions';
import {
@ -46,6 +56,10 @@ export class ElementState implements LayerElement {
getLinks?: (config: ValueLinkConfig) => LinkModel[];
// cached for tooltips/mousemove
oneClickMode = OneClickMode.Off;
showConfirmation = false;
constructor(
public item: CanvasElementItem,
public options: CanvasElementOptions,
@ -63,7 +77,7 @@ export class ElementState implements LayerElement {
options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0, rotation: 0 };
options.background = options.background ?? { color: { fixed: 'transparent' } };
options.border = options.border ?? { color: { fixed: 'dark-green' } };
options.oneClickMode = options.oneClickMode ?? OneClickMode.Off;
const scene = this.getScene();
if (!options.name) {
const newName = scene?.getNextElementName();
@ -376,6 +390,12 @@ export class ElementState implements LayerElement {
this.options.links = this.options.links?.filter((link) => link !== null);
if (this.options.links?.some((link) => link.oneClick === true)) {
this.oneClickMode = OneClickMode.Link;
} else if (this.options.actions?.some((action) => action.oneClick === true)) {
this.oneClickMode = OneClickMode.Action;
}
if (frames) {
const defaultField = {
name: 'Default field',
@ -588,31 +608,26 @@ export class ElementState implements LayerElement {
handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => {
const scene = this.getScene();
const shouldHandleTooltip =
!scene?.isEditingEnabled && !scene?.tooltip?.isOpen && this.options.oneClickMode === OneClickMode.Off;
const shouldHandleTooltip = !scene?.isEditingEnabled && !scene?.tooltip?.isOpen;
if (shouldHandleTooltip) {
this.handleTooltip(event);
} else if (!isSelected) {
scene?.connections.handleMouseEnter(event);
}
const shouldHandleOneClickLink =
this.options.oneClickMode === OneClickMode.Link && this.options.links && this.options.links.length > 0;
const shouldHandleOneClickAction =
this.options.oneClickMode === OneClickMode.Action && this.options.actions && this.options.actions.length > 0;
if (shouldHandleOneClickLink && this.div) {
const primaryDataLink = this.getPrimaryDataLink();
if (primaryDataLink) {
this.div.style.cursor = 'pointer';
this.div.title = `Navigate to ${primaryDataLink.title === '' ? 'data link' : primaryDataLink.title}`;
}
} else if (shouldHandleOneClickAction && this.div) {
const primaryAction = this.getPrimaryAction();
if (primaryAction) {
this.div.style.cursor = 'pointer';
this.div.title = primaryAction.title;
if (this.div != null) {
if (this.oneClickMode === OneClickMode.Link) {
const primaryDataLink = this.getPrimaryDataLink();
if (primaryDataLink) {
this.div.style.cursor = 'pointer';
this.div.title = `Navigate to ${primaryDataLink.title === '' ? 'data link' : primaryDataLink.title}`;
}
} else if (this.oneClickMode === OneClickMode.Action) {
const primaryAction = this.getPrimaryAction();
if (primaryAction) {
this.div.style.cursor = 'pointer';
this.div.title = primaryAction.title;
}
}
}
};
@ -620,7 +635,7 @@ export class ElementState implements LayerElement {
getPrimaryDataLink = () => {
if (this.getLinks) {
const links = this.getLinks({ valueRowIndex: getRowIndex(this.data.field, this.getScene()!) });
return links[0];
return links.find((link) => link.oneClick === true);
}
return undefined;
@ -652,7 +667,7 @@ export class ElementState implements LayerElement {
actionsDefaultFieldConfig.actions,
config
);
return actions[0];
return actions.find((action) => action.oneClick === true);
}
return undefined;
@ -672,11 +687,11 @@ export class ElementState implements LayerElement {
handleMouseLeave = (event: React.MouseEvent) => {
const scene = this.getScene();
if (scene?.tooltipCallback && !scene?.tooltip?.isOpen && this.options.oneClickMode === OneClickMode.Off) {
if (scene?.tooltipCallback && !scene?.tooltip?.isOpen) {
scene.tooltipCallback(undefined);
}
if (this.options.oneClickMode !== OneClickMode.Off && this.div) {
if (this.oneClickMode !== OneClickMode.Off && this.div) {
this.div.style.cursor = 'auto';
this.div.title = '';
}
@ -684,16 +699,14 @@ export class ElementState implements LayerElement {
onElementClick = (event: React.MouseEvent) => {
// If one-click access is enabled, open the primary link
if (this.options.oneClickMode === OneClickMode.Link) {
if (this.oneClickMode === OneClickMode.Link) {
let primaryDataLink = this.getPrimaryDataLink();
if (primaryDataLink) {
window.open(primaryDataLink.href, primaryDataLink.target ?? '_self');
}
} else if (this.options.oneClickMode === OneClickMode.Action) {
let primaryAction = this.getPrimaryAction();
if (primaryAction && primaryAction.onClick) {
primaryAction.onClick(event);
}
} else if (this.oneClickMode === OneClickMode.Action) {
this.showConfirmation = true;
this.forceUpdate();
} else {
this.handleTooltip(event);
this.onTooltipCallback();
@ -721,29 +734,68 @@ export class ElementState implements LayerElement {
}
};
forceUpdate = () => {
const scene = this.getScene();
if (scene?.actionConfirmationCallback) {
scene.actionConfirmationCallback();
}
};
renderActionsConfirmModal = (action: ActionModel | undefined) => {
if (!action) {
return;
}
return (
<>
{this.showConfirmation && action && (
<ConfirmModal
isOpen={true}
title={t('grafana-ui.action-editor.button.confirm-action', 'Confirm action')}
body={action.confirmation}
confirmText={t('grafana-ui.action-editor.button.confirm', 'Confirm')}
confirmButtonVariant="primary"
onConfirm={() => {
this.showConfirmation = false;
action.onClick(new MouseEvent('click'));
this.forceUpdate();
}}
onDismiss={() => {
this.showConfirmation = false;
this.forceUpdate();
}}
/>
)}
</>
);
};
render() {
const { item, div } = this;
const scene = this.getScene();
const isSelected = div && scene && scene.selecto && scene.selecto.getSelectedTargets().includes(div);
return (
<div
key={this.UID}
ref={this.initElement}
onMouseEnter={(e: React.MouseEvent) => this.handleMouseEnter(e, isSelected)}
onMouseLeave={!scene?.isEditingEnabled ? this.handleMouseLeave : undefined}
onClick={!scene?.isEditingEnabled ? this.onElementClick : undefined}
onKeyDown={!scene?.isEditingEnabled ? this.onElementKeyDown : undefined}
role="button"
tabIndex={0}
>
<item.display
key={`${this.UID}/${this.revId}`}
config={this.options.config}
data={this.data}
isSelected={isSelected}
/>
</div>
<>
<div
key={this.UID}
ref={this.initElement}
onMouseEnter={(e: React.MouseEvent) => this.handleMouseEnter(e, isSelected)}
onMouseLeave={!scene?.isEditingEnabled ? this.handleMouseLeave : undefined}
onClick={!scene?.isEditingEnabled ? this.onElementClick : undefined}
onKeyDown={!scene?.isEditingEnabled ? this.onElementKeyDown : undefined}
role="button"
tabIndex={0}
>
<item.display
key={`${this.UID}/${this.revId}`}
config={this.options.config}
data={this.data}
isSelected={isSelected}
/>
</div>
{this.showConfirmation && this.renderActionsConfirmModal(this.getPrimaryAction())}
</>
);
}
}

View File

@ -97,6 +97,8 @@ export class Scene {
moveableActionCallback?: (moved: boolean) => void;
actionConfirmationCallback?: () => void;
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
subscription: Subscription;

View File

@ -85,6 +85,7 @@ export class CanvasPanel extends Component<Props, State> {
this.scene.setBackgroundCallback = this.openSetBackground;
this.scene.tooltipCallback = this.tooltipCallback;
this.scene.moveableActionCallback = this.moveableActionCallback;
this.scene.actionConfirmationCallback = this.actionConfirmationCallback;
this.subs.add(
this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => {
@ -302,6 +303,10 @@ export class CanvasPanel extends Component<Props, State> {
this.forceUpdate();
};
actionConfirmationCallback = () => {
this.forceUpdate();
};
closeInlineEdit = () => {
this.setState({ openInlineEdit: false });
isInlineEditOpen = false;

View File

@ -88,7 +88,6 @@ export const CanvasTooltip = ({ scene }: Props) => {
}
});
}
// ---------
if (scene.data?.series) {
getElementFields(scene.data?.series, element.options).forEach((field) => {

View File

@ -1,6 +1,5 @@
import { capitalize, get as lodashGet } from 'lodash';
import { get as lodashGet } from 'lodash';
import { OneClickMode } from '@grafana/data';
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
import { config } from '@grafana/runtime';
import { CanvasElementOptions } from 'app/features/canvas/element';
@ -123,36 +122,10 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
optionBuilder.addBorder(builder, ctx);
}
const oneClickModeOptions: Array<{
value: OneClickMode;
label: string;
}> = [
{ value: OneClickMode.Off, label: capitalize(OneClickMode.Off) },
{ value: OneClickMode.Link, label: capitalize(OneClickMode.Link) },
];
let oneClickCategory = 'Data links';
let oneClickDescription = 'When enabled, a single click opens the first link';
if (actionsEnabled) {
oneClickModeOptions.push({ value: OneClickMode.Action, label: capitalize(OneClickMode.Action) });
oneClickCategory += ' and actions';
oneClickDescription += ' or action';
}
builder.addRadio({
category: [oneClickCategory],
path: 'oneClickMode',
name: 'One-click',
description: oneClickDescription,
settings: {
options: oneClickModeOptions,
},
defaultValue: OneClickMode.Off,
});
optionBuilder.addDataLinks(builder, ctx);
optionBuilder.addActions(builder, ctx);
if (actionsEnabled) {
optionBuilder.addActions(builder, ctx);
}
},
};
}

View File

@ -1,4 +1,4 @@
import { OneClickMode, PanelModel } from '@grafana/data';
import { PanelModel } from '@grafana/data';
import { canvasMigrationHandler } from './migrations';
@ -20,6 +20,16 @@ describe('Canvas migration', () => {
},
},
],
links: [
{
title: 'Link1',
url: 'www.link1.com',
},
{
title: 'Link2',
url: 'www.link2.com',
},
],
},
],
},
@ -29,7 +39,7 @@ describe('Canvas migration', () => {
panel.options = canvasMigrationHandler(panel);
expect(panel.options.root.elements[0].oneClickMode).toBe(OneClickMode.Link);
expect(panel.options.root.elements[0].links[0].oneClick).toBe(true);
expect(panel.options.root.elements[0].actions[0].fetch.url).toBe('http://test.com');
});
});

View File

@ -46,12 +46,6 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
const root = panel.options?.root;
if (root?.elements) {
for (const element of root.elements) {
// migrate oneClickLinks to oneClickMode
if (element.oneClickLinks) {
element.oneClickMode = OneClickMode.Link;
delete element.oneClickLinks;
}
// migrate action options to new format (fetch)
if (element.actions) {
for (const action of element.actions) {
@ -65,5 +59,22 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
}
}
// migrate oneClickMode to first link/action oneClick
if (parseFloat(pluginVersion) <= 11.6) {
const root = panel.options?.root;
if (root?.elements) {
for (const element of root.elements) {
if (element.oneClickMode === OneClickMode.Link || element.oneClickLinks) {
element.links[0].oneClick = true;
} else if (element.oneClickMode === OneClickMode.Action) {
element.actions[0].oneClick = true;
}
delete element.oneClickMode;
delete element.oneClickLinks;
}
}
}
return panel.options;
};

View File

@ -64,7 +64,6 @@ export const plugin = new PanelPlugin<Options>(CanvasPanel)
},
},
[FieldConfigProperty.Actions]: {
hideFromDefaults: true,
settings: {
showOneClick: false,
},

View File

@ -1699,6 +1699,7 @@
"actions-confirmation-label": "Confirmation message",
"actions-confirmation-message": "Provide a descriptive prompt to confirm or cancel the action.",
"footer-add-annotation": "Add annotation",
"footer-click-to-action": "Click to {{actionTitle}}",
"footer-click-to-navigate": "Click to open {{linkTitle}}"
}
},

View File

@ -1699,6 +1699,7 @@
"actions-confirmation-label": "Cőʼnƒįřmäŧįőʼn męşşäģę",
"actions-confirmation-message": "Přővįđę ä đęşčřįpŧįvę přőmpŧ ŧő čőʼnƒįřm őř čäʼnčęľ ŧĥę äčŧįőʼn.",
"footer-add-annotation": "Åđđ äʼnʼnőŧäŧįőʼn",
"footer-click-to-action": "Cľįčĸ ŧő {{actionTitle}}",
"footer-click-to-navigate": "Cľįčĸ ŧő őpęʼn {{linkTitle}}"
}
},