diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts
index 7e9aa1052aa..97362791486 100644
--- a/packages/grafana-data/src/field/fieldOverrides.ts
+++ b/packages/grafana-data/src/field/fieldOverrides.ts
@@ -485,7 +485,10 @@ export const getLinksSupplier =
if (href) {
href = locationUtil.assureBaseUrl(href.replace(/\n/g, ''));
href = replaceVariables(href, dataLinkScopedVars, VariableFormatID.UriEncode);
- href = locationUtil.processUrl(href);
+
+ if (href?.length > 0) {
+ href = locationUtil.processUrl(href);
+ }
}
if (link.onClick) {
diff --git a/packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx
index 3e95d3946b4..4993c3c617b 100644
--- a/packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx
+++ b/packages/grafana-ui/src/components/Table/Cells/DataLinksCell.tsx
@@ -8,17 +8,20 @@ export const DataLinksCell = (props: TableCellProps) => {
return (
- {links &&
- links.map((link, idx) => {
- return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-
-
- {link.title}
-
-
- );
- })}
+ {links?.map((link, idx) => {
+ return !link.href && link.onClick == null ? (
+
+ {link.title}
+
+ ) : (
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+
+
+ {link.title}
+
+
+ );
+ })}
);
};
diff --git a/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx
index 5b0cf8bcc5e..8dbfabc32f3 100644
--- a/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx
+++ b/packages/grafana-ui/src/components/Table/Cells/DefaultCell.tsx
@@ -23,7 +23,8 @@ export const DefaultCell = (props: TableCellProps) => {
const showFilters = props.onCellFilterAdded && field.config.filterable;
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
const cellOptions = getCellOptions(field);
- const hasLinks = Boolean(getCellLinks(field, row)?.length);
+ const cellLinks = getCellLinks(field, row);
+ const hasLinks = cellLinks?.some((link) => link.href || link.onClick != null);
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
let value: string | ReactElement;
@@ -79,7 +80,9 @@ export const DefaultCell = (props: TableCellProps) => {
return (
{hasLinks ? (
-
getCellLinks(field, row) || []}>
+ getCellLinks(field, row)?.filter((link) => link.href || link.onClick != null) || []}
+ >
{(api) => {
if (api.openMenu) {
return (
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx
new file mode 100644
index 00000000000..f041887502b
--- /dev/null
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.test.tsx
@@ -0,0 +1,85 @@
+import { render, screen } from '@testing-library/react';
+
+import { Field, FieldType, LinkModel } from '@grafana/data';
+import { TableCellDisplayMode } from '@grafana/schema';
+
+import AutoCell from './AutoCell';
+
+describe('AutoCell', () => {
+ describe('Displays data Links', () => {
+ const getFieldWithLinks = (links: LinkModel[]): Field => {
+ return {
+ name: 'Category',
+ type: FieldType.string,
+ values: ['A', 'B', 'A', 'B', 'A'],
+ config: {
+ custom: {
+ cellOptions: {
+ type: TableCellDisplayMode.Auto,
+ wrapText: false,
+ },
+ },
+ },
+ display: (value: unknown) => ({
+ text: String(value),
+ numeric: 0,
+ color: undefined,
+ prefix: undefined,
+ suffix: undefined,
+ }),
+ state: {},
+ getLinks: () => links,
+ };
+ };
+
+ it('shows multiple datalinks in a context menu behind a button', () => {
+ const linksForField = [
+ { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
+ { href: 'http://asdasd2.com', title: 'Test Title2' } as LinkModel,
+ ];
+
+ jest.mock('../utils', () => ({
+ getCellLinks: () => linksForField,
+ }));
+
+ const field = getFieldWithLinks(linksForField);
+
+ render(
+
+ );
+ const submitButton = screen.getByRole('button');
+ expect(submitButton).toBeInTheDocument();
+ });
+
+ it('does not show button for menu for multiple links if one is invalid', () => {
+ const linksForField = [
+ { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
+ { title: 'Test Title2' } as LinkModel,
+ ];
+
+ jest.mock('../utils', () => ({
+ getCellLinks: () => linksForField,
+ }));
+
+ const field = getFieldWithLinks(linksForField);
+
+ render(
+
+ );
+ const submitButton = screen.queryByRole('button');
+ expect(submitButton).not.toBeInTheDocument();
+ });
+ });
+});
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 c484601c8b6..d40e363d569 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx
@@ -15,13 +15,16 @@ export default function AutoCell({ value, field, justifyContent, rowIdx, cellOpt
const displayValue = field.display!(value);
const formattedValue = formattedValueToString(displayValue);
- const hasLinks = Boolean(getCellLinks(field, rowIdx)?.length);
+ const cellLinks = getCellLinks(field, rowIdx);
+ const hasLinks = cellLinks?.some((link) => link.href || link.onClick != null);
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
return (
{hasLinks ? (
-
getCellLinks(field, rowIdx) || []}>
+ getCellLinks(field, rowIdx)?.filter((link) => link.href || link.onClick != null) || []}
+ >
{(api) => {
if (api.openMenu) {
return (
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.test.tsx
new file mode 100644
index 00000000000..a3584e48a98
--- /dev/null
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.test.tsx
@@ -0,0 +1,78 @@
+import { render, screen } from '@testing-library/react';
+
+import { Field, FieldType, LinkModel } from '@grafana/data';
+import { TableCellDisplayMode } from '@grafana/schema';
+
+import { DataLinksCell } from './DataLinksCell';
+
+describe('DataLinksCell', () => {
+ describe('Displays data Links', () => {
+ const getFieldWithLinks = (links: LinkModel[]): Field => {
+ return {
+ name: 'Category',
+ type: FieldType.string,
+ values: ['A', 'B', 'A', 'B', 'A'],
+ config: {
+ custom: {
+ cellOptions: {
+ type: TableCellDisplayMode.Auto,
+ wrapText: false,
+ },
+ },
+ },
+ display: (value: unknown) => ({
+ text: String(value),
+ numeric: 0,
+ color: undefined,
+ prefix: undefined,
+ suffix: undefined,
+ }),
+ state: {},
+ getLinks: () => links,
+ };
+ };
+
+ it('shows multiple datalinks in separate spans', () => {
+ const linksForField = [
+ { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
+ { href: 'http://asdasd2.com', title: 'Test Title2' } as LinkModel,
+ ];
+
+ jest.mock('../utils', () => ({
+ getCellLinks: () => linksForField,
+ }));
+
+ const field = getFieldWithLinks(linksForField);
+
+ render();
+
+ linksForField.forEach((link) => {
+ expect(screen.getByRole('link', { name: link.title })).toHaveAttribute('href', link.href);
+ });
+ });
+
+ it('Does not create a link if href is missing from link', () => {
+ const linksForField = [
+ { href: 'http://asdasd.com', title: 'Test Title' } as LinkModel,
+ { title: 'Test Title2' } as LinkModel,
+ ];
+
+ jest.mock('../utils', () => ({
+ getCellLinks: () => linksForField,
+ }));
+
+ const field = getFieldWithLinks(linksForField);
+
+ render();
+
+ linksForField.forEach((link) => {
+ if (link.href !== undefined) {
+ expect(screen.getByRole('link', { name: link.title })).toHaveAttribute('href', link.href);
+ } else {
+ expect(screen.queryByRole('link', { name: link.title })).not.toBeInTheDocument();
+ expect(screen.getByText(link.title)).toBeInTheDocument();
+ }
+ });
+ });
+ });
+});
diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx
index 858faa49b4d..5163d7414cd 100644
--- a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx
+++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx
@@ -15,7 +15,11 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => {
{links &&
links.map((link, idx) => {
- return (
+ return !link.href && link.onClick == null ? (
+
+ {link.title}
+
+ ) : (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
@@ -46,4 +50,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: theme.colors.text.link,
},
}),
+ cellLinkEmpty: css({
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ userSelect: 'text',
+ whiteSpace: 'nowrap',
+ fontWeight: theme.typography.fontWeightMedium,
+ paddingRight: theme.spacing(1.5),
+ }),
});
diff --git a/packages/grafana-ui/src/components/Table/TableRT/styles.ts b/packages/grafana-ui/src/components/Table/TableRT/styles.ts
index f276de36abc..24fcf5e17cc 100644
--- a/packages/grafana-ui/src/components/Table/TableRT/styles.ts
+++ b/packages/grafana-ui/src/components/Table/TableRT/styles.ts
@@ -192,6 +192,14 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
color: theme.colors.text.link,
},
}),
+ cellLinkEmpty: css({
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ userSelect: 'text',
+ whiteSpace: 'nowrap',
+ fontWeight: theme.typography.fontWeightMedium,
+ paddingRight: theme.spacing(1.5),
+ }),
cellLinkForColoredCell: css({
cursor: 'pointer',
overflow: 'hidden',