diff --git a/packages/hawtio/src/plugins/logs/Logs.tsx b/packages/hawtio/src/plugins/logs/Logs.tsx index 6f1d30cca..abf147b16 100644 --- a/packages/hawtio/src/plugins/logs/Logs.tsx +++ b/packages/hawtio/src/plugins/logs/Logs.tsx @@ -1,5 +1,4 @@ import { - Bullseye, Button, Card, CardBody, @@ -11,37 +10,23 @@ import { DescriptionListGroup, DescriptionListTerm, Divider, - EmptyState, - EmptyStateBody, - EmptyStateIcon, Label, Modal, PageSection, - Pagination, - PaginationProps, - Panel, - SearchInput, Skeleton, Title, - Toolbar, - ToolbarContent, ToolbarFilter, - ToolbarGroup, - ToolbarItem, - EmptyStateHeader, - EmptyStateFooter, MenuToggle, MenuToggleElement, SelectOption, Select, SelectList, } from '@patternfly/react-core' -import { SearchIcon } from '@patternfly/react-icons' -import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table' -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { log } from './globals' import { LogEntry, LogFilter } from './log-entry' import { LOGS_UPDATE_INTERVAL, logsService } from './logs-service' +import { FilteredTable } from '@hawtiosrc/ui' export const Logs: React.FunctionComponent = () => { return ( @@ -57,6 +42,22 @@ export const Logs: React.FunctionComponent = () => { ) } +class LogRowData { + timestamp: string + level: string + logger: string + message: string + logEntry: LogEntry + + constructor(entry: LogEntry) { + this.timestamp = entry.getTimestamp() + this.level = entry.event.level + this.logger = entry.event.logger + this.message = entry.event.message + this.logEntry = entry + } +} + const LogsTable: React.FunctionComponent = () => { const [logs, setLogs] = useState([]) const timestamp = useRef(0) @@ -67,18 +68,17 @@ const LogsTable: React.FunctionComponent = () => { const [filters, setFilters] = useState(emptyFilters) // Temporal filter values holder until applying it const tempFilters = useRef<{ logger: string; message: string; properties: string }>(emptyFilters) - const [filteredLogs, setFilteredLogs] = useState([]) const [isSelectLevelOpen, setIsSelectLevelOpen] = useState(false) - // Pagination - const [page, setPage] = useState(1) - const [perPage, setPerPage] = useState(10) - const [paginatedLogs, setPaginatedLogs] = useState(filteredLogs.slice(0, perPage)) - // Modal const [isModalOpen, setIsModalOpen] = useState(false) const [selected, setSelected] = useState(null) + const rows = useMemo(() => { + console.log('filtering using ', filters) + return logsService.filter(logs, filters).map(log => new LogRowData(log)) + }, [logs, filters, tempFilters]) + useEffect(() => { const loadLogs = async () => { const result = await logsService.loadLogs() @@ -110,16 +110,6 @@ const LogsTable: React.FunctionComponent = () => { return () => timeoutHandle && clearTimeout(timeoutHandle) }, []) - useEffect(() => { - const filteredLogs = logsService.filter(logs, filters) - setFilteredLogs(filteredLogs) - }, [logs, filters]) - - useEffect(() => { - setPaginatedLogs(filteredLogs.slice(0, perPage)) - setPage(1) - }, [filteredLogs, perPage]) - if (!loaded) { return } @@ -142,129 +132,8 @@ const LogsTable: React.FunctionComponent = () => { }) } - const applyFilters = () => { - setFilters(prev => ({ ...prev, ...tempFilters.current })) - } - - const clearAllFilters = () => { - setFilters(emptyFilters) - tempFilters.current = emptyFilters - } - - const handleSetPage = ( - _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, - newPage: number, - _perPage?: number, - startIdx?: number, - endIdx?: number, - ) => { - setPaginatedLogs(filteredLogs.slice(startIdx, endIdx)) - setPage(newPage) - } - - const handlePerPageSelect = ( - _event: React.MouseEvent | React.KeyboardEvent | MouseEvent, - newPerPage: number, - newPage: number, - startIdx?: number, - endIdx?: number, - ) => { - setPaginatedLogs(filteredLogs.slice(startIdx, endIdx)) - setPage(newPage) - setPerPage(newPerPage) - } - - const renderPagination = (variant: PaginationProps['variant'], isCompact: boolean) => ( - - ) - const logLevels = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'] - const tableToolbar = ( - - - - - handleFiltersChange( - 'level', - filters.level.filter(l => l !== chip), - ) - } - deleteChipGroup={() => handleFiltersChange('level', [])} - categoryName='Level' - > - - - - handleFiltersChange('logger', value)} - onSearch={() => applyFilters()} - onClear={() => handleFiltersChange('logger', '', true)} - /> - - - handleFiltersChange('message', value)} - onSearch={() => applyFilters()} - onClear={() => handleFiltersChange('message', '', true)} - /> - - - handleFiltersChange('properties', value)} - onSearch={() => applyFilters()} - onClear={() => handleFiltersChange('properties', '', true)} - /> - - - {renderPagination('top', true)} - - - ) const selectLog = (log: LogEntry) => { setSelected(log) @@ -275,67 +144,77 @@ const LogsTable: React.FunctionComponent = () => { setIsModalOpen(!isModalOpen) } - const highlightSearch = (text: string, search: string) => { - if (search === '') { - return text + const levelToggles = ( + handleFiltersChange( + 'level', + filters.level.filter(l => l !== chip), + ) } - const lowerCaseSearch = search.toLowerCase() - const res = text - .split(new RegExp(`(${search})`, 'gi')) - .map((s, i) => (s.toLowerCase() === lowerCaseSearch ? {s} : s)) - return res - } + deleteChipGroup={() => handleFiltersChange('level', [])} + categoryName='Level' + > + + ) + + const newTable = <> + + + }, + { + name: "Logger", + key: 'logger', + }, + { + name: "Message", + key: 'message', + } + ]} searchCategories={[ + { + name: 'Logger', + key: 'logger' + }, + { + name: 'Message', + key: 'message' + } + ]} onClick={(row) => { + setSelected(row.logEntry) + setIsModalOpen(true) + }} extraToolbar={levelToggles} /> + - return ( - - {tableToolbar} - - - - - - - - - - - {paginatedLogs.map((log, index) => ( - selectLog(log)}> - - - - - - ))} - {filteredLogs.length === 0 && ( - - - - )} - -
TimestampLevelLoggerMessage
{log.getTimestamp()} - - {highlightSearch(log.event.logger, filters.logger)}{highlightSearch(log.event.message, filters.message)}
- - - } - headingLevel='h2' - /> - Clear all filters and try again. - - - - - -
- {renderPagination('bottom', false)} - -
- ) + return newTable } const LogModal: React.FunctionComponent<{ diff --git a/packages/hawtio/src/plugins/runtime/SysProps.tsx b/packages/hawtio/src/plugins/runtime/SysProps.tsx index e55e0fe36..3b8421c82 100644 --- a/packages/hawtio/src/plugins/runtime/SysProps.tsx +++ b/packages/hawtio/src/plugins/runtime/SysProps.tsx @@ -1,248 +1,35 @@ -import { objectSorter } from '@hawtiosrc/util/objects' -import { - Bullseye, - Dropdown, - DropdownItem, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - FormGroup, - Pagination, - Panel, - PanelHeader, - PanelMain, - PanelMainBody, - SearchInput, - Toolbar, - ToolbarContent, - ToolbarFilter, - ToolbarGroup, - ToolbarItem, - EmptyStateHeader, - MenuToggleElement, - MenuToggle, - DropdownList, -} from '@patternfly/react-core' -import { SearchIcon } from '@patternfly/react-icons' -import { Table, Tbody, Td, Th, ThProps, Thead, Tr } from '@patternfly/react-table' import React, { useEffect, useState } from 'react' import { runtimeService } from './runtime-service' -import { SystemProperty } from './types' +import { FilteredTable } from '@hawtiosrc/ui' export const SysProps: React.FunctionComponent = () => { const [properties, setProperties] = useState<{ key: string; value: string }[]>([]) - const [filteredProperties, setFilteredProperties] = useState([]) - const [page, setPage] = useState(1) - const [perPage, setPerPage] = useState(20) - const [searchTerm, setSearchTerm] = useState('') - const [filters, setFilters] = useState([]) - const [filteredAttribute, setFilteredAttribute] = useState('name') - const [sortIndex, setSortIndex] = React.useState(-1) - const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc') - const [isDropdownOpen, setIsDropdownOpen] = useState(false) useEffect(() => { runtimeService.loadSystemProperties().then(props => { setProperties(props) - setFilteredProperties(props) }) }, []) - useEffect(() => { - //filter with findTerm - let filtered: SystemProperty[] = [...properties] - - //add current search word to filters and filter - ;[...filters, `${filteredAttribute}:${searchTerm}`].forEach(value => { - const attr = value.split(':')[0] ?? '' - const searchTerm = value.split(':')[1] ?? '' - filtered = filtered.filter(prop => - (attr === 'name' ? prop.key : prop.value).toLowerCase().includes(searchTerm.toLowerCase()), - ) - }) - - setPage(1) - setFilteredProperties([...filtered]) - }, [searchTerm, properties, filters, filteredAttribute]) - - const onDeleteFilter = (filter: string) => { - if (`${filteredAttribute}:${searchTerm}` === filter) { - setSearchTerm('') - } else { - const newFilters = filters.filter(f => f !== filter) - setFilters(newFilters) - } - } - - const addToFilters = () => { - if (searchTerm !== '') { - setFilters([...filters, `${filteredAttribute}:${searchTerm}`]) - setSearchTerm('') - } - } - const clearFilters = () => { - setFilters([]) - setSearchTerm('') - } - - const PropsPagination = () => { - return ( - setPage(value)} - onPerPageSelect={(_evt, value) => { - setPerPage(value) - setPage(1) - }} - variant='top' - /> - ) - } - - const getPageProperties = (): SystemProperty[] => { - const start = (page - 1) * perPage - const end = start + perPage - return filteredProperties.slice(start, end) - } - - const attributes = [ - { key: 'name', value: 'Name' }, - { key: 'value', value: 'Value' }, - ] - - const dropdownItems = attributes.map(a => ( - { - setFilteredAttribute(a.key) - }} - key={a.key} - > - {a.value} - - )) - - const getSortParams = (sortColumn: number): ThProps['sort'] => ({ - sortBy: { - index: sortIndex, - direction: sortDirection, - defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' - }, - onSort: (_event, index, direction) => { - setSortIndex(index) - setSortDirection(direction) - }, - columnIndex: sortColumn, - }) - - const sortProperties = (): SystemProperty[] => { - let sortedProps = filteredProperties - if (sortIndex >= 0) { - sortedProps = filteredProperties.sort((a, b) => { - const aValue = sortIndex === 1 ? a.value : a.key - const bValue = sortIndex === 1 ? b.value : b.key - return objectSorter(aValue, bValue, sortDirection === 'desc') - }) - } - - return sortedProps - } - - const tableToolbar = ( - - - - { - setIsDropdownOpen(false) - addToFilters() - }} - defaultValue='name' - onOpenChange={setIsDropdownOpen} - toggle={(toggleRef: React.Ref) => ( - setIsDropdownOpen(!isDropdownOpen)} - isExpanded={isDropdownOpen} - > - {attributes.find(att => att.key === filteredAttribute)?.value} - - )} - shouldFocusToggleOnSelect - isOpen={isDropdownOpen} - > - {dropdownItems} - - onDeleteFilter(filter as string)} - deleteChipGroup={clearFilters} - categoryName='Filters' - > - setSearchTerm(value)} - aria-label='Search input' - /> - - - - - - - - - ) - return ( - - {tableToolbar} - - - {sortProperties().length > 0 && ( - - - - - - - - - - {getPageProperties().map((prop, index) => { - return ( - - - - - ) - })} - -
- Property Name - - Property Value -
{prop.key}{prop.value}
-
- )} - {filteredProperties.length === 0 && ( - - - } /> - No results found. - - - )} -
-
-
+ ) } diff --git a/packages/hawtio/src/plugins/runtime/Threads.tsx b/packages/hawtio/src/plugins/runtime/Threads.tsx index 59b81d867..b530f276e 100644 --- a/packages/hawtio/src/plugins/runtime/Threads.tsx +++ b/packages/hawtio/src/plugins/runtime/Threads.tsx @@ -1,39 +1,17 @@ import { - Bullseye, Button, CodeBlock, CodeBlockCode, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - FormGroup, Modal, - Pagination, - Panel, - PanelHeader, - PanelMain, - PanelMainBody, - SearchInput, - Toolbar, - ToolbarContent, - ToolbarFilter, ToolbarGroup, ToolbarItem, - EmptyStateHeader, - DropdownItem, - MenuToggle, - MenuToggleElement, - Dropdown, - DropdownList, } from '@patternfly/react-core' import React, { useEffect, useState } from 'react' import { Thread } from './types' -import { objectSorter } from '@hawtiosrc/util/objects' -import { SearchIcon } from '@patternfly/react-icons' -import { Table, Tbody, Td, Th, Thead, ThProps, Tr } from '@patternfly/react-table' import { runtimeService } from './runtime-service' -import { ThreadInfoModal, ThreadState } from './ThreadInfoModal' +import { ThreadInfoModal } from './ThreadInfoModal' +import { FilteredTable } from '@hawtiosrc/ui' const ThreadsDumpModal: React.FunctionComponent<{ isOpen: boolean @@ -69,156 +47,24 @@ const ThreadsDumpModal: React.FunctionComponent<{ export const Threads: React.FunctionComponent = () => { const [threads, setThreads] = useState([]) - const [filteredThreads, setFilteredThreads] = useState([]) - - const [page, setPage] = useState(1) - const [perPage, setPerPage] = useState(20) - const [searchTerm, setSearchTerm] = useState('') - const [filters, setFilters] = useState([]) - const [attributeMenuItem, setAttributeMenuItem] = useState('Name') - const [sortIndex, setSortIndex] = React.useState(-1) - const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc') - const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const [currentThread, setCurrentThread] = useState() const [isThreadsDumpModalOpen, setIsThreadsDumpModalOpen] = useState(false) const [isThreadDetailsOpen, setIsThreadDetailsOpen] = useState(false) - const [currentThread, setCurrentThread] = useState() const [threadConnectionMonitoring, setThreadConnectionMonitoring] = useState(false) useEffect(() => { const readThreads = async () => { const threads = await runtimeService.loadThreads() setThreads(threads) - setFilteredThreads(threads) setThreadConnectionMonitoring(await runtimeService.isThreadContentionMonitoringEnabled()) runtimeService.registerLoadThreadsRequest(threads => { setThreads(threads) - setFilteredThreads(threads) }) } readThreads() return () => runtimeService.unregisterAll() }, []) - useEffect(() => { - let filtered: Thread[] = [...threads] - - //add current searchTerm and filter - ;[...filters, `${attributeMenuItem}:${searchTerm}`].forEach(value => { - const attr = value.split(':')[0] ?? '' - const searchTerm = value.split(':')[1] ?? '' - filtered = filtered.filter(thread => - (attr === 'Name' ? thread.threadName : String(thread.threadState)) - .toLowerCase() - .includes(searchTerm.toLowerCase()), - ) - }) - - setPage(1) - setFilteredThreads([...filtered]) - }, [threads, searchTerm, attributeMenuItem, filters]) - - const onDeleteFilter = (filter: string) => { - if (`${attributeMenuItem}:${searchTerm}` === filter) { - setSearchTerm('') - } else { - const newFilters = filters.filter(f => f !== filter) - setFilters(newFilters) - } - } - - const addToFilters = () => { - if (searchTerm !== '') { - setFilters([...filters, `${attributeMenuItem}:${searchTerm}`]) - setSearchTerm('') - } - } - - const clearFilters = () => { - setFilters([]) - setSearchTerm('') - } - - const PropsPagination = () => { - return ( - setPage(value)} - onPerPageSelect={(_evt, value) => { - setPerPage(value) - setPage(1) - }} - variant='top' - /> - ) - } - - const getThreadsPage = (): Thread[] => { - const start = (page - 1) * perPage - const end = start + perPage - return filteredThreads.slice(start, end) - } - - const tableColumns = [ - { key: 'threadId', value: 'ID' }, - { key: 'threadState', value: 'State' }, - { key: 'threadName', value: 'Name' }, - { key: 'waitedTime', value: 'Waited Time' }, - { key: 'blockedTime', value: 'Blocked Time' }, - { key: 'inNative', value: 'Native' }, - { key: 'suspended', value: 'Suspended' }, - ] - - const dropdownItems = [ - { - setAttributeMenuItem('Name') - }} - key={'name-key'} - > - Name - , - { - setAttributeMenuItem('State') - }} - key={'state'} - > - State - , - ] - - const getIndexedThread = (thread: Thread): (string | number)[] => { - const { suspended, blockedTime, inNative, threadId, threadName, threadState, waitedTime } = thread - return [threadId, threadState, threadName, waitedTime, blockedTime, String(inNative), String(suspended)] - } - - const getSortParams = (sortColumn: number): ThProps['sort'] => ({ - sortBy: { - index: sortIndex, - direction: sortDirection, - defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' - }, - onSort: (_event, index, direction) => { - setSortIndex(index) - setSortDirection(direction) - }, - columnIndex: sortColumn, - }) - - const sortThreads = (): Thread[] => { - let sortedThreads = filteredThreads - if (sortIndex >= 0) { - sortedThreads = filteredThreads.sort((a, b) => { - const aValue = getIndexedThread(a)[sortIndex] - const bValue = getIndexedThread(b)[sortIndex] - return objectSorter(aValue, bValue, sortDirection === 'desc') - }) - } - return sortedThreads - } - const onThreadDumpClick = () => { setIsThreadsDumpModalOpen(true) } @@ -228,132 +74,53 @@ export const Threads: React.FunctionComponent = () => { setThreadConnectionMonitoring(!threadConnectionMonitoring) } - const tableToolbar = ( - - - - { - setIsDropdownOpen(false) - addToFilters() - }} - onOpenChange={setIsDropdownOpen} - defaultValue='Name' - toggle={(toggleRef: React.Ref) => ( - setIsDropdownOpen(!isDropdownOpen)} - > - {tableColumns.find(att => att.value === attributeMenuItem)?.value} - - )} - isOpen={isDropdownOpen} - > - {dropdownItems} - - onDeleteFilter(filter as string)} - deleteChipGroup={clearFilters} - categoryName='Filters' - > - setSearchTerm(value)} - aria-label='Search input' - /> - - - - - - - - - - - - - - - - - + const DetailsButton = (thread: Thread) => + + const ExtraToolBar = () => ( + + + + + + + ) - return ( - - - - {tableToolbar} - - - {sortThreads().length > 0 && ( - - - - - {tableColumns.map((att, index) => ( - - ))} - - - - {getThreadsPage().map((thread, index) => { - return ( - - {tableColumns.map((att, column) => ( - - ))} - - - ) - })} - -
- {att.value} - -
- {att.key === 'threadState' ? ( - - ) : ( - getIndexedThread(thread)[column] - )} - - -
-
- )} - {filteredThreads.length === 0 && ( - - - } /> - No results found. - - - )} -
-
-
- ) + + return <> + + + + } /> + } diff --git a/packages/hawtio/src/ui/index.ts b/packages/hawtio/src/ui/index.ts index 437e2a5f2..e07cc6921 100644 --- a/packages/hawtio/src/ui/index.ts +++ b/packages/hawtio/src/ui/index.ts @@ -1 +1,2 @@ export { HawtioLoadingPage } from './page' +export { FilteredTable, ExpandableText } from './util' \ No newline at end of file diff --git a/packages/hawtio/src/ui/util/ExpandableText.tsx b/packages/hawtio/src/ui/util/ExpandableText.tsx new file mode 100644 index 000000000..1bb3d89cb --- /dev/null +++ b/packages/hawtio/src/ui/util/ExpandableText.tsx @@ -0,0 +1,17 @@ +import { isString } from "@hawtiosrc/util/objects" +import { ExpandableSection } from "@patternfly/react-core" +import React from "react" + + +export const ExpandableText: React.FunctionComponent<{ children: React.ReactNode }> = ({ children }) => { + + if (isString(children)) { + return ( { + (e.target as HTMLElement)?.classList.contains('pf-v5-c-expandable-section__toggle-text') && e.stopPropagation() + } + } truncateMaxLines={3} variant="truncate" toggleTextExpanded="Show less" toggleTextCollapsed="Show more"> + {children} + ) + } + return children +} \ No newline at end of file diff --git a/packages/hawtio/src/ui/util/FilteredTable.tsx b/packages/hawtio/src/ui/util/FilteredTable.tsx new file mode 100644 index 000000000..7ee10b3ed --- /dev/null +++ b/packages/hawtio/src/ui/util/FilteredTable.tsx @@ -0,0 +1,289 @@ +import { Bullseye, Button, Dropdown, DropdownItem, DropdownList, EmptyState, EmptyStateBody, EmptyStateHeader, EmptyStateIcon, FormGroup, MenuToggle, MenuToggleElement, Pagination, Panel, PanelHeader, PanelMain, PanelMainBody, SearchInput, Toolbar, ToolbarContent, ToolbarFilter, ToolbarGroup, ToolbarItem } from "@patternfly/react-core" +import { Table, Thead, Tr, Th, Tbody, Td, ThProps } from "@patternfly/react-table" +import React, { useEffect, useMemo, useState } from "react" + +import { SearchIcon } from '@patternfly/react-icons' +import { ExpandableText } from "./ExpandableText" +import { isNumber, objectSorter } from "@hawtiosrc/util/objects" + +interface Props { + extraToolbar?: React.ReactNode, + tableColumns: { name?: string, key?: keyof T, renderer?: (value: T) => React.ReactNode }[], + rows: T[], + searchCategories: { name: string, key: keyof T }[], + onClick?: (value: T) => void, + highlightSearch?: boolean +} + +function numberOrString(value: unknown): string | number { + if (isNumber(value)) { + return Number(value) + } else { + return String(value) + } +} + +export function FilteredTable({ extraToolbar, tableColumns, rows, searchCategories, onClick, highlightSearch = false }: Props) { + const defaultSearchCategory = searchCategories[0]?.key + + const [filters, setFilters] = useState<{ key?: keyof T, value: string, name: string }[]>([]) + const [searchTerm, setSearchTerm] = useState<{ key?: keyof T, value: string }>({ key: defaultSearchCategory, value: '' }) + const [filteredRows, setFilteredRows] = useState([]) + + const [attributeMenuItem, setAttributeMenuItem] = useState<{ name: string, key: keyof T } | null>(() => { + if (searchCategories[0]) { + return { key: searchCategories[0].key, name: searchCategories[0].name } + } + return null + }) + + + const [page, setPage] = useState(1) + const [perPage, setPerPage] = useState(20) + + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + + const [sortIndex, setSortIndex] = useState(-1) + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') + + useEffect(() => { + let filtered: T[] = [...rows] + + //add current searchTerm and filter + ;[...filters, searchTerm].forEach(filter => { + const key = filter.key + if (!key) { + return + } + const searchTerm = filter.value + filtered = filtered.filter(value => { + return String(value[key]).toLowerCase().includes(searchTerm.toLowerCase()) + }) + }) + + //If user is filtering - refreshing the threds themselves would reset the page count + if (filtered.length != rows.length) { + setPage(1) + } + setFilteredRows([...filtered]) + }, [rows, searchTerm, attributeMenuItem, filters]) + + const getValuesForDisplay = (item: T): (string | number)[] => { + return tableColumns.map(({ key }) => numberOrString(key && item[key])) + } + + const sortedFilteredRows = useMemo(() => { + let sortedThreads = filteredRows + if (sortIndex >= 0) { + sortedThreads = filteredRows.sort((a, b) => { + const aValue = getValuesForDisplay(a)[sortIndex] + const bValue = getValuesForDisplay(b)[sortIndex] + return objectSorter(aValue, bValue, sortDirection === 'desc') + }) + } + return sortedThreads + }, [filteredRows, sortDirection, sortIndex]) + + + const clearFilters = () => { + setFilters([]) + setSearchTerm({ + key: searchTerm.key, + value: '' + }) + } + + const addToFilters = () => { + if (searchTerm.value !== '' && attributeMenuItem) { + setFilters([...filters, { ...searchTerm, name: attributeMenuItem.name }]) + setSearchTerm({ key: searchTerm.key, value: '' }) + } + } + + const getRowPage = () => { + const start = (page - 1) * perPage + const end = start + perPage + return filteredRows.slice(start, end) + } + + + const propsPagination = ( + setPage(value)} + onPerPageSelect={(_evt, value) => { + setPerPage(value) + setPage(1) + }} + variant='top' + /> + ) + + const filterDropdownItems = searchCategories.map(category => ( + setAttributeMenuItem(category)}> + {category.name} + + )) + + const getFilterChips = () => { + let chips = filters.map(({ name, value }) => `${name}: ${value}`) + if (searchTerm.value) { + chips.push(`${attributeMenuItem?.name}: ${searchTerm.value}`) + } + + return chips + } + + const onDeleteFilter = (filter: string) => { + if (filter === `${attributeMenuItem}: ${searchTerm.value}`) { + setSearchTerm({ + key: attributeMenuItem?.key, + value: '' + }) + } else { + const newFilters = filters.filter(f => `${f.name}: ${f.value}` !== filter) + setFilters(newFilters) + } + } + + const tableToolBar = ( + + + + { + setIsDropdownOpen(false) + addToFilters() + }} + onOpenChange={setIsDropdownOpen} + defaultValue='Name' + toggle={(toggleRef: React.Ref) => ( + setIsDropdownOpen(!isDropdownOpen)} + > + {attributeMenuItem?.name} + + )} + isOpen={isDropdownOpen} + > + {filterDropdownItems} + + onDeleteFilter(filter as string)} + deleteChipGroup={clearFilters} + categoryName='Filters' + > + setSearchTerm({ key: attributeMenuItem?.key, value })} + aria-label='Search input' + /> + + + + {extraToolbar} + + + {propsPagination} + + + ) + + const getSortParams = (sortColumn: number): ThProps['sort'] | undefined => { + if (tableColumns[sortColumn]?.name && tableColumns[sortColumn].key) { + return { + sortBy: { + index: sortIndex, + direction: sortDirection, + defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' + }, + onSort: (_event, index, direction) => { + setSortIndex(index) + setSortDirection(direction) + }, + columnIndex: sortColumn, + } + } + return undefined + + } + + const highlightSearchedText = (text: string, search: string) => { + if (search === '') { + return text + } + + + const lowerCaseSearch = search.toLowerCase() + const res = text + .split(new RegExp(`(${search})`, 'gi')) + .map((s, i) => (s.toLowerCase() === lowerCaseSearch ? {s} : s)) + return res + } + + const defaultCellRenderer = (data: (string | number)[], row : number, column: number) => { + let text : React.ReactNode = data[column] + if (highlightSearch) { + text = highlightSearchedText(String(text), [...filters, searchTerm].filter(f => f.key === tableColumns[column]?.key).map(f => f.value).join("|")) + } + return {text} + } + + + return ( + {tableToolBar} + + + {sortedFilteredRows.length > 0 && ( + + + + + {tableColumns.map((att, index) => ( + + ))} + + + + {getRowPage().map((item, index) => { + const values = getValuesForDisplay(item) + return ( + onClick?.(item)}> + {tableColumns.map(({ renderer }, column) => ( + + ))} + + ) + })} + +
+ {att.name} +
+ {renderer ? renderer(item) : defaultCellRenderer(values, index, column)} +
+
+ )} + {sortedFilteredRows.length === 0 && ( + + + } /> + No results found. + + + )} +
+
+
) +} \ No newline at end of file diff --git a/packages/hawtio/src/ui/util/index.ts b/packages/hawtio/src/ui/util/index.ts new file mode 100644 index 000000000..a435e8ad6 --- /dev/null +++ b/packages/hawtio/src/ui/util/index.ts @@ -0,0 +1,2 @@ +export {FilteredTable} from './FilteredTable' +export {ExpandableText} from './ExpandableText' \ No newline at end of file