diff --git a/.changeset/open-dolls-arrive.md b/.changeset/open-dolls-arrive.md new file mode 100644 index 0000000..a8a71db --- /dev/null +++ b/.changeset/open-dolls-arrive.md @@ -0,0 +1,5 @@ +--- +'grafana-zabbix': patch +--- + +Upgrade react-table to v8 diff --git a/package.json b/package.json index e7ecf65..ea60253 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@swc/core": "^1.3.90", "@swc/helpers": "^0.5.0", "@swc/jest": "^0.2.26", + "@tanstack/react-table": "^8.21.3", "@testing-library/jest-dom": "6.1.4", "@testing-library/react": "14.0.0", "@types/glob": "^8.0.0", @@ -93,7 +94,6 @@ "postcss-scss": "4.0.4", "prettier": "^3.0.3", "prop-types": "15.7.2", - "react-table-6": "6.11.0", "react-use": "17.4.0", "replace-in-file-webpack-plugin": "^1.0.6", "sass": "1.63.2", diff --git a/src/datasource/types.ts b/src/datasource/types.ts index 2ec29ba..8df4885 100644 --- a/src/datasource/types.ts +++ b/src/datasource/types.ts @@ -108,6 +108,7 @@ export interface ProblemDTO { datasource?: DataSourceRef | string; comments?: string; host?: string; + hostInMaintenance?: boolean; hostTechName?: string; proxy?: string; severity?: string; diff --git a/src/panel-triggers/components/Problems/AckCell.tsx b/src/panel-triggers/components/Problems/AckCell.tsx deleted file mode 100644 index 5872406..0000000 --- a/src/panel-triggers/components/Problems/AckCell.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { css } from '@emotion/css'; -import { RTCell } from '../../types'; -import { ProblemDTO } from '../../../datasource/types'; -import { FAIcon } from '../../../components'; -import { useTheme, stylesFactory } from '@grafana/ui'; -import { GrafanaTheme } from '@grafana/data'; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - countLabel: css` - font-size: ${theme.typography.size.sm}; - `, - }; -}); - -export const AckCell: React.FC> = (props: RTCell) => { - const problem = props.original; - const theme = useTheme(); - const styles = getStyles(theme); - - return ( -
- {problem.acknowledges?.length > 0 && ( - <> - - ({problem.acknowledges?.length}) - - )} -
- ); -}; - -export default AckCell; diff --git a/src/panel-triggers/components/Problems/Cells/AckCell.tsx b/src/panel-triggers/components/Problems/Cells/AckCell.tsx new file mode 100644 index 0000000..59c3248 --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/AckCell.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { css } from '@emotion/css'; +import { ZBXAcknowledge } from '../../../../datasource/types'; +import { FAIcon } from '../../../../components'; +import { useTheme2 } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + countLabel: css` + font-size: ${theme.typography.fontSize}; + `, + }; +}; + +export const AckCell = (props: { acknowledges?: ZBXAcknowledge[] }) => { + const acknowledges = props.acknowledges || []; + const styles = getStyles(useTheme2()); + + return ( +
+ {acknowledges?.length > 0 && ( + <> + + ({acknowledges?.length}) + + )} +
+ ); +}; diff --git a/src/panel-triggers/components/Problems/Cells/HostCell.tsx b/src/panel-triggers/components/Problems/Cells/HostCell.tsx new file mode 100644 index 0000000..930a636 --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/HostCell.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import FAIcon from '../../../../components/FAIcon/FAIcon'; + +interface HostCellProps { + name: string; + maintenance: boolean; +} + +export const HostCell: React.FC = ({ name, maintenance }) => { + return ( +
+ {name} + {maintenance && } +
+ ); +}; diff --git a/src/panel-triggers/components/Problems/Cells/LastChangeCell.tsx b/src/panel-triggers/components/Problems/Cells/LastChangeCell.tsx new file mode 100644 index 0000000..1945cd0 --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/LastChangeCell.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import moment from 'moment/moment'; +import { ProblemDTO } from '../../../../datasource/types'; + +export function LastChangeCell(props: { original: ProblemDTO; customFormat?: string }) { + const { original, customFormat } = props; + const DEFAULT_TIME_FORMAT = 'DD MMM YYYY HH:mm:ss'; + const timestamp = moment.unix(original.timestamp); + const format = customFormat || DEFAULT_TIME_FORMAT; + return {timestamp.format(format)}; +} diff --git a/src/panel-triggers/components/Problems/Cells/SeverityCell.tsx b/src/panel-triggers/components/Problems/Cells/SeverityCell.tsx new file mode 100644 index 0000000..42cad9b --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/SeverityCell.tsx @@ -0,0 +1,42 @@ +import { TriggerSeverity } from '../../../types'; +import { ProblemDTO } from '../../../../datasource/types'; +import _ from 'lodash'; +import React from 'react'; +import { DEFAULT_OK_COLOR } from '../constants'; +import { Cell } from '@tanstack/react-table'; + +export function SeverityCell(props: { + cell: Cell; + problemSeverityDesc: TriggerSeverity[]; + markAckEvents?: boolean; + ackEventColor?: string; + okColor?: string; +}) { + const { cell, problemSeverityDesc, markAckEvents, ackEventColor, okColor = DEFAULT_OK_COLOR } = props; + const { + row: { + original: { severity, acknowledged }, + }, + } = cell; + let color: string; + + let severityDesc: TriggerSeverity; + const severityAsNum = Number(severity); + severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severityAsNum); + if (severity && cell.getValue() === '1') { + severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severityAsNum); + } + + color = cell.getValue() === '0' ? okColor : severityDesc.color; + + // Mark acknowledged triggers with different color + if (markAckEvents && acknowledged === '1') { + color = ackEventColor; + } + + return ( +
+ {severityDesc.severity} +
+ ); +} diff --git a/src/panel-triggers/components/Problems/Cells/StatusCell.tsx b/src/panel-triggers/components/Problems/Cells/StatusCell.tsx new file mode 100644 index 0000000..c9c0d9e --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/StatusCell.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Cell } from '@tanstack/react-table'; +import { isNewProblem } from '../../../utils'; +import { ProblemDTO } from '../../../../datasource/types'; +import { DEFAULT_OK_COLOR, DEFAULT_PROBLEM_COLOR } from '../constants'; + +export function StatusCellV8(props: { cell: Cell; highlightNewerThan?: string }) { + const { cell, highlightNewerThan } = props; + const status = cell.getValue() === '0' ? 'RESOLVED' : 'PROBLEM'; + const color = cell.getValue() === '0' ? DEFAULT_OK_COLOR : DEFAULT_PROBLEM_COLOR; + let newProblem = false; + if (highlightNewerThan) { + newProblem = isNewProblem(cell.row.original, highlightNewerThan); + } + return ( + + {status} + + ); +} diff --git a/src/panel-triggers/components/Problems/Cells/StatusIconCell.tsx b/src/panel-triggers/components/Problems/Cells/StatusIconCell.tsx new file mode 100644 index 0000000..b633b3a --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/StatusIconCell.tsx @@ -0,0 +1,22 @@ +import { ProblemDTO } from '../../../../datasource/types'; +import { isNewProblem } from '../../../utils'; +import React from 'react'; +import { Row } from '@tanstack/react-table'; +import { cx } from '@emotion/css'; +import { GFHeartIcon } from '../../../../components'; + +export function StatusIconCellV8(props: { cellValue: string; row: Row; highlightNewerThan?: string }) { + const { cellValue, row, highlightNewerThan } = props; + const status = cellValue === '0' ? 'ok' : 'problem'; + let newProblem = false; + if (highlightNewerThan) { + newProblem = isNewProblem(row.original, highlightNewerThan); + } + const className = cx( + 'zbx-problem-status-icon', + { 'problem-status--new': newProblem }, + { 'zbx-problem': cellValue === '1' }, + { 'zbx-ok': cellValue === '0' } + ); + return ; +} diff --git a/src/panel-triggers/components/Problems/Cells/TagCell.tsx b/src/panel-triggers/components/Problems/Cells/TagCell.tsx new file mode 100644 index 0000000..eb2867c --- /dev/null +++ b/src/panel-triggers/components/Problems/Cells/TagCell.tsx @@ -0,0 +1,27 @@ +import { ZBXTag } from '../../../../datasource/types'; +import { DataSourceRef } from '@grafana/schema'; +import React from 'react'; +import { EventTag } from '../../EventTag'; + +interface TagCellProps { + tags?: ZBXTag[]; + dataSource: DataSourceRef; + ctrlKey?: boolean; + shiftKey?: boolean; + handleTagClick: (tag: ZBXTag, datasource?: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void; +} + +export const TagCell = (props: TagCellProps) => { + const { tags, dataSource, handleTagClick } = props; + + return [ + (tags ?? []).map((tag) => ( + handleTagClick?.(tag, dataSource)} + /> + )), + ]; +}; diff --git a/src/panel-triggers/components/Problems/ProblemDetails.tsx b/src/panel-triggers/components/Problems/ProblemDetails.tsx index 0a42853..3ad8cc6 100644 --- a/src/panel-triggers/components/Problems/ProblemDetails.tsx +++ b/src/panel-triggers/components/Problems/ProblemDetails.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { css } from '@emotion/css'; -// eslint-disable-next-line -import moment from 'moment'; +import moment from 'moment/moment'; import { GrafanaTheme2, TimeRange } from '@grafana/data'; import { DataSourceRef } from '@grafana/schema'; import { Tooltip, useStyles2 } from '@grafana/ui'; @@ -15,13 +14,13 @@ import ProblemTimeline from './ProblemTimeline'; import { AckButton, ExecScriptButton, ExploreButton, FAIcon, ModalController } from '../../../components'; import { ExecScriptData, ExecScriptModal } from '../ExecScriptModal'; import ProblemStatusBar from './ProblemStatusBar'; -import { RTRow } from '../../types'; import { ProblemItems } from './ProblemItems'; import { ProblemHosts, ProblemHostsDescription } from './ProblemHosts'; import { ProblemGroups } from './ProblemGroups'; import { ProblemExpression } from './ProblemExpression'; -interface Props extends RTRow { +interface Props { + original: ProblemDTO; rootWidth: number; timeRange: TimeRange; showTimeline?: boolean; diff --git a/src/panel-triggers/components/Problems/ProblemTimeline.tsx b/src/panel-triggers/components/Problems/ProblemTimeline.tsx index f9d2751..a258c54 100644 --- a/src/panel-triggers/components/Problems/ProblemTimeline.tsx +++ b/src/panel-triggers/components/Problems/ProblemTimeline.tsx @@ -2,15 +2,16 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; // eslint-disable-next-line import moment from 'moment'; -import { ZBXEvent, ZBXAcknowledge } from '../../../datasource/types'; +import { ZBXAcknowledge, ZBXEvent } from '../../../datasource/types'; import { TimeRange } from '@grafana/data'; - -const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; -const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; -const EVENT_POINT_SIZE = 16; -const INNER_POINT_SIZE = 0.6; -const HIGHLIGHTED_POINT_SIZE = 1.1; -const EVENT_REGION_HEIGHT = Math.round(EVENT_POINT_SIZE * 0.6); +import { + DEFAULT_OK_COLOR, + DEFAULT_PROBLEM_COLOR, + EVENT_POINT_SIZE, + EVENT_REGION_HEIGHT, + HIGHLIGHTED_POINT_SIZE, + INNER_POINT_SIZE, +} from './constants'; export interface ProblemTimelineProps { events: ZBXEvent[]; diff --git a/src/panel-triggers/components/Problems/Problems.tsx b/src/panel-triggers/components/Problems/Problems.tsx index 0585a04..ee06481 100644 --- a/src/panel-triggers/components/Problems/Problems.tsx +++ b/src/panel-triggers/components/Problems/Problems.tsx @@ -1,21 +1,30 @@ -import React, { PureComponent, useRef, useState, useMemo } from 'react'; +import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import moment from 'moment/moment'; import { cx } from '@emotion/css'; -import ReactTable from 'react-table-6'; -import _ from 'lodash'; -// eslint-disable-next-line -import moment from 'moment'; -import { isNewProblem } from '../../utils'; -import { EventTag } from '../EventTag'; -import { ProblemDetails } from './ProblemDetails'; import { AckProblemData } from '../AckModal'; -import { FAIcon, GFHeartIcon } from '../../../components'; -import { ProblemsPanelOptions, RTCell, RTResized, TriggerSeverity } from '../../types'; +import { ProblemsPanelOptions, RTResized } from '../../types'; import { ProblemDTO, ZBXAlert, ZBXEvent, ZBXTag } from '../../../datasource/types'; import { APIExecuteScriptResponse, ZBXScript } from '../../../datasource/zabbix/connectors/zabbix_api/types'; -import { AckCell } from './AckCell'; 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[]; @@ -36,6 +45,8 @@ export interface ProblemListProps { onColumnResize?: (newResized: RTResized) => void; } +const columnHelper = createColumnHelper(); + export const ProblemList = (props: ProblemListProps) => { const { pageSize, @@ -55,368 +66,405 @@ export const ProblemList = (props: ProblemListProps) => { onExecuteScript, } = props; - const [expanded, setExpanded] = useState({}); - const [expandedProblems, setExpandedProblems] = useState({}); - const [page, setPage] = useState(0); 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; - const handleProblemAck = (problem: ProblemDTO, data: AckProblemData) => { - return onProblemAck!(problem, data); - }; + // Pagination state + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: effectivePageSize, + }); - const handlePageSizeChange = (pageSize, pageIndex) => { - onPageSizeChange?.(pageSize, pageIndex); - }; + // Update pagination when pageSize prop changes + useEffect(() => { + setPagination((prev) => ({ + ...prev, + pageSize: effectivePageSize, + })); + }, [effectivePageSize]); - const handleResizedChange = (newResized, event) => { - onColumnResize?.(newResized); - }; + 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); - const handleExpandedChange = (expandedChange: any, event: any) => { - reportInteraction('grafana_zabbix_panel_row_expanded', {}); - const newExpandedProblems = {}; + // Convert to old format for compatibility + const resized: RTResized = Object.entries(newSizing).map(([id, value]) => ({ + id, + value: value as number, + })); - for (const row in expandedChange) { - const rowId = Number(row); - const problemIndex = effectivePageSize * page + rowId; - if (expandedChange[row] && problemIndex < problems.length) { - const expandedProblem = problems[problemIndex].eventid; - if (expandedProblem) { - newExpandedProblems[expandedProblem] = true; - } - } - } - - const nextExpanded = { ...expanded }; - nextExpanded[page] = expandedChange; - - const nextExpandedProblems = { ...expandedProblems }; - nextExpandedProblems[page] = newExpandedProblems; - - setExpanded(nextExpanded); - setExpandedProblems(nextExpandedProblems); - }; + 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); }; - const getExpandedPage = (page: number) => { - const expandedProblemsPage = expandedProblems[page] || {}; - const expandedPage = {}; - - // Go through the page and search for expanded problems - const startIndex = effectivePageSize * page; - const endIndex = Math.min(startIndex + effectivePageSize, problems.length); - for (let i = startIndex; i < endIndex; i++) { - const problem = problems[i]; - if (expandedProblemsPage[problem.eventid]) { - expandedPage[i - startIndex] = {}; - } - } - - return expandedPage; + // Helper functions for pagination interactions + const reportPageChange = (action: 'next' | 'prev') => { + reportInteraction('grafana_zabbix_panel_page_change', { action }); }; - const columns = useMemo(() => { - const result = []; - const highlightNewerThan = panelOptions.highlightNewEvents && panelOptions.highlightNewerThan; - const statusCell = (props) => StatusCell(props, highlightNewerThan); - const statusIconCell = (props) => StatusIconCell(props, highlightNewerThan); - const hostNameCell = (props) => ( - - ); - const hostTechNameCell = (props) => ( - - ); + const reportPageSizeChange = (pageSize: number) => { + reportInteraction('grafana_zabbix_panel_page_size_change', { pageSize }); + }; - const allColumns = [ - { Header: 'Host', id: 'host', show: panelOptions.hostField, Cell: hostNameCell }, - { - Header: 'Host (Technical Name)', - id: 'hostTechName', - show: panelOptions.hostTechNameField, - Cell: hostTechNameCell, - }, - { Header: 'Host Groups', accessor: 'groups', show: panelOptions.hostGroups, Cell: GroupCell }, - { Header: 'Proxy', accessor: 'proxy', show: panelOptions.hostProxy }, - { - Header: 'Severity', - show: panelOptions.severityField, - className: 'problem-severity', - width: 120, - accessor: (problem) => problem.priority, - id: 'severity', - Cell: (props) => - SeverityCell( - props, - panelOptions.triggerSeverity, - panelOptions.markAckEvents, - panelOptions.ackEventColor, - panelOptions.okEventColor - ), - }, - { - Header: '', - id: 'statusIcon', - show: panelOptions.statusIcon, - className: 'problem-status-icon', - width: 50, - accessor: 'value', - Cell: statusIconCell, - }, - { Header: 'Status', accessor: 'value', show: panelOptions.statusField, width: 100, Cell: statusCell }, - { Header: 'Problem', accessor: 'name', minWidth: 200, Cell: ProblemCell }, - { Header: 'Operational data', accessor: 'opdata', show: panelOptions.opdataField, width: 150, Cell: OpdataCell }, - { - Header: 'Ack', - id: 'ack', - show: panelOptions.ackField, - width: 70, - Cell: (props) => , - }, - { - Header: 'Tags', - accessor: 'tags', - show: panelOptions.showTags, - className: 'problem-tags', - Cell: (props) => , - }, - { - Header: 'Age', - className: 'problem-age', - width: 100, - show: panelOptions.ageField, - accessor: 'timestamp', - id: 'age', - Cell: AgeCell, - }, - { - Header: 'Time', - className: 'last-change', - width: 150, - accessor: 'timestamp', - id: 'lastchange', - Cell: (props) => LastChangeCell(props, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat), - }, - { Header: '', className: 'custom-expander', width: 60, expander: true, Expander: CustomExpander }, - ]; - for (const column of allColumns) { - if (column.show || column.show === undefined) { - delete column.show; - result.push(column); - } + const handlePageInputChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + if (!inputValue) { + return; } - return result; - }, [panelOptions, handleTagClick]); + const pageNumber = Number(inputValue); + const maxPage = table.getPageCount(); - const pageSizeOptions = useMemo(() => { + // 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 = _.uniq(_.sortBy(options)); + options = Array.from(new Set(options)).sort((a, b) => a - b); } return options; }, [pageSize]); return (
- ( - +
+ {loading && ( +
+
Loading...
+
)} - expanded={getExpandedPage(page)} - onExpandedChange={handleExpandedChange} - onPageChange={(newPage) => { - reportInteraction('grafana_zabbix_panel_page_change', { - action: newPage > page ? 'next' : 'prev', - }); - - setPage(newPage); - }} - onPageSizeChange={handlePageSizeChange} - onResizedChange={handleResizedChange} - /> + + + {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()} + + + +
+
); }; - -interface HostCellProps { - name: string; - maintenance: boolean; -} - -const HostCell: React.FC = ({ name, maintenance }) => { - return ( -
- {name} - {maintenance && } -
- ); -}; - -function SeverityCell( - props: RTCell, - problemSeverityDesc: TriggerSeverity[], - markAckEvents?: boolean, - ackEventColor?: string, - okColor = DEFAULT_OK_COLOR -) { - const problem = props.original; - let color: string; - - let severityDesc: TriggerSeverity; - const severity = Number(problem.severity); - severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severity); - if (problem.severity && problem.value === '1') { - severityDesc = _.find(problemSeverityDesc, (s) => s.priority === severity); - } - - color = problem.value === '0' ? okColor : severityDesc.color; - - // Mark acknowledged triggers with different color - if (markAckEvents && problem.acknowledged === '1') { - color = ackEventColor; - } - - return ( -
- {severityDesc.severity} -
- ); -} - -const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; -const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; - -function StatusCell(props: RTCell, highlightNewerThan?: string) { - const status = props.value === '0' ? 'RESOLVED' : 'PROBLEM'; - const color = props.value === '0' ? DEFAULT_OK_COLOR : DEFAULT_PROBLEM_COLOR; - let newProblem = false; - if (highlightNewerThan) { - newProblem = isNewProblem(props.original, highlightNewerThan); - } - return ( - - {status} - - ); -} - -function StatusIconCell(props: RTCell, highlightNewerThan?: string) { - const status = props.value === '0' ? 'ok' : 'problem'; - let newProblem = false; - if (highlightNewerThan) { - newProblem = isNewProblem(props.original, highlightNewerThan); - } - const className = cx( - 'zbx-problem-status-icon', - { 'problem-status--new': newProblem }, - { 'zbx-problem': props.value === '1' }, - { 'zbx-ok': props.value === '0' } - ); - return ; -} - -function GroupCell(props: RTCell) { - let groups = ''; - if (props.value && props.value.length) { - groups = props.value.map((g) => g.name).join(', '); - } - return {groups}; -} - -function ProblemCell(props: RTCell) { - // const comments = props.original.comments; - return ( -
- {props.value} - {/* {comments && } */} -
- ); -} - -function OpdataCell(props: RTCell) { - const problem = props.original; - return ( -
- {problem.opdata} -
- ); -} - -function AgeCell(props: RTCell) { - const problem = props.original; - const timestamp = moment.unix(problem.timestamp); - const age = timestamp.fromNow(true); - return {age}; -} - -function LastChangeCell(props: RTCell, customFormat?: string) { - const DEFAULT_TIME_FORMAT = 'DD MMM YYYY HH:mm:ss'; - const problem = props.original; - const timestamp = moment.unix(problem.timestamp); - const format = customFormat || DEFAULT_TIME_FORMAT; - const lastchange = timestamp.format(format); - return {lastchange}; -} - -interface TagCellProps extends RTCell { - onTagClick: (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => void; -} - -class TagCell extends PureComponent { - handleTagClick = (tag: ZBXTag, datasource: DataSourceRef | string, ctrlKey?: boolean, shiftKey?: boolean) => { - if (this.props.onTagClick) { - this.props.onTagClick(tag, datasource, ctrlKey, shiftKey); - } - }; - - render() { - const tags = this.props.value || []; - return [ - tags.map((tag) => ( - - )), - ]; - } -} - -function CustomExpander(props: RTCell) { - return ( - - - - ); -} diff --git a/src/panel-triggers/components/Problems/constants.ts b/src/panel-triggers/components/Problems/constants.ts new file mode 100644 index 0000000..99e3033 --- /dev/null +++ b/src/panel-triggers/components/Problems/constants.ts @@ -0,0 +1,6 @@ +export const DEFAULT_OK_COLOR = 'rgb(56, 189, 113)'; +export const DEFAULT_PROBLEM_COLOR = 'rgb(215, 0, 0)'; +export const EVENT_POINT_SIZE = 16; +export const INNER_POINT_SIZE = 0.6; +export const HIGHLIGHTED_POINT_SIZE = 1.1; +export const EVENT_REGION_HEIGHT = Math.round(EVENT_POINT_SIZE * 0.6); diff --git a/src/panel-triggers/types.ts b/src/panel-triggers/types.ts index c1ee546..27d4a1c 100644 --- a/src/panel-triggers/types.ts +++ b/src/panel-triggers/types.ts @@ -125,50 +125,6 @@ export interface TriggerSeverity { export type TriggerColor = string; -export interface RTRow { - /** the materialized row of data */ - row: any; - /** the original row of data */ - original: T; - /** the index of the row in the original array */ - index: number; - /** the index of the row relative to the current view */ - viewIndex: number; - /** the nesting level of this row */ - level: number; - /** the nesting path of this row */ - nestingPath: number[]; - /** true if this row's values were aggregated */ - aggregated?: boolean; - /** true if this row was produced by a pivot */ - groupedByPivot?: boolean; - /** any sub rows defined by the `subRowKey` prop */ - subRows?: boolean; -} - -export interface RTCell extends RTRow { - /** true if this row is expanded */ - isExpanded?: boolean; - /** the materialized value of this cell */ - value: any; - /** the resize information for this cell's column */ - resized: any[]; - /** true if the column is visible */ - show?: boolean; - /** the resolved width of this cell */ - width: number; - /** the resolved maxWidth of this cell */ - maxWidth: number; - /** the resolved tdProps from `getTdProps` for this cell */ - tdProps: any; - /** the resolved column props from 'getProps' for this cell's column */ - columnProps: any; - /** the resolved array of classes for this cell */ - classes: string[]; - /** the resolved styles for this cell */ - styles: any; -} - export interface RTResize { id: string; value: number; diff --git a/src/styles/_panel-problems.scss b/src/styles/_panel-problems.scss index 6d1eb3a..6cd38a3 100644 --- a/src/styles/_panel-problems.scss +++ b/src/styles/_panel-problems.scss @@ -1,4 +1,6 @@ .panel-problems { + display: flex; + flex-direction: column; height: 100%; .fa-icon-container { @@ -15,213 +17,26 @@ } } - // styles - .ReactTable { + // styles + .react-table-v8-wrapper { height: 100%; display: flex; flex-direction: column; - overflow: hidden; + overflow-y: auto; + overflow-x: auto; + } - .rt-table { - height: 100%; - } - - .rt-thead { - &.-header { - height: 2.4em; - } - - .rt-th { - &.-sort-desc { - box-shadow: inset 0 -3px 0 0 $blue; - } - - &.-sort-asc { - box-shadow: inset 0 3px 0 0 $blue; - } - } - } - - .rt-tr-group { - flex: 0 0 auto; + .react-table-v8 { + tbody tr { border-bottom: solid 1px $problems-border-color; - // border-left: solid 2px transparent; &:last-child { border-bottom: solid 1px $problems-border-color; - } - - &:hover { - - .rt-tr { - background: $problems-table-row-hovered; - z-index: 1; - } - - .rt-tr.-even { - background: $problems-table-row-hovered; - } - } - - .rt-tr { - .rt-td { - border: 0; - transition: 0s; - } - - &.-even { - background: $problems-table-stripe; - } - } - } - - .rt-noData { - z-index: 2; - background: unset; - color: $text-muted; - } - - .pagination-bottom { - margin-top: auto; - flex: 1 0 auto; - display: flex; - flex-direction: column; - align-items: center; - $footer-height: 30px; - padding-top: 4px; - margin-left: -10px; - margin-right: -10px; - box-shadow: 0px -2px 5px $problems-footer-shadow; - z-index: 1; - - .-pagination { - margin-top: auto; - align-items: center; - padding: 0px; - - .-previous { - flex: 1; - height: $footer-height; - } - - .-center { - flex: 3; - width: 32rem; - - .-pageJump { - margin: 0px 0.4rem; - - input { - height: $footer-height; - } - } - - .select-wrap.-pageSizeOptions select { - width: 8rem; - height: $footer-height; - } - } - - .-next { - flex: 1; - height: $footer-height; - } - } - } - - .problem-severity { - - &.rt-td { - padding: 0px; - } - - .severity-cell { - width: 100%; - height: 100%; - padding: 0.45em 0 0.45em 1.1em; - color: $white; - font-weight: 500; - } - } - - .problem-status--new { - animation: blink-opacity 2s ease-in-out infinite; - } - - .rt-tr .rt-td { - &.custom-expander { - text-align: center; - padding: 0.6em 0 0 0; - - i { - color: #676767; - font-size: 1.2rem; - } - - & .expanded { - i { - color: $problem-expander-expanded-color; - } - } - } - - &.custom-expander:hover { - background-color: $problem-expander-highlighted-background; - - i { - color: $problem-expander-highlighted-color; - } - } - - &.last-change { text-align: left; } - - &.problem-status-icon { - padding: 0; - margin-top: 0; - font-size: 1.5em; - - i { - width: 100%; - padding-left: 0.6em; - text-align: center; - - &.zbx-problem { - color: $problem-icon-problem-color; - } - - &.zbx-ok { - color: $problem-icon-ok-color; - } - } - } - } - - .comments-icon { - float: right; - padding-right: 0.6rem; - - i { - color: $gray-2; - } - } - - .problem-tags { - &.rt-td { - padding-left: 0.6rem; - } - - .label-tag, .zbx-tag { - cursor: pointer; - margin-right: 0.6rem; - - &.highlighted { - box-shadow: 0 0 10px $orange; - } - } } + // Problem details container styles for react-table-v8 .problem-details-container { display: flex; flex-direction: column; @@ -293,7 +108,6 @@ .description-label { font-weight: 500; - // font-style: italic; color: $text-muted; cursor: pointer; } @@ -344,13 +158,12 @@ .problem-item-value { font-weight: 500; - overflow: auto; - display: -webkit-box; - max-height: 60px; + overflow: auto; + display: -webkit-box; + max-height: 60px; } } - .problem-statusbar { margin-bottom: 0.6rem; display: flex; @@ -439,7 +252,6 @@ } } - .problem-details-right { flex: 0 0 auto; padding: 0.5rem 2rem; @@ -582,45 +394,18 @@ } } - @for $i from 8 through 9 { - .item-#{$i} { - width: 2em * $i; - } - &.font-size--#{$i * 10} .rt-table { - font-size: 1% * $i * 10; - - & .rt-tr .rt-td.custom-expander i { - font-size: calc(1.2rem * $i / 10); - } - - .problem-details-container.show { - font-size: 13px; - } - } - } - - @for $i from 11 through 25 { - .item-#{$i} { - width: 2em * $i; - } - &.font-size--#{$i * 10} { - .rt-table { - font-size: 1% * $i * 10; - - & .rt-tr .rt-td.custom-expander i { - font-size: calc(1.2rem * $i / 10); - } - - .problem-details-container.show { - font-size: 13px; - } - } - - .rt-noData { - top: 4.5em; - font-size: 1% * $i * 10; - } - } + // Pagination v8 styles for panel-problems + .pagination-v8 { + margin-top: auto; + flex: 1 0 auto; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 4px; + margin-left: -10px; + margin-right: -10px; + box-shadow: 0px -2px 5px $problems-footer-shadow; + z-index: 1; } } diff --git a/src/styles/_react-table.scss b/src/styles/_react-table.scss index d8665a0..becd005 100644 --- a/src/styles/_react-table.scss +++ b/src/styles/_react-table.scss @@ -1,67 +1,314 @@ -// ReactTable basic overrides (does not include pivot/groups/filters) - -.ReactTable { - border: none; -} - -.ReactTable .rt-table { - // Allow some space for the no-data text +// ReactTable v8 styles (native table elements) +.react-table-v8-wrapper { + position: relative; min-height: 90px; + overflow-x: auto; + overflow-y: auto; + width: 100%; + flex-grow: 1; + + // Loading overlay (same as v6) + .-loading { + display: block; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: $input-bg; + transition: all 0.3s ease; + z-index: -1; + opacity: 0; + pointer-events: none; + + &.-active { + opacity: 0.8; + z-index: 10; + pointer-events: all; + } + + > div { + position: absolute; + display: block; + text-align: center; + width: 100%; + top: 50%; + left: 0; + font-size: 15px; + color: $input-color; + transform: translateY(-50%); + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + } + } + + // Reduce table opacity when loading + &.is-loading { + .react-table-v8 { + opacity: 0.3; + pointer-events: none; + } + } } -.ReactTable .rt-thead.-header { - box-shadow: none; - background: $list-item-bg; - border-top: 2px solid $body-bg; - border-bottom: 2px solid $body-bg; - height: 2em; +.react-table-v8 { + width: 100%; + border: none; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; + + // Header styles + thead { + tr { + box-shadow: none; + background: $list-item-bg; + border-top: 2px solid $body-bg; + border-bottom: 2px solid $body-bg; + height: 2em; + } + + th { + text-align: left; + color: $blue; + font-weight: 500; + padding: 0.45em 0 0.45em 1.1em; + border-right: none; + box-shadow: none; + background: transparent; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + box-sizing: border-box; + position: relative; + + .resizer { + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 5px; + background: rgba(0, 0, 0, 0.1); + cursor: col-resize; + user-select: none; + touch-action: none; + opacity: 0; + z-index: 1; + transition: opacity 0.2s; + + &:hover, + &.isResizing { + opacity: 1; + background: $blue; + } + } + + &:hover .resizer { + opacity: 0.5; + } + } + } + + // Body styles + tbody { + td { + padding: 0.45em 0 0.45em 1.1em; + border-bottom: solid 1px $problems-border-color; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + + &:last-child { + border-right: none; + } + } + + // Severity column styling + td.problem-severity { + padding: 0; + position: relative; + + .severity-cell { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: 0.45em 0 0.45em 1.1em; + color: $white; + font-weight: 500; + display: flex; + align-items: center; + } + } + + // Tags column styling + td.problem-tags { + padding-left: 0.6rem; + + .label-tag, + .zbx-tag { + cursor: pointer; + margin-right: 0.6rem; + + &.highlighted { + box-shadow: 0 0 10px $orange; + } + } + } + + // No data cell styling + td.no-data-cell { + text-align: center; + padding: 0; + background: transparent; + border: none; + + .rt-noData { + position: relative; + top: 0; + left: 0; + z-index: 2; + background: unset; + color: $text-muted; + padding: 2rem 0; + } + } + + tr { + background: transparent; + + // Alternating row colors (stripe even rows with solid color) + &.even-row, + &.even-row-expanded { + background: $problems-table-stripe; + + td { + background: inherit; + } + } + + &:hover:not(.no-data-row) td { + background: $problems-table-row-hovered; + } + } + + td.custom-expander { + text-align: center; + padding: 0; + position: relative; + + button { + background: transparent; + border: none; + padding: 0; + cursor: pointer; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + i { + color: #676767; + font-size: 1.2rem; + } + + &.expanded i { + color: $problem-expander-expanded-color; + } + + &:hover { + background-color: $problem-expander-highlighted-background; + + i { + color: $problem-expander-highlighted-color; + } + } + } + } + } } -.ReactTable .rt-thead.-header .rt-th { - text-align: left; - color: $blue; - font-weight: 500; -} -.ReactTable .rt-thead .rt-td, -.ReactTable .rt-thead .rt-th { - padding: 0.45em 0 0.45em 1.1em; - border-right: none; - box-shadow: none; -} -.ReactTable .rt-tbody .rt-td { - padding: 0.45em 0 0.45em 1.1em; - border-bottom: 2px solid $body-bg; - border-right: 2px solid $body-bg; -} -.ReactTable .rt-tbody .rt-td:last-child { - border-right: none; -} -.ReactTable .-pagination { + +// Pagination v8 styles +.pagination-v8 { + margin-top: $panel-margin; border-top: none; box-shadow: none; - margin-top: $panel-margin; } -.ReactTable .-pagination .-btn { - color: $blue; - background: $list-item-bg; + +.pagination-v8-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + flex-wrap: wrap; } -.ReactTable .-pagination input, -.ReactTable .-pagination select { + +.pagination-v8-btn { + &.-btn { + color: $blue; + background: $list-item-bg; + border: 1px solid transparent; + padding: 0.5rem 1.5rem; + cursor: pointer; + border-radius: 3px; + + &:hover:not(:disabled) { + opacity: 0.8; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } +} + +.pagination-v8-info { + color: $text-color; + margin: 0 0.25rem; + text-align: center; +} + +.pagination-v8-page-input { + width: 50px; color: $input-color; background-color: $input-bg; + padding: 0.5rem; + border-radius: 3px; + text-align: center; + + &:focus { + outline: none; + border-color: $blue; + } } -.ReactTable .-loading { - background: $input-bg; -} -.ReactTable .-loading.-active { - opacity: 0.8; -} -.ReactTable .-loading > div { + +.pagination-v8-select { color: $input-color; -} -.ReactTable .rt-tr .rt-td:last-child { - text-align: right; -} -.ReactTable .rt-noData { - top: 60px; - z-index: inherit; + background-color: $input-bg; + border: 1px solid $input-border-color; + padding: 0.5rem 0.5rem; + border-radius: 3px; + cursor: pointer; + height: auto; + line-height: normal; + width: auto; + min-width: 80px; + + &:focus { + outline: none; + border-color: $blue; + } + + option { + background-color: $input-bg; + color: $input-color; + } } diff --git a/src/styles/grafana-zabbix.scss b/src/styles/grafana-zabbix.scss index 58c9da1..e9fc67e 100644 --- a/src/styles/grafana-zabbix.scss +++ b/src/styles/grafana-zabbix.scss @@ -1,6 +1,4 @@ // DEPENDENCIES -@import '../../node_modules/react-table-6/react-table.css'; - @import 'variables'; @import 'panel-triggers'; @import 'panel-problems'; diff --git a/yarn.lock b/yarn.lock index 733b043..5e148bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3059,6 +3059,18 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-table@npm:^8.21.3": + version: 8.21.3 + resolution: "@tanstack/react-table@npm:8.21.3" + dependencies: + "@tanstack/table-core": "npm:8.21.3" + peerDependencies: + react: ">=16.8" + react-dom: ">=16.8" + checksum: 10c0/85d1d0fcb690ecc011f68a5a61c96f82142e31a0270dcf9cbc699a6f36715b1653fe6ff1518302a6d08b7093351fc4cabefd055a7db3cd8ac01e068956b0f944 + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:^3.5.1": version: 3.13.12 resolution: "@tanstack/react-virtual@npm:3.13.12" @@ -3071,6 +3083,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/table-core@npm:8.21.3": + version: 8.21.3 + resolution: "@tanstack/table-core@npm:8.21.3" + checksum: 10c0/40e3560e6d55e07cc047024aa7f83bd47a9323d21920d4adabba8071fd2d21230c48460b26cedf392588f8265b9edc133abb1b0d6d0adf4dae0970032900a8c9 + languageName: node + linkType: hard + "@tanstack/virtual-core@npm:3.13.12": version: 3.13.12 resolution: "@tanstack/virtual-core@npm:3.13.12" @@ -7389,6 +7408,7 @@ __metadata: "@swc/core": "npm:^1.3.90" "@swc/helpers": "npm:^0.5.0" "@swc/jest": "npm:^0.2.26" + "@tanstack/react-table": "npm:^8.21.3" "@testing-library/jest-dom": "npm:6.1.4" "@testing-library/react": "npm:14.0.0" "@types/glob": "npm:^8.0.0" @@ -7432,7 +7452,6 @@ __metadata: react: "npm:18.3.1" react-dom: "npm:18.3.1" react-router-dom: "npm:^6.22.0" - react-table-6: "npm:6.11.0" react-use: "npm:17.4.0" replace-in-file-webpack-plugin: "npm:^1.0.6" rxjs: "npm:7.8.2" @@ -11372,19 +11391,6 @@ __metadata: languageName: node linkType: hard -"react-table-6@npm:6.11.0": - version: 6.11.0 - resolution: "react-table-6@npm:6.11.0" - dependencies: - classnames: "npm:^2.2.5" - peerDependencies: - prop-types: ^15.5.0 - react: ^16.x.x - react-dom: ^16.x.x - checksum: 10c0/4b65f88320f00bcbf4782cb95a4413c549330dbdbc79d6432f9d6800453f9db376a086a162e464b4d1cddf9e1b9a692b0a777d79a85aa4b1e1661bf2caaad7e1 - languageName: node - linkType: hard - "react-table@npm:7.8.0": version: 7.8.0 resolution: "react-table@npm:7.8.0"