mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: port health reports to react-table (#1017)
* refactor: fix table header sort button focus styles * refactor: extract FeatureNameCell component * refactor: port health reports to react-table * refactor: hide columns on small screens * refactor: sort features by name
This commit is contained in:
		
							parent
							
								
									20d738f725
								
							
						
					
					
						commit
						76ea65b65c
					
				| @ -0,0 +1,18 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | ||||
| import { IReportTableRow } from 'component/Reporting/ReportTable/ReportTable'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| 
 | ||||
| interface IReportExpiredCellProps { | ||||
|     row: { | ||||
|         original: IReportTableRow; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export const ReportExpiredCell: VFC<IReportExpiredCellProps> = ({ row }) => { | ||||
|     if (row.original.expiredAt) { | ||||
|         return <DateCell value={row.original.expiredAt} />; | ||||
|     } | ||||
| 
 | ||||
|     return <TextCell>N/A</TextCell>; | ||||
| }; | ||||
| @ -0,0 +1,29 @@ | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes'; | ||||
| import { | ||||
|     getDiffInDays, | ||||
|     expired, | ||||
|     toggleExpiryByTypeMap, | ||||
| } from 'component/Reporting/utils'; | ||||
| import { subDays, parseISO } from 'date-fns'; | ||||
| 
 | ||||
| export const formatExpiredAt = ( | ||||
|     feature: IFeatureToggleListItem | ||||
| ): string | undefined => { | ||||
|     const { type, createdAt } = feature; | ||||
| 
 | ||||
|     if (type === KILLSWITCH || type === PERMISSION) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const date = parseISO(createdAt); | ||||
|     const now = new Date(); | ||||
|     const diff = getDiffInDays(date, now); | ||||
| 
 | ||||
|     if (expired(diff, type)) { | ||||
|         const result = diff - toggleExpiryByTypeMap[type]; | ||||
|         return subDays(now, result).toISOString(); | ||||
|     } | ||||
| 
 | ||||
|     return; | ||||
| }; | ||||
| @ -0,0 +1,43 @@ | ||||
| import { VFC, ReactElement } from 'react'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| import { ReportProblemOutlined, Check } from '@mui/icons-material'; | ||||
| import { styled } from '@mui/material'; | ||||
| import { IReportTableRow } from 'component/Reporting/ReportTable/ReportTable'; | ||||
| 
 | ||||
| interface IReportStatusCellProps { | ||||
|     row: { | ||||
|         original: IReportTableRow; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export const ReportStatusCell: VFC<IReportStatusCellProps> = ({ | ||||
|     row, | ||||
| }): ReactElement => { | ||||
|     if (row.original.status === 'potentially-stale') { | ||||
|         return ( | ||||
|             <TextCell> | ||||
|                 <StyledText> | ||||
|                     <ReportProblemOutlined /> | ||||
|                     <span>Potentially stale</span> | ||||
|                 </StyledText> | ||||
|             </TextCell> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <TextCell> | ||||
|             <StyledText> | ||||
|                 <Check /> | ||||
|                 <span>Healthy</span> | ||||
|             </StyledText> | ||||
|         </TextCell> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const StyledText = styled('span')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     gap: '1ch', | ||||
|     alignItems: 'center', | ||||
|     textAlign: 'right', | ||||
|     '& svg': { color: theme.palette.inactiveIcon }, | ||||
| })); | ||||
| @ -0,0 +1,21 @@ | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| import { getDiffInDays, expired } from 'component/Reporting/utils'; | ||||
| import { PERMISSION, KILLSWITCH } from 'constants/featureToggleTypes'; | ||||
| import { parseISO } from 'date-fns'; | ||||
| 
 | ||||
| export type ReportingStatus = 'potentially-stale' | 'healthy'; | ||||
| 
 | ||||
| export const formatStatus = ( | ||||
|     feature: IFeatureToggleListItem | ||||
| ): ReportingStatus => { | ||||
|     const { type, createdAt } = feature; | ||||
|     const date = parseISO(createdAt); | ||||
|     const now = new Date(); | ||||
|     const diff = getDiffInDays(date, now); | ||||
| 
 | ||||
|     if (expired(diff, type) && type !== KILLSWITCH && type !== PERMISSION) { | ||||
|         return 'potentially-stale'; | ||||
|     } | ||||
| 
 | ||||
|     return 'healthy'; | ||||
| }; | ||||
							
								
								
									
										192
									
								
								frontend/src/component/Reporting/ReportTable/ReportTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								frontend/src/component/Reporting/ReportTable/ReportTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,192 @@ | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| import { | ||||
|     TableSearch, | ||||
|     SortableTableHeader, | ||||
|     TableCell, | ||||
| } from 'component/common/Table'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { sortTypes } from 'utils/sortTypes'; | ||||
| import { useSortBy, useGlobalFilter, useTable } from 'react-table'; | ||||
| import { Table, TableBody, TableRow, useMediaQuery } from '@mui/material'; | ||||
| import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell'; | ||||
| import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; | ||||
| import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; | ||||
| import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; | ||||
| import { ReportExpiredCell } from 'component/Reporting/ReportExpiredCell/ReportExpiredCell'; | ||||
| import { ReportStatusCell } from 'component/Reporting/ReportStatusCell/ReportStatusCell'; | ||||
| import { useMemo, useEffect } from 'react'; | ||||
| import { | ||||
|     formatStatus, | ||||
|     ReportingStatus, | ||||
| } from 'component/Reporting/ReportStatusCell/formatStatus'; | ||||
| import { formatExpiredAt } from 'component/Reporting/ReportExpiredCell/formatExpiredAt'; | ||||
| import { FeatureStaleCell } from 'component/feature/FeatureToggleList/FeatureStaleCell/FeatureStaleCell'; | ||||
| import theme from 'themes/theme'; | ||||
| 
 | ||||
| interface IReportTableProps { | ||||
|     projectId: string; | ||||
|     features: IFeatureToggleListItem[]; | ||||
| } | ||||
| 
 | ||||
| export interface IReportTableRow { | ||||
|     project: string; | ||||
|     name: string; | ||||
|     type: string; | ||||
|     stale?: boolean; | ||||
|     status: ReportingStatus; | ||||
|     lastSeenAt?: string; | ||||
|     createdAt: string; | ||||
|     expiredAt?: string; | ||||
| } | ||||
| 
 | ||||
| export const ReportTable = ({ projectId, features }: IReportTableProps) => { | ||||
|     const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); | ||||
| 
 | ||||
|     const data: IReportTableRow[] = useMemo(() => { | ||||
|         return features.map(feature => { | ||||
|             return createReportTableRow(projectId, feature); | ||||
|         }); | ||||
|     }, [projectId, features]); | ||||
| 
 | ||||
|     const initialState = useMemo( | ||||
|         () => ({ | ||||
|             hiddenColumns: ['description'], | ||||
|             sortBy: [{ id: 'name' }], | ||||
|         }), | ||||
|         [] | ||||
|     ); | ||||
| 
 | ||||
|     const { | ||||
|         getTableProps, | ||||
|         getTableBodyProps, | ||||
|         headerGroups, | ||||
|         rows, | ||||
|         prepareRow, | ||||
|         state: { globalFilter }, | ||||
|         setGlobalFilter, | ||||
|         setHiddenColumns, | ||||
|     } = useTable( | ||||
|         { | ||||
|             columns: COLUMNS as any, | ||||
|             data: data as any, | ||||
|             initialState, | ||||
|             sortTypes, | ||||
|             autoResetGlobalFilter: false, | ||||
|             autoResetSortBy: false, | ||||
|             disableSortRemove: true, | ||||
|         }, | ||||
|         useGlobalFilter, | ||||
|         useSortBy | ||||
|     ); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         if (isSmallScreen) { | ||||
|             setHiddenColumns(['createdAt', 'expiredAt', 'description']); | ||||
|         } else { | ||||
|             setHiddenColumns(['description']); | ||||
|         } | ||||
|     }, [setHiddenColumns, isSmallScreen]); | ||||
| 
 | ||||
|     const header = ( | ||||
|         <PageHeader | ||||
|             title="Overview" | ||||
|             actions={ | ||||
|                 <TableSearch | ||||
|                     initialValue={globalFilter} | ||||
|                     onChange={setGlobalFilter} | ||||
|                 /> | ||||
|             } | ||||
|         /> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <PageContent header={header}> | ||||
|             <SearchHighlightProvider value={globalFilter}> | ||||
|                 <Table {...getTableProps()}> | ||||
|                     <SortableTableHeader headerGroups={headerGroups} /> | ||||
|                     <TableBody {...getTableBodyProps()}> | ||||
|                         {rows.map(row => { | ||||
|                             prepareRow(row); | ||||
|                             return ( | ||||
|                                 <TableRow hover {...row.getRowProps()}> | ||||
|                                     {row.cells.map(cell => ( | ||||
|                                         <TableCell {...cell.getCellProps()}> | ||||
|                                             {cell.render('Cell')} | ||||
|                                         </TableCell> | ||||
|                                     ))} | ||||
|                                 </TableRow> | ||||
|                             ); | ||||
|                         })} | ||||
|                     </TableBody> | ||||
|                 </Table> | ||||
|             </SearchHighlightProvider> | ||||
|         </PageContent> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const createReportTableRow = ( | ||||
|     projectId: string, | ||||
|     report: IFeatureToggleListItem | ||||
| ): IReportTableRow => { | ||||
|     return { | ||||
|         project: projectId, | ||||
|         name: report.name, | ||||
|         type: report.type, | ||||
|         stale: report.stale, | ||||
|         status: formatStatus(report), | ||||
|         lastSeenAt: report.lastSeenAt, | ||||
|         createdAt: report.createdAt, | ||||
|         expiredAt: formatExpiredAt(report), | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const COLUMNS = [ | ||||
|     { | ||||
|         Header: 'Seen', | ||||
|         accessor: 'lastSeenAt', | ||||
|         sortType: 'date', | ||||
|         align: 'center', | ||||
|         Cell: FeatureSeenCell, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Type', | ||||
|         accessor: 'type', | ||||
|         align: 'center', | ||||
|         Cell: FeatureTypeCell, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Feature toggle name', | ||||
|         accessor: 'name', | ||||
|         width: '60%', | ||||
|         sortType: 'alphanumeric', | ||||
|         Cell: FeatureNameCell, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Created on', | ||||
|         accessor: 'createdAt', | ||||
|         sortType: 'date', | ||||
|         Cell: DateCell, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Expired', | ||||
|         accessor: 'expiredAt', | ||||
|         Cell: ReportExpiredCell, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'Status', | ||||
|         accessor: 'status', | ||||
|         align: 'right', | ||||
|         Cell: ReportStatusCell, | ||||
|     }, | ||||
|     { | ||||
|         Header: 'State', | ||||
|         accessor: 'stale', | ||||
|         sortType: 'boolean', | ||||
|         Cell: FeatureStaleCell, | ||||
|     }, | ||||
|     { | ||||
|         accessor: 'description', | ||||
|     }, | ||||
| ]; | ||||
| @ -1,91 +0,0 @@ | ||||
| import { makeStyles } from 'tss-react/mui'; | ||||
| 
 | ||||
| export const useStyles = makeStyles()(theme => ({ | ||||
|     reportToggleList: { | ||||
|         width: '100%', | ||||
|         margin: 'var(--card-margin-y) 0', | ||||
|         borderRadius: 10, | ||||
|         boxShadow: 'none', | ||||
|     }, | ||||
|     bulkAction: { | ||||
|         backgroundColor: '#f2f2f2', | ||||
|         fontSize: 'var(--p-size)', | ||||
|     }, | ||||
|     sortIcon: { | ||||
|         marginLeft: 8, | ||||
|     }, | ||||
|     reportToggleListHeader: { | ||||
|         display: 'flex', | ||||
|         justifyContent: 'space-between', | ||||
|         alignItems: 'center', | ||||
|         borderBottom: '1px solid #f1f1f1', | ||||
|         padding: '1rem var(--card-padding-x)', | ||||
|     }, | ||||
|     reportToggleListInnerContainer: { | ||||
|         padding: 'var(--card-padding)', | ||||
|     }, | ||||
|     reportToggleListHeading: { | ||||
|         fontSize: 'var(--h1-size)', | ||||
|         margin: 0, | ||||
|         fontWeight: 'bold', | ||||
|     }, | ||||
|     reportIcon: { | ||||
|         fontsize: '1.5rem', | ||||
|         marginRight: 5, | ||||
|     }, | ||||
|     reportingToggleTable: { | ||||
|         width: ' 100%', | ||||
|         borderSpacing: '0 0.8rem', | ||||
|         '& th': { | ||||
|             textAlign: 'left', | ||||
|             cursor: 'pointer', | ||||
|         }, | ||||
|     }, | ||||
|     expired: { | ||||
|         color: 'var(--danger)', | ||||
|     }, | ||||
|     active: { | ||||
|         color: 'var(--success)', | ||||
|     }, | ||||
|     stale: { | ||||
|         color: 'var(--danger)', | ||||
|     }, | ||||
|     reportStatus: { | ||||
|         display: 'flex', | ||||
|         alignItems: 'center', | ||||
|     }, | ||||
|     tableRow: { | ||||
|         '&:hover': { | ||||
|             backgroundColor: '#eeeeee', | ||||
|         }, | ||||
|     }, | ||||
|     checkbox: { | ||||
|         margin: 0, | ||||
|         padding: 0, | ||||
|     }, | ||||
|     link: { | ||||
|         color: theme.palette.primary.main, | ||||
|         textDecoration: 'none', | ||||
|         fontWeight: theme.fontWeight.bold, | ||||
|     }, | ||||
|     hideColumn: { | ||||
|         [theme.breakpoints.down(800)]: { | ||||
|             display: 'none', | ||||
|         }, | ||||
|     }, | ||||
|     th: { | ||||
|         [theme.breakpoints.down(800)]: { | ||||
|             minWidth: '120px', | ||||
|         }, | ||||
|     }, | ||||
|     hideColumnStatus: { | ||||
|         [theme.breakpoints.down(550)]: { | ||||
|             display: 'none', | ||||
|         }, | ||||
|     }, | ||||
|     hideColumnLastSeen: { | ||||
|         [theme.breakpoints.down(425)]: { | ||||
|             display: 'none', | ||||
|         }, | ||||
|     }, | ||||
| })); | ||||
| @ -1,110 +0,0 @@ | ||||
| import { useState, useEffect, VFC } from 'react'; | ||||
| import { Paper, MenuItem } from '@mui/material'; | ||||
| import { useFeaturesSort } from 'hooks/useFeaturesSort'; | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import DropdownMenu from 'component/common/DropdownMenu/DropdownMenu'; | ||||
| import { | ||||
|     getObjectProperties, | ||||
|     getCheckedState, | ||||
|     applyCheckedToFeatures, | ||||
| } from '../utils'; | ||||
| import { ReportToggleListItem } from './ReportToggleListItem/ReportToggleListItem'; | ||||
| import { ReportToggleListHeader } from './ReportToggleListHeader/ReportToggleListHeader'; | ||||
| import { useStyles } from './ReportToggleList.styles'; | ||||
| 
 | ||||
| /* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */ | ||||
| const BULK_ACTIONS_ON = false; | ||||
| 
 | ||||
| interface IReportToggleListProps { | ||||
|     selectedProject: string; | ||||
|     features: IFeatureToggleListItem[]; | ||||
| } | ||||
| 
 | ||||
| export const ReportToggleList: VFC<IReportToggleListProps> = ({ | ||||
|     features, | ||||
|     selectedProject, | ||||
| }) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const [checkAll, setCheckAll] = useState(false); | ||||
|     const [localFeatures, setFeatures] = useState<IFeatureToggleListItem[]>([]); | ||||
|     // @ts-expect-error
 | ||||
|     const { setSort, sorted } = useFeaturesSort(localFeatures); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         const formattedFeatures = features.map(feature => ({ | ||||
|             ...getObjectProperties( | ||||
|                 feature, | ||||
|                 'name', | ||||
|                 'lastSeenAt', | ||||
|                 'createdAt', | ||||
|                 'stale', | ||||
|                 'type' | ||||
|             ), | ||||
|             // @ts-expect-error
 | ||||
|             checked: getCheckedState(feature.name, features), | ||||
|             setFeatures, | ||||
|         })); | ||||
| 
 | ||||
|         // @ts-expect-error
 | ||||
|         setFeatures(formattedFeatures); | ||||
|     }, [features, selectedProject]); | ||||
| 
 | ||||
|     const handleCheckAll = () => { | ||||
|         if (!checkAll) { | ||||
|             setCheckAll(true); | ||||
|             return setFeatures(prev => applyCheckedToFeatures(prev, true)); | ||||
|         } | ||||
|         setCheckAll(false); | ||||
|         return setFeatures(prev => applyCheckedToFeatures(prev, false)); | ||||
|     }; | ||||
| 
 | ||||
|     const renderListRows = () => | ||||
|         sorted.map(feature => ( | ||||
|             // @ts-expect-error
 | ||||
|             <ReportToggleListItem | ||||
|                 key={feature.name} | ||||
|                 {...feature} | ||||
|                 project={selectedProject} | ||||
|                 bulkActionsOn={BULK_ACTIONS_ON} | ||||
|             /> | ||||
|         )); | ||||
| 
 | ||||
|     const renderBulkActionsMenu = () => ( | ||||
|         <DropdownMenu | ||||
|             id="bulk-actions" | ||||
|             label="Bulk actions" | ||||
|             renderOptions={() => ( | ||||
|                 <> | ||||
|                     <MenuItem>Mark toggles as stale</MenuItem> | ||||
|                     <MenuItem>Delete toggles</MenuItem> | ||||
|                 </> | ||||
|             )} | ||||
|         /> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <Paper className={styles.reportToggleList}> | ||||
|             <div className={styles.reportToggleListHeader}> | ||||
|                 <h3 className={styles.reportToggleListHeading}>Overview</h3> | ||||
|                 <ConditionallyRender | ||||
|                     condition={BULK_ACTIONS_ON} | ||||
|                     show={renderBulkActionsMenu} | ||||
|                 /> | ||||
|             </div> | ||||
|             <div className={styles.reportToggleListInnerContainer}> | ||||
|                 <table className={styles.reportingToggleTable}> | ||||
|                     <ReportToggleListHeader | ||||
|                         handleCheckAll={handleCheckAll} | ||||
|                         checkAll={checkAll} | ||||
|                         // @ts-expect-error
 | ||||
|                         setSort={setSort} | ||||
|                         bulkActionsOn={BULK_ACTIONS_ON} | ||||
|                     /> | ||||
| 
 | ||||
|                     <tbody>{renderListRows()}</tbody> | ||||
|                 </table> | ||||
|             </div> | ||||
|         </Paper> | ||||
|     ); | ||||
| }; | ||||
| @ -1,104 +0,0 @@ | ||||
| import { Dispatch, SetStateAction, VFC } from 'react'; | ||||
| import { Checkbox } from '@mui/material'; | ||||
| import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { ReportingSortType } from 'component/Reporting/constants'; | ||||
| import { useStyles } from '../ReportToggleList.styles'; | ||||
| 
 | ||||
| interface IReportToggleListHeaderProps { | ||||
|     checkAll: boolean; | ||||
|     setSort: Dispatch< | ||||
|         SetStateAction<{ type: ReportingSortType; desc?: boolean }> | ||||
|     >; | ||||
|     bulkActionsOn: boolean; | ||||
|     handleCheckAll: () => void; | ||||
| } | ||||
| 
 | ||||
| export const ReportToggleListHeader: VFC<IReportToggleListHeaderProps> = ({ | ||||
|     handleCheckAll, | ||||
|     checkAll, | ||||
|     setSort, | ||||
|     bulkActionsOn, | ||||
| }) => { | ||||
|     const { classes: styles } = useStyles(); | ||||
|     const handleSort = (type: ReportingSortType) => { | ||||
|         setSort(prev => ({ | ||||
|             type, | ||||
|             desc: !prev.desc, | ||||
|         })); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <ConditionallyRender | ||||
|                     condition={bulkActionsOn} | ||||
|                     show={ | ||||
|                         <th> | ||||
|                             <Checkbox | ||||
|                                 onChange={handleCheckAll} | ||||
|                                 value={checkAll} | ||||
|                                 checked={checkAll} | ||||
|                                 className={styles.checkbox} | ||||
|                             /> | ||||
|                         </th> | ||||
|                     } | ||||
|                 /> | ||||
| 
 | ||||
|                 <th | ||||
|                     role="button" | ||||
|                     tabIndex={0} | ||||
|                     style={{ width: '150px' }} | ||||
|                     onClick={() => handleSort('name')} | ||||
|                 > | ||||
|                     Name | ||||
|                     <UnfoldMoreOutlinedIcon className={styles.sortIcon} /> | ||||
|                 </th> | ||||
|                 <th | ||||
|                     role="button" | ||||
|                     className={styles.hideColumnLastSeen} | ||||
|                     tabIndex={0} | ||||
|                     onClick={() => handleSort('last-seen')} | ||||
|                 > | ||||
|                     Last seen | ||||
|                     <UnfoldMoreOutlinedIcon className={styles.sortIcon} /> | ||||
|                 </th> | ||||
|                 <th | ||||
|                     role="button" | ||||
|                     tabIndex={0} | ||||
|                     className={styles.hideColumn} | ||||
|                     onClick={() => handleSort('created')} | ||||
|                 > | ||||
|                     Created | ||||
|                     <UnfoldMoreOutlinedIcon className={styles.sortIcon} /> | ||||
|                 </th> | ||||
|                 <th | ||||
|                     role="button" | ||||
|                     tabIndex={0} | ||||
|                     className={styles.hideColumn} | ||||
|                     onClick={() => handleSort('expired')} | ||||
|                 > | ||||
|                     Expired | ||||
|                     <UnfoldMoreOutlinedIcon className={styles.sortIcon} /> | ||||
|                 </th> | ||||
|                 <th | ||||
|                     role="button" | ||||
|                     tabIndex={0} | ||||
|                     className={styles.hideColumnStatus} | ||||
|                     onClick={() => handleSort('status')} | ||||
|                 > | ||||
|                     Status | ||||
|                     <UnfoldMoreOutlinedIcon className={styles.sortIcon} /> | ||||
|                 </th> | ||||
|                 <th | ||||
|                     role="button" | ||||
|                     tabIndex={0} | ||||
|                     onClick={() => handleSort('expired')} | ||||
|                 > | ||||
|                     Report | ||||
|                     <UnfoldMoreOutlinedIcon className={styles.sortIcon} /> | ||||
|                 </th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|     ); | ||||
| }; | ||||
| @ -1,173 +0,0 @@ | ||||
| import { memo, ReactNode } from 'react'; | ||||
| import classnames from 'classnames'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { Checkbox } from '@mui/material'; | ||||
| import CheckIcon from '@mui/icons-material/Check'; | ||||
| import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import FeatureStatus from 'component/feature/FeatureView/FeatureStatus/FeatureStatus'; | ||||
| import { | ||||
|     pluralize, | ||||
|     getDates, | ||||
|     expired, | ||||
|     toggleExpiryByTypeMap, | ||||
|     getDiffInDays, | ||||
| } from 'component/Reporting/utils'; | ||||
| import { KILLSWITCH, PERMISSION } from 'constants/featureToggleTypes'; | ||||
| import { useStyles } from '../ReportToggleList.styles'; | ||||
| import { getTogglePath } from 'utils/routePathHelpers'; | ||||
| 
 | ||||
| interface IReportToggleListItemProps { | ||||
|     name: string; | ||||
|     stale: boolean; | ||||
|     project: string; | ||||
|     lastSeenAt?: string; | ||||
|     createdAt: string; | ||||
|     type: string; | ||||
|     checked: boolean; | ||||
|     bulkActionsOn: boolean; | ||||
|     setFeatures: () => void; | ||||
| } | ||||
| 
 | ||||
| export const ReportToggleListItem = memo<IReportToggleListItemProps>( | ||||
|     ({ | ||||
|         name, | ||||
|         stale, | ||||
|         lastSeenAt, | ||||
|         createdAt, | ||||
|         project, | ||||
|         type, | ||||
|         checked, | ||||
|         bulkActionsOn, | ||||
|         setFeatures, | ||||
|     }) => { | ||||
|         const { classes: styles } = useStyles(); | ||||
|         const nameMatches = (feature: { name: string }) => | ||||
|             feature.name === name; | ||||
| 
 | ||||
|         const handleChange = () => { | ||||
|             // @ts-expect-error
 | ||||
|             setFeatures(prevState => { | ||||
|                 const newState = [...prevState]; | ||||
| 
 | ||||
|                 return newState.map(feature => { | ||||
|                     if (nameMatches(feature)) { | ||||
|                         return { ...feature, checked: !feature.checked }; | ||||
|                     } | ||||
|                     return feature; | ||||
|                 }); | ||||
|             }); | ||||
|         }; | ||||
| 
 | ||||
|         const formatCreatedAt = () => { | ||||
|             const [date, now] = getDates(createdAt); | ||||
| 
 | ||||
|             const diff = getDiffInDays(date, now); | ||||
|             if (diff === 0) return '1 day'; | ||||
| 
 | ||||
|             const formatted = pluralize(diff, 'day'); | ||||
| 
 | ||||
|             return `${formatted} ago`; | ||||
|         }; | ||||
| 
 | ||||
|         const formatExpiredAt = () => { | ||||
|             if (type === KILLSWITCH || type === PERMISSION) { | ||||
|                 return 'N/A'; | ||||
|             } | ||||
| 
 | ||||
|             const [date, now] = getDates(createdAt); | ||||
|             const diff = getDiffInDays(date, now); | ||||
| 
 | ||||
|             if (expired(diff, type)) { | ||||
|                 const result = diff - toggleExpiryByTypeMap[type]; | ||||
|                 if (result === 0) return '1 day'; | ||||
| 
 | ||||
|                 return pluralize(result, 'day'); | ||||
|             } | ||||
|             return 'N/A'; | ||||
|         }; | ||||
| 
 | ||||
|         const formatLastSeenAt = () => { | ||||
|             return ( | ||||
|                 <FeatureStatus | ||||
|                     lastSeenAt={lastSeenAt} | ||||
|                     tooltipPlacement="bottom" | ||||
|                 /> | ||||
|             ); | ||||
|         }; | ||||
| 
 | ||||
|         const renderStatus = (icon: ReactNode, text: ReactNode) => ( | ||||
|             <span className={styles.reportStatus}> | ||||
|                 {icon} | ||||
|                 {text} | ||||
|             </span> | ||||
|         ); | ||||
| 
 | ||||
|         const formatReportStatus = () => { | ||||
|             if (type === KILLSWITCH || type === PERMISSION) { | ||||
|                 return renderStatus( | ||||
|                     <CheckIcon className={styles.reportIcon} />, | ||||
|                     'Healthy' | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             const [date, now] = getDates(createdAt); | ||||
|             const diff = getDiffInDays(date, now); | ||||
| 
 | ||||
|             if (expired(diff, type)) { | ||||
|                 return renderStatus( | ||||
|                     <ReportProblemOutlinedIcon className={styles.reportIcon} />, | ||||
|                     'Potentially stale' | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             return renderStatus( | ||||
|                 <CheckIcon className={styles.reportIcon} />, | ||||
|                 'Healthy' | ||||
|             ); | ||||
|         }; | ||||
| 
 | ||||
|         const statusClasses = classnames( | ||||
|             styles.active, | ||||
|             styles.hideColumnStatus, | ||||
|             { | ||||
|                 [styles.stale]: stale, | ||||
|             } | ||||
|         ); | ||||
| 
 | ||||
|         return ( | ||||
|             <tr className={styles.tableRow}> | ||||
|                 <ConditionallyRender | ||||
|                     condition={bulkActionsOn} | ||||
|                     show={ | ||||
|                         <td> | ||||
|                             <Checkbox | ||||
|                                 checked={checked} | ||||
|                                 value={checked} | ||||
|                                 onChange={handleChange} | ||||
|                                 className={styles.checkbox} | ||||
|                             /> | ||||
|                         </td> | ||||
|                     } | ||||
|                 /> | ||||
|                 <td> | ||||
|                     <Link | ||||
|                         to={getTogglePath(project, name)} | ||||
|                         className={styles.link} | ||||
|                     > | ||||
|                         {name} | ||||
|                     </Link> | ||||
|                 </td> | ||||
|                 <td className={styles.hideColumnLastSeen}> | ||||
|                     {formatLastSeenAt()} | ||||
|                 </td> | ||||
|                 <td className={styles.hideColumn}>{formatCreatedAt()}</td> | ||||
|                 <td className={`${styles.expired} ${styles.hideColumn}`}> | ||||
|                     {formatExpiredAt()} | ||||
|                 </td> | ||||
|                 <td className={statusClasses}>{stale ? 'Stale' : 'Active'}</td> | ||||
|                 <td>{formatReportStatus()}</td> | ||||
|             </tr> | ||||
|         ); | ||||
|     } | ||||
| ); | ||||
| @ -1,7 +0,0 @@ | ||||
| export type ReportingSortType = | ||||
|     | 'name' | ||||
|     | 'last-seen' | ||||
|     | 'created' | ||||
|     | 'expired' | ||||
|     | 'status' | ||||
|     | 'report'; | ||||
| @ -1,130 +0,0 @@ | ||||
| import { | ||||
|     sortFeaturesByNameAscending, | ||||
|     sortFeaturesByNameDescending, | ||||
|     sortFeaturesByLastSeenAscending, | ||||
|     sortFeaturesByLastSeenDescending, | ||||
|     sortFeaturesByCreatedAtAscending, | ||||
|     sortFeaturesByCreatedAtDescending, | ||||
|     sortFeaturesByExpiredAtAscending, | ||||
|     sortFeaturesByExpiredAtDescending, | ||||
|     sortFeaturesByStatusAscending, | ||||
|     sortFeaturesByStatusDescending, | ||||
| } from 'component/Reporting/utils'; | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| 
 | ||||
| const getTestData = (): IFeatureToggleListItem[] => [ | ||||
|     { | ||||
|         name: 'abe', | ||||
|         createdAt: '2021-02-14T02:42:34.515Z', | ||||
|         lastSeenAt: '2021-02-21T19:34:21.830Z', | ||||
|         type: 'release', | ||||
|         stale: false, | ||||
|         environments: [], | ||||
|     }, | ||||
|     { | ||||
|         name: 'bet', | ||||
|         createdAt: '2021-02-13T02:42:34.515Z', | ||||
|         lastSeenAt: '2021-02-19T19:34:21.830Z', | ||||
|         type: 'release', | ||||
|         stale: false, | ||||
|         environments: [], | ||||
|     }, | ||||
|     { | ||||
|         name: 'cat', | ||||
|         createdAt: '2021-02-12T02:42:34.515Z', | ||||
|         lastSeenAt: '2021-02-18T19:34:21.830Z', | ||||
|         type: 'experiment', | ||||
|         stale: true, | ||||
|         environments: [], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| test('it sorts features by name ascending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByNameAscending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('abe'); | ||||
|     expect(result[2].name).toBe('cat'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by name descending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByNameDescending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('cat'); | ||||
|     expect(result[2].name).toBe('abe'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by lastSeenAt ascending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByLastSeenAscending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('cat'); | ||||
|     expect(result[2].name).toBe('abe'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by lastSeenAt descending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByLastSeenDescending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('abe'); | ||||
|     expect(result[2].name).toBe('cat'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by createdAt ascending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByCreatedAtAscending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('cat'); | ||||
|     expect(result[2].name).toBe('abe'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by createdAt descending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByCreatedAtDescending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('abe'); | ||||
|     expect(result[2].name).toBe('cat'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by expired ascending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByExpiredAtAscending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('cat'); | ||||
|     expect(result[2].name).toBe('abe'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by expired descending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByExpiredAtDescending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('abe'); | ||||
|     expect(result[2].name).toBe('cat'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by status ascending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByStatusAscending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('abe'); | ||||
|     expect(result[2].name).toBe('cat'); | ||||
| }); | ||||
| 
 | ||||
| test('it sorts features by status descending', () => { | ||||
|     const testData = getTestData(); | ||||
| 
 | ||||
|     const result = sortFeaturesByStatusDescending(testData); | ||||
| 
 | ||||
|     expect(result[0].name).toBe('cat'); | ||||
|     expect(result[2].name).toBe('abe'); | ||||
| }); | ||||
| @ -1,10 +1,6 @@ | ||||
| import parseISO from 'date-fns/parseISO'; | ||||
| import differenceInDays from 'date-fns/differenceInDays'; | ||||
| 
 | ||||
| import { EXPERIMENT, OPERATIONAL, RELEASE } from 'constants/featureToggleTypes'; | ||||
| 
 | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| 
 | ||||
| const FORTY_DAYS = 40; | ||||
| const SEVEN_DAYS = 7; | ||||
| 
 | ||||
| @ -14,197 +10,10 @@ export const toggleExpiryByTypeMap: Record<string, number> = { | ||||
|     [OPERATIONAL]: SEVEN_DAYS, | ||||
| }; | ||||
| 
 | ||||
| export interface IFeatureToggleListItemCheck extends IFeatureToggleListItem { | ||||
|     checked: boolean; | ||||
| } | ||||
| 
 | ||||
| export const applyCheckedToFeatures = ( | ||||
|     features: IFeatureToggleListItem[], | ||||
|     checkedState: boolean | ||||
| ): IFeatureToggleListItemCheck[] => { | ||||
|     return features.map(feature => ({ | ||||
|         ...feature, | ||||
|         checked: checkedState, | ||||
|     })); | ||||
| export const getDiffInDays = (date: Date, now: Date) => { | ||||
|     return Math.abs(differenceInDays(date, now)); | ||||
| }; | ||||
| 
 | ||||
| export const getCheckedState = ( | ||||
|     name: string, | ||||
|     features: IFeatureToggleListItemCheck[] | ||||
| ) => { | ||||
|     const feature = features.find(feature => feature.name === name); | ||||
| 
 | ||||
|     if (feature) { | ||||
|         return feature.checked; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
| }; | ||||
| 
 | ||||
| export const getDiffInDays = (date: Date, now: Date) => | ||||
|     Math.abs(differenceInDays(date, now)); | ||||
| 
 | ||||
| export const expired = (diff: number, type: string) => { | ||||
|     if (diff >= toggleExpiryByTypeMap[type]) return true; | ||||
|     return false; | ||||
| }; | ||||
| 
 | ||||
| export const getObjectProperties = <T extends object>( | ||||
|     target: T, | ||||
|     ...keys: (keyof T)[] | ||||
| ): Partial<T> => { | ||||
|     const newObject: Partial<T> = {}; | ||||
| 
 | ||||
|     keys.forEach(key => { | ||||
|         if (target[key] !== undefined) { | ||||
|             newObject[key] = target[key]; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     return newObject; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByNameAscending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => { | ||||
|     const sorted = [...features]; | ||||
|     sorted.sort((a, b) => { | ||||
|         if (a.name < b.name) { | ||||
|             return -1; | ||||
|         } | ||||
|         if (a.name > b.name) { | ||||
|             return 1; | ||||
|         } | ||||
|         return 0; | ||||
|     }); | ||||
|     return sorted; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByNameDescending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => | ||||
|     sortFeaturesByNameAscending([...features]).reverse(); | ||||
| 
 | ||||
| export const sortFeaturesByLastSeenAscending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => { | ||||
|     const sorted = [...features]; | ||||
|     sorted.sort((a, b) => { | ||||
|         if (!a.lastSeenAt) return -1; | ||||
|         if (!b.lastSeenAt) return 1; | ||||
| 
 | ||||
|         const dateA = parseISO(a.lastSeenAt); | ||||
|         const dateB = parseISO(b.lastSeenAt); | ||||
| 
 | ||||
|         return dateA.getTime() - dateB.getTime(); | ||||
|     }); | ||||
|     return sorted; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByLastSeenDescending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => | ||||
|     sortFeaturesByLastSeenAscending([...features]).reverse(); | ||||
| 
 | ||||
| export const sortFeaturesByCreatedAtAscending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => { | ||||
|     const sorted = [...features]; | ||||
|     sorted.sort((a, b) => { | ||||
|         const dateA = parseISO(a.createdAt); | ||||
|         const dateB = parseISO(b.createdAt); | ||||
| 
 | ||||
|         return dateA.getTime() - dateB.getTime(); | ||||
|     }); | ||||
|     return sorted; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByCreatedAtDescending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => | ||||
|     sortFeaturesByCreatedAtAscending([...features]).reverse(); | ||||
| 
 | ||||
| export const sortFeaturesByExpiredAtAscending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => { | ||||
|     const sorted = [...features]; | ||||
|     sorted.sort((a, b) => { | ||||
|         const now = new Date(); | ||||
|         const dateA = parseISO(a.createdAt); | ||||
|         const dateB = parseISO(b.createdAt); | ||||
| 
 | ||||
|         const diffA = getDiffInDays(dateA, now); | ||||
|         const diffB = getDiffInDays(dateB, now); | ||||
| 
 | ||||
|         if (!expired(diffA, a.type) && expired(diffB, b.type)) { | ||||
|             return 1; | ||||
|         } | ||||
| 
 | ||||
|         if (expired(diffA, a.type) && !expired(diffB, b.type)) { | ||||
|             return -1; | ||||
|         } | ||||
| 
 | ||||
|         const expiredByA = diffA - toggleExpiryByTypeMap[a.type]; | ||||
|         const expiredByB = diffB - toggleExpiryByTypeMap[b.type]; | ||||
| 
 | ||||
|         return expiredByB - expiredByA; | ||||
|     }); | ||||
|     return sorted; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByExpiredAtDescending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => { | ||||
|     const sorted = [...features]; | ||||
|     const now = new Date(); | ||||
|     sorted.sort((a, b) => { | ||||
|         const dateA = parseISO(a.createdAt); | ||||
|         const dateB = parseISO(b.createdAt); | ||||
| 
 | ||||
|         const diffA = getDiffInDays(dateA, now); | ||||
|         const diffB = getDiffInDays(dateB, now); | ||||
| 
 | ||||
|         if (!expired(diffA, a.type) && expired(diffB, b.type)) { | ||||
|             return 1; | ||||
|         } | ||||
| 
 | ||||
|         if (expired(diffA, a.type) && !expired(diffB, b.type)) { | ||||
|             return -1; | ||||
|         } | ||||
| 
 | ||||
|         const expiredByA = diffA - toggleExpiryByTypeMap[a.type]; | ||||
|         const expiredByB = diffB - toggleExpiryByTypeMap[b.type]; | ||||
| 
 | ||||
|         return expiredByA - expiredByB; | ||||
|     }); | ||||
|     return sorted; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByStatusAscending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => { | ||||
|     const sorted = [...features]; | ||||
|     sorted.sort((a, b) => { | ||||
|         if (a.stale) return 1; | ||||
|         if (b.stale) return -1; | ||||
|         return 0; | ||||
|     }); | ||||
|     return sorted; | ||||
| }; | ||||
| 
 | ||||
| export const sortFeaturesByStatusDescending = ( | ||||
|     features: IFeatureToggleListItem[] | ||||
| ): IFeatureToggleListItem[] => | ||||
|     sortFeaturesByStatusAscending([...features]).reverse(); | ||||
| 
 | ||||
| export const pluralize = (items: number, word: string): string => { | ||||
|     if (items === 1) return `${items} ${word}`; | ||||
|     return `${items} ${word}s`; | ||||
| }; | ||||
| 
 | ||||
| export const getDates = (dateString: string): [Date, Date] => { | ||||
|     const date = parseISO(dateString); | ||||
|     const now = new Date(); | ||||
| 
 | ||||
|     return [date, now]; | ||||
|     return diff >= toggleExpiryByTypeMap[type]; | ||||
| }; | ||||
|  | ||||
| @ -19,6 +19,7 @@ export const useStyles = makeStyles()(theme => ({ | ||||
|         whiteSpace: 'nowrap', | ||||
|         width: '100%', | ||||
|         position: 'relative', | ||||
|         zIndex: 1, | ||||
|         ':hover, :focus, &:focus-visible, &:active': { | ||||
|             outline: 'revert', | ||||
|             '.hover-only': { | ||||
|  | ||||
| @ -0,0 +1,22 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | ||||
| 
 | ||||
| interface IFeatureNameCellProps { | ||||
|     row: { | ||||
|         original: { | ||||
|             name: string; | ||||
|             description: string; | ||||
|             project: string; | ||||
|         }; | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => { | ||||
|     return ( | ||||
|         <LinkCell | ||||
|             title={row.original.name} | ||||
|             subtitle={row.original.description} | ||||
|             to={`/projects/${row.original.project}/features/${row.original.name}`} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -25,6 +25,7 @@ import { useLocalStorage } from 'hooks/useLocalStorage'; | ||||
| import { FeatureSchema } from 'openapi'; | ||||
| import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton'; | ||||
| import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell'; | ||||
| import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; | ||||
| 
 | ||||
| const featuresPlaceholder: FeatureSchema[] = Array(15).fill({ | ||||
|     name: 'Name of the feature', | ||||
| @ -55,18 +56,7 @@ const columns = [ | ||||
|         accessor: 'name', | ||||
|         maxWidth: 300, | ||||
|         width: '67%', | ||||
|         Cell: ({ | ||||
|             row: { | ||||
|                 // @ts-expect-error -- props type
 | ||||
|                 original: { name, description, project }, | ||||
|             }, | ||||
|         }) => ( | ||||
|             <LinkCell | ||||
|                 title={name} | ||||
|                 subtitle={description} | ||||
|                 to={`/projects/${project}/features/${name}`} | ||||
|             /> | ||||
|         ), | ||||
|         Cell: FeatureNameCell, | ||||
|         sortType: 'alphanumeric', | ||||
|     }, | ||||
|     { | ||||
| @ -139,6 +129,7 @@ export const FeatureToggleListTable: VFC = () => { | ||||
|         { | ||||
|             // @ts-expect-error -- fix in react-table v8
 | ||||
|             columns, | ||||
|             // @ts-expect-error -- fix in react-table v8
 | ||||
|             data, | ||||
|             initialState, | ||||
|             sortTypes, | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import { useHealthReport } from 'hooks/api/getters/useHealthReport/useHealthReport'; | ||||
| import ApiError from 'component/common/ApiError/ApiError'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { ReportToggleList } from 'component/Reporting/ReportToggleList/ReportToggleList'; | ||||
| import { ReportCard } from 'component/Reporting/ReportCard/ReportCard'; | ||||
| import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { ReportTable } from 'component/Reporting/ReportTable/ReportTable'; | ||||
| 
 | ||||
| interface IProjectHealthProps { | ||||
|     projectId: string; | ||||
| @ -33,8 +33,8 @@ const ProjectHealth = ({ projectId }: IProjectHealthProps) => { | ||||
|                 } | ||||
|             /> | ||||
|             <ReportCard healthReport={healthReport} /> | ||||
|             <ReportToggleList | ||||
|                 selectedProject={projectId} | ||||
|             <ReportTable | ||||
|                 projectId={projectId} | ||||
|                 features={healthReport.features} | ||||
|             /> | ||||
|         </div> | ||||
|  | ||||
| @ -150,7 +150,7 @@ exports[`renders an empty list correctly 1`] = ` | ||||
|                 <button | ||||
|                   aria-label="" | ||||
|                   aria-labelledby={null} | ||||
|                   className="tss-14l86a5-sortedButton tss-h39o0j-sortButton" | ||||
|                   className="tss-14l86a5-sortedButton tss-1inbba3-sortButton" | ||||
|                   data-mui-internal-clone-element={true} | ||||
|                   onBlur={[Function]} | ||||
|                   onClick={[Function]} | ||||
|  | ||||
| @ -185,6 +185,7 @@ const compareNullableDates = ( | ||||
| ): number => { | ||||
|     return a && b ? a?.getTime?.() - b?.getTime?.() : a ? 1 : b ? -1 : 0; | ||||
| }; | ||||
| 
 | ||||
| const sortByExpired = ( | ||||
|     features: Readonly<FeatureSchema[]> | ||||
| ): FeatureSchema[] => { | ||||
|  | ||||
| @ -1,91 +0,0 @@ | ||||
| import { useState, useCallback } from 'react'; | ||||
| import { | ||||
|     sortFeaturesByNameAscending, | ||||
|     sortFeaturesByNameDescending, | ||||
|     sortFeaturesByLastSeenAscending, | ||||
|     sortFeaturesByLastSeenDescending, | ||||
|     sortFeaturesByCreatedAtAscending, | ||||
|     sortFeaturesByCreatedAtDescending, | ||||
|     sortFeaturesByExpiredAtAscending, | ||||
|     sortFeaturesByExpiredAtDescending, | ||||
|     sortFeaturesByStatusAscending, | ||||
|     sortFeaturesByStatusDescending, | ||||
| } from 'component/Reporting/utils'; | ||||
| 
 | ||||
| import { ReportingSortType } from 'component/Reporting/constants'; | ||||
| import { IFeatureToggleListItem } from 'interfaces/featureToggle'; | ||||
| 
 | ||||
| /** | ||||
|  * @deprecated | ||||
|  */ | ||||
| const useSort = () => { | ||||
|     const [sortData, setSortData] = useState<{ | ||||
|         sortKey: ReportingSortType; | ||||
|         ascending: boolean; | ||||
|     }>({ | ||||
|         sortKey: 'name', | ||||
|         ascending: true, | ||||
|     }); | ||||
| 
 | ||||
|     const handleSortName = (features: IFeatureToggleListItem[]) => { | ||||
|         if (sortData.ascending) { | ||||
|             return sortFeaturesByNameAscending(features); | ||||
|         } | ||||
| 
 | ||||
|         return sortFeaturesByNameDescending(features); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSortLastSeen = (features: IFeatureToggleListItem[]) => { | ||||
|         if (sortData.ascending) { | ||||
|             return sortFeaturesByLastSeenAscending(features); | ||||
|         } | ||||
|         return sortFeaturesByLastSeenDescending(features); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSortCreatedAt = (features: IFeatureToggleListItem[]) => { | ||||
|         if (sortData.ascending) { | ||||
|             return sortFeaturesByCreatedAtAscending(features); | ||||
|         } | ||||
|         return sortFeaturesByCreatedAtDescending(features); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSortExpiredAt = (features: IFeatureToggleListItem[]) => { | ||||
|         if (sortData.ascending) { | ||||
|             return sortFeaturesByExpiredAtAscending(features); | ||||
|         } | ||||
|         return sortFeaturesByExpiredAtDescending(features); | ||||
|     }; | ||||
| 
 | ||||
|     const handleSortStatus = (features: IFeatureToggleListItem[]) => { | ||||
|         if (sortData.ascending) { | ||||
|             return sortFeaturesByStatusAscending(features); | ||||
|         } | ||||
|         return sortFeaturesByStatusDescending(features); | ||||
|     }; | ||||
| 
 | ||||
|     const sort = useCallback( | ||||
|         (features: IFeatureToggleListItem[]): IFeatureToggleListItem[] => { | ||||
|             switch (sortData.sortKey) { | ||||
|                 case 'name': | ||||
|                     return handleSortName(features); | ||||
|                 case 'last-seen': | ||||
|                     return handleSortLastSeen(features); | ||||
|                 case 'created': | ||||
|                     return handleSortCreatedAt(features); | ||||
|                 case 'expired': | ||||
|                 case 'report': | ||||
|                     return handleSortExpiredAt(features); | ||||
|                 case 'status': | ||||
|                     return handleSortStatus(features); | ||||
|                 default: | ||||
|                     return features; | ||||
|             } | ||||
|         }, | ||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|         [sortData] | ||||
|     ); | ||||
| 
 | ||||
|     return [sort, setSortData] as const; | ||||
| }; | ||||
| 
 | ||||
| export default useSort; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user