diff --git a/frontend/src/component/Reporting/ReportExpiredCell/ReportExpiredCell.tsx b/frontend/src/component/Reporting/ReportExpiredCell/ReportExpiredCell.tsx new file mode 100644 index 0000000000..960e1d9704 --- /dev/null +++ b/frontend/src/component/Reporting/ReportExpiredCell/ReportExpiredCell.tsx @@ -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 = ({ row }) => { + if (row.original.expiredAt) { + return ; + } + + return N/A; +}; diff --git a/frontend/src/component/Reporting/ReportExpiredCell/formatExpiredAt.ts b/frontend/src/component/Reporting/ReportExpiredCell/formatExpiredAt.ts new file mode 100644 index 0000000000..69df97efef --- /dev/null +++ b/frontend/src/component/Reporting/ReportExpiredCell/formatExpiredAt.ts @@ -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; +}; diff --git a/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx b/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx new file mode 100644 index 0000000000..601e7def9f --- /dev/null +++ b/frontend/src/component/Reporting/ReportStatusCell/ReportStatusCell.tsx @@ -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 = ({ + row, +}): ReactElement => { + if (row.original.status === 'potentially-stale') { + return ( + + + + Potentially stale + + + ); + } + + return ( + + + + Healthy + + + ); +}; + +const StyledText = styled('span')(({ theme }) => ({ + display: 'flex', + gap: '1ch', + alignItems: 'center', + textAlign: 'right', + '& svg': { color: theme.palette.inactiveIcon }, +})); diff --git a/frontend/src/component/Reporting/ReportStatusCell/formatStatus.ts b/frontend/src/component/Reporting/ReportStatusCell/formatStatus.ts new file mode 100644 index 0000000000..419c6f1729 --- /dev/null +++ b/frontend/src/component/Reporting/ReportStatusCell/formatStatus.ts @@ -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'; +}; diff --git a/frontend/src/component/Reporting/ReportTable/ReportTable.tsx b/frontend/src/component/Reporting/ReportTable/ReportTable.tsx new file mode 100644 index 0000000000..f428942fc7 --- /dev/null +++ b/frontend/src/component/Reporting/ReportTable/ReportTable.tsx @@ -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 = ( + + } + /> + ); + + return ( + + + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
+
+ ); +}; + +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', + }, +]; diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.styles.ts b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.styles.ts deleted file mode 100644 index dc7aea6442..0000000000 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.styles.ts +++ /dev/null @@ -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', - }, - }, -})); diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.tsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.tsx deleted file mode 100644 index b0dac53df4..0000000000 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.tsx +++ /dev/null @@ -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 = ({ - features, - selectedProject, -}) => { - const { classes: styles } = useStyles(); - const [checkAll, setCheckAll] = useState(false); - const [localFeatures, setFeatures] = useState([]); - // @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 - - )); - - const renderBulkActionsMenu = () => ( - ( - <> - Mark toggles as stale - Delete toggles - - )} - /> - ); - - return ( - -
-

Overview

- -
-
- - - - {renderListRows()} -
-
-
- ); -}; diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.tsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.tsx deleted file mode 100644 index bcb8bbe97e..0000000000 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.tsx +++ /dev/null @@ -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 = ({ - handleCheckAll, - checkAll, - setSort, - bulkActionsOn, -}) => { - const { classes: styles } = useStyles(); - const handleSort = (type: ReportingSortType) => { - setSort(prev => ({ - type, - desc: !prev.desc, - })); - }; - - return ( - - - - - - } - /> - - handleSort('name')} - > - Name - - - handleSort('last-seen')} - > - Last seen - - - handleSort('created')} - > - Created - - - handleSort('expired')} - > - Expired - - - handleSort('status')} - > - Status - - - handleSort('expired')} - > - Report - - - - - ); -}; diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.tsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.tsx deleted file mode 100644 index bfe97a1ea2..0000000000 --- a/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.tsx +++ /dev/null @@ -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( - ({ - 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 ( - - ); - }; - - const renderStatus = (icon: ReactNode, text: ReactNode) => ( - - {icon} - {text} - - ); - - const formatReportStatus = () => { - if (type === KILLSWITCH || type === PERMISSION) { - return renderStatus( - , - 'Healthy' - ); - } - - const [date, now] = getDates(createdAt); - const diff = getDiffInDays(date, now); - - if (expired(diff, type)) { - return renderStatus( - , - 'Potentially stale' - ); - } - - return renderStatus( - , - 'Healthy' - ); - }; - - const statusClasses = classnames( - styles.active, - styles.hideColumnStatus, - { - [styles.stale]: stale, - } - ); - - return ( - - - - - } - /> - - - {name} - - - - {formatLastSeenAt()} - - {formatCreatedAt()} - - {formatExpiredAt()} - - {stale ? 'Stale' : 'Active'} - {formatReportStatus()} - - ); - } -); diff --git a/frontend/src/component/Reporting/constants.ts b/frontend/src/component/Reporting/constants.ts deleted file mode 100644 index 775a4651b4..0000000000 --- a/frontend/src/component/Reporting/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type ReportingSortType = - | 'name' - | 'last-seen' - | 'created' - | 'expired' - | 'status' - | 'report'; diff --git a/frontend/src/component/Reporting/utils.test.ts b/frontend/src/component/Reporting/utils.test.ts deleted file mode 100644 index d132e60480..0000000000 --- a/frontend/src/component/Reporting/utils.test.ts +++ /dev/null @@ -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'); -}); diff --git a/frontend/src/component/Reporting/utils.ts b/frontend/src/component/Reporting/utils.ts index c1821e3a65..ab68760446 100644 --- a/frontend/src/component/Reporting/utils.ts +++ b/frontend/src/component/Reporting/utils.ts @@ -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 = { [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 = ( - target: T, - ...keys: (keyof T)[] -): Partial => { - const newObject: Partial = {}; - - 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]; }; diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts index e428a07c02..e7e9c88560 100644 --- a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts @@ -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': { diff --git a/frontend/src/component/common/Table/cells/FeatureNameCell/FeatureNameCell.tsx b/frontend/src/component/common/Table/cells/FeatureNameCell/FeatureNameCell.tsx new file mode 100644 index 0000000000..e41f34ec41 --- /dev/null +++ b/frontend/src/component/common/Table/cells/FeatureNameCell/FeatureNameCell.tsx @@ -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 = ({ row }) => { + return ( + + ); +}; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 2edf4d93c3..a5ca137517 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -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 }, - }, - }) => ( - - ), + 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, diff --git a/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx b/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx index dc56e425de..8b7c6a3f92 100644 --- a/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx +++ b/frontend/src/component/project/Project/ProjectHealth/ProjectHealth.tsx @@ -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) => { } /> - diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap index 0f5579dd97..18c03eda96 100644 --- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap +++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap @@ -150,7 +150,7 @@ exports[`renders an empty list correctly 1`] = `