diff --git a/.betterer.results b/.betterer.results index e33c3ac4abd..21ed4ac7d28 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1367,13 +1367,6 @@ exports[`better eslint`] = { [0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "2"], [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] ], - "public/app/features/actions/ActionsListItem.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Icon/Icon\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], - [0, 0, 0, "\'@grafana/ui/src/components/IconButton/IconButton\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], - [0, 0, 0, "\'@grafana/ui/src/themes\' import is restricted from being used by a pattern. Import from the public export instead.", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"] - ], "public/app/features/actions/ParamsEditor.tsx:5381": [ [0, 0, 0, "\'@grafana/ui/src/components/IconButton/IconButton\' import is restricted from being used by a pattern. Import from the public export instead.", "0"], [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "1"], diff --git a/packages/grafana-data/src/types/action.ts b/packages/grafana-data/src/types/action.ts index bbdfd91b4f5..fc17aa25046 100644 --- a/packages/grafana-data/src/types/action.ts +++ b/packages/grafana-data/src/types/action.ts @@ -16,6 +16,7 @@ export interface Action { // once multiple types are valid, usage of this will need to be optional [ActionType.Fetch]: FetchOptions; confirmation?: string; + oneClick?: boolean; } /** @@ -25,6 +26,7 @@ export interface ActionModel { title: string; onClick: (event: any, origin?: any) => void; confirmation?: string; + oneClick?: boolean; } interface FetchOptions { diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index e87d01fbea2..8ff7535790c 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -17,6 +17,7 @@ interface DataLinkEditorProps { value: DataLink; suggestions: VariableSuggestion[]; onChange: (index: number, link: DataLink, callback?: () => void) => void; + showOneClick?: boolean; } const getStyles = (theme: GrafanaTheme2) => ({ @@ -30,58 +31,63 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), }); -export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLast }: DataLinkEditorProps) => { - const styles = useStyles2(getStyles); +export const DataLinkEditor = memo( + ({ index, value, onChange, suggestions, isLast, showOneClick = false }: DataLinkEditorProps) => { + const styles = useStyles2(getStyles); - const onUrlChange = (url: string, callback?: () => void) => { - onChange(index, { ...value, url }, callback); - }; - const onTitleChange = (event: ChangeEvent) => { - onChange(index, { ...value, title: event.target.value }); - }; + const onUrlChange = (url: string, callback?: () => void) => { + onChange(index, { ...value, url }, callback); + }; - const onOpenInNewTabChanged = () => { - onChange(index, { ...value, targetBlank: !value.targetBlank }); - }; + const onTitleChange = (event: ChangeEvent) => { + onChange(index, { ...value, title: event.target.value }); + }; - const onOneClickChanged = () => { - onChange(index, { ...value, oneClick: !value.oneClick }); - }; + const onOpenInNewTabChanged = () => { + onChange(index, { ...value, targetBlank: !value.targetBlank }); + }; - return ( -
- - - + const onOneClickChanged = () => { + onChange(index, { ...value, oneClick: !value.oneClick }); + }; - - - + return ( +
+ + + - - - + + + - + + + + {showOneClick && ( + + + )} - > - - - {isLast && ( -
- - With data links you can reference data variables like series name, labels and values. Type CMD+Space, - CTRL+Space, or $ to open variable suggestions. - -
- )} -
- ); -}); + {isLast && ( +
+ + With data links you can reference data variables like series name, labels and values. Type CMD+Space, + CTRL+Space, or $ to open variable suggestions. + +
+ )} +
+ ); + } +); DataLinkEditor.displayName = 'DataLinkEditor'; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx index 5348251f1f2..02287da7215 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinkEditorModalContent.tsx @@ -14,14 +14,16 @@ interface DataLinkEditorModalContentProps { getSuggestions: () => VariableSuggestion[]; onSave: (index: number, ink: DataLink) => void; onCancel: (index: number) => void; + showOneClick?: boolean; } export const DataLinkEditorModalContent = ({ link, index, - getSuggestions, onSave, onCancel, + getSuggestions, + showOneClick, }: DataLinkEditorModalContentProps) => { const [dirtyLink, setDirtyLink] = useState(link); return ( @@ -30,10 +32,11 @@ export const DataLinkEditorModalContent = ({ value={dirtyLink} index={index} isLast={false} - suggestions={getSuggestions()} onChange={(index, link) => { setDirtyLink(link); }} + suggestions={getSuggestions()} + showOneClick={showOneClick} /> diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index fa4e61589f3..99b15ea1697 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -1,198 +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 { DataFrame, DataLink, GrafanaTheme2, VariableSuggestion } from '@grafana/data'; - -import { useStyles2 } from '../../../themes'; -import { Trans } from '../../../utils/i18n'; -import { Button } from '../../Button'; -import { Modal } from '../../Modal/Modal'; +import { DataLink, VariableSuggestion } from '@grafana/data'; import { DataLinkEditorModalContent } from './DataLinkEditorModalContent'; -import { DataLinksListItem } from './DataLinksListItem'; +import { DataLinksInlineEditorBase, DataLinksInlineEditorBaseProps } from './DataLinksInlineEditorBase'; -interface DataLinksInlineEditorProps { +type DataLinksInlineEditorProps = Omit, 'children' | 'type' | 'items'> & { links?: DataLink[]; - onChange: (links: DataLink[]) => void; - getSuggestions: () => VariableSuggestion[]; - data: DataFrame[]; showOneClick?: boolean; -} - -export const DataLinksInlineEditor = ({ - links, - onChange, - getSuggestions, - data, - showOneClick = false, -}: DataLinksInlineEditorProps) => { - const [editIndex, setEditIndex] = useState(null); - const [isNew, setIsNew] = useState(false); - - const [linksSafe, setLinksSafe] = useState([]); - - useEffect(() => { - setLinksSafe(links ?? []); - }, [links]); - - const styles = useStyles2(getDataLinksInlineEditorStyles); - const isEditing = editIndex !== null; - - const onDataLinkChange = (index: number, link: DataLink) => { - if (isNew) { - if (link.title.trim() === '' && link.url.trim() === '') { - setIsNew(false); - setEditIndex(null); - return; - } else { - setEditIndex(null); - setIsNew(false); - } - } - - if (link.oneClick === true) { - linksSafe.forEach((link) => { - if (link.oneClick) { - link.oneClick = false; - } - }); - } - - const update = cloneDeep(linksSafe); - update[index] = link; - onChange(update); - setEditIndex(null); - }; - - const onDataLinkAdd = () => { - let update = cloneDeep(linksSafe); - setEditIndex(update.length); - setIsNew(true); - }; - - const onDataLinkCancel = (index: number) => { - if (isNew) { - setIsNew(false); - } - setEditIndex(null); - }; - - const onDataLinkRemove = (index: number) => { - const update = cloneDeep(linksSafe); - update.splice(index, 1); - onChange(update); - }; - - const onDragEnd = (result: DropResult) => { - if (!links || !result.destination) { - return; - } - - const update = cloneDeep(linksSafe); - const link = update[result.source.index]; - - update.splice(result.source.index, 1); - update.splice(result.destination.index, 0, link); - - setLinksSafe(update); - onChange(update); - }; - - return ( -
- {/* one-link placeholder */} - {showOneClick && linksSafe.length > 0 && ( -
- - One-click link - -
- )} - - - - {(provided) => ( -
0 ? '28px' : '0px' }} - > - {linksSafe.map((link, idx) => { - const key = `${link.title}/${idx}`; - return ( - setEditIndex(idx)} - onRemove={() => onDataLinkRemove(idx)} - data={data} - itemKey={key} - /> - ); - })} - {provided.placeholder} -
- )} -
-
- - {isEditing && editIndex !== null && ( - { - onDataLinkCancel(editIndex); - }} - > - - - )} - - -
- ); + getSuggestions: () => VariableSuggestion[]; }; -const getDataLinksInlineEditorStyles = (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: '92px', - }), - oneClickSpan: css({ - padding: 10, - // Negates the padding on the span from moving the underlying link - marginBottom: -10, - display: 'inline-block', - }), - button: css({ - marginLeft: theme.spacing(1), - }), -}); +export const DataLinksInlineEditor = ({ links, getSuggestions, showOneClick, ...rest }: DataLinksInlineEditorProps) => ( + type="link" items={links} {...rest}> + {(item, index, onSave, onCancel) => ( + + )} + +); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx new file mode 100644 index 00000000000..a5f66c6d0c3 --- /dev/null +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditorBase.tsx @@ -0,0 +1,191 @@ +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, DataLink, GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../themes'; +import { t } from '../../../utils/i18n'; +import { Button } from '../../Button'; +import { Modal } from '../../Modal/Modal'; + +import { DataLinksListItemBase } from './DataLinksListItemBase'; + +export interface DataLinksInlineEditorBaseProps { + type: 'link' | 'action'; + items?: T[]; + onChange: (items: T[]) => void; + data: DataFrame[]; + children: ( + item: T, + index: number, + onSave: (index: number, item: T) => void, + onCancel: (index: number) => void + ) => React.ReactNode; +} + +/** @internal */ +export function DataLinksInlineEditorBase({ + type, + items, + onChange, + data, + children, +}: DataLinksInlineEditorBaseProps) { + const [editIndex, setEditIndex] = useState(null); + const [isNew, setIsNew] = useState(false); + + const [itemsSafe, setItemsSafe] = useState([]); + + useEffect(() => { + setItemsSafe(items ?? []); + }, [items]); + + const styles = useStyles2(getDataLinksInlineEditorStyles); + const isEditing = editIndex !== null; + + const _onChange = (index: number, item: T) => { + if (isNew) { + const title = item.title; + // @ts-ignore - https://github.com/microsoft/TypeScript/issues/27808 + const url = item.url ?? item.fetch?.url ?? ''; + + if (title.trim() === '' && url.trim() === '') { + setIsNew(false); + setEditIndex(null); + return; + } else { + setEditIndex(null); + setIsNew(false); + } + } + + if (item.oneClick === true) { + itemsSafe.forEach((item) => { + if (item.oneClick) { + item.oneClick = false; + } + }); + } + + const update = cloneDeep(itemsSafe); + update[index] = item; + onChange(update); + setEditIndex(null); + }; + + const _onCancel = (index: number) => { + if (isNew) { + setIsNew(false); + } + setEditIndex(null); + }; + + const onDataLinkAdd = () => { + let update = cloneDeep(itemsSafe); + setEditIndex(update.length); + setIsNew(true); + }; + + const onDataLinkRemove = (index: number) => { + const update = cloneDeep(itemsSafe); + update.splice(index, 1); + onChange(update); + }; + + const onDragEnd = (result: DropResult) => { + if (items == null || result.destination == null) { + return; + } + + const update = cloneDeep(itemsSafe); + const link = update[result.source.index]; + + update.splice(result.source.index, 1); + update.splice(result.destination.index, 0, link); + + setItemsSafe(update); + onChange(update); + }; + + const getItemText = (action: 'edit' | 'add') => { + let text = ''; + switch (type) { + case 'link': + text = + action === 'edit' + ? t('grafana-ui.data-links-inline-editor.edit-link', 'Edit link') + : t('grafana-ui.data-links-inline-editor.add-link', 'Add link'); + break; + case 'action': + text = + action === 'edit' + ? t('grafana-ui.action-editor.inline.edit-action', 'Edit action') + : t('grafana-ui.action-editor.inline.add-action', 'Add action'); + break; + } + + return text; + }; + + return ( +
+ + + {(provided) => ( +
+ {itemsSafe.map((item, idx) => { + const key = `${item.title}/${idx}`; + return ( + + key={key} + index={idx} + item={item} + onChange={_onChange} + onEdit={() => setEditIndex(idx)} + onRemove={() => onDataLinkRemove(idx)} + data={data} + itemKey={key} + /> + ); + })} + {provided.placeholder} +
+ )} +
+
+ + {isEditing && editIndex !== null && ( + { + _onCancel(editIndex); + }} + > + {children(itemsSafe[editIndex], editIndex, _onChange, _onCancel)} + + )} + + +
+ ); +} + +const getDataLinksInlineEditorStyles = (theme: GrafanaTheme2) => ({ + container: css({ + position: 'relative', + }), + wrapper: css({ + marginBottom: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + }), + button: css({ + marginLeft: theme.spacing(1), + }), +}); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx index b44576d53f7..d74f11e09f8 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.test.tsx @@ -13,7 +13,7 @@ const baseLink = { function setupTestContext(options: Partial) { const defaults: DataLinksListItemProps = { index: 0, - link: baseLink, + item: baseLink, data: [], onChange: jest.fn(), onEdit: jest.fn(), @@ -42,11 +42,11 @@ function setupTestContext(options: Partial) { describe('DataLinksListItem', () => { describe('when link has title', () => { it('then the link title should be visible', () => { - const link = { + const item = { ...baseLink, title: 'Some Data Link Title', }; - setupTestContext({ link }); + setupTestContext({ item }); expect(screen.getByText(/some data link title/i)).toBeInTheDocument(); }); @@ -54,62 +54,14 @@ describe('DataLinksListItem', () => { describe('when link has url', () => { it('then the link url should be visible', () => { - const link = { + const item = { ...baseLink, url: 'http://localhost:3000', }; - setupTestContext({ link }); + setupTestContext({ item }); expect(screen.getByText(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); expect(screen.getByTitle(/http:\/\/localhost\:3000/i)).toBeInTheDocument(); }); }); - - describe('when link is missing title', () => { - it('then the link title should be replaced by [Data link title not provided]', () => { - const link = { - ...baseLink, - title: undefined as unknown as string, - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link title not provided/i)).toBeInTheDocument(); - }); - }); - - describe('when link is missing url', () => { - it('then the link url should be replaced by [Data link url not provided]', () => { - const link = { - ...baseLink, - url: undefined as unknown as string, - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument(); - }); - }); - - describe('when link title is empty', () => { - it('then the link title should be replaced by [Data link title not provided]', () => { - const link = { - ...baseLink, - title: ' ', - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link title not provided/i)).toBeInTheDocument(); - }); - }); - - describe('when link url is empty', () => { - it('then the link url should be replaced by [Data link url not provided]', () => { - const link = { - ...baseLink, - url: ' ', - }; - setupTestContext({ link }); - - expect(screen.getByText(/data link url not provided/i)).toBeInTheDocument(); - }); - }); }); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 9bddc35da71..ce8fbe3d95d 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -1,119 +1,6 @@ -import { css, cx } from '@emotion/css'; -import { Draggable } from '@hello-pangea/dnd'; +import { DataLink } from '@grafana/data'; -import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; +import { DataLinksListItemBase, DataLinksListItemBaseProps } from './DataLinksListItemBase'; -import { useStyles2 } from '../../../themes'; -import { t } from '../../../utils/i18n'; -import { Badge } from '../../Badge/Badge'; -import { Icon } from '../../Icon/Icon'; -import { IconButton } from '../../IconButton/IconButton'; - -export interface DataLinksListItemProps { - index: number; - link: DataLink; - data: DataFrame[]; - onChange: (index: number, link: DataLink) => void; - onEdit: () => void; - onRemove: () => void; - isEditing?: boolean; - itemKey: string; -} - -export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: DataLinksListItemProps) => { - const styles = useStyles2(getDataLinkListItemStyles); - const { title = '', url = '', oneClick = false } = link; - - const hasTitle = title.trim() !== ''; - const hasUrl = url.trim() !== ''; - - return ( - - {(provided) => ( -
-
-
- {hasTitle ? title : 'Data link title not provided'} -
-
- {hasUrl ? url : 'Data link url not provided'} -
-
-
- {oneClick && ( - - )} - - -
- -
-
-
- )} -
- ); -}; - -const getDataLinkListItemStyles = (theme: GrafanaTheme2) => { - return { - wrapper: css({ - display: 'flex', - flexGrow: 1, - alignItems: 'center', - justifyContent: 'space-between', - padding: '5px 0 5px 10px', - borderRadius: theme.shape.radius.default, - background: theme.colors.background.secondary, - gap: 8, - }), - linkDetails: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - maxWidth: `calc(100% - 100px)`, - }), - notConfigured: css({ - fontStyle: 'italic', - }), - title: css({ - color: theme.colors.text.primary, - fontSize: theme.typography.size.sm, - fontWeight: theme.typography.fontWeightMedium, - }), - url: css({ - color: theme.colors.text.secondary, - fontSize: theme.typography.size.sm, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }), - dragRow: css({ - position: 'relative', - margin: '8px', - }), - icons: css({ - display: 'flex', - padding: 6, - 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, - }), - }; -}; +export const DataLinksListItem = DataLinksListItemBase; +export type DataLinksListItemProps = DataLinksListItemBaseProps; diff --git a/public/app/features/actions/ActionsListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx similarity index 52% rename from public/app/features/actions/ActionsListItem.tsx rename to packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx index 6dd9cebf58f..6ddbc97e994 100644 --- a/public/app/features/actions/ActionsListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItemBase.tsx @@ -1,27 +1,41 @@ import { css, cx } from '@emotion/css'; import { Draggable } from '@hello-pangea/dnd'; -import { Action, DataFrame, GrafanaTheme2 } from '@grafana/data'; -import { Icon } from '@grafana/ui/src/components/Icon/Icon'; -import { IconButton } from '@grafana/ui/src/components/IconButton/IconButton'; -import { useStyles2 } from '@grafana/ui/src/themes'; +import { Action, DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; -export interface ActionsListItemProps { +import { useStyles2 } from '../../../themes'; +import { t } from '../../../utils/i18n'; +import { Badge } from '../../Badge/Badge'; +import { Icon } from '../../Icon/Icon'; +import { IconButton } from '../../IconButton/IconButton'; + +export interface DataLinksListItemBaseProps { index: number; - action: Action; + item: T; data: DataFrame[]; - onChange: (index: number, action: Action) => void; + onChange: (index: number, item: T) => void; onEdit: () => void; onRemove: () => void; isEditing?: boolean; itemKey: string; } -export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: ActionsListItemProps) => { - const styles = useStyles2(getActionListItemStyles); - const { title = '' } = action; +/** @internal */ +export function DataLinksListItemBase({ + item, + onEdit, + onRemove, + index, + itemKey, +}: DataLinksListItemBaseProps) { + const styles = useStyles2(getDataLinkListItemStyles); + const { title = '', oneClick = false } = item; + + // @ts-ignore - https://github.com/microsoft/TypeScript/issues/27808 + const url = item.url ?? item.fetch?.url ?? ''; const hasTitle = title.trim() !== ''; + const hasUrl = url.trim() !== ''; return ( @@ -34,12 +48,32 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act >
- {hasTitle ? title : 'Action title not provided'} + {hasTitle ? title : t('grafana-ui.data-links-inline-editor.title-not-provided', 'Title not provided')} +
+
+ {hasUrl ? url : t('grafana-ui.data-links-inline-editor.url-not-provided', 'Data link url not provided')}
- - + {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} /> -
- ); + 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>", 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>",