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',