github.com/grafana/pyroscope@v1.18.0/public/app/ui/Table.tsx (about) 1 import React, { useState, ReactNode, CSSProperties, RefObject } from 'react'; 2 import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft'; 3 import { faChevronRight } from '@fortawesome/free-solid-svg-icons/faChevronRight'; 4 import clsx from 'clsx'; 5 6 import styles from './Table.module.scss'; 7 import LoadingSpinner from './LoadingSpinner'; 8 import Button from './Button'; 9 10 interface CustomProp { 11 [k: string]: string | CSSProperties | ReactNode | number | undefined; 12 } 13 14 export interface Cell extends CustomProp { 15 value: ReactNode | string; 16 style?: CSSProperties; 17 } 18 19 interface HeadCell extends CustomProp { 20 name: string; 21 label: string; 22 sortable?: number; 23 default?: boolean; 24 } 25 26 export interface BodyRow { 27 'data-row'?: string; 28 isRowSelected?: boolean; 29 isRowDisabled?: boolean; 30 cells: Cell[]; 31 onClick?: () => void; 32 className?: string; 33 } 34 35 export type TableBodyType = 36 | { 37 type: 'not-filled'; 38 value: string | ReactNode; 39 bodyClassName?: string; 40 } 41 | { 42 type: 'filled'; 43 bodyRows: BodyRow[]; 44 }; 45 46 type Table = TableBodyType & { 47 headRow: HeadCell[]; 48 }; 49 50 interface TableSortProps { 51 sortBy: string; 52 updateSortParams: (v: string) => void; 53 sortByDirection: 'desc' | 'asc'; 54 } 55 56 export const useTableSort = (headRow: HeadCell[]): TableSortProps => { 57 const defaultSortByCell = 58 headRow.filter((row) => row?.default)[0] || headRow[0]; 59 const [sortBy, updateSortBy] = useState(defaultSortByCell.name); 60 const [sortByDirection, setSortByDirection] = useState<'desc' | 'asc'>( 61 'desc' 62 ); 63 64 const updateSortParams = (newSortBy: string) => { 65 let dir = sortByDirection; 66 67 if (sortBy === newSortBy) { 68 dir = dir === 'asc' ? 'desc' : 'asc'; 69 } else { 70 dir = 'desc'; 71 } 72 73 updateSortBy(newSortBy); 74 setSortByDirection(dir); 75 }; 76 77 return { sortBy, sortByDirection, updateSortParams }; 78 }; 79 80 interface TableProps { 81 sortByDirection?: string; 82 sortBy?: string; 83 updateSortParams?: (newSortBy: string) => void; 84 table: Table; 85 tableBodyRef?: RefObject<HTMLTableSectionElement>; 86 className?: string; 87 isLoading?: boolean; 88 /* enables pagination */ 89 itemsPerPage?: number; 90 tableStyle?: React.CSSProperties; 91 } 92 93 function TableComponent({ 94 sortByDirection, 95 sortBy, 96 updateSortParams, 97 table, 98 tableBodyRef, 99 className, 100 isLoading, 101 itemsPerPage, 102 tableStyle, 103 }: TableProps) { 104 const hasSort = sortByDirection && sortBy && updateSortParams; 105 const [currPage, setCurrPage] = useState(0); 106 107 return isLoading ? ( 108 <div className={styles.loadingSpinner}> 109 <LoadingSpinner /> 110 </div> 111 ) : ( 112 <> 113 <table 114 className={clsx(styles.table, { 115 [className || '']: className, 116 })} 117 data-testid="table-ui" 118 style={tableStyle} 119 > 120 <thead> 121 <tr> 122 {table.headRow.map( 123 ({ sortable, label, name, ...rest }, idx: number) => 124 !sortable || table.type === 'not-filled' || !hasSort ? ( 125 // eslint-disable-next-line react/no-array-index-key 126 <th key={idx} {...rest}> 127 {label} 128 </th> 129 ) : ( 130 <th 131 {...rest} 132 // eslint-disable-next-line react/no-array-index-key 133 key={idx} 134 className={styles.sortable} 135 onClick={() => updateSortParams(name)} 136 > 137 {label} 138 <span 139 className={clsx(styles.sortArrow, { 140 [styles[sortByDirection]]: sortBy === name, 141 })} 142 /> 143 </th> 144 ) 145 )} 146 </tr> 147 </thead> 148 <tbody ref={tableBodyRef}> 149 {table.type === 'not-filled' ? ( 150 <tr className={table?.bodyClassName}> 151 <td colSpan={table.headRow.length}>{table.value}</td> 152 </tr> 153 ) : ( 154 paginate(table.bodyRows, currPage, itemsPerPage).map( 155 ({ cells, isRowSelected, isRowDisabled, className, ...rest }) => { 156 // The problem is that when you switch apps or time-range and the function 157 // names stay the same it leads to an issue where rows don't get re-rendered 158 // So we force a rerender each time. 159 const renderID = Math.random(); 160 161 return ( 162 <tr 163 key={renderID} 164 {...rest} 165 className={clsx(className, { 166 [styles.isRowSelected]: isRowSelected, 167 [styles.isRowDisabled]: isRowDisabled, 168 })} 169 > 170 {cells && 171 cells.map( 172 ({ style, value, ...rest }: Cell, index: number) => ( 173 // eslint-disable-next-line react/no-array-index-key 174 <td key={renderID + index} style={style} {...rest}> 175 {value} 176 </td> 177 ) 178 )} 179 </tr> 180 ); 181 } 182 ) 183 )} 184 </tbody> 185 </table> 186 <PaginationNavigation 187 bodyRows={table.type === 'filled' ? table.bodyRows : undefined} 188 itemsPerPage={itemsPerPage} 189 currPage={currPage} 190 setCurrPage={setCurrPage} 191 /> 192 </> 193 ); 194 } 195 196 function paginate( 197 bodyRows: Extract<Table, { type: 'filled' }>['bodyRows'], 198 currPage: number, 199 itemsPerPage?: TableProps['itemsPerPage'] 200 ) { 201 if (!itemsPerPage) { 202 return bodyRows; 203 } 204 205 return bodyRows.slice(currPage * itemsPerPage, itemsPerPage * (currPage + 1)); 206 } 207 208 interface PaginationNavigationProps { 209 bodyRows?: Extract<Table, { type: 'filled' }>['bodyRows']; 210 currPage: number; 211 itemsPerPage?: TableProps['itemsPerPage']; 212 setCurrPage: (i: number) => void; 213 } 214 215 function PaginationNavigation({ 216 itemsPerPage, 217 currPage, 218 setCurrPage, 219 bodyRows, 220 }: PaginationNavigationProps) { 221 if (!itemsPerPage) { 222 return null; 223 } 224 225 const isThereNextPage = bodyRows 226 ? paginate(bodyRows, currPage + 1, itemsPerPage).length > 0 227 : false; 228 229 const isTherePreviousPage = bodyRows 230 ? paginate(bodyRows, currPage - 1, itemsPerPage).length > 0 231 : false; 232 233 return ( 234 <nav className={styles.pagination}> 235 <Button 236 aria-label="Previous Page" 237 disabled={!isTherePreviousPage} 238 kind="float" 239 icon={faChevronLeft} 240 onClick={() => setCurrPage(currPage - 1)} 241 /> 242 <Button 243 disabled={!isThereNextPage} 244 aria-label="Next Page" 245 kind="float" 246 icon={faChevronRight} 247 onClick={() => setCurrPage(currPage + 1)} 248 /> 249 </nav> 250 ); 251 } 252 253 export default TableComponent;