-
-
+ {oneClick && (
+
+ )}
+
+
@@ -48,9 +82,9 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act
)}
);
-};
+}
-const getActionListItemStyles = (theme: GrafanaTheme2) => {
+const getDataLinkListItemStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
display: 'flex',
@@ -66,6 +100,7 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
+ maxWidth: `calc(100% - 100px)`,
}),
errored: css({
color: theme.colors.error.text,
@@ -85,15 +120,6 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
- maxWidth: `calc(100% - 100px)`,
- }),
- dragIcon: css({
- cursor: 'grab',
- color: theme.colors.text.secondary,
- margin: theme.spacing(0, 0.5),
- }),
- icon: css({
- color: theme.colors.text.secondary,
}),
dragRow: css({
position: 'relative',
@@ -105,5 +131,13 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => {
alignItems: 'center',
gap: 8,
}),
+ dragIcon: css({
+ cursor: 'grab',
+ color: theme.colors.text.secondary,
+ margin: theme.spacing(0, 0.5),
+ }),
+ icon: css({
+ color: theme.colors.text.secondary,
+ }),
};
};
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index 900190b9acf..8296a00cb70 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -169,6 +169,10 @@ export { MenuGroup, type MenuItemsGroup, type MenuGroupProps } from './Menu/Menu
export { MenuItem, type MenuItemProps } from './Menu/MenuItem';
export { WithContextMenu } from './ContextMenu/WithContextMenu';
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
+export {
+ DataLinksInlineEditorBase,
+ type DataLinksInlineEditorBaseProps,
+} from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase';
export { DataLinkInput } from './DataLinks/DataLinkInput';
export {
DataLinksContextMenu,
diff --git a/public/app/features/actions/ActionEditor.tsx b/public/app/features/actions/ActionEditor.tsx
index bc8edcddd1d..4194ab2680c 100644
--- a/public/app/features/actions/ActionEditor.tsx
+++ b/public/app/features/actions/ActionEditor.tsx
@@ -2,6 +2,8 @@ import { css } from '@emotion/css';
import { memo } from 'react';
import { Action, GrafanaTheme2, httpMethodOptions, HttpRequestMethod, VariableSuggestion } from '@grafana/data';
+import { config } from '@grafana/runtime';
+import { Switch } from '@grafana/ui/';
import { Field } from '@grafana/ui/src/components/Forms/Field';
import { InlineField } from '@grafana/ui/src/components/Forms/InlineField';
import { InlineFieldRow } from '@grafana/ui/src/components/Forms/InlineFieldRow';
@@ -19,11 +21,12 @@ interface ActionEditorProps {
value: Action;
onChange: (index: number, action: Action) => void;
suggestions: VariableSuggestion[];
+ showOneClick?: boolean;
}
const LABEL_WIDTH = 13;
-export const ActionEditor = memo(({ index, value, onChange, suggestions }: ActionEditorProps) => {
+export const ActionEditor = memo(({ index, value, onChange, suggestions, showOneClick }: ActionEditorProps) => {
const styles = useStyles2(getStyles);
const onTitleChange = (title: string) => {
@@ -34,6 +37,10 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio
onChange(index, { ...value, confirmation });
};
+ const onOneClickChanged = () => {
+ onChange(index, { ...value, oneClick: !value.oneClick });
+ };
+
const onUrlChange = (url: string) => {
onChange(index, {
...value,
@@ -101,6 +108,8 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio
value.fetch.method !== HttpRequestMethod.GET &&
value.fetch.headers?.some(([name, value]) => name === 'Content-Type' && value === 'application/json');
+ const action = config.featureToggles.vizActions ? 'or action' : '';
+
return (
@@ -133,6 +142,19 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions }: Actio
/>
+ {showOneClick && (
+
+
+
+ )}
+
void;
onCancel: (index: number) => void;
getSuggestions: () => VariableSuggestion[];
+ showOneClick: boolean;
}
export const ActionEditorModalContent = ({
@@ -22,6 +23,7 @@ export const ActionEditorModalContent = ({
onSave,
onCancel,
getSuggestions,
+ showOneClick,
}: ActionEditorModalContentProps) => {
const [dirtyAction, setDirtyAction] = useState(action);
@@ -34,6 +36,7 @@ export const ActionEditorModalContent = ({
setDirtyAction(action);
}}
suggestions={getSuggestions()}
+ showOneClick={showOneClick}
/>
onCancel(index)} fill="outline">
diff --git a/public/app/features/actions/ActionsInlineEditor.tsx b/public/app/features/actions/ActionsInlineEditor.tsx
index 188f170d085..7cdb1ff34a4 100644
--- a/public/app/features/actions/ActionsInlineEditor.tsx
+++ b/public/app/features/actions/ActionsInlineEditor.tsx
@@ -1,193 +1,26 @@
-import { css } from '@emotion/css';
-import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
-import { cloneDeep } from 'lodash';
-import { useEffect, useState } from 'react';
-
-import { Action, DataFrame, GrafanaTheme2, defaultActionConfig, VariableSuggestion } from '@grafana/data';
-import { Button } from '@grafana/ui/src/components/Button';
-import { Modal } from '@grafana/ui/src/components/Modal/Modal';
-import { useStyles2 } from '@grafana/ui/src/themes';
-import { Trans } from 'app/core/internationalization';
+import { Action, defaultActionConfig, VariableSuggestion } from '@grafana/data';
+import { DataLinksInlineEditorBase, DataLinksInlineEditorBaseProps } from '@grafana/ui';
import { ActionEditorModalContent } from './ActionEditorModalContent';
-import { ActionListItem } from './ActionsListItem';
-interface ActionsInlineEditorProps {
- actions?: Action[];
- onChange: (actions: Action[]) => void;
- data: DataFrame[];
- getSuggestions: () => VariableSuggestion[];
+type DataLinksInlineEditorProps = Omit, 'children' | 'type' | 'items'> & {
+ actions: Action[];
showOneClick?: boolean;
-}
-
-export const ActionsInlineEditor = ({
- actions,
- onChange,
- data,
- getSuggestions,
- showOneClick = false,
-}: ActionsInlineEditorProps) => {
- const [editIndex, setEditIndex] = useState(null);
- const [isNew, setIsNew] = useState(false);
-
- const [actionsSafe, setActionsSafe] = useState([]);
-
- useEffect(() => {
- setActionsSafe(actions ?? []);
- }, [actions]);
-
- const styles = useStyles2(getActionsInlineEditorStyle);
- const isEditing = editIndex !== null;
-
- const onActionChange = (index: number, action: Action) => {
- if (isNew) {
- if (action.title.trim() === '') {
- setIsNew(false);
- setEditIndex(null);
- return;
- } else {
- setEditIndex(null);
- setIsNew(false);
- }
- }
- const update = cloneDeep(actionsSafe);
- update[index] = action;
- onChange(update);
-
- setEditIndex(null);
- };
-
- const onActionAdd = () => {
- let update = cloneDeep(actionsSafe);
- setEditIndex(update.length);
- setIsNew(true);
- };
-
- const onActionCancel = (index: number) => {
- if (isNew) {
- setIsNew(false);
- }
- setEditIndex(null);
- };
-
- const onActionRemove = (index: number) => {
- const update = cloneDeep(actionsSafe);
- update.splice(index, 1);
- onChange(update);
- };
-
- const onDragEnd = (result: DropResult) => {
- if (!actions || !result.destination) {
- return;
- }
-
- const update = cloneDeep(actionsSafe);
- const action = update[result.source.index];
-
- update.splice(result.source.index, 1);
- update.splice(result.destination.index, 0, action);
-
- setActionsSafe(update);
- onChange(update);
- };
-
- return (
-
- {/* one-link placeholder */}
- {showOneClick && actionsSafe.length > 0 && (
-
-
- One-click action
-
-
- )}
-
-
-
- {(provided) => (
- 0 ? '28px' : '0px' }}
- >
- {actionsSafe.map((action, idx) => {
- const key = `${action.title}/${idx}`;
-
- return (
-
setEditIndex(idx)}
- onRemove={() => onActionRemove(idx)}
- data={data}
- itemKey={key}
- />
- );
- })}
- {provided.placeholder}
-
- )}
-
-
-
- {isEditing && editIndex !== null && (
-
{
- onActionCancel(editIndex);
- }}
- >
-
-
- )}
-
-
- Add action
-
-
- );
+ getSuggestions: () => VariableSuggestion[];
};
-const getActionsInlineEditorStyle = (theme: GrafanaTheme2) => ({
- container: css({
- position: 'relative',
- }),
- wrapper: css({
- marginBottom: theme.spacing(2),
- display: 'flex',
- flexDirection: 'column',
- }),
- oneClickOverlay: css({
- border: `2px dashed ${theme.colors.text.link}`,
- fontSize: 10,
- color: theme.colors.text.primary,
- marginBottom: theme.spacing(1),
- position: 'absolute',
- width: '100%',
- height: '89px',
- }),
- oneClickSpan: css({
- padding: 10,
- // Negates the padding on the span from moving the underlying link
- marginBottom: -10,
- display: 'inline-block',
- }),
- itemWrapper: css({
- padding: '4px 8px 8px 8px',
- }),
- button: css({
- marginLeft: theme.spacing(1),
- }),
-});
+export const ActionsInlineEditor = ({ actions, getSuggestions, showOneClick, ...rest }: DataLinksInlineEditorProps) => (
+ type="action" items={actions} {...rest}>
+ {(item, index, onSave, onCancel) => (
+
+ )}
+
+);
diff --git a/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx b/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx
index 06613ffe843..4928c7c3368 100644
--- a/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx
+++ b/public/app/plugins/panel/canvas/editor/element/ActionsEditor.tsx
@@ -1,20 +1,26 @@
-import { StandardEditorProps, OneClickMode, Action, VariableSuggestionsScope } from '@grafana/data';
+import { StandardEditorProps, Action, VariableSuggestionsScope } from '@grafana/data';
+import { ActionsInlineEditor } from 'app/features/actions/ActionsInlineEditor';
import { CanvasElementOptions } from 'app/features/canvas/element';
-import { ActionsInlineEditor } from '../../../../../features/actions/ActionsInlineEditor';
-
type Props = StandardEditorProps;
export function ActionsEditor({ value, onChange, item, context }: Props) {
- const oneClickMode = item.settings?.oneClickMode;
+ const dataLinks = item.settings?.links || [];
return (
{
+ if (actions.some(({ oneClick }) => oneClick === true)) {
+ dataLinks.forEach((link) => {
+ link.oneClick = false;
+ });
+ }
+ onChange(actions);
+ }}
getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])}
data={[]}
- showOneClick={oneClickMode === OneClickMode.Action}
+ showOneClick={true}
/>
);
}
diff --git a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx
index 95779a4b7a1..9849b35d368 100644
--- a/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx
+++ b/public/app/plugins/panel/canvas/editor/element/DataLinksEditor.tsx
@@ -1,19 +1,26 @@
-import { StandardEditorProps, DataLink, VariableSuggestionsScope, OneClickMode } from '@grafana/data';
+import { StandardEditorProps, DataLink, VariableSuggestionsScope } from '@grafana/data';
import { DataLinksInlineEditor } from '@grafana/ui';
import { CanvasElementOptions } from 'app/features/canvas/element';
type Props = StandardEditorProps;
export function DataLinksEditor({ value, onChange, item, context }: Props) {
- const oneClickMode = item.settings?.oneClickMode;
+ const actions = item.settings?.actions || [];
return (
{
+ if (links.some(({ oneClick }) => oneClick === true)) {
+ actions.forEach((action) => {
+ action.oneClick = false;
+ });
+ }
+ onChange(links);
+ }}
getSuggestions={() => (context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : [])}
data={[]}
- showOneClick={oneClickMode === OneClickMode.Link}
+ showOneClick={false}
/>
);
}
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index c40a820b16d..c739515378a 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -34,12 +34,6 @@
"save-button": "Save"
}
},
- "actions-editor": {
- "inline": {
- "add-button": "Add action",
- "one-click-action": "One-click action"
- }
- },
"admin": {
"anon-users": {
"not-found": "No anonymous users found."
@@ -1574,12 +1568,17 @@
"confirm": "Confirm",
"confirm-action": "Confirm action"
},
+ "inline": {
+ "add-action": "Add action",
+ "edit-action": "Edit action"
+ },
"modal": {
"action-body": "Body",
"action-method": "Method",
"action-query-params": "Query parameters",
"action-title": "Title",
- "action-title-placeholder": "Action title"
+ "action-title-placeholder": "Action title",
+ "one-click-description": "Only one link {{ action }} can have one click enabled at a time"
}
},
"auto-save-field": {
@@ -1606,9 +1605,13 @@
},
"data-links-inline-editor": {
"add-link": "Add link",
+ "edit-link": "Edit link",
"one-click": "One click",
"one-click-enabled": "One click enabled",
- "one-click-link": "One-click link"
+ "title-not-provided": "Title not provided",
+ "tooltip-edit": "Edit",
+ "tooltip-remove": "Remove",
+ "url-not-provided": "Data link url not provided"
},
"data-source-http-settings": {
"access-help": "Help <1>1>",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index f67fd6afb55..4c73778c894 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -34,12 +34,6 @@
"save-button": "Ŝävę"
}
},
- "actions-editor": {
- "inline": {
- "add-button": "Åđđ äčŧįőʼn",
- "one-click-action": "Øʼnę-čľįčĸ äčŧįőʼn"
- }
- },
"admin": {
"anon-users": {
"not-found": "Ńő äʼnőʼnymőūş ūşęřş ƒőūʼnđ."
@@ -1574,12 +1568,17 @@
"confirm": "Cőʼnƒįřm",
"confirm-action": "Cőʼnƒįřm äčŧįőʼn"
},
+ "inline": {
+ "add-action": "Åđđ äčŧįőʼn",
+ "edit-action": "Ēđįŧ äčŧįőʼn"
+ },
"modal": {
"action-body": "ßőđy",
"action-method": "Męŧĥőđ",
"action-query-params": "Qūęřy päřämęŧęřş",
"action-title": "Ŧįŧľę",
- "action-title-placeholder": "Åčŧįőʼn ŧįŧľę"
+ "action-title-placeholder": "Åčŧįőʼn ŧįŧľę",
+ "one-click-description": "Øʼnľy őʼnę ľįʼnĸ {{ action }} čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę"
}
},
"auto-save-field": {
@@ -1606,9 +1605,13 @@
},
"data-links-inline-editor": {
"add-link": "Åđđ ľįʼnĸ",
+ "edit-link": "Ēđįŧ ľįʼnĸ",
"one-click": "Øʼnę čľįčĸ",
"one-click-enabled": "Øʼnę čľįčĸ ęʼnäþľęđ",
- "one-click-link": "Øʼnę-čľįčĸ ľįʼnĸ"
+ "title-not-provided": "Ŧįŧľę ʼnőŧ přővįđęđ",
+ "tooltip-edit": "Ēđįŧ",
+ "tooltip-remove": "Ŗęmővę",
+ "url-not-provided": "Đäŧä ľįʼnĸ ūřľ ʼnőŧ přővįđęđ"
},
"data-source-http-settings": {
"access-help": "Ħęľp <1>1>",