import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import moment from 'moment/moment'; import { cx } from '@emotion/css'; import { AckProblemData } from '../AckModal'; import { ProblemsPanelOptions, RTResized } from '../../types'; import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types'; import { TimeRange } from '@grafana/data'; import { DataSourceRef } from '@grafana/schema'; import { HostCell } from './Cells/HostCell'; import { SeverityCell } from './Cells/SeverityCell'; import { StatusIconCellV8 } from './Cells/StatusIconCell'; import { StatusCellV8 } from './Cells/StatusCell'; import { AckCell } from './Cells/AckCell'; import { TagCell } from './Cells/TagCell'; import { LastChangeCell } from './Cells/LastChangeCell'; import { ColumnResizeMode, createColumnHelper, flexRender, getCoreRowModel, getExpandedRowModel, getPaginationRowModel, useReactTable, } from '@tanstack/react-table'; import { reportInteraction } from '@grafana/runtime'; import { ProblemDetails } from './ProblemDetails'; export interface ProblemListProps { problems: ProblemDTO[]; panelOptions: ProblemsPanelOptions; loading?: boolean; timeRange?: TimeRange; range?: TimeRange; pageSize?: number; fontSize?: number; panelId?: number; getProblemEvents: (problem: ProblemDTO) => Promise; getProblemAlerts: (problem: ProblemDTO) => Promise; getScripts: (problem: ProblemDTO) => Promise; onExecuteScript: (problem: ProblemDTO, scriptid: string, scope: string) => Promise; onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => void; onTagClick?: (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => void; onPageSizeChange?: (pageSize: number, pageIndex: number) => void; onColumnResize?: (newResized: RTResized) => void; } const columnHelper = createColumnHelper(); export const ProblemList = (props: ProblemListProps) => { const { pageSize, fontSize, problems, panelOptions, onProblemAck, onPageSizeChange, onColumnResize, onTagClick, loading, timeRange, panelId, getProblemEvents, getProblemAlerts, getScripts, onExecuteScript, } = props; const rootRef = useRef(null); // Define columns inside component to access props via closure const columns = useMemo(() => { const highlightNewerThan = panelOptions.highlightNewEvents && panelOptions.highlightNewerThan; return [ columnHelper.accessor('host', { header: 'Host', size: 120, cell: ({ cell }) => , }), columnHelper.accessor('hostTechName', { header: 'Host (Technical Name)', size: 170, cell: ({ cell }) => , }), columnHelper.accessor('groups', { header: 'Host Groups', size: 150, cell: ({ cell }) => { const groups = cell.getValue() ?? []; return {groups.map((g) => g.name).join(', ')}; }, }), columnHelper.accessor('proxy', { header: 'Proxy', size: 120, }), columnHelper.accessor('priority', { header: 'Severity', size: 80, meta: { className: 'problem-severity', }, cell: ({ cell }) => ( ), }), columnHelper.display({ id: 'statusIcon', header: 'Status Icon', size: 50, meta: { className: 'problem-status-icon', }, cell: ({ cell }) => ( ), }), columnHelper.accessor('value', { header: 'Status', size: 70, cell: ({ cell }) => , }), columnHelper.accessor('name', { header: 'Problem', size: 250, minSize: 200, cell: ({ cell }) => {cell.getValue()}, }), columnHelper.accessor('opdata', { header: 'Operational data', size: 150, }), columnHelper.accessor('acknowledged', { header: 'Ack', size: 70, cell: ({ cell }) => , }), columnHelper.accessor('tags', { header: 'Tags', size: 150, meta: { className: 'problem-tags', }, cell: ({ cell }) => ( ), }), columnHelper.accessor('timestamp', { id: 'age', header: 'Age', size: 100, meta: { className: 'problem-age', }, cell: ({ cell }) => moment.unix(cell.row.original.timestamp), }), columnHelper.accessor('timestamp', { id: 'lastchange', header: 'Time', size: 150, meta: { className: 'last-change', }, cell: ({ cell }) => ( ), }), columnHelper.display({ header: null, id: 'expander', size: 60, meta: { className: 'custom-expander', }, cell: ({ row }) => ( ), }), ]; }, [panelOptions]); // Convert resizedColumns from old format to column sizing state const getColumnSizingFromResized = (resized?: RTResized): Record => { if (!resized || resized.length === 0) { return {}; } const sizing: Record = {}; resized.forEach((col) => { sizing[col.id] = col.value; }); return sizing; }; const [columnSizing, setColumnSizing] = useState>( getColumnSizingFromResized(panelOptions.resizedColumns) ); const [columnResizeMode] = useState('onChange'); // Default pageSize to 10 if not provided const effectivePageSize = pageSize || 10; // Pagination state const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: effectivePageSize, }); // Update pagination when pageSize prop changes useEffect(() => { setPagination((prev) => ({ ...prev, pageSize: effectivePageSize, })); }, [effectivePageSize]); const table = useReactTable({ data: problems, columns, enableColumnResizing: true, columnResizeMode, state: { columnSizing, pagination, }, onPaginationChange: setPagination, meta: { panelOptions, }, initialState: { columnVisibility: { host: panelOptions.hostField, hostTechName: panelOptions.hostTechNameField, groups: panelOptions.hostGroups, proxy: panelOptions.hostProxy, severity: panelOptions.severityField, statusIcon: panelOptions.statusIcon, opdata: panelOptions.opdataField, ack: panelOptions.ackField, tags: panelOptions.showTags, age: panelOptions.ageField, }, }, onColumnSizingChange: (updater) => { const newSizing = typeof updater === 'function' ? updater(columnSizing) : updater; setColumnSizing(newSizing); // Convert to old format for compatibility const resized: RTResized = Object.entries(newSizing).map(([id, value]) => ({ id, value: value as number, })); onColumnResize?.(resized); }, getRowCanExpand: () => true, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getPaginationRowModel: getPaginationRowModel(), }); const handleTagClick = (tag: ZBXTag, datasource: DataSourceRef, ctrlKey?: boolean, shiftKey?: boolean) => { onTagClick?.(tag, datasource, ctrlKey, shiftKey); }; // Helper functions for pagination interactions const reportPageChange = (action: 'next' | 'prev') => { reportInteraction('grafana_zabbix_panel_page_change', { action }); }; const reportPageSizeChange = (pageSize: number) => { reportInteraction('grafana_zabbix_panel_page_size_change', { pageSize }); }; const handlePageInputChange = (e: React.ChangeEvent) => { const inputValue = e.target.value; if (!inputValue) { return; } const pageNumber = Number(inputValue); const maxPage = table.getPageCount(); // Clamp the value between 1 and maxPage const clampedPage = Math.max(1, Math.min(pageNumber, maxPage)); const newPageIndex = clampedPage - 1; if (newPageIndex !== table.getState().pagination.pageIndex) { reportPageChange(newPageIndex > table.getState().pagination.pageIndex ? 'next' : 'prev'); table.setPageIndex(newPageIndex); } }; const handlePageInputBlur = (e: React.FocusEvent) => { // On blur, ensure the input shows a valid value const inputValue = e.target.value; if (!inputValue) { e.target.value = String(table.getState().pagination.pageIndex + 1); return; } const pageNumber = Number(inputValue); const maxPage = table.getPageCount(); const clampedPage = Math.max(1, Math.min(pageNumber, maxPage)); e.target.value = String(clampedPage); }; const handlePreviousPage = () => { reportPageChange('prev'); table.previousPage(); }; const handleNextPage = () => { reportPageChange('next'); table.nextPage(); }; const handlePageSizeChange = (e: React.ChangeEvent) => { const newPageSize = Number(e.target.value); reportPageSizeChange(newPageSize); table.setPageSize(newPageSize); onPageSizeChange?.(newPageSize, table.getState().pagination.pageIndex); }; // Calculate page size options const pageSizeOptions = React.useMemo(() => { let options = [5, 10, 20, 25, 50, 100]; if (pageSize) { options.push(pageSize); options = Array.from(new Set(options)).sort((a, b) => a - b); } return options; }, [pageSize]); return (
{loading && (
Loading...
)} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.length === 0 ? ( ) : ( table.getRowModel().rows.map((row, rowIndex) => ( {row.getVisibleCells().map((cell) => { const className = (cell.column.columnDef.meta as any)?.className; return ( ); })} {row.getIsExpanded() && ( )} )) )}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {header.column.getCanResize() && (
)}
No problems found
{flexRender(cell.column.columnDef.cell, cell.getContext())}
Page{' '} {' '} of {table.getPageCount()}
); };