mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +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:
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 differenceInDays from 'date-fns/differenceInDays';
|
||||||
|
|
||||||
import { EXPERIMENT, OPERATIONAL, RELEASE } from 'constants/featureToggleTypes';
|
import { EXPERIMENT, OPERATIONAL, RELEASE } from 'constants/featureToggleTypes';
|
||||||
|
|
||||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|
||||||
|
|
||||||
const FORTY_DAYS = 40;
|
const FORTY_DAYS = 40;
|
||||||
const SEVEN_DAYS = 7;
|
const SEVEN_DAYS = 7;
|
||||||
|
|
||||||
@ -14,197 +10,10 @@ export const toggleExpiryByTypeMap: Record<string, number> = {
|
|||||||
[OPERATIONAL]: SEVEN_DAYS,
|
[OPERATIONAL]: SEVEN_DAYS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IFeatureToggleListItemCheck extends IFeatureToggleListItem {
|
export const getDiffInDays = (date: Date, now: Date) => {
|
||||||
checked: boolean;
|
return Math.abs(differenceInDays(date, now));
|
||||||
}
|
|
||||||
|
|
||||||
export const applyCheckedToFeatures = (
|
|
||||||
features: IFeatureToggleListItem[],
|
|
||||||
checkedState: boolean
|
|
||||||
): IFeatureToggleListItemCheck[] => {
|
|
||||||
return features.map(feature => ({
|
|
||||||
...feature,
|
|
||||||
checked: checkedState,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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) => {
|
export const expired = (diff: number, type: string) => {
|
||||||
if (diff >= toggleExpiryByTypeMap[type]) return true;
|
return diff >= toggleExpiryByTypeMap[type];
|
||||||
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];
|
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,7 @@ export const useStyles = makeStyles()(theme => ({
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
':hover, :focus, &:focus-visible, &:active': {
|
':hover, :focus, &:focus-visible, &:active': {
|
||||||
outline: 'revert',
|
outline: 'revert',
|
||||||
'.hover-only': {
|
'.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 { FeatureSchema } from 'openapi';
|
||||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||||
|
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
||||||
|
|
||||||
const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
||||||
name: 'Name of the feature',
|
name: 'Name of the feature',
|
||||||
@ -55,18 +56,7 @@ const columns = [
|
|||||||
accessor: 'name',
|
accessor: 'name',
|
||||||
maxWidth: 300,
|
maxWidth: 300,
|
||||||
width: '67%',
|
width: '67%',
|
||||||
Cell: ({
|
Cell: FeatureNameCell,
|
||||||
row: {
|
|
||||||
// @ts-expect-error -- props type
|
|
||||||
original: { name, description, project },
|
|
||||||
},
|
|
||||||
}) => (
|
|
||||||
<LinkCell
|
|
||||||
title={name}
|
|
||||||
subtitle={description}
|
|
||||||
to={`/projects/${project}/features/${name}`}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
sortType: 'alphanumeric',
|
sortType: 'alphanumeric',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -139,6 +129,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
{
|
{
|
||||||
// @ts-expect-error -- fix in react-table v8
|
// @ts-expect-error -- fix in react-table v8
|
||||||
columns,
|
columns,
|
||||||
|
// @ts-expect-error -- fix in react-table v8
|
||||||
data,
|
data,
|
||||||
initialState,
|
initialState,
|
||||||
sortTypes,
|
sortTypes,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useHealthReport } from 'hooks/api/getters/useHealthReport/useHealthReport';
|
import { useHealthReport } from 'hooks/api/getters/useHealthReport/useHealthReport';
|
||||||
import ApiError from 'component/common/ApiError/ApiError';
|
import ApiError from 'component/common/ApiError/ApiError';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { ReportToggleList } from 'component/Reporting/ReportToggleList/ReportToggleList';
|
|
||||||
import { ReportCard } from 'component/Reporting/ReportCard/ReportCard';
|
import { ReportCard } from 'component/Reporting/ReportCard/ReportCard';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
import { ReportTable } from 'component/Reporting/ReportTable/ReportTable';
|
||||||
|
|
||||||
interface IProjectHealthProps {
|
interface IProjectHealthProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -33,8 +33,8 @@ const ProjectHealth = ({ projectId }: IProjectHealthProps) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ReportCard healthReport={healthReport} />
|
<ReportCard healthReport={healthReport} />
|
||||||
<ReportToggleList
|
<ReportTable
|
||||||
selectedProject={projectId}
|
projectId={projectId}
|
||||||
features={healthReport.features}
|
features={healthReport.features}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -150,7 +150,7 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
<button
|
<button
|
||||||
aria-label=""
|
aria-label=""
|
||||||
aria-labelledby={null}
|
aria-labelledby={null}
|
||||||
className="tss-14l86a5-sortedButton tss-h39o0j-sortButton"
|
className="tss-14l86a5-sortedButton tss-1inbba3-sortButton"
|
||||||
data-mui-internal-clone-element={true}
|
data-mui-internal-clone-element={true}
|
||||||
onBlur={[Function]}
|
onBlur={[Function]}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -185,6 +185,7 @@ const compareNullableDates = (
|
|||||||
): number => {
|
): number => {
|
||||||
return a && b ? a?.getTime?.() - b?.getTime?.() : a ? 1 : b ? -1 : 0;
|
return a && b ? a?.getTime?.() - b?.getTime?.() : a ? 1 : b ? -1 : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortByExpired = (
|
const sortByExpired = (
|
||||||
features: Readonly<FeatureSchema[]>
|
features: Readonly<FeatureSchema[]>
|
||||||
): 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