1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02: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:
olav 2022-05-25 12:45:30 +02:00 committed by GitHub
parent 20d738f725
commit 76ea65b65c
19 changed files with 337 additions and 916 deletions

View File

@ -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>;
};

View File

@ -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;
};

View File

@ -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 },
}));

View File

@ -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';
};

View 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',
},
];

View File

@ -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',
},
},
}));

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
}
);

View File

@ -1,7 +0,0 @@
export type ReportingSortType =
| 'name'
| 'last-seen'
| 'created'
| 'expired'
| 'status'
| 'report';

View File

@ -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');
});

View File

@ -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];
};

View File

@ -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': {

View File

@ -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}`}
/>
);
};

View File

@ -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,

View File

@ -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>

View File

@ -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]}

View File

@ -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[] => {

View File

@ -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;