diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx index e169c756a94..a1e7aa55ec0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { formattedValueToString } from '@grafana/data'; -import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; +import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink'; import { AutoCellProps, TableCellStyles } from '../types'; export function AutoCell({ value, field, rowIdx }: AutoCellProps) { diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx index 700dbf2e33e..3f370e55c71 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx @@ -2,7 +2,7 @@ import { ThresholdsConfig, ThresholdsMode, VizOrientation, getFieldConfigWithMin import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@grafana/schema'; import { BarGauge } from '../../../BarGauge/BarGauge'; -import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; +import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink'; import { TABLE } from '../constants'; import { BarGaugeCellProps } from '../types'; import { getCellOptions, getAlignmentFactor } from '../utils'; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx index c3b085cbb5b..4013f2f04f8 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { TableCellDisplayMode } from '../../types'; -import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; +import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink'; import { ImageCellProps, TableCellStyles } from '../types'; export const ImageCell = ({ cellOptions, field, value, rowIdx }: ImageCellProps) => { diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx index 7d3e36e3f65..625ab881976 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { renderMarkdown } from '@grafana/data'; -import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; +import { MaybeWrapWithLink } from '../components/MaybeWrapWithLink'; import { MarkdownCellProps, TableCellStyles } from '../types'; export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) { diff --git a/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.test.tsx new file mode 100644 index 00000000000..c0a2947b770 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.test.tsx @@ -0,0 +1,199 @@ +import { render, screen } from '@testing-library/react'; + +import { ActionType, Field, FieldType, HttpRequestMethod } from '@grafana/data'; + +import { MaybeWrapWithLink } from './MaybeWrapWithLink'; + +describe('MaybeWrapWithLink', () => { + describe('single link', () => { + it('renders children as a link when there is a single link', () => { + const link = { title: 'My link', url: 'http://example.com' }; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => [{ title: link.title, href: link.url, target: '_blank', origin: field }]), + config: { + links: [link], + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + const linkElement = screen.getByTitle(link.title); + expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', link.url); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('does not throw if getLinks unexpectedly returns nothing when a single link is present', () => { + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links: [{ title: 'My link', url: 'http://example.com' }], + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const childElement = screen.getByText('Test Link'); + expect(childElement).toBeInTheDocument(); + }); + }); + + describe('multi link and/or actions', () => { + it('renders a popup target link if multiple links are present', () => { + const links = [ + { title: 'My link', url: 'http://example.com' }, + { title: 'Another link', url: 'http://example.com' }, + ]; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => links.map((l) => ({ title: l.title, href: l.url, target: '_blank', origin: field }))), + config: { + links, + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('renders a popup target link if multiple actions are present', () => { + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links: [], + actions: [ + { + type: ActionType.Fetch, + title: 'My action', + [ActionType.Fetch]: { method: HttpRequestMethod.GET, url: 'http://example.com' }, + }, + { + type: ActionType.Fetch, + title: 'Another action', + [ActionType.Fetch]: { method: HttpRequestMethod.POST, url: 'http://example.com' }, + }, + ], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('renders a popup target link if a single action is present', () => { + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links: [], + actions: [ + { + type: ActionType.Fetch, + title: 'My action', + [ActionType.Fetch]: { method: HttpRequestMethod.GET, url: 'http://example.com' }, + }, + ], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + + it('renders a popup target link if a mixture of actions and links are present', () => { + const links = [{ title: 'My link', url: 'http://example.com' }]; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => links.map((l) => ({ title: l.title, href: l.url, target: '_blank', origin: field }))), + config: { + links, + actions: [ + { + type: ActionType.Fetch, + title: 'My action', + [ActionType.Fetch]: { method: HttpRequestMethod.GET, url: 'http://example.com' }, + }, + ], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const linkElement = screen.getByTitle('view data links and actions'); + expect(linkElement).toBeInTheDocument(); + expect(linkElement.tagName).toBe('A'); + expect(linkElement).toHaveAttribute('aria-haspopup', 'menu'); + expect(linkElement).toHaveTextContent('Test Link'); + }); + }); + + describe('no links or actions', () => { + it('passes the children through when no links or actions are present', () => { + const links = [ + { title: 'My link', url: 'http://example.com' }, + { title: 'Another link', url: 'http://example.com' }, + ]; + const field: Field = { + type: FieldType.string, + name: 'Test Field', + values: [], + getLinks: jest.fn(() => []), + config: { + links, + actions: [], + }, + }; + const rowIdx = 0; + const children = Test Link; + + render(); + + const childElement = screen.getByText('Test Link'); + expect(childElement).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx similarity index 69% rename from packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx rename to packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx index ed8e3c88a8d..b43728dd1e8 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/MaybeWrapWithLink.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/components/MaybeWrapWithLink.tsx @@ -1,10 +1,10 @@ import { memo, ReactNode } from 'react'; import { Field } from '@grafana/data'; +import { t } from '@grafana/i18n'; -import { renderSingleLink } from '../DataLinksActionsTooltip'; - -import { getCellLinks } from './utils'; +import { renderSingleLink } from '../../DataLinksActionsTooltip'; +import { getCellLinks } from '../utils'; interface MaybeWrapWithLinkProps { field: Field; @@ -23,8 +23,12 @@ export const MaybeWrapWithLink = memo(({ field, rowIdx, children }: MaybeWrapWit } // as faux link that acts as hit-area for tooltip activation else if (linksCount + actionsCount > 0) { - // eslint-disable-next-line jsx-a11y/anchor-is-valid - return {children}; + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + {children} + + ); } // raw value diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 594c036e9d2..b705495578d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12751,6 +12751,9 @@ "label-alt-text": "Alt text", "label-title-text": "Title text" }, + "link-wrapper": { + "menu": "view data links and actions" + }, "markdown-cell-options-editor": { "description-dynamic-height": "We recommend enabling pagination with this option to avoid performance issues.", "label": {