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": {