Add the ability to convert specific tags to columns on problems panel (#2113)

This closes #2112

Added the ability on problems panel to specify a comma separated list of
tag names to be converted to columns.
If a tag name is present multiple times, it will return the value of all
tags separated with comma.
For optimum readability, the tag names are Capitalized for the visible
column name.
Also, for optimum readability, the custom tags are always placed before
the "Tags" column.

In case a tag is not there for a problem, an empty string is returned.

---------

Co-authored-by: Jocelyn Collado-Kuri <jcolladokuri@icloud.com>
This commit is contained in:
Christos Diamantis
2026-01-15 22:36:36 +02:00
committed by GitHub
parent 95554b0b8c
commit 3ada0d15e6
6 changed files with 99 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
'grafana-zabbix': patch
---
Enhance the problems panel with the ability to convert specific tags to columns. Single or multiple tags supported.

View File

@@ -25,6 +25,7 @@ import {
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { ProblemDetails } from './ProblemDetails'; import { ProblemDetails } from './ProblemDetails';
import { capitalizeFirstLetter, parseCustomTagColumns } from './utils';
export interface ProblemListProps { export interface ProblemListProps {
problems: ProblemDTO[]; problems: ProblemDTO[];
@@ -47,6 +48,33 @@ export interface ProblemListProps {
const columnHelper = createColumnHelper<ProblemDTO>(); const columnHelper = createColumnHelper<ProblemDTO>();
const buildCustomTagColumns = (customTagColumns?: string) => {
const tagNames = parseCustomTagColumns(customTagColumns);
return tagNames.map((tagName) =>
columnHelper.accessor(
(row) => {
const tags = row.tags ?? [];
const values = tags
.filter((t) => t.tag === tagName)
.map((t) => t.value)
.filter(Boolean);
return values.length ? values.join(', ') : '';
},
{
id: `problem-tag_${tagName}`,
header: capitalizeFirstLetter(tagName),
size: 150,
meta: {
className: `problem-tag_${tagName}`,
},
cell: ({ getValue }) => <span>{getValue() as string}</span>,
}
)
);
};
export const ProblemList = (props: ProblemListProps) => { export const ProblemList = (props: ProblemListProps) => {
const { const {
pageSize, pageSize,
@@ -72,6 +100,8 @@ export const ProblemList = (props: ProblemListProps) => {
const columns = useMemo(() => { const columns = useMemo(() => {
const highlightNewerThan = panelOptions.highlightNewEvents && panelOptions.highlightNewerThan; const highlightNewerThan = panelOptions.highlightNewEvents && panelOptions.highlightNewerThan;
const customTagColumns = buildCustomTagColumns(panelOptions.customTagColumns);
return [ return [
columnHelper.accessor('host', { columnHelper.accessor('host', {
header: 'Host', header: 'Host',
@@ -146,6 +176,7 @@ export const ProblemList = (props: ProblemListProps) => {
size: 70, size: 70,
cell: ({ cell }) => <AckCell acknowledges={cell.row.original.acknowledges} />, cell: ({ cell }) => <AckCell acknowledges={cell.row.original.acknowledges} />,
}), }),
...customTagColumns,
columnHelper.accessor('tags', { columnHelper.accessor('tags', {
header: 'Tags', header: 'Tags',
size: 150, size: 150,

View File

@@ -0,0 +1,38 @@
import { capitalizeFirstLetter, parseCustomTagColumns } from './utils';
describe('capitalizeFirstLetter', () => {
it('capitalizes first letter and lowercases the rest', () => {
expect(capitalizeFirstLetter('zabbixgrafana')).toBe('Zabbixgrafana');
expect(capitalizeFirstLetter('ZABBIXGRAFANA')).toBe('Zabbixgrafana');
expect(capitalizeFirstLetter('zAbBiXgRaFaNa')).toBe('Zabbixgrafana');
});
it('returns empty string for empty input', () => {
expect(capitalizeFirstLetter('')).toBe('');
});
it('handles single-character strings', () => {
expect(capitalizeFirstLetter('a')).toBe('A');
expect(capitalizeFirstLetter('A')).toBe('A');
});
});
describe('parseCustomTagColumns', () => {
it('returns empty array for undefined or empty input', () => {
expect(parseCustomTagColumns(undefined)).toEqual([]);
expect(parseCustomTagColumns('')).toEqual([]);
expect(parseCustomTagColumns(' ')).toEqual([]);
});
it('splits comma-separated values and trims whitespace', () => {
expect(parseCustomTagColumns('env, region ,service')).toEqual(['env', 'region', 'service']);
});
it('filters out empty values', () => {
expect(parseCustomTagColumns('env,, ,region,')).toEqual(['env', 'region']);
});
it('preserves order', () => {
expect(parseCustomTagColumns('a,b,c')).toEqual(['a', 'b', 'c']);
});
});

View File

@@ -0,0 +1,12 @@
export const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
export const parseCustomTagColumns = (customTagColumns?: string): string[] => {
if (!customTagColumns) {
return [];
}
return customTagColumns
.split(',')
.map((tagName) => tagName.trim())
.filter(Boolean);
};

View File

@@ -220,6 +220,17 @@ export const plugin = new PanelPlugin<ProblemsPanelOptions, {}>(ProblemsPanel)
name: 'Datasource name', name: 'Datasource name',
defaultValue: defaultPanelOptions.showDatasourceName, defaultValue: defaultPanelOptions.showDatasourceName,
category: ['Fields'], category: ['Fields'],
})
// Select tag name to display as column
.addTextInput({
path: 'customTagColumns',
name: 'Tags to columns',
defaultValue: '',
description: 'Comma-separated list of tag names to display as columns (e.g., component, scope, environment)',
settings: {
placeholder: 'component, scope, target',
},
category: ['Fields'],
}); });
}); });

View File

@@ -42,6 +42,8 @@ export interface ProblemsPanelOptions {
okEventColor: TriggerColor; okEventColor: TriggerColor;
ackEventColor: TriggerColor; ackEventColor: TriggerColor;
markAckEvents?: boolean; markAckEvents?: boolean;
// Custom tag names to display as column
customTagColumns?: string;
} }
export const DEFAULT_SEVERITY: TriggerSeverity[] = [ export const DEFAULT_SEVERITY: TriggerSeverity[] = [