mirror of
https://github.com/grafana/grafana.git
synced 2026-07-03 03:37:53 +00:00
Table: Display alt text in image cell when image fails to load (#126904)
* chore(gdev): add panel to Table kitchen sink for image cell * test(table): add tests for ImageCell sub-component, TDD, bug test fails * fix(table): display alt text in ImageCell when image does not load --------- Co-authored-by: Paul Marbach <paul.marbach@grafana.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f8863d817
commit
26ce105050
3 changed files with 244 additions and 1 deletions
|
|
@ -1057,6 +1057,166 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"panel-13": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"id": 13,
|
||||
"title": "Image cell table",
|
||||
"description": "",
|
||||
"links": [],
|
||||
"data": {
|
||||
"kind": "QueryGroup",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"kind": "PanelQuery",
|
||||
"spec": {
|
||||
"query": {
|
||||
"kind": "DataQuery",
|
||||
"group": "grafana-testdata-datasource",
|
||||
"version": "v0",
|
||||
"spec": {
|
||||
"csvContent": "img_loaded,img_loaded_title,img_broken_alt,img_broken_noalt\nhttps://grafana.com/media/menus/products/grafana-menu-icon.svg,https://grafana.com/media/menus/products/grafana-menu-icon.svg,https://example.com/does-not-exist.png,https://example.com/does-not-exist.png",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
},
|
||||
"refId": "A",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [],
|
||||
"queryOptions": {}
|
||||
}
|
||||
},
|
||||
"vizConfig": {
|
||||
"kind": "VizConfig",
|
||||
"group": "table",
|
||||
"version": "12.2.0",
|
||||
"spec": {
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"showHeader": true
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"value": 0,
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"inspect": false
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "img_loaded"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Image (loaded)"
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 140
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "img_loaded_title"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"title": "Grafana logo",
|
||||
"type": "image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Image (loaded, with title)"
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 180
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "img_broken_alt"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"alt": "Image failed to load",
|
||||
"type": "image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Image (broken, with alt)"
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 200
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "img_broken_noalt"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Image (broken, no alt)"
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-2": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
|
|
@ -2629,6 +2789,19 @@
|
|||
"name": "panel-12"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "GridLayoutItem",
|
||||
"spec": {
|
||||
"x": 0,
|
||||
"y": 31,
|
||||
"width": 12,
|
||||
"height": 7,
|
||||
"element": {
|
||||
"kind": "ElementReference",
|
||||
"name": "panel-13"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import { type Field, FieldType } from '@grafana/data';
|
||||
|
||||
import { TableCellDisplayMode } from '../../types';
|
||||
|
||||
import { ImageCell } from './ImageCell';
|
||||
|
||||
const makeField = (url: string): Field => ({
|
||||
name: 'image',
|
||||
type: FieldType.string,
|
||||
values: [url],
|
||||
config: {},
|
||||
display: () => ({ text: url, numeric: 0, color: undefined }),
|
||||
});
|
||||
|
||||
const baseCellOptions = {
|
||||
type: TableCellDisplayMode.Image as const,
|
||||
};
|
||||
|
||||
describe('ImageCell', () => {
|
||||
it('renders an img element with the correct src when the URL is valid', () => {
|
||||
const url = 'https://example.com/image.png';
|
||||
render(<ImageCell cellOptions={baseCellOptions} field={makeField(url)} value={url} rowIdx={0} />);
|
||||
const img = screen.getByRole('img');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute('src', url);
|
||||
});
|
||||
|
||||
it('renders the configured alt text on the img element', () => {
|
||||
const url = 'https://example.com/image.png';
|
||||
const cellOptions = { ...baseCellOptions, alt: 'My alt text' };
|
||||
render(<ImageCell cellOptions={cellOptions} field={makeField(url)} value={url} rowIdx={0} />);
|
||||
expect(screen.getByRole('img')).toHaveAttribute('alt', 'My alt text');
|
||||
});
|
||||
|
||||
it('renders configured alt text instead of the raw URL when the image fails to load', () => {
|
||||
const url = 'https://example.com/broken.png';
|
||||
const cellOptions = { ...baseCellOptions, alt: 'Broken image alt' };
|
||||
render(<ImageCell cellOptions={cellOptions} field={makeField(url)} value={url} rowIdx={0} />);
|
||||
|
||||
fireEvent.error(screen.getByRole('img'));
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Broken image alt')).toBeInTheDocument();
|
||||
expect(screen.queryByText(url)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to the raw URL when the image fails to load and no alt text is configured', () => {
|
||||
const url = 'https://example.com/broken.png';
|
||||
render(<ImageCell cellOptions={baseCellOptions} field={makeField(url)} value={url} rowIdx={0} />);
|
||||
|
||||
fireEvent.error(screen.getByRole('img'));
|
||||
|
||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(url)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing when the field display text is empty', () => {
|
||||
const field: Field = {
|
||||
name: 'image',
|
||||
type: FieldType.string,
|
||||
values: [''],
|
||||
config: {},
|
||||
display: () => ({ text: '', numeric: 0, color: undefined }),
|
||||
};
|
||||
const { container } = render(<ImageCell cellOptions={baseCellOptions} field={field} value="" rowIdx={0} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
|
@ -17,7 +17,7 @@ export const ImageCell = ({ cellOptions, field, value, rowIdx }: ImageCellProps)
|
|||
|
||||
return (
|
||||
<MaybeWrapWithLink field={field} rowIdx={rowIdx}>
|
||||
{error ? text : <img alt={alt} src={text} title={title} onError={() => setError(true)} />}
|
||||
{error ? (alt ?? text) : <img alt={alt} src={text} title={title} onError={() => setError(true)} />}
|
||||
</MaybeWrapWithLink>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue