/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
import React, { useState, useEffect } from 'react';
import {
ExpandableRowContent,
Table, Thead, Tbody, Tr, Th, Td,
SortByDirection,
} from '@patternfly/react-table';
import { EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateActions } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
import './cockpit-components-table.scss';
/* This is a wrapper around PF Table component
* See https://www.patternfly.org/components/table/
* Properties (all optional unless specified otherwise):
* - caption
* - id: optional identifier
* - className: additional classes added to the Table
* - actions: additional listing-wide actions (displayed next to the list's title)
* - columns: { title: string, header: boolean, sortable: boolean }[] or string[]
* - rows: {
* columns: (React.Node or string or { title: string, key: string, ...extraProps: object}}[]
Through extraProps the consumers can pass arbitrary properties to the
* props: { key: string, ...extraProps: object }
This property is mandatory and should contain a unique `key`, all additional properties are optional.
Through extraProps the consumers can pass arbitrary properties to the |
* expandedContent: (React.Node)[])
* selected: boolean option if the row is selected
* initiallyExpanded : the entry will be initially rendered as expanded, but then behaves normally
* }[]
* - emptyCaption: header caption to show if list is empty
* - emptyCaptionDetail: extra details to show after emptyCaption if list is empty
* - emptyComponent: Whole empty state component to show if the list is empty
* - isEmptyStateInTable: if empty state is result of a filter function this should be set, otherwise false
* - loading: Set to string when the content is still loading. This string is shown.
* - variant: For compact tables pass 'compact'
* - gridBreakPoint: Specifies the grid breakpoints ('', 'grid' | 'grid-md' | 'grid-lg' | 'grid-xl' | 'grid-2xl')
* - sortBy: { index: Number, direction: SortByDirection }
* - sortMethod: callback function used for sorting rows. Called with 3 parameters: sortMethod(rows, activeSortDirection, activeSortIndex)
* - style: object of additional css rules
* - afterToggle: function to be called when content is toggled
* - onSelect: function to be called when a checkbox is clicked. Called with 5 parameters:
* event, isSelected, rowIndex, rowData, extraData. rowData contains props with an id property of the clicked row.
* - onHeaderSelect: event, isSelected.
*/
export const ListingTable = ({
actions = [],
afterToggle,
caption = '',
className,
columns: cells = [],
emptyCaption = '',
emptyCaptionDetail,
emptyComponent,
isEmptyStateInTable = false,
loading = '',
onRowClick,
onSelect,
onHeaderSelect,
rows: tableRows = [],
showHeader = true,
sortBy,
sortMethod,
...extraProps
}) => {
let rows = [...tableRows];
const [expanded, setExpanded] = useState({});
const [newItems, setNewItems] = useState([]);
const [currentRowsKeys, setCurrentRowsKeys] = useState([]);
const [activeSortIndex, setActiveSortIndex] = useState(sortBy?.index ?? 0);
const [activeSortDirection, setActiveSortDirection] = useState(sortBy?.direction ?? SortByDirection.asc);
const rowKeys = rows.map(row => row.props?.key)
.filter(key => key !== undefined);
const rowKeysStr = JSON.stringify(rowKeys);
const currentRowsKeysStr = JSON.stringify(currentRowsKeys);
useEffect(() => {
// Don't highlight all when the list gets loaded
const _currentRowsKeys = JSON.parse(currentRowsKeysStr);
const _rowKeys = JSON.parse(rowKeysStr);
if (_currentRowsKeys.length !== 0) {
const new_keys = _rowKeys.filter(key => _currentRowsKeys.indexOf(key) === -1);
if (new_keys.length) {
setTimeout(() => setNewItems(items => items.filter(item => new_keys.indexOf(item) < 0)), 4000);
setNewItems(ni => [...ni, ...new_keys]);
}
}
setCurrentRowsKeys(crk => [...new Set([...crk, ..._rowKeys])]);
}, [currentRowsKeysStr, rowKeysStr]);
const isSortable = cells.some(col => col.sortable);
const isExpandable = rows.some(row => row.expandedContent);
const tableProps = {};
/* Basic table properties */
tableProps.className = "ct-table";
if (className)
tableProps.className = tableProps.className + " " + className;
if (rows.length == 0)
tableProps.className += ' ct-table-empty';
const header = (
(caption || actions.length != 0)
?
{caption}
{actions && {actions}
}
: null
);
if (loading)
return
{loading}
;
if (rows == 0) {
let emptyState = null;
if (emptyComponent)
emptyState = emptyComponent;
else
emptyState = (
{emptyCaption}
{emptyCaptionDetail}
{actions.length > 0 &&
{actions}
}
);
if (!isEmptyStateInTable)
return emptyState;
const emptyStateCell = (
[{
props: { colSpan: cells.length },
title: emptyState
}]
);
rows = [{ columns: emptyStateCell }];
}
const sortRows = () => {
const sortedRows = rows.sort((a, b) => {
const aitem = a.columns[activeSortIndex];
const bitem = b.columns[activeSortIndex];
return ((typeof aitem == 'string' ? aitem : (aitem.sortKey || aitem.title)).localeCompare(typeof bitem == 'string' ? bitem : (bitem.sortKey || bitem.title)));
});
return activeSortDirection === SortByDirection.asc ? sortedRows : sortedRows.reverse();
};
const onSort = (event, index, direction) => {
setActiveSortIndex(index);
setActiveSortDirection(direction);
};
const rowsComponents = (isSortable ? (sortMethod ? sortMethod(rows, activeSortDirection, activeSortIndex) : sortRows()) : rows).map((row, rowIndex) => {
const rowProps = row.props || {};
if (onRowClick) {
rowProps.isClickable = true;
rowProps.onRowClick = (event) => onRowClick(event, row);
}
if (rowProps.key && newItems.indexOf(rowProps.key) >= 0)
rowProps.className = (rowProps.className || "") + " ct-new-item";
const rowKey = rowProps.key || rowIndex;
const isExpanded = expanded[rowKey] === undefined ? !!row.initiallyExpanded : expanded[rowKey];
const rowPair = (
{isExpandable
? (row.expandedContent
? {
if (afterToggle)
afterToggle(!expanded[rowKey]);
setExpanded({ ...expanded, [rowKey]: !expanded[rowKey] });
}
}} />
: | | )
: null
}
{onSelect &&
|
}
{row.columns.map((cell, cellIndex) => {
const { key, ...cellProps } = cell.props || {};
const dataLabel = typeof cells[cellIndex] == 'object' ? cells[cellIndex].title : cells[cellIndex];
const colKey = dataLabel || cellIndex;
if (cells[cellIndex]?.header)
return (
{typeof cell == 'object' ? cell.title : cell}
|
);
return (
{typeof cell == 'object' ? cell.title : cell}
|
);
})}
{row.expandedContent &&
{row.expandedContent}
|
}
);
return
{rowPair};
});
return (
<>
{header}
{showHeader &&
{isExpandable && | }
{!onHeaderSelect && onSelect && | }
{onHeaderSelect && onSelect && r.selected)
}} />}
{cells.map((column, columnIndex) => {
const columnProps = column.props;
const sortParams = (
column.sortable
? {
sort: {
sortBy: {
index: activeSortIndex,
direction: activeSortDirection
},
onSort,
columnIndex
}
}
: {}
);
return (
|
{typeof column == 'object' ? column.title : column}
|
);
})}
}
{rowsComponents}
>
);
};