mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: One click links and actions (#99616)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
02118cc6aa
commit
5aeaa18ac2
@ -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>
|
||||
|
@ -62,6 +62,7 @@ export const getActions = (
|
||||
onClick: (evt: MouseEvent, origin: Field) => {
|
||||
buildActionOnClick(action, boundReplaceVariables);
|
||||
},
|
||||
oneClick: action.oneClick ?? false,
|
||||
};
|
||||
|
||||
return actionModel;
|
||||
|
@ -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
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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 ?? [],
|
||||
}),
|
||||
|
||||
|
@ -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())}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +97,8 @@ export class Scene {
|
||||
|
||||
moveableActionCallback?: (moved: boolean) => void;
|
||||
|
||||
actionConfirmationCallback?: () => void;
|
||||
|
||||
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
|
||||
subscription: Subscription;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -88,7 +88,6 @@ export const CanvasTooltip = ({ scene }: Props) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
// ---------
|
||||
|
||||
if (scene.data?.series) {
|
||||
getElementFields(scene.data?.series, element.options).forEach((field) => {
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -64,7 +64,6 @@ export const plugin = new PanelPlugin<Options>(CanvasPanel)
|
||||
},
|
||||
},
|
||||
[FieldConfigProperty.Actions]: {
|
||||
hideFromDefaults: true,
|
||||
settings: {
|
||||
showOneClick: false,
|
||||
},
|
||||
|
@ -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}}"
|
||||
}
|
||||
},
|
||||
|
@ -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}}"
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user